Hello Instancing
This tutorial illustrates instanced drawing with luma.gl using both WGSL and GLSL shaders and also shows how a custom luma.gl shader module can be used to pass colors.
Tutorials are maintained on a best-effort basis and may not be fully up to date (contributions welcome).
Hello Instancing
It is assumed you've set up your development environment as described in Setup.
Instancing lets the GPU draw many copies of the same geometry with a single draw call. When rendering many similar geometries this can be significant speedups.
In this sample, each triangle shares the same vertex data while per-instance
attributes provide unique colors and offsets. A small color shader module passes
those colors from the vertex shader to the fragment shader.
The code below creates a Model with per-instance attributes to draw four colored
triangles inside an AnimationLoopTemplate.
A luma.gl shader module essentially contains some shader code fragments that will be inserted into our vertex and fragment shaders. Shader modules usually define shader functions that implement generic functionality that can be reused in different shader programs.
In this case, we're defining a minimal shader module to pass a color from the vertex shader to the fragment shader. This module also demonstrates a common convention in luma.gl to prefix function and variable names in a shader module with the name of the module to avoid name collisions.
Now let's update our vertex and fragment shaders to use the shader module functions:
import {Buffer} from '@luma.gl/core';
import {AnimationLoopTemplate, AnimationProps, Model} from '@luma.gl/engine';
import {webgl2Adapter} from '@luma.gl/webgl';
import {webgpuAdapter} from '@luma.gl/webgpu';
const colorShaderModule = {
name: 'color',
source: /* wgsl */ `
// TODO: wgsl not yet implemented
// struct ColorVaryings {
// color: vec3<f32>,
// };
// @vertex
// fn color_setColor(color: vec3f, input: ColorVaryings) -> ColorVaryings {
// var output = input;
// output.color = color;
// return output;
// }
// @fragment
// fn color_getColor(input: ColorVaryings) -> vec4<f32> {
// return vec4<f32>(input.color, 1.0);
// }
`,
vs: /* glsl */ `
out vec3 color_vColor;
void color_setColor(vec3 color) {
color_vColor = color;
}
`,
fs: /* glsl */ `
in vec3 color_vColor;
vec3 color_getColor() {
return color_vColor;
}
`
};
const source = /* wgsl */ `
struct VertexInputs {
@location(0) position: vec2<f32>,
@location(1) instanceColor: vec3<f32>,
@location(2) instanceOffset: vec2<f32>,
};
struct FragmentInputs {
@builtin(position) Position: vec4<f32>,
@location(0) color: vec3<f32>,
}
@vertex
fn vertexMain(inputs: VertexInputs) -> FragmentInputs {
var outputs: FragmentInputs;
outputs.color = inputs.instanceColor;
outputs.Position = vec4<f32>(inputs.position + inputs.instanceOffset, 0.0, 1.0);
return outputs;
}
@fragment
fn fragmentMain(inputs: FragmentInputs) -> @location(0) vec4<f32> {
return vec4<f32>(inputs.color, 1.0);
}
`;
const vs = /* glsl */ `
#version 300 es
in vec2 position;
in vec3 instanceColor;
in vec2 instanceOffset;
void main() {
color_setColor(instanceColor);
gl_Position = vec4(position + instanceOffset, 0.0, 1.0);
}
`;
const fs = /* glsl */ `
#version 300 es
out vec4 fragColor;
void main() {
fragColor = vec4(color_getColor(), 1.0);
}
`;
class AppAnimationLoopTemplate extends AnimationLoopTemplate {
model: Model;
positionBuffer: Buffer;
colorBuffer: Buffer;
offsetBuffer: Buffer;
constructor({device}: AnimationProps) {
super();
this.positionBuffer = device.createBuffer(new Float32Array([-0.2, -0.2, 0.2, -0.2, 0.0, 0.2]));
this.colorBuffer = device.createBuffer(
new Float32Array([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0])
);
this.offsetBuffer = device.createBuffer(
new Float32Array([0.5, 0.5, -0.5, 0.5, 0.5, -0.5, -0.5, -0.5])
);
this.model = new Model(device, {
source,
vs,
fs,
modules: [colorShaderModule],
bufferLayout: [
{name: 'position', format: 'float32x2'},
{name: 'instanceColor', format: 'float32x3', stepMode: 'instance'},
{name: 'instanceOffset', format: 'float32x2', stepMode: 'instance'}
],
attributes: {
position: this.positionBuffer,
instanceColor: this.colorBuffer,
instanceOffset: this.offsetBuffer
},
vertexCount: 3,
instanceCount: 4,
parameters: {
depthWriteEnabled: true,
depthCompare: 'less-equal'
}
});
}
onFinalize() {
this.model.destroy();
this.positionBuffer.destroy();
this.colorBuffer.destroy();
this.offsetBuffer.destroy();
}
onRender({device}: AnimationProps) {
const renderPass = device.beginRenderPass({clearColor: [0, 0, 0, 1]});
this.model.draw(renderPass);
renderPass.end();
}
}
const animationLoop = makeAnimationLoop(AnimationLoopTemplate, {adapters: [webgpuAdapter, webgl2Adapter]})
animationLoop.start();
Note that geometry vertex positions are stored once in one separate GPU buffer, while two additional buffers supply
per-instance colors and offsets. The buffer layout marks those attributes with stepMode: 'instance' so they advance once per triangle. A single draw call then renders four instances in different screen quadrants.
Running the application will draw four colored triangles using instancing.