Skip to content

Instantly share code, notes, and snippets.

@pbeshai
Last active February 11, 2023 11:53
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pbeshai/dbed2fdac94b44d3b4573624a37fa9db to your computer and use it in GitHub Desktop.
Save pbeshai/dbed2fdac94b44d3b4573624a37fa9db to your computer and use it in GitHub Desktop.
Particles Flowing with WebGL and regl - I
license: mit
height: 720
border: no

Particles Flowing with WebGL and regl - I

Example of having particles flow with WebGL and regl. The main idea is to use update state with shaders by writing to framebuffers. In this example, the particles just wander randomly based on their previous position.

Big thanks to Mikola Lysenko, the creator of regl, for explaining the concept of writing to framebuffers from shaders to me after OpenVis Conf 2017!

Note: it appears that currently framebuffers aren't supported by iOS :( If you have any ideas on how to accomplish this on a way that works on phones, I'd love to hear it!

This block was developed with blockup.

function createInitialParticleBuffer(t){var e=regl.texture({data:t,shape:[sqrtNumParticles,sqrtNumParticles,4],type:"float"});return regl.framebuffer({color:e,depth:!1,stencil:!1})}function cycleParticleStates(){var t=prevParticleState;prevParticleState=currParticleState,currParticleState=nextParticleState,nextParticleState=t}var width=window.innerWidth,height=window.innerHeight,pointWidth=3,animationTickLimit=-1;animationTickLimit>=0&&console.log("Limiting to "+animationTickLimit+" ticks");var sqrtNumParticles=256,numParticles=sqrtNumParticles*sqrtNumParticles;console.log("Using "+numParticles+" particles");for(var regl=createREGL({extensions:"OES_texture_float"}),initialParticleState=new Float32Array(4*numParticles),i=0;i<numParticles;++i)initialParticleState[4*i]=2*Math.random()-1,initialParticleState[4*i+1]=2*Math.random()-1;for(var prevParticleState=createInitialParticleBuffer(initialParticleState),currParticleState=createInitialParticleBuffer(initialParticleState),nextParticleState=createInitialParticleBuffer(initialParticleState),particleTextureIndex=[],i$1=0;i$1<sqrtNumParticles;i$1++)for(var j=0;j<sqrtNumParticles;j++)particleTextureIndex.push(i$1/sqrtNumParticles,j/sqrtNumParticles);var updateParticles=regl({framebuffer:function(){return nextParticleState},vert:"\n // set the precision of floating point numbers\n precision mediump float;\n\n // vertex of the triangle\n attribute vec2 position;\n\n // index into the texture state\n varying vec2 particleTextureIndex;\n\n void main() {\n // map bottom left -1,-1 (normalized device coords) to 0,0 (particle texture index)\n // and 1,1 (ndc) to 1,1 (texture)\n particleTextureIndex = 0.5 * (1.0 + position);\n\n gl_Position = vec4(position, 0, 1);\n }\n ",frag:"\n\t// set the precision of floating point numbers\n precision mediump float;\n\n // states to read from to get velocity\n\tuniform sampler2D currParticleState;\n\tuniform sampler2D prevParticleState;\n\n // index into the texture state\n varying vec2 particleTextureIndex;\n\n // seemingly standard 1-liner random function\n // http://stackoverflow.com/questions/4200224/random-noise-functions-for-glsl\n float rand(vec2 co){\n\t return fract(sin(dot(co.xy, vec2(12.9898,78.233))) * 43758.5453);\n\t}\n\n void main() {\n\t\tvec2 currPosition = texture2D(currParticleState, particleTextureIndex).xy;\n\t\tvec2 prevPosition = texture2D(prevParticleState, particleTextureIndex).xy;\n\n\t\tvec2 velocity = currPosition - prevPosition;\n\t\tvec2 random = 0.5 - vec2(rand(currPosition), rand(10.0 * currPosition));\n\n\t\tvec2 position = currPosition + (0.95 * velocity) + (0.0005 * random);\n\n\t\t// we store the new position as the color in this frame buffer\n \tgl_FragColor = vec4(position, 0, 1);\n }\n\t",attributes:{position:[-4,0,4,4,4,-4]},uniforms:{currParticleState:function(){return currParticleState},prevParticleState:function(){return prevParticleState}},count:3}),drawParticles=regl({vert:"\n\t// set the precision of floating point numbers\n precision mediump float;\n\n\tattribute vec2 particleTextureIndex;\n\tuniform sampler2D particleState;\n\n // variables to send to the fragment shader\n varying vec3 fragColor;\n\n // values that are the same for all vertices\n uniform float pointWidth;\n\n\tvoid main() {\n\t\t// read in position from the state texture\n\t\tvec2 position = texture2D(particleState, particleTextureIndex).xy;\n\n\t\t// copy color over to fragment shader\n\t\tfragColor = vec3(abs(particleTextureIndex), 1.0);\n\n\t\t// scale to normalized device coordinates\n\t\t// gl_Position is a special variable that holds the position of a vertex\n gl_Position = vec4(position, 0.0, 1.0);\n\n\t\t// update the size of a particles based on the prop pointWidth\n\t\tgl_PointSize = pointWidth;\n\t}\n\t",frag:"\n // set the precision of floating point numbers\n precision mediump float;\n\n // this value is populated by the vertex shader\n varying vec3 fragColor;\n\n void main() {\n // gl_FragColor is a special variable that holds the color of a pixel\n gl_FragColor = vec4(fragColor, 1);\n }\n ",attributes:{particleTextureIndex:particleTextureIndex},uniforms:{particleState:function(){return currParticleState},pointWidth:pointWidth},count:numParticles,primitive:"points",depth:{enable:!1,mask:!1}}),frameLoop=regl.frame(function(t){var e=t.tick;regl.clear({color:[0,0,0,1],depth:1}),drawParticles(),updateParticles(),cycleParticleStates(),e===animationTickLimit&&(console.log("Hit tick "+e+", canceling animation loop"),frameLoop.cancel())});
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNjcmlwdC5qcyJdLCJuYW1lcyI6WyJjcmVhdGVJbml0aWFsUGFydGljbGVCdWZmZXIiLCJpbml0aWFsUGFydGljbGVTdGF0ZSIsImNvbnN0IiwiaW5pdGlhbFRleHR1cmUiLCJyZWdsIiwidGV4dHVyZSIsImRhdGEiLCJzaGFwZSIsInNxcnROdW1QYXJ0aWNsZXMiLCJ0eXBlIiwiZnJhbWVidWZmZXIiLCJjb2xvciIsImRlcHRoIiwic3RlbmNpbCIsImN5Y2xlUGFydGljbGVTdGF0ZXMiLCJ0bXAiLCJwcmV2UGFydGljbGVTdGF0ZSIsImN1cnJQYXJ0aWNsZVN0YXRlIiwibmV4dFBhcnRpY2xlU3RhdGUiLCJ3aWR0aCIsIndpbmRvdyIsImlubmVyV2lkdGgiLCJoZWlnaHQiLCJpbm5lckhlaWdodCIsInBvaW50V2lkdGgiLCJhbmltYXRpb25UaWNrTGltaXQiLCJjb25zb2xlIiwibG9nIiwibnVtUGFydGljbGVzIiwibGV0IiwiY3JlYXRlUkVHTCIsImV4dGVuc2lvbnMiLCJGbG9hdDMyQXJyYXkiLCJpIiwiTWF0aCIsInJhbmRvbSIsInBhcnRpY2xlVGV4dHVyZUluZGV4IiwiaiIsInB1c2giLCJ1cGRhdGVQYXJ0aWNsZXMiLCJ2ZXJ0IiwiZnJhZyIsImF0dHJpYnV0ZXMiLCJwb3NpdGlvbiIsInVuaWZvcm1zIiwiY291bnQiLCJkcmF3UGFydGljbGVzIiwicGFydGljbGVTdGF0ZSIsInByaW1pdGl2ZSIsImVuYWJsZSIsIm1hc2siLCJmcmFtZUxvb3AiLCJmcmFtZSIsInJlZiIsInRpY2siLCJjbGVhciIsImNhbmNlbCJdLCJtYXBwaW5ncyI6IkFBOEJBLFFBQVNBLDZCQUE0QkMsR0FFcENDLEdBQU1DLEdBQWlCQyxLQUFLQyxTQUMxQkMsS0FBTUwsRUFDTk0sT0FBUUMsaUJBQWtCQSxpQkFBa0IsR0FDNUNDLEtBQU0sU0FJUixPQUFPTCxNQUFLTSxhQUNYQyxNQUFPUixFQUNQUyxPQUFPLEVBQ1BDLFNBQVMsSUFVWCxRQUFTQyx1QkFDUlosR0FBTWEsR0FBTUMsaUJBQ1pBLG1CQUFvQkMsa0JBQ3BCQSxrQkFBb0JDLGtCQUNwQkEsa0JBQW9CSCxFQXhEckJiLEdBQU1pQixPQUFRQyxPQUFPQyxXQUNmQyxPQUFTRixPQUFPRyxZQUNoQkMsV0FBZSxFQUVmQyxvQkFBdUIsQ0FDekJBLHFCQUFzQixHQUN4QkMsUUFBUUMsSUFBSSxlQUFhRixtQkFBRSxTQUc3QnZCLElBQU1NLGtCQUFtQixJQUNuQm9CLGFBQWVwQixpQkFBbUJBLGdCQUN4Q2tCLFNBQVFDLElBQUksU0FBT0MsYUFBRSxhQVlyQixLQUFLQyxHQVRDekIsTUFBTzBCLFlBRVhDLFdBQVksc0JBTVI5QixxQkFBdUIsR0FBSStCLGNBQThCLEVBQWpCSixjQUNyQ0ssRUFBSSxFQUFHQSxFQUFJTCxlQUFnQkssRUFFbkNoQyxxQkFBeUIsRUFBSmdDLEdBQVMsRUFBSUMsS0FBS0MsU0FBVyxFQUNsRGxDLHFCQUF5QixFQUFKZ0MsRUFBUSxHQUFLLEVBQUlDLEtBQUtDLFNBQVcsQ0FvQ3ZELEtBQUtOLEdBZkRiLG1CQUFvQmhCLDRCQUE0QkMsc0JBQ2hEZ0Isa0JBQW9CakIsNEJBQTRCQyxzQkFDaERpQixrQkFBb0JsQiw0QkFBNEJDLHNCQVk5Q21DLHdCQUNHSCxJQUFDLEVBQUlBLElBQUVBLGlCQUFJQSxNQUNuQixJQUFLSixHQUFJUSxHQUFJLEVBQUdBLEVBQUk3QixpQkFBa0I2QixJQUNyQ0QscUJBQXFCRSxLQUFLTCxJQUFDekIsaUJBQW1CNkIsRUFBRzdCLGlCQUtuRE4sSUFBTXFDLGlCQUFrQm5DLE1BRXRCTSxZQUFhLFdBQUEsTUFBQVEsb0JBR2JzQixLQUFNLGtkQW1CUEMsS0FBTSwyL0JBK0JOQyxZQXNGR0MsV0FDRCxFQUFBLEVBQ0EsRUFBQSxFQW5GRyxHQUFHLElBS1BDLFVBRUUzQixrQkFBbUIsV0FBRyxNQUFHQSxvQkFtRjNCRCxrQkFBbUIsV0FBQSxNQUFBQSxxQkE5RW5CNkIsTUFBTyxJQUtIQyxjQUFnQjFDLE1BQ3JCb0MsS0FBTSxvMEJBNkJMQyxLQUFNLGdUQWFQQyxZQUdDTixxQkFBQUEsc0JBR0RRLFVBRUNHLGNBQWUsV0FBRyxNQUFHOUIsb0JBQ3JCTyxXQUFBQSxZQUlEcUIsTUFBT2pCLGFBR1BvQixVQUFXLFNBR1ZwQyxPQUNFcUMsUUFBUSxFQUNSQyxNQUFNLEtBS0pDLFVBQVkvQyxLQUFLZ0QsTUFBTSxTQUFDQyxNQUFFQyxHQUFJRCxFQUFBQyxJQUVuQ2xELE1BQUttRCxPQUVKNUMsT0FBUSxFQUFHLEVBQUcsRUFBRyxHQUNqQkMsTUFBTyxJQUlSa0MsZ0JBR0FQLGtCQUdBekIsc0JBR0l3QyxJQUFTN0IscUJBQ1pDLFFBQVFDLElBQUksWUFBWTJCLEVBQUksOEJBRzVCSCxVQUFVSyIsImZpbGUiOiJzY3JpcHQuanMiLCJzb3VyY2VzQ29udGVudCI6WyJjb25zdCB3aWR0aCA9IHdpbmRvdy5pbm5lcldpZHRoO1xuY29uc3QgaGVpZ2h0ID0gd2luZG93LmlubmVySGVpZ2h0O1xuY29uc3QgcG9pbnRXaWR0aCA9IDM7XG5cbmNvbnN0IGFuaW1hdGlvblRpY2tMaW1pdCA9IC0xOyAvLyAtMSBkaXNhYmxlc1xuaWYgKGFuaW1hdGlvblRpY2tMaW1pdCA+PSAwKSB7XG4gIGNvbnNvbGUubG9nKGBMaW1pdGluZyB0byAke2FuaW1hdGlvblRpY2tMaW1pdH0gdGlja3NgKTtcbn1cblxuY29uc3Qgc3FydE51bVBhcnRpY2xlcyA9IDI1NjtcbmNvbnN0IG51bVBhcnRpY2xlcyA9IHNxcnROdW1QYXJ0aWNsZXMgKiBzcXJ0TnVtUGFydGljbGVzO1xuY29uc29sZS5sb2coYFVzaW5nICR7bnVtUGFydGljbGVzfSBwYXJ0aWNsZXNgKTtcblxuLy8gaW5pdGlhbGl6ZSByZWdsXG5jb25zdCByZWdsID0gY3JlYXRlUkVHTCh7XG5cdC8vIG5lZWQgdGhpcyB0byB1c2UgdGhlIHRleHR1cmVzIGFzIHN0YXRlc1xuICBleHRlbnNpb25zOiAnT0VTX3RleHR1cmVfZmxvYXQnLFxufSk7XG5cblxuLy8gaW5pdGlhbCBwYXJ0aWNsZXMgc3RhdGUgYW5kIHRleHR1cmUgZm9yIGJ1ZmZlclxuLy8gbXVsdGlwbHkgYnkgNCBmb3IgUiBHIEIgQVxuY29uc3QgaW5pdGlhbFBhcnRpY2xlU3RhdGUgPSBuZXcgRmxvYXQzMkFycmF5KG51bVBhcnRpY2xlcyAqIDQpO1xuZm9yIChsZXQgaSA9IDA7IGkgPCBudW1QYXJ0aWNsZXM7ICsraSkge1xuXHQvLyBzdG9yZSB4IHRoZW4geSBhbmQgdGhlbiBsZWF2ZSAyIHNwb3RzIGVtcHR5XG5cdGluaXRpYWxQYXJ0aWNsZVN0YXRlW2kgKiA0XSA9IDIgKiBNYXRoLnJhbmRvbSgpIC0gMTsgLy8geCBwb3NpdGlvblxuXHRpbml0aWFsUGFydGljbGVTdGF0ZVtpICogNCArIDFdID0gMiAqIE1hdGgucmFuZG9tKCkgLSAxOy8vIHkgcG9zaXRpb25cbn1cblxuLy8gY3JlYXRlIGEgcmVnbCBmcmFtZWJ1ZmZlciBob2xkaW5nIHRoZSBpbml0aWFsIHBhcnRpY2xlIHN0YXRlXG5mdW5jdGlvbiBjcmVhdGVJbml0aWFsUGFydGljbGVCdWZmZXIoaW5pdGlhbFBhcnRpY2xlU3RhdGUpIHtcblx0Ly8gY3JlYXRlIGEgdGV4dHVyZSB3aGVyZSBSIGhvbGRzIHBhcnRpY2xlIFggYW5kIEcgaG9sZHMgcGFydGljbGUgWSBwb3NpdGlvblxuXHRjb25zdCBpbml0aWFsVGV4dHVyZSA9IHJlZ2wudGV4dHVyZSh7XG5cdCAgZGF0YTogaW5pdGlhbFBhcnRpY2xlU3RhdGUsXG5cdCAgc2hhcGU6IFtzcXJ0TnVtUGFydGljbGVzLCBzcXJ0TnVtUGFydGljbGVzLCA0XSxcblx0ICB0eXBlOiAnZmxvYXQnXG5cdH0pO1xuXG5cdC8vIGNyZWF0ZSBhIGZyYW1lIGJ1ZmZlciB1c2luZyB0aGUgc3RhdGUgYXMgdGhlIGNvbG9yZWQgdGV4dHVyZVxuXHRyZXR1cm4gcmVnbC5mcmFtZWJ1ZmZlcih7XG5cdFx0Y29sb3I6IGluaXRpYWxUZXh0dXJlLFxuXHRcdGRlcHRoOiBmYWxzZSxcblx0XHRzdGVuY2lsOiBmYWxzZSxcblx0fSk7XG59XG5cbi8vIGluaXRpYWxpemUgcGFydGljbGUgc3RhdGVzXG5sZXQgcHJldlBhcnRpY2xlU3RhdGUgPSBjcmVhdGVJbml0aWFsUGFydGljbGVCdWZmZXIoaW5pdGlhbFBhcnRpY2xlU3RhdGUpO1xubGV0IGN1cnJQYXJ0aWNsZVN0YXRlID0gY3JlYXRlSW5pdGlhbFBhcnRpY2xlQnVmZmVyKGluaXRpYWxQYXJ0aWNsZVN0YXRlKTtcbmxldCBuZXh0UGFydGljbGVTdGF0ZSA9IGNyZWF0ZUluaXRpYWxQYXJ0aWNsZUJ1ZmZlcihpbml0aWFsUGFydGljbGVTdGF0ZSk7XG5cbi8vIGN5Y2xlIHdoaWNoIGJ1ZmZlciBpcyBiZWluZyBwb2ludGVkIHRvIGJ5IHRoZSBzdGF0ZSB2YXJpYWJsZXNcbmZ1bmN0aW9uIGN5Y2xlUGFydGljbGVTdGF0ZXMoKSB7XG5cdGNvbnN0IHRtcCA9IHByZXZQYXJ0aWNsZVN0YXRlO1xuXHRwcmV2UGFydGljbGVTdGF0ZSA9IGN1cnJQYXJ0aWNsZVN0YXRlO1xuXHRjdXJyUGFydGljbGVTdGF0ZSA9IG5leHRQYXJ0aWNsZVN0YXRlO1xuXHRuZXh0UGFydGljbGVTdGF0ZSA9IHRtcDtcbn1cblxuXG4vLyBjcmVhdGUgYXJyYXkgb2YgaW5kaWNlcyBpbnRvIHRoZSBwYXJ0aWNsZSB0ZXh0dXJlIGZvciBlYWNoIHBhcnRpY2xlXG5jb25zdCBwYXJ0aWNsZVRleHR1cmVJbmRleCA9IFtdO1xuZm9yIChsZXQgaSA9IDA7IGkgPCBzcXJ0TnVtUGFydGljbGVzOyBpKyspIHtcblx0Zm9yIChsZXQgaiA9IDA7IGogPCBzcXJ0TnVtUGFydGljbGVzOyBqKyspIHtcblx0XHRwYXJ0aWNsZVRleHR1cmVJbmRleC5wdXNoKGkgLyBzcXJ0TnVtUGFydGljbGVzLCBqIC8gc3FydE51bVBhcnRpY2xlcyk7XG5cdH1cbn1cblxuLy8gcmVnbCBjb21tYW5kIHRoYXQgdXBkYXRlcyBwYXJ0aWNsZXMgc3RhdGUgYmFzZWQgb24gcHJldmlvdXMgdHdvXG5jb25zdCB1cGRhdGVQYXJ0aWNsZXMgPSByZWdsKHtcblx0Ly8gd3JpdGUgdG8gYSBmcmFtZWJ1ZmZlciBpbnN0ZWFkIG9mIHRvIHRoZSBzY3JlZW5cbiAgZnJhbWVidWZmZXI6ICgpID0+IG5leHRQYXJ0aWNsZVN0YXRlLFxuICAvLyBeXl5eXiBpbXBvcnRhbnQgc3R1ZmYuICAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cblxuICB2ZXJ0OiBgXG4gIC8vIHNldCB0aGUgcHJlY2lzaW9uIG9mIGZsb2F0aW5nIHBvaW50IG51bWJlcnNcbiAgcHJlY2lzaW9uIG1lZGl1bXAgZmxvYXQ7XG5cbiAgLy8gdmVydGV4IG9mIHRoZSB0cmlhbmdsZVxuICBhdHRyaWJ1dGUgdmVjMiBwb3NpdGlvbjtcblxuICAvLyBpbmRleCBpbnRvIHRoZSB0ZXh0dXJlIHN0YXRlXG4gIHZhcnlpbmcgdmVjMiBwYXJ0aWNsZVRleHR1cmVJbmRleDtcblxuICB2b2lkIG1haW4oKSB7XG4gICAgLy8gbWFwIGJvdHRvbSBsZWZ0IC0xLC0xIChub3JtYWxpemVkIGRldmljZSBjb29yZHMpIHRvIDAsMCAocGFydGljbGUgdGV4dHVyZSBpbmRleClcbiAgICAvLyBhbmQgMSwxIChuZGMpIHRvIDEsMSAodGV4dHVyZSlcbiAgICBwYXJ0aWNsZVRleHR1cmVJbmRleCA9IDAuNSAqICgxLjAgKyBwb3NpdGlvbik7XG5cbiAgICBnbF9Qb3NpdGlvbiA9IHZlYzQocG9zaXRpb24sIDAsIDEpO1xuICB9XG4gIGAsXG5cblx0ZnJhZzogYFxuXHQvLyBzZXQgdGhlIHByZWNpc2lvbiBvZiBmbG9hdGluZyBwb2ludCBudW1iZXJzXG4gIHByZWNpc2lvbiBtZWRpdW1wIGZsb2F0O1xuXG4gIC8vIHN0YXRlcyB0byByZWFkIGZyb20gdG8gZ2V0IHZlbG9jaXR5XG5cdHVuaWZvcm0gc2FtcGxlcjJEIGN1cnJQYXJ0aWNsZVN0YXRlO1xuXHR1bmlmb3JtIHNhbXBsZXIyRCBwcmV2UGFydGljbGVTdGF0ZTtcblxuICAvLyBpbmRleCBpbnRvIHRoZSB0ZXh0dXJlIHN0YXRlXG4gIHZhcnlpbmcgdmVjMiBwYXJ0aWNsZVRleHR1cmVJbmRleDtcblxuICAvLyBzZWVtaW5nbHkgc3RhbmRhcmQgMS1saW5lciByYW5kb20gZnVuY3Rpb25cbiAgLy8gaHR0cDovL3N0YWNrb3ZlcmZsb3cuY29tL3F1ZXN0aW9ucy80MjAwMjI0L3JhbmRvbS1ub2lzZS1mdW5jdGlvbnMtZm9yLWdsc2xcbiAgZmxvYXQgcmFuZCh2ZWMyIGNvKXtcblx0ICByZXR1cm4gZnJhY3Qoc2luKGRvdChjby54eSwgdmVjMigxMi45ODk4LDc4LjIzMykpKSAqIDQzNzU4LjU0NTMpO1xuXHR9XG5cbiAgdm9pZCBtYWluKCkge1xuXHRcdHZlYzIgY3VyclBvc2l0aW9uID0gdGV4dHVyZTJEKGN1cnJQYXJ0aWNsZVN0YXRlLCBwYXJ0aWNsZVRleHR1cmVJbmRleCkueHk7XG5cdFx0dmVjMiBwcmV2UG9zaXRpb24gPSB0ZXh0dXJlMkQocHJldlBhcnRpY2xlU3RhdGUsIHBhcnRpY2xlVGV4dHVyZUluZGV4KS54eTtcblxuXHRcdHZlYzIgdmVsb2NpdHkgPSBjdXJyUG9zaXRpb24gLSBwcmV2UG9zaXRpb247XG5cdFx0dmVjMiByYW5kb20gPSAwLjUgLSB2ZWMyKHJhbmQoY3VyclBvc2l0aW9uKSwgcmFuZCgxMC4wICogY3VyclBvc2l0aW9uKSk7XG5cblx0XHR2ZWMyIHBvc2l0aW9uID0gY3VyclBvc2l0aW9uICsgKDAuOTUgKiB2ZWxvY2l0eSkgKyAoMC4wMDA1ICogcmFuZG9tKTtcblxuXHRcdC8vIHdlIHN0b3JlIHRoZSBuZXcgcG9zaXRpb24gYXMgdGhlIGNvbG9yIGluIHRoaXMgZnJhbWUgYnVmZmVyXG4gIFx0Z2xfRnJhZ0NvbG9yID0gdmVjNChwb3NpdGlvbiwgMCwgMSk7XG4gIH1cblx0YCxcblxuXHRhdHRyaWJ1dGVzOiB7XG5cdFx0Ly8gYSB0cmlhbmdsZSBiaWcgZW5vdWdoIHRvIGZpbGwgdGhlIHNjcmVlblxuICAgIHBvc2l0aW9uOiBbXG4gICAgICAtNCwgMCxcbiAgICAgIDQsIDQsXG4gICAgICA0LCAtNFxuICAgIF1cbiAgfSxcblxuICAvLyBwYXNzIGluIHByZXZpb3VzIHN0YXRlcyB0byB3b3JrIGZyb21cbiAgdW5pZm9ybXM6IHtcbiAgXHQvLyBtdXN0IHVzZSBhIGZ1bmN0aW9uIHNvIGl0IGdldHMgdXBkYXRlZCBlYWNoIGNhbGxcbiAgICBjdXJyUGFydGljbGVTdGF0ZTogKCkgPT4gY3VyclBhcnRpY2xlU3RhdGUsXG4gICAgcHJldlBhcnRpY2xlU3RhdGU6ICgpID0+IHByZXZQYXJ0aWNsZVN0YXRlLFxuICB9LFxuXG4gIC8vIGl0J3MgYSB0cmlhbmdsZSAtIDMgdmVydGljZXNcbiAgY291bnQ6IDMsXG59KTtcblxuXG4vLyByZWdsIGNvbW1hbmQgdGhhdCBkcmF3cyBwYXJ0aWNsZXMgYXQgdGhlaXIgY3VycmVudCBzdGF0ZVxuY29uc3QgZHJhd1BhcnRpY2xlcyA9IHJlZ2woe1xuXHR2ZXJ0OiBgXG5cdC8vIHNldCB0aGUgcHJlY2lzaW9uIG9mIGZsb2F0aW5nIHBvaW50IG51bWJlcnNcbiAgcHJlY2lzaW9uIG1lZGl1bXAgZmxvYXQ7XG5cblx0YXR0cmlidXRlIHZlYzIgcGFydGljbGVUZXh0dXJlSW5kZXg7XG5cdHVuaWZvcm0gc2FtcGxlcjJEIHBhcnRpY2xlU3RhdGU7XG5cbiAgLy8gdmFyaWFibGVzIHRvIHNlbmQgdG8gdGhlIGZyYWdtZW50IHNoYWRlclxuICB2YXJ5aW5nIHZlYzMgZnJhZ0NvbG9yO1xuXG4gIC8vIHZhbHVlcyB0aGF0IGFyZSB0aGUgc2FtZSBmb3IgYWxsIHZlcnRpY2VzXG4gIHVuaWZvcm0gZmxvYXQgcG9pbnRXaWR0aDtcblxuXHR2b2lkIG1haW4oKSB7XG5cdFx0Ly8gcmVhZCBpbiBwb3NpdGlvbiBmcm9tIHRoZSBzdGF0ZSB0ZXh0dXJlXG5cdFx0dmVjMiBwb3NpdGlvbiA9IHRleHR1cmUyRChwYXJ0aWNsZVN0YXRlLCBwYXJ0aWNsZVRleHR1cmVJbmRleCkueHk7XG5cblx0XHQvLyBjb3B5IGNvbG9yIG92ZXIgdG8gZnJhZ21lbnQgc2hhZGVyXG5cdFx0ZnJhZ0NvbG9yID0gdmVjMyhhYnMocGFydGljbGVUZXh0dXJlSW5kZXgpLCAxLjApO1xuXG5cdFx0Ly8gc2NhbGUgdG8gbm9ybWFsaXplZCBkZXZpY2UgY29vcmRpbmF0ZXNcblx0XHQvLyBnbF9Qb3NpdGlvbiBpcyBhIHNwZWNpYWwgdmFyaWFibGUgdGhhdCBob2xkcyB0aGUgcG9zaXRpb24gb2YgYSB2ZXJ0ZXhcbiAgICBnbF9Qb3NpdGlvbiA9IHZlYzQocG9zaXRpb24sIDAuMCwgMS4wKTtcblxuXHRcdC8vIHVwZGF0ZSB0aGUgc2l6ZSBvZiBhIHBhcnRpY2xlcyBiYXNlZCBvbiB0aGUgcHJvcCBwb2ludFdpZHRoXG5cdFx0Z2xfUG9pbnRTaXplID0gcG9pbnRXaWR0aDtcblx0fVxuXHRgLFxuXG4gIGZyYWc6IGBcbiAgLy8gc2V0IHRoZSBwcmVjaXNpb24gb2YgZmxvYXRpbmcgcG9pbnQgbnVtYmVyc1xuICBwcmVjaXNpb24gbWVkaXVtcCBmbG9hdDtcblxuICAvLyB0aGlzIHZhbHVlIGlzIHBvcHVsYXRlZCBieSB0aGUgdmVydGV4IHNoYWRlclxuICB2YXJ5aW5nIHZlYzMgZnJhZ0NvbG9yO1xuXG4gIHZvaWQgbWFpbigpIHtcbiAgICAvLyBnbF9GcmFnQ29sb3IgaXMgYSBzcGVjaWFsIHZhcmlhYmxlIHRoYXQgaG9sZHMgdGhlIGNvbG9yIG9mIGEgcGl4ZWxcbiAgICBnbF9GcmFnQ29sb3IgPSB2ZWM0KGZyYWdDb2xvciwgMSk7XG4gIH1cbiAgYCxcblxuXHRhdHRyaWJ1dGVzOiB7XG5cdFx0Ly8gZWFjaCBvZiB0aGVzZSBnZXRzIG1hcHBlZCB0byBhIHNpbmdsZSBlbnRyeSBmb3IgZWFjaCBvZiB0aGUgcG9pbnRzLlxuXHRcdC8vIHRoaXMgbWVhbnMgdGhlIHZlcnRleCBzaGFkZXIgd2lsbCByZWNlaXZlIGp1c3QgdGhlIHJlbGV2YW50IHZhbHVlIGZvciBhIGdpdmVuIHBvaW50LlxuXHRcdHBhcnRpY2xlVGV4dHVyZUluZGV4LFxuXHR9LFxuXG5cdHVuaWZvcm1zOiB7XG5cdFx0Ly8gaW1wb3J0YW50IHRvIHVzZSBhIGZ1bmN0aW9uIGhlcmUgc28gaXQgZ2V0cyB0aGUgbmV3IGJ1ZmZlciBlYWNoIHJlbmRlclxuXHRcdHBhcnRpY2xlU3RhdGU6ICgpID0+IGN1cnJQYXJ0aWNsZVN0YXRlLFxuXHRcdHBvaW50V2lkdGgsXG5cdH0sXG5cblx0Ly8gc3BlY2lmeSB0aGUgbnVtYmVyIG9mIHBvaW50cyB0byBkcmF3XG5cdGNvdW50OiBudW1QYXJ0aWNsZXMsXG5cblx0Ly8gc3BlY2lmeSB0aGF0IGVhY2ggdmVydGV4IGlzIGEgcG9pbnQgKG5vdCBwYXJ0IG9mIGEgbWVzaClcblx0cHJpbWl0aXZlOiAncG9pbnRzJyxcblxuICAvLyB3ZSBkb24ndCBjYXJlIGFib3V0IGRlcHRoIGNvbXB1dGF0aW9uc1xuICBkZXB0aDoge1xuICAgIGVuYWJsZTogZmFsc2UsXG4gICAgbWFzazogZmFsc2UsXG4gIH0sXG59KTtcblxuLy8gc3RhcnQgdGhlIGFuaW1hdGlvbiBsb29wXG5jb25zdCBmcmFtZUxvb3AgPSByZWdsLmZyYW1lKCh7IHRpY2sgfSkgPT4ge1xuXHQvLyBjbGVhciB0aGUgYnVmZmVyXG5cdHJlZ2wuY2xlYXIoe1xuXHRcdC8vIGJhY2tncm91bmQgY29sb3IgKGJsYWNrKVxuXHRcdGNvbG9yOiBbMCwgMCwgMCwgMV0sXG5cdFx0ZGVwdGg6IDEsXG5cdH0pO1xuXG5cdC8vIGRyYXcgdGhlIHBvaW50cyB1c2luZyBvdXIgY3JlYXRlZCByZWdsIGZ1bmNcblx0ZHJhd1BhcnRpY2xlcygpO1xuXG5cdC8vIHVwZGF0ZSBwb3NpdGlvbiBvZiBwYXJ0aWNsZXMgaW4gc3RhdGUgYnVmZmVyc1xuXHR1cGRhdGVQYXJ0aWNsZXMoKTtcblxuXHQvLyB1cGRhdGUgcG9pbnRlcnMgZm9yIG5leHQsIGN1cnJlbnQsIGFuZCBwcmV2aW91cyBwYXJ0aWNsZSBzdGF0ZXNcblx0Y3ljbGVQYXJ0aWNsZVN0YXRlcygpO1xuXG5cdC8vIHNpbXBsZSB3YXkgb2Ygc3RvcHBpbmcgdGhlIGFuaW1hdGlvbiBhZnRlciBhIGZldyB0aWNrc1xuXHRpZiAodGljayA9PT0gYW5pbWF0aW9uVGlja0xpbWl0KSB7XG5cdFx0Y29uc29sZS5sb2coYEhpdCB0aWNrICR7dGlja30sIGNhbmNlbGluZyBhbmltYXRpb24gbG9vcGApO1xuXG5cdFx0Ly8gY2FuY2VsIHRoaXMgbG9vcFxuXHRcdGZyYW1lTG9vcC5jYW5jZWwoKTtcblx0fVxufSk7XG4iXX0=
<!DOCTYPE html>
<title>Particles Flowing with WebGL and regl - I</title>
<body>
<script src="https://npmcdn.com/regl@1.3.0/dist/regl.js"></script>
<script src="dist.js"></script>
</body>
const width = window.innerWidth;
const height = window.innerHeight;
const pointWidth = 3;
const animationTickLimit = -1; // -1 disables
if (animationTickLimit >= 0) {
console.log(`Limiting to ${animationTickLimit} ticks`);
}
const sqrtNumParticles = 256;
const numParticles = sqrtNumParticles * sqrtNumParticles;
console.log(`Using ${numParticles} particles`);
// initialize regl
const regl = createREGL({
// need this to use the textures as states
extensions: 'OES_texture_float',
});
// initial particles state and texture for buffer
// multiply by 4 for R G B A
const initialParticleState = new Float32Array(numParticles * 4);
for (let i = 0; i < numParticles; ++i) {
// store x then y and then leave 2 spots empty
initialParticleState[i * 4] = 2 * Math.random() - 1; // x position
initialParticleState[i * 4 + 1] = 2 * Math.random() - 1;// y position
}
// create a regl framebuffer holding the initial particle state
function createInitialParticleBuffer(initialParticleState) {
// create a texture where R holds particle X and G holds particle Y position
const initialTexture = regl.texture({
data: initialParticleState,
shape: [sqrtNumParticles, sqrtNumParticles, 4],
type: 'float'
});
// create a frame buffer using the state as the colored texture
return regl.framebuffer({
color: initialTexture,
depth: false,
stencil: false,
});
}
// initialize particle states
let prevParticleState = createInitialParticleBuffer(initialParticleState);
let currParticleState = createInitialParticleBuffer(initialParticleState);
let nextParticleState = createInitialParticleBuffer(initialParticleState);
// cycle which buffer is being pointed to by the state variables
function cycleParticleStates() {
const tmp = prevParticleState;
prevParticleState = currParticleState;
currParticleState = nextParticleState;
nextParticleState = tmp;
}
// create array of indices into the particle texture for each particle
const particleTextureIndex = [];
for (let i = 0; i < sqrtNumParticles; i++) {
for (let j = 0; j < sqrtNumParticles; j++) {
particleTextureIndex.push(i / sqrtNumParticles, j / sqrtNumParticles);
}
}
// regl command that updates particles state based on previous two
const updateParticles = regl({
// write to a framebuffer instead of to the screen
framebuffer: () => nextParticleState,
// ^^^^^ important stuff. ------------------------------------------
vert: `
// set the precision of floating point numbers
precision mediump float;
// vertex of the triangle
attribute vec2 position;
// index into the texture state
varying vec2 particleTextureIndex;
void main() {
// map bottom left -1,-1 (normalized device coords) to 0,0 (particle texture index)
// and 1,1 (ndc) to 1,1 (texture)
particleTextureIndex = 0.5 * (1.0 + position);
gl_Position = vec4(position, 0, 1);
}
`,
frag: `
// set the precision of floating point numbers
precision mediump float;
// states to read from to get velocity
uniform sampler2D currParticleState;
uniform sampler2D prevParticleState;
// index into the texture state
varying vec2 particleTextureIndex;
// seemingly standard 1-liner random function
// http://stackoverflow.com/questions/4200224/random-noise-functions-for-glsl
float rand(vec2 co){
return fract(sin(dot(co.xy, vec2(12.9898,78.233))) * 43758.5453);
}
void main() {
vec2 currPosition = texture2D(currParticleState, particleTextureIndex).xy;
vec2 prevPosition = texture2D(prevParticleState, particleTextureIndex).xy;
vec2 velocity = currPosition - prevPosition;
vec2 random = 0.5 - vec2(rand(currPosition), rand(10.0 * currPosition));
vec2 position = currPosition + (0.95 * velocity) + (0.0005 * random);
// we store the new position as the color in this frame buffer
gl_FragColor = vec4(position, 0, 1);
}
`,
attributes: {
// a triangle big enough to fill the screen
position: [
-4, 0,
4, 4,
4, -4
]
},
// pass in previous states to work from
uniforms: {
// must use a function so it gets updated each call
currParticleState: () => currParticleState,
prevParticleState: () => prevParticleState,
},
// it's a triangle - 3 vertices
count: 3,
});
// regl command that draws particles at their current state
const drawParticles = regl({
vert: `
// set the precision of floating point numbers
precision mediump float;
attribute vec2 particleTextureIndex;
uniform sampler2D particleState;
// variables to send to the fragment shader
varying vec3 fragColor;
// values that are the same for all vertices
uniform float pointWidth;
void main() {
// read in position from the state texture
vec2 position = texture2D(particleState, particleTextureIndex).xy;
// copy color over to fragment shader
fragColor = vec3(abs(particleTextureIndex), 1.0);
// scale to normalized device coordinates
// gl_Position is a special variable that holds the position of a vertex
gl_Position = vec4(position, 0.0, 1.0);
// update the size of a particles based on the prop pointWidth
gl_PointSize = pointWidth;
}
`,
frag: `
// set the precision of floating point numbers
precision mediump float;
// this value is populated by the vertex shader
varying vec3 fragColor;
void main() {
// gl_FragColor is a special variable that holds the color of a pixel
gl_FragColor = vec4(fragColor, 1);
}
`,
attributes: {
// each of these gets mapped to a single entry for each of the points.
// this means the vertex shader will receive just the relevant value for a given point.
particleTextureIndex,
},
uniforms: {
// important to use a function here so it gets the new buffer each render
particleState: () => currParticleState,
pointWidth,
},
// specify the number of points to draw
count: numParticles,
// specify that each vertex is a point (not part of a mesh)
primitive: 'points',
// we don't care about depth computations
depth: {
enable: false,
mask: false,
},
});
// start the animation loop
const frameLoop = regl.frame(({ tick }) => {
// clear the buffer
regl.clear({
// background color (black)
color: [0, 0, 0, 1],
depth: 1,
});
// draw the points using our created regl func
drawParticles();
// update position of particles in state buffers
updateParticles();
// update pointers for next, current, and previous particle states
cycleParticleStates();
// simple way of stopping the animation after a few ticks
if (tick === animationTickLimit) {
console.log(`Hit tick ${tick}, canceling animation loop`);
// cancel this loop
frameLoop.cancel();
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment