Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mercator projection and zooming in #11

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
56 changes: 47 additions & 9 deletions demo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ canvas.height = canvas.clientHeight;
const gl = canvas.getContext('webgl', {antialiasing: false});

const wind = window.wind = new WindGL(gl);
wind.numParticles = 65536;

function frame() {
if (wind.windData) {
Expand All @@ -20,7 +19,7 @@ frame();

const gui = new dat.GUI();
gui.add(wind, 'numParticles', 1024, 589824);
gui.add(wind, 'fadeOpacity', 0.96, 0.999).step(0.001).updateDisplay();
gui.add(wind, 'fadeOpacity', 0.96, 0.999, 0.001).updateDisplay();
gui.add(wind, 'speedFactor', 0.05, 1.0);
gui.add(wind, 'dropRate', 0, 0.1);
gui.add(wind, 'dropRateBump', 0, 0.2);
Expand All @@ -38,28 +37,54 @@ const windFiles = {
};

const meta = {
'zoom': 0,
'2016-11-20+h': 0,
'retina resolution': true,
'github.com/mapbox/webgl-wind': function () {
window.location = 'https://github.com/mapbox/webgl-wind';
}
};

gui.add(meta, 'zoom', 0, 8, 0.01).onChange(updateZoom);
gui.add(meta, '2016-11-20+h', 0, 48, 6).onFinishChange(updateWind);

if (pxRatio !== 1) {
gui.add(meta, 'retina resolution').onFinishChange(updateRetina);
}

gui.add(meta, 'github.com/mapbox/webgl-wind');

updateWind(0);
updateRetina();

function updateZoom() {
const halfSize = 0.5 / Math.pow(2, meta.zoom);
wind.setView([
0.5 - halfSize,
0.5 - halfSize,
0.5 + halfSize,
0.5 + halfSize
]);
drawCoastline();
wind.resize();
wind.numParticles = wind.numParticles;
}

function updateRetina() {
const ratio = meta['retina resolution'] ? pxRatio : 1;
canvas.width = canvas.clientWidth * ratio;
canvas.height = canvas.clientHeight * ratio;
wind.resize();
}

let coastlineFeatures;

getJSON('https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_110m_coastline.geojson', function (data) {
coastlineFeatures = data.features;
drawCoastline();
});

function drawCoastline() {
const canvas = document.getElementById('coastline');
canvas.width = canvas.clientWidth * pxRatio;
canvas.height = canvas.clientHeight * pxRatio;
Expand All @@ -70,24 +95,30 @@ getJSON('https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_110m_coastl
ctx.strokeStyle = 'white';
ctx.beginPath();

for (let i = 0; i < data.features.length; i++) {
const line = data.features[i].geometry.coordinates;
for (let i = 0; i < coastlineFeatures.length; i++) {
const line = coastlineFeatures[i].geometry.coordinates;
for (let j = 0; j < line.length; j++) {
const x = (line[j][0] + 180) / 360;
const y = latY(line[j][1]);
const minX = wind.bbox[0];
const minY = latY(180 * wind.bbox[3] - 90);
const maxX = wind.bbox[2];
const maxY = latY(180 * wind.bbox[1] - 90);
ctx[j ? 'lineTo' : 'moveTo'](
(line[j][0] + 180) * canvas.width / 360,
(-line[j][1] + 90) * canvas.height / 180);
(x - minX) / (maxX - minX) * canvas.width,
(y - minY) / (maxY - minY) * canvas.height);
}
}
ctx.stroke();
});
}

function updateWind(name) {
getJSON('wind/' + windFiles[name] + '.json', function (windData) {
const windImage = new Image();
windData.image = windImage;
windImage.src = 'wind/' + windFiles[name] + '.png';
windImage.onload = function () {
wind.setWind(windData);
wind.setWind(windData, windImage);
wind.resize();
};
});
}
Expand All @@ -105,3 +136,10 @@ function getJSON(url, callback) {
};
xhr.send();
}

