[UE 5.7] Morph Targets fail to reset to zero (visual freeze) due to missing update logic in FSkeletalMeshObjectGPUSkin

Summary

We have encountered a regression in UE 5.7 where Skeletal Mesh Morph Targets do not visually reset when all weights are set to 0.0.

The mesh retains the deformation from the previous frame. This issue was not present in UE 5.5.After analyzing the render thread code, it appears to be caused by changes in how bMorphNeedsUpdate is handled and a missing check for the “transition to zero” state in

FSkeletalMeshObjectGPUSkin.

Technical Analysis

We compared the implementation of FSkeletalMeshObjectGPUSkin

between UE 5.5 and UE 5.7.1.

UE 5.5 (Correct Behavior) In 5.5,UpdateDynamicData_RenderThread compares the previous and current frame’s active morphs. Even if the current frame has no active morphs (weights are 0), ActiveMorphTargetsEqual returns false (because the previous frame had morphs), setting

bMorphNeedsUpdate to true.

C++

// UE 5.5 Logic
bMorphNeedsUpdate = 
    InDynamicData->ExternalMorphWeightData.HasActiveMorphs() ||
    (DynamicData ? (DynamicData->LODIndex != InDynamicData->LODIndex ||
    !DynamicData->ActiveMorphTargetsEqual(InDynamicData->ActiveMorphTargets, InDynamicData->MorphTargetWeights)) // <--- Detected change here
    : true);

This ensured ProcessUpdatedDynamicData was called to clear the buffer.

2. UE 5.7 (The Issue) In 5.7, bMorphNeedsUpdate has become a member variable, and the update logic inside FSkeletalMeshObjectGPUSkin::ProcessUpdatedDynamicData seems to optimize too aggressively when there are no active morphs.

C++

// UE 5.7 FSkeletalMeshObjectGPUSkin::ProcessUpdatedDynamicData
 
const bool bHasWeightedActiveMorphs = DynamicData->NumWeightedActiveMorphTargets > 0;
// ...
if (GEnableMorphTargets && ... && (bHasWeightedActiveMorphs || bHasExternalMorphs))
{
    // Normal update path
    if (bMorphNeedsUpdate) { ... }
}
else
{
    // Regression Cause:
    // If weights become 0, we enter this block.
    // The code assumes we don't need to do anything, effectively skipping the "Clear" pass.
    MorphVertexBuffer = nullptr;
    bMorphNeedsUpdate = false; 
}

Because it enters the else block immediately, the render thread never executes the logic to bind a cleared/null buffer or reset the existing one. The GPU continues to use the dirty buffer from the previous frame.

Workaround

We confirmed this analysis by setting the weight to 0.001f instead of 0.f.

This forces bHasWeightedActiveMorphs to true, triggering the update path, and the mesh updates correctly.

Request

Could you confirm if this logic change was intended? It seems the logic needs a check to see if “morphs were active last frame” to perform a one-time clear/reset.

[Attachment Removed]

재현 방법

  1. Apply any Morph Target weight (e.g., 1.0) to a Skeletal Mesh.
  2. In the next frame, set all Morph Target weights to 0.f.
  3. Result: The mesh visually stays at 1.0 (or the last non-zero value).
  4. Expected: The mesh should revert to its base pose (zero deformation).
    [Attachment Removed]

Hi there,

Thank you for the detailed description of the issue and reproduction steps. I was able to reproduce the issue as described.

I tend to agree with your analysis, and I don’t believe this new behaviour is intentional. I don’t see this issue currently on the issue tracker, so I have submitted a new bug report. Once it is made publicly available, you will be able to view it here: Unreal Engine Issues and Bug Tracker (UE\-359709)

I also agree that providing a very low weight is probably the best low-effort workaround for the time being.

An engine dev will investigate the bug at a later date. We don’t provide updates on EPS, but progress can be followed on that public issue tracker page. I will close this case now, but feel free to respond here if you have any follow-up questions.

Thanks,

Hayden

[Attachment Removed]