2011+12+04

Hello FBO Simulation

It’s been some time but finally I’ve gotten around to put GLOW to a more serious test. For long I’ve been interested in making particle simulations using floating point textures. This tutorial should give you a good overview on how to do this in GLOW (and in principle to implement in any other WebGL framework).

This tutorial is very much based on an example that you can see here. I’m not 100% satisfied with the looks just yet, but thought the techniques used could inspire someone to do something amazing. As usual it’s a very good idea to view the source (which is quite thoroughly commented)

If you haven’t used GLOW before it could be a good idea to get basics first or you’re just like me – just pretend you know it and dive right in.

First we use the brand new GLOW.Load to load some data and shaders…

new GLOW.Load( {

  animal: "animals.js",
  depthShader: "Depth.glsl",
  particleSimulationShader: "ParticleSimulation.glsl",
  particleRenderShader: "ParticleRender.glsl",
  depthToScreenShader: "DepthToScreen.glsl",
  onLoadComplete: function( loadedData ) {

    ...init everything here...

  }
} );

As you can see in the example code, the first thing we do is to parse out animation frames and whatnot but it’s really out of the scope for this tutorial. I think the comments in the code and the code itself should be fairly simple to follow so I’ll let you look there for how to use morph targets to create an animation.

Now let’s focus on the central part of this tutorial – the particle simulation using shaders and frame buffer objects (FBOs). To do this, we have to have one FBO and two shaders:

  • The particle data FBO – stores data for each particle. One pixel per particle.
  • The particle simulation shader – updates the particle data in the FBO
  • The particle render shader – renders the particle using the data in the FBO

As you probably know, an FBO is very much like a texture just that you can render to it – or write to it using shaders. This means we can use an FBO as a chunk of memory, one pixel per particle. But if we were to use an ordinary FBO we’d be limited to 4 unsigned bytes (RGBA) per particle. This isn’t even close to good enough if you want to do some serious calculations. Fortunately there’s a way to fix this, we can use a WebGL extension called OES_texture_float which you enable through…

if( !context.enableExtension( "OES_texture_float" )) {
  alert( "No support for float textures!" );
  return;
}

This extension should be implemented in the latest version of all major browsers that support WebGL and gives you 4 floats per pixel/particle. If you need more than 4 floats per particle, you have to use multiple FBOs and shaders and is out of scope for this tutorial.

Another thing that’s important to have is vertex shader textures – the ability to sample textures in the vertex shader. GLOW has support to check if this is supported through…

if( !context.maxVertexTextureImageUnits()) {
  alert( "No support for vertex shader textures!" );
  return;
}

If you don’t have the ability to sample textures in the vertex shader you can’t simulate changes in positions or any other thing you have to do in the particle render shader’s vertex shader. I think the latest version of all major browsers support this but you have to check it.

Before we go about and create the FBO we need to generate some data. First it’s important to know that we’re using points (as opposed to triangles) to update the FBO. The particle simulation’s vertex and fragment shader needs to run exactly once per pixel in the FBO. This leads us to one of the most central piece of data needed – the aSimulationDataXY. This is basically the position of each pixel in the FBO. In theory it ranges from -1 -> 1. In reality, it looks like this…

// i ranges from 0 -> FBO.width * FBO.height

aSimulationPoints.push( i );

var x = ( i % FBO.width ) / FBO.width;
var y = Math.floor( i / FBO.width ) / FBO.height;

aSimulationDataXYs.push( x * 2 - 1 + 1 / FBO.width  );  // X
aSimulationDataXYs.push( y * 2 - 1 + 1 / FBO.height );  // Y

As you see, we need to offset one pixel to the right and down because of how gl_Position is scaled into the viewport. You might need to experiment a bit to get this correct for your FBO size. Also, make sure it’s working both in ANGLE and OpenGL.