function latY(lat) {
const sin = Math.sin(lat * Math.PI / 180),
y = (0.5 - 0.25 * Math.log((1 + sin) / (1 - sin)) / Math.PI);
return y < 0 ? 0 :
y > 1 ? 1 : y;
}
41 changes: 36 additions & 5 deletions dist/wind-gl.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,15 @@ function bindFramebuffer(gl, framebuffer, texture) {
}
}

var drawVert = "precision mediump float;\n\nattribute float a_index;\n\nuniform sampler2D u_particles;\nuniform float u_particles_res;\n\nvarying vec2 v_particle_pos;\n\nvoid main() {\n vec4 color = texture2D(u_particles, vec2(\n fract(a_index / u_particles_res),\n floor(a_index / u_particles_res) / u_particles_res));\n\n // decode current particle position from the pixel's RGBA value\n v_particle_pos = vec2(\n color.r / 255.0 + color.b,\n color.g / 255.0 + color.a);\n\n gl_PointSize = 1.0;\n gl_Position = vec4(2.0 * v_particle_pos.x - 1.0, 1.0 - 2.0 * v_particle_pos.y, 0, 1);\n}\n";
var drawVert = "precision mediump float;\n\nattribute float a_index;\n\nuniform sampler2D u_particles;\nuniform float u_particles_res;\n\nuniform mat4 u_matrix;\nuniform vec4 u_bbox;\n\nvarying vec2 v_particle_pos;\n\nvoid main() {\n vec4 color = texture2D(u_particles, vec2(\n fract(a_index / u_particles_res),\n floor(a_index / u_particles_res) / u_particles_res));\n\n // decode current particle position from the pixel's RGBA value\n vec2 pos = vec2(\n color.r / 255.0 + color.b,\n color.g / 255.0 + color.a);\n\n // convert to global geographic position\n v_particle_pos = u_bbox.xy + pos * (u_bbox.zw - u_bbox.xy);\n\n // project the position with mercator projection\n float s = sin(radians(v_particle_pos.y * 180.0 - 90.0));\n float y = 1.0 - (degrees(log((1.0 + s) / (1.0 - s))) / 360.0 + 1.0) / 2.0;\n\n gl_PointSize = 1.0;\n gl_Position = u_matrix * vec4(v_particle_pos.x, y, 0, 1);\n}\n";

var drawFrag = "precision mediump float;\n\nuniform sampler2D u_wind;\nuniform vec2 u_wind_min;\nuniform vec2 u_wind_max;\nuniform sampler2D u_color_ramp;\n\nvarying vec2 v_particle_pos;\n\nvoid main() {\n vec2 velocity = mix(u_wind_min, u_wind_max, texture2D(u_wind, v_particle_pos).rg);\n float speed_t = length(velocity) / length(u_wind_max);\n\n // color ramp is encoded in a 16x16 texture\n vec2 ramp_pos = vec2(\n fract(16.0 * speed_t),\n floor(16.0 * speed_t) / 16.0);\n\n gl_FragColor = texture2D(u_color_ramp, ramp_pos);\n}\n";

var quadVert = "precision mediump float;\n\nattribute vec2 a_pos;\n\nvarying vec2 v_tex_pos;\n\nvoid main() {\n v_tex_pos = a_pos;\n gl_Position = vec4(1.0 - 2.0 * a_pos, 0, 1);\n}\n";

var screenFrag = "precision mediump float;\n\nuniform sampler2D u_screen;\nuniform float u_opacity;\n\nvarying vec2 v_tex_pos;\n\nvoid main() {\n vec4 color = texture2D(u_screen, 1.0 - v_tex_pos);\n // a hack to guarantee opacity fade out even with a value close to 1.0\n gl_FragColor = vec4(floor(255.0 * color * u_opacity) / 255.0);\n}\n";

