skeleton
stableCreate and manipulate skeletal rigs with named bones and parent-child hierarchy, and play back animation clips with keyframe interpolation and blending.
use plugin skeleton Plugin snippets can be edited in the Playground, but native capabilities are executed locally with the Zolo CLI.
Functions (17)
- Skeleton.new Create a new empty skeleton rig
- Skeleton.add_bone Add a bone to the skeleton
- Skeleton.get_bone Get bone data by index
- Skeleton.set_bone_position Set a bone's local position
- Skeleton.set_bone_rotation Set a bone's local rotation (Euler)
- Skeleton.bone_count Get the number of bones
- Skeleton.bone_names Get all bone names as a table
- Skeleton.find_bone Find a bone index by name
- Skeleton.set_parent Reparent a bone
- Skeleton.remove_bone Remove a bone and fix parent references
- Skeleton.get_world_transform Get accumulated world position of a bone
- AnimClip.new Create a new animation clip with a duration
- AnimClip.add_keyframe Add a positional keyframe for a bone
- AnimClip.sample Sample all bone transforms at a time
- AnimClip.duration Get the clip's total duration
- AnimClip.keyframe_count Get the total number of keyframes
- AnimClip.blend Blend two clips together at a given weight
From import to result
- 01Import only what you use
- 02Copy the smallest example
- 03Run it with the Zolo CLI
Overview¶
skeleton models a character rig as a flat array of named bones, each carrying a
local position, a local Euler rotation, and an integer parent index (-1 marks a
root). A skeleton is an opaque Skeleton handle: you create one with
Skeleton.new(), append bones with add_bone, and address every bone by the
integer index that add_bone returns. World-space position is derived on demand
by walking the parent chain, so reparenting or moving a bone automatically affects
its descendants.
Animation lives in a separate AnimClip handle. A clip has a duration and a list
of positional keyframes, each tied to a bone index, kept sorted by time. Sampling
a clip linearly interpolates between surrounding keyframes, and two clips can be
cross-faded with blend. Reach for this plugin whenever you need lightweight
hierarchical transforms plus keyframed playback without pulling in a full
animation engine.
Common patterns¶
Build a small rig and read a child bone's accumulated world position:
use plugin skeleton::{Skeleton}
let rig = Skeleton.new()
let hip = rig.add_bone("hip", -1, 0.0, 1.0, 0.0)
let spine = rig.add_bone("spine", hip, 0.0, 0.5, 0.0)
let head = rig.add_bone("head", spine, 0.0, 0.3, 0.0)
let world = rig.get_world_transform(head)
print("head world y: {world["y"]}")Author a clip for a bone, then sample it midway through:
use plugin skeleton::{Skeleton, AnimClip}
let rig = Skeleton.new()
let hip = rig.add_bone("hip", -1, 0.0, 0.0, 0.0)
let clip = AnimClip.new(1.0)
clip.add_keyframe(0.0, hip, 0.0, 0.0, 0.0)
clip.add_keyframe(1.0, hip, 0.0, 2.0, 0.0)
let pose = clip.sample(0.5)
for _, t in pose {
print("bone {t["bone_index"]}: y={t["y"]}")
}Cross-fade a walk and a run clip to drive a locomotion blend:
use plugin skeleton::{Skeleton, AnimClip}
let rig = Skeleton.new()
let spine = rig.add_bone("spine", -1, 0.0, 0.0, 0.0)
let walk = AnimClip.new(1.0)
walk.add_keyframe(0.0, spine, 0.0, 0.0, 0.0)
walk.add_keyframe(1.0, spine, 0.0, 0.2, 0.0)
let run = AnimClip.new(1.0)
run.add_keyframe(0.0, spine, 0.0, 0.0, 0.0)
run.add_keyframe(1.0, spine, 0.0, 0.5, 0.0)
let blended = walk.blend(run, 1.0, 0.75)
for _, t in blended {
print("blended y: {t["y"]}")
}Create a new empty skeleton rig
Creates a new empty skeleton handle with no bones. Add bones with add_bone.
use plugin skeleton::{Skeleton}
let rig = Skeleton.new()Add a bone to the skeleton
Adds a bone named name to the skeleton with a local position (x, y, z). Use parent_index = -1 for root bones. Returns the new bone's index.
use plugin skeleton::{Skeleton}
let rig = Skeleton.new()
let root = rig.add_bone("hip", -1, 0.0, 0.0, 0.0)
let spine = rig.add_bone("spine", root, 0.0, 1.0, 0.0)
let head = rig.add_bone("head", spine, 0.0, 0.5, 0.0)Get bone data by index
Returns a table with bone data: name, parent, x, y, z, rx, ry, rz.
use plugin skeleton::{Skeleton}
let rig = Skeleton.new()
rig.add_bone("hip", -1, 0.0, 0.0, 0.0)
let bone = rig.get_bone(0)
print("bone: {bone["name"]} at ({bone["x"]}, {bone["y"]})")Set a bone's local position
Sets the local position of the bone at index.
use plugin skeleton::{Skeleton}
let rig = Skeleton.new()
let idx = rig.add_bone("hip", -1, 0.0, 0.0, 0.0)
rig.set_bone_position(idx, 0.0, 1.0, 0.0)Set a bone's local rotation (Euler)
Sets the local rotation (Euler angles) of the bone at index.
use plugin skeleton::{Skeleton}
let rig = Skeleton.new()
let idx = rig.add_bone("arm", -1, 1.0, 0.0, 0.0)
rig.set_bone_rotation(idx, 0.0, 0.0, 1.5707)Get the number of bones
Returns the total number of bones in the skeleton.
use plugin skeleton::{Skeleton}
let rig = Skeleton.new()
rig.add_bone("hip", -1, 0.0, 0.0, 0.0)
rig.add_bone("spine", 0, 0.0, 1.0, 0.0)
print("bones: {rig.bone_count()}")Get all bone names as a table
Returns a table mapping index to bone name for all bones.
use plugin skeleton::{Skeleton}
let rig = Skeleton.new()
rig.add_bone("hip", -1, 0.0, 0.0, 0.0)
rig.add_bone("spine", 0, 0.0, 1.0, 0.0)
let names = rig.bone_names()
for i, name in names {
print("{i}: {name}")
}Find a bone index by name
Returns the index of the bone with name, or -1 if not found.
use plugin skeleton::{Skeleton}
let rig = Skeleton.new()
rig.add_bone("head", -1, 0.0, 1.8, 0.0)
let idx = rig.find_bone("head")
print("head is at index {idx}")Reparent a bone
Changes the parent of the bone at index to parent_index. Use -1 to make it a root bone.
use plugin skeleton::{Skeleton}
let rig = Skeleton.new()
rig.add_bone("root", -1, 0.0, 0.0, 0.0)
rig.add_bone("child", -1, 1.0, 0.0, 0.0)
rig.set_parent(1, 0)Remove a bone and fix parent references
Removes the bone at index. Parent indices of remaining bones that referenced it or were after it are automatically adjusted.
use plugin skeleton::{Skeleton}
let rig = Skeleton.new()
rig.add_bone("temp", -1, 0.0, 0.0, 0.0)
rig.remove_bone(0)Get accumulated world position of a bone
Returns the accumulated world-space position {x, y, z} of the bone at index by summing positions up the parent chain.
use plugin skeleton::{Skeleton}
let rig = Skeleton.new()
let hip = rig.add_bone("hip", -1, 0.0, 1.0, 0.0)
let spine = rig.add_bone("spine", hip, 0.0, 0.5, 0.0)
let world = rig.get_world_transform(spine)
print("spine world y: {world["y"]}")Because positions accumulate up the chain, moving a parent shifts every descendant's world transform without touching the child's local position:
use plugin skeleton::{Skeleton}
let rig = Skeleton.new()
let hip = rig.add_bone("hip", -1, 0.0, 1.0, 0.0)
let knee = rig.add_bone("knee", hip, 0.0, -0.5, 0.0)
rig.set_bone_position(hip, 0.0, 2.0, 0.0)
let world = rig.get_world_transform(knee)
print("knee world y: {world["y"]}")Create a new animation clip with a duration
Creates a new animation clip lasting duration seconds with no keyframes.
use plugin skeleton::{AnimClip}
let clip = AnimClip.new(2.0)Add a positional keyframe for a bone
Adds a positional keyframe for bone_index at time seconds with position (x, y, z). Keyframes are kept sorted by time automatically.
use plugin skeleton::{Skeleton, AnimClip}
let rig = Skeleton.new()
let hip = rig.add_bone("hip", -1, 0.0, 0.0, 0.0)
let clip = AnimClip.new(1.0)
clip.add_keyframe(0.0, hip, 0.0, 0.0, 0.0)
clip.add_keyframe(0.5, hip, 0.0, 1.0, 0.0)
clip.add_keyframe(1.0, hip, 0.0, 0.0, 0.0)Sample all bone transforms at a time
Samples the clip at time seconds and returns a table of bone transforms, each with bone_index, x, y, z. Values are linearly interpolated between keyframes.
use plugin skeleton::{Skeleton, AnimClip}
let rig = Skeleton.new()
let hip = rig.add_bone("hip", -1, 0.0, 0.0, 0.0)
let clip = AnimClip.new(1.0)
clip.add_keyframe(0.0, hip, 0.0, 0.0, 0.0)
clip.add_keyframe(1.0, hip, 0.0, 2.0, 0.0)
let transforms = clip.sample(0.5)
for _, t in transforms {
print("bone {t["bone_index"]}: y={t["y"]}")
}Sampling several bones at once returns one entry per animated bone, so you can drive a whole rig from a single call:
use plugin skeleton::{Skeleton, AnimClip}
let rig = Skeleton.new()
let arm = rig.add_bone("arm", -1, 1.0, 0.0, 0.0)
let leg = rig.add_bone("leg", -1, -1.0, 0.0, 0.0)
let clip = AnimClip.new(2.0)
clip.add_keyframe(0.0, arm, 1.0, 0.0, 0.0)
clip.add_keyframe(2.0, arm, 1.0, 1.0, 0.0)
clip.add_keyframe(0.0, leg, -1.0, 0.0, 0.0)
clip.add_keyframe(2.0, leg, -1.0, -1.0, 0.0)
for _, t in clip.sample(1.0) {
print("bone {t["bone_index"]} -> ({t["x"]}, {t["y"]})")
}Get the clip's total duration
Returns the total duration of the clip in seconds.
use plugin skeleton::{AnimClip}
let clip = AnimClip.new(3.5)
print("duration: {clip.duration()}")Get the total number of keyframes
Returns the total number of keyframes stored in the clip.
use plugin skeleton::{Skeleton, AnimClip}
let rig = Skeleton.new()
let idx = rig.add_bone("bone", -1, 0.0, 0.0, 0.0)
let clip = AnimClip.new(1.0)
clip.add_keyframe(0.0, idx, 0.0, 0.0, 0.0)
clip.add_keyframe(1.0, idx, 1.0, 0.0, 0.0)
print("keyframes: {clip.keyframe_count()}")Blend two clips together at a given weight
Blends this clip with other at time, where factor is 0.0 (fully this clip) to 1.0 (fully other). Returns a table of blended bone transforms.
use plugin skeleton::{Skeleton, AnimClip}
let rig = Skeleton.new()
let b = rig.add_bone("spine", -1, 0.0, 0.0, 0.0)
let walk = AnimClip.new(1.0)
walk.add_keyframe(0.0, b, 0.0, 0.0, 0.0)
walk.add_keyframe(1.0, b, 0.0, 0.2, 0.0)
let run = AnimClip.new(1.0)
run.add_keyframe(0.0, b, 0.0, 0.0, 0.0)
run.add_keyframe(1.0, b, 0.0, 0.5, 0.0)
let blended = walk.blend(run, 0.5, 0.75)
for _, t in blended {
print("y={t["y"]}")
}