Animation

Skeletons & skinning

How Meep maps bones to a humanoid taxonomy, computes precomputed bounding volumes for skinned meshes, and optimizes animation keyframe tracks.

Skeletal animation in Meep is built on top of Three.js SkinnedMesh and Skeleton, extended with a humanoid bone taxonomy, precomputed bounding volumes, and a keyframe optimizer. The relevant source is split between engine/graphics/ecs/mesh/skeleton/ and engine/graphics/geometry/skining/.

Humanoid bone taxonomy

HumanoidBoneType defines 21 canonical bone names as string constants:

ConstantString value
Hips"hips"
Spine"spine"
Chest"chest"
Neck"neck"
Head"head"
ShoulderLeft / ShoulderRight"shoulder-left" / "shoulder-right"
UpperArmLeft / UpperArmRight"upper-arm-left" / "upper-arm-right"
ForearmLeft / ForearmRight"forearm-left" / "forearm-right"
HandLeft / HandRight"hand-left" / "hand-right"
ThighLeft / ThighRight"thigh-left" / "thigh-right"
ShinLeft / ShinRight"shin-left" / "shin-right"
FootLeft / FootRight"foot-left" / "foot-right"
ToeLeft / ToeRight"toe-left" / "toe-right"

These string values are assigned to bone.boneType on each Bone object after the mapping pass. IK solvers and helper utilities look up bones by boneType rather than by the raw mesh-exported name.

Automatic bone retargeting

BoneMapping.build(names) maps an arbitrary array of bone name strings onto HumanoidBoneType values without requiring exact name matches. It:

  1. Strips the longest common prefix from all names (e.g. "mixamorig" on Mixamo exports).
  2. Computes a string-similarity score between each stripped name and each HumanoidBoneType value.
  3. Greedily assigns the highest-similarity pairs, consuming each input name and each bone type at most once.

BoneMapping.apply(bones) writes the resulting boneType onto each Bone instance. The whole process runs lazily — findSkeletonBoneByType triggers it the first time any consumer requests a typed bone lookup.

import { BoneMapping } from
    "@woosh/meep-engine/src/engine/graphics/ecs/mesh/skeleton/BoneMapping.js";
import { HumanoidBoneType } from
    "@woosh/meep-engine/src/engine/graphics/ecs/mesh/skeleton/HumanoidBoneType.js";

const mapping = new BoneMapping();
mapping.build(skeleton.bones.map(b => b.name));
mapping.apply(skeleton.bones);

// Now look up a bone:
const footLeft = skeleton.bones.find(b => b.boneType === HumanoidBoneType.FootLeft);

The engine’s SkeletonUtils.findSkeletonBoneByType wraps this pattern and is what TwoBoneInverseKinematicsSolver and OneBoneSurfaceAlignmentSolver call internally — see Inverse kinematics.

Precomputed skinned mesh bounding volumes

Three.js computes bounding volumes for SkinnedMesh from the bind-pose geometry, which is wrong for culling once the character is animated. Meep provides computeSkinnedMeshBoundingVolumes(mesh) to recompute them from the actual skinned vertex positions.

It works by:

  1. Calling computeSkinnedMeshVertices(destination, mesh), which pre-builds a 4×3 transposed bone matrix for each bone (boneWorld × boneInverse) then applies the four-bone linear-blend skinning in a tight, zero-call inner loop over every vertex.
  2. Computing an axis-aligned bounding box (AABB3) and a bounding sphere from the resulting positions.
  3. Writing both back to geometry.boundingBox and geometry.boundingSphere.
import { computeSkinnedMeshBoundingVolumes } from
    "@woosh/meep-engine/src/engine/graphics/geometry/skining/computeSkinnedMeshBoundingVolumes.js";

// call once per character after pose or skeleton changes:
computeSkinnedMeshBoundingVolumes(skinnedMesh);

The updated boundingSphere is also what AnimationGraphSystem reads when computing the screen-space pixel area for animation-LOD culling — characters that occupy fewer than 32 pixels are not ticked.

Keyframe optimization

AnimationOptimizer removes redundant keyframes from Three.js KeyframeTrack arrays. For each track it removes:

  • Adjacent keyframes at the same time value.
  • Interior keyframes whose value is identical to both their predecessor and successor (constant runs), when the interpolation mode is not InterpolateSmooth.

The result is written back into the track in-place (compact shift), reducing memory and the per-frame interpolation cost.

import { AnimationOptimizer } from
    "@woosh/meep-engine/src/engine/ecs/animation/AnimationOptimizer.js";

const optimizer = new AnimationOptimizer();
optimizer.optimize(threeAnimationClip); // processes all tracks

optimizeTrack(track) is also available to process a single track.

Skeleton utilities

SkeletonUtils.js in engine/graphics/ecs/mesh/ exports a set of helpers:

FunctionPurpose
extractSkeletonFromMeshComponent(component)Walk a Mesh component’s Object3D tree to find the first SkinnedMesh and return its Skeleton
findSkeletonBoneByType(skeleton, boneType)Return the Bone whose boneType matches, triggering BoneMapping if needed
getSkeletonBoneByType(component, boneType)Convenience: extract skeleton then find by type
getSkeletonBone(component, boneName)Find by raw name; suggests the closest match on failure
getSkeletonBoneByName(component, name)Iterative stack-based traversal by bone.name
  • Animation graphs — state machines and clip blending
  • Inverse kinematics — two-bone IK and surface alignment use findSkeletonBoneByType
  • Source: engine/graphics/ecs/mesh/skeleton/, engine/graphics/geometry/skining/