A face per copy
Open the live demo · Read the source · View on GitHub
An ordinary morph weight lives on the node, so one draw of one mesh always
shows one shape. A batch is one draw of many copies, and the ordinary weight
would give every copy in it the same shape too. InstancedMeshNode .setMorphWeights is the way out: each copy gets its own weights, read from a
texture by instance id rather than from a uniform.
Step 1: A batch that can grow #
InstancedMeshNode takes a mesh, a material and how many copies it can hold.
Each copy still shares the one delta texture the mesh was packed with — that
part is exactly what an ordinary morph target already is — but from here each
copy can be told how much of it to wear.
final InstancedMeshNode faces = InstancedMeshNode(
DeviceMesh.upload(context.device, withBump),
Material(
name: 'faces',
baseColor: Vector4(0.8, 0.8, 0.85, 1.0),
doubleSided: true,
),
capacity: _count,
name: 'faces',
);
for (var i = 0; i < _count; i++) {
faces.addInstance(
Matrix4.translation(Vector3((i - (_count - 1) / 2) * 1.1, 0.0, 0.0)),
);
}
if (deltaTexture != null) {
faces.morph = MorphState(
texture: deltaTexture,
targetCount: packed.targetCount,
reaches: packed.reaches,
);
}
_faces = faces;
Step 2: A weight for every copy #
Each frame writes a different weight into each instance's own slot. The weights do not sit in the instance record next to the transform: they ride in a second texture, rebuilt only when a weight actually changed.
final InstancedMeshNode? faces = _faces;
if (faces == null) return;
for (var i = 0; i < _count; i++) {
final double weight = (i / (_count - 1)) * spread;
faces.setMorphWeights(i, <double>[weight]);
}
Step 3: Watch a shared draw wear six faces #
The batch from step 1 is still one draw call, whatever each instance's own weight says.
final InstancedMeshNode faces = InstancedMeshNode(
DeviceMesh.upload(context.device, withBump),
Material(
name: 'faces',
baseColor: Vector4(0.8, 0.8, 0.85, 1.0),
doubleSided: true,
),
capacity: _count,
name: 'faces',
);
for (var i = 0; i < _count; i++) {
faces.addInstance(
Matrix4.translation(Vector3((i - (_count - 1) / 2) * 1.1, 0.0, 0.0)),
);
}
if (deltaTexture != null) {
faces.morph = MorphState(
texture: deltaTexture,
targetCount: packed.targetCount,
reaches: packed.reaches,
);
}
_faces = faces;
Drag Spread from nought. At nought every face sits flat: the deltas are
there, but every instance's own weight is nought. As Spread rises, the face
at one end stays flat while the one at the other end grows its full bump, and
the ones between it show every step in between, each read from its own row of
the weights texture rather than from the mesh's shared uniform. All six still
come from the one draw call frame.instances reports.