WebGL 2 Development with PicoGL.js, Part 4: Transform Feedback

In part 3, we learned how to use uniform buffers and instanced drawing to make our rendering more efficient. Transform feedback is another WebGL 2 feature that targets performance and can significantly improve the performance of animations. In WebGL 1, you would normally have to update object transforms on the CPU, which meant iterating over them serially. With transform feedback, we can capture vertex shader outputs from one frame into a buffer and use them as inputs for the next frame. This allows us to move our animation updates to the GPU, taking advantage of its massive parallelism.

There are a few parts that are new here, so let’s go over them one by one:

We #define a constant, PI, that will help us set up a rotation.

We use PI to create a rotation matrix that rotates our positions by 2 degrees. Doing this in the vertex shader allows us to update the animation on the GPU.

We output the transformed position to the out variable, vPosition, as well as using it to set gl_Position. We’ve used vertex shader out variables before to pass data to the fragment shader, but this one’s being used to capture the transformed positions so they can be used in the next frame.

Lines 1-7: We create two position buffers. positionsA contains our initial positions, positionsB starts out empty and will be updated after we start rendering. On a given frame, one of these buffers will be used as input to the vertex shader, and the other will capture the output. On the following frame, the input and output buffers are swapped.

Lines 9-13: The color buffer isn’t part of the transform feedback, so we only need one.

Lines 15-21: We create a separate vertex array for each position buffer. They share the color buffer.

Lines 23-27: We create two transform feedback objects, one for each position buffer.

Lines 29-33: We create two draw calls, one for each configuration of input and output position buffers. drawCallA will read from positionsA and write vertex shader results to positionsB. drawCallB reads from positionsB and writes to positionsA.

When we refer to vertex shader results, we mean whatever we write to the transform feedback varyings in our vertex shader, i.e. vPosition in this case.

With all those dots connected, running the animation is straightforward:

We reference the draw call to use for the current frame in currentDrawCall. Make sure this starts as drawCallA, since our initial positions are in positionsA. On each frame, we draw using the current draw call, then swap to the other draw call for the next frame. Because of how we set things up, this has the effect of swapping the input and output position buffers. We use the output from the previous frame as input for the current frame, and that allows us to continue the animation. If all went well, you should see the triangle from the top of this post rotating counterclockwise. A live version is available here.

The complete code for this example is available here. If you have any questions, feel free to post them in the comments, visit the PicoGL.js Gitter chat room, or look me up on Twitter.