Recursive AI

Animate a glTF character in C#

onegame is a free, open-source game engine for .NET from Recursive AI. MIT or Apache-2.0, no editor required.

Play a named glTF animation clip. Use figure.glb from the example zip; the full program submits its pose without opening a window.

Terminal
dotnet add package \
  RecursiveAI.OneGame.Host

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

C#
using OneGame.Gltf;
 
var model =
    GltfLoader.LoadFile("figure.glb");
var rig = new Rig(model);
var animator = new Animator(
    rig, rig.Node("Root"));
animator.AddState("walk",
    new ClipMotion(rig.Clip("walk")));
animator.UpdateTo(0);
animator.UpdateTo(0.25);
Console.WriteLine(
    animator.Pose.Rotation.Length);

Prints:

Text
15

Unzip the example, then dotnet run.

The zip includes figure.glb: a skinned figure with the named clips idle, walk, run and jump, generated by the public example’s model builder.

The full program

C#
using OneGame.Core;
using OneGame.Gltf;
using OneGame.Host;
 
var rig = new Rig(GltfLoader.LoadFile("figure.glb"));
var animator = new Animator(rig, rig.Node("Root"));
animator.AddState("walk", new ClipMotion(rig.Clip("walk")));
using var view = new SceneView3D();
view.Models.Add("hero", rig.Model);
view.Describer = (_, scene) =>
{
    animator.UpdateTo((scene.Tick + scene.Alpha.ToSingle()) / 60.0);
    scene.Model("hero", Transform3.Identity, animator.Pose);
};
var world = new World(1, new EntityRegistry());
view.Prepare(world, 0, Fixed.Zero);
view.Prepare(world, 15, Fixed.Zero);
Console.WriteLine($"Models submitted: {view.ListModels().Count}");
Console.WriteLine($"Pose nodes: {animator.Pose.Rotation.Length}");

dotnet run prints:

Text
Models submitted: 1
Pose nodes: 15

figure.glb is the public example’s code-generated, skinned character. It has the clips idle, walk, run and jump. The view advances the animator from tick time and copies its pose into the submitted model draw.

Fixed is a deterministic fixed-point number type; FVec3 is a three-dimensional vector of those numbers. The full program uses them for scene time and placement.

A missing clip or node name throws KeyNotFoundException listing the names the rig contains.

Move a 3D character · 3D rigid-body physics


The short answer

Load the glTF model with GltfLoader, construct a Rig, and give each drawn character an Animator. Add a state backed by ClipMotion, advance the animator from view time, and pass animator.Pose to SceneWriter.Model. The first added state is the starting state, so the example begins walking without a separate play call.

The download submits one posed model to a scene view and prints the submission and node counts. It does not open a game window or encode an animation video. In a rendered game the same scene writer call supplies the pose that the 3D renderer draws.

Does glTF support animation?

Yes. The onegame glTF loader and animation APIs read the model’s clips and produce a node pose, including translation, rotation, scale and morph weights. A skeleton, skin and named clips must actually be present in the input you intend to animate. Loading a model does not invent a walk cycle for a static mesh.

The sample asset is the public repository’s code-generated figure: a box-built character with a skin and the clips idle, walk, run and jump. Its license files are included in the download. The code chooses walk by name through rig.Clip("walk"), not by assuming that an exporter puts the desired animation at a particular index. Replace both the file and the requested clip name when using your own model.

A rig supplies named node and clip lookup. A wrong name is an error, not a request to fall back to another clip. Inspect the names in your exported model and the exception’s list of known names before debugging interpolation or state transitions.

Can I animate a GLB in C#?

Yes. The bundled figure.glb is binary glTF. GltfLoader.LoadFile reads it, and Rig wraps the resulting model. Model data can be shared, but an animator belongs to one drawn character: two characters with different walk timing or jump state need separate animator instances.

The view registers the imported model under the drawing name hero. That name belongs to the model library, while walk names an animation state and a source clip in this example. Keeping those roles separate prevents a common integration mistake: trying to draw a clip name as though it were a registered model.

UpdateTo takes absolute time. The recipe derives seconds from the scene’s tick and interpolation alpha at the documented tick rate. Call it once per frame from view code rather than incrementing simulation positions with animation floats. The animator advances in fixed 1/60-second steps. Gaps over a quarter second do not replay all the missed updates: the clock moves, gliding parameters settle, and the pose is made from the animator’s current state. Use Play(state, progress) when a restored view must show a specific moment.

Passing a root node holds its own translation at rest in the pose unless KeepRootMotion is enabled. In the sample, Root therefore does not drag the whole figure away from the entity transform as the walk clip plays. This is distinct from moving the character in deterministic simulation code.

Does this create a 3D animated character?

No. It plays an existing character; it is not a modeling or rigging tool. For game integration, keep the animator in view state and choose its parameters from synced state, the game properties saved and replicated with the world. A speed parameter can drive a blend space between idle, walk and run. A trigger can start a non-looping jump state, with a transition back after its exit progress.

Animation does not write deterministic game state. When the simulation must follow authored root movement, the documented route is to bake a fixed-point root-motion track and consume it in simulation code, not read a floating-point pose back into entity position. The animation guide covers blending, transitions and that separate root-motion path.

All onegame recipes