WebGL 2 Development with PicoGL.js

Tarek Sherif

BioDigital

WebGL 2 is a BIG Update

  • New shading language
  • Vertex array objects
  • Uniform buffer objects
  • Instanced drawing
  • Transform feedback
  • Multiple render targets
  • Query objects
  • etc, etc, etc...

How to get started?

PicoGL.js

  • Just simplify management of the rendering layer
    • No math, scene graph, physics, etc.
  • User manipulates GL constructs
    • Programs, VAOs, UBOs, transform feedbacks, queries, etc.
  • PicoGL.js just provides a friendly API, helps with layout and works around implementation bugs

PicoGL.js 101

                
  // App manages global GL state
  var app = PicoGL.createApp(canvas)
  .clearColor(0.0, 0.0, 0.0, 1.0);
  
  // Program, VBO, VAO
  var program = app.createProgram(vertexShaderSource, fragmentShaderSource);
  var positions = app.createVertexBuffer(PicoGL.FLOAT, 2, positionData);
  var vertexArray = app.createVertexArray()
  .vertexAttributeBuffer(0, positions);

  // Draw call manages per-draw state
  var drawCall = app.createDrawCall(program, vertexArray);

  app.clear();
  drawCall.draw();
              
            

Vertex Array Objects

  • In WebGL 1, to manage geometry for each frame you have to:
    • Bind your buffers
    • Set up the vertex attribute pointer
    • Enable the attribute pointer
  • In WebGL 2, you bind your buffers and set up pointers on a vertex array object (VAO)
    • Then for each frame, you just bind the VAO

Vertex Array Objects

In raw WebGL 2:
          
  gl.bindVertexArray(vertexArray);
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(0);
  
  gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
  gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(1);  
  gl.bindVertexArray(null);
  
  function draw() {
      gl.bindVertexArray(vertexArray);
      gl.drawArrays(gl.TRIANGLES, 0, numVertices);
  
      requestAnimationFrame(draw);
  }
          
        

Vertex Array Objects

In PicoGL.js:
          
  var vertexArray = app.createVertexArray()
  .vertexAttributeBuffer(0, positionBuffer)
  .vertexAttributeBuffer(1, normalBuffer);
  
  app.createDrawCall(program, vertexArray);
  
  function draw() {
      drawCall.draw()
  
      requestAnimationFrame(draw);
  }
          
        

Vertex Array Objects

Why do you care?
  • Reduces CPU overhead per draw call
  • Generally simplifies code

Uniform Buffer Objects

  • In WebGL 1, you have to update each uniform individually by sending data to the GPU.
  • In WebGL 2, you can store all your uniform values in a uniform buffer object and then just bind it to update all uniform values at once.
    • WARNING: To do this manually, you have to align uniform variables in buffer memory according to the STD140 layout!

Uniform Buffer Objects

In raw WebGL 2:
                
  var uniformsLocation = gl.getUniformBlockIndex(program, "myUniforms");
  gl.uniformBlockBinding(program, uniformsLocation, 0);

  // HAVE TO KNOW STD140 LAYOUT!!!
  var uboData = new Float32Array(24);
  uboData.set(viewProjMatrix);
  uboData.set(eyePosition, 16);
  uboData.set(lightPosition, 20);

  var uniformBuffer = gl.createBuffer();
  gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, uniformBuffer);
  gl.bufferData(gl.UNIFORM_BUFFER, uboData, gl.STATIC_DRAW);
              
            

Uniform Buffer Objects

In PicoGL.js:
                
  // PicoGL.js handles layout.
  var uniformBuffer = app.createUniformBuffer([
      PicoGL.FLOAT_MAT4,
      PicoGL.FLOAT_VEC4,
      PicoGL.FLOAT_VEC4
  ])
  .set(0, viewProjMatrix)
  .set(1, eyePosition)
  .set(2, lightPosition)
  .update();

  drawCall.uniformBlock("myUniforms", uniformBuffer);
              
            

Uniform Buffer Objects

Why do you care?
  • Can significantly reduce overhead for updating uniforms

