Home / Docs / 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.
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.
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.
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();
| Member | Notes |
|---|---|
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. |
Time | The 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.
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.
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.
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.
| Override | Effect |
|---|---|
MetallicOverride | Metallic factor, 0 to 1. |
RoughnessOverride | Roughness factor, 0 to 1. |
EmissiveFactorOverride | Emissive colour as RGB intensity. |
LightIntensityScale | Not an override — a persistent scale on lights the file carries. See below. |
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.
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.
| Use | Read |
|---|---|
| Frustum and shadow culling | LocalBounds / GetWorldBounds() — inflated, deliberately conservative. |
| Picking, selection boxes, layout snapping | LocalBoundsRaw / GetWorldBoundsRaw() — matches the rendered body. |
| Computing your own scale | LocalSize, the extent of the raw box. |
| Opting out entirely | CullingEnabled = 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.
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.