flutter3d
Showcase Changelog 38 packages API reference

A face per copy

since 0.5.2 Animation

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.