Skeleton¶
SimVX provides skeletal animation support through the Bone dataclass and Skeleton class. Together they define a bone hierarchy and compute joint matrices for GPU vertex skinning.
Bone¶
A Bone represents a single bone in the hierarchy:
Field |
Type |
Description |
|---|---|---|
|
|
Human-readable bone name |
|
|
Index of parent bone (-1 for root) |
|
|
Transforms from mesh space to bone-local space |
|
|
Default local-space transform relative to parent |
Skeleton¶
Skeleton holds an ordered list of bones and computes the final joint matrices used by the GPU skinning shader.
Creating a Skeleton¶
Bones must be ordered so that every parent appears before its children:
import numpy as np
from simvx.core import Bone, Skeleton, translate
# Build a simple arm: root -> upper_arm -> forearm -> hand
bones = [
Bone(name="root", parent_index=-1),
Bone(name="upper_arm", parent_index=0,
local_transform=translate((0, 2, 0))),
Bone(name="forearm", parent_index=1,
local_transform=translate((0, 1.5, 0))),
Bone(name="hand", parent_index=2,
local_transform=translate((0, 1.0, 0))),
]
skeleton = Skeleton(bones)
print(skeleton.bone_count) # 4
Computing a Pose¶
Call compute_pose() to walk the hierarchy and produce joint matrices. Optionally override individual bone transforms:
# Default pose (uses each bone's local_transform)
skeleton.compute_pose()
# Animated pose: override forearm rotation
import math
animated = {
2: rotate((1, 0, 0), math.radians(45)) @ translate((0, 1.5, 0)), # forearm rotated 45 deg
}
skeleton.compute_pose(bone_transforms=animated)
Joint Matrices¶
After compute_pose(), the joint_matrices property returns a (bone_count, 4, 4) NumPy array. Each matrix is world_transform @ inverse_bind_matrix, ready to upload to an SSBO.
matrices = skeleton.joint_matrices # shape (4, 4, 4) for 4 bones
The vertex shader applies skinning by blending up to 4 joint matrices per vertex using bone weights:
vec4 skinned = vec4(0.0);
for (int i = 0; i < 4; i++) {
skinned += joint_matrices[bone_ids[i]] * vec4(position, 1.0) * bone_weights[i];
}
Skinning a Mesh¶
Set MeshInstance3D.skeleton to the skeleton and the renderer draws that mesh
through the GPU skinning path, re-reading joint_matrices every frame:
arm = MeshInstance3D(mesh=rigged_mesh, skeleton=skeleton)
The mesh needs the matching per-vertex joint indices and weights. import_gltf
attaches those and sets skeleton on every primitive of a rigged node, so an
imported model is ready to pose. Setting skeleton back to None returns the
mesh to the rigid path. The skeleton does not have to be the mesh’s parent, or
in the tree at all: only its joint matrices are read.
Skinning needs both halves: per-vertex joints and weights on the mesh, and
bones on the skeleton. Either one missing leaves nothing to drive. A skeleton
on a mesh that carries no joints and weights (a primitive built by hand, or an
unrigged Mesh.cube()), and a Skeleton() that was never given bones, both
render rigidly, identically to the same mesh with no skeleton, and the renderer
logs one warning naming the node and the missing half the first time it is
submitted. .skeleton itself is left alone: the fallback is a draw decision,
not an edit to the scene.
Finding Bones¶
Look up a bone index by name:
idx = skeleton.find_bone("forearm") # 2
idx = skeleton.find_bone("missing") # -1
API Reference¶
See simvx.core.skeleton for the complete skeleton API.