MIT License NuGet

Home / Docs / Instancing

Instancing

One control, one draw, many copies — each with its own transform, and for InstancedModel, its own animation clip, speed and phase. The robots, birds, rocks and beach tiles in the reference application are all one instanced control each.

The pattern

You derive your own type from MeshInstanceTransform, put your own fields on it, and add your objects to Instances. The object in the list is the instance, so editing it takes effect on the next frame with no copy step and no handle to keep in sync.

// Your own fields ride along with the engine's.
internal class Person : MeshInstanceTransform
{
    internal float Yaw { get; set; }
    internal string? Animation { get; set; }
}

robotField = new InstancedModel { ModelName = "Assets/3DGodotRobot.glb" };
AddControl(robotField);

for (var i = 0; i < 10; i++)
{
    robotField.Instances.Add(new Person
    {
        PosX = -2f, PosY = 1f, PosZ = 6f + i * 3f,
        Width = 1f, Height = 1f, Depth = 0.5f,
        Animation = "Idle-loop"
    });
}

Adding and removing at runtime, including in the middle of the list, preserves order. There is no rebuild step and no instance-count ceiling to declare up front.

What an instance carries

FieldNotes
PosX, PosY, PosZWorld position of the instance anchor, which is the centre of the shared template box.
Width, Height, DepthTarget size in world units. Plain float here, not float?0 means unset and is settled to the template size during the host's update.
RotationA Quaternion, pivoting on the anchor. Note the difference from Model.Rotation, which is a float around Y.
EnableSkips the copy without removing it from the list. True by default.
AnimationClipClip index, not a name. 0 is the default clip.
AnimationSpeedPlayback rate, 1 being normal. Varying it slightly per copy is what stops a crowd looking mechanical.
AnimationTimeOffsetPhase offset in seconds, so copies do not step in unison.
Selected, HighlightPer-instance picking state, so one copy out of two hundred can be outlined.
ID, NameIdentity, useful when the list is rebuilt from data.

The animation fields are read by InstancedModel and ignored by InstancedMesh3D, because procedural geometry has no clips.

Clip names, since instances take indices

Instances address clips by index. Names are usually what your own code wants to think in, so the reference application keeps a name on its instance type and converts once per frame. This is the whole conversion, and it is worth copying rather than reinventing:

int ResolveAnimationClip(string? name)
{
    if (string.IsNullOrWhiteSpace(name))
        return 0;

    var names = robotField.AnimationNames;

    for (var i = 0; i < names.Count; i++)
        if (names[i] == name)
            return i;

    return 0;   // unmatched falls back to the default clip
}

// Per frame, per instance:
person.AnimationClip = ResolveAnimationClip(person.Animation);

InstancedModel.AnimationNames is populated after the GLB loads, so before that the loop finds nothing and every copy plays clip 0. A null, empty or unmatched name falls back to the default clip rather than throwing, which is the behaviour you want while an asset is still loading.

Template bounds and the anchor

The geometry is shared, so the bounds are too. Everything on Mesh3DBase has a template-scoped counterpart here.

MemberMeaning
TemplateLocalBoundsInflated by AnimatedBoundsScale for animated models. What culling tests.
TemplateLocalBoundsRawNot inflated. What anchor and scale computation uses.
TemplateLocalSizeFull size of the raw template box.
TemplateAnchorLocalCentre of the raw template box — the same centre-anchor convention as single models.
BuildInstanceMatrix(instance)The world matrix for one copy, built exactly like a single model's.
InstanceAnchorWorldOffset(instance)The conversion to use when you want to pin the template's origin instead of its centre.
GetInstanceWorldBoundsRaw(instance)World bounds of one copy, matching the rendered body.
TryPickInstance(...), TryPickInstanceSurface(...)Box-level and surface-level ray tests, both reporting which MeshInstanceTransform was hit.
Setting Highlight on the host cascades

Assigning a whole new Highlight object to the instanced control copies it into every instance that exists at that moment. Nested assignment — Highlight = { Style = ... } — does not cascade, because no assignment to the property happens. Instances added later keep their own default and are resolved as "inherit the host" by ObjectPicker, which is what makes a panel that grows its instance list frame by frame still highlight correctly.

Culling, and what instancing does not buy you

Each instance gets a bounding-sphere frustum test, and the whole batch is skipped only when every copy is invisible. The shadow pass does not frustum-cull at all; it is gated by CastShadows instead.

  • Absent Indirect draw and GPU-driven culling. Culling and submission are CPU-side. Tens of thousands of instances is a CPU cost you can measure.
  • Absent Level of detail. Every copy draws the full mesh at every distance.
  • Absent Transparent sorting for instanced controls. ITransparentSortable exists only on Mesh3DBase, so instanced batches are not depth-sorted against transparent single objects.