Additive layers
Open the live demo · Read the source · View on GitHub
An ordinary layer replaces the base's pose for the joints it covers. An additive layer does something else: it measures how far its own clip has moved from its own rest frame, and lays that difference on top of the base instead. This page turns three heads at the same steady rate, then nods the right two in the same way every couple of seconds. The left head only turns, so there is something to compare against.
Step 1: A clip that names its own rest frame #
The turn clip is a full revolution, one keyframe per quarter turn. The nod is
different: it names a referenceTime, the moment its own difference is
measured from. At that moment it asks for nothing, which is what makes it
safe to add rather than replace.
_turnTrack = _turnAroundY();
final AnimationClip turn = AnimationClip(
name: 'turn',
tracks: <AnimationTrack>[_turnTrack],
);
_nodTrack = _nodAroundX();
final AnimationClip nod = AnimationClip(
name: 'nod',
tracks: <AnimationTrack>[_nodTrack],
// The rest pose the nod is a difference from: at time zero it asks
// for nothing, so laid on top of a turn it leaves the turn alone.
referenceTime: 0.0,
);
Step 2: Play it additively #
playLayer takes a blend. AnimationBlend.additive turns "this clip's
pose" into "this clip's distance from its own rest frame, added to whatever
the base is already doing". The middle head gets this. The right head gets
the same clip with AnimationBlend.override, which is what a layer did
before there was a choice.
if (_additiveNod != null) _additive.layers.remove(_additiveNod);
_additiveNod = _additive.playLayer(
1,
wrap: AnimationWrap.once,
fadeIn: 0.0,
blend: AnimationBlend.additive,
);
Step 3: Advance it #
The nod is played again every couple of seconds. The override layer is taken off once it has finished: a layer stays at full weight on its last pose until somebody removes it, and that last pose is the head facing dead ahead.
_sinceNod += dt;
if (_sinceNod >= _period) {
_sinceNod -= _period;
_nodBoth();
}
for (final AnimationPlayer player in <AnimationPlayer>[
_plain,
_additive,
_override,
]) {
player.speed = turnSpeed;
}
_plain.update(dt);
_additive.update(dt);
_override.update(dt);
// A finished override layer would go on holding its last pose, which is
// the head facing dead ahead: taking it off is what lets the turn resume.
final AnimationLayer? finished = _overrideNod;
if (finished != null && finished.isFinished) {
_override.layers.remove(finished);
_overrideNod = null;
}
What to look at #
All three heads turn together. When the nod arrives, the middle head nods and keeps turning, because nothing replaces the turn's own value: the nod adds only the difference between where its clip is right now and where it sits at its own reference time. The right head does the nod too, but for as long as it lasts the head stops turning and faces front, then picks the turn up again. Drag Turn speed to zero and the difference shows most plainly: the middle head nods from wherever it stopped, the right head still snaps forward first.
Step 4: What this page checks #
The middle head has to be exactly the turn with the nod on top, and the right head has to look different from it while the nod plays.
final AnimationLayer? nod = _additiveNod;
if (nod == null) throw StateError('the additive head has no nod playing');
final Quaternion turn = _sample(_turnTrack, _additive.time);
final Quaternion delta =
(_sample(_nodTrack, 0.0).conjugated() * _sample(_nodTrack, nod.time))
..normalize();
if ((delta.w - 1.0).abs() < 1e-3) {
throw StateError('the nod has not moved yet; nothing to add');
}
Quaternion rotationOf(MeshNode head) =>
Quaternion.fromRotation(head.worldMatrix.getRotation());
final Quaternion added = rotationOf(_additiveHead);
if (_dot(added, (turn * delta)..normalize()) < 0.999) {
throw StateError('the middle head is not the turn with the nod on top');
}
if (_dot(added, rotationOf(_overrideHead)) > 0.999) {
throw StateError(
'the right head should have had its turn replaced by the nod, and '
'looks the same as the one that kept it',
);
}