├── .gitignore ├── LICENSE ├── README.md ├── data ├── download.sh └── prepare.js ├── demo ├── index.html ├── index.js └── wind │ ├── 2016112000.json │ ├── 2016112000.png │ ├── 2016112006.json │ ├── 2016112006.png │ ├── 2016112012.json │ ├── 2016112012.png │ ├── 2016112018.json │ ├── 2016112018.png │ ├── 2016112100.json │ ├── 2016112100.png │ ├── 2016112106.json │ ├── 2016112106.png │ ├── 2016112112.json │ ├── 2016112112.png │ ├── 2016112118.json │ ├── 2016112118.png │ ├── 2016112200.json │ └── 2016112200.png ├── dist └── wind-gl.js ├── package.json ├── rollup.config.js └── src ├── index.js ├── shaders ├── draw.frag.glsl ├── draw.vert.glsl ├── quad.vert.glsl ├── screen.frag.glsl └── update.frag.glsl └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | yarn.lock 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2016, Mapbox 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose 6 | with or without fee is hereby granted, provided that the above copyright notice 7 | and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 11 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 13 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 14 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 15 | THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## WebGL Wind — [Demo](https://mapbox.github.io/webgl-wind/demo/) 2 | 3 | A WebGL-powered visualization of wind power. 4 | Capable of rendering up to 1 million wind particles at 60fps. 5 | 6 | This project is heavily inspired by the work of: 7 | 8 | - [Cameron Beccario](https://twitter.com/cambecc) 9 | and his wonderful [Earth project](https://earth.nullschool.net/) 10 | with its [open-source version](https://github.com/cambecc/earth). 11 | - [Fernanda Viégas and Martin Wattenberg](http://hint.fm/) and their 12 | [US Wind Map project](http://hint.fm/projects/wind/). 13 | - [Chris Wellons](http://nullprogram.com) and his WebGL tutorials, 14 | in particular [A GPU Approach to Particle Physics](http://nullprogram.com/blog/2014/06/29/). 15 | - [Greggman](http://games.greggman.com/game/) and his [WebGL Fundamentals](http://webglfundamentals.org/) guide. 16 | 17 | ### Running the demo locally 18 | 19 | ```bash 20 | npm install 21 | npm run build 22 | npm start 23 | # open http://127.0.0.1:1337/demo/ 24 | ``` 25 | 26 | ### Downloading weather data 27 | 28 | 1. Install [ecCodes](https://confluence.ecmwf.int//display/ECC/ecCodes+Home) (e.g. `brew install eccodes`). 29 | 2. Edit constants in `data/download.sh` for desired date, time and resolution. 30 | 3. Run `./data/download.sh ` to generate wind data files (`png` and `json`) for use with the library. 31 | -------------------------------------------------------------------------------- /data/download.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | GFS_DATE="20161120" 4 | GFS_TIME="00"; # 00, 06, 12, 18 5 | RES="1p00" # 0p25, 0p50 or 1p00 6 | BBOX="leftlon=0&rightlon=360&toplat=90&bottomlat=-90" 7 | LEVEL="lev_10_m_above_ground=on" 8 | GFS_URL="http://nomads.ncep.noaa.gov/cgi-bin/filter_gfs_${RES}.pl?file=gfs.t${GFS_TIME}z.pgrb2.${RES}.f000&${LEVEL}&${BBOX}&dir=%2Fgfs.${GFS_DATE}${GFS_TIME}" 9 | 10 | curl "${GFS_URL}&var_UGRD=on" -o utmp.grib 11 | curl "${GFS_URL}&var_VGRD=on" -o vtmp.grib 12 | 13 | grib_set -r -s packingType=grid_simple utmp.grib utmp.grib 14 | grib_set -r -s packingType=grid_simple vtmp.grib vtmp.grib 15 | 16 | printf "{\"u\":`grib_dump -j utmp.grib`,\"v\":`grib_dump -j vtmp.grib`}" > tmp.json 17 | 18 | rm utmp.grib vtmp.grib 19 | 20 | DIR=`dirname $0` 21 | node ${DIR}/prepare.js ${1}/${GFS_DATE}${GFS_TIME} 22 | 23 | rm tmp.json 24 | -------------------------------------------------------------------------------- /data/prepare.js: -------------------------------------------------------------------------------- 1 | const PNG = require('pngjs').PNG; 2 | const fs = require('fs'); 3 | 4 | const data = JSON.parse(fs.readFileSync('tmp.json')); 5 | const name = process.argv[2]; 6 | const u = data.u; 7 | const v = data.v; 8 | 9 | const width = u.Ni; 10 | const height = u.Nj - 1; 11 | 12 | const png = new PNG({ 13 | colorType: 2, 14 | filterType: 4, 15 | width: width, 16 | height: height 17 | }); 18 | 19 | for (let y = 0; y < height; y++) { 20 | for (let x = 0; x < width; x++) { 21 | const i = (y * width + x) * 4; 22 | const k = y * width + (x + width / 2) % width; 23 | png.data[i + 0] = Math.floor(255 * (u.values[k] - u.minimum) / (u.maximum - u.minimum)); 24 | png.data[i + 1] = Math.floor(255 * (v.values[k] - v.minimum) / (v.maximum - v.minimum)); 25 | png.data[i + 2] = 0; 26 | png.data[i + 3] = 255; 27 | } 28 | } 29 | 30 | png.pack().pipe(fs.createWriteStream(name + '.png')); 31 | 32 | fs.writeFileSync(name + '.json', JSON.stringify({ 33 | source: 'http://nomads.ncep.noaa.gov', 34 | date: formatDate(u.dataDate + '', u.dataTime), 35 | width: width, 36 | height: height, 37 | uMin: u.minimum, 38 | uMax: u.maximum, 39 | vMin: v.minimum, 40 | vMax: v.maximum 41 | }, null, 2) + '\n'); 42 | 43 | function formatDate(date, time) { 44 | return date.substr(0, 4) + '-' + date.substr(4, 2) + '-' + date.substr(6, 2) + 'T' + 45 | (time < 10 ? '0' + time : time) + ':00Z'; 46 | } 47 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebGL wind simulation 5 | 6 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | // using var to work around a WebKit bug 2 | var canvas = document.getElementById('canvas'); // eslint-disable-line 3 | 4 | const pxRatio = Math.max(Math.floor(window.devicePixelRatio) || 1, 2); 5 | canvas.width = canvas.clientWidth; 6 | canvas.height = canvas.clientHeight; 7 | 8 | const gl = canvas.getContext('webgl', {antialiasing: false}); 9 | 10 | const wind = window.wind = new WindGL(gl); 11 | wind.numParticles = 65536; 12 | 13 | function frame() { 14 | if (wind.windData) { 15 | wind.draw(); 16 | } 17 | requestAnimationFrame(frame); 18 | } 19 | frame(); 20 | 21 | const gui = new dat.GUI(); 22 | gui.add(wind, 'numParticles', 1024, 589824); 23 | gui.add(wind, 'fadeOpacity', 0.96, 0.999).step(0.001).updateDisplay(); 24 | gui.add(wind, 'speedFactor', 0.05, 1.0); 25 | gui.add(wind, 'dropRate', 0, 0.1); 26 | gui.add(wind, 'dropRateBump', 0, 0.2); 27 | 28 | const windFiles = { 29 | 0: '2016112000', 30 | 6: '2016112006', 31 | 12: '2016112012', 32 | 18: '2016112018', 33 | 24: '2016112100', 34 | 30: '2016112106', 35 | 36: '2016112112', 36 | 42: '2016112118', 37 | 48: '2016112200' 38 | }; 39 | 40 | const meta = { 41 | '2016-11-20+h': 0, 42 | 'retina resolution': true, 43 | 'github.com/mapbox/webgl-wind': function () { 44 | window.location = 'https://github.com/mapbox/webgl-wind'; 45 | } 46 | }; 47 | gui.add(meta, '2016-11-20+h', 0, 48, 6).onFinishChange(updateWind); 48 | if (pxRatio !== 1) { 49 | gui.add(meta, 'retina resolution').onFinishChange(updateRetina); 50 | } 51 | gui.add(meta, 'github.com/mapbox/webgl-wind'); 52 | updateWind(0); 53 | updateRetina(); 54 | 55 | function updateRetina() { 56 | const ratio = meta['retina resolution'] ? pxRatio : 1; 57 | canvas.width = canvas.clientWidth * ratio; 58 | canvas.height = canvas.clientHeight * ratio; 59 | wind.resize(); 60 | } 61 | 62 | getJSON('https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_110m_coastline.geojson', function (data) { 63 | const canvas = document.getElementById('coastline'); 64 | canvas.width = canvas.clientWidth * pxRatio; 65 | canvas.height = canvas.clientHeight * pxRatio; 66 | 67 | const ctx = canvas.getContext('2d'); 68 | ctx.lineWidth = pxRatio; 69 | ctx.lineJoin = ctx.lineCap = 'round'; 70 | ctx.strokeStyle = 'white'; 71 | ctx.beginPath(); 72 | 73 | for (let i = 0; i < data.features.length; i++) { 74 | const line = data.features[i].geometry.coordinates; 75 | for (let j = 0; j < line.length; j++) { 76 | ctx[j ? 'lineTo' : 'moveTo']( 77 | (line[j][0] + 180) * canvas.width / 360, 78 | (-line[j][1] + 90) * canvas.height / 180); 79 | } 80 | } 81 | ctx.stroke(); 82 | }); 83 | 84 | function updateWind(name) { 85 | getJSON('wind/' + windFiles[name] + '.json', function (windData) { 86 | const windImage = new Image(); 87 | windData.image = windImage; 88 | windImage.src = 'wind/' + windFiles[name] + '.png'; 89 | windImage.onload = function () { 90 | wind.setWind(windData); 91 | }; 92 | }); 93 | } 94 | 95 | function getJSON(url, callback) { 96 | const xhr = new XMLHttpRequest(); 97 | xhr.responseType = 'json'; 98 | xhr.open('get', url, true); 99 | xhr.onload = function () { 100 | if (xhr.status >= 200 && xhr.status < 300) { 101 | callback(xhr.response); 102 | } else { 103 | throw new Error(xhr.statusText); 104 | } 105 | }; 106 | xhr.send(); 107 | } 108 | -------------------------------------------------------------------------------- /demo/wind/2016112000.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "http://nomads.ncep.noaa.gov", 3 | "date": "2016-11-20T00:00Z", 4 | "width": 360, 5 | "height": 180, 6 | "uMin": -21.32, 7 | "uMax": 26.8, 8 | "vMin": -21.57, 9 | "vMax": 21.42 10 | } 11 | -------------------------------------------------------------------------------- /demo/wind/2016112000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/webgl-wind/9e60d65477b55a3e6d5493d3d866fd43ed1656c0/demo/wind/2016112000.png -------------------------------------------------------------------------------- /demo/wind/2016112006.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "http://nomads.ncep.noaa.gov", 3 | "date": "2016-11-20T600:00Z", 4 | "width": 360, 5 | "height": 180, 6 | "uMin": -19.38, 7 | "uMax": 25.57, 8 | "vMin": -21.19, 9 | "vMax": 22.77 10 | } 11 | -------------------------------------------------------------------------------- /demo/wind/2016112006.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/webgl-wind/9e60d65477b55a3e6d5493d3d866fd43ed1656c0/demo/wind/2016112006.png -------------------------------------------------------------------------------- /demo/wind/2016112012.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "http://nomads.ncep.noaa.gov", 3 | "date": "2016-11-20T1200:00Z", 4 | "width": 360, 5 | "height": 180, 6 | "uMin": -18.22, 7 | "uMax": 23.94, 8 | "vMin": -20.24, 9 | "vMax": 21 10 | } 11 | -------------------------------------------------------------------------------- /demo/wind/2016112012.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/webgl-wind/9e60d65477b55a3e6d5493d3d866fd43ed1656c0/demo/wind/2016112012.png -------------------------------------------------------------------------------- /demo/wind/2016112018.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "http://nomads.ncep.noaa.gov", 3 | "date": "2016-11-20T1800:00Z", 4 | "width": 360, 5 | "height": 180, 6 | "uMin": -20.26, 7 | "uMax": 23.24, 8 | "vMin": -20.41, 9 | "vMax": 19.66 10 | } 11 | -------------------------------------------------------------------------------- /demo/wind/2016112018.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/webgl-wind/9e60d65477b55a3e6d5493d3d866fd43ed1656c0/demo/wind/2016112018.png -------------------------------------------------------------------------------- /demo/wind/2016112100.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "http://nomads.ncep.noaa.gov", 3 | "date": "2016-11-21T00:00Z", 4 | "width": 360, 5 | "height": 180, 6 | "uMin": -19.16, 7 | "uMax": 26.04, 8 | "vMin": -22.08, 9 | "vMax": 19.17 10 | } 11 | -------------------------------------------------------------------------------- /demo/wind/2016112100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/webgl-wind/9e60d65477b55a3e6d5493d3d866fd43ed1656c0/demo/wind/2016112100.png -------------------------------------------------------------------------------- /demo/wind/2016112106.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "http://nomads.ncep.noaa.gov", 3 | "date": "2016-11-21T600:00Z", 4 | "width": 360, 5 | "height": 180, 6 | "uMin": -19.73, 7 | "uMax": 24.57, 8 | "vMin": -21.79, 9 | "vMax": 19.6 10 | } 11 | -------------------------------------------------------------------------------- /demo/wind/2016112106.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/webgl-wind/9e60d65477b55a3e6d5493d3d866fd43ed1656c0/demo/wind/2016112106.png -------------------------------------------------------------------------------- /demo/wind/2016112112.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "http://nomads.ncep.noaa.gov", 3 | "date": "2016-11-21T1200:00Z", 4 | "width": 360, 5 | "height": 180, 6 | "uMin": -21.69, 7 | "uMax": 25.09, 8 | "vMin": -20.24, 9 | "vMax": 19.17 10 | } 11 | -------------------------------------------------------------------------------- /demo/wind/2016112112.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/webgl-wind/9e60d65477b55a3e6d5493d3d866fd43ed1656c0/demo/wind/2016112112.png -------------------------------------------------------------------------------- /demo/wind/2016112118.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "http://nomads.ncep.noaa.gov", 3 | "date": "2016-11-21T1800:00Z", 4 | "width": 360, 5 | "height": 180, 6 | "uMin": -24.02, 7 | "uMax": 26.31, 8 | "vMin": -20.91, 9 | "vMax": 21.22 10 | } 11 | -------------------------------------------------------------------------------- /demo/wind/2016112118.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/webgl-wind/9e60d65477b55a3e6d5493d3d866fd43ed1656c0/demo/wind/2016112118.png -------------------------------------------------------------------------------- /demo/wind/2016112200.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "http://nomads.ncep.noaa.gov", 3 | "date": "2016-11-22T00:00Z", 4 | "width": 360, 5 | "height": 180, 6 | "uMin": -21.38, 7 | "uMax": 24.52, 8 | "vMin": -21.05, 9 | "vMax": 19.43 10 | } 11 | -------------------------------------------------------------------------------- /demo/wind/2016112200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/webgl-wind/9e60d65477b55a3e6d5493d3d866fd43ed1656c0/demo/wind/2016112200.png -------------------------------------------------------------------------------- /dist/wind-gl.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global.WindGL = factory()); 5 | }(this, (function () { 'use strict'; 6 | 7 | function createShader(gl, type, source) { 8 | var shader = gl.createShader(type); 9 | gl.shaderSource(shader, source); 10 | 11 | gl.compileShader(shader); 12 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 13 | throw new Error(gl.getShaderInfoLog(shader)); 14 | } 15 | 16 | return shader; 17 | } 18 | 19 | function createProgram(gl, vertexSource, fragmentSource) { 20 | var program = gl.createProgram(); 21 | 22 | var vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource); 23 | var fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource); 24 | 25 | gl.attachShader(program, vertexShader); 26 | gl.attachShader(program, fragmentShader); 27 | 28 | gl.linkProgram(program); 29 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 30 | throw new Error(gl.getProgramInfoLog(program)); 31 | } 32 | 33 | var wrapper = {program: program}; 34 | 35 | var numAttributes = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES); 36 | for (var i = 0; i < numAttributes; i++) { 37 | var attribute = gl.getActiveAttrib(program, i); 38 | wrapper[attribute.name] = gl.getAttribLocation(program, attribute.name); 39 | } 40 | var numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); 41 | for (var i$1 = 0; i$1 < numUniforms; i$1++) { 42 | var uniform = gl.getActiveUniform(program, i$1); 43 | wrapper[uniform.name] = gl.getUniformLocation(program, uniform.name); 44 | } 45 | 46 | return wrapper; 47 | } 48 | 49 | function createTexture(gl, filter, data, width, height) { 50 | var texture = gl.createTexture(); 51 | gl.bindTexture(gl.TEXTURE_2D, texture); 52 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 53 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 54 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter); 55 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter); 56 | if (data instanceof Uint8Array) { 57 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data); 58 | } else { 59 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data); 60 | } 61 | gl.bindTexture(gl.TEXTURE_2D, null); 62 | return texture; 63 | } 64 | 65 | function bindTexture(gl, texture, unit) { 66 | gl.activeTexture(gl.TEXTURE0 + unit); 67 | gl.bindTexture(gl.TEXTURE_2D, texture); 68 | } 69 | 70 | function createBuffer(gl, data) { 71 | var buffer = gl.createBuffer(); 72 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 73 | gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); 74 | return buffer; 75 | } 76 | 77 | function bindAttribute(gl, buffer, attribute, numComponents) { 78 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 79 | gl.enableVertexAttribArray(attribute); 80 | gl.vertexAttribPointer(attribute, numComponents, gl.FLOAT, false, 0, 0); 81 | } 82 | 83 | function bindFramebuffer(gl, framebuffer, texture) { 84 | gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); 85 | if (texture) { 86 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); 87 | } 88 | } 89 | 90 | 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"; 91 | 92 | 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"; 93 | 94 | 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"; 95 | 96 | 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"; 97 | 98 | 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"; 99 | 100 | var defaultRampColors = { 101 | 0.0: '#3288bd', 102 | 0.1: '#66c2a5', 103 | 0.2: '#abdda4', 104 | 0.3: '#e6f598', 105 | 0.4: '#fee08b', 106 | 0.5: '#fdae61', 107 | 0.6: '#f46d43', 108 | 1.0: '#d53e4f' 109 | }; 110 | 111 | var WindGL = function WindGL(gl) { 112 | this.gl = gl; 113 | 114 | this.fadeOpacity = 0.996; // how fast the particle trails fade on each frame 115 | this.speedFactor = 0.25; // how fast the particles move 116 | this.dropRate = 0.003; // how often the particles move to a random place 117 | this.dropRateBump = 0.01; // drop rate increase relative to individual particle speed 118 | 119 | this.drawProgram = createProgram(gl, drawVert, drawFrag); 120 | this.screenProgram = createProgram(gl, quadVert, screenFrag); 121 | this.updateProgram = createProgram(gl, quadVert, updateFrag); 122 | 123 | this.quadBuffer = createBuffer(gl, new Float32Array([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1])); 124 | this.framebuffer = gl.createFramebuffer(); 125 | 126 | this.setColorRamp(defaultRampColors); 127 | this.resize(); 128 | }; 129 | 130 | var prototypeAccessors = { numParticles: {} }; 131 | 132 | WindGL.prototype.resize = function resize () { 133 | var gl = this.gl; 134 | var emptyPixels = new Uint8Array(gl.canvas.width * gl.canvas.height * 4); 135 | // screen textures to hold the drawn screen for the previous and the current frame 136 | this.backgroundTexture = createTexture(gl, gl.NEAREST, emptyPixels, gl.canvas.width, gl.canvas.height); 137 | this.screenTexture = createTexture(gl, gl.NEAREST, emptyPixels, gl.canvas.width, gl.canvas.height); 138 | }; 139 | 140 | WindGL.prototype.setColorRamp = function setColorRamp (colors) { 141 | // lookup texture for colorizing the particles according to their speed 142 | this.colorRampTexture = createTexture(this.gl, this.gl.LINEAR, getColorRamp(colors), 16, 16); 143 | }; 144 | 145 | prototypeAccessors.numParticles.set = function (numParticles) { 146 | var gl = this.gl; 147 | 148 | // we create a square texture where each pixel will hold a particle position encoded as RGBA 149 | var particleRes = this.particleStateResolution = Math.ceil(Math.sqrt(numParticles)); 150 | this._numParticles = particleRes * particleRes; 151 | 152 | var particleState = new Uint8Array(this._numParticles * 4); 153 | for (var i = 0; i < particleState.length; i++) { 154 | particleState[i] = Math.floor(Math.random() * 256); // randomize the initial particle positions 155 | } 156 | // textures to hold the particle state for the current and the next frame 157 | this.particleStateTexture0 = createTexture(gl, gl.NEAREST, particleState, particleRes, particleRes); 158 | this.particleStateTexture1 = createTexture(gl, gl.NEAREST, particleState, particleRes, particleRes); 159 | 160 | var particleIndices = new Float32Array(this._numParticles); 161 | for (var i$1 = 0; i$1 < this._numParticles; i$1++) { particleIndices[i$1] = i$1; } 162 | this.particleIndexBuffer = createBuffer(gl, particleIndices); 163 | }; 164 | prototypeAccessors.numParticles.get = function () { 165 | return this._numParticles; 166 | }; 167 | 168 | WindGL.prototype.setWind = function setWind (windData) { 169 | this.windData = windData; 170 | this.windTexture = createTexture(this.gl, this.gl.LINEAR, windData.image); 171 | }; 172 | 173 | WindGL.prototype.draw = function draw () { 174 | var gl = this.gl; 175 | gl.disable(gl.DEPTH_TEST); 176 | gl.disable(gl.STENCIL_TEST); 177 | 178 | bindTexture(gl, this.windTexture, 0); 179 | bindTexture(gl, this.particleStateTexture0, 1); 180 | 181 | this.drawScreen(); 182 | this.updateParticles(); 183 | }; 184 | 185 | WindGL.prototype.drawScreen = function drawScreen () { 186 | var gl = this.gl; 187 | // draw the screen into a temporary framebuffer to retain it as the background on the next frame 188 | bindFramebuffer(gl, this.framebuffer, this.screenTexture); 189 | gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); 190 | 191 | this.drawTexture(this.backgroundTexture, this.fadeOpacity); 192 | this.drawParticles(); 193 | 194 | bindFramebuffer(gl, null); 195 | // enable blending to support drawing on top of an existing background (e.g. a map) 196 | gl.enable(gl.BLEND); 197 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 198 | this.drawTexture(this.screenTexture, 1.0); 199 | gl.disable(gl.BLEND); 200 | 201 | // save the current screen as the background for the next frame 202 | var temp = this.backgroundTexture; 203 | this.backgroundTexture = this.screenTexture; 204 | this.screenTexture = temp; 205 | }; 206 | 207 | WindGL.prototype.drawTexture = function drawTexture (texture, opacity) { 208 | var gl = this.gl; 209 | var program = this.screenProgram; 210 | gl.useProgram(program.program); 211 | 212 | bindAttribute(gl, this.quadBuffer, program.a_pos, 2); 213 | bindTexture(gl, texture, 2); 214 | gl.uniform1i(program.u_screen, 2); 215 | gl.uniform1f(program.u_opacity, opacity); 216 | 217 | gl.drawArrays(gl.TRIANGLES, 0, 6); 218 | }; 219 | 220 | WindGL.prototype.drawParticles = function drawParticles () { 221 | var gl = this.gl; 222 | var program = this.drawProgram; 223 | gl.useProgram(program.program); 224 | 225 | bindAttribute(gl, this.particleIndexBuffer, program.a_index, 1); 226 | bindTexture(gl, this.colorRampTexture, 2); 227 | 228 | gl.uniform1i(program.u_wind, 0); 229 | gl.uniform1i(program.u_particles, 1); 230 | gl.uniform1i(program.u_color_ramp, 2); 231 | 232 | gl.uniform1f(program.u_particles_res, this.particleStateResolution); 233 | gl.uniform2f(program.u_wind_min, this.windData.uMin, this.windData.vMin); 234 | gl.uniform2f(program.u_wind_max, this.windData.uMax, this.windData.vMax); 235 | 236 | gl.drawArrays(gl.POINTS, 0, this._numParticles); 237 | }; 238 | 239 | WindGL.prototype.updateParticles = function updateParticles () { 240 | var gl = this.gl; 241 | bindFramebuffer(gl, this.framebuffer, this.particleStateTexture1); 242 | gl.viewport(0, 0, this.particleStateResolution, this.particleStateResolution); 243 | 244 | var program = this.updateProgram; 245 | gl.useProgram(program.program); 246 | 247 | bindAttribute(gl, this.quadBuffer, program.a_pos, 2); 248 | 249 | gl.uniform1i(program.u_wind, 0); 250 | gl.uniform1i(program.u_particles, 1); 251 | 252 | gl.uniform1f(program.u_rand_seed, Math.random()); 253 | gl.uniform2f(program.u_wind_res, this.windData.width, this.windData.height); 254 | gl.uniform2f(program.u_wind_min, this.windData.uMin, this.windData.vMin); 255 | gl.uniform2f(program.u_wind_max, this.windData.uMax, this.windData.vMax); 256 | gl.uniform1f(program.u_speed_factor, this.speedFactor); 257 | gl.uniform1f(program.u_drop_rate, this.dropRate); 258 | gl.uniform1f(program.u_drop_rate_bump, this.dropRateBump); 259 | 260 | gl.drawArrays(gl.TRIANGLES, 0, 6); 261 | 262 | // swap the particle state textures so the new one becomes the current one 263 | var temp = this.particleStateTexture0; 264 | this.particleStateTexture0 = this.particleStateTexture1; 265 | this.particleStateTexture1 = temp; 266 | }; 267 | 268 | Object.defineProperties( WindGL.prototype, prototypeAccessors ); 269 | 270 | function getColorRamp(colors) { 271 | var canvas = document.createElement('canvas'); 272 | var ctx = canvas.getContext('2d'); 273 | 274 | canvas.width = 256; 275 | canvas.height = 1; 276 | 277 | var gradient = ctx.createLinearGradient(0, 0, 256, 0); 278 | for (var stop in colors) { 279 | gradient.addColorStop(+stop, colors[stop]); 280 | } 281 | 282 | ctx.fillStyle = gradient; 283 | ctx.fillRect(0, 0, 256, 1); 284 | 285 | return new Uint8Array(ctx.getImageData(0, 0, 256, 1).data); 286 | } 287 | 288 | return WindGL; 289 | 290 | }))); 291 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webgl-wind", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "rollup -c", 8 | "watch": "rollup -c -w", 9 | "test": "eslint src/*.js demo/index.js data/prepare.js", 10 | "serve": "st -nc -i index.html", 11 | "start": "run-p serve watch" 12 | }, 13 | "keywords": [], 14 | "author": "Vladimir Agafonkin", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "eslint": "^3.15.0", 18 | "eslint-config-mourner": "^2.0.1", 19 | "npm-run-all": "^4.0.1", 20 | "pngjs": "^3.0.0", 21 | "rollup": "^0.41.4", 22 | "rollup-plugin-buble": "^0.15.0", 23 | "rollup-plugin-string": "^2.0.2", 24 | "rollup-watch": "^3.2.2", 25 | "st": "^1.2.0" 26 | }, 27 | "eslintConfig": { 28 | "extends": "mourner", 29 | "parserOptions": { 30 | "sourceType": "module" 31 | }, 32 | "globals": { 33 | "dat": false, 34 | "WindGL": false 35 | }, 36 | "rules": { 37 | "no-var": 2, 38 | "prefer-const": 2 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import string from 'rollup-plugin-string'; 2 | import buble from 'rollup-plugin-buble'; 3 | 4 | export default { 5 | entry: 'src/index.js', 6 | dest: 'dist/wind-gl.js', 7 | format: 'umd', 8 | moduleName: 'WindGL', 9 | plugins: [ 10 | string({include: './src/shaders/*.glsl'}), 11 | buble() 12 | ] 13 | }; 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | import * as util from './util'; 3 | 4 | import drawVert from './shaders/draw.vert.glsl'; 5 | import drawFrag from './shaders/draw.frag.glsl'; 6 | 7 | import quadVert from './shaders/quad.vert.glsl'; 8 | 9 | import screenFrag from './shaders/screen.frag.glsl'; 10 | import updateFrag from './shaders/update.frag.glsl'; 11 | 12 | const defaultRampColors = { 13 | 0.0: '#3288bd', 14 | 0.1: '#66c2a5', 15 | 0.2: '#abdda4', 16 | 0.3: '#e6f598', 17 | 0.4: '#fee08b', 18 | 0.5: '#fdae61', 19 | 0.6: '#f46d43', 20 | 1.0: '#d53e4f' 21 | }; 22 | 23 | export default class WindGL { 24 | constructor(gl) { 25 | this.gl = gl; 26 | 27 | this.fadeOpacity = 0.996; // how fast the particle trails fade on each frame 28 | this.speedFactor = 0.25; // how fast the particles move 29 | this.dropRate = 0.003; // how often the particles move to a random place 30 | this.dropRateBump = 0.01; // drop rate increase relative to individual particle speed 31 | 32 | this.drawProgram = util.createProgram(gl, drawVert, drawFrag); 33 | this.screenProgram = util.createProgram(gl, quadVert, screenFrag); 34 | this.updateProgram = util.createProgram(gl, quadVert, updateFrag); 35 | 36 | this.quadBuffer = util.createBuffer(gl, new Float32Array([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1])); 37 | this.framebuffer = gl.createFramebuffer(); 38 | 39 | this.setColorRamp(defaultRampColors); 40 | this.resize(); 41 | } 42 | 43 | resize() { 44 | const gl = this.gl; 45 | const emptyPixels = new Uint8Array(gl.canvas.width * gl.canvas.height * 4); 46 | // screen textures to hold the drawn screen for the previous and the current frame 47 | this.backgroundTexture = util.createTexture(gl, gl.NEAREST, emptyPixels, gl.canvas.width, gl.canvas.height); 48 | this.screenTexture = util.createTexture(gl, gl.NEAREST, emptyPixels, gl.canvas.width, gl.canvas.height); 49 | } 50 | 51 | setColorRamp(colors) { 52 | // lookup texture for colorizing the particles according to their speed 53 | this.colorRampTexture = util.createTexture(this.gl, this.gl.LINEAR, getColorRamp(colors), 16, 16); 54 | } 55 | 56 | set numParticles(numParticles) { 57 | const gl = this.gl; 58 | 59 | // we create a square texture where each pixel will hold a particle position encoded as RGBA 60 | const particleRes = this.particleStateResolution = Math.ceil(Math.sqrt(numParticles)); 61 | this._numParticles = particleRes * particleRes; 62 | 63 | const particleState = new Uint8Array(this._numParticles * 4); 64 | for (let i = 0; i < particleState.length; i++) { 65 | particleState[i] = Math.floor(Math.random() * 256); // randomize the initial particle positions 66 | } 67 | // textures to hold the particle state for the current and the next frame 68 | this.particleStateTexture0 = util.createTexture(gl, gl.NEAREST, particleState, particleRes, particleRes); 69 | this.particleStateTexture1 = util.createTexture(gl, gl.NEAREST, particleState, particleRes, particleRes); 70 | 71 | const particleIndices = new Float32Array(this._numParticles); 72 | for (let i = 0; i < this._numParticles; i++) particleIndices[i] = i; 73 | this.particleIndexBuffer = util.createBuffer(gl, particleIndices); 74 | } 75 | get numParticles() { 76 | return this._numParticles; 77 | } 78 | 79 | setWind(windData) { 80 | this.windData = windData; 81 | this.windTexture = util.createTexture(this.gl, this.gl.LINEAR, windData.image); 82 | } 83 | 84 | draw() { 85 | const gl = this.gl; 86 | gl.disable(gl.DEPTH_TEST); 87 | gl.disable(gl.STENCIL_TEST); 88 | 89 | util.bindTexture(gl, this.windTexture, 0); 90 | util.bindTexture(gl, this.particleStateTexture0, 1); 91 | 92 | this.drawScreen(); 93 | this.updateParticles(); 94 | } 95 | 96 | drawScreen() { 97 | const gl = this.gl; 98 | // draw the screen into a temporary framebuffer to retain it as the background on the next frame 99 | util.bindFramebuffer(gl, this.framebuffer, this.screenTexture); 100 | gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); 101 | 102 | this.drawTexture(this.backgroundTexture, this.fadeOpacity); 103 | this.drawParticles(); 104 | 105 | util.bindFramebuffer(gl, null); 106 | // enable blending to support drawing on top of an existing background (e.g. a map) 107 | gl.enable(gl.BLEND); 108 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 109 | this.drawTexture(this.screenTexture, 1.0); 110 | gl.disable(gl.BLEND); 111 | 112 | // save the current screen as the background for the next frame 113 | const temp = this.backgroundTexture; 114 | this.backgroundTexture = this.screenTexture; 115 | this.screenTexture = temp; 116 | } 117 | 118 | drawTexture(texture, opacity) { 119 | const gl = this.gl; 120 | const program = this.screenProgram; 121 | gl.useProgram(program.program); 122 | 123 | util.bindAttribute(gl, this.quadBuffer, program.a_pos, 2); 124 | util.bindTexture(gl, texture, 2); 125 | gl.uniform1i(program.u_screen, 2); 126 | gl.uniform1f(program.u_opacity, opacity); 127 | 128 | gl.drawArrays(gl.TRIANGLES, 0, 6); 129 | } 130 | 131 | drawParticles() { 132 | const gl = this.gl; 133 | const program = this.drawProgram; 134 | gl.useProgram(program.program); 135 | 136 | util.bindAttribute(gl, this.particleIndexBuffer, program.a_index, 1); 137 | util.bindTexture(gl, this.colorRampTexture, 2); 138 | 139 | gl.uniform1i(program.u_wind, 0); 140 | gl.uniform1i(program.u_particles, 1); 141 | gl.uniform1i(program.u_color_ramp, 2); 142 | 143 | gl.uniform1f(program.u_particles_res, this.particleStateResolution); 144 | gl.uniform2f(program.u_wind_min, this.windData.uMin, this.windData.vMin); 145 | gl.uniform2f(program.u_wind_max, this.windData.uMax, this.windData.vMax); 146 | 147 | gl.drawArrays(gl.POINTS, 0, this._numParticles); 148 | } 149 | 150 | updateParticles() { 151 | const gl = this.gl; 152 | util.bindFramebuffer(gl, this.framebuffer, this.particleStateTexture1); 153 | gl.viewport(0, 0, this.particleStateResolution, this.particleStateResolution); 154 | 155 | const program = this.updateProgram; 156 | gl.useProgram(program.program); 157 | 158 | util.bindAttribute(gl, this.quadBuffer, program.a_pos, 2); 159 | 160 | gl.uniform1i(program.u_wind, 0); 161 | gl.uniform1i(program.u_particles, 1); 162 | 163 | gl.uniform1f(program.u_rand_seed, Math.random()); 164 | gl.uniform2f(program.u_wind_res, this.windData.width, this.windData.height); 165 | gl.uniform2f(program.u_wind_min, this.windData.uMin, this.windData.vMin); 166 | gl.uniform2f(program.u_wind_max, this.windData.uMax, this.windData.vMax); 167 | gl.uniform1f(program.u_speed_factor, this.speedFactor); 168 | gl.uniform1f(program.u_drop_rate, this.dropRate); 169 | gl.uniform1f(program.u_drop_rate_bump, this.dropRateBump); 170 | 171 | gl.drawArrays(gl.TRIANGLES, 0, 6); 172 | 173 | // swap the particle state textures so the new one becomes the current one 174 | const temp = this.particleStateTexture0; 175 | this.particleStateTexture0 = this.particleStateTexture1; 176 | this.particleStateTexture1 = temp; 177 | } 178 | } 179 | 180 | function getColorRamp(colors) { 181 | const canvas = document.createElement('canvas'); 182 | const ctx = canvas.getContext('2d'); 183 | 184 | canvas.width = 256; 185 | canvas.height = 1; 186 | 187 | const gradient = ctx.createLinearGradient(0, 0, 256, 0); 188 | for (const stop in colors) { 189 | gradient.addColorStop(+stop, colors[stop]); 190 | } 191 | 192 | ctx.fillStyle = gradient; 193 | ctx.fillRect(0, 0, 256, 1); 194 | 195 | return new Uint8Array(ctx.getImageData(0, 0, 256, 1).data); 196 | } 197 | -------------------------------------------------------------------------------- /src/shaders/draw.frag.glsl: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform sampler2D u_wind; 4 | uniform vec2 u_wind_min; 5 | uniform vec2 u_wind_max; 6 | uniform sampler2D u_color_ramp; 7 | 8 | varying vec2 v_particle_pos; 9 | 10 | void main() { 11 | vec2 velocity = mix(u_wind_min, u_wind_max, texture2D(u_wind, v_particle_pos).rg); 12 | float speed_t = length(velocity) / length(u_wind_max); 13 | 14 | // color ramp is encoded in a 16x16 texture 15 | vec2 ramp_pos = vec2( 16 | fract(16.0 * speed_t), 17 | floor(16.0 * speed_t) / 16.0); 18 | 19 | gl_FragColor = texture2D(u_color_ramp, ramp_pos); 20 | } 21 | -------------------------------------------------------------------------------- /src/shaders/draw.vert.glsl: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | attribute float a_index; 4 | 5 | uniform sampler2D u_particles; 6 | uniform float u_particles_res; 7 | 8 | varying vec2 v_particle_pos; 9 | 10 | void main() { 11 | vec4 color = texture2D(u_particles, vec2( 12 | fract(a_index / u_particles_res), 13 | floor(a_index / u_particles_res) / u_particles_res)); 14 | 15 | // decode current particle position from the pixel's RGBA value 16 | v_particle_pos = vec2( 17 | color.r / 255.0 + color.b, 18 | color.g / 255.0 + color.a); 19 | 20 | gl_PointSize = 1.0; 21 | gl_Position = vec4(2.0 * v_particle_pos.x - 1.0, 1.0 - 2.0 * v_particle_pos.y, 0, 1); 22 | } 23 | -------------------------------------------------------------------------------- /src/shaders/quad.vert.glsl: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | attribute vec2 a_pos; 4 | 5 | varying vec2 v_tex_pos; 6 | 7 | void main() { 8 | v_tex_pos = a_pos; 9 | gl_Position = vec4(1.0 - 2.0 * a_pos, 0, 1); 10 | } 11 | -------------------------------------------------------------------------------- /src/shaders/screen.frag.glsl: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform sampler2D u_screen; 4 | uniform float u_opacity; 5 | 6 | varying vec2 v_tex_pos; 7 | 8 | void main() { 9 | vec4 color = texture2D(u_screen, 1.0 - v_tex_pos); 10 | // a hack to guarantee opacity fade out even with a value close to 1.0 11 | gl_FragColor = vec4(floor(255.0 * color * u_opacity) / 255.0); 12 | } 13 | -------------------------------------------------------------------------------- /src/shaders/update.frag.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | uniform sampler2D u_particles; 4 | uniform sampler2D u_wind; 5 | uniform vec2 u_wind_res; 6 | uniform vec2 u_wind_min; 7 | uniform vec2 u_wind_max; 8 | uniform float u_rand_seed; 9 | uniform float u_speed_factor; 10 | uniform float u_drop_rate; 11 | uniform float u_drop_rate_bump; 12 | 13 | varying vec2 v_tex_pos; 14 | 15 | // pseudo-random generator 16 | const vec3 rand_constants = vec3(12.9898, 78.233, 4375.85453); 17 | float rand(const vec2 co) { 18 | float t = dot(rand_constants.xy, co); 19 | return fract(sin(t) * (rand_constants.z + t)); 20 | } 21 | 22 | // wind speed lookup; use manual bilinear filtering based on 4 adjacent pixels for smooth interpolation 23 | vec2 lookup_wind(const vec2 uv) { 24 | // return texture2D(u_wind, uv).rg; // lower-res hardware filtering 25 | vec2 px = 1.0 / u_wind_res; 26 | vec2 vc = (floor(uv * u_wind_res)) * px; 27 | vec2 f = fract(uv * u_wind_res); 28 | vec2 tl = texture2D(u_wind, vc).rg; 29 | vec2 tr = texture2D(u_wind, vc + vec2(px.x, 0)).rg; 30 | vec2 bl = texture2D(u_wind, vc + vec2(0, px.y)).rg; 31 | vec2 br = texture2D(u_wind, vc + px).rg; 32 | return mix(mix(tl, tr, f.x), mix(bl, br, f.x), f.y); 33 | } 34 | 35 | void main() { 36 | vec4 color = texture2D(u_particles, v_tex_pos); 37 | vec2 pos = vec2( 38 | color.r / 255.0 + color.b, 39 | color.g / 255.0 + color.a); // decode particle position from pixel RGBA 40 | 41 | vec2 velocity = mix(u_wind_min, u_wind_max, lookup_wind(pos)); 42 | float speed_t = length(velocity) / length(u_wind_max); 43 | 44 | // take EPSG:4236 distortion into account for calculating where the particle moved 45 | float distortion = cos(radians(pos.y * 180.0 - 90.0)); 46 | vec2 offset = vec2(velocity.x / distortion, -velocity.y) * 0.0001 * u_speed_factor; 47 | 48 | // update particle position, wrapping around the date line 49 | pos = fract(1.0 + pos + offset); 50 | 51 | // a random seed to use for the particle drop 52 | vec2 seed = (pos + v_tex_pos) * u_rand_seed; 53 | 54 | // drop rate is a chance a particle will restart at random position, to avoid degeneration 55 | float drop_rate = u_drop_rate + speed_t * u_drop_rate_bump; 56 | float drop = step(1.0 - drop_rate, rand(seed)); 57 | 58 | vec2 random_pos = vec2( 59 | rand(seed + 1.3), 60 | rand(seed + 2.1)); 61 | pos = mix(pos, random_pos, drop); 62 | 63 | // encode the new particle position back into RGBA 64 | gl_FragColor = vec4( 65 | fract(pos * 255.0), 66 | floor(pos * 255.0) / 255.0); 67 | } 68 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | 2 | function createShader(gl, type, source) { 3 | const shader = gl.createShader(type); 4 | gl.shaderSource(shader, source); 5 | 6 | gl.compileShader(shader); 7 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 8 | throw new Error(gl.getShaderInfoLog(shader)); 9 | } 10 | 11 | return shader; 12 | } 13 | 14 | export function createProgram(gl, vertexSource, fragmentSource) { 15 | const program = gl.createProgram(); 16 | 17 | const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource); 18 | const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource); 19 | 20 | gl.attachShader(program, vertexShader); 21 | gl.attachShader(program, fragmentShader); 22 | 23 | gl.linkProgram(program); 24 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 25 | throw new Error(gl.getProgramInfoLog(program)); 26 | } 27 | 28 | const wrapper = {program: program}; 29 | 30 | const numAttributes = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES); 31 | for (let i = 0; i < numAttributes; i++) { 32 | const attribute = gl.getActiveAttrib(program, i); 33 | wrapper[attribute.name] = gl.getAttribLocation(program, attribute.name); 34 | } 35 | const numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); 36 | for (let i = 0; i < numUniforms; i++) { 37 | const uniform = gl.getActiveUniform(program, i); 38 | wrapper[uniform.name] = gl.getUniformLocation(program, uniform.name); 39 | } 40 | 41 | return wrapper; 42 | } 43 | 44 | export function createTexture(gl, filter, data, width, height) { 45 | const texture = gl.createTexture(); 46 | gl.bindTexture(gl.TEXTURE_2D, texture); 47 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 48 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 49 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter); 50 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter); 51 | if (data instanceof Uint8Array) { 52 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data); 53 | } else { 54 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data); 55 | } 56 | gl.bindTexture(gl.TEXTURE_2D, null); 57 | return texture; 58 | } 59 | 60 | export function bindTexture(gl, texture, unit) { 61 | gl.activeTexture(gl.TEXTURE0 + unit); 62 | gl.bindTexture(gl.TEXTURE_2D, texture); 63 | } 64 | 65 | export function createBuffer(gl, data) { 66 | const buffer = gl.createBuffer(); 67 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 68 | gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); 69 | return buffer; 70 | } 71 | 72 | export function bindAttribute(gl, buffer, attribute, numComponents) { 73 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 74 | gl.enableVertexAttribArray(attribute); 75 | gl.vertexAttribPointer(attribute, numComponents, gl.FLOAT, false, 0, 0); 76 | } 77 | 78 | export function bindFramebuffer(gl, framebuffer, texture) { 79 | gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); 80 | if (texture) { 81 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); 82 | } 83 | } 84 | --------------------------------------------------------------------------------