### Implementing a Particle System with WebGL2 and Transform Feedback Robert Muth (http://robert.muth.org) #### NYC WebGL Developers Meetup #### 2017/07/12
#### About Me * Ph.D. CS. University of Arizona (Compilers) * Day job: Google (Storage Infrastructure) * Noteworthy past projects: * [PNaCl](https://blog.chromium.org/2017/05/goodbye-pnacl-hello-webassembly.html) (Google) * [Pin](https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool) (Intel) * WebGL Hobbyist
#### Me and WebGL * Got into 3D Graphics via [Eric Haines' Class at Udacity](https://www.udacity.com/course/interactive-3d-graphics--cs291) * Maintain Dart based WebGL2 engine [ChronosGL](https://github.com/ChronosTeam/ChronosGL) * Like to experiment with procedural generated animations * My pieces [art.muth.org](http://art.muth.org) * Pieces by others [arscalculanda.com](http://www.arscalculanda.com/about.html)
#### WebGL2 * Spec Finalized 2017/01 * Documentation is sparse but similar to OpenGL ES 3.0 (2012) * Support * Firefox since 51 * Chrome since 56 * Safari [behind flag](https://webkit.org/status/#) * Edge [under consideration](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/webgl20/) * Even available on (Android) mobile browsers * Useful: [WebGL Report](http://webglreport.com) and [WebGL Stats](https://webglstats.com)
### WebGL2 Features In This Presentation * Shader language Version 3.00 es * Vertex Array Objects (VAO) * Transform Feedback * (Point Sprites)
## [Particle Demo](http://chronosteam.github.io/ChronosGL/Examples/transform_feedback/transform_feedback.html) #### (Inspired by [Helios](http://art.muth.org/helios.html)) [Dart code on github](http://github.com/ChronosTeam/ChronosGL/tree/master/example/transform_feedback)
#### Demo Take-Away * Orbiting camera * Lots of particles (ions) flying around * Emitted from 5 sources (poles) * Absorbed by 5 sinks (poles) * Random initial direction * Inspired by electrical fields * Looks nice but bogus physics * Much higher particle count with GPU * while maintaining high FPS
## CPU-Side Implementation

Simulation Time-Step For Single Particle [Dart]

          
Vector3 Update(Vector3 pos, double dt,
               List<Vector3> sources, List<Vector3> sinks) {
    Vector3 force = new Vector3(0.0, 0.0, 0.0);

    for (Vector3 pole in sources) { // pushing forces
        Vector3 t = pos - pole;
         final double dist = t.length;
         if (dist <= kMinDistance) continue;
         if (dist > kMaxDistance) {
             return sources[RandInt(sources.length)] + RandVector3(0.35);
         }
         force += t / (dist * dist);
    }

    for (Vector3 pole in sinks) {  // pulling forces
        Vector3 t = pole - pos;
        final double dist = t.length;
        if (dist <= kMinDistance) {
            return sources[RandInt(sources.length)] + RandVector3(0.35);
        }
        force += t / (dist * dist);
    }

    return pos + force.normalized() * dt;  // bogus physics
}
        
        
### Point Sprites * Ideal for particles * No triangles: each object defined by single vec3 * No orientation - always face camera * Predefined Vertex Shader outputs: * ``gl_Position [vec3]`` : determines location * ``gl_PointSize [float]`` : determines size * Predefined Fragment Shader input: * ``gl_PointCoord [vec2]`` : provides texture UVs

Point Sprites Texture Generation


	    
          var canvas =  document.createElement('canvas');
          canvas.width = 64;
          canvas.height = 64;
          var cx = canvas.width / 2;
          var cy = canvas.height / 2;
          var ctx = canvas.getContext("2d");

          var gradient1 = ctx.createRadialGradient(cx, cy, 1, cx, cy, 22);
          gradient1.addColorStop(0, 'gray');
          gradient1.addColorStop(1, 'black');
          ctx.fillStyle = gradient1;
          ctx.fillRect(0, 0, canvas.width, canvas.height);
          // see top image for intermediate result

          var gradient2 = ctx.createRadialGradient(cx, cy, 1, cx, cy, 6);
          gradient2.addColorStop(0, 'white');
          gradient2.addColorStop(1, 'gray');
          ctx.globalAlpha = 0.9;
          ctx.fillStyle = gradient2;
          ctx.arc(cx, cy, 4, 0, 2 * 3.145);
          ctx.fill();
          // see bottom image for final result
            
          