var updateFrag = "precision highp float;\n\nuniform sampler2D u_particles;\nuniform sampler2D u_wind;\nuniform vec2 u_wind_res;\nuniform vec2 u_wind_min;\nuniform vec2 u_wind_max;\nuniform float u_rand_seed;\nuniform float u_speed_factor;\nuniform float u_drop_rate;\nuniform float u_drop_rate_bump;\n\nvarying vec2 v_tex_pos;\n\n// pseudo-random generator\nconst vec3 rand_constants = vec3(12.9898, 78.233, 4375.85453);\nfloat rand(const vec2 co) {\n float t = dot(rand_constants.xy, co);\n return fract(sin(t) * (rand_constants.z + t));\n}\n\n// wind speed lookup; use manual bilinear filtering based on 4 adjacent pixels for smooth interpolation\nvec2 lookup_wind(const vec2 uv) {\n // return texture2D(u_wind, uv).rg; // lower-res hardware filtering\n vec2 px = 1.0 / u_wind_res;\n vec2 vc = (floor(uv * u_wind_res)) * px;\n vec2 f = fract(uv * u_wind_res);\n vec2 tl = texture2D(u_wind, vc).rg;\n vec2 tr = texture2D(u_wind, vc + vec2(px.x, 0)).rg;\n vec2 bl = texture2D(u_wind, vc + vec2(0, px.y)).rg;\n vec2 br = texture2D(u_wind, vc + px).rg;\n return mix(mix(tl, tr, f.x), mix(bl, br, f.x), f.y);\n}\n\nvoid main() {\n vec4 color = texture2D(u_particles, v_tex_pos);\n vec2 pos = vec2(\n color.r / 255.0 + color.b,\n color.g / 255.0 + color.a); // decode particle position from pixel RGBA\n\n vec2 velocity = mix(u_wind_min, u_wind_max, lookup_wind(pos));\n float speed_t = length(velocity) / length(u_wind_max);\n\n // take EPSG:4236 distortion into account for calculating where the particle moved\n float distortion = cos(radians(pos.y * 180.0 - 90.0));\n vec2 offset = vec2(velocity.x / distortion, -velocity.y) * 0.0001 * u_speed_factor;\n\n // update particle position, wrapping around the date line\n pos = fract(1.0 + pos + offset);\n\n // a random seed to use for the particle drop\n vec2 seed = (pos + v_tex_pos) * u_rand_seed;\n\n // drop rate is a chance a particle will restart at random position, to avoid degeneration\n float drop_rate = u_drop_rate + speed_t * u_drop_rate_bump;\n float drop = step(1.0 - drop_rate, rand(seed));\n\n vec2 random_pos = vec2(\n rand(seed + 1.3),\n rand(seed + 2.1));\n pos = mix(pos, random_pos, drop);\n\n // encode the new particle position back into RGBA\n gl_FragColor = vec4(\n fract(pos * 255.0),\n floor(pos * 255.0) / 255.0);\n}\n";
var updateFrag = "precision highp float;\n\nuniform sampler2D u_particles;\nuniform sampler2D u_wind;\nuniform vec2 u_wind_res;\nuniform vec2 u_wind_min;\nuniform vec2 u_wind_max;\nuniform float u_rand_seed;\nuniform float u_speed_factor;\nuniform float u_drop_rate;\nuniform float u_drop_rate_bump;\nuniform vec4 u_bbox;\n\nvarying vec2 v_tex_pos;\n\n// pseudo-random generator\nconst vec3 rand_constants = vec3(12.9898, 78.233, 4375.85453);\nfloat rand(const vec2 co) {\n float t = dot(rand_constants.xy, co);\n return fract(sin(t) * (rand_constants.z + t));\n}\n\n// wind speed lookup; use manual bilinear filtering based on 4 adjacent pixels for smooth interpolation\nvec2 lookup_wind(const vec2 uv) {\n // return texture2D(u_wind, uv).rg; // lower-res hardware filtering\n vec2 px = 1.0 / u_wind_res;\n vec2 vc = (floor(uv * u_wind_res)) * px;\n vec2 f = fract(uv * u_wind_res);\n vec2 tl = texture2D(u_wind, vc).rg;\n vec2 tr = texture2D(u_wind, vc + vec2(px.x, 0)).rg;\n vec2 bl = texture2D(u_wind, vc + vec2(0, px.y)).rg;\n vec2 br = texture2D(u_wind, vc + px).rg;\n return mix(mix(tl, tr, f.x), mix(bl, br, f.x), f.y);\n}\n\nvoid main() {\n vec4 color = texture2D(u_particles, v_tex_pos);\n vec2 pos = vec2(\n color.r / 255.0 + color.b,\n color.g / 255.0 + color.a); // decode particle position from pixel RGBA\n\n // convert to global geographic position\n vec2 global_pos = u_bbox.xy + pos * (u_bbox.zw - u_bbox.xy);\n\n vec2 velocity = mix(u_wind_min, u_wind_max, lookup_wind(global_pos));\n float speed_t = length(velocity) / length(u_wind_max);\n\n // take EPSG:4236 distortion into account for calculating where the particle moved\n float distortion = cos(radians(global_pos.y * 180.0 - 90.0));\n vec2 offset = vec2(velocity.x / distortion, -velocity.y) * 0.0001 * u_speed_factor;\n\n // update particle position, wrapping around the boundaries\n pos = fract(1.0 + pos + offset);\n\n // a random seed to use for the particle drop\n vec2 seed = (pos + v_tex_pos) * u_rand_seed;\n\n // drop rate is a chance a particle will restart at random position, to avoid degeneration\n float drop_rate = u_drop_rate + speed_t * u_drop_rate_bump;\n\n float retain = step(drop_rate, rand(seed));\n\n vec2 random_pos = vec2(rand(seed + 1.3), 1.0 - rand(seed + 2.1));\n pos = mix(pos, random_pos, 1.0 - retain);\n\n // encode the new particle position back into RGBA\n gl_FragColor = vec4(\n fract(pos * 255.0),\n floor(pos * 255.0) / 255.0);\n}\n";

