Two-Texture Fluid Dynamics
Vfrag
Pfrag
Frag
Setup
Loop
UI
Save
Stats
/* Fragment shader for velocity texture */ void main() { // Read my center location (to get velocity) vec2 read=texcoords; // read from here vec4 C=texture2D(Vtex,read,backMIP); // Back-looking advection (Stam-style) vec2 back=read-dt*Vscale*C.xy/dx*pix; vec4 V=texture2D(Vtex,back,-1.0); // Velocity vec4 P=texture2D(Ptex,back,-1.0); // Physics // Add any P-to-V physics here! V.y+=dt*5.0*P.r; // red is bouyant. float div=0.0; // Loop over mipmap levels, from top down vec4 L,R,T,B; for (float lvl=9.0;lvl>=0.0;lvl-=1.0) { float scale=pow(2.0,lvl); float dist=scale*pix; L=texture2D(Vtex,back+vec2(-dist,0.0),lvl); R=texture2D(Vtex,back+vec2(+dist,0.0),lvl); T=texture2D(Vtex,back+vec2(0.0,+dist),lvl); B=texture2D(Vtex,back+vec2(0.0,-dist),lvl); // pseudopressure drives velocity // (from Navier Stokes equation) V.x += -dt*P2V*(R.z - L.z)/(dx); V.y += -dt*P2V*(T.z - B.z)/(dy); // velocity convergence drives pseudopressure div += V2P*((R.x - L.x)/dx + (T.y - B.y)/dy); } V.z = -div; // Clamp the top and bottom of the domain if (texcoords.y>0.99 || texcoords.y<0.01) V.xy=vec2(0.0); // no-slip at top & bottom // Write out new color gl_FragColor=V; if (doSetup>0.5) { // first timestep setup if (read.y<0.5) gl_FragColor=vec4(1,0,0,0.999); else gl_FragColor=vec4(-1,0,0,0); } }
/* Fragment shader for physics (P) texture */ void main() { // Read my center location (to get velocity) vec2 read=texcoords; // read from here vec4 C=texture2D(Vtex,read,backMIP); // Back-looking advection (Stam-style) vec2 back=read-dt*Vscale*C.xy/dx*pix; vec4 V=texture2D(Vtex,back,-1.0); // Velocity vec4 P=texture2D(Ptex,back,-1.0); // Physics // Insert P-to-P or V-to-P physics here! if (length(read-vec2(0.3,0.3))<0.01) { // special area P+=dt*vec4(10.0,0,0,0); } // Write out new color gl_FragColor=P; if (doSetup>0.5) { // first timestep setup if (read.x<0.5) gl_FragColor=vec4(0,0,1,0); else gl_FragColor=vec4(0,1,0,1); } }
/* Fragment shader shared setup code. Setup pastes this before Vfrag and Pfrag. */ varying vec2 texcoords; uniform float time; uniform float doSetup; uniform sampler2D Vtex,Ptex; float pix=1.0/512.0; // texture coords per pixel float dx=0.2; // grid size, meters/pixel float dy=0.2; // grid size, meters/pixel float dt=0.01; // or use uniform! float backMIP=1.2; // mipmap blurring for velocity estimate float P2V=0.1; /* pressure-to-velocity */ float V2P=1.0; /* velocity-to-pressure */ float Vscale=0.5*dx/dt; /* converts velocity to meters/second */
// Setup to render into an offscreen buffer // (See mrdoob.github.com/three.js/examples/webgl_.html) var w=512, h=512; // size of offscreen tex (pixels) function make_offscreen(shaderCode) { var off={}; // offscreen texture object var options={ minFilter: THREE.LinearMipmapLinearFilter, // we want mipmaps magFilter: THREE.LinearFilter, // we want nice filtering wrapS:THREE.RepeatWrapping, // horizontal // wrapT:THREE.RepeatWrapping, // vertical format:THREE.RGBAFormat, // texture has 4 channels: RGBA type:THREE.FloatType // uncomment for float (default is byte) }; off.texnew = new THREE.WebGLRenderTarget( w,h, options ); off.tex = new THREE.WebGLRenderTarget( w,h, options ); // The offscreen buffer has its own camera and scene off.camera = new THREE.OrthographicCamera( 0,1, 1,0, -100, 100 ); off.scene = new THREE.Scene(); off.sphere = new THREE.Mesh( new THREE.SphereGeometry(0.02,10,10), new THREE.MeshBasicMaterial( { color: 0x5500cc} ) ); off.sphere.position.set(100,0,0); // push away, so it doesn't mess stuff up off.scene.add( off.sphere ); // Here's how to run a shader into the offscreen buffer var plane=new THREE.Mesh( new THREE.PlaneGeometry(1,1), off.frag=new THREE.ShaderMaterial( { uniforms: { time: { type: "f", value: 0.0 }, dt: {type: "f", value: 0.0 }, doSetup: {type: "f", value: 1.0}, }, vertexShader: lib.simpleVertexShader, fragmentShader: shaderCode } ) ); off.uniforms=off.frag.uniforms; plane.position.set(0.5,0.5,0.0); // center it off.scene.add(plane); off.render=function(off) { // Update uniform variables off.uniforms.time.value=lib.time; off.uniforms.dt.value=lib.dt; // Render offscreen scene into tex PixAnvil.renderer.render( off.scene, off.camera, off.texnew, false ); // swap old and new textures ("ping-pong") // We just read from tex, write to texnew var t=off.texnew; off.texnew=off.tex; off.tex=t; // Clear the depth buffer PixAnvil.renderer.clear(false,true); } return off; }; // Two separate textures for Velocity and Physics var Frag=PixAnvil.loadTab("Frag"); // shared setup sim.V=make_offscreen(Frag+PixAnvil.loadTab("Vfrag")); sim.P=make_offscreen(Frag+PixAnvil.loadTab("Pfrag")); // I'm amplifying the texture colors using a shader var cubeShow = new THREE.CubeGeometry( 10,0.0001,10 ); for (var whichTex=0.0;whichTex<=1.0; whichTex+=1.0) { var easyShow = new THREE.Mesh( cubeShow, sim.showShader=new THREE.ShaderMaterial( { uniforms: { Vtex: {type: "t", value: sim.V.tex }, Ptex: {type: "t", value: sim.P.tex }, }, vertexShader: lib.simpleVertexShader, fragmentShader: "uniform sampler2D Vtex, Ptex;\n"+ "varying vec2 texcoords;\n"+ "void main(void) { \n"+ (whichTex==0.0? " vec4 t=texture2D(Ptex,texcoords);\n": " vec4 t=texture2D(Vtex,texcoords);\n"+ " t=(t*0.5+0.5)*t.a; // standard V scaling\n" )+ " gl_FragColor=t;\n"+ " gl_FragColor.a=1.0;\n"+ "}" } ) ); easyShow.position.set(+11*whichTex,6,5); scene.add( easyShow ); } // Add a brown TV frame (so you can see background) var frame = new THREE.Mesh( cubeShow, new THREE.MeshBasicMaterial( { color: 0x884422 } ) ); frame.position.set(0,6.1,5); frame.scale.set(1.1,1,1.1); scene.add( frame ); // Ground var groundTex=THREE.ImageUtils.loadTexture( "textures/checkerboard_noisy.jpg" ); groundTex.wrapS=groundTex.wrapT=THREE.RepeatWrapping; groundTex.repeat=new vec2(25.0,25.0); sim.ground = new THREE.Mesh( new THREE.CubeGeometry(50,50,0.0001), new THREE.MeshLambertMaterial( {color: 0xffccaa, opacity: 1, map:groundTex}) ); sim.ground.position.z=-1; /* hack for radius 1 */ sim.ground.receiveShadow=true; scene.add(sim.ground); // Sun-like spotlight var l=new THREE.SpotLight(); sim.light = l; l.position.set( -50, -100, 100 ); l.castShadow=true; l.shadowCameraNear = 50; l.shadowCameraFar = 500; l.shadowCameraFov = 10; // degrees field of view scene.add(l);
// Change offscreen scene--move the sphere sim.V.sphere.position.x=0.5 + 0.5*Math.sin(lib.time*0.2); sim.V.sphere.position.y=0.5; // +0.5*Math.sin(lib.time); sim.V.sphere.material.setValues({color:0xffffff}); // Shaders read from updated (swapped) texture function updateTex(uniforms) { uniforms["Vtex"]={type:"t", value: sim.V.tex}; uniforms["Ptex"]={type:"t", value: sim.P.tex}; } updateTex(sim.V.uniforms); updateTex(sim.P.uniforms); updateTex(sim.showShader.uniforms); // Render each side sim.V.render(sim.V); sim.P.render(sim.P); // Turn off doSetup for next iterations sim.V.uniforms["doSetup"].value-=0.2; sim.P.uniforms["doSetup"].value-=0.2; // The newly rendered tex will be seen on the cube
// Camera control: // Update camera coordinate system var s=camera; if (!s.X) { // startup: create initial coordinates s.X=new vec3(1,0,0); s.Y=new vec3(0,0,1); s.Z=new vec3(0,-1,-0.2); // camera Z is world Y s.P=new vec3(0,-5,1.3); // initial location } // Move camera via keyboard var move=new vec3(0,0,0); // sums current frame motion // X control via A and D if (lib.key['a']) move.pe(new vec3(-1,0,0)); if (lib.key['d']) move.pe(new vec3(+1,0,0)); // Y control via W and S if (lib.key['w']) move.pe(new vec3(0,0,-1)); if (lib.key['s']) move.pe(new vec3(0,0,+1)); // Z control via Q and Z if (lib.key['q']) move.pe(new vec3(0,+1,0)); if (lib.key['z']) move.pe(new vec3(0,-1,0)); move.te(5.0*lib.dt); // meters/second motion rate s.P.pe(s.X.t(move.x).p(s.Y.t(move.y)).p(s.Z.t(move.z))); // Rotate camera via mouse var speed=0.01; // radians per mouse pixel if (lib.mouseleft) { // move Z with mouse s.Z.pe(s.X.t(-lib.mousedx*speed).p( s.Y.t( lib.mousedy*speed))); } // Keep level: make sure X is horizontal. s.X.z=0.0; s.Y.crossVectors(s.Z,s.X.normalize()); // Orthonormalize s.X.crossVectors(s.Y,s.Z).normalize(); s.Y.crossVectors(s.Z,s.X).normalize(); s.Z.normalize(); // Write coordinate system into matrix s.matrixAutoUpdate=false; // don't trash s.matrixWorldNeedsUpdate=true; // show var m=s.matrix; // the camera's matrix // Utility function: set a matrix column function setCol(m,col,vec) { m.elements[4*col+0]=vec.x; m.elements[4*col+1]=vec.y; m.elements[4*col+2]=vec.z; } setCol(m,0,s.X); setCol(m,1,s.Y); setCol(m,2,s.Z); setCol(m,3,s.P); // position from sim
To save, copy this text into a
plain text
editor like Notepad, and save as HTML.
Startup: 20 ms
Time:
10x
1x
0.1x
0.01x
0.001x
0.0001x