DynamicBone
Secondary motion (jiggle, sway, lag) for tails, ears, hair, ropes and cloaks, simulated in the
engine: put DynamicBone on the ROOT bone of a chain and its subtree swings and settles under
gravity, wind and the character's motion, kept out of the body by DynamicBoneCollider capsules. A
whole body going limp is a Ragdoll; a hand or a foot placed on a target is IK.
- make a tail, ears or hair swing and bounce when the character moves
- keep a tail or a cape from passing through the body
- make a cloak or cape hang from the shoulders and follow the body's turn
- keep hair or a tail above the floor
- blow hair or a cloak in the wind
- turn the jiggle off for a cutscene, or reset it after a teleport
- make a rope of plain nodes hang and swing
Platforms: iOS, Android, desktop, the web player and the le.codes viewer app; not on the web (Web Lite) or in headless tests — check DynamicBone.supported; there it warns once, the animation plays as it is, and dynamicBone.chain stays empty.
At a glance
const main = async () => {
const scene = new Scene()
const fox = await Model.load(asset('./fox.glb'))
scene.add(fox)
scene.open()
fox.anim.playLoop('walk')
const tail = fox.bone('tail')!.aspect(DynamicBone, {
stiffness: [0.5, 0.15], // stiff at the base, loose at the tip
damping: 0.1,
gravity: 2, // m/s²
angleLimit: [30, 45], // degrees off the animated direction
radius: 0.03, // metres, for the collisions
})
for (const ear of ['earend', 'R_earend']) fox.bone(ear)!.aspect(DynamicBone, { stiffness: 0.6, damping: 0.2, angleLimit: 30, radius: 0.01 })
fox.bone('Hips')!.aspect(DynamicBoneCollider, { radius: 0.11, to: 'chest' }) // a capsule the tail stays out of
tail.dynamicBone.floor = 0 // and above the ground plane y = 0
console.log(`tail chain: ${tail.dynamicBone.chain.map((b) => b.name).join(' > ')}`)
}
main()Swinging bone chains: DynamicBone options
bone.aspect(DynamicBone, { … })
bone.dynamicBone // the aspect — every field is live (pushed when it changes)
bone.dynamicBone.chain // Node[] — the chain's bones, root first
bone.dynamicBone.particles // Vec3[] — simulated world positions: the bones, then a tip per leaf
bone.dynamicBone.reset() // snap onto the animated pose next frame (a teleport, a camera cut)
bone.dynamicBone.guide([{ bone, at, weight }]) // world points bones are drawn to each substep; replaces, [] clearsAny node tree works, not only a skinned model: a rope of plain nodes, with bones naming its
joints. Per-bone values (DynamicBoneCurve) take one number for the whole chain or a [root, tip]
pair interpolated down it by depth — a tail stiff at the base and loose at the tip.
| Field | Default | Meaning |
|---|---|---|
stiffness |
0.3 |
0–1: how fast a bone returns to the animated shape, per 1/60 s. 0 = a rope, 1 = rigid (the animation exactly) |
damping |
0.1 |
0–1: velocity lost per 1/60 s — 0.02 swings like a rope, 0.1 settles like a tail, 0.3 is honey |
gravity |
0 |
m/s², downward. A hanging rest pose feels none of it; a pose that sticks out droops as far as stiffness lets it |
angleLimit |
0 |
Max deviation from the animated direction, degrees per bone (0 = free) |
radius |
0.02 |
Particle radius for the collisions, metres |
mass |
1 |
Relative mass per bone: [3, 1] makes a tail's base carry its tip, 1 shares evenly like a rope |
weight |
1 |
0–1 blend from the animation to the simulation. At 0 nothing is simulated, and the chain re-arms on the animated pose when it comes back |
follow |
0 |
0–1 of the character's travel the chain takes rigidly (0 = the full whip on a dash) |
wind |
[0, 0, 0] |
World wind, m/s² |
floor |
'none' |
'probe' (a ray down from the chain root against the physics world, every frame), a height (a plane at that world y) or a world point (a plane through it, floorNormal up) |
floorNormal |
[0, 1, 0] |
The floor plane's normal when floor is a point |
floorFriction |
0.5 |
Coulomb coefficient against the floor (0 = ice, 1 = a rubber sole) |
colliders |
'auto' |
'auto' = every DynamicBoneCollider on the same model; 'humanoid' = those plus capsules generated on the standard humanoid bones (hips, spine, head, limbs — the Ragdoll's layout); 'none'; or the nodes carrying the colliders to use |
bones |
the whole subtree | Only these bone names join the chain (the root always does; a bone left out takes its subtree with it) |
depth |
0 |
Depth limit under the root (0 = none) |
pinned, pinGive |
—, 0 |
Bones that ride the animation while their subtrees simulate — a cloak's strips under one anchor, listed round the ring; pinGive = metres a collider may push a pinned bone off its place |
link |
0 |
0–1: ties between the bones at the same depth of neighbouring columns (a cloak; neighbours in pinned order) |
side |
'none' |
The cloth's outside, for colliders marked oneSided: 'auto' = away from the root bone's axis, or a direction in the root bone's frame |
spin, spinInertia |
0, 0 |
The body's TURN: above 0 the chain lives in the parent bone's rotating frame, so a damped cloak rides a turn; its inertia comes back as frame forces × spin (1 = physical). spinInertia = the time constant, seconds, with which the cloth's rotation follows the body's (0 = glued to the turn) |
rate |
60 |
Substep rate, Hz (at most 4 substeps a frame) |
iterations |
4 |
Constraint passes per substep, 1–8 (a long rope may want 8) |
teleport |
1 |
A root jump longer than this many metres in one frame resets the chain instead of whipping it |
guideHold |
0 |
0–1: how firmly guide() targets hold through the constraint passes |
edges |
true |
Bone segments and links collide with the capsules too, not only the particles |
A child bone that carries a DynamicBone or a DynamicBoneCollider when the chain attaches is left
out of it, with its subtree (it is another chain, or the body).
Turn it off for a cutscene, reset after a teleport: weight, reset()
const main = async () => {
const scene = new Scene()
const fox = await Model.load(asset('./fox.glb'))
scene.add(fox)
scene.open()
const tail = fox.bone('tail')!.aspect(DynamicBone, { stiffness: [0.5, 0.15], gravity: 2 })
tail.dynamicBone.weight = 0 // a cutscene: the tail plays the animation exactly
tail.dynamicBone.weight = 1 // back to the simulation, re-armed on the animated pose
fox.position = [20, 0, 0] // a teleport…
tail.dynamicBone.reset() // …snaps the chain onto the pose instead of whipping it
}
main()weight blends in between (0–1). A root jump longer than teleport metres in one frame resets the
chain by itself; call reset() for a shorter jump or a camera cut.
Keep a tail or cape out of the character's body: DynamicBoneCollider
A capsule (or a sphere) on a body bone that the chains' bones cannot pass through — hips and chest under a tail, arms and back under a cape.
bone.aspect(DynamicBoneCollider, { radius?, offset?, end?, to?, oneSided? })| Field | Default | Meaning |
|---|---|---|
radius |
0.1 |
Metres, in the world — the bone's scale does not touch it |
offset |
[0, 0, 0] |
Start of the capsule, in the bone's local space |
end |
— | End of the capsule, in the bone's local space |
to |
— | A bone (by name) whose origin is the capsule's end, instead of end |
oneSided |
false |
The cloth belongs on the chain's side of it: while the capsule moves away from that side it puts a bone it ran into over to the side instead of dragging it (an arm swinging forward under a cape) |
Neither end nor to = a sphere at offset. A collider is picked up by every DynamicBone of the
same model with colliders: 'auto' or 'humanoid', or used where it is listed.
When it runs: after animation and IK, before skinning
The stage runs after the late phase and the IK chains, before skinning — the Animator, an active ragdoll and late-phase bone writes are its input.
a chain's bone read in update() still shows the animated pose — the stage runs after the
late phase. particles hold the simulation (as of the last frame), and updateBefore() reads the
previous frame's final pose.
See also
- Animator — the pose the chains follow
- IK and feet — solved just before this stage
- Model —
model.bone(name)