MIT License NuGet

Home / Docs / Models and animation

Models and animation

glTF 2.0 is the only model format the engine reads. A Model is a control like any other: you give it a file name and a target size, add it to a panel, and update it every frame.

Loading one

robot = new Model
{
    Name = "Assets/3DGodotRobot.glb",
    PosX = -2f, PosY = 1f, PosZ = 6f,
    Width = 1f, Height = 1f, Depth = 0.5f,
    Rotation = MathF.PI / 2f,
    Highlight = { Style = HighlightStyle.Wireframe }
};
AddControl(robot);

AddControl queues the load. Nothing about lighting, shadows, ambient occlusion or the sky appears in that code because those are pipeline concerns, not per-object ones — a model that has been added is lit and shadowed by whatever the frame is doing.

Sizes are metres, rotation is radians

Width = 1f makes the model one unit wide regardless of what the artist exported, because the engine derives the scale from the asset's own bounds. Rotation on Model is a float in radians around Y — writing 90 gives you 90 radians, which normalises to roughly 117 degrees and looks almost right. See Placement, anchors and the camera.

The file has to be in both MauiAsset and Content item groups, or it will load on the desktop and silently fail in a packaged build. Project layout explains why.

Playing a clip

Clips come from the file and are addressed by the names the artist used. Everything below returns null or an empty list until the control is Ready, so read the clip list after loading rather than in the constructor.

// Advancing the clock is what plays the animation.
robot.Update(time);

// Once Ready:
foreach (var name in robot.GetAnimationNames())
    DeviceServices.BaseApp?.AddLog(LogType.Info, name);

robot.PlayAnimation("Walk");          // returns the name actually activated
robot.SwitchToNextAnimation();        // wraps; what a debug key usually wants
var playing = robot.GetCurrentAnimationName();
MemberNotes
GetAnimationNames()Clip names from animations[].name. Empty list when the asset has none.
GetAnimations()Same list with durations attached, as ModelAnimationInfo.
PlayAnimation(string)Switches clip and returns the name that actually became active, or null.
SwitchToNextAnimation()Advances through the list, wrapping.
GetCurrentAnimationName()What is playing now.
TimeThe animation clock in seconds, if you want to scrub it yourself.
Model has its own Update overload

Update(float time, string? name = null, float? alpha = null). It forwards to the base control update, swaps the asset when name is non-null, and — the part that matters — pushes time into the skinning path when the control is Ready and has content. A model you never update draws correctly and stands perfectly still.

Switching clips is a cut

There is no crossfade, no blend tree, no additive layering and no state machine. A character that needs to walk and aim at the same time is work you do yourself, or work that waits for the roadmap.

Swapping the asset

SetModel(name, forceReload = false) keeps the control and its placement and replaces the geometry: it clears Ready, marks the control changed, and re-queues the load. It returns false and does nothing when the name is unchanged and forceReload is false.

// Same position, size and rotation; different geometry.
robot.SetModel("Assets/3DGodotRobotAlt.glb");

// Equivalent, inside your update:
robot.Update(time, name: "Assets/3DGodotRobotAlt.glb");

Between the call and the next Ready the control draws nothing, which is the same rule as first load. If a swap must not flash, keep two controls and cross their Alpha.

Material overrides

A handful of one-shot overrides exist for the case where you want to tint or dull an imported material without editing the file. They are consumed by the next update and reset themselves to null, so setting one is a request, not a state you have to remember to clear.

OverrideEffect
MetallicOverrideMetallic factor, 0 to 1.
RoughnessOverrideRoughness factor, 0 to 1.
EmissiveFactorOverrideEmissive colour as RGB intensity.
LightIntensityScaleNot an override — a persistent scale on lights the file carries. See below.

Lights that arrive inside the file

glTF can carry KHR_lights_punctual lights. Model exposes them as ImportedPunctualLights and AppendWorldLights(ref SceneLightParams) folds them into the frame.

Two consequences worth knowing before you import an interior scene. The frame budget is eight punctual lights in total, so an asset carrying twelve of them crowds out your own. And glTF intensities are photometric, which is why Model.LightIntensityScale and RenderQuality.KhrLightIntensityScale exist — the latter defaults to 0.05, because the raw candela values are enormous next to the engine's own intensity convention.

Bounds, culling and picking

A skinned mesh can swing outside its bind-pose bounds, so the box used for culling is grown by RenderQuality.AnimatedBoundsScale, 1.5 by default. That leaves two boxes, and picking a wrong one is a visible bug rather than a subtle one.

UseRead
Frustum and shadow cullingLocalBounds / GetWorldBounds() — inflated, deliberately conservative.
Picking, selection boxes, layout snappingLocalBoundsRaw / GetWorldBoundsRaw() — matches the rendered body.
Computing your own scaleLocalSize, the extent of the raw box.
Opting out entirelyCullingEnabled = false, for anything that legitimately extends past its bounds.

An animation that throws a limb more than 50 per cent past the bind pose will pop at the frustum edge; raise AnimatedBoundsScale or clear CullingEnabled on that one control. Both are cheap next to computing exact skinned bounds every frame, which is the alternative nobody wants to pay for.

When it is not a file

Mesh3D takes a list of Surface objects — vertices, indices, a material — and behaves like a Model from that point on, including the same placement convention and the same shadow and GI participation. The sea, the beach tiles and the cube fields in the reference application are all built this way. Its Rotation is a quaternion rather than a float.

See Models, animation and instancing for the full type list, including GLTFTools for pulling one mesh out of a shared GLB and PickMesh for the CPU-side geometry behind surface-accurate picking.