var defaultRampColors = {
0.0: '#3288bd',
Expand All @@ -115,6 +115,7 @@ var WindGL = function WindGL(gl) {
this.speedFactor = 0.25; // how fast the particles move
this.dropRate = 0.003; // how often the particles move to a random place
this.dropRateBump = 0.01; // drop rate increase relative to individual particle speed
this.numParticles = 65536;

this.drawProgram = createProgram(gl, drawVert, drawFrag);
this.screenProgram = createProgram(gl, quadVert, screenFrag);
Expand All @@ -124,6 +125,7 @@ var WindGL = function WindGL(gl) {
this.framebuffer = gl.createFramebuffer();

this.setColorRamp(defaultRampColors);
this.setView([0, 0, 1, 1]);
this.resize();
};

Expand Down Expand Up @@ -165,9 +167,28 @@ prototypeAccessors.numParticles.get = function () {
return this._numParticles;
};

WindGL.prototype.setWind = function setWind (windData) {
this.windData = windData;
this.windTexture = createTexture(this.gl, this.gl.LINEAR, windData.image);
WindGL.prototype.setWind = function setWind (data, image) {
this.windData = data;
this.windTexture = createTexture(this.gl, this.gl.LINEAR, image);
};

WindGL.prototype.setView = function setView (bbox, matrix) {
this.bbox = bbox;

if (matrix) {
this.matrix = matrix;

} else {
var minX = bbox[0];
var minY = mercY(bbox[3]);
var maxX = bbox[2];
var maxY = mercY(bbox[1]);

var kx = 2 / (maxX - minX);
var ky = 2 / (maxY - minY);

this.matrix = new Float32Array([kx, 0, 0, 0, 0, ky, 0, 0, 0, 0, 1, 0, -1 - minX * kx, -1 - minY * ky, 0, 1]);
}
};

WindGL.prototype.draw = function draw () {
Expand Down Expand Up @@ -232,6 +253,8 @@ WindGL.prototype.drawParticles = function drawParticles () {
gl.uniform1f(program.u_particles_res, this.particleStateResolution);
gl.uniform2f(program.u_wind_min, this.windData.uMin, this.windData.vMin);
gl.uniform2f(program.u_wind_max, this.windData.uMax, this.windData.vMax);
gl.uniformMatrix4fv(program.u_matrix, false, this.matrix);
gl.uniform4fv(program.u_bbox, this.bbox);

gl.drawArrays(gl.POINTS, 0, this._numParticles);
};
Expand All @@ -256,6 +279,7 @@ WindGL.prototype.updateParticles = function updateParticles () {
gl.uniform1f(program.u_speed_factor, this.speedFactor);
gl.uniform1f(program.u_drop_rate, this.dropRate);
gl.uniform1f(program.u_drop_rate_bump, this.dropRateBump);
gl.uniform4fv(program.u_bbox, this.bbox);

gl.drawArrays(gl.TRIANGLES, 0, 6);

Expand Down Expand Up @@ -285,6 +309,13 @@ function getColorRamp(colors) {
return new Uint8Array(ctx.getImageData(0, 0, 256, 1).data);
}

function mercY(y) {
var s = Math.sin(Math.PI * (y - 0.5));
var y2 = 1.0 - (Math.log((1.0 + s) / (1.0 - s)) / (2 * Math.PI) + 1.0) / 2.0;
return y2 < 0 ? 0 :
y2 > 1 ? 1 : y2;
}

return WindGL;

})));
37 changes: 34 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default class WindGL {
this.speedFactor = 0.25; // how fast the particles move
this.dropRate = 0.003; // how often the particles move to a random place
this.dropRateBump = 0.01; // drop rate increase relative to individual particle speed
this.numParticles = 65536;

this.drawProgram = util.createProgram(gl, drawVert, drawFrag);
this.screenProgram = util.createProgram(gl, quadVert, screenFrag);
Expand All @@ -37,6 +38,7 @@ export default class WindGL {
this.framebuffer = gl.createFramebuffer();

this.setColorRamp(defaultRampColors);
this.setView([0, 0, 1, 1]);
this.resize();
}

Expand Down Expand Up @@ -76,9 +78,28 @@ export default class WindGL {
return this._numParticles;
}

setWind(windData) {
this.windData = windData;
this.windTexture = util.createTexture(this.gl, this.gl.LINEAR, windData.image);
setWind(data, image) {
this.windData = data;
this.windTexture = util.createTexture(this.gl, this.gl.LINEAR, image);
}

setView(bbox, matrix) {
this.bbox = bbox;

if (matrix) {
this.matrix = matrix;

} else {
const minX = bbox[0];
const minY = mercY(bbox[3]);
const maxX = bbox[2];
const maxY = mercY(bbox[1]);

const kx = 2 / (maxX - minX);
const ky = 2 / (maxY - minY);

this.matrix = new Float32Array([kx, 0, 0, 0, 0, ky, 0, 0, 0, 0, 1, 0, -1 - minX * kx, -1 - minY * ky, 0, 1]);
}
}

draw() {
Expand Down Expand Up @@ -143,6 +164,8 @@ export default class WindGL {
gl.uniform1f(program.u_particles_res, this.particleStateResolution);
gl.uniform2f(program.u_wind_min, this.windData.uMin, this.windData.vMin);
gl.uniform2f(program.u_wind_max, this.windData.uMax, this.windData.vMax);
gl.uniformMatrix4fv(program.u_matrix, false, this.matrix);
gl.uniform4fv(program.u_bbox, this.bbox);

gl.drawArrays(gl.POINTS, 0, this._numParticles);
}
Expand All @@ -167,6 +190,7 @@ export default class WindGL {
gl.uniform1f(program.u_speed_factor, this.speedFactor);
gl.uniform1f(program.u_drop_rate, this.dropRate);
gl.uniform1f(program.u_drop_rate_bump, this.dropRateBump);
gl.uniform4fv(program.u_bbox, this.bbox);

gl.drawArrays(gl.TRIANGLES, 0, 6);

Expand Down Expand Up @@ -194,3 +218,10 @@ function getColorRamp(colors) {

return new Uint8Array(ctx.getImageData(0, 0, 256, 1).data);
}

function mercY(y) {
const s = Math.sin(Math.PI * (y - 0.5));
const y2 = 1.0 - (Math.log((1.0 + s) / (1.0 - s)) / (2 * Math.PI) + 1.0) / 2.0;
return y2 < 0 ? 0 :
y2 > 1 ? 1 : y2;
}
14 changes: 12 additions & 2 deletions src/shaders/draw.vert.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ attribute float a_index;
uniform sampler2D u_particles;
uniform float u_particles_res;

uniform mat4 u_matrix;
uniform vec4 u_bbox;

varying vec2 v_particle_pos;

void main() {
Expand All @@ -13,10 +16,17 @@ void main() {
floor(a_index / u_particles_res) / u_particles_res));

// decode current particle position from the pixel's RGBA value
v_particle_pos = vec2(
vec2 pos = vec2(
color.r / 255.0 + color.b,
color.g / 255.0 + color.a);

// convert to global geographic position
v_particle_pos = u_bbox.xy + pos * (u_bbox.zw - u_bbox.xy);

// project the position with mercator projection
float s = sin(radians(v_particle_pos.y * 180.0 - 90.0));
float y = 1.0 - (degrees(log((1.0 + s) / (1.0 - s))) / 360.0 + 1.0) / 2.0;

gl_PointSize = 1.0;
gl_Position = vec4(2.0 * v_particle_pos.x - 1.0, 1.0 - 2.0 * v_particle_pos.y, 0, 1);
gl_Position = u_matrix * vec4(v_particle_pos.x, y, 0, 1);
}
19 changes: 11 additions & 8 deletions src/shaders/update.frag.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ uniform float u_rand_seed;
uniform float u_speed_factor;
uniform float u_drop_rate;
uniform float u_drop_rate_bump;
uniform vec4 u_bbox;

varying vec2 v_tex_pos;

Expand Down Expand Up @@ -38,27 +39,29 @@ void main() {
color.r / 255.0 + color.b,
color.g / 255.0 + color.a); // decode particle position from pixel RGBA

vec2 velocity = mix(u_wind_min, u_wind_max, lookup_wind(pos));
// convert to global geographic position
vec2 global_pos = u_bbox.xy + pos * (u_bbox.zw - u_bbox.xy);

vec2 velocity = mix(u_wind_min, u_wind_max, lookup_wind(global_pos));
float speed_t = length(velocity) / length(u_wind_max);

// take EPSG:4236 distortion into account for calculating where the particle moved
float distortion = cos(radians(pos.y * 180.0 - 90.0));
float distortion = cos(radians(global_pos.y * 180.0 - 90.0));
vec2 offset = vec2(velocity.x / distortion, -velocity.y) * 0.0001 * u_speed_factor;

// update particle position, wrapping around the date line
// update particle position, wrapping around the boundaries
pos = fract(1.0 + pos + offset);

// a random seed to use for the particle drop
vec2 seed = (pos + v_tex_pos) * u_rand_seed;

// drop rate is a chance a particle will restart at random position, to avoid degeneration
float drop_rate = u_drop_rate + speed_t * u_drop_rate_bump;
float drop = step(1.0 - drop_rate, rand(seed));

vec2 random_pos = vec2(
rand(seed + 1.3),
rand(seed + 2.1));
pos = mix(pos, random_pos, drop);
float retain = step(drop_rate, rand(seed));

vec2 random_pos = vec2(rand(seed + 1.3), 1.0 - rand(seed + 2.1));
pos = mix(pos, random_pos, 1.0 - retain);

// encode the new particle position back into RGBA
gl_FragColor = vec4(
Expand Down