FPreAnimatedSkeletalAnimationTraits::CachePreAnimatedValue caches wrong AnimScriptInstance

Hello!

We have animated skeletal meshes bound in a Level Subsequence with additive Control Rig tracks, and the subsequence is starting before the first frame. We’ve found several simple actions, such as generating Camera Cut thumbnails or dragging the playhead off and then back onto the playable range of the subsequence results in a temporary AnimScriptInstance being cached/restored to our skeletal meshes, breaking the skeletal meshes’ connection to their Animation Blueprint (and thus breaking slots/blending).

It appears Skeletal Meshes that have additive Control Rigs have ```FAnimCustomInstanceHelper::BindToSkeletalMeshComponent``` and ```FAnimCustomInstanceHelper::UnbindFromSkeletalMeshComponent``` called on them repeatedly on initialization, assigning and then unassigning ControlRigLayerInstances that then use the mesh’s default AnimInstance as the SourceAnimInstance. This appears normal.

However, we noticed that in the case where our content subsequences that house animations and ControlRigs are nudged in the negative from the first frame of our root, we’re pretty consistently seeing the following callstack occuring while that temporary ControlRigLayerInstance is assigned, caching it incorrectly instead of our Animation Blueprint instance:

UE::MovieScene::FPreAnimatedSkeletalAnimationTraits::CachePreAnimatedValue(const FObjectKey &) MovieSceneSkeletalAnimationSystem.cpp:97 [Inlined] UE::MovieScene::TPreAnimatedStateStorage_ObjectTraits::CachePreAnimatedValue(const UE::MovieScene::FCachePreAnimatedValueParams &, const UE::MovieScene::FPreAnimatedStateEntry &, UObject *) MovieScenePreAnimatedObjectStorage.h:186 UE::MovieScene::TPreAnimatedStateStorage_ObjectTraits::CachePreAnimatedValue(const UE::MovieScene::FCachePreAnimatedValueParams &, UObject *, EPreAnimatedCaptureSourceTracking) MovieScenePreAnimatedObjectStorage.h:175 UE::MovieScene::FEvaluateSkeletalAnimations::EvaluateSkeletalAnimations(USkeletalMeshComponent *, const UE::MovieScene::FBoundObjectActiveSkeletalAnimations &) MovieSceneSkeletalAnimationSystem.cpp:515 UE::MovieScene::FEvaluateSkeletalAnimations::Run() MovieSceneSkeletalAnimationSystem.cpp:489 UE::MovieScene::FScheduledTask::Run(const UE::MovieScene::FEntitySystemScheduler *, FTaskExecutionFlags) MovieSceneTaskScheduler.cpp:101 UE::MovieScene::FEntitySystemScheduler::PrerequisiteCompleted(const UE::MovieScene::FScheduledTask *, int *) MovieSceneTaskScheduler.cpp:650 [Inlined] UE::MovieScene::FEntitySystemScheduler::PrerequisiteCompleted(FTaskID, int *) MovieSceneTaskScheduler.cpp:627 UE::MovieScene::FEntitySystemScheduler::CompleteTask(const UE::MovieScene::FScheduledTask *, FTaskExecutionFlags) MovieSceneTaskScheduler.cpp:599 UE::MovieScene::FScheduledTask::Run(const UE::MovieScene::FEntitySystemScheduler *, FTaskExecutionFlags) MovieSceneTaskScheduler.cpp:160 UE::MovieScene::FEntitySystemScheduler::ExecuteTasks() MovieSceneTaskScheduler.cpp:494 [Inlined] FMovieSceneEntitySystemGraph::ScheduleTasks(UE::MovieScene::FEntityManager *) MovieSceneEntitySystemGraphs.cpp:593 FMovieSceneEntitySystemRunner::GameThread_EvaluationPhase(UMovieSceneEntitySystemLinker *) MovieSceneEntitySystemRunner.cpp:1077 FMovieSceneEntitySystemRunner::FlushNext(UMovieSceneEntitySystemLinker *) MovieSceneEntitySystemRunner.cpp:375 FMovieSceneEntitySystemRunner::FlushOutstanding(double, ERunnerFlushState) MovieSceneEntitySystemRunner.cpp:512 FMovieSceneRootEvaluationTemplateInstance::EvaluateSynchronousBlocking(FMovieSceneContext) MovieSceneEvaluationTemplateInstance.cpp:186 FSequencer::EvaluateInternal(FMovieSceneEvaluationRange, bool) Sequencer.cpp:3554 FSequencer::Tick(float) Sequencer.cpp:1097This will then be restored incorrectly to our mesh, and when trying to create an AnimSequencerInstance to interact with Sequencer, we don’t have a valid SourceAnimInstance to supply to the new instance, so Sequencer will just play the animation tracks with no blending.

Also, at this point, nothing expects to restore our animation instance to the original AnimBP instance, so it stays in a bad state until something resets the AnimInstance back to an instance of the Animation Blueprint.

Any help in finding a fix for this would be appreciated.

Andres

