Skip to main content

Instanced Transform

info

The tutorial pages have not yet been updated for luma.gl v9.

  • TransformFeedback based examples are temporarily disabled until Transform class is ported to luma.gl v9

Note: Transform examples temporarily disabled

In this final tutorial, we'll pull together almost everything we've learned in past tutorials into a single scene: lighing, textures, geometry, shader modules, instancing and transform feedback. Whew! This will build on the previous tutorial, so it might be helpful to start with its source code.

We'll be drawing 4 instanced cubes, textured and lit in the same way as in the lighting tutorial, but with the animations updated by transform feedback.

The Transform class is the only addition we need for our imports:

import {AnimationLoop, Model, Transform, CubeGeometry} from '@luma.gl/engine';
import {Buffer, Texture2D, clear, setParameters} from '@luma.gl/webgl';
import {phongLighting} from '@luma.gl/shadertools';
import {Matrix4} from '@math.gl/core';

The vertex shader for our transform feedback is quite simple. It just increments a scalar rotation value on each run:

const transformVs = `
in float rotations;

out float vRotation;

void main() {
vRotation = rotations + 0.01;
}
`;

In order to handle rotation updates in the transform feedback, we have to move construction of the rotation matrix into the vertex shader. We'll use an axis-angle, passing the axis and rotation angle as instanced ins (with the rotation angles being updated by transform feedback):

const vs = `\
in vec3 positions;
in vec3 normals;
in vec2 texCoords;
in vec2 offsets;
in vec3 axes;
in float rotations;

uniform mat4 uView;
uniform mat4 uProjection;

out vec3 vPosition;
out vec3 vNormal;
out vec2 vUV;

void main(void) {
float s = sin(rotations);
float c = cos(rotations);
float t = 1.0 - c;
float xt = axes.x * t;
float yt = axes.y * t;
float zt = axes.z * t;
float xs = axes.x * s;
float ys = axes.y * s;
float zs = axes.z * s;

mat3 rotationMat = mat3(
axes.x * xt + c,
axes.y * xt + zs,
axes.z * xt - ys,
axes.x * yt - zs,
axes.y * yt + c,
axes.z * yt + xs,
axes.x * zt + ys,
axes.y * zt - xs,
axes.z * zt + c
);

vPosition = rotationMat * positions;
vPosition.xy += offsets;
vNormal = rotationMat * normals;
vUV = texCoords;
gl_Position = uProjection * uView * vec4(vPosition, 1.0);
}

We also pass an offsets instanced in to position each cube.

Our fragment shader doesn't change at all:

const fs = `\
precision highp float;

uniform sampler2D uTexture;
uniform vec3 uEyePosition;

in vec3 vPosition;
in vec3 vNormal;
in vec2 vUV;

void main(void) {
vec3 materialColor = texture2D(uTexture, vec2(vUV.x, 1.0 - vUV.y)).rgb;
vec3 surfaceColor = lighting_getLightColor(materialColor, uEyePosition, vPosition, normalize(vNormal));

gl_FragColor = vec4(surfaceColor, 1.0);
}
`;

Our onInitialize method will need several updates. First we create buffers for our instanced data:

const offsetBuffer = device.createBuffer(new Float32Array([3, 3, -3, 3, 3, -3, -3, -3]));

const axisBufferData = new Float32Array(12);
for (let i = 0; i < 4; ++i) {
const vi = i * 3;
const x = Math.random();
const y = Math.random();
const z = Math.random();
const l = Math.sqrt(x * x + y * y + z * z);

axisBufferData[vi] = x / l;
axisBufferData[vi + 1] = y / l;
axisBufferData[vi + 2] = z / l;
}
const axisBuffer = device.createBuffer(axisBufferData);

const rotationBuffer = device.createBuffer(
new Float32Array([
Math.random() * Math.PI * 2,
Math.random() * Math.PI * 2,
Math.random() * Math.PI * 2,
Math.random() * Math.PI * 2
])
);

The offsetBuffer sets positions so the cubes will be in a square formation. The axisBuffer looks more complicated, but its simply a set of 4 normalized vectors about which we'll rotate our cubes. Finally, the rotationBuffer simply starts with 4 random angles between 0 and 2π.

The Transform is straightforward to set up, simply taking the rotationBuffer and our vertex shader as input:

const transform = new Transform(device, {
vs: transformVs,
sourceBuffers: {
rotations: rotationBuffer
},
feedbackMap: {
rotations: 'vRotation'
},
elementCount: 4
});

And the Model needs to be updated to take the instanced ins and instanceCount:

const model = new Model(device, {
vs,
fs,
geometry: new CubeGeometry(),
ins: {
offsets: [offsetBuffer, {divisor: 1}],
axes: [axisBuffer, {divisor: 1}],
rotations: [rotationBuffer, {divisor: 1}]
},
uniforms: {
uTexture: texture,
uEyePosition: eyePosition,
uView: viewMatrix
},
modules: [phongLighting],
moduleSettings: {
material: {
specularColor: [255, 255, 255]
},
lights: [
{
type: 'ambient',
color: [255, 255, 255]
},
{
type: 'point',
color: [255, 255, 255],
position: [4, 8, 4]
}
]
},
instanceCount: 4
});

Our onRender needs an update to perform the transform feedback and pass the transformed rotation buffer to the Model:

  override onRender({device, aspect, model, transform, projectionMatrix}) {
projectionMatrix.perspective({fovy: Math.PI / 3, aspect});

transform.run();

clear(device, {color: [0, 0, 0, 1], depth: true});
model
.setAttributes({rotations: [transform.getBuffer('vRotation'), {divisor: 1}]})
.setUniforms({uProjection: projectionMatrix})
.draw();

transform.swap();
}

If all went well, you should see 4 rotating cubes. This scene is significantly more complex than anything we've seen before, so take some time to play around with it and get to know the various parts.

import {AnimationLoop, Model, Transform, CubeGeometry} from '@luma.gl/engine';
import {Buffer, Texture2D, clear, setParameters} from '@luma.gl/webgl';
import {phongLighting} from '@luma.gl/shadertools';
import {Matrix4} from '@math.gl/core';

const transformVs = `
in float rotations;