Vertex Shader

	   #version 300 es
precision highp float;

layout (location=0) invec3 aPosition;

uniform mat4 uPerspectiveViewMatrix;
uniform float uPointSize;

void main(void) {
    gl_Position = uPerspectiveViewMatrix * vec4(aPosition, 1.0);
    gl_PointSize = uPointSize / gl_Position.z;
}
	   
         
Simplifiction of   Perspective • View • Model   approach

Fragment Shader

          
#version 300 es
precision highp float;

out vec4 oFragColor;

uniform sampler2D uTexture;

void main(void) {
    oFragColor = texture(uTexture, gl_PointCoord);
}
          
        
#### Animation Loop * Update each particle position * Collect all positions in ArrayBuffer * Copy ArrayBuffer to attribute buffer (CPU → GPU) * Standard call: ``gl.drawArrays(gl.POINTS, ...)``

Aside: Shaders in ChronosGL [Dart]

            
const String aPosition = "aPosition";
const String uPointSize = "uPointSize";
...

const String vertexShader = """
void main() {
    gl_Position = ${uPerspectiveViewMatrix} * vec4(${aPosition}, 1.0);
    gl_PointSize = ${uPointSize} / gl_Position.z;
}
""";
            
          
  • Canonical name for attributes, uniforms, varyings, transforms
  • Canonical name maps to canonical type
  • Shader prologs are auto-generated
  • String interpolation enables IDE support and error checking
## GPU-Side Implementation

Traditional WebGL Shader Configuration

New Shader Configuration in WebGL 2


vertex shader state can be exposed

Computation Only Variant


skip fragment shader entirely
### Transform-Feedback Limitations * Must emit exactly one record for each input * Not a full blown geometry shader

Approach Used For Demo

explicit GPU → GPU copy (fast)
### New Animation Loop * Standard call: ``gl.drawArrays(gl.POINTS, ...)``: * Updates screen * Computes particle positions for next round * Copy transform output to attribute input (GPU → GPU)
### Ping-Pong Technique Avoids Copy ##### (Probably not worth it for this example) * odd frames * attribute input: buffer A * transform output: buffer B * even frames * attribute input: buffer B * transform output: buffer A

Fragment Shader (unchanged)


#version 300 es
precision highp float;

out vec4 oFragColor;

uniform sampler2D uTexture;

void main(void) {
    oFragColor = texture(uTexture, gl_PointCoord);
}

        

New Vertex Shader 1/3

	    
#version 300 es
precision highp float;

layout (location=0) in vec3 aPosition;

out vec3 tPosition;

uniform mat4 uPerspectiveViewMatrix;
uniform float uPointSize;
uniform vec3 uSinks[5];
uniform vec3 uSources[5];

const float kMaxDistance = 100.1;
const float kMinDistance = 0.2;
const float dt = 0.06;

void main() {
    gl_Position = uPerspectiveViewMatrix * vec4(aPosition, 1.0);
    gl_PointSize = uPointSize / gl_Position.z;

    tPosition = Update(aPosition, gl_Position.xyz); // for next round
}
	    
          

New Vertex Shader 2/3

	    
vec3 Update(vec3 pos, vec3 seed) {
    vec3 force = vec3(0.0, 0.0, 0.0);
    for (int i = 0; i < uSources.length(); ++i) {
       vec3 d = pos - uSources[i];
       float l = length(d);
       if (l <= kMinDistance) continue;
       if (l >= kMaxDistance) {
           return RandomSource(seed) + vec3rand(seed) * 0.35;
       }
       force += d / (l * l);
    }

    for (int i = 0; i < uSinks.length(); ++i) {
       vec3 d = uSinks[i] - pos;
       float l = length(d);
       if (l <= kMinDistance) {
          return RandomSource(seed) + vec3rand(seed) * 0.35;
       }
       force += d / (l * l);
    }
    return pos + normalize(force) * dt;
}

	    
	  

New Vertex Shader 3/3

	    
// [0.0, 1.0]
float rand(vec2 seed) {
    return fract(sin(dot(seed, vec2(12.9898, 78.233))) * 43758.5453);
}

// [0, n[
int irand(int n, vec2 seed) {
    return int(floor(rand(seed) * float(n)));
}

vec3 vec3rand(vec3 seed) {
    return vec3(rand(seed.yz) - 0.5, rand(seed.xz) - 0.5, rand(seed.xy) - 0.5);
}

