Skip to content

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::{Skeleton.new, Skeleton.add_bone, Skeleton.get_bone, …}
17 functions Graphics
/ 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

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"]}")
}
enespt-br