You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
#jira UE-168068 #rb jeanluc.corenthin #rnx #preflight 63c0647ea06ab115ea7aa818 [CL 23668173 by Alexis Matte in ue5-main branch]
335 lines
11 KiB
C++
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
|
|
|
|
|