vec3 RandomSource(vec3 seed) {
    return uSources[irand(uSources.length(), seed.xy * seed.z)];
}

	    
	  
Randomness in shaders is a bit painful
# Implementation Details

Compiling and Linking the Shaders [Dart]

          
          Shader CompileShader(int type, String code) {
              Shader out = gl.createShader(type);
              gl.shaderSource(out, code);
              gl.compileShader(out);
              return out;
          }

          Program LinkProgram(String vertCode, String fragCode, List<String> transforms) {
              Program out = gl.createProgram();
              gl.attachShader(program, CompileShader(gl, gl.VERTEX_SHADER, vertCode);
              gl.attachShader(program, CompileShader(gl, gl.FRAGMENT_SHADER, fragCode);
              if (transforms.length > 0) {
                  gl.transformFeedbackVaryings(program, transforms, gl.SEPARATE_ATTRIBS);
              }
              gl.linkProgram(program);
              return out;
          }
          
        
Order of transforms matters!

Traditional WebGL Draw Setup

            
          gl.useProgram(program);

          for uni in uniforms:          // UBO can simplify this
            gl.uniform<uni-type>(<uni-location>, <uni-value>);

          for att in attributes:
            gl.enableVertexAttribArray(<att-location>);

          for att in attributes:
            gl.bindBuffer(gl.ARRAY_BUFFER, <att-buffer>);
            gl.vertexAttribPointer(<att-location>, ...)

          gl.drawArrays(gl.POINTS, ...);

          for att in attributes:
            gl.disableVertexAttribArray(<att-location>);
            
          

VAO Initialization + New Draw Setup

            
          var vao = gl.createVertexArray();
          gl.bindVertexArray(vao);

          for att in attributes:
             gl.enableVertexAttribArray(<att-location>);

          for att in attributes:
              gl.bindBuffer(gl.ARRAY_BUFFER, <att-buffer>);
              gl.vertexAttribPointer(<att-location>, ...)

          gl.bindVertexArray(null);
            
            
          gl.useProgram(program);

          for uni in uniforms:          // UBO can simplify this
              gl.uniform<uni-type>(<uni-location>, <uni-value>);

          gl.bindVertexArray(vao);

          gl.drawArrays(gl.POINTS, ...);

            
          

Transform Feedback Initialization + Draw Call Changes

            
          var tf = gl.createTransformFeedback();
          gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
          // 0. 1, ... match gl.transformFeedbackVaryings arg
          gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, <buffer 0>);
          gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, <buffer 1>);
            
            
          gl.useProgram(program);

          gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
          gl.beginTransformFeedback(gl.POINTS);

          for uni in uniforms:          // UBO can simplify this
              gl.uniform<uni-type>(<uni-location>, <uni-value>);

          gl.bindVertexArray(vao);

          gl.drawArrays(gl.POINTS, ...);

          gl.endTransformFeedback();

            
          
#### WebGL2 Frameworks * [Wikipedia List of WebGL Frameworks](https://en.wikipedia.org/wiki/List_of_WebGL_frameworks) * [Damien Seguin's List of WebGL Frameworks](https://gist.github.com/dmnsgn/76878ba6903cf15789b712464875cfdc) * [PicoGL.js](https://tsherif.github.io/picogl.js) pure WebGL2 * [three.js](https://threejs.org) * [babylon.JS](https://www.babylonjs.com) * [ChronosGL](https://github.com/ChronosTeam/ChronosGL) pure WebGL2, Dart
#### References * [WebGL 2.0 Quick Reference Guide](https://www.khronos.org/files/webgl20-reference-guide.pdf) * [OpenGL ES 3.0 Spec](https://www.khronos.org/registry/OpenGL/specs/es/3.0/es_spec_3.0.pdf) * [GLSL ES 3.0 Spec](https://www.khronos.org/registry/OpenGL/specs/es/3.0/GLSL_ES_Specification_3.00.pdf) * [WebGL2Fundamentals](https://webgl2fundamentals.org) * [WebGL2Samples](http://webglsamples.org/WebGL2Samples) * [WeblGLAcademy](http://www.webglacademy.com/)
#### Demos + Tutorials * [WebGL2Fundamentals](https://webgl2fundamentals.org) * [WebGL2Samples](http://webglsamples.org/WebGL2Samples) * [WeblGLAcademy](http://www.webglacademy.com/) (WebGL1)