├── favicon.svg ├── README.md ├── LICENSE ├── .github └── workflows │ └── static.yml ├── index.html ├── script.min.js └── script.js /favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [5kb webgl2 fluid simulation](https://loicmagne.github.io/webgl2_fluidsim/) 2 | 3 | Implementation of the [Stable Fluid](https://www.ljll.math.upmc.fr/~frey/cours/references/Stam%20J.,%20Stable%20fluids.pdf) algorithm, using webgl2 features (texelFetch, textureSize, vertex array objects) in about 500 lines of codes, and compressed to less than ~5kb of javascript. 4 | 5 | ## References 6 | 7 | GPU Gems: 8 | Stable Fluids paper: 9 | WebGL1 implementation: 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Loïc Magne 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | - name: Setup Pages 34 | uses: actions/configure-pages@v3 35 | - name: Upload artifact 36 | uses: actions/upload-pages-artifact@v1 37 | with: 38 | # Upload entire repository 39 | path: '.' 40 | - name: Deploy to GitHub Pages 41 | id: deployment 42 | uses: actions/deploy-pages@v1 43 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | webgl2 fluid simulation 9 | 10 | 49 | 50 | 51 | 52 |
53 | github 54 |
55 | 56 | 57 |
58 |
59 | 60 | 61 |
62 |
63 | 64 | 65 |
66 |
67 | 68 | 69 |
70 |
71 | 72 | 73 |
74 |
75 |
76 | 77 | 78 |
79 | 80 |
81 | 82 | 83 |
84 | 85 |
86 | 87 | 88 |
89 |
90 |
91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /script.min.js: -------------------------------------------------------------------------------- 1 | "use strict";var e=document.querySelector("#fluidsim_canvas");const v=e.getContext("webgl2");v.getExtension("EXT_color_buffer_float");let t={i:1,t:.5,o:"dye",v:.1,u:.99,l:.99,p:128,_:1024,h:20},o,r,c,s,u;function i(e){return o<1?{width:e,height:Math.round(e/o)}:{width:Math.round(e*o),height:e}}function x(){var e=v.canvas.clientWidth,n=v.canvas.clientHeight;if(o=v.canvas.clientWidth/v.canvas.clientHeight,v.canvas.width!==e||v.canvas.height!==n)return v.canvas.width=e,v.canvas.height=n,e=i(t.p),n=i(t._),r=e.width,c=e.height,s=n.width,u=n.height,1}x();let a=v.createVertexArray();var n=new Float32Array([-1,-1,1,1,1,-1,-1,-1,-1,1,1,1]),l=(l=a,n=n,A=2,O=v.FLOAT,w=!1,C=M=0,v.bindVertexArray(l),l=v.createBuffer(),v.bindBuffer(v.ARRAY_BUFFER,l),v.bufferData(v.ARRAY_BUFFER,n,v.STATIC_DRAW),v.enableVertexAttribArray(0),v.vertexAttribPointer(0,A,O,w,M,C),`#version 300 es 2 | in vec2 a_position; 3 | out vec2 v_position; 4 | 5 | void main() { 6 | v_position = a_position * 0.5 + 0.5; 7 | gl_Position = vec4(a_position, 0, 1); 8 | }`);function R(e,n,i){n=e.createShader(n);return e.shaderSource(n,i),e.compileShader(n),e.getShaderParameter(n,e.COMPILE_STATUS)?n:(console.error(e.getShaderInfoLog(n)),e.deleteShader(n),null)}function p(e,n){var i,t,e=R(v,v.VERTEX_SHADER,e),n=R(v,v.FRAGMENT_SHADER,n),e=(i=v,e=e,n=n,t=i.createProgram(),i.attachShader(t,e),i.attachShader(t,n),i.linkProgram(t),i.getProgramParameter(t,i.LINK_STATUS)?t:(console.error(i.getProgramInfoLog(t)),i.deleteProgram(t),null)),n=function(n,i){var t={},o=n.getProgramParameter(i,n.ACTIVE_UNIFORMS);for(let e=0;e 0.0 ? u_alpha : 1.0; 51 | res = coef * texture(u_x, v_position + dir * u_res); 52 | }`),d=p(l,`#version 300 es 53 | precision highp float; 54 | 55 | in vec2 v_position; 56 | uniform sampler2D u_x; 57 | uniform float u_alpha; 58 | out vec4 res; 59 | 60 | void main() { 61 | res = u_alpha * texture(u_x, v_position); 62 | }`),b=p(l,`#version 300 es 63 | precision highp float; 64 | 65 | in vec2 v_position; 66 | uniform sampler2D u_x; 67 | uniform vec2 u_point; 68 | uniform vec3 u_value; 69 | uniform float u_radius; 70 | uniform float u_ratio; 71 | out vec4 res; 72 | 73 | void main() { 74 | vec4 init = texture(u_x, v_position); 75 | vec2 v = v_position - u_point; 76 | v.x *= u_ratio; 77 | vec3 force = exp(-dot(v,v)/u_radius) * u_value; 78 | 79 | res = vec4(init.xyz + force, 1.); 80 | }`);function g(e,n,i,t,o,r){const c=v.createTexture(),s=(v.activeTexture(v.TEXTURE0),v.bindTexture(v.TEXTURE_2D,c),v.texParameteri(v.TEXTURE_2D,v.TEXTURE_MIN_FILTER,r),v.texParameteri(v.TEXTURE_2D,v.TEXTURE_MAG_FILTER,r),v.texParameteri(v.TEXTURE_2D,v.TEXTURE_WRAP_S,v.CLAMP_TO_EDGE),v.texParameteri(v.TEXTURE_2D,v.TEXTURE_WRAP_T,v.CLAMP_TO_EDGE),v.texImage2D(v.TEXTURE_2D,0,i,e,n,0,t,o,null),v.createFramebuffer());return v.bindFramebuffer(v.FRAMEBUFFER,s),v.framebufferTexture2D(v.FRAMEBUFFER,v.COLOR_ATTACHMENT0,v.TEXTURE_2D,c,0),{F:c,D:s,bind:()=>{v.bindFramebuffer(v.FRAMEBUFFER,s),v.viewport(0,0,e,n)},N:e=>(v.activeTexture(v.TEXTURE0+e),v.bindTexture(v.TEXTURE_2D,c),e)}}function F(e,n,i,t,o,r){return{read:g(e,n,i,t,o,r),write:g(e,n,i,t,o,r),S:function(){var e=this.read;this.read=this.write,this.write=e}}}function U(e,n,i,t,o,r,c){n=g(n,i,t,o,r,c);return v.useProgram(d.m),v.uniform1i(d.g.u_x,e.N(0)),I(n,v.TRIANGLES,6),n}function D(e,n,i,t,o,r,c){var s=F(n,i,t,o,r,c);return s.read=U(e.read,n,i,t,o,r,c),s.write=U(e.write,n,i,t,o,r,c),s}let y=F(r,c,v.RG32F,v.RG,v.FLOAT,v.NEAREST),N=F(r,c,v.R32F,v.RED,v.FLOAT,v.NEAREST),z=g(r,c,v.R32F,v.RED,v.FLOAT,v.NEAREST),Y=g(r,c,v.RG32F,v.RG,v.FLOAT,v.NEAREST),S=F(s,u,v.RGBA32F,v.RGBA,v.FLOAT,v.NEAREST);const q={F:null,D:null,bind:()=>{v.bindFramebuffer(v.FRAMEBUFFER,null),v.viewport(0,0,v.canvas.width,v.canvas.height)}};function I(e,n,i,t=!1){v.bindVertexArray(a),e.bind(),t&&(v.clearColor(0,0,0,1),v.clear(v.COLOR_BUFFER_BIT)),v.drawArrays(n,0,i)}function E(e,n){v.useProgram(m.m),v.uniform2f(m.g.u_res,1/r,1/c),v.uniform1i(m.g.u_x,e.read.N(0)),v.uniform1f(m.g.u_alpha,n),I(e.write,v.TRIANGLES,6),e.S()}let V=0;requestAnimationFrame(function e(n){var i=(n-V)/1e3,n=(V=n,x()&&(y=D(y,r,c,v.RG32F,v.RG,v.FLOAT,v.NEAREST),N=D(N,r,c,v.R32F,v.RED,v.FLOAT,v.NEAREST),S=D(S,s,u,v.RGBA32F,v.RGBA,v.FLOAT,v.NEAREST),z=g(r,c,v.R32F,v.RED,v.FLOAT,v.NEAREST),Y=g(r,c,v.RG32F,v.RG,v.FLOAT,v.NEAREST)),T.forEach(e=>{v.useProgram(b.m),v.uniform1i(b.g.u_x,y.read.N(0)),v.uniform2fv(b.g.u_point,[e.x,e.y]),v.uniform3fv(b.g.u_value,[e.dx*o,e.dy,0].map(e=>e*t.h)),v.uniform1f(b.g.u_radius,t.v),v.uniform1f(b.g.u_ratio,o),I(y.write,v.TRIANGLES,6),y.S(),v.uniform1i(b.g.u_x,S.read.N(0)),v.uniform3fv(b.g.u_value,e.color.map(e=>.2*e)),I(S.write,v.TRIANGLES,6),S.S()}),i);E(y,-1),v.useProgram(_.m),v.uniform1i(_.g.u_v,0),v.uniform1i(_.g.u_x,y.read.N(0)),v.uniform1f(_.g.u_dt,n),v.uniform1f(_.g.u_dissipation,t.u),I(y.write,v.TRIANGLES,6),y.S(),E(S,0),v.useProgram(_.m),v.uniform1i(_.g.u_v,y.read.N(0)),v.uniform1i(_.g.u_x,S.read.N(1)),v.uniform1f(_.g.u_dt,n),v.uniform1f(_.g.u_dissipation,t.l),I(S.write,v.TRIANGLES,6),S.S(),E(y,-1),v.useProgram(h.m),n=1/(t.i*n),v.uniform1f(h.g.u_alpha,n),v.uniform1f(h.g.u_beta,4+n);for(let e=0;e<20;e++)v.uniform1i(h.g.u_x,y.read.N(0)),v.uniform1i(h.g.u_b,y.read.N(0)),I(y.write,v.TRIANGLES,6),y.S();E(y,-1),v.useProgram(L.m),v.uniform1i(L.g.u_x,y.read.N(0)),I(z,v.TRIANGLES,6),v.useProgram(d.m),v.uniform1i(d.g.u_x,N.read.N(0)),v.uniform1f(d.g.u_alpha,t.t),I(N.write,v.TRIANGLES,6),N.S();for(let e=0;e<50;e++)E(N,1),v.useProgram(h.m),v.uniform1i(h.g.u_b,z.N(0)),v.uniform1i(h.g.u_x,N.read.N(1)),v.uniform1f(h.g.u_alpha,-1),v.uniform1f(h.g.u_beta,4),I(N.write,v.TRIANGLES,6),N.S();E(y,-1),E(N,1),v.useProgram(f.m),v.uniform1i(f.g.u_p,N.read.N(0)),v.uniform1i(f.g.u_v,y.read.N(1)),I(y.write,v.TRIANGLES,6),y.S(),i=("velocity"==t.o?y:"pressure"==t.o?N:S).read,v.useProgram(d.m),v.uniform1i(d.g.u_x,i.N(0)),v.uniform1f(d.g.u_alpha,1),I(q,v.TRIANGLES,6),requestAnimationFrame(e)});const T=[];function X(e){return{id:e.pointerId,x:e.offsetX/v.canvas.clientWidth,y:1-e.offsetY/v.canvas.clientHeight,dx:0,dy:0,color:[Math.random(),Math.random(),Math.random()]}}e.addEventListener("pointerdown",e=>{T.push(X(e))}),e.addEventListener("pointerup",n=>{var e=T.findIndex(e=>e.id===n.pointerId);e<0||T.splice(e,1)}),e.addEventListener("pointermove",n=>{var e,i=T.findIndex(e=>e.id===n.pointerId);i<0||(e=X(n),T[i]=(i=T[i],(e=e).color=i.color,e.dx=e.x-i.x,e.dy=e.y-i.y,e))}),e.addEventListener("pointerout",n=>{var e=T.findIndex(e=>e.id===n.pointerId);e<0||T.splice(e,1)});var n=document.querySelector("#viscosity"),A=document.querySelector("#pressure"),O=document.querySelector("#radius"),w=document.querySelector("#velocity_dissipation"),M=document.querySelector("#density_dissipation"),C=document.querySelector("#display_radio");function P(e,n,i){return n*Math.pow(i/n,e)}const j=e=>P(e,1e-4,1e3),k=e=>P(e,1e-4,.01),B=e=>1-P(e,.001,.1),G=e=>1-P(e,.001,.1);t.i=j(n.value),t.t=A.value,t.v=k(O.value),t.u=B(w.value),t.l=G(M.value),n.addEventListener("input",e=>{t.i=j(e.target.value)}),A.addEventListener("input",e=>{t.t=e.target.value}),O.addEventListener("input",e=>{t.v=k(e.target.value)}),w.addEventListener("input",e=>{t.u=B(e.target.value)}),M.addEventListener("input",e=>{t.l=G(e.target.value)}),C.addEventListener("input",e=>{t.o=e.target.value}); -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** @type {HTMLCanvasElement} */ 4 | const canvas = document.querySelector('#fluidsim_canvas'); 5 | /** @type {WebGL2RenderingContext} */ 6 | const gl = canvas.getContext('webgl2'); 7 | 8 | gl.getExtension('EXT_color_buffer_float'); 9 | 10 | let config = { 11 | NU: 1., 12 | PRESSURE: 0.5, 13 | DISPLAY: 'dye', 14 | RADIUS: 0.1, 15 | VELOCITY_DISSIPATION: 0.99, 16 | DYE_DISSIPATION: 0.99, 17 | SIM_RESOLUTION: 128, 18 | DYE_RESOLUTION: 1024, 19 | SPLAT_FORCE: 20. 20 | } 21 | 22 | let aspect_ratio; 23 | let sim_width, sim_height; 24 | let dye_width, dye_height; 25 | 26 | function get_aspect_ratio() { 27 | return gl.canvas.clientWidth / gl.canvas.clientHeight; 28 | } 29 | 30 | function get_size(target_size) { 31 | if (aspect_ratio < 1) return { width: target_size, height: Math.round(target_size / aspect_ratio) }; 32 | else return { width: Math.round(target_size * aspect_ratio), height: target_size }; 33 | } 34 | 35 | function setup_sizes() { 36 | const w = gl.canvas.clientWidth; 37 | const h = gl.canvas.clientHeight; 38 | 39 | aspect_ratio = get_aspect_ratio(); 40 | 41 | if (gl.canvas.width === w && gl.canvas.height === h) return false; 42 | 43 | gl.canvas.width = w; 44 | gl.canvas.height = h; 45 | 46 | const sim_size = get_size(config.SIM_RESOLUTION); 47 | const dye_size = get_size(config.DYE_RESOLUTION); 48 | 49 | sim_width = sim_size.width; 50 | sim_height = sim_size.height; 51 | dye_width = dye_size.width; 52 | dye_height = dye_size.height; 53 | 54 | return true; 55 | } 56 | 57 | setup_sizes(); 58 | 59 | /* GEOMETRY SETUP */ 60 | 61 | let full_vao = gl.createVertexArray(); 62 | 63 | const POSITION_LOCATION = 0; 64 | 65 | function setup_geometry(vao, position_data, size, type, normalized, stride, offset) { 66 | gl.bindVertexArray(vao); 67 | 68 | const position_buffer = gl.createBuffer(); 69 | gl.bindBuffer(gl.ARRAY_BUFFER, position_buffer); 70 | gl.bufferData(gl.ARRAY_BUFFER, position_data, gl.STATIC_DRAW); 71 | gl.enableVertexAttribArray(POSITION_LOCATION); 72 | gl.vertexAttribPointer(POSITION_LOCATION, size, type, normalized, stride, offset); 73 | } 74 | 75 | const full_pos = new Float32Array([-1, -1, 1, 1, 1, -1, -1, -1, -1, 1, 1, 1,]); 76 | setup_geometry(full_vao, full_pos, 2, gl.FLOAT, false, 0, 0); 77 | 78 | /* SHADERS SETUP */ 79 | 80 | const utility_neightbors = ` 81 | struct Neighbors { 82 | vec4 l; 83 | vec4 r; 84 | vec4 t; 85 | vec4 b; 86 | vec4 c; 87 | }; 88 | 89 | Neighbors tex_neighbors(sampler2D tex, ivec2 pos) { 90 | vec4 b = texelFetch(tex, pos - ivec2(0, 1), 0); 91 | vec4 t = texelFetch(tex, pos + ivec2(0, 1), 0); 92 | vec4 l = texelFetch(tex, pos - ivec2(1, 0), 0); 93 | vec4 r = texelFetch(tex, pos + ivec2(1, 0), 0); 94 | vec4 c = texelFetch(tex, pos, 0); 95 | return Neighbors(l, r, t, b, c); 96 | }` 97 | 98 | const base_vs = `#version 300 es 99 | in vec2 a_position; 100 | out vec2 v_position; 101 | 102 | void main() { 103 | v_position = a_position * 0.5 + 0.5; 104 | gl_Position = vec4(a_position, 0, 1); 105 | }`; 106 | 107 | const advection_fs = `#version 300 es 108 | precision highp float; 109 | 110 | uniform sampler2D u_v; 111 | uniform sampler2D u_x; 112 | uniform float u_dt; 113 | uniform float u_dissipation; 114 | out vec4 res; 115 | 116 | vec4 bilerp(sampler2D tex, vec2 x_norm, vec2 size) { 117 | vec2 x = x_norm * size - 0.5; 118 | vec2 fx = fract(x); 119 | ivec2 ix = ivec2(floor(x)); 120 | 121 | vec4 x00 = texelFetch(tex, ix + ivec2(0,0), 0); 122 | vec4 x01 = texelFetch(tex, ix + ivec2(0,1), 0); 123 | vec4 x10 = texelFetch(tex, ix + ivec2(1,0), 0); 124 | vec4 x11 = texelFetch(tex, ix + ivec2(1,1), 0); 125 | 126 | return mix(mix(x00, x10, fx.x), mix(x01, x11, fx.x), fx.y); 127 | } 128 | 129 | void main() { 130 | vec2 size_v = vec2(textureSize(u_v, 0)); 131 | vec2 size_x = vec2(textureSize(u_x, 0)); 132 | vec2 aspect_ratio = vec2(size_x.x / size_x.y, 1.0); 133 | vec2 normalized_pos = gl_FragCoord.xy / size_x; 134 | vec2 prev = normalized_pos - u_dt * bilerp(u_v, normalized_pos, size_v).xy / aspect_ratio; 135 | res = u_dissipation * bilerp(u_x, prev, size_x); 136 | }` 137 | 138 | const jacobi_fs = `#version 300 es 139 | precision highp float; 140 | 141 | uniform sampler2D u_x; 142 | uniform sampler2D u_b; 143 | uniform float u_alpha; 144 | uniform float u_beta; 145 | out vec4 res; 146 | 147 | ${utility_neightbors} 148 | 149 | void main() { 150 | ivec2 pos = ivec2(gl_FragCoord.xy); 151 | Neighbors n = tex_neighbors(u_x, pos); 152 | vec4 b = texelFetch(u_b, pos, 0); 153 | res = (n.b + n.t + n.l + n.r + u_alpha * b) / u_beta; 154 | }` 155 | 156 | const subtract_grad_fs = `#version 300 es 157 | precision highp float; 158 | 159 | uniform sampler2D u_v; 160 | uniform sampler2D u_p; 161 | out vec4 res; 162 | 163 | ${utility_neightbors} 164 | 165 | void main() { 166 | ivec2 pos = ivec2(gl_FragCoord.xy); 167 | Neighbors n = tex_neighbors(u_p, pos); 168 | 169 | vec4 grad = vec4(n.r.x - n.l.x, n.t.x - n.b.x, 0, 0) / 2.; 170 | vec4 init_v = texelFetch(u_v, pos, 0); 171 | res = init_v - grad; 172 | }` 173 | 174 | const div_fs = `#version 300 es 175 | precision highp float; 176 | 177 | uniform sampler2D u_x; 178 | out vec4 res; 179 | 180 | ${utility_neightbors} 181 | 182 | void main() { 183 | ivec2 pos = ivec2(gl_FragCoord.xy); 184 | Neighbors n = tex_neighbors(u_x, pos); 185 | 186 | float div = (n.r.x - n.l.x + n.t.y - n.b.y) / 2.; 187 | 188 | res = vec4(div, 0, 0, 1); 189 | }` 190 | 191 | const boundary_fs = `#version 300 es 192 | precision highp float; 193 | 194 | in vec2 v_position; 195 | uniform sampler2D u_x; 196 | uniform vec2 u_res; 197 | uniform float u_alpha; 198 | out vec4 res; 199 | 200 | void main() { 201 | vec2 dir = vec2(0, 0); 202 | dir += vec2(lessThan(v_position, u_res)); 203 | dir -= vec2(greaterThan(v_position, vec2(1.0) - u_res)); 204 | float coef = length(dir) > 0.0 ? u_alpha : 1.0; 205 | res = coef * texture(u_x, v_position + dir * u_res); 206 | }`; 207 | 208 | const display_fs = `#version 300 es 209 | precision highp float; 210 | 211 | in vec2 v_position; 212 | uniform sampler2D u_x; 213 | uniform float u_alpha; 214 | out vec4 res; 215 | 216 | void main() { 217 | res = u_alpha * texture(u_x, v_position); 218 | }`; 219 | 220 | const splat_fs = `#version 300 es 221 | precision highp float; 222 | 223 | in vec2 v_position; 224 | uniform sampler2D u_x; 225 | uniform vec2 u_point; 226 | uniform vec3 u_value; 227 | uniform float u_radius; 228 | uniform float u_ratio; 229 | out vec4 res; 230 | 231 | void main() { 232 | vec4 init = texture(u_x, v_position); 233 | vec2 v = v_position - u_point; 234 | v.x *= u_ratio; 235 | vec3 force = exp(-dot(v,v)/u_radius) * u_value; 236 | 237 | res = vec4(init.xyz + force, 1.); 238 | }`; 239 | 240 | function compile_shader(gl, type, source) { 241 | const shader = gl.createShader(type); 242 | gl.shaderSource(shader, source); 243 | gl.compileShader(shader); 244 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 245 | console.error(gl.getShaderInfoLog(shader)); 246 | gl.deleteShader(shader); 247 | return null; 248 | } 249 | return shader; 250 | } 251 | 252 | function compile_program(gl, vs, fs) { 253 | const program = gl.createProgram(); 254 | gl.attachShader(program, vs); 255 | gl.attachShader(program, fs); 256 | gl.linkProgram(program); 257 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 258 | console.error(gl.getProgramInfoLog(program)); 259 | gl.deleteProgram(program); 260 | return null; 261 | } 262 | return program; 263 | } 264 | 265 | function compile_uniforms(gl, program) { 266 | const uniforms = {}; 267 | const n = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); 268 | for (let i = 0; i < n; ++i) { 269 | const info = gl.getActiveUniform(program, i); 270 | uniforms[info.name] = gl.getUniformLocation(program, info.name); 271 | } 272 | return uniforms; 273 | } 274 | 275 | function create_program(vs_source, fs_source) { 276 | const vs = compile_shader(gl, gl.VERTEX_SHADER, vs_source); 277 | const fs = compile_shader(gl, gl.FRAGMENT_SHADER, fs_source); 278 | const program = compile_program(gl, vs, fs); 279 | const uniforms = compile_uniforms(gl, program); 280 | 281 | gl.bindAttribLocation(program, POSITION_LOCATION, "a_position"); 282 | 283 | return { program, uniforms }; 284 | } 285 | 286 | const advection_program = create_program(base_vs, advection_fs); 287 | const jacobi_program = create_program(base_vs, jacobi_fs); 288 | const subtract_grad_program = create_program(base_vs, subtract_grad_fs); 289 | const div_program = create_program(base_vs, div_fs); 290 | const boundary_program = create_program(base_vs, boundary_fs); 291 | const display_program = create_program(base_vs, display_fs); 292 | const splat_program = create_program(base_vs, splat_fs); 293 | 294 | /* TARGET TEXTURES / FRAMEBUFFERS SETUP */ 295 | 296 | function create_fbo(w, h, internal_format, format, type, filter) { 297 | const texture = gl.createTexture(); 298 | gl.activeTexture(gl.TEXTURE0); 299 | gl.bindTexture(gl.TEXTURE_2D, texture); 300 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter); 301 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter); 302 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 303 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 304 | gl.texImage2D(gl.TEXTURE_2D, 0, internal_format, w, h, 0, format, type, null); 305 | 306 | const fb = gl.createFramebuffer(); 307 | gl.bindFramebuffer(gl.FRAMEBUFFER, fb); 308 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); 309 | 310 | return { 311 | tex: texture, 312 | fb: fb, 313 | bind: () => { 314 | gl.bindFramebuffer(gl.FRAMEBUFFER, fb); 315 | gl.viewport(0, 0, w, h); 316 | }, 317 | bind_tex: (i) => { 318 | gl.activeTexture(gl.TEXTURE0+i); 319 | gl.bindTexture(gl.TEXTURE_2D, texture); 320 | return i; 321 | }, 322 | }; 323 | } 324 | 325 | function create_fbo_pair(w, h, internal_format, format, type, filter) { 326 | return { 327 | read: create_fbo(w, h, internal_format, format, type, filter), 328 | write: create_fbo(w, h, internal_format, format, type, filter), 329 | swap: function() { 330 | const temp = this.read; 331 | this.read = this.write; 332 | this.write = temp; 333 | }, 334 | }; 335 | } 336 | 337 | function resize_fbo(src, w, h, internal_format, format, type, filter) { 338 | const new_fbo = create_fbo(w, h, internal_format, format, type, filter); 339 | gl.useProgram(display_program.program); 340 | gl.uniform1i(display_program.uniforms.u_x, src.bind_tex(0)); 341 | render(new_fbo, gl.TRIANGLES, 6); 342 | return new_fbo; 343 | } 344 | 345 | function resize_fbo_pair(src, w, h, internal_format, format, type, filter) { 346 | const new_fbo = create_fbo_pair(w, h, internal_format, format, type, filter); 347 | new_fbo.read = resize_fbo(src.read, w, h, internal_format, format, type, filter); 348 | new_fbo.write = resize_fbo(src.write, w, h, internal_format, format, type, filter); 349 | return new_fbo; 350 | } 351 | 352 | let velocity = create_fbo_pair(sim_width, sim_height, gl.RG32F, gl.RG, gl.FLOAT, gl.NEAREST); 353 | let pressure = create_fbo_pair(sim_width, sim_height, gl.R32F, gl.RED, gl.FLOAT, gl.NEAREST); 354 | let tmp_1f = create_fbo(sim_width, sim_height, gl.R32F, gl.RED, gl.FLOAT, gl.NEAREST); 355 | let tmp_2f = create_fbo(sim_width, sim_height, gl.RG32F, gl.RG, gl.FLOAT, gl.NEAREST); 356 | let dye = create_fbo_pair(dye_width, dye_height, gl.RGBA32F, gl.RGBA, gl.FLOAT, gl.NEAREST); 357 | 358 | const screen = { 359 | tex: null, 360 | fb: null, 361 | bind: () => { 362 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 363 | gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); 364 | } 365 | } 366 | 367 | function setup_fbos() { 368 | velocity = resize_fbo_pair(velocity, sim_width, sim_height, gl.RG32F, gl.RG, gl.FLOAT, gl.NEAREST); 369 | pressure = resize_fbo_pair(pressure, sim_width, sim_height, gl.R32F, gl.RED, gl.FLOAT, gl.NEAREST); 370 | dye = resize_fbo_pair(dye, dye_width, dye_height, gl.RGBA32F, gl.RGBA, gl.FLOAT, gl.NEAREST); 371 | tmp_1f = create_fbo(sim_width, sim_height, gl.R32F, gl.RED, gl.FLOAT, gl.NEAREST); 372 | tmp_2f = create_fbo(sim_width, sim_height, gl.RG32F, gl.RG, gl.FLOAT, gl.NEAREST); 373 | } 374 | 375 | /* SIMULATION / RENDERING */ 376 | 377 | function render(fbo, geometry, count, clear = false) { 378 | gl.bindVertexArray(full_vao); 379 | fbo.bind(); 380 | if (clear) { 381 | gl.clearColor(0.0, 0.0, 0.0, 1.0); 382 | gl.clear(gl.COLOR_BUFFER_BIT); 383 | } 384 | gl.drawArrays(geometry, 0, count); 385 | } 386 | 387 | function render_screen(fbo) { 388 | gl.useProgram(display_program.program); 389 | gl.uniform1i(display_program.uniforms.u_x, fbo.bind_tex(0)); 390 | gl.uniform1f(display_program.uniforms.u_alpha, 1.0); 391 | render(screen, gl.TRIANGLES, 6); 392 | } 393 | 394 | function set_boundary(fbo_pair, alpha) { 395 | gl.useProgram(boundary_program.program); 396 | gl.uniform2f(boundary_program.uniforms.u_res, 1. / sim_width, 1. / sim_height) 397 | gl.uniform1i(boundary_program.uniforms.u_x, fbo_pair.read.bind_tex(0)); 398 | gl.uniform1f(boundary_program.uniforms.u_alpha, alpha); 399 | render(fbo_pair.write, gl.TRIANGLES, 6); 400 | fbo_pair.swap(); 401 | } 402 | 403 | function step_sim(dt) { 404 | 405 | // Advect velocity 406 | set_boundary(velocity, -1.0); 407 | gl.useProgram(advection_program.program); 408 | 409 | gl.uniform1i(advection_program.uniforms.u_v, 0); 410 | gl.uniform1i(advection_program.uniforms.u_x, velocity.read.bind_tex(0)); 411 | gl.uniform1f(advection_program.uniforms.u_dt, dt); 412 | gl.uniform1f(advection_program.uniforms.u_dissipation, config.VELOCITY_DISSIPATION); 413 | 414 | render(velocity.write, gl.TRIANGLES, 6); 415 | velocity.swap(); 416 | 417 | // Advect dye 418 | set_boundary(dye, 0.); 419 | gl.useProgram(advection_program.program); 420 | 421 | gl.uniform1i(advection_program.uniforms.u_v, velocity.read.bind_tex(0)); 422 | gl.uniform1i(advection_program.uniforms.u_x, dye.read.bind_tex(1)); 423 | gl.uniform1f(advection_program.uniforms.u_dt, dt); 424 | gl.uniform1f(advection_program.uniforms.u_dissipation, config.DYE_DISSIPATION); 425 | 426 | render(dye.write, gl.TRIANGLES, 6); 427 | dye.swap(); 428 | 429 | // Diffuse velocity 430 | set_boundary(velocity, -1.0); 431 | gl.useProgram(jacobi_program.program); 432 | 433 | const factor = 1./ (config.NU * dt); 434 | gl.uniform1f(jacobi_program.uniforms.u_alpha, factor); 435 | gl.uniform1f(jacobi_program.uniforms.u_beta, factor + 4.0); 436 | for (let i = 0; i < 20; i++) { 437 | gl.uniform1i(jacobi_program.uniforms.u_x, velocity.read.bind_tex(0)); 438 | gl.uniform1i(jacobi_program.uniforms.u_b, velocity.read.bind_tex(0)); 439 | render(velocity.write, gl.TRIANGLES, 6); 440 | velocity.swap(); 441 | } 442 | 443 | // Project velocity 444 | // Compute divergence 445 | set_boundary(velocity, -1); 446 | gl.useProgram(div_program.program); 447 | gl.uniform1i(div_program.uniforms.u_x, velocity.read.bind_tex(0)); 448 | render(tmp_1f, gl.TRIANGLES, 6); 449 | 450 | // Clear pressure 451 | gl.useProgram(display_program.program); 452 | gl.uniform1i(display_program.uniforms.u_x, pressure.read.bind_tex(0)); 453 | gl.uniform1f(display_program.uniforms.u_alpha, config.PRESSURE); 454 | render(pressure.write, gl.TRIANGLES, 6); 455 | pressure.swap(); 456 | 457 | // Solve for pressure 458 | for (let i = 0; i < 50; i++) { 459 | set_boundary(pressure, 1.); 460 | 461 | // Jacobi iteration 462 | gl.useProgram(jacobi_program.program); 463 | gl.uniform1i(jacobi_program.uniforms.u_b, tmp_1f.bind_tex(0)); 464 | gl.uniform1i(jacobi_program.uniforms.u_x, pressure.read.bind_tex(1)); 465 | gl.uniform1f(jacobi_program.uniforms.u_alpha, -1.0); 466 | gl.uniform1f(jacobi_program.uniforms.u_beta, 4.0); 467 | render(pressure.write, gl.TRIANGLES, 6); 468 | pressure.swap(); 469 | } 470 | 471 | // Compute pressure gradient and subtract from velocity 472 | set_boundary(velocity, -1.); 473 | set_boundary(pressure, 1.); 474 | gl.useProgram(subtract_grad_program.program); 475 | gl.uniform1i(subtract_grad_program.uniforms.u_p, pressure.read.bind_tex(0)); 476 | gl.uniform1i(subtract_grad_program.uniforms.u_v, velocity.read.bind_tex(1)); 477 | render(velocity.write, gl.TRIANGLES, 6); 478 | velocity.swap(); 479 | } 480 | 481 | let last_time = 0; 482 | function loop(t) { 483 | let dt = (t - last_time) / 1000.; 484 | last_time = t; 485 | 486 | if (setup_sizes()) setup_fbos(); 487 | step_user(); 488 | step_sim(dt); 489 | render_screen( 490 | config.DISPLAY == 'velocity' ? velocity.read : 491 | config.DISPLAY == 'pressure' ? pressure.read : 492 | dye.read 493 | ); 494 | 495 | requestAnimationFrame(loop); 496 | } 497 | 498 | requestAnimationFrame(loop); 499 | 500 | /* USER INPUTS */ 501 | 502 | const pointers = []; 503 | 504 | function create_pointer(pointer) { 505 | return { 506 | id: pointer.pointerId, 507 | x: pointer.offsetX / gl.canvas.clientWidth, 508 | y: 1. - pointer.offsetY / gl.canvas.clientHeight, 509 | dx: 0, 510 | dy: 0, 511 | color: [Math.random(), Math.random(), Math.random()] 512 | }; 513 | } 514 | 515 | function update_pointer(old_ptr, new_ptr) { 516 | new_ptr.color = old_ptr.color; 517 | new_ptr.dx = new_ptr.x - old_ptr.x; 518 | new_ptr.dy = new_ptr.y - old_ptr.y; 519 | return new_ptr; 520 | } 521 | 522 | canvas.addEventListener('pointerdown', (e) => { 523 | pointers.push(create_pointer(e)); 524 | }); 525 | 526 | canvas.addEventListener('pointerup', (e) => { 527 | const pointer_idx = pointers.findIndex(p => p.id === e.pointerId); 528 | if (pointer_idx < 0) return; 529 | 530 | pointers.splice(pointer_idx, 1); 531 | }); 532 | 533 | canvas.addEventListener('pointermove', (e) => { 534 | const pointer_idx = pointers.findIndex(p => p.id === e.pointerId); 535 | if (pointer_idx < 0) return; 536 | 537 | const new_pointer = create_pointer(e); 538 | pointers[pointer_idx] = update_pointer(pointers[pointer_idx], new_pointer); 539 | }); 540 | 541 | canvas.addEventListener('pointerout', (e) => { 542 | const pointer_idx = pointers.findIndex(p => p.id === e.pointerId); 543 | if (pointer_idx < 0) return; 544 | 545 | pointers.splice(pointer_idx, 1); 546 | }); 547 | 548 | function step_user() { 549 | pointers.forEach(p => { 550 | gl.useProgram(splat_program.program); 551 | gl.uniform1i(splat_program.uniforms.u_x, velocity.read.bind_tex(0)); 552 | gl.uniform2fv(splat_program.uniforms.u_point, [p.x, p.y]); 553 | gl.uniform3fv(splat_program.uniforms.u_value, [p.dx * aspect_ratio, p.dy, 0].map(c => c * config.SPLAT_FORCE)); 554 | gl.uniform1f(splat_program.uniforms.u_radius, config.RADIUS); 555 | gl.uniform1f(splat_program.uniforms.u_ratio, aspect_ratio); 556 | render(velocity.write, gl.TRIANGLES, 6); 557 | velocity.swap(); 558 | 559 | gl.uniform1i(splat_program.uniforms.u_x, dye.read.bind_tex(0)); 560 | gl.uniform3fv(splat_program.uniforms.u_value, p.color.map(c => c * 0.2)); 561 | render(dye.write, gl.TRIANGLES, 6); 562 | dye.swap(); 563 | }); 564 | } 565 | 566 | /* UI */ 567 | 568 | const viscosity_slider = document.querySelector('#viscosity'); 569 | const pressure_slider = document.querySelector('#pressure'); 570 | const radius_slider = document.querySelector('#radius'); 571 | const velocity_dissipation_slider = document.querySelector('#velocity_dissipation'); 572 | const density_dissipation_slider = document.querySelector('#density_dissipation'); 573 | const display_radio = document.querySelector('#display_radio'); 574 | 575 | // Transform a 0-1 slider value to an a-b log scale 576 | function log_scale(value, a, b) { 577 | return a * Math.pow(b/a, value); 578 | } 579 | 580 | const viscosity_transform = value => log_scale(value, 0.0001, 1000.); 581 | const radius_transform = value => log_scale(value, 0.0001, 0.01); 582 | const velocity_dissipation_transform = value => 1. - log_scale(value, 0.001, 0.1); 583 | const density_dissipation_transform = value => 1. - log_scale(value, 0.001, 0.1); 584 | 585 | config.NU = viscosity_transform(viscosity_slider.value); 586 | config.PRESSURE = pressure_slider.value; 587 | config.RADIUS = radius_transform(radius_slider.value); 588 | config.VELOCITY_DISSIPATION = velocity_dissipation_transform(velocity_dissipation_slider.value); 589 | config.DYE_DISSIPATION = density_dissipation_transform(density_dissipation_slider.value); 590 | 591 | viscosity_slider.addEventListener('input', e => {config.NU = viscosity_transform(e.target.value);}); 592 | pressure_slider.addEventListener('input', e => {config.PRESSURE = e.target.value;}); 593 | radius_slider.addEventListener('input', (e) => {config.RADIUS = radius_transform(e.target.value);}); 594 | velocity_dissipation_slider.addEventListener('input', e => {config.VELOCITY_DISSIPATION = velocity_dissipation_transform(e.target.value);}); 595 | density_dissipation_slider.addEventListener('input', e => {config.DYE_DISSIPATION = density_dissipation_transform(e.target.value);}); 596 | display_radio.addEventListener('input', (e) => {config.DISPLAY = e.target.value;}); --------------------------------------------------------------------------------