Many copies in one draw
Open the live demo · Read the source · View on GitHub
Repeating one mesh with ordinary nodes costs a draw call per copy. An
InstancedMeshNode stores many transforms and colours in one vertex buffer, so
the renderer can submit the whole field together.
Step 1: Allocate the batch #
The node owns one capsule mesh, one material, and enough room for 64 instance records. Capacity is explicit, which keeps a known-size field from reallocating while it is filled.
_field = InstancedMeshNode(
DeviceMesh.upload(
context.device,
const CapsuleShape(
radius: 0.28,
height: 0.55,
segments: 20,
rings: 6,
).build(),
),
Material(
name: 'instance material',
baseColor: Vector4(1.0, 1.0, 1.0, 1.0),
roughness: 0.48,
),
capacity: instanceCount,
name: 'capsule field',
);
Step 2: Add transforms and colours #
Each record contains a transform in the batch's local space and an RGBA colour. The colour multiplies the mesh's vertex colour, letting copies differ without separate materials.
const int side = 8;
for (var i = 0; i < instanceCount; i++) {
final int x = i % side;
final int z = i ~/ side;
final double height = 0.7 + 0.3 * math.sin(x * 0.9 + z * 0.65);
final Matrix4 transform = Matrix4.identity()
..setTranslationRaw((x - 3.5) * 1.35, height, (z - 3.5) * 1.35)
..rotateY((x + z) * 0.24)
..scaleByDouble(1.0, 0.85 + height * 0.2, 1.0, 1.0);
_field.addInstance(
transform,
color: Vector4(
0.3 + x / 12.0,
0.32 + z / 13.0,
0.78 - (x + z) / 36.0,
1.0,
),
);
}
Step 3: Add one node to the scene #
The scene receives the batch rather than 64 mesh nodes. Culling treats its bounds as the union of every instance, while the scene renderer encodes one instanced draw for the visible field.
final Scene scene = Scene()
..ambientColor = Vector3(0.45, 0.52, 0.68)
..ambientIntensity = 0.13
..add(_field)
..add(
LightNode(name: 'sun', intensity: 3.2)
..setLocalForward(Vector3(-0.5, -0.8, -0.32)),
);
Step 4: Move the whole field #
Instance transforms stay unchanged while the node rotates. Moving the batch therefore changes one node transform instead of rewriting all 64 records.
_angle = (_angle + dt * rotationSpeed) % (math.pi * 2.0);
_field.setRotationYawPitchRoll(_angle, 0.0, 0.0);
Step 5: Read the frame count #
FrameResult.instances counts each copy even though they share a draw. The
page checks that all 64 records are present and reached the rendered frame.
if (_field.count != instanceCount ||
_field.capacity < instanceCount ||
frame.instances < instanceCount ||
frame.drawCalls < 1) {
throw StateError('the instanced field was not drawn');
}