DocsAPI reference

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 #

mojo
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: Int
  • name: String

struct AnimationState #

mojo
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 #

mojo
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 through take_root_motion.

  • root_bone: Int

  • finished: Bool — A non-looping state reached its end (its phase holds 1). Cleared when play switches state or restarts one (restart=True).

  • __init__ __init__(out self, var skeleton: Skeleton, var clips: List[Clip])

  • add_clip_state add_clip_state( mut self, name: String, clip: Int, looping: Bool = True, speed: Float32 = 1.0, ) raises -> Int
    A state that plays one clip.

  • add_state add_state(mut self, var state: AnimationState) raises -> Int
    Validates state.tree against 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 (see BlendTree.validate).

  • add_event add_event(mut self, state: Int, phase: Float32, name: String) raises
    Adds an event name to state that fires each time the state's phase passes phase. phase is normalised to the state's cycle and must be in [0, 1). Raises for an unknown state or a phase outside that range.

  • find_state find_state(self, name: String) -> Int
    Index of the first state called name, or -1 when there is none.

  • set_parameter set_parameter(mut self, name: String, value: Float32) raises
    Sets a blend parameter on every state that declares it.

  • play play( mut self, state: Int, fade_seconds: Float32 = 0.0, start_phase: Float32 = 0.0, restart: Bool = False, ) raises
    Switches to state at start_phase, crossfading from the current one over fade_seconds, and clears finished. Playing the state that is already current is a no-op (so calling play every frame is safe), even when that state is non-looping and has finished. Pass restart=True to start it again from start_phase anyway, 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_state current_state(self) -> Int
    Index of the playing state (the incoming one during a crossfade), or -1 before the first play.

  • phase phase(self) -> Float32
    Normalised phase of the current state: in [0, 1) while looping; a non-looping state holds 1 once it has finished.

  • fade_weight fade_weight(self) -> Float32
    Weight of the incoming state during a crossfade (1 when none is running).

  • update update(mut self, dt: Float32) raises
    Advances time, evaluates and blends the layers, then applies root motion.

  • enable_root_motion enable_root_motion(mut self, bone: Int = 0) raises
    Starts extracting the horizontal motion of bone (the hierarchy root by default).

  • take_root_motion take_root_motion(mut self) -> Vec3
    Horizontal root displacement accumulated since the last call (model space).

  • take_events take_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.

  • palette palette(self) -> List[Mat4]
    Skinning matrices for the current pose in palette (skin joint) order. Allocates a new list; palette_into is the allocation-free form.

  • palette_into palette_into(mut self, mut out: List[Mat4])
    Skinning matrices for the current pose into out (no allocation once sized).

  • bone_matrices bone_matrices(self) -> List[Mat4]
    Every bone's matrix in model space for the current pose (attachments, IK, debug).

  • bone_matrices_into bone_matrices_into(self, mut out: List[Mat4])
    Every bone's model-space matrix for the current pose into out (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.kind of a leaf that samples one clip.
  • comptime NODE_BLEND_1D = 1 — BlendNode.kind of a node that blends its children along one parameter.
  • comptime NODE_BLEND_2D = 2 — BlendNode.kind of a node that blends its children over two parameters (gradient bands).
  • comptime MAX_CHILDREN = 8 — Children per blend node.

struct BlendNode #

mojo
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: parameter is x and parameter2 is 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 #

mojo
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_parameter add_parameter(mut self, name: String, initial: Float32 = 0.0) -> Int
    Declares a blend parameter with an initial value and returns its index, which add_blend_1d / add_blend_2d take. Duplicate names are not rejected; set_parameter updates the first match.

  • set_parameter set_parameter(mut self, name: String, value: Float32) raises
    Sets the first parameter called name to value. Raises when the tree has no such parameter.

  • add_clip add_clip(mut self, clip: Int, speed: Float32 = 1.0) -> Int
    Adds a leaf that plays clip index clip at speed times its natural rate and returns the node index. The clip index is checked by validate, not here.

  • add_blend_1d add_blend_1d(mut self, parameter: Int) -> Int
    Adds a 1D blend node driven by parameter index parameter and returns the node index. Attach children with add_child; the parameter index is checked by validate.

  • add_blend_2d add_blend_2d(mut self, parameter_x: Int, parameter_y: Int) -> Int
    Adds a 2D blend node driven by parameters parameter_x and parameter_y and returns the node index. Attach children with add_child; the parameter indices are checked by validate.

  • add_child add_child(mut self, node: Int, child: Int, at: Vec2) raises
    Attaches child to a blend node with full weight at parameter value at (1D nodes use at.x()). Children of a 1D node must be added in increasing order. Raises when an index is out of range, node is a clip, the node is full, a 1D threshold does not increase, or the edge would close a cycle (child is node or one of its ancestors): a tree must stay acyclic for evaluation to terminate. A node may still be shared by several parents.

  • set_root set_root(mut self, node: Int) raises
    Makes node the entry node of the tree. Raises when the index is out of range.

  • validate validate(self, clip_count: Int) raises
    Checks that the tree can be evaluated with clip_count clips 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_child already refuses these, but nodes is public). Animator.add_state calls it.

  • leaf_weights_into leaf_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 into weights.

  • leaf_weights leaf_weights(self) -> List[Float32]
    Per-node weights for the current parameters (see leaf_weights_into) in a new list; allocates.

  • cycle_rate cycle_rate(self, clips: List[Clip]) -> Float32
    Phase advance per second: the weighted average of each leaf's speed / duration.

  • cycle_rate_with cycle_rate_with( self, clips: List[Clip], mut weights: List[Float32] ) -> Float32
    cycle_rate using the caller's weight scratch (no allocation once sized).

  • evaluate evaluate( self, clips: List[Clip], skeleton: Skeleton, phase: Float32, mut pose: Pose, mut scratch: Pose, )
    Blends every weighted leaf at phase into pose (allocates a weight list; the animator uses evaluate_with).

  • evaluate_with evaluate_with( self, clips: List[Clip], skeleton: Skeleton, phase: Float32, mut pose: Pose, mut scratch: Pose, mut weights: List[Float32], )
    Blends every weighted leaf at phase into pose. scratch is a pose of the same skeleton for the per-leaf samples and weights the 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.path of a translation channel (3 components).
  • comptime PATH_ROTATION = 1 — ClipChannel.path of a rotation channel (quaternion x, y, z, w).
  • comptime PATH_SCALE = 2 — ClipChannel.path of a scale channel (3 components).
  • comptime PATH_WEIGHTS = 3 — ClipChannel.path of 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 #

