Skip to content
Plugins (190)

skeleton

stable

Create and manipulate skeletal rigs with named bones and parent-child hierarchy, and play back animation clips with keyframe interpolation and blending.

use plugin skeleton
17 functions Graphics
Runs with the native Zolo host

Plugin snippets can be edited in the Playground, but native capabilities are executed locally with the Zolo CLI.

/ filter jk navigate Esc clear
Functions (17)
  1. Skeleton.new Create a new empty skeleton rig
  2. Skeleton.add_bone Add a bone to the skeleton
  3. Skeleton.get_bone Get bone data by index
  4. Skeleton.set_bone_position Set a bone's local position
  5. Skeleton.set_bone_rotation Set a bone's local rotation (Euler)
  6. Skeleton.bone_count Get the number of bones
  7. Skeleton.bone_names Get all bone names as a table
  8. Skeleton.find_bone Find a bone index by name
  9. Skeleton.set_parent Reparent a bone
  10. Skeleton.remove_bone Remove a bone and fix parent references
  11. Skeleton.get_world_transform Get accumulated world position of a bone
  12. AnimClip.new Create a new animation clip with a duration
  13. AnimClip.add_keyframe Add a positional keyframe for a bone
  14. AnimClip.sample Sample all bone transforms at a time
  15. AnimClip.duration Get the clip's total duration
  16. AnimClip.keyframe_count Get the total number of keyframes
  17. AnimClip.blend Blend two clips together at a given weight

From import to result

  1. 01Import only what you use
  2. 02Copy the smallest example
  3. 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"]}")
}

Global index

Find your way through Zolo

Try an idea

Start here

9 results

9 results

en