Steps to Reproduce

  1. Create a Level Sequence
  2. Create a child Subsequence and put it into a Subsequence track in your Root Sequence
  3. Create an actor with a skeletal mesh and a simple Animation Blueprint on their mesh
  4. Create a slot on the character Skeleton such as UpperBody.UpperBody, and blend in the slot on a bone like spine_02
  5. In your child sequence, add an animation that’s playing on the UpperBody slot
  6. Add any additive control rig to the child
  7. In your Root sequence, offset your child sequence -1 second from the start

Repro option 1:

  1. Scrub/click your playhead off of the range of the subsequence
  2. Scrub/click your playhead back to within the range of the subsequence

Repro option 2:

  1. In your Root Sequence, create a camera cut track
  2. Spawn a camera, then assign a camera to your track, and allow the thumbnails to generate

At this point, the animation should now be playing on the full body instead of being layered, because Sequencer has stomped the mesh’s AnimScriptInstance with an older ControlRigLayerInstance with no SourceAnimInstance, thus not being able to assign the AnimationBlueprint as the source for the AnimSequencerInstance.

I did some testing in vanilla 5.5.4 and I’m finding I can’t reproduce our *exact* issue; I suspect we’re running into this problem due to backporting we’ve done of features from 5.5.4 to a slightly older version.

However, I was able to achieve what I think is the same state very easily. The actor instance can indeed get into a bad state interfacing with Sequencer v Control Rig, right from the start.

Unlike our project, this doesn’t recur on reopening the sequence, and does not persist once you reload the level. So uploading the project isn’t useful. But it only requires one single level sequence, and I can share steps.

  1. Create a project with the ThirdPerson Template
  2. In the default map, place a BP_ThirdPersonCharacter
  3. In their animation blueprint, add a Layered Blend Per Bone that blends their UpperBody slot into their regular pose (change in screenshot below)
  4. Add a Level Sequence Actor in the level and create a new level sequence for it
  5. Add the BP_ThirdPersonCharacter to the Level Sequence. It’ll be added with a Control Rig.
  6. Add an animation to the Third Person Character’s mesh, like FwdRun. Swap the animation section’s Slot name to UpperBody.
  7. Delete the Control Rig Track

[Image Removed]

You’ll find Sequencer doesn’t gracefully return to an AnimScriptInstance of the AnimationBlueprint, and the animation will not blend on the upper body - it’ll run via an AnimSequencerInstance, and play on the whole body, and in the AnimationBlueprint you won’t be able to find a valid Debug Object that matches the Animation Blueprint. Even if you close and reopen Sequencer, the skeletal mesh will remain in this bad state using the old ControlRigLayerInstance.

I’m pretty sure these are the same issue. Something in our code is causing this to trigger every time on loading the sequence, and nothing is able to recover from this bad state, rendering the skeletal mesh broken from then forward. I suspect if there’s a fix for this bug, it might be able to be leveraged to fix ours.

I was able to find a temporary (albeit ugly and imperfect) workaround by modifying the engine source to get our team unblocked.

I modified ControlRigLayerInstanceProxy.h/cpp to cache the proxy’s last valid AnimSource in a weak pointer. If the CurrentSourceAnimInstance is not valid, we return the contents of that weak pointer in GetSourceAnimInstance() instead of null.

This short circuits the branching case in FAnimCustomInstanceHelper::BindToSkeletalMeshComponent where we find an old cached ControlRigLayerInstance assigned with no current AnimSource and assign an AnimSequencerInstance as the new source (AnimCustomInstanceHelper.h ~ln 122). Instead it will will forward to the

```if (bCreateSequencerInterface && bSupportDifferentSourceAnimInstance && !CurrentSequencerInterface->DoesSupportDifferentSourceAnimInstance())```

case, and keep the Animation Blueprint instance as the AnimSource as desired.

The fix is imperfect, and results in cases where the mesh can to stop playing Sequencer animations in Editor occasionally; However, it allows our team to jostle the playhead and get the meshes back in the correct state, giving them a means to fix the issue and get themselves unblocked.

I don’t think this is the right direction for a proper solution, as it seems we shouldn’t be caching what instances were used before Sequencer and getting a ControlRigLayerInstance that was assigned as part of Sequencer initialization. If there is a better way to approach this problem, any assistance would be very appreciated.

Andres

Hey There,

Just wanted to let you know that I’ve reproed the cases above and I’m just confering with the dev team on the issue you’ve raised.

Thanks for your patience,

Dustin

Can you see if AnimationUIFlickerFixActive() is active for you all in MovieSceneSkeletalAnimationSystem.cpp, line 603? That was a fix for an issue that may be related to what you are seeing here.

Hey, Mike! Thanks for your response!

We haven’t pulled the AnimationUIFlickerFixActive() change, so in our code this logic controlled is only activated by the cvar Sequencer.Animation.UIFlickerFix. It wasn’t true for us, so I’ve rolled back my hack fix and I’m testing with this turned on.

So far, with the UIFlickerFix on, it appears to be addressing the problem! This is promising, but I’ll report back next week after we’ve done some more testing!

Andres

Thank you, Dustin!