Cook a skinned glTF into a .shinmodel with skeleton + clips, place it with hi.ComponentRender + hi.ComponentAnimation, and either play a clip by name or bind an animation graph.
Deep design: Skeletal animation. Sample: src/games/example/assets/models/Fox.glb, graph assets/animations/locomotion-demo.animgraph.json, scene scenes/skeletal_animation.json, archetype skinned_bar.
Prerequisites
- Product build that cooks assets (Kaji) so Shinra emits SRM1 skeleton + clip trailers.
- A skinned glTF/GLB with exactly one skin, JOINTS_0 / WEIGHTS_0, and one or more animation clips. Every authored animation is cooked and keeps its name. A USDZ with one
Skeleton,SkelBindingAPImeshes andSkelAnimationclips cooks the same way (see asset formats).
1. Source model
Sample (Fox GLB, clips Survey / Walk / Run):
| Path | Role |
|---|---|
assets/models/Fox.glb | Source skinned GLB |
assets/models/Fox.model.json | Placeable model doc (Shinra companion) |
assets/models/Fox.material.json | Cooked material companion |
assets/animations/locomotion-demo.animgraph.json | Clip states + speed parameter |
A two-bone bar (assets/models/skinned_bar.gltf, clip "Idle") is still in the shared pack if you want a minimal clip-only actor. Clip-only playback is also on scenes/brainstem_animation.json (HUD tour).
2. Cook
Kaji product / assets path (preferred):
bin/kaji/kaji assets --workspace=<repo> --project=src/games/example
# or full game/editor build, which cooks as part of staging
bin/kaji/kaji game --workspace=<repo> --project=src/games/example --type=monolithic --config=debugDirect Shinra (debug / CI-style check):
cd src/shinra && cargo run --release -- \
--input ../games/example/assets \
--output /tmp/shinra-out \
--target macos-metalCooked models/Fox.shinmodel must report has_skeletal_animation, non-zero joint_count, and clip names (sample: Survey, Walk, Run).
3. Archetype with animation
ComponentAnimation is attachable storage; the archetype must list it so scene "animation" binds a player:
const hi = @import("hikari_game");
pub const SkinnedBarEntity = hi.defineActor(.{
.archetype = "skinned_bar",
.components = .{ hi.ComponentRender, hi.ComponentAnimation },
});Sample: src/games/example/src/entities/skinned_bar_entity.zig.
4. Scene actor
Scene model placeables use a .model.json URI. Graph refs are extension-free stems.
{
"id": "skel_bar",
"archetype": "skinned_bar",
"transform": {
"position": [0, 0.12, 0],
"rotation_euler": [0, 0, 0],
"scale": [0.02, 0.02, 0.02]
},
"components": {
"render": {
"model": "asset://./models/Fox.model.json",
"is_visible": true,
"casts_shadow": true
},
"animation": {
"graph": "asset://./animations/locomotion-demo",
"playing": true,
"looping": true,
"speed": 1.0,
"time": 0.0
}
}
}- Clip strings (when you author
"clip"instead of"graph") are hashed with FNV-1a and matched to the cooked name table. - List the pack that holds the model in scene
"packs". The sample scene namesshared, whoselayout.jsonincludesmodels/skinned_bar*andmodels/BrainStem*— notFox/animations/**. Editor live-tree loads still resolve Fox; a packed game needs matching include globs.
Full sample: src/games/example/scenes/skeletal_animation.json.
5. Play from code (optional)
Clip control (takes the actor off the graph until you hand it back):
hi.animation().play(actor, "Survey");
hi.animation().setLooping(actor, true);
hi.animation().setSpeed(actor, 1.0);
// hi.animation().pause(actor); // retain current time
// hi.animation().resumePlayback(actor); // continue current clip
// hi.animation().stop(actor);
// hi.animation().setTime(actor, 0.0);
// hi.animation().playGraph(actor); // return to the bound state machineRequires the entity to have ComponentAnimation (same as the scene block).
Drive the sample locomotion graph with the speed parameter (0 = Survey, ~0.1 = Walk, >1 = Run):
_ = hi.animation().setGraphFloat(actor, "speed", 1.5);
_ = hi.animation().playGraph(actor);For models with several clips, enumerate the cooked names instead of duplicating an exporter-specific list in gameplay code:
const animation = hi.animation();
for (0..animation.clipCount(actor)) |i| {
const clip_name = animation.clipName(actor, @intCast(i)) orelse continue;
// Populate a developer menu, select an initial state, etc.
_ = clip_name;
}
if (animation.hasClip(actor, "Run")) animation.play(actor, "Run");Kawa exposes the same controls and clip discovery through Animation:
let actor = Actors.find("hero");
let count = Animation.clip_count(actor);
let clip = Animation.clip_name(actor, 0);
Animation.play(actor, clip);
Animation.set_graph_float(actor, "speed", 1.5);
Animation.set_graph(actor, "asset://./animations/locomotion-demo");
Animation.pause(actor);
Animation.resume_playback(actor);
Animation.stop(actor); // Pauses at frame zero.Animation.has_clip, set_time, set_speed, set_looping, is_playing,
set_graph_bool, and trigger_graph mirror the Zig gameplay API. Invalid actors,
missing animation components, and unknown clips return false (or nil for
clip_name) instead of trapping.
The model asset viewer shows every clip in a scrollable list. Selecting a row switches the active clip and resets its timeline; play, pause, stop, loop, scrub, and the joint overlay all operate on the selection.
6. Listen for clip events (optional)
Markers placed on a clip in the graph editor (select a single-clip state → Events
→ Add event) reach gameplay on the frame the animation crosses them. The demo
graph carries a footstep marker on Walk and Run. Declare the callback on any logic
component of the archetype:
pub fn onAnimationEvent(self: *@This(), actor: hi.ActorContext, event: *const hi.AnimationEvent) void {
const id = actor.id;
_ = self;
if (event.is("footstep")) hi.debug().log("{s} stepped (foot {d})", .{ id, event.value });
}Or poll from any system that runs after animation:
var it = hi.animation().events(actor);
while (it.next()) |event| if (event.is("footstep")) playFootstep(event.value);
if (hi.animation().fired(actor, "impact")) applyHit();Kawa gets the same marker as on_animation_event(name, value, time):
fn on_animation_event(name, value, time) {
if (name == "footstep") { Audio.play("footstep"); }
}A marker belongs to the clip, so it fires from every state and blend space that plays that clip; in a blend only the dominant clip fires, so a Walk/Run blend does not double its footsteps. See animation-events.
Verify
- Cook succeeds and
Fox.shinmodelhas skeletal flag + joints + animations. - Open the editor / game, load scene
skeletal_animation. - The Fox should pose (Survey while
speedis 0). Use the visible Speed slider: above 0.1 walks, above 1 runs. The control sets the graph parameter throughhi.animation().setGraphFloat. - For clip-only, open
scenes/brainstem_animation(Browse scenes → Tutorials → Clip Playback) or callhi.animation().playwith an enumerated name. - Change
"clip"or callhi.animation().playwith a wrong name → soft no-op (clip hash miss).
See also
- First mesh and material —
asset://render binds - Assets and Shinra — cook / packs
- Skeletal animation design — contracts