out float vRotation;

void main() {
vRotation = rotations + 0.01;
}
`;

const vs = `\
in vec3 positions;
in vec3 normals;
in vec2 texCoords;
in vec2 offsets;
in vec3 axes;
in float rotations;

uniform mat4 uView;
uniform mat4 uProjection;

out vec3 vPosition;
out vec3 vNormal;
out vec2 vUV;

void main(void) {
float s = sin(rotations);
float c = cos(rotations);
float t = 1.0 - c;
float xt = axes.x * t;
float yt = axes.y * t;
float zt = axes.z * t;
float xs = axes.x * s;
float ys = axes.y * s;
float zs = axes.z * s;

mat3 rotationMat = mat3(
axes.x * xt + c,
axes.y * xt + zs,
axes.z * xt - ys,
axes.x * yt - zs,
axes.y * yt + c,
axes.z * yt + xs,
axes.x * zt + ys,
axes.y * zt - xs,
axes.z * zt + c
);

vPosition = rotationMat * positions;
vPosition.xy += offsets;
vNormal = rotationMat * normals;
vUV = texCoords;
gl_Position = uProjection * uView * vec4(vPosition, 1.0);
}
`;

const fs = `\
precision highp float;

uniform sampler2D uTexture;
uniform vec3 uEyePosition;

in vec3 vPosition;
in vec3 vNormal;
in vec2 vUV;

void main(void) {
vec3 materialColor = texture2D(uTexture, vec2(vUV.x, 1.0 - vUV.y)).rgb;
vec3 surfaceColor = lighting_getLightColor(materialColor, uEyePosition, vPosition, normalize(vNormal));

gl_FragColor = vec4(surfaceColor, 1.0);
}
`;

const loop = new AnimationLoop({
override onInitialize({gl}) {
setParameters(gl, {
depthTest: true,
depthFunc: gl.LEQUAL
});

const offsetBuffer = device.createBuffer(new Float32Array([3, 3, -3, 3, 3, -3, -3, -3]));

const axisBufferData = new Float32Array(12);
for (let i = 0; i < 4; ++i) {
const vi = i * 3;
const x = Math.random();
const y = Math.random();
const z = Math.random();
const l = Math.sqrt(x * x + y * y + z * z);

axisBufferData[vi] = x / l;
axisBufferData[vi + 1] = y / l;
axisBufferData[vi + 2] = z / l;
}
const axisBuffer = device.createBuffer(axisBufferData);

const rotationBuffer = device.createBuffer(
new Float32Array([
Math.random() * Math.PI * 2,
Math.random() * Math.PI * 2,
Math.random() * Math.PI * 2,
Math.random() * Math.PI * 2
])
);

const texture = device.createTexture({data: 'vis-logo.png'});

const eyePosition = [0, 0, 10];
const viewMatrix = new Matrix4().lookAt({eye: eyePosition});
const projectionMatrix = new Matrix4();

const transform = new Transform(device, {
vs: transformVs,
sourceBuffers: {
rotations: rotationBuffer
},
feedbackMap: {
rotations: 'vRotation'
},
elementCount: 4
});

const model = new Model(device, {
vs,
fs,
geometry: new CubeGeometry(),
ins: {
offsets: [offsetBuffer, {divisor: 1}],
axes: [axisBuffer, {divisor: 1}],
rotations: [rotationBuffer, {divisor: 1}]
},
uniforms: {
uTexture: texture,
uEyePosition: eyePosition,
uView: viewMatrix
},
modules: [phongLighting],
moduleSettings: {
material: {
specularColor: [255, 255, 255]
},
lights: [
{
type: 'ambient',
color: [255, 255, 255]
},
{
type: 'point',
color: [255, 255, 255],
position: [4, 8, 4]
}
]
},
instanceCount: 4
});

return {
model,
transform,
projectionMatrix
};
},

override onRender({device, aspect, model, transform, projectionMatrix}) {
projectionMatrix.perspective({fovy: Math.PI / 3, aspect});

transform.run();

clear(device, {color: [0, 0, 0, 1], depth: true});
model
.setAttributes({rotations: [transform.getBuffer('vRotation'), {divisor: 1}]})
.setUniforms({uProjection: projectionMatrix})
.draw();

transform.swap();
}
});

loop.start();