Skip to main content

Transform Feedback

This tutorial demonstrates animating geometry using transform feedback via the BufferTransform class. A compute shader rotates triangle vertices on the GPU each frame.

caution

Tutorials are maintained on a best-effort basis and may not be fully up to date (contributions welcome).

Transform Feedback

Animation via transform feedback.

It is assumed you've set up your development environment as described in Setup.

Transform feedback allows us to capture vertex shader results from one pass and use them in subsequent passes. It is a powerful tool that can be used to set up massively parrallelized animations or data transformations. Note that transform feedback can only be used with WebGL 2.

Two buffers store the triangle's positions. BufferTransform runs a small vertex shader that multiplies each position by a rotation matrix and writes the result into the alternate buffer. After the transform step the buffers swap and the scene is rendered with the updated positions.

The complete source for this example is shown below:

// luma.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {Buffer} from '@luma.gl/core';
import {AnimationLoopTemplate, AnimationProps, Model, Swap, BufferTransform} from '@luma.gl/engine';

const transformVs = /* glsl */ `\
#version 300 es
#define SIN2 0.03489949
#define COS2 0.99939082

mat2 rotation = mat2(
COS2, SIN2,
-SIN2, COS2
);

in vec2 oldPositions;
out vec2 newPositions;

void main() {
newPositions = rotation * oldPositions;
}
`;

const renderVs = /* glsl */ `\
#version 300 es

in vec2 position;
in vec3 color;
out vec3 vColor;

void main() {
vColor = color;
gl_Position = vec4(position, 0.0, 1.0);
}
`;

const renderFs = /* glsl */ `\
#version 300 es
precision highp float;

in vec3 vColor;
out vec4 fragColor;

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

class AppAnimationLoopTemplate extends AnimationLoopTemplate {
transform: BufferTransform;
model: Model;

positionBuffers: Swap<Buffer>;
colorBuffer: Buffer;

constructor({device, animationLoop}: AnimationProps) {
super();

if (device.type !== 'webgl') {
throw new Error('This demo is only implemented for WebGL2');
}

this.positionBuffers = new Swap({
current: device.createBuffer(new Float32Array([-0.5, -0.5, 0.5, -0.5, 0.0, 0.5])),
next: device.createBuffer(new Float32Array(6))
});

this.colorBuffer = device.createBuffer(
new Float32Array([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0])
);

this.transform = new BufferTransform(device, {
vs: transformVs,
bufferLayout: [{name: 'oldPositions', format: 'float32x2'}],
outputs: ['newPositions'],
vertexCount: 3
});

this.model = new Model(device, {
vs: renderVs,
fs: renderFs,
attributes: {color: this.colorBuffer},
bufferLayout: [
{name: 'position', format: 'float32x2'},
{name: 'color', format: 'float32x3'}
],
vertexCount: 3
});
}

onFinalize() {
this.transform.destroy();
this.model.destroy();
this.positionBuffers.destroy();
this.colorBuffer.destroy();
}

onRender({device}) {
// Run a rotation step
this.transform.run({
inputBuffers: {oldPositions: this.positionBuffers.current},
outputBuffers: {newPositions: this.positionBuffers.next}
});
this.positionBuffers.swap();

// Render with the latest positions
const renderPass = device.beginRenderPass({clearColor: [0, 0, 0, 1]});
this.model.setAttributes({position: this.positionBuffers.current});
this.model.draw(renderPass);
renderPass.end();
}
}

const animationLoop = makeAnimationLoop(AnimationLoopTemplate, {adapters: [webgl2Adapter]})
animationLoop.start();

Keeping the rotation logic in a transform feedback pass avoids CPU involvement and lets the triangle animate smoothly as GPU buffers swap each frame.