cinder.anim
cinder.anim #
Source: cinder/anim/__init__.mojo
Skeletal animation: skeletons, clip sampling, blend trees, animators with crossfades, root motion and events, and a job-parallel system publishing joint palettes to the ECS.
cinder.anim.animator #
Source: cinder/anim/animator.mojo
Animator: states (blend trees) with crossfades, animation events and root motion. update(dt) advances the active state (and the one fading out), blends them, extracts root motion and collects events; pose then holds the local pose and palette() the skinning matrices. Events are authored per state at a normalised phase in [0, 1) and fire once each time the phase passes them, including across loop wraps.
struct AnimationEvent #
struct AnimationEvent(Copyable, Movable)An event that fired during Animator.update: the index of the state that owns it and the event's name.
Fields:
state: Intname: String
struct AnimationState #
struct AnimationState(Copyable, Movable)A named animator state: a blend tree, whether its phase loops, and its events. Build it with AnimationState(name, tree^, looping) and register it with Animator.add_state, which validates the tree; add events afterwards with Animator.add_event.
Fields:
-
name: String -
tree: BlendTree -
looping: Bool -
events: List[_StateEvent] -
__init____init__( out self, name: String, var tree: BlendTree, looping: Bool = True )
struct Animator #
struct Animator(Movable)Plays the states of one skeleton with crossfades, events and root motion. Owns its skeleton, clips and poses. Author states with add_clip_state / add_state, start one with play (play(state, restart=True) replays a finished non-looping state), then call update(dt) once per frame (it raises until a state has been played). Read the result from pose, palette() / palette_into or bone_matrices(). Nothing is synchronised: AnimationSystem updates each animator from a single job at a time.
Fields:
-
skeleton: Skeleton -
clips: List[Clip] -
states: List[AnimationState] -
pose: Pose -
current: _Layer -
previous: _Layer -
fade_remaining: Float32 -
fade_duration: Float32 -
speed: Float32 — Global playback rate. -
root_motion: Bool — When set, the root bone's horizontal translation is removed from the pose and reported throughtake_root_motion. -
root_bone: Int -
finished: Bool — A non-looping state reached its end (its phase holds 1). Cleared whenplayswitches state or restarts one (restart=True). -
__init____init__(out self, var skeleton: Skeleton, var clips: List[Clip]) -
add_clip_stateadd_clip_state( mut self, name: String, clip: Int, looping: Bool = True, speed: Float32 = 1.0, ) raises -> Int
A state that plays one clip. -
add_stateadd_state(mut self, var state: AnimationState) raises -> Int
Validatesstate.treeagainst the animator's clips, appends the state and returns its index. Raises when the tree has no root, names a clip that does not exist, or has a blend node without children or with a parameter out of range (seeBlendTree.validate). -
add_eventadd_event(mut self, state: Int, phase: Float32, name: String) raises
Adds an eventnametostatethat fires each time the state's phase passesphase.phaseis normalised to the state's cycle and must be in [0, 1). Raises for an unknown state or a phase outside that range. -
find_statefind_state(self, name: String) -> Int
Index of the first state calledname, or -1 when there is none. -
set_parameterset_parameter(mut self, name: String, value: Float32) raises
Sets a blend parameter on every state that declares it. -
playplay( mut self, state: Int, fade_seconds: Float32 = 0.0, start_phase: Float32 = 0.0, restart: Bool = False, ) raises
Switches tostateatstart_phase, crossfading from the current one overfade_seconds, and clearsfinished. Playing the state that is already current is a no-op (so callingplayevery frame is safe), even when that state is non-looping and hasfinished. Passrestart=Trueto start it again fromstart_phaseanyway, for example to replay a finished jump; with a fade the old playthrough fades out as the new one starts. Raises for an unknown state. -
current_statecurrent_state(self) -> Int
Index of the playing state (the incoming one during a crossfade), or -1 before the firstplay. -
phasephase(self) -> Float32
Normalised phase of the current state: in [0, 1) while looping; a non-looping state holds 1 once it has finished. -
fade_weightfade_weight(self) -> Float32
Weight of the incoming state during a crossfade (1 when none is running). -
updateupdate(mut self, dt: Float32) raises
Advances time, evaluates and blends the layers, then applies root motion. -
enable_root_motionenable_root_motion(mut self, bone: Int = 0) raises
Starts extracting the horizontal motion ofbone(the hierarchy root by default). -
take_root_motiontake_root_motion(mut self) -> Vec3
Horizontal root displacement accumulated since the last call (model space). -
take_eventstake_events(mut self) -> List[AnimationEvent]
Returns the events fired since the last call and clears the list. Only the current state fires events; a state that is fading out fires none. -
palettepalette(self) -> List[Mat4]
Skinning matrices for the current pose in palette (skin joint) order. Allocates a new list;palette_intois the allocation-free form. -
palette_intopalette_into(mut self, mut out: List[Mat4])
Skinning matrices for the current pose intoout(no allocation once sized). -
bone_matricesbone_matrices(self) -> List[Mat4]
Every bone's matrix in model space for the current pose (attachments, IK, debug). -
bone_matrices_intobone_matrices_into(self, mut out: List[Mat4])
Every bone's model-space matrix for the current pose intoout(resized; no allocation once large enough).
cinder.anim.blend #
Source: cinder/anim/blend.mojo
Blend trees: clips, 1D and 2D blend nodes, evaluated phase-synchronised. A tree runs on a normalised phase in [0, 1). Every leaf clip plays at phase * duration, so blended cycles (walk and run) stay in step; the phase advances at the weighted average of the leaves' cycle rates. Leaf weights come from the blend parameters: 1D nodes interpolate between the two nearest thresholds, 2D nodes use gradient-band interpolation (Johansen 2009), which is smooth, sums to one and reproduces each child exactly at its own point.
comptime NODE_CLIP = 0—BlendNode.kindof a leaf that samples one clip.comptime NODE_BLEND_1D = 1—BlendNode.kindof a node that blends its children along one parameter.comptime NODE_BLEND_2D = 2—BlendNode.kindof a node that blends its children over two parameters (gradient bands).comptime MAX_CHILDREN = 8— Children per blend node.
struct BlendNode #
struct BlendNode(Copyable, Movable)One node of a BlendTree: a clip leaf or a 1D/2D blend over child node indices. Created by BlendTree.add_clip, add_blend_1d and add_blend_2d; indices a kind does not use stay -1 and its lists stay empty.
Fields:
-
kind: Int -
clip: Int — Clip index (leaf). -
speed: Float32 — Playback rate multiplier (leaf). -
parameter: Int — Blend parameter index (1D: one scalar; 2D:parameteris x andparameter2is y). -
parameter2: Int -
children: List[Int] -
points: List[Vec2] — Per child: the parameter value (1D uses x) where the child has full weight. -
__init____init__(out self, kind: Int)
struct BlendTree #
struct BlendTree(Copyable, Movable)Nodes are stored flat; root names the entry node. Build with add_clip, add_blend_1d and add_blend_2d, then set_root.
Fields:
-
nodes: List[BlendNode] -
root: Int -
parameter_names: List[String] -
parameters: List[Float32] -
__init____init__(out self) -
add_parameteradd_parameter(mut self, name: String, initial: Float32 = 0.0) -> Int
Declares a blend parameter with aninitialvalue and returns its index, whichadd_blend_1d/add_blend_2dtake. Duplicate names are not rejected;set_parameterupdates the first match. -
set_parameterset_parameter(mut self, name: String, value: Float32) raises
Sets the first parameter callednametovalue. Raises when the tree has no such parameter. -
add_clipadd_clip(mut self, clip: Int, speed: Float32 = 1.0) -> Int
Adds a leaf that plays clip indexclipatspeedtimes its natural rate and returns the node index. The clip index is checked byvalidate, not here. -
add_blend_1dadd_blend_1d(mut self, parameter: Int) -> Int
Adds a 1D blend node driven by parameter indexparameterand returns the node index. Attach children withadd_child; the parameter index is checked byvalidate. -
add_blend_2dadd_blend_2d(mut self, parameter_x: Int, parameter_y: Int) -> Int
Adds a 2D blend node driven by parametersparameter_xandparameter_yand returns the node index. Attach children withadd_child; the parameter indices are checked byvalidate. -
add_childadd_child(mut self, node: Int, child: Int, at: Vec2) raises
Attacheschildto a blend node with full weight at parameter valueat(1D nodes useat.x()). Children of a 1D node must be added in increasing order. Raises when an index is out of range,nodeis a clip, the node is full, a 1D threshold does not increase, or the edge would close a cycle (childisnodeor one of its ancestors): a tree must stay acyclic for evaluation to terminate. A node may still be shared by several parents. -
set_rootset_root(mut self, node: Int) raises
Makesnodethe entry node of the tree. Raises when the index is out of range. -
validatevalidate(self, clip_count: Int) raises
Checks that the tree can be evaluated withclip_countclips and raises on the first problem: no root (or one out of range), a clip leaf naming a missing clip, a blend node without children or with a child index out of range, a blend parameter index out of range (2D nodes need both), or a cycle (a node that is its own descendant;add_childalready refuses these, butnodesis public).Animator.add_statecalls it. -
leaf_weights_intoleaf_weights_into(self, mut weights: List[Float32])
Weight per node (leaves carry the blended weight, inner nodes their subtree's), summing to one over the leaves, written intoweights. -
leaf_weightsleaf_weights(self) -> List[Float32]
Per-node weights for the current parameters (seeleaf_weights_into) in a new list; allocates. -
cycle_ratecycle_rate(self, clips: List[Clip]) -> Float32
Phase advance per second: the weighted average of each leaf's speed / duration. -
cycle_rate_withcycle_rate_with( self, clips: List[Clip], mut weights: List[Float32] ) -> Float32
cycle_rateusing the caller's weight scratch (no allocation once sized). -
evaluateevaluate( self, clips: List[Clip], skeleton: Skeleton, phase: Float32, mut pose: Pose, mut scratch: Pose, )
Blends every weighted leaf atphaseintopose(allocates a weight list; the animator usesevaluate_with). -
evaluate_withevaluate_with( self, clips: List[Clip], skeleton: Skeleton, phase: Float32, mut pose: Pose, mut scratch: Pose, mut weights: List[Float32], )
Blends every weighted leaf atphaseintopose.scratchis a pose of the same skeleton for the per-leaf samples andweightsthe per-node weight scratch; callers reuse both across frames so nothing is allocated.
cinder.anim.sampling #
Source: cinder/anim/sampling.mojo
Clip sampling: step, linear and cubic-spline keyframes into a local pose. Pose is the skeleton's local transforms plus morph weights. Sampling starts from the rest pose so channels that are missing from a clip keep the skeleton's values (glTF semantics).
comptime PATH_TRANSLATION = 0—ClipChannel.pathof a translation channel (3 components).comptime PATH_ROTATION = 1—ClipChannel.pathof a rotation channel (quaternion x, y, z, w).comptime PATH_SCALE = 2—ClipChannel.pathof a scale channel (3 components).comptime PATH_WEIGHTS = 3—ClipChannel.pathof a morph target weights channel (one component per target).comptime INTERP_LINEAR = 0—ClipChannel.interpolation: linear between keys (slerp for rotations).comptime INTERP_STEP = 1—ClipChannel.interpolation: each key's value holds until the next key.comptime INTERP_CUBIC = 2—ClipChannel.interpolation: glTF cubic spline; each key stores in-tangent, value and out-tangent.
struct Pose #
struct Pose(Copyable, Movable)A local pose: one parent-space transform per bone plus the morph target weights. Pose(skeleton) starts at the rest pose with zero weights. A pose is tied to the skeleton it was built for: sampling, blending and reset index by bone and never resize.
Fields:
-
local: List[Transform] -
morph_weights: List[Float32] -
__init____init__(out self, skeleton: Skeleton) -
__init____init__( out self, var local: List[Transform], var morph_weights: List[Float32] ) -
resetreset(mut self, skeleton: Skeleton)
Restores the rest pose ofskeletonand zeroes the morph weights in place (no allocation).skeletonmust be the one the pose was built for. -
copy_fromcopy_from(mut self, other: Pose)
Element-wise copy (same skeleton), no allocation. -
blend_withblend_with(mut self, b: Pose, t: Float32)
self = lerp(self, b, t) component-wise.
def sample_component #
def sample_component( ch: ClipChannel, pair: _KeyPair, component: Int ) -> Float32One output component at the located time.
def sample_rotation #
def sample_rotation(ch: ClipChannel, pair: _KeyPair) -> QuatRotations interpolate along the shorter arc (slerp for linear, normalised spline for cubic, as the glTF specification describes).
def sample_clip #
def sample_clip(clip: Clip, skeleton: Skeleton, time: Float32, mut pose: Pose)Writes every channel of clip at time (seconds, unclamped: before the first key or after the last key the end values hold) into pose. Channels whose node is not in the skeleton are ignored; pose must have been created for skeleton.
def wrap_time #
def wrap_time(time: Float32, duration: Float32) -> Float32Loops time into [0, duration).
cinder.anim.skeleton #
Source: cinder/anim/skeleton.mojo
Skeletons: a bone hierarchy with rest pose, the skin's joint list and inverse bind matrices. A skeleton is built from a glTF skin (every joint plus the ancestors needed to reach a common root, so animated parents move their children) or read from a cooked .cskel file. Clips keep glTF node indices, which node_to_bone maps to bones.
comptime SKELETON_MAGIC = "CSKL"— Four-byte tag at the start of a cooked.cskelfile.comptime SKELETON_VERSION = 1— Cooked skeleton format version;read_skeletonrejects any other.comptime MAX_BONES = 256— Upper bound for a palette (the renderer's joints buffer grows in 1024-matrix chunks).
struct Skeleton #
struct Skeleton(Copyable, Movable)Bone hierarchy, rest pose and skin data of one glTF skin. Bones are ordered parents first. Build one with skeleton_from_gltf or read_skeleton (both validate it); palette and world_transforms turn a local pose into matrices.
Fields:
-
names: List[String] -
parents: List[Int] — Parent bone per bone (-1 for roots); parents always precede children. -
rest: List[Transform] — Rest pose in parent space. -
node_to_bone: List[Int] — glTF node index -> bone index (-1 when the node is not part of the skeleton). -
joints: List[Int] — Skin joint i -> bone index (palette order). -
inverse_bind: List[Mat4] -
mesh_node: Int — glTF node that carries the skinned mesh (-1 when unknown); the palette is expressed in its rest space so the render instance keeps the node's world transform. -
mesh_node_inverse: Mat4 -
morph_count: Int — Morph target weights animated on the mesh node (0 when none). -
__init____init__(out self) -
bone_countbone_count(self) -> Int
Number of bones (skin joints plus the ancestors needed to reach a common root). -
joint_countjoint_count(self) -> Int
Number of skin joints, which is the length of the skinning palette. -
find_bonefind_bone(self, name: String) -> Int
Index of the first bone calledname, or -1 when there is none. -
bone_of_nodebone_of_node(self, node: Int) -> Int
Bone index of glTF nodenode, or -1 when the node is out of range or not part of the skeleton. -
world_transforms_intoworld_transforms_into( self, local: List[Transform], mut out: List[Mat4] )
Bone-space matrices from a local pose (parents first), in model space, written intoout(resized; no allocation once large enough). -
world_transformsworld_transforms(self, local: List[Transform]) -> List[Mat4]
Model-space bone matrices for the local poselocalin a new list; allocates (seeworld_transforms_into). -
palette_intopalette_into( self, local: List[Transform], mut worlds: List[Mat4], mut out: List[Mat4], )
Skinning matrices in palette order intoout;worldsis scratch for the bone matrices. -
palettepalette(self, local: List[Transform]) -> List[Mat4]
Skinning matrices in palette order: inverse(mesh node) * bone world * inverse bind. -
validatevalidate(self) raises
Checks the invariants and raises on the first violation: 1 toMAX_BONESbones, one name and rest transform per bone, every parent before its child (or -1), node map and joints inside the bone range (or -1), and one inverse bind matrix per joint.
def skeleton_from_gltf #
def skeleton_from_gltf(asset: GltfAsset, skin: Int) raises -> SkeletonBuilds the skeleton of asset.skins[skin]: the joints and every ancestor up to the outermost ancestor shared by all joints (so animated parents are evaluated), in parent-before-child order. The mesh node is the first node using the skin.
def write_skeleton #
def write_skeleton(skeleton: Skeleton) -> List[UInt8]Serialises skeleton to the cooked .cskel format: magic, version, bones (name, parent, rest transform), node map, joints with inverse bind matrices, mesh node and its inverse, morph count. Indices are stored plus one so -1 round-trips as 0. Does not validate.
def read_skeleton #
def read_skeleton(var data: List[UInt8]) raises -> SkeletonParses a cooked .cskel buffer (consumed) into a validated skeleton. Raises on a wrong magic or version, implausible bone, node or joint counts, data that ends early, trailing bytes, or a result that fails Skeleton.validate.
cinder.anim.system #
Source: cinder/anim/system.mojo
The animation system: animators stored in a pool, advanced in parallel on the job system, and published to the ECS as JointPalette components for the render bridge. Animators live in the system (they own clips and poses); entities refer to them with AnimatorRef. update(dt, jobs) runs one job per animator: each job touches only its own animator, so no synchronisation is needed beyond the completion counter.
comptime ANIMATOR_REF_COMPONENT_ID = 8— ECS component id ofAnimatorRef.
struct AnimatorRef #
struct AnimatorRef(Component)Links an entity to AnimationSystem.animators[index].
Fields:
-
index: Int -
component_idcomponent_id() -> Int
struct AnimationSystem #
struct AnimationSystem(Movable)Owns every animator, updates them in parallel and publishes their palettes to the ECS. Entities refer to animators by index through AnimatorRef; updates counts completed update calls. Jobs receive raw pointers into animators, so the system must stay in place and the list must not change while update runs.
Fields:
-
animators: List[Animator] -
updates: Int -
__init____init__(out self) -
addadd(mut self, var animator: Animator) -> Int
Takes ownership ofanimatorand returns its index forAnimatorRef. Call between frames, never whileupdateis running. -
__len____len__(self) -> Int -
clearclear(mut self)
Drops every animator (a level restart);AnimatorRefs become stale. -
updateupdate( mut self, dt: Float32, jobs: Optional[Pointer[JobSystem, MutUntrackedOrigin]], ) raises
Advances every animator bydt. With a job system (and more than one animator) each animator is its own job; otherwise they run inline, one after the other. Either way every animator is updated, and when any of them raised (an animator that was never played raises "update before play") the call raises afterwards, naming the first one that failed; the others have still advanced.updatescounts only calls that did not raise. -
publishpublish(mut self, mut world: World) raises -> Int
Writes aJointPalettefor every entity with anAnimatorRef, in place when the component exists (no steady-state allocation). Returns the count.