luma.gl/docs/tutorials/instanced-transform.mdx
2023-05-29 07:13:17 -04:00

405 lines
10 KiB
Plaintext

import {DeviceTabs} from '@site/src/react-luma';
import {InstancedTransformExample} from '@site';
# Instanced Transform
<DeviceTabs />
<InstancedTransformExample />
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:
```typescript
import {AnimationLoop, Model, Transform, CubeGeometry} from '@luma.gl/engine';
import {Buffer, Texture2D, clear, setParameters, isWebGL2} 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:
```typescript
const transformVs = `
attribute float rotations;
varying 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](https://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle), passing the axis and rotation angle as instanced attributes (with the rotation angles being updated by transform feedback):
```typescript
const vs = `\
attribute vec3 positions;
attribute vec3 normals;
attribute vec2 texCoords;
attribute vec2 offsets;
attribute vec3 axes;
attribute float rotations;
uniform mat4 uView;
uniform mat4 uProjection;
varying vec3 vPosition;
varying vec3 vNormal;
varying 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 attribute to position each cube.
Our fragment shader doesn't change at all:
```typescript
const fs = `\
precision highp float;
uniform sampler2D uTexture;
uniform vec3 uEyePosition;
varying vec3 vPosition;
varying vec3 vNormal;
varying 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:
```typescript
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&pi;.
The `Transform` is straightforward to set up, simply taking the `rotationBuffer` and our vertex shader as input:
```typescript
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 attributes and `instanceCount`:
```typescript
const model = new Model(device, {
vs,
fs,
geometry: new CubeGeometry(),
attributes: {
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`:
```typescript
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.
```typescript
import {AnimationLoop, Model, Transform, CubeGeometry} from '@luma.gl/engine';
import {Buffer, Texture2D, clear, setParameters, isWebGL2} from '@luma.gl/webgl';
import {phongLighting} from '@luma.gl/shadertools';
import {Matrix4} from '@math.gl/core';
const transformVs = `
attribute float rotations;
varying float vRotation;
void main() {
vRotation = rotations + 0.01;
}
`;
const vs = `\
attribute vec3 positions;
attribute vec3 normals;
attribute vec2 texCoords;
attribute vec2 offsets;
attribute vec3 axes;
attribute float rotations;
uniform mat4 uView;
uniform mat4 uProjection;
varying vec3 vPosition;
varying vec3 vNormal;
varying 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;
varying vec3 vPosition;
varying vec3 vNormal;
varying 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(),
attributes: {
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();
```