Files
UnrealEngineUWP/Engine/Source/Runtime/Engine/Private/MorphTools.cpp
Alexis Matte 9978edc658 Fix crash when deleting morph target, also delete the raw data so the morph target get deleted properly
#jira UE-168068
#rb jeanluc.corenthin
#rnx
#preflight 63c0647ea06ab115ea7aa818

[CL 23668173 by Alexis Matte in ue5-main branch]
2023-01-12 15:05:24 -05:00

335 lines
11 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
/*=============================================================================
MorphTools.cpp: Morph target creation helper classes.
=============================================================================*/
#include "Engine/SkeletalMesh.h"
#include "EngineLogs.h"
#include "Rendering/SkeletalMeshLODModel.h"
#include "Algo/AnyOf.h"
#include "Interfaces/ITargetPlatform.h"
#include "UObject/FortniteMainBranchObjectVersion.h"
#include "UObject/UE5PrivateFrostyStreamObjectVersion.h"
FArchive& operator<<(FArchive& Ar, FMorphTargetLODModel& M)
{
Ar.UsingCustomVersion(FEditorObjectVersion::GUID);
Ar.UsingCustomVersion(FFortniteMainBranchObjectVersion::GUID);
Ar.UsingCustomVersion(FUE5PrivateFrostyStreamObjectVersion::GUID);
if (!Ar.IsObjectReferenceCollector())
{
if (Ar.IsLoading() && Ar.CustomVer(FEditorObjectVersion::GUID) < FEditorObjectVersion::AddedMorphTargetSectionIndices)
{
Ar << M.Vertices << M.NumBaseMeshVerts;
M.bGeneratedByEngine = false;
}
else if (Ar.IsLoading() && Ar.CustomVer(FFortniteMainBranchObjectVersion::GUID) < FFortniteMainBranchObjectVersion::SaveGeneratedMorphTargetByEngine)
{
Ar << M.Vertices << M.NumBaseMeshVerts << M.SectionIndices;
M.bGeneratedByEngine = false;
}
else
{
bool bVerticesAreStrippedForCookedBuilds = false;
if (Ar.IsPersistent() && (Ar.CustomVer(FUE5PrivateFrostyStreamObjectVersion::GUID) >= FUE5PrivateFrostyStreamObjectVersion::StripMorphTargetSourceDataForCookedBuilds))
{
// Strip source morph data for cooked build if targets don't include mobile. Mobile uses CPU morphing which needs the source morph data.
bVerticesAreStrippedForCookedBuilds = Ar.IsCooking() && (!Ar.CookingTarget()->SupportsFeature(ETargetPlatformFeatures::MobileRendering));
Ar << bVerticesAreStrippedForCookedBuilds;
}
if (bVerticesAreStrippedForCookedBuilds)
{
M.NumVertices = M.Vertices.Num();
Ar << M.NumVertices;
}
else
{
Ar << M.Vertices;
if (Ar.IsLoading())
{
M.NumVertices = M.Vertices.Num();
}
}
Ar << M.NumBaseMeshVerts << M.SectionIndices << M.bGeneratedByEngine;
}
}
return Ar;
}
/** compare based on base mesh source vertex indices */
struct FCompareMorphTargetDeltas
{
FORCEINLINE bool operator()( const FMorphTargetDelta& A, const FMorphTargetDelta& B ) const
{
return ((int32)A.SourceIdx - (int32)B.SourceIdx) < 0 ? true : false;
}
};
const FMorphTargetDelta* UMorphTarget::GetMorphTargetDelta(int32 LODIndex, int32& OutNumDeltas) const
{
if(LODIndex < MorphLODModels.Num())
{
// Calling GetMorphLODModels to potentially get from subclasses
const FMorphTargetLODModel& MorphModel = GetMorphLODModels()[LODIndex];
OutNumDeltas = MorphModel.Vertices.Num();
return MorphModel.Vertices.GetData();
}
OutNumDeltas = 0;
return NULL;
}
bool UMorphTarget::HasDataForLOD(int32 LODIndex) const
{
// If we have an entry for this LOD, and it has verts
#if WITH_EDITOR
return (MorphLODModels.IsValidIndex(LODIndex) && MorphLODModels[LODIndex].Vertices.Num() > 0);
#else
// Morph target's vertices array could have been emptied after render data is created, so check NumVertices instead
return (MorphLODModels.IsValidIndex(LODIndex) && MorphLODModels[LODIndex].NumVertices > 0);
#endif
}
bool UMorphTarget::HasValidData() const
{
for (const FMorphTargetLODModel& Model : MorphLODModels)
{
if (Model.Vertices.Num() > 0)
{
return true;
}
#if !WITH_EDITOR
// In cooked builds, Model.Vertices is stripped but Model.NumVertices is valid
else if (Model.NumVertices > 0)
{
return true;
}
#endif
}
return false;
}
bool UMorphTarget::HasDataForSection(int32 LODIndex, int32 SectionIndex) const
{
return HasDataForLOD(LODIndex) && MorphLODModels[LODIndex].SectionIndices.Contains(SectionIndex);
}
void UMorphTarget::EmptyMorphLODModels()
{
MorphLODModels.Empty();
}
void UMorphTarget::DiscardVertexData()
{
for (FMorphTargetLODModel& Model : MorphLODModels)
{
Model.DiscardVertexData();
}
}
#if WITH_EDITOR
void UMorphTarget::PopulateDeltas(const TArray<FMorphTargetDelta>& Deltas, const int32 LODIndex, const TArray<FSkelMeshSection>& Sections, const bool bCompareNormal, const bool bGeneratedByReductionSetting, const float PositionThreshold)
{
// create the LOD entry if it doesn't already exist
if (LODIndex >= MorphLODModels.Num())
{
MorphLODModels.AddDefaulted(LODIndex - MorphLODModels.Num() + 1);
}
// morph mesh data to modify
FMorphTargetLODModel& MorphModel = MorphLODModels[LODIndex];
// copy the wedge point indices
// for now just keep every thing
// set the original number of vertices
MorphModel.NumBaseMeshVerts = Deltas.Num();
// empty morph mesh vertices first
MorphModel.Vertices.Empty(Deltas.Num());
// mark if generated by reduction setting, so that we can remove them later if we want to
// we don't want to delete if it has been imported
MorphModel.bGeneratedByEngine = bGeneratedByReductionSetting;
// Still keep this (could remove in long term due to incoming data)
for (const FMorphTargetDelta& Delta : Deltas)
{
if (Delta.PositionDelta.SizeSquared() > FMath::Square(PositionThreshold) ||
( bCompareNormal && Delta.TangentZDelta.SizeSquared() > 0.01f))
{
MorphModel.Vertices.Add(Delta);
for (int32 SectionIdx = 0; SectionIdx < Sections.Num(); ++SectionIdx)
{
if (MorphModel.SectionIndices.Contains(SectionIdx))
{
continue;
}
const uint32 BaseVertexBufferIndex = (uint32)(Sections[SectionIdx].GetVertexBufferIndex());
const uint32 LastVertexBufferIndex = (uint32)(BaseVertexBufferIndex + Sections[SectionIdx].GetNumVertices());
if (BaseVertexBufferIndex <= Delta.SourceIdx && Delta.SourceIdx < LastVertexBufferIndex)
{
MorphModel.SectionIndices.AddUnique(SectionIdx);
break;
}
}
}
}
// sort the array of vertices for this morph target based on the base mesh indices
// that each vertex is associated with. This allows us to sequentially traverse the list
// when applying the morph blends to each vertex.
MorphModel.Vertices.Sort(FCompareMorphTargetDeltas());
// remove array slack
MorphModel.Vertices.Shrink();
MorphModel.NumVertices = MorphModel.Vertices.Num();
}
void UMorphTarget::RemoveEmptyMorphTargets()
{
// do not remove LOD 0
for (int32 CurIndex = MorphLODModels.Num() - 1; CurIndex > 0; --CurIndex)
{
// if it's empty, no reason to keep
if (MorphLODModels[CurIndex].Vertices.Num() == 0)
{
MorphLODModels.RemoveAt(CurIndex);
}
else
{
// we leave empty ones between. We can't remove between ones.
// Once we found valid one, just get out
break;
}
}
}
TUniquePtr<FFinishBuildMorphTargetData> UMorphTarget::CreateFinishBuildMorphTargetData() const
{
return MakeUnique<FFinishBuildMorphTargetData>();
}
void FFinishBuildMorphTargetData::ApplyEditorData(USkeletalMesh * SkeletalMesh, bool bIsSerializeSaving) const
{
//Return if we do not need to apply data
if (!bApplyMorphTargetsData)
{
return;
}
if (SkeletalMesh->HasAnyFlags(RF_BeginDestroyed))
{
//No need to apply the morph targets if the asset is being deleted.
//Also acquiring the GCScopeGuard when we are in the destruction process will create a deadlock, if there is code
//in the BeginDestroy function of the skeletalmesh that touch any property that is lock by the async build.
return;
}
//Make sure we do not create new morph target during a gc
FGCScopeGuard GCScopeGuard;
FSkeletalMeshModel * SkelMeshModel = SkeletalMesh->GetImportedModel();
check(SkelMeshModel);
TMap<FName, UMorphTarget*> ExistingMorphTargets;
for (UMorphTarget* MorphTarget : SkeletalMesh->GetMorphTargets())
{
ExistingMorphTargets.Add(MorphTarget->GetFName(), MorphTarget);
}
int32 MorphTargetNumber = MorphLODModelsPerTargetName.Num();
TArray<UMorphTarget*> ToDeleteMorphTargets;
ToDeleteMorphTargets.Append(SkeletalMesh->GetMorphTargets());
SkeletalMesh->GetMorphTargets().Empty();
//Rebuild the MorphTarget object
for (const TPair<FName, TArray<FMorphTargetLODModel>>& TargetNameAndMorphLODModels : MorphLODModelsPerTargetName)
{
FName MorphTargetName = TargetNameAndMorphLODModels.Key;
const TArray<FMorphTargetLODModel>& MorphTargetLODModels = TargetNameAndMorphLODModels.Value;
int32 MorphLODModelNumber = MorphTargetLODModels.Num();
UMorphTarget * MorphTarget = ExistingMorphTargets.FindRef(MorphTargetName);
if (!MorphTarget)
{
if (!Algo::AnyOf(MorphTargetLODModels, [](const FMorphTargetLODModel& Model) { return Model.Vertices.Num() > 0;}))
{
//Skip this empty morphtarget
continue;
}
//When we save the cook result we should never have to build a new morph target
//When saving cook build, we call GetPlatformSkeletalMeshRenderData in USkeletalMesh::BeginCacheForCookedPlatformData
//which happen before the serialization of that cook skeletalmesh
if (!bIsSerializeSaving)
{
//Avoid recycling morphtarget with NewObject it cannot be done asynchronously
//Find the UMorphTarget and simply clear the data if it exist.
TArray<UObject*> SubObjects;
GetObjectsWithOuter(SkeletalMesh, SubObjects, true);
for (UObject* SubObject : SubObjects)
{
if (SubObject->GetFName() == MorphTargetName)
{
if (UMorphTarget* SubMorphTarget = Cast<UMorphTarget>(SubObject))
{
MorphTarget = SubMorphTarget;
MorphTarget->EmptyMorphLODModels();
MorphTarget->ClearGarbage();
break;
}
}
}
//Create a new morph target, if the object do not exist (creating a new uobject is ok to do asynchronously)
if (!MorphTarget)
{
MorphTarget = NewObject<UMorphTarget>(SkeletalMesh, MorphTargetName);
}
check(MorphTarget);
}
else
{
UE_ASSET_LOG(LogSkeletalMesh, Error, SkeletalMesh, TEXT("Cannot cache a skeletalmesh during a serialize if some morph targets need to be created. The solution is to Pre cache the skeletalmesh before the serialization so no morph target get created."));
continue;
}
}
else
{
ToDeleteMorphTargets.Remove(MorphTarget);
}
MorphTarget->EmptyMorphLODModels();
SkeletalMesh->GetMorphTargets().Add(MorphTarget);
MorphTarget->GetMorphLODModels().AddDefaulted(MorphLODModelNumber);
for (int32 MorphDataIndex = 0; MorphDataIndex < MorphLODModelNumber; ++MorphDataIndex)
{
MorphTarget->GetMorphLODModels()[MorphDataIndex] = MorphTargetLODModels[MorphDataIndex];
}
}
//Rebuild the mapping and rehook the curve data
SkeletalMesh->InitMorphTargets();
//Clear any async flags after the morphtargets have been set to the skeletalmesh
for (UMorphTarget* MorphTarget : SkeletalMesh->GetMorphTargets())
{
const EInternalObjectFlags AsyncFlags = EInternalObjectFlags::Async | EInternalObjectFlags::AsyncLoading;
MorphTarget->ClearInternalFlags(AsyncFlags);
}
for (UMorphTarget* ToDeleteMorphTarget : ToDeleteMorphTargets)
{
ToDeleteMorphTarget->BaseSkelMesh = nullptr;
ToDeleteMorphTarget->EmptyMorphLODModels();
ToDeleteMorphTarget->MarkAsGarbage();
}
}
#endif // WITH_EDITOR