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:
| Constant | String 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:
- Strips the longest common prefix from all names (e.g.
"mixamorig"on Mixamo exports). - Computes a string-similarity score between each stripped name and each
HumanoidBoneTypevalue. - 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:
- Calling
computeSkinnedMeshVertices(destination, mesh), which pre-builds a4×3transposed 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. - Computing an axis-aligned bounding box (
AABB3) and a bounding sphere from the resulting positions. - Writing both back to
geometry.boundingBoxandgeometry.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:
| Function | Purpose |
|---|---|
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 |
Related
- 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/