MIT License NuGet

Home / Features / Models and animation

Models, animation and instancing

glTF 2.0 is the only model and animation format. Not the preferred one — the only one. That decision removes an abstraction layer, an import pipeline and a class of bugs where two formats disagree about bind poses.

A single model

Model places a GLB in the world using the engine's uniform placement convention: position is the world position of the anchor, and width, height and depth are the target bounds.

var robot = new Model
{
    Name = "Assets/3DGodotRobot.glb",
    PosX = -2f, PosY = 1f, PosZ = 6f,
    Width = 1f, Height = 1f, Depth = 0.5f,
    Rotation = MathF.PI / 2f
};
AddControl(robot);
Two things trip everyone up once

Width, Height and Depth are target sizes in world units, not multipliers. Setting Width = 1f makes the model one unit wide whatever the file said. And Rotation is in radians around Y — writing 90 gives you 90 radians, which normalises to about 117 degrees and looks almost right, which is the worst kind of bug.

Placement, anchors and the camera covers the anchor maths, including AnchorWorldOffset for the case where you want to pin the model's own origin rather than the centre of its bounds.

Animation

Skinning, morph targets and named clips all come from the file. Clips are addressed by name, and the names are the ones the artist used.

// After Load has completed:
foreach (var name in robot.GetAnimationNames())
    app.AddLog(LogType.Info, name);

robot.PlayAnimation("Walk");

// Or cycle, which is what a debug key usually wants:
robot.SwitchToNextAnimation();

// Advance the clock yourself; Time is the animation clock.
robot.Update(time);
MemberNotes
GetAnimationNames()Clip names, available once the asset has loaded.
PlayAnimation(string)Switches clip. Returns the name that was actually selected.
GetCurrentAnimationName()What is playing now.
SwitchToNextAnimation()Advances through the clip list, wrapping.
TimeThe animation clock, in seconds.
SetModel(name, forceReload)Swaps the underlying GLB, keeping placement.
No blending between clips

Switching a clip is a cut, not a crossfade. There is no state machine, no blend tree and no additive layering. If a character needs to walk and aim at the same time, that is work the engine does not do for you today.

Many models, one control

An InstancedModel is declared once and bound to a list of instance transforms. Skinning and morph targets are per instance, so twenty characters can each play a different clip from one draw. This is the pattern the reference application uses for robots, birds, rocks and the beach.

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

for (var i = 0; i < 10; i++)
{
    var person = new Person
    {
        PosX = -2f, PosY = 1f, PosZ = 6f + i * 3f,
        Width = 1f, Height = 1f, Depth = 0.5f,
        Animation = animations[i]
    };

    robotField.Instances.Add(person);
}

Person derives from MeshInstanceTransform, so the object in your list is the instance. Editing its fields takes effect on the next frame with no copy step, and inserting or removing in the middle of the list preserves order.

Field on MeshInstanceTransformNotes
PosX/Y/Z, Width, Height, Depth, RotationSame semantics as on a single model, relative to the shared template bounds.
AnimationClipClip index for this copy. AnimationClipCount and AnimationNames on the parent tell you the range.
AnimationSpeedPlayback rate. Varying it slightly per copy is what stops a crowd looking mechanical.
AnimationTimeOffsetPhase offset, so copies do not step in unison.
EnableSkips the copy without removing it from the list.
Selected, HighlightPer-instance picking state, so an individual copy can be outlined.
ID, NameIdentity, useful when the list is rebuilt from data.

Procedural geometry

Not everything comes from a file. Mesh3D takes a list of Surface objects — vertices, indices and a material — and behaves like any other control from there. The sea, the beach tiles and the cube fields in the reference application are built this way.

TypeUse
Mesh3DProcedural geometry with a quaternion Rotation, a ColorTint, and ExcludeFromAo for surfaces that should not receive occlusion.
InstancedMesh3DThe same, instanced. No per-instance animation — procedural geometry has no clips.
GLTFAnimationPlayerClip evaluation, if you want to drive a skeleton yourself.
GLTFToolsPulling individual meshes out of one GLB at runtime.
PickMeshThe CPU-side geometry used for surface-accurate picking.

Culling, and why animated bounds are inflated

Frustum culling and light-space shadow culling both run on the CPU against bounding volumes. The subtlety is that a skinned mesh can move outside its own bind-pose bounds, so the bounds used for culling are grown by AnimatedBoundsScale, 1.5 by default.

MemberWhich bounds
LocalBoundsInflated. This is what culling tests, and it is deliberately conservative.
LocalBoundsRawNot inflated. This is what picking tests, because a pick should not hit empty air.
LocalSizeExtent of the raw bounds, which is what you divide by to compute a target scale.
GetWorldBounds()Bounds after the world matrix, for your own spatial queries.
CullingEnabledPer-control opt-out. Useful for anything that legitimately extends past its bounds.

An animation that swings a limb further than 50 per cent past the bind pose will pop at the frustum edge. Raise AnimatedBoundsScale or clear CullingEnabled on that control; both are cheap compared with the alternative of computing exact skinned bounds every frame.

Lights that arrive with the model

A glTF file can carry KHR_lights_punctual lights, and Model surfaces them as ImportedPunctualLights, appending them into the frame's lighting through AppendWorldLights.

Two things to remember: the frame budget is eight lights total, so an asset carrying twelve of them will crowd out your own; and glTF intensities are photometric, which is why Model.LightIntensityScale and RenderQuality.KhrLightIntensityScale exist. See Lighting and shadows.

Known gaps

  • Absent Animation blending and state machines. One clip at a time, switched as a cut.
  • Absent Level of detail. No automatic LOD selection or mesh simplification.
  • Absent Indirect draw and GPU-driven culling. Instances are culled and submitted from the CPU. Roadmap Track A.
  • Absent Occlusion culling. Frustum and shadow-frustum only; a wall does not cull what is behind it.