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

TypeScript
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

TypeScript
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, [] clears

Any 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()

TypeScript
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.

TypeScript
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.

Note

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