Transform Feedback

  • Capture output from the vertex shader to use as input for the next frame
  • Not available in WebGL 1
    • (Can be emulated by writing vertex shader output to float textures)

Transform Feedback

In raw WebGL 2:
                
  var program = gl.createProgram();
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.transformFeedbackVaryings(program, ["vPosition"], gl.SEPARATE_ATTRIBS);
  gl.linkProgram(program);

  var positionBufferA = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBufferA);
  gl.bufferData(gl.ARRAY_BUFFER, positionData, gl.STREAM_COPY);

  var positionBufferB = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBufferB);
  gl.bufferData(gl.ARRAY_BUFFER, positionData.byteLength, gl.STREAM_COPY);


  gl.bindBuffer(gl.ARRAY_BUFFER, positionBufferA);
  gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(0);

  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, positionBufferB);

  gl.beginTransformFeedback(gl.TRIANGLES);
  gl.drawArrays(gl.TRIANGLES, 0, numVertices);
  gl.endTransformFeedback();
              
            

Transform Feedback

In PicoGl.js:
                
  var program = app.createProgram(vertexShader, fragmentShader, ["vPosition"]);
  
  var positionBufferA = app.createVertexBuffer(PicoGL.FLOAT, 3, positionData);
  var positionBufferB = app.createVertexBuffer(PicoGL.FLOAT, 3, positionData.length);
  
  var vertexArray = app.createVertexArray()
  .vertexAttributeBuffer(0, positionBufferA)
  
  var transformFeedback = app.createTransformFeedback()
  .feedbackBuffer(0, positionBufferB);

  var drawCall = app.createDrawCall(program, vertexArray)
  .transformFeedback(transformFeedback);

  drawCall.draw();
              
            

Transform feedback

Why do you care?

Instanced Drawing

  • It's common to draw the same geometry multiple times while changing uniform values
    • E.g. transform, color
  • In WebGL 1, you have to iterate over each object and draw it individually
  • In WebGL 2, you can draw objects that share geometry in a single draw call

Instanced Drawing

In raw WebGL 2:
                
  var positionBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.bufferData(gl.ARRAY_BUFFER,  positionData, gl.STATIC_DRAW);
  gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(0);
  
  var colorBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
  gl.bufferData(gl.ARRAY_BUFFER,  colorData, gl.STATIC_DRAW);
  gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(1);
  gl.vertexAttribDivisor(1, 1);
  
  gl.drawArraysInstanced(gl.TRIANGLES, 0, numVertices, numInstances);
              
            

Instanced Drawing

In PicoGl.js:
                
  var positionBuffer = app.createVertexBuffer(PicoGL.FLOAT, 3, positionData);
  var colorBuffer = app.createVertexBuffer(PicoGL.FLOAT, 3, colorData);
  
  var vertexArray = app.createVertexArray()
  .vertexAttributeBuffer(0, positionBuffer)
  .instanceAttributeBuffer(1, colorBuffer);

  var drawCall = app.createDrawCall(program, vertexArray);
  drawCall.draw();
              
            

Instanced Drawing

Why do you care?

Multiple Render Targets

  • In WebGL 1, you have to do a separate draw call for every framebuffer target you want to draw to
  • In WebGL 2, you can bind multiple targets to a single framebuffer and draw to all of them in one draw call

Multiple Render Targets

In raw WebGL 2:
                
  var framebuffer = gl.createFramebuffer();
  gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
  
  var target1 = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, target1);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, gl.drawingBufferWidth, gl.drawingBufferHeight);
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, target1, 0);
  
  var target2 = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, target2);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, gl.drawingBufferWidth, gl.drawingBufferHeight);
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.TEXTURE_2D, target2, 0);
  
  gl.drawBuffers([
      gl.COLOR_ATTACHMENT0,
      gl.COLOR_ATTACHMENT1
  ]);
              
            

Multiple Render Targets

In PicoGL.js:
                
  var framebuffer = app.createFramebuffer()
  .colorTarget(0)
  .colorTarget(1);
              
            

Multiple Render Targets

Why do you care?

Other Examples

Thanks!

Some Resources: