├── .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 |
--------------------------------------------------------------------------------