mojo
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] )

  • reset reset(mut self, skeleton: Skeleton)
    Restores the rest pose of skeleton and zeroes the morph weights in place (no allocation). skeleton must be the one the pose was built for.

  • copy_from copy_from(mut self, other: Pose)
    Element-wise copy (same skeleton), no allocation.

  • blend_with blend_with(mut self, b: Pose, t: Float32)
    self = lerp(self, b, t) component-wise.

def sample_component #

mojo
def sample_component( ch: ClipChannel, pair: _KeyPair, component: Int ) -> Float32

One output component at the located time.

def sample_rotation #

mojo
def sample_rotation(ch: ClipChannel, pair: _KeyPair) -> Quat

Rotations interpolate along the shorter arc (slerp for linear, normalised spline for cubic, as the glTF specification describes).

def sample_clip #

mojo
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 #

mojo
def wrap_time(time: Float32, duration: Float32) -> Float32

Loops 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 .cskel file.
  • comptime SKELETON_VERSION = 1 — Cooked skeleton format version; read_skeleton rejects any other.
  • comptime MAX_BONES = 256 — Upper bound for a palette (the renderer's joints buffer grows in 1024-matrix chunks).

struct Skeleton #

mojo
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_count bone_count(self) -> Int
    Number of bones (skin joints plus the ancestors needed to reach a common root).

  • joint_count joint_count(self) -> Int
    Number of skin joints, which is the length of the skinning palette.

  • find_bone find_bone(self, name: String) -> Int
    Index of the first bone called name, or -1 when there is none.

  • bone_of_node bone_of_node(self, node: Int) -> Int
    Bone index of glTF node node, or -1 when the node is out of range or not part of the skeleton.

  • world_transforms_into world_transforms_into( self, local: List[Transform], mut out: List[Mat4] )
    Bone-space matrices from a local pose (parents first), in model space, written into out (resized; no allocation once large enough).

  • world_transforms world_transforms(self, local: List[Transform]) -> List[Mat4]
    Model-space bone matrices for the local pose local in a new list; allocates (see world_transforms_into).

  • palette_into palette_into( self, local: List[Transform], mut worlds: List[Mat4], mut out: List[Mat4], )
    Skinning matrices in palette order into out; worlds is scratch for the bone matrices.

  • palette palette(self, local: List[Transform]) -> List[Mat4]
    Skinning matrices in palette order: inverse(mesh node) * bone world * inverse bind.

  • validate validate(self) raises
    Checks the invariants and raises on the first violation: 1 to MAX_BONES bones, 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 #

mojo
def skeleton_from_gltf(asset: GltfAsset, skin: Int) raises -> Skeleton

Builds 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 #

mojo
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 #

mojo
def read_skeleton(var data: List[UInt8]) raises -> Skeleton

Parses 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 of AnimatorRef.

struct AnimatorRef #

mojo
struct AnimatorRef(Component)

Links an entity to AnimationSystem.animators[index].

Fields:

  • index: Int

  • component_id component_id() -> Int

struct AnimationSystem #

mojo
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)

  • add add(mut self, var animator: Animator) -> Int
    Takes ownership of animator and returns its index for AnimatorRef. Call between frames, never while update is running.

  • __len__ __len__(self) -> Int

  • clear clear(mut self)
    Drops every animator (a level restart); AnimatorRefs become stale.

  • update update( mut self, dt: Float32, jobs: Optional[Pointer[JobSystem, MutUntrackedOrigin]], ) raises
    Advances every animator by dt. 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. updates counts only calls that did not raise.

  • publish publish(mut self, mut world: World) raises -> Int
    Writes a JointPalette for every entity with an AnimatorRef, in place when the component exists (no steady-state allocation). Returns the count.

View as MarkdownSource: docs/api/anim.md