Undulation is the wave-like follow-through you see in tails, ropes, antennae, hair chains, and similar setups. Instead of manually animating every bone, we can animate only the root and simulate the rest of the chain. In this example, every bone tip behaves like a small point mass attached to a spring. The spring pulls the tip toward where the bone would normally be if it followed its parent rigidly. The tip is then constrained back to the bone length, and the bone is rotated to point toward it.
The whole system only needs a few things:
The chain’s rest pose.
A simulated tip position and velocity for each bone.
A spring-damper.
A length constraint.
A rotation that aims each bone toward its simulated tip.
The result is delayed motion, overshoot, and settling without needing a full physics solver.
Input
Simulation
Output
You can follow the tutorial by downloading and testing the script in 3ds Max to better understand the process.
The root bone is animated manually in the setup_scene function, while the remaining bones are stored inside chain. The root is moved and rotated over a few frames
3. Capturing the Rest Pose
Before simulation starts, we store each bone’s transform relative to its parent. we call this rest pose and it is the state that the bone will try to get back to all the time but forces like gravity and the stiffness parameters will fight gainst it.
rigidTM is basically the bone’s rigid target transform. basically where the bone would be of it followed its parent perfectly without any delay or other forces fighting against it.
4. Storing the Bone Tips
We also need the tip of every bone. For most bones, the child’s position gives us the tip. We convert that position into the current bone’s local space:
For the last bone, which has no child, the previous tip offset is reused. Once we have chain_tips, the world-space tip of a bone can be calculated with:
rigidTip = chain_tips[i] * rigidTM
5. Simulation State
Each bone has two values that persist between frames:
simTips
simVels
simTips[i] is the simulated world-space tip position. simVels[i] is its current velocity.
If we don’t do anything to the simVels. the simTips stay where they are in the world space and don’t follow the parent, but we use this Spring-Damper to calculate velocity, which is a 3d vector that tells us how much and in what direction to move to reach the rigidTip, the velocity is effected by two main controls:
_stiffness = .1
_damping = .7
_stiffness controls how strongly the tip is pulled toward rigidTip. Higher stiffness makes the chain react faster. Lower stiffness gives more lag.
_damping controls how much velocity survives each frame. Values closer to 1.0 keep the motion going longer, while lower values make it settle faster.
7. Keeping the Bone Length Fixed
The spring is free to move simTips[i] anywhere, which could stretch the bone. To prevent that, the tip is projected back to _bone_len from the bone position:
This means the bone can rotate, but it cannot change length. The spring controls the direction of the motion, while the constraint keeps the skeleton rigid.
8. Aiming the Bone
Now we have two directions:
rigidTip - chain[i].pos --> The direction of the rigid bone simTips[i] - chain[i].pos --> And the direction toward the simulated tip
tsQuatFromTo calculates the rotation between those directions:
Starting from rigidTM also means we preserve the original bone orientation and only add the swing required by the simulation.
This version does not simulate twist.
9. Why the Chain Undulates
The important part is the order of the loop:
for i=1 to chain.count do
The chain is evaluated from top to bottom. When bone 1 is simulated, bone 2 sees bone 1’s new transform. Bone 3 then sees bone 2’s new transform, and so on. Each bone adds a little spring delay and overshoot. As that delay accumulates down the hierarchy, the motion travels through the chain and forms a wave. There is no actual wave equation involved. The undulation appears naturally from each bone following the simulated result of the bone before it.
10. The Core Loop
The important part of the entire system is surprisingly small:
The bone finds where it should normally be, its simulated tip chases that position, the tip is constrained to the correct length, and the bone is rotated toward it. Repeating that process down the hierarchy is enough to produce convincing tail-style follow-through and undulation.
Hey, I read and reimplemented your algorithm and it works really well ! A few things that I felt were missing :
– on any bone, .transform/.pos is in world space
In Section 8 :
– tsQuatFromTo > what is A and B ? this one is pretty straight forward, A is first arg and B is second arg
– tsRotateTMAboutPivot > kind of same problem for this one with tm / q / p ? tm and q were easily deduced being we pass a transform and a quat, but p I had to look up on the GitHub and it’s tm.pos
Thank you !
That is correct, probably better naming would have made it easier to understand but these names are usually used in most physics papers and well known
Variable names like:
v > velocity
p > position
tm > transform matrix
q > quaternion
Hey, I read and reimplemented your algorithm and it works really well ! A few things that I felt were missing :
– on any bone, .transform/.pos is in world space
In Section 8 :
– tsQuatFromTo > what is A and B ? this one is pretty straight forward, A is first arg and B is second arg
– tsRotateTMAboutPivot > kind of same problem for this one with tm / q / p ? tm and q were easily deduced being we pass a transform and a quat, but p I had to look up on the GitHub and it’s tm.pos
Thank you !
That is correct, probably better naming would have made it easier to understand but these names are usually used in most physics papers and well known
Variable names like:
v > velocity
p > position
tm > transform matrix
q > quaternion