Hello,
I’ve been trying to generate a skeletal mesh with morph targets.
The reason I’m doing this, is because it is a modular character, with lerping between morph targets.
I can render the character, but the morph target does not seem to be working at all, probably because of the following check:
Error: Ensure condition failed: DynamicData->MorphTargetWeights.Num() == LODData.MorphTargetVertexInfoBuffers.GetNumMorphs() [File:D:\build\++UE5\Sync\Engine\Source\Runtime\Engine\Private\SkeletalRenderGPUSkin.cpp] [Line: 937]
LODData.MorphTargetVertexInfoBuffers.GetNumMorphs() seems to return 0, however the morph target does exist on the mesh. I have also given the merged mesh CPU and GPU access just to be sure.
Here is my code, sorry if it’s not great - I’m only 3 days into learning C++:
USkeletalMesh* URenderer::MergeSkeletalMeshesWithMorphTargets(USkeletalMeshComponent* PlayerMesh, const TArray<USkeletalMesh*> MeshesToMerge, const FMeshParts MeshPartsA, const FMeshParts MeshPartsB, const float LerpAmount)
{
// Return if base mesh null
if (!PlayerMesh)
{
UE_LOG(LogTemp, Warning, TEXT("No player mesh supplied."));
return nullptr;
}
if (MeshesToMerge.Num() == 0)
{
UE_LOG(LogTemp, Warning, TEXT("No meshes to merge."));
return nullptr;
}
// Set up new base mesh
USkeletalMesh* BaseMesh = NewObject<USkeletalMesh>(PlayerMesh, "BaseMesh");
// Return if base mesh null
if (!BaseMesh)
{
UE_LOG(LogTemp, Warning, TEXT("No base mesh supplied."));
return nullptr;
}
// Set up other optional params
const TArray<FSkelMeshMergeSectionMapping> SectionMappings;
const TArray<FSkelMeshMergeMeshUVTransforms> UvTransforms;
const int32 StripTopLODs = 0;
FSkelMeshMergeUVTransformMapping Mapping = FSkelMeshMergeUVTransformMapping();
Mapping.UVTransformsPerMesh = UvTransforms;
// Create a skeletal mesh merge utility object
FSkeletalMeshMerge MeshMerge = FSkeletalMeshMerge(
BaseMesh,
MeshesToMerge,
SectionMappings,
StripTopLODs,
EMeshBufferAccess::ForceCPUAndGPU,
&Mapping
);
// Perform the merge and get the resulting skeletal mesh
if (!MeshMerge.DoMerge())
{
UE_LOG(LogTemp, Warning, TEXT("Skeletal mesh merge failed."));
return nullptr;
}
// Calculate and aggregate morph target deltas
TArray<FMorphTargetDelta> NewDeltas;
TArray<FSkelMeshSection> Sections;
const int32 NumVertices = 0;
AppendDeltas(&NewDeltas, &Sections, MeshPartsA.Head, MeshPartsB.Head, LerpAmount);
AppendDeltas(&NewDeltas, &Sections, MeshPartsA.Body, MeshPartsB.Body, LerpAmount);
// Create a new morph target and populate it with our deltas
constexpr bool bCompareNormal = false;
constexpr bool bGeneratedByReductionSetting = true;
UMorphTarget* NewMorphTarget = NewObject<UMorphTarget>(BaseMesh, "LerpedMorph");
NewMorphTarget->PopulateDeltas(NewDeltas, 0, Sections, bCompareNormal, bGeneratedByReductionSetting);
// Add the new morph target to the base skeletal mesh
TArray<TObjectPtr<UMorphTarget>> MorphTargets = BaseMesh->GetMorphTargets();
MorphTargets.Add(NewMorphTarget);
BaseMesh->SetMorphTargets(MorphTargets);
BaseMesh->InitMorphTargets();
// Log number of local morph target deltas
int32 NumSrcDeltas = 0;
const FMorphTargetDelta* MorphDeltas = NewMorphTarget->GetMorphTargetDelta(LodIndex, NumSrcDeltas);
UE_LOG(LogTemp, Warning, TEXT("MorphDeltas: %d"), NumSrcDeltas);
// Log number of morph targets in the render data
FSkeletalMeshRenderData* RenderData = BaseMesh->GetResourceForRendering();
const FSkeletalMeshLODRenderData& LODData = RenderData->LODRenderData[LodIndex];
int32 NumMorphs = LODData.MorphTargetVertexInfoBuffers.GetNumMorphs();
UE_LOG(LogTemp, Warning, TEXT("NumRenderMorphs: %d"), NumMorphs);
bool bMorphCpuDataValid = LODData.MorphTargetVertexInfoBuffers.IsMorphCPUDataValid();
bool bMorphResourcesInitialized = LODData.MorphTargetVertexInfoBuffers.IsMorphResourcesInitialized();
if (bMorphCpuDataValid)
UE_LOG(LogTemp, Warning, TEXT("Morph CPU data valid"));
if (bMorphResourcesInitialized)
UE_LOG(LogTemp, Warning, TEXT("Morph resources initialized"));
// Set the morph target value to 0.5 for testing
PlayerMesh->SetMorphTarget(FName("LerpedMorph"), 0.5);
float value = PlayerMesh->GetMorphTarget(FName("LerpedMorph"));
UE_LOG(LogTemp, Warning, TEXT("LerpedMorph: %f"), value);
PlayerMesh->MarkRenderStateDirty();
// Return the merged skeletal mesh
return BaseMesh;
}
void URenderer::AppendDeltas(TArray<FMorphTargetDelta>* Deltas, TArray<FSkelMeshSection>* Sections, USkeletalMesh* SkeletalMeshA, USkeletalMesh* SkeletalMeshB, const float LerpAmount)
{
if (!SkeletalMeshA || !SkeletalMeshB)
return;
TArray<FMorphTargetDelta> NewDeltas = LerpMorphTargets(SkeletalMeshA, SkeletalMeshB, LerpAmount);
for (FMorphTargetDelta Delta : NewDeltas)
Deltas->Add(Delta);
AppendSections(Sections, SkeletalMeshA, NewDeltas.Num());
}
void URenderer::AppendSections(TArray<FSkelMeshSection>* Sections, const USkeletalMesh* SkeletalMesh, int32 MeshDeltaCount)
{
TArray<FSkelMeshSection> MeshSections = SkeletalMesh->GetImportedModel()->LODModels[0].Sections;
for (int i = 0; i < MeshSections.Num(); ++i)
{
Sections->Add(MeshSections[i]);
Sections->Last().BaseIndex += MeshDeltaCount;
}
}
// Blueprint function that takes in two skeletal meshes and a float value (0-1) and lerps between their morph targets
TArray<FMorphTargetDelta> URenderer::LerpMorphTargets(USkeletalMesh* SkeletalMeshA, USkeletalMesh* SkeletalMeshB, float Amount)
{
// Create an array to store the new deltas
TArray<FMorphTargetDelta> NewDeltas;
//Throw error if lerp amount is invalid
if (Amount < 0 || Amount > 1)
{
UE_LOG(LogTemp, Error, TEXT("Invalid lerp amount (%f) - lerp must be between 0 and 1"), Amount);
return NewDeltas;
}
// Get morph targets for both skeletal meshes
TArray<UMorphTarget*>& SkeletalMeshAMorphTargets = SkeletalMeshA->GetMorphTargets();
TArray<UMorphTarget*>& SkeletalMeshBMorphTargets = SkeletalMeshB->GetMorphTargets();
TArray<FString> MorphTargetNames;
MorphTargetNames.Add("Morph 1");
MorphTargetNames.Add("Morph 2");
MorphTargetNames.Add("Morph 3");
// Find morph target by name for each
for (FString MorphTargetName : MorphTargetNames)
{
UE_LOG(LogTemp, Log, TEXT("Processing morph target: %s"), *MorphTargetName);
// If both found, lerp between them, else skip
UMorphTarget* a = FindMorphTargetByName(SkeletalMeshAMorphTargets, MorphTargetName);
UMorphTarget* b = FindMorphTargetByName(SkeletalMeshBMorphTargets, MorphTargetName);
if (a == nullptr || b == nullptr)
{
UE_LOG(LogTemp, Log, TEXT("Skipping morph target: %s"), *MorphTargetName);
continue;
}
// Throw an error if morph target has invalid data
if (!a->HasValidData() || !b->HasValidData())
{
UE_LOG(LogTemp, Warning, TEXT("Invalid data on morph target!"));
return NewDeltas;
}
// Get LOD meshes for both morph targets
TArray<FMorphTargetLODModel> LODModelsA = a->GetMorphLODModels();
TArray<FMorphTargetLODModel> LODModelsB = b->GetMorphLODModels();
UE_LOG(LogTemp, Log, TEXT("LOD model A count: %d"), LODModelsA.Num());
UE_LOG(LogTemp, Log, TEXT("LOD model B count: %d"), LODModelsB.Num());
// Throw an error if LOD model count different
if (LODModelsA.Num() != LODModelsB.Num())
{
UE_LOG(LogTemp, Warning, TEXT("Skeletal mesh 1 has a different number of LOD models to Skeletal mesh 2! (%d | %d)"), LODModelsA.Num(), LODModelsB.Num());
return NewDeltas;
}
// Iterate over LOD models
for (int32 lodIndex = 0; lodIndex < LODModelsA.Num(); lodIndex++)
{
UE_LOG(LogTemp, Log, TEXT("Processing LOD index: %d"), lodIndex);
// Get the vertex deltas for each LOD mesh
TArray<FMorphTargetDelta>& DeltasA = LODModelsA[lodIndex].Vertices;
TArray<FMorphTargetDelta>& DeltasB = LODModelsB[lodIndex].Vertices;
UE_LOG(LogTemp, Log, TEXT("Vertex deltas A count: %d"), DeltasA.Num());
UE_LOG(LogTemp, Log, TEXT("Vertex deltas B count: %d"), DeltasA.Num());
// Iterate over the deltas in DeltasA
for (const FMorphTargetDelta DeltaA : DeltasA)
{
// Get the related delta in DeltasB
FMorphTargetDelta DeltaB = FindVertexById(DeltasB, DeltaA.SourceIdx);
// Lerp the position and tangent vectors
float PositionX = FMath::Lerp(DeltaA.PositionDelta.X, DeltaB.PositionDelta.X, Amount);
float PositionY = FMath::Lerp(DeltaA.PositionDelta.Y, DeltaB.PositionDelta.Y, Amount);
float PositionZ = FMath::Lerp(DeltaA.PositionDelta.Z, DeltaB.PositionDelta.Z, Amount);
float TangentX = FMath::Lerp(DeltaA.TangentZDelta.X, DeltaB.TangentZDelta.X, Amount);
float TangentY = FMath::Lerp(DeltaA.TangentZDelta.Y, DeltaB.TangentZDelta.Y, Amount);
float TangentZ = FMath::Lerp(DeltaA.TangentZDelta.Z, DeltaB.TangentZDelta.Z, Amount);
// Assign result to a new delta and add it to the array
FMorphTargetDelta NewDelta;
NewDelta.SourceIdx = DeltaA.SourceIdx;
NewDelta.PositionDelta = FVector3f(PositionX, PositionY, PositionZ);
NewDelta.TangentZDelta = FVector3f(TangentX, TangentY, TangentZ);
NewDeltas.Add(NewDelta);
}
}
}
return NewDeltas;
}
UMorphTarget* URenderer::FindMorphTargetByName(TArray<UMorphTarget*>& MorphTargets, const FString& MorphTargetName)
{
for (UMorphTarget* MorphTarget : MorphTargets)
if (MorphTarget->GetName().Compare(MorphTargetName, ESearchCase::CaseSensitive) == 0)
return MorphTarget;
return nullptr;
}
FMorphTargetDelta URenderer::FindVertexById(TArray<FMorphTargetDelta>& Deltas, const int32 Index)
{
for (const FMorphTargetDelta Delta : Deltas)
if (Delta.SourceIdx == Index)
return Delta;
return Deltas[0];
}
with the result of the logs showing:
LerpedMorph: 0.500000 //morph target *does* exist at runtime, and weight is applied
MorphDeltas: 1243 //number of deltas on the morph target before render step
NumRenderMorphs: 0 //number of supposed morphs available during render step (where the check is failing)
bMorphCpuDataValid: false //invalid CPU data
bMorphResourcesInitialized: false //not initialized, probably because the data is invalid
Questions are:
- What would be causing the Morph CPU data to be invalid?
- Would this be causing the render data to not be set up properly, thus causing this error?
The only resources I’ve managed to find on this are either old/outdated or unanswered/unresolved like this one: FSkeletalMeshMerge and MorphTargets - #6 by Omerpaz95
I just wish there was a good example of this actually working and being possible.