diff --git a/demo/index.js b/demo/index.js index 21e39c59..216d112d 100644 --- a/demo/index.js +++ b/demo/index.js @@ -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) { @@ -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); @@ -38,20 +37,39 @@ 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; @@ -59,7 +77,14 @@ function updateRetina() { 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; @@ -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(); }; }); } @@ -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; +} diff --git a/dist/wind-gl.js b/dist/wind-gl.js index c1f4b79e..56ef6ccb 100644 --- a/dist/wind-gl.js +++ b/dist/wind-gl.js @@ -87,7 +87,7 @@ 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"; @@ -95,7 +95,7 @@ var quadVert = "precision mediump float;\n\nattribute vec2 a_pos;\n\nvarying vec 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', @@ -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); @@ -124,6 +125,7 @@ var WindGL = function WindGL(gl) { this.framebuffer = gl.createFramebuffer(); this.setColorRamp(defaultRampColors); + this.setView([0, 0, 1, 1]); this.resize(); }; @@ -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 () { @@ -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); }; @@ -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); @@ -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; }))); diff --git a/src/index.js b/src/index.js index fefbe1b9..91b886ea 100644 --- a/src/index.js +++ b/src/index.js @@ -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); @@ -37,6 +38,7 @@ export default class WindGL { this.framebuffer = gl.createFramebuffer(); this.setColorRamp(defaultRampColors); + this.setView([0, 0, 1, 1]); this.resize(); } @@ -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() { @@ -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); } @@ -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); @@ -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; +} diff --git a/src/shaders/draw.vert.glsl b/src/shaders/draw.vert.glsl index 14884c45..41e56f1e 100644 --- a/src/shaders/draw.vert.glsl +++ b/src/shaders/draw.vert.glsl @@ -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() { @@ -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); } diff --git a/src/shaders/update.frag.glsl b/src/shaders/update.frag.glsl index 82b92169..79d45313 100644 --- a/src/shaders/update.frag.glsl +++ b/src/shaders/update.frag.glsl @@ -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; @@ -38,14 +39,17 @@ 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 @@ -53,12 +57,11 @@ void main() { // 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(