Summary
Frame-referenced additive animations sample their base pose at the wrong time: RefFrameIndex / NumberOfSampledKeys instead of RefFrameIndex / NumberOfFrames. Every Ref Frame Index except 0 is sampled early by RefFrameIndex / NumberOfSampledKeys of a frame. That’s about half a frame for a middle reference and almost a whole frame for the last one. So the additive’s delta is not identity at its own reference frame, and the whole additive is offset by a constant amount. In aim offsets this shows up as visibly misaligned weapon sights.
What Type of Bug are you experiencing?
Animation
Steps to Reproduce
- Create an AnimSequence of 30 frames at 30 fps (31 keys, 1.0 s) in which a bone rotates linearly at 6° per frame, e.g. −90° at frame 0, 0° at frame 15, +90° at frame 30.
- In its Additive Settings, set Additive Anim Type to Mesh Space (Local Space is also affected), Base Pose Type to Selected Local animation frame, and Ref Frame Index to 15. Save so the additive is compressed.
- In an Animation Blueprint, apply it with Apply Mesh Space Additive on top of a base pose identical to frame 15. Drive it with a Sequence Evaluator at Explicit Time 0.5 s, which is frame 15.
- Compare the output bone rotation with the base pose, or with the raw frame-15 pose.
- Optionally, repeat with Base Pose Type Selected animation frame, Base Pose Animation set to the same anim, and frame 15: same result.
- Optionally, repeat with Ref Frame Index 0: no offset.
Expected Result
The additive base pose is sampled at the time of key RefFrameIndex: 15 × (1.0 s ÷ 30) = 0.5 s. At the reference frame the additive delta is identity, so the output equals the base pose exactly.
Observed Result
The base pose is sampled at 15 ÷ 31 × 1.0 s = 0.4839 s, which is 0.484 frames early. With the reference sampled early, the bone comes out 2.90° off (0.484 frames × 6°) at the reference frame. The offset is the same at every evaluation time.
In a first-person rifle aim offset built this way (±90° pitch over 30 frames), the weapon pointed 2.9° above the camera at every pitch. Every bone matched the anim at the camera’s pitch plus 2.90°, to within 0.01°. After the workaround below, the in-game pose matched the anim at the camera’s pitch with a fit error of 0.001°.
Affects Versions
5.8
Platform(s)
Windows
Additional Notes
Confirmed in Unreal Engine 5.8. The same code appears to be present throughout UE5. UE4’s equivalent used NumFrames, which in UE4 held the key count, so it has the same off-by-one.
- Location: Engine/Source/Runtime/Engine/Private/Animation/AnimSequence.cpp, static GetSequencePose(), called from UAnimSequence::GetAdditiveBasePose() for ABPT_AnimFrame and ABPT_LocalAnimFrame:
const double Fraction = (Seq.GetNumberOfSampledKeys() > 0)
? FMath::Clamp((double)FrameIndex / (double)Seq.GetNumberOfSampledKeys(), 0.0, 1.0) : 0.0;
const double BasePoseTime = Seq.GetPlayLength() * Fraction;
-
Size of the error: RefFrameIndex / NumberOfSampledKeys frames. It’s exact only at index 0, about 0.5 frames at a middle reference frame, and almost 1 frame at the last frame.
-
Inconsistency: the same formula is in the attribute base-time lambda in UAnimSequence (ABPT_AnimFrame || ABPT_LocalAnimFrame branch). The curve path in AnimCompressionTypes.cpp does it correctly with GetTimeAtFrame(RefFrameIndex). So the bones and the curves of one additive use different base times.
-
Suggested fix: use Seq.GetTimeAtFrame(FrameIndex) in GetSequencePose(), or divide by GetNumberOfFrames(). Do the same in the attribute lambda. Fixing it will change the compressed result of every existing frame-referenced additive.
Workaround: use frame 0 as the reference. For example, set Base Pose Type to Selected animation frame, Base Pose Animation to a separate anim holding the neutral pose, and Ref Frame Index to 0.