"Keep quaternion keys hemisphere-continuous"
- Scripted animation keyframes look correct at each key pose, but the character explodes between keys — limbs swing a full 360 degrees during interpolation.
- The artifact only appears in transitions, never at the keyed frames themselves.
- The problem is intermittent — some rotations interpolate fine, others violently detour.
Quaternions q and -q represent the same orientation but interpolation between them takes opposite paths around the 4D sphere. When you convert euler angles to quaternions per-keyframe independently, some keys may land on q and their neighbors on -q. The interpolator (slerp) doesn't know they're equivalent and takes the long path — a near-360 degree swing instead of a tiny step.
This is invisible at the key poses (both q and -q produce the same visible rotation) and only manifests during the blend.
Negate the quaternion if it's on the wrong hemisphere:
# Python (Blender scripted keyframes)
import mathutils
prev_q = None
for frame in keyframes:
q = compute_rotation(frame) # euler to quat
if prev_q is not None and prev_q.dot(q) < 0:
q = -q # flip to same hemisphere
set_keyframe(bone, frame, q)
prev_q = q// C# equivalent
Quaternion prev = Quaternion.Identity;
foreach (var key in keys)
{
var q = key.Rotation;
if (Quaternion.Dot(prev, q) < 0f)
q = -q;
ApplyKey(bone, key.Frame, q);
prev = q;
}The check is one dot product per key. Apply it at authoring time (when generating keyframes) — the runtime interpolator doesn't need to change.
A quaternion lives on the surface of a 4D unit sphere. Slerp always takes the shorter arc between two points on that sphere. When q and -q are on opposite hemispheres (dot product < 0), the "short arc" is actually the long way around in 3D rotation space. Negating one to match the other's hemisphere ensures slerp takes the minimal rotation path — which is what you see as a smooth transition instead of a violent spin.
This applies to any scripted keyframe pipeline — Blender Python, runtime procedural animation, or custom FBX exporters. The engine's own AnimGraph cross-fades handle this internally, but direct sequence playback with scripted keys does not.