Now let’s look at the particle simulation shader. A very simplified vertex- and fragment shader that uses points and the aSimulationDataXY might look like (thanks @pyalot for the gl_FragCoord trick)…

// vertex shader
attribute  vec2  aSimulationDataXYs;

void main(void) {
  gl_Position = vec4( aSimulationDataXYUVs.x,
                      aSimulationDataXYUVs.y,
                      1.0, 1.0 );
}

// fragment shader
uniform  sampler2D  uParticlesFBO;
uniform  vec2       uViewportSize;

void main( void ) {
  vec4 particleData = texture2D( uParticlesFBO,
                                 gl_FragCoord.xy / uViewportSize );
  particleData.x += 0.1;  // time
  particleData.y += 0.2;  // rotation
  particleData.z += 0.3;  // size
  particleData.w += 0.4;  // color
  gl_FragColor = particleData;
}

In this example we use x for time and so on… and simply increase these. Naturally you want to do more complex calculations here and you can let the different values represent whatever you want. Remember that you can read data from multiple FBOs or from offseted positions in the FBO to create very complex simulations. In my example I sample a depth FBO to see if a particle is inside or outside the volume of the animal and update the particle FBO accordingly. I also read out the luminosity, which is also stored in the depth FBO, to affect the particle’s color.

The next step is to sample the particle FBO in the particle render vertex shader to use these values for some interesting result. It might look something like this…

// vertex shader
uniform  sampler2D  uParticlesFBO;

attribute  vec2  aParticleUVs;
attribute  vec2  aParticleSpacePositions;

varying  vec3  vColor;

void main(void)
{
  vec4 particleData = texture2D( uParticlesFBO, aParticleUVs );
  vec4 particlePosition;

  ...do something with the particle data...

  gl_Position = uPerspectiveMatrix *
                uViewMatrix * vec4( particlePosition, 1.0 );
}

//# ParticleRenderFragment

varying vec3 vColor;

void main( void ) {
    gl_FragColor = vec4( vColor, 1.0 );
}

As you can see we’re using a sampler in the vertex shader. The attribute aParticleUVs is very similar to the aSimulationXY above, but ranging from 0 -> 1…

// i ranges from 0 -> FBO.width * FBO.height

var u = ( i % FBO.width ) / FBO.width;
var v = Math.floor( i / FBO.width ) / FBO.height;

aParticleUVs.push( u );
aParticleUVs.push( v );

There’s been a lot of talk about the FBO, now this is how you create it…

particlesFBO = new GLOW.FBO( { width: 128,
                               height: 128,
                               type: GL.FLOAT,
                               magFilter: GL.NEAREST,
                               minFilter: GL.NEAREST,
                               depth: false,
                               data: new Float32Array( data ) } );

Width and height is self-explanatory but the rest needs some explanation…

  • type: This is where we tell that each element (RGBA) should be FLOAT instead of unsigned byte.
  • magFilter: to avoid ”leaking” between pixels in the FBO, this needs to be set to NEAREST.
  • minFilter: same as magFilter.
  • depth: the FBO shouldn’t have a depth buffer, which is enabled by default. The FBO shouldn’t have a stencil buffer either but stencils are disabled by default.
  • data: this is the initial data to be uploaded to the FBO. 4 floats per pixel.

You create the shaders as usual, using the particle FBO as a texture. I think that all we need to setup, let’s look at the update loop…

context.enableDepthTest( false );
particlesFBO.bind();
particleSimulationShader.draw();
particlesFBO.unbind();
context.enableDepthTest( true );
particleRenderShaders.draw();

First we disable depth test (just in case) then we bind the particles FBO and draw the particle simulation shader, which updates all the pixels in the particles FBO. We unbind the particles FBO, enables depth test again and draw the particle render shader.

Of course the sky is the limit when it comes to FBO simulations – you don’t have to do particles. Just look at what the crazy dudes at Miaumiau Interactive pulled off – completely amazing!

Hope you got a tiny bit inspired!