Files
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

1578 lines
60 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "MetaHumanCharacterSkelMeshUtils.h"
#include "Engine/SkeletalMesh.h"
#include "SkelMeshDNAUtils.h"
#include "DNAAsset.h"
#include "DNAUtils.h"
#include "Rendering/SkeletalMeshModel.h"
#include "Rendering/SkeletalMeshLODModel.h"
#include "AnimationRuntime.h"
#include "InterchangeDnaModule.h"
#include "MetaHumanRigEvaluatedState.h"
#include "MetaHumanCoreTechMeshUtils.h"
#include "Animation/AnimCurveMetadata.h"
#include "Animation/AnimBlueprint.h"
#include "Animation/AnimInstance.h"
#include "Animation/AnimBlueprintGeneratedClass.h"
#include "Materials/Material.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Misc/FileHelper.h"
#include "Logging/StructuredLog.h"
#include "AssetToolsModule.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "Interfaces/IPluginManager.h"
#include "Framework/Application/SlateApplication.h"
#include "Subsystem/MetaHumanCharacterBuild.h"
#include "Subsystem/MetaHumanCharacterSkinMaterials.h"
#include "Engine/SkeletalMeshLODSettings.h"
#include "ControlRigBlueprintLegacy.h"
#include "PhysicsEngine/PhysicsAsset.h"
#include "SkeletalMeshAttributes.h"
#include "Async/ParallelFor.h"
#include "VectorUtil.h"
#include "MetaHumanCharacter.h"
#include "MetaHumanCharacterEditorLog.h"
#include "UI/Widgets/DNAImportDialogWidget.h"
#include "MetaHumanCommonDataUtils.h"
static FAutoConsoleCommand CVarImportDNA(
TEXT("MH.Import.DNA"),
TEXT("Launches the DNA import dialog."),
FConsoleCommandDelegate::CreateLambda([]()
{
TArray<FString> OutFiles;
TSharedRef<SDNAImportDialogWidget> Window = SNew(SDNAImportDialogWidget);
FSlateApplication::Get().AddModalWindow(Window, FGlobalTabmanager::Get()->GetRootWindow());
const FString DNAPath = Window->GetFilePath();
const FString FileName = Window->GetImportName();
const FString ImportPath = TEXT("/Game/ImportedMesh");
TArray<uint8> DNADataAsBuffer;
if (FFileHelper::LoadFileToArray(DNADataAsBuffer, *DNAPath))
{
TSharedPtr<IDNAReader> DNAReader = ReadDNAFromBuffer(&DNADataAsBuffer, EDNADataLayer::All);
if (DNAReader)
{
EMetaHumanImportDNAType ImportType = Window->GetMeshType() == "Face" ? EMetaHumanImportDNAType::Face : EMetaHumanImportDNAType::Body;
EMetaHumanCharacterSkinPreviewMaterial MaterialType = *Window->GetSelectedMaterial().Get();
const FString UniqueAssetName = MakeUniqueObjectName(GetTransientPackage(), USkeletalMesh::StaticClass(), FName{ FileName }, EUniqueObjectNameOptions::GloballyUnique).ToString();
USkeletalMesh* SkelMeshAsset = FMetaHumanCharacterSkelMeshUtils::GetSkeletalMeshAssetFromDNA(DNAReader, ImportPath, UniqueAssetName, ImportType);
FMetaHumanCharacterSkelMeshUtils::PopulateSkelMeshData(SkelMeshAsset, DNAReader, true);
const bool bUseMaterialsWithVTSupport = false;
FMetaHumanCharacterFaceMaterialSet Materials = FMetaHumanCharacterSkinMaterials::GetHeadPreviewMaterialInstance(MaterialType, bUseMaterialsWithVTSupport);
FMetaHumanCharacterSkinMaterials::SetHeadMaterialsOnMesh(Materials, SkelMeshAsset);
if (MaterialType == EMetaHumanCharacterSkinPreviewMaterial::Clay)
{
Materials.ForEachSkinMaterial<UMaterialInstanceDynamic>(
[](EMetaHumanCharacterSkinMaterialSlot, UMaterialInstanceDynamic* Material)
{
Material->SetScalarParameterValue(TEXT("ClayMaterial"), 1.0f);
}
);
}
}
}
})
);
namespace UE::MetaHuman
{
static TAutoConsoleVariable<bool> CVarMHCRebuildMeshDescriptionAfterInterchange
{
TEXT("mh.Character.RebuildMeshDescriptionAfterInterchange"),
true,
TEXT("Set to true to force an update of the skeletal mesh description after it has been imported through the DNA interchange."),
ECVF_Default
};
template<class IdentityState>
static void UpdateLODModelVertexPositions(
USkeletalMesh* InSkelMesh,
const FMetaHumanRigEvaluatedState& InVerticesAndNormals,
const IdentityState& InState,
const FDNAToSkelMeshMap* InDNAToSkelMeshMap,
ELodUpdateOption InUpdateOption,
EVertexPositionsAndNormals InVertexUpdateOption,
TObjectPtr<USkeletalMesh> InMergedHeadAndBody,
const UE::MetaHuman::FMergedMeshMapping::FVertexMap* InMergedMeshVertexMap,
bool bInUpdateMergedMeshNormals)
{
FSkeletalMeshModel* ImportedModel = InSkelMesh->GetImportedModel();
// Expects vertex map to be initialized beforehand
int32 LODStart = 0;
int32 LODRangeSize = 1;
switch (InUpdateOption)
{
case ELodUpdateOption::LOD0Only:
// the default
break;
case ELodUpdateOption::LOD0AndLOD1Only:
LODRangeSize = FMath::Min(2, ImportedModel->LODModels.Num());
break;
case ELodUpdateOption::LOD1AndHigher:
LODStart = 1;
LODRangeSize = ImportedModel->LODModels.Num();
break;
case ELodUpdateOption::All:
LODRangeSize = ImportedModel->LODModels.Num();
break;
default:
check(false);
}
for (int32 LODIndex = LODStart; LODIndex < LODRangeSize; LODIndex++)
{
FMeshDescription* MergedMeshDescription = nullptr;
const TArray<FVertexID>* OriginalMeshToMergedMesh = nullptr;
TVertexAttributesRef<FVector3f> MergedVertexPositions;
TVertexInstanceAttributesRef<FVector3f> MergedInstanceNormals;
if (InMergedHeadAndBody
&& InMergedMeshVertexMap
&& ensure(InMergedMeshVertexMap->OriginalLODVertexMap.IsValidIndex(LODIndex)))
{
const FMergedMeshMapping::FLODVertexMap& LODMap = InMergedMeshVertexMap->OriginalLODVertexMap[LODIndex];
if (LODMap.IsValid()
&& InMergedHeadAndBody->IsValidLODIndex(LODMap.MergedMeshLODIndex)
&& InMergedHeadAndBody->HasMeshDescription(LODMap.MergedMeshLODIndex))
{
MergedMeshDescription = InMergedHeadAndBody->GetMeshDescription(LODMap.MergedMeshLODIndex);
FSkeletalMeshAttributes Attributes(*MergedMeshDescription);
MergedVertexPositions = Attributes.GetVertexPositions();
MergedInstanceNormals = Attributes.GetVertexInstanceNormals();
OriginalMeshToMergedMesh = &LODMap.OriginalMeshToMergedMesh;
}
}
FSkeletalMeshLODModel& LODModel = ImportedModel->LODModels[LODIndex];
int32 SectionIndex = 0;
int32 NumInvalidSourceVertexIndices = 0;
for (FSkelMeshSection& Section : LODModel.Sections)
{
auto SetNormalForSoftSkinVertex =
[
InVertexUpdateOption,
bInUpdateMergedMeshNormals,
MergedMeshDescription,
OriginalMeshToMergedMesh,
&MergedVertexPositions,
&MergedInstanceNormals,
&NumInvalidSourceVertexIndices,
&LODModel,
SectionBaseVertexIndex = Section.BaseVertexIndex
](
const FVector3f& InPosition,
const FVector3f& InNormal,
int32 InVertexIndex,
FSoftSkinVertex& OutVertex)
{
if (InVertexUpdateOption == EVertexPositionsAndNormals::PositionOnly || InVertexUpdateOption == EVertexPositionsAndNormals::Both)
{
OutVertex.Position = InPosition;
}
if (InVertexUpdateOption == EVertexPositionsAndNormals::NormalsOnly || InVertexUpdateOption == EVertexPositionsAndNormals::Both)
{
// Normalize the input normal to ensure it's unit length.
FVector3f TangentZVector = InNormal.GetSafeNormal();
// Store the normal and handedness (always right-handed) in TangentZ.
// Note that TangentX and TangentY will be regenerated
OutVertex.TangentZ = FVector4f(TangentZVector, /*Handedness*/-1.0f);
}
if (MergedMeshDescription)
{
const int32 SourceVertexIndex = SectionBaseVertexIndex + InVertexIndex;
if (LODModel.GetRawPointIndices().IsValidIndex(SourceVertexIndex))
{
// The vertex in the original face or body mesh
const int32 OriginalMeshVertexIndex = LODModel.GetRawPointIndices()[SourceVertexIndex];
// OriginalMeshToMergedMesh should contain this index if it was constructed correctly
if (ensure(OriginalMeshToMergedMesh->IsValidIndex(OriginalMeshVertexIndex)))
{
// The vertex in the merged face and body mesh
const int32 MergedTargetVertexIndex = (*OriginalMeshToMergedMesh)[OriginalMeshVertexIndex];
const FVertexID MergedVertexID(MergedTargetVertexIndex);
if (InVertexUpdateOption == EVertexPositionsAndNormals::PositionOnly || InVertexUpdateOption == EVertexPositionsAndNormals::Both)
{
if (ensure(MergedTargetVertexIndex < MergedVertexPositions.GetNumElements()))
{
MergedVertexPositions.Set(MergedVertexID, OutVertex.Position);
}
}
if (bInUpdateMergedMeshNormals
&& (InVertexUpdateOption == EVertexPositionsAndNormals::NormalsOnly || InVertexUpdateOption == EVertexPositionsAndNormals::Both))
{
const TArrayView<const FVertexInstanceID> VertexInstances = MergedMeshDescription->GetVertexVertexInstanceIDs(MergedVertexID);
for (const FVertexInstanceID Instance : VertexInstances)
{
if (ensure(Instance.GetValue() < MergedInstanceNormals.GetNumElements()))
{
MergedInstanceNormals.Set(Instance, OutVertex.TangentZ);
}
}
}
}
}
else
{
NumInvalidSourceVertexIndices++;
}
}
};
const int32 DNAMeshIndex = InDNAToSkelMeshMap->ImportVtxToDNAMeshIndex[LODIndex][Section.GetVertexBufferIndex()];
const int32 NumSoftVertices = Section.GetNumVertices();
const TArray<TArray<int32>>& OverlappingMap = InDNAToSkelMeshMap->OverlappingVertices[LODIndex][SectionIndex];
int32 VertexBufferIndex = Section.GetVertexBufferIndex();
const TArray<int32>& ImportVtxToDNAVtxIndex = InDNAToSkelMeshMap->ImportVtxToDNAVtxIndex[LODIndex];
for (int32 VertexIndex = 0; VertexIndex < NumSoftVertices; VertexIndex++)
{
const int32 DNAVertexIndex = ImportVtxToDNAVtxIndex[VertexBufferIndex];
if (DNAVertexIndex >= 0)
{
const FVector3f Position = InState.GetVertex(InVerticesAndNormals.Vertices, DNAMeshIndex, DNAVertexIndex);
const FVector3f Normal = InState.GetVertex(InVerticesAndNormals.VertexNormals, DNAMeshIndex, DNAVertexIndex);
SetNormalForSoftSkinVertex(Position, Normal, VertexIndex, Section.SoftVertices[VertexIndex]);
// Check if the current vertex has overlapping vertices, and then update them as well.
const TArray<int32>& OverlappedIndices = OverlappingMap[VertexIndex];
for (int32 OverlappingVertexIndex : OverlappedIndices)
{
SetNormalForSoftSkinVertex(Position, Normal, OverlappingVertexIndex, Section.SoftVertices[OverlappingVertexIndex]);
}
}
VertexBufferIndex++;
}
SectionIndex++;
}
if (NumInvalidSourceVertexIndices > 0)
{
UE_LOGFMT(LogMetaHumanCharacterEditor, Error, "Failed to update merged mesh: Original mesh has {NumInvalidSourceVertexIndices} invalid raw point indices",
NumInvalidSourceVertexIndices);
}
}
}
static void UpdateJoints(USkeletalMesh* InSkelMesh, IDNAReader* InDNAReader, FDNAToSkelMeshMap* InDNAToSkelMeshMap, EMetaHumanCharacterOrientation InCharacterOrientation)
{
{ // Scoping of RefSkelModifier
FReferenceSkeletonModifier RefSkelModifier(InSkelMesh->GetRefSkeleton(), InSkelMesh->GetSkeleton());
// copy here
TArray<FTransform> RawBonePose = InSkelMesh->GetRefSkeleton().GetRawRefBonePose();
// calculate component space ahead of current transform
TArray<FTransform> ComponentTransforms;
FAnimationRuntime::FillUpComponentSpaceTransforms(InSkelMesh->GetRefSkeleton(), RawBonePose, ComponentTransforms);
const TArray<FMeshBoneInfo>& RawBoneInfo = InSkelMesh->GetRefSkeleton().GetRawRefBoneInfo();
// Skipping root joint (index 0) to avoid blinking of the mesh due to bounding box issue
for (uint16 JointIndex = 0; JointIndex < InDNAReader->GetJointCount(); JointIndex++)
{
int32 BoneIndex = InDNAToSkelMeshMap->GetUEBoneIndex(JointIndex);
FTransform DNATransform = FTransform::Identity;
// Updating bind pose affects just translations.
FVector Translate = InDNAReader->GetNeutralJointTranslation(JointIndex);
FVector RotationVector = InDNAReader->GetNeutralJointRotation(JointIndex);
FRotator Rotation(RotationVector.X, RotationVector.Y, RotationVector.Z);
if (InDNAReader->GetJointParentIndex(JointIndex) == JointIndex) // This is the highest joint of the dna - not necessarily the UE root bone
{
if (InCharacterOrientation == EMetaHumanCharacterOrientation::Y_UP)
{
FQuat YUpToZUpRotation = FQuat(FRotator(0, 0, 90));
FQuat ComponentRotation = YUpToZUpRotation * FQuat(Rotation);
DNATransform.SetTranslation(FVector(Translate.X, Translate.Z, -Translate.Y));
DNATransform.SetRotation(ComponentRotation);
}
else if (InCharacterOrientation == EMetaHumanCharacterOrientation::Z_UP)
{
DNATransform.SetTranslation(Translate);
DNATransform.SetRotation(Rotation.Quaternion());
}
else
{
check(false);
}
ComponentTransforms[BoneIndex] = DNATransform;
}
else
{
DNATransform.SetTranslation(Translate);
DNATransform.SetRotation(Rotation.Quaternion());
if (ensure(RawBoneInfo[BoneIndex].ParentIndex != INDEX_NONE))
{
ComponentTransforms[BoneIndex] = DNATransform * ComponentTransforms[RawBoneInfo[BoneIndex].ParentIndex];
}
}
ComponentTransforms[BoneIndex].NormalizeRotation();
}
for (uint16 BoneIndex = 0; BoneIndex < RawBoneInfo.Num(); BoneIndex++)
{
FTransform LocalTransform;
if (BoneIndex == 0)
{
LocalTransform = ComponentTransforms[BoneIndex];
}
else
{
LocalTransform = ComponentTransforms[BoneIndex].GetRelativeTransform(ComponentTransforms[RawBoneInfo[BoneIndex].ParentIndex]);
}
LocalTransform.NormalizeRotation();
RefSkelModifier.UpdateRefPoseTransform(BoneIndex, LocalTransform);
}
}
InSkelMesh->GetRefBasesInvMatrix().Reset();
InSkelMesh->CalculateInvRefMatrices(); // Needs to be called after RefSkelModifier is destroyed
}
static FVector3f GetOrientatedPosition(const FVector& InPosition, EMetaHumanCharacterOrientation InCharacterOrientation)
{
FVector3f OutPosition;
if (InCharacterOrientation == EMetaHumanCharacterOrientation::Y_UP)
{
OutPosition = FVector3f{ InPosition };
}
else if (InCharacterOrientation == EMetaHumanCharacterOrientation::Z_UP)
{
OutPosition[0] = InPosition.X;
OutPosition[1] = -InPosition.Z;
OutPosition[2] = InPosition.Y;
}
else
{
check(false);
}
return OutPosition;
}
static void UpdateBaseMesh(USkeletalMesh* InSkelMesh, IDNAReader* InDNAReader, FDNAToSkelMeshMap* InDNAToSkelMeshMap, ELodUpdateOption InUpdateOption, EMetaHumanCharacterOrientation InCharacterOrientation)
{
FSkeletalMeshModel* ImportedModel = InSkelMesh->GetImportedModel();
int32 LODStart = 0;
int32 LODRangeSize = ImportedModel->LODModels.Num();
if (InUpdateOption == ELodUpdateOption::LOD1AndHigher)
{
LODStart = 1;
}
else if (InUpdateOption == ELodUpdateOption::LOD0Only && LODRangeSize > 0)
{
LODRangeSize = 1;
}
else if (InUpdateOption == ELodUpdateOption::LOD0Only && LODRangeSize > 1)
{
LODRangeSize = 2;
}
// Expects vertex map to be initialized beforehand
for (int32 LODIndex = LODStart; LODIndex < LODRangeSize; LODIndex++)
{
FSkeletalMeshLODModel& LODModel = ImportedModel->LODModels[LODIndex];
int32 SectionIndex = 0;
for (FSkelMeshSection& Section : LODModel.Sections)
{
int32& DNAMeshIndex = InDNAToSkelMeshMap->ImportVtxToDNAMeshIndex[LODIndex][Section.GetVertexBufferIndex()];
const int32 NumSoftVertices = Section.GetNumVertices();
auto& OverlappingMap = InDNAToSkelMeshMap->OverlappingVertices[LODIndex][SectionIndex];
int32 VertexBufferIndex = Section.GetVertexBufferIndex();
for (int32 VertexIndex = 0; VertexIndex < NumSoftVertices; VertexIndex++)
{
int32& DNAVertexIndex = InDNAToSkelMeshMap->ImportVtxToDNAVtxIndex[LODIndex][VertexBufferIndex];
if (DNAVertexIndex >= 0)
{
const FVector Position = InDNAReader->GetVertexPosition(DNAMeshIndex, DNAVertexIndex);
FSoftSkinVertex& Vertex = Section.SoftVertices[VertexIndex];
Vertex.Position = GetOrientatedPosition(Position, InCharacterOrientation);
// Check if the current vertex has overlapping vertices, and then update them as well.
TArray<int32>& OverlappedIndices = OverlappingMap[VertexIndex];
int32 OverlappingCount = OverlappedIndices.Num();
for (int32 OverlappingIndex = 0; OverlappingIndex < OverlappingCount; ++OverlappingIndex)
{
int32 OverlappingVertexIndex = OverlappedIndices[OverlappingIndex];
FSoftSkinVertex& OverlappingVertex = Section.SoftVertices[OverlappingVertexIndex];
OverlappingVertex.Position = GetOrientatedPosition(Position, InCharacterOrientation);
}
}
VertexBufferIndex++;
}
SectionIndex++;
}
}
}
struct DuplicateUVInfo
{
FVector3f Position;
TArray<FVector2f> NeighbouringUVs;
};
void GetTemplateUVsAndPositions(FMeshDescription& InTemplateMeshDescription, TSet<FVertexID>* InVertexIDFilter, TArray<FVector2f>& OutTemplateUVs, TArray<FVector3f>& OutTemplatePositions, TMap<FVector2f, TArray<DuplicateUVInfo>>& OutDuplicateUVInfos)
{
OutTemplateUVs.Empty();
OutTemplatePositions.Empty();
OutDuplicateUVInfos.Empty();
FStaticMeshAttributes Attributes(InTemplateMeshDescription);
TVertexInstanceAttributesConstRef<FVector2f> UVs = Attributes.GetVertexInstanceUVs();
TVertexAttributesRef<FVector3f> MeshVertices = Attributes.GetVertexPositions();
TMap<FVector2f, int32> FirstUVIndexMap;
TArray<FVertexID> UVIndexToVertexId;
TMap<FVertexID, TArray<int32>> VertexIdToUVIndex;
TMap<FVector2f, TArray<int32>> DuplicateUVIndices;
for (const FVertexInstanceID VertexInstanceID : InTemplateMeshDescription.VertexInstances().GetElementIDs())
{
const FVertexID VertexID = InTemplateMeshDescription.GetVertexInstanceVertex(VertexInstanceID);
if (InVertexIDFilter && !InVertexIDFilter->Contains(VertexID))
{
continue;
}
constexpr int32 UVChannel = 0;
const FVector2f UV = UVs.Get(VertexInstanceID, UVChannel);
const FVector3f Position = MeshVertices.Get(VertexID);
const int32 UVIndex = OutTemplateUVs.Num();
OutTemplateUVs.Add(UV);
OutTemplatePositions.Add(Position);
// Look for duplicates and build indices map
if (int32* FoundFirstUVIndexPtr = FirstUVIndexMap.Find(UV))
{
int32 FoundFirstUVIndex = *FoundFirstUVIndexPtr;
if (OutTemplatePositions[FoundFirstUVIndex] != Position)
{
TArray<int32>& DupicatedIndices = DuplicateUVIndices.FindOrAdd(UV);
if (DupicatedIndices.IsEmpty())
{
DupicatedIndices.Add(FoundFirstUVIndex);
}
DupicatedIndices.Add(UVIndex);
}
}
else
{
FirstUVIndexMap.Add(UV, UVIndex);
}
UVIndexToVertexId.Add(VertexID);
VertexIdToUVIndex.FindOrAdd(VertexID).Add(UVIndex);
}
// Build UV info containing neighbouring UVs for each set of duplicates
for (const TPair<FVector2f, TArray<int32>>& DuplicateUVGroup : DuplicateUVIndices)
{
FVector2f UV = DuplicateUVGroup.Key;
TArray<UE::MetaHuman::DuplicateUVInfo>& DuplicateInfosGroup = OutDuplicateUVInfos.Emplace(UV);
for (int32 UVIndex : DuplicateUVGroup.Value)
{
UE::MetaHuman::DuplicateUVInfo UVInfo;
UVInfo.Position = OutTemplatePositions[UVIndex];
FVertexID VertexID = UVIndexToVertexId[UVIndex];
for (const FEdgeID EdgeID : InTemplateMeshDescription.GetVertexConnectedEdgeIDs(VertexID))
{
const FVertexID Vertex0 = InTemplateMeshDescription.GetEdgeVertex(EdgeID, 0);
const FVertexID Vertex1 = InTemplateMeshDescription.GetEdgeVertex(EdgeID, 1);
FVertexID NeighbourVertexID = (Vertex0 == VertexID) ? Vertex1 : Vertex0;
TArray<int32> NeighbourUVIndices = VertexIdToUVIndex[NeighbourVertexID];
for (int32 NeighbourUVIndex : NeighbourUVIndices)
{
UVInfo.NeighbouringUVs.Add(OutTemplateUVs[NeighbourUVIndex]);
}
}
DuplicateInfosGroup.Add(UVInfo);
}
}
}
int32 CountSharedUVs(const TArray<FVector2f>& A, const TArray<FVector2f>& B)
{
TSet<FVector2f> SetA(A);
int32 Count = 0;
for (const FVector2f& Value : B)
{
if (SetA.Contains(Value))
{
++Count;
}
}
return Count;
}
bool GetUVCorrespondingVertices(
const TArray<FVector2f>& InTemplateUVs,
const TArray<FVector3f>& InTemplateVertexPositions,
const TMap<FVector2f, TArray<DuplicateUVInfo>>& InDuplicateUVInfos,
const TSharedPtr<IDNAReader>& InArchetypeDNAReader,
int32 InDNAMeshIndex,
TArray<FVector3f>& OutVertices)
{
OutVertices.Empty();
if (InDNAMeshIndex >= InArchetypeDNAReader->GetMeshCount())
{
UE_LOG(LogMetaHumanCharacterEditor, Error, TEXT("DNA archetype does not contain mesh for index %d"), InDNAMeshIndex);
return false;
}
const uint32 NumVertices = InArchetypeDNAReader->GetVertexPositionCount(InDNAMeshIndex);
if (NumVertices == 0)
{
UE_LOG(LogMetaHumanCharacterEditor, Error, TEXT("DNA archetype contains no vertices for mesh index %d"), InDNAMeshIndex);
return false;
}
const int32 NumUVs = InArchetypeDNAReader->GetVertexTextureCoordinateCount(InDNAMeshIndex);
if (NumUVs == 0)
{
UE_LOG(LogMetaHumanCharacterEditor, Error, TEXT("DNA archetype contains no UVs for mesh index %d"), InDNAMeshIndex);
return false;
}
// Get UVs from the archetype in dna UV order and keep a mapping from dna position to UV indices
TArray<FVector2f> ArchetypeUVs;
ArchetypeUVs.Init({}, NumUVs);
TArray<bool> UVSet;
UVSet.Init(false, NumUVs);
TMap<int32, int32> DNAPositionToUVIndices;
DNAPositionToUVIndices.Reserve(NumUVs);
TArray<int32> DNAPositionsWithDuplicateUVs;
for (int32 UVIndex = 0; UVIndex < NumUVs; UVIndex++)
{
FVertexLayout VertexLayout = InArchetypeDNAReader->GetVertexLayout(InDNAMeshIndex, UVIndex);
const int32 DNAPositionIndex = VertexLayout.Position;
const int32 DNATextureCoordinateIndex = VertexLayout.TextureCoordinate;
const FTextureCoordinate DNATextureCoord = InArchetypeDNAReader->GetVertexTextureCoordinate(InDNAMeshIndex, DNATextureCoordinateIndex);
// Skel Mesh UV buffer is mirrored in V to DNA. See conversion from DNA to UE basis in MetaHumanInterchangeDnaTranslator.cpp
const FVector2f ArchetypeUV = FVector2f(DNATextureCoord.U, 1.0f - DNATextureCoord.V);
ArchetypeUVs[DNATextureCoordinateIndex] = ArchetypeUV;
UVSet[DNATextureCoordinateIndex] = true;
DNAPositionToUVIndices.Emplace(DNAPositionIndex, DNATextureCoordinateIndex);
if (InDuplicateUVInfos.Contains(ArchetypeUV))
{
DNAPositionsWithDuplicateUVs.Add(DNAPositionIndex);
}
}
// Check map has populated all vertex positions
if (DNAPositionToUVIndices.Num() != NumVertices)
{
UE_LOG(LogMetaHumanCharacterEditor, Error, TEXT("DNA archetype has invalid vertex layout for mesh index %d. Number of vertex positions does not match number referenced by vertex layout."), InDNAMeshIndex);
return false;
}
// Check UVs have been set for each texture coordinate index
int32 NumSetUVs = 0;
for (int32 V = 0; V < UVSet.Num(); ++V)
{
if (UVSet[V])
{
NumSetUVs++;
}
}
if (NumSetUVs != NumUVs)
{
UE_LOG(LogMetaHumanCharacterEditor, Error, TEXT("DNA archetype has invalid vertex layout for mesh index %d. Number of texture coordinates does not match number referenced by vertex layout."), InDNAMeshIndex);
return false;
}
// Get template indices in DNA UV order by finding closest corresponding UVs
TArray<TPair<int32, float>> TemplateUVIndices = UE::MetaHuman::GetClosestUVIndices(InTemplateUVs, ArchetypeUVs);
// Pre-compute DNA neighbours. Indices are DNA positions
TMap<int32, TArray<int32>> PositionNeighbours = UE::MetaHuman::GetNeighbouringVertices(InArchetypeDNAReader, InDNAMeshIndex, DNAPositionsWithDuplicateUVs);
// Iterate in DNA vertex order and get the template vertex position using the corresponding indices
OutVertices.AddUninitialized(NumVertices);
for (uint32 DNAPositionIndex = 0; DNAPositionIndex < NumVertices; DNAPositionIndex++)
{
int32 DNATextureCoordIndex = DNAPositionToUVIndices[DNAPositionIndex];
float Dist = TemplateUVIndices[DNATextureCoordIndex].Value;
if (Dist > UE_KINDA_SMALL_NUMBER)
{
UE_LOG(LogMetaHumanCharacterEditor, Warning, TEXT("UV mismatch. Closest vertex to DNA Position Index %d is above UV distance threshold: %f"), DNAPositionIndex, Dist);
OutVertices.Empty();
return false;
}
int32 TemplateMeshIndex = TemplateUVIndices[DNATextureCoordIndex].Key;
OutVertices[DNAPositionIndex] = InTemplateVertexPositions[TemplateMeshIndex];
// Check if there are duplicates and update position using neighbours
FVector2f ArchetypeUV = ArchetypeUVs[DNATextureCoordIndex];
if (const TArray<DuplicateUVInfo>* DuplicateUVInfos = InDuplicateUVInfos.Find(ArchetypeUV))
{
TArray<FVector2f> NeighbouringUVs;
if (const TArray<int32>* NeighbourPositions = PositionNeighbours.Find(DNAPositionIndex))
{
for (int32 NeighbourPositionIndex : *NeighbourPositions)
{
if (int32* NeighbourDNATextureCoordIndex = DNAPositionToUVIndices.Find(NeighbourPositionIndex))
{
NeighbouringUVs.Add(ArchetypeUVs[*NeighbourDNATextureCoordIndex]);
}
}
}
int32 MaxMatchingNeighboursCount = 0;
bool bMatchedPosition = false;
FVector3f BestMatchedPosition = {};
for (const DuplicateUVInfo& CandidateUVInfo : *DuplicateUVInfos)
{
int NumMatchingNeighbours = CountSharedUVs(CandidateUVInfo.NeighbouringUVs, NeighbouringUVs);
if (NumMatchingNeighbours > MaxMatchingNeighboursCount)
{
MaxMatchingNeighboursCount = NumMatchingNeighbours;
BestMatchedPosition = CandidateUVInfo.Position;
bMatchedPosition = true;
}
}
if (bMatchedPosition)
{
OutVertices[DNAPositionIndex] = BestMatchedPosition;
}
}
}
return true;
}
}
void FMetaHumanCharacterSkelMeshUtils::UpdateSkelMeshFromDNA(TSharedRef<IDNAReader> InDNAReader, EUpdateFlags InUpdateFlags, TSharedRef<FDNAToSkelMeshMap>& InOutDNAToSkelMeshMap, EMetaHumanCharacterOrientation InCharacterOrientation, TNotNull<USkeletalMesh*> OutSkeletalMesh)
{
// The order of execution in this function is fairly important and is split in 3 steps:
// 1. The USkelMeshDNAUtils update the Import Model LOD data of the Skeletal Mesh since this is the reference for the DNA vertex map
// 2. The Mesh Description of the Skeletal Mesh is updated from the Import Model so that the internal mesh state is in sync with the changes
// 3. The Skeleta Mesh is built with the DDC & render data being fully updated
//
// TODO: Note that it is not necessary to do update the whole mesh; the process could be simplified by updating the Mesh Description
// directly from the DNA and potentially not re-building the entire Skeletal Mesh instead only the required parts of the cache/DDC
if (EnumHasAllFlags(InUpdateFlags, EUpdateFlags::Joints))
{
InOutDNAToSkelMeshMap->MapJoints(&InDNAReader.Get());
UE::MetaHuman::UpdateJoints(OutSkeletalMesh, &InDNAReader.Get(), &InOutDNAToSkelMeshMap.Get(), InCharacterOrientation);
}
if (EnumHasAllFlags(InUpdateFlags, EUpdateFlags::BaseMesh))
{
UE::MetaHuman::UpdateBaseMesh(OutSkeletalMesh, &InDNAReader.Get(), &InOutDNAToSkelMeshMap.Get(), ELodUpdateOption::All, InCharacterOrientation);
}
if (EnumHasAllFlags(InUpdateFlags, EUpdateFlags::SkinWeights))
{
USkelMeshDNAUtils::UpdateSkinWeights(OutSkeletalMesh, &InDNAReader.Get(), &InOutDNAToSkelMeshMap.Get(), ELodUpdateOption::All);
}
if (EnumHasAnyFlags(InUpdateFlags, EUpdateFlags::DNABehavior | EUpdateFlags::DNAGeometry))
{
// Set the Behavior part of DNA in skeletal mesh AssetUserData
if (UAssetUserData* UserData = OutSkeletalMesh->GetAssetUserDataOfClass(UDNAAsset::StaticClass()))
{
UDNAAsset* DNAAsset = CastChecked<UDNAAsset>(UserData);
if (EnumHasAllFlags(InUpdateFlags, EUpdateFlags::DNABehavior))
{
DNAAsset->SetBehaviorReader(InDNAReader);
}
if (EnumHasAllFlags(InUpdateFlags, EUpdateFlags::DNAGeometry))
{
DNAAsset->SetGeometryReader(InDNAReader);
}
}
}
// Skeletal mesh has changed, so mark it as dirty
//OutSkeletalMesh->Modify();
OutSkeletalMesh->MarkPackageDirty();
// Commit a Mesh Descriptions for each ImportModel LOD
UpdateMeshDescriptionFromLODModel(OutSkeletalMesh);
//OutSkeletalMesh->InvalidateDeriveDataCacheGUID();
OutSkeletalMesh->PostEditChange();
// Update the DNA vertex map since building the Skeletal Mesh can result in re-ordering of the render vertices
InOutDNAToSkelMeshMap = TSharedRef<FDNAToSkelMeshMap>(USkelMeshDNAUtils::CreateMapForUpdatingNeutralMesh(OutSkeletalMesh));
}
void FMetaHumanCharacterSkelMeshUtils::UpdateMeshDescriptionFromLODModel(TNotNull<USkeletalMesh*> InSkeletalMesh)
{
for (int32 LODIndex = 0; LODIndex < InSkeletalMesh->GetImportedModel()->LODModels.Num(); ++LODIndex)
{
const FSkeletalMeshLODModel& LODModel = InSkeletalMesh->GetImportedModel()->LODModels[LODIndex];
FMeshDescription MeshDescription;
LODModel.GetMeshDescription(InSkeletalMesh, LODIndex, MeshDescription);
InSkeletalMesh->CreateMeshDescription(LODIndex, MoveTemp(MeshDescription));
InSkeletalMesh->CommitMeshDescription(LODIndex);
}
}
void FMetaHumanCharacterSkelMeshUtils::UpdateMeshDescriptionFromLODModelVerticesNormalsAndTangents(TNotNull<USkeletalMesh*> InSkeletalMesh)
{
// based on void FSkeletalMeshLODModel::GetMeshDescription(const USkeletalMesh * InSkeletalMesh, const int32 InLODIndex, FMeshDescription & OutMeshDescription) const
TArray<bool> IsUpdated;
IsUpdated.AddDefaulted(InSkeletalMesh->GetImportedModel()->LODModels.Num());
// run all LODs in parallel as USkeletalMesh::CommitMeshDescription is threadsafe
ParallelFor(InSkeletalMesh->GetImportedModel()->LODModels.Num(), [&](int32 LODIndex)
{
IsUpdated[LODIndex] = false;
const FSkeletalMeshLODModel& LODModel = InSkeletalMesh->GetImportedModel()->LODModels[LODIndex];
FMeshDescription* MeshDescription = InSkeletalMesh->GetMeshDescription(LODIndex);
if (!MeshDescription)
{
UE_LOG(LogMetaHumanCharacterEditor, Warning, TEXT("No mesh description for LOD %d"), LODIndex);
return;
}
InSkeletalMesh->ModifyMeshDescription(LODIndex);
FSkeletalMeshAttributes MeshAttributes(*MeshDescription);
TVertexAttributesRef<FVector3f> VertexPositions = MeshAttributes.GetVertexPositions();
TVertexInstanceAttributesRef<FVector3f> VertexInstanceNormals = MeshAttributes.GetVertexInstanceNormals();
TVertexInstanceAttributesRef<FVector3f> VertexInstanceTangents = MeshAttributes.GetVertexInstanceTangents();
TVertexInstanceAttributesRef<float> VertexInstanceBinormalSigns = MeshAttributes.GetVertexInstanceBinormalSigns();
// Map the section vertices back to the import vertices to remove seams, but only if there's
// mapping available.
TArray<int32> SourceToTargetVertexMap;
int32 TargetVertexCount = 0;
if (LODModel.GetRawPointIndices().Num() == LODModel.NumVertices)
{
SourceToTargetVertexMap.Reserve(LODModel.GetRawPointIndices().Num());
for (const uint32 VertexIndex : LODModel.GetRawPointIndices())
{
SourceToTargetVertexMap.Add(VertexIndex);
TargetVertexCount = FMath::Max(TargetVertexCount, static_cast<int32>(VertexIndex));
}
TargetVertexCount += 1;
}
else
{
SourceToTargetVertexMap.Reserve(LODModel.NumVertices);
for (uint32 Index = 0; Index < LODModel.NumVertices; Index++)
{
SourceToTargetVertexMap.Add(Index);
}
TargetVertexCount = LODModel.NumVertices;
}
if (MeshDescription->Vertices().Num() != TargetVertexCount)
{
UE_LOG(LogMetaHumanCharacterEditor, Warning, TEXT("Mesh Description does not match Skeletal Mesh model for LOD %d"), LODIndex);
return;
}
// verify that the target normals, tangents, and sign match in size
int32 NextVertexInstanceID = 0;
for (int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); SectionIndex++)
{
const FSkelMeshSection& Section = LODModel.Sections[SectionIndex];
NextVertexInstanceID += Section.NumTriangles * 3;
}
if (NextVertexInstanceID != MeshDescription->VertexInstances().Num())
{
UE_LOG(LogMetaHumanCharacterEditor, Warning, TEXT("Mesh Description does not match Skeletal Mesh model for LOD %d"), LODIndex);
return;
}
NextVertexInstanceID = 0;
for (int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); SectionIndex++)
{
const FSkelMeshSection& Section = LODModel.Sections[SectionIndex];
const TArray<FSoftSkinVertex>& SourceVertices = Section.SoftVertices;
for (int32 VertexIndex = 0; VertexIndex < SourceVertices.Num(); VertexIndex++)
{
const int32 SourceVertexIndex = VertexIndex + Section.BaseVertexIndex;
const int32 TargetVertexIndex = SourceToTargetVertexMap[SourceVertexIndex];
// the original method creates a target VertexIDs array that is incremental
//const FVertexID VertexID = VertexIDs[TargetVertexIndex];
const FVertexID VertexID = FVertexID(TargetVertexIndex);
VertexPositions.Set(VertexID, SourceVertices[VertexIndex].Position);
}
for (int32 TriangleID = 0; TriangleID < int32(Section.NumTriangles); TriangleID++)
{
const int32 VertexIndexBase = TriangleID * 3 + Section.BaseIndex;
for (int32 Corner = 0; Corner < 3; Corner++)
{
const int32 SourceVertexIndex = LODModel.IndexBuffer[VertexIndexBase + Corner];
const int32 TargetVertexIndex = SourceToTargetVertexMap[SourceVertexIndex];
//const FVertexID VertexID = VertexIDs[TargetVertexIndex];
const FVertexID VertexID = FVertexID(TargetVertexIndex);
const FVertexInstanceID VertexInstanceID = FVertexInstanceID(NextVertexInstanceID++);
const FSoftSkinVertex& SourceVertex = SourceVertices[SourceVertexIndex - Section.BaseVertexIndex];
// set normals, tangents, and sign
VertexInstanceNormals.Set(VertexInstanceID, SourceVertex.TangentZ);
VertexInstanceTangents.Set(VertexInstanceID, SourceVertex.TangentX);
VertexInstanceBinormalSigns.Set(VertexInstanceID, FMatrix44f(
SourceVertex.TangentX.GetSafeNormal(),
SourceVertex.TangentY.GetSafeNormal(),
FVector3f(SourceVertex.TangentZ.GetSafeNormal()),
FVector3f::ZeroVector).Determinant() < 0.0f ? -1.0f : +1.0f);
}
}
}
IsUpdated[LODIndex] = true;
InSkeletalMesh->CommitMeshDescription(LODIndex);
});
for (int32 LODIndex = 0; LODIndex < InSkeletalMesh->GetImportedModel()->LODModels.Num(); ++LODIndex)
{
if (!IsUpdated[LODIndex])
{
UE_LOG(LogMetaHumanCharacterEditor, Warning, TEXT("Full mesh description update for lod %d"), LODIndex);
const FSkeletalMeshLODModel& LODModel = InSkeletalMesh->GetImportedModel()->LODModels[LODIndex];
FMeshDescription MeshDescription;
LODModel.GetMeshDescription(InSkeletalMesh, LODIndex, MeshDescription);
InSkeletalMesh->CreateMeshDescription(LODIndex, MoveTemp(MeshDescription));
InSkeletalMesh->CommitMeshDescription(LODIndex);
}
}
}
bool FMetaHumanCharacterSkelMeshUtils::CompareDnaToSkelMeshVertices(TSharedPtr<const IDNAReader> InDNAReader, TNotNull<const USkeletalMesh*> InSkeletalMesh, const FDNAToSkelMeshMap& InDNAToSkelMeshMap, float Tolerance /*= UE_KINDA_SMALL_NUMBER*/)
{
const FSkeletalMeshModel* ImportedModel = InSkeletalMesh->GetImportedModel();
if (!ImportedModel)
{
return false;
}
const int32 MeshCount = InDNAReader->GetMeshCount();
for (int32 LODIndex = 0; LODIndex < InDNAReader->GetLODCount(); LODIndex++)
{
if (ImportedModel->LODModels.IsValidIndex(LODIndex))
{
// Skeletal mesh might have fewer LODs than DNA, and that is fine.
const FSkeletalMeshLODModel& LODModel = ImportedModel->LODModels[LODIndex];
for (int32 MeshIndex = 0; MeshIndex < MeshCount; MeshIndex++)
{
const int32 VertexCount = InDNAReader->GetVertexPositionCount(MeshIndex);
for (int32 DNAVertexIndex = 0; DNAVertexIndex < VertexCount; DNAVertexIndex++)
{
const int32 VertexIndex = InDNAToSkelMeshMap.ImportDNAVtxToUEVtxIndex[LODIndex][MeshIndex][DNAVertexIndex];
TArray<FSoftSkinVertex> Vertices;
LODModel.GetVertices(Vertices);
if (Vertices.IsValidIndex(VertexIndex))
{
const FVector UpdatedPosition = InDNAReader->GetVertexPosition(MeshIndex, DNAVertexIndex);
const bool bPositionsEqual = Vertices[VertexIndex].Position.Equals(FVector3f{ UpdatedPosition }, Tolerance);
if (!bPositionsEqual)
{
// TODO: Log vertex index with mismatching position.
return false;
}
}
else
{
// TODO: Log mismatching vertex index/ DNA index not found.
return false;
}
}
}
}
}
return true;
}
bool FMetaHumanCharacterSkelMeshUtils::CompareDnaToStateVerticesAndNormals(TSharedPtr<const IDNAReader> InDNAReader, const TArray<FVector3f>& InStateVertices, const TArray<FVector3f>& InStateNormals, TSharedPtr<const FMetaHumanCharacterIdentity::FState> InState, float Tolerance /*= UE_KINDA_SMALL_NUMBER*/)
{
const int32 MeshCount = InDNAReader->GetMeshCount();
for (int32 MeshIndex = 0; MeshIndex < MeshCount; MeshIndex++)
{
const int32 VertexCount = InDNAReader->GetVertexPositionCount(MeshIndex);
for (int32 DNAVertexIndex = 0; DNAVertexIndex < VertexCount; DNAVertexIndex++)
{
const FVector UpdatedPosition = InDNAReader->GetVertexPosition(MeshIndex, DNAVertexIndex);
const FVector3f StatePosition = InState->GetVertex(InStateVertices, MeshIndex, DNAVertexIndex);
const bool bPositionsEqual = StatePosition.Equals(FVector3f{ UpdatedPosition }, Tolerance);
if (!bPositionsEqual)
{
UE_LOG(LogMetaHumanCharacterEditor, Warning, TEXT("Vertex position mismatch at mesh %d (%s) and index %d, DNA: %f,%f,%f, State: %f,%f,%f"),MeshIndex, *InDNAReader->GetMeshName(MeshIndex), DNAVertexIndex,
UpdatedPosition.X, UpdatedPosition.Y, UpdatedPosition.Z, StatePosition.X, StatePosition.Y, StatePosition.Z);
return false;
}
const FVector UpdatedNormal = InDNAReader->GetVertexNormal(MeshIndex, DNAVertexIndex);
const FVector3f StateNormal = InState->GetVertex(InStateNormals, MeshIndex, DNAVertexIndex);
const bool bNormalsEqual = StateNormal.Equals(FVector3f{ UpdatedNormal }, Tolerance);
if (!bNormalsEqual)
{
UE_LOG(LogMetaHumanCharacterEditor, Warning, TEXT("Vertex normal mismatch at mesh %d (%s) and index %d, DNA: %f,%f,%f, State: %f,%f,%f"), MeshIndex, *InDNAReader->GetMeshName(MeshIndex), DNAVertexIndex,
UpdatedNormal.X, UpdatedNormal.Y, UpdatedNormal.Z, StateNormal.X, StateNormal.Y, StateNormal.Z);
return false;
}
}
}
return true;
}
bool FMetaHumanCharacterSkelMeshUtils::CheckDNACompatibility(IDNAReader* InDnaReaderA, IDNAReader* InDnaReaderB)
{
if (!InDnaReaderA || !InDnaReaderB)
{
return false;
}
// Joints
{
const uint16 JointCountA = InDnaReaderA->GetJointCount();
const uint16 JointCountB = InDnaReaderB->GetJointCount();
// Compare joint count
if (JointCountA != JointCountB)
{
UE_LOG(LogMetaHumanCharacterEditor, Warning, TEXT("Joint count mismatch: %d vs %d"), JointCountA, JointCountB);
return false;
}
bool bJointsOk = true;
FStringBuilderBase ResultMsg;
for (uint16 JointIndex = 0; JointIndex < JointCountA; JointIndex++)
{
const uint16 JointParentA = InDnaReaderA->GetJointParentIndex(JointIndex);
const uint16 JointParentB = InDnaReaderB->GetJointParentIndex(JointIndex);
// Compare joint names
if (InDnaReaderA->GetJointName(JointIndex) != InDnaReaderB->GetJointName(JointIndex))
{
ResultMsg.Appendf(TEXT("Joint name mismatch: '%s' vs '%s'"), *InDnaReaderA->GetJointName(JointParentA), *InDnaReaderB->GetJointName(JointParentB));
ResultMsg.AppendChar('\n');
bJointsOk = false;
continue;
}
// Compare parents
if (InDnaReaderA->GetJointParentIndex(JointIndex) != InDnaReaderB->GetJointParentIndex(JointIndex))
{
ResultMsg.Appendf(TEXT("Joint parent mismatch for joint '%s': '%s' vs '%s'"), *InDnaReaderA->GetJointName(JointIndex), *InDnaReaderA->GetJointName(JointParentA), *InDnaReaderA->GetJointName(JointParentB));
ResultMsg.AppendChar('\n');
bJointsOk = false;
}
}
if (!bJointsOk)
{
UE_LOG(LogMetaHumanCharacterEditor, Warning, TEXT("%s"), *ResultMsg);
return false;
}
}
// Meshes
{
bool bMeshesOk = true;
const uint16 MeshCountA = InDnaReaderA->GetMeshCount();
const uint16 MeshCountB = InDnaReaderB->GetMeshCount();
const uint16 MeshCount = FMath::Max(MeshCountA, MeshCountB);
FStringBuilderBase ResultMsg;
for (uint16 MeshIndex = 0; MeshIndex < MeshCount; MeshIndex++)
{
if (MeshIndex < MeshCountA && MeshIndex < MeshCountB)
{
const uint16 VertexCountA = InDnaReaderA->GetVertexPositionCount(MeshIndex);
const uint16 VertexCountB = InDnaReaderB->GetVertexPositionCount(MeshIndex);
// Compare vertex count
if (VertexCountA != VertexCountB)
{
ResultMsg.Appendf(TEXT("Vertex count mismatch on mesh '%s' (mesh index: %u): %u vs %u"), *InDnaReaderA->GetMeshName(MeshIndex), MeshIndex, VertexCountA, VertexCountB);
ResultMsg.AppendChar('\n');
bMeshesOk = false;
}
}
else
{
break;
}
}
if (!bMeshesOk)
{
UE_LOG(LogMetaHumanCharacterEditor, Warning, TEXT("%s"), *ResultMsg);
return false;
}
}
return true;
}
void FMetaHumanCharacterSkelMeshUtils::UpdateLODModelVertexPositions(
TNotNull<USkeletalMesh*> InSkelMesh,
const FMetaHumanRigEvaluatedState& InVerticesAndNormals,
const FMetaHumanCharacterIdentity::FState& InState,
const FDNAToSkelMeshMap& InDNAToSkelMeshMap,
ELodUpdateOption InUpdateOption,
EVertexPositionsAndNormals InVertexUpdateOption,
TObjectPtr<USkeletalMesh> InMergedHeadAndBody,
const UE::MetaHuman::FMergedMeshMapping* InMergedMeshMapping,
bool bInUpdateMergedMeshNormals)
{
UE::MetaHuman::UpdateLODModelVertexPositions<FMetaHumanCharacterIdentity::FState>(
InSkelMesh,
InVerticesAndNormals,
InState,
&InDNAToSkelMeshMap,
InUpdateOption,
InVertexUpdateOption,
InMergedHeadAndBody,
InMergedMeshMapping ? &InMergedMeshMapping->FaceVertexMap : nullptr,
bInUpdateMergedMeshNormals);
}
void FMetaHumanCharacterSkelMeshUtils::UpdateLODModelVertexPositions(
TNotNull<USkeletalMesh*> InSkelMesh,
const FMetaHumanRigEvaluatedState& InVerticesAndNormals,
const FMetaHumanCharacterBodyIdentity::FState& InState,
const FDNAToSkelMeshMap& InDNAToSkelMeshMap,
ELodUpdateOption InUpdateOption,
EVertexPositionsAndNormals InVertexUpdateOption,
TObjectPtr<USkeletalMesh> InMergedHeadAndBody,
const UE::MetaHuman::FMergedMeshMapping* InMergedMeshMapping,
bool bInUpdateMergedMeshNormals)
{
UE::MetaHuman::UpdateLODModelVertexPositions<FMetaHumanCharacterBodyIdentity::FState>(
InSkelMesh,
InVerticesAndNormals,
InState,
&InDNAToSkelMeshMap,
InUpdateOption,
InVertexUpdateOption,
InMergedHeadAndBody,
InMergedMeshMapping ? &InMergedMeshMapping->BodyVertexMap : nullptr,
bInUpdateMergedMeshNormals);
}
void FMetaHumanCharacterSkelMeshUtils::UpdateBindPoseFromSource(TNotNull<USkeletalMesh*> InSourceSkelMesh, TNotNull<USkeletalMesh*> InTargetSkelMesh)
{
// Scoping of RefSkelModifier
{
FReferenceSkeletonModifier RefSkelModifier(InTargetSkelMesh->GetRefSkeleton(), InTargetSkelMesh->GetSkeleton());
TArray<FTransform> SourceRawBonePose = InSourceSkelMesh->GetRefSkeleton().GetRawRefBonePose();
TArray<FMeshBoneInfo> SourceBoneInfo = InSourceSkelMesh->GetRefSkeleton().GetRefBoneInfo();
// Set bone transforms from source pose by matching bone name
for (int32 SourceBoneIndex = 0; SourceBoneIndex < SourceBoneInfo.Num(); SourceBoneIndex++)
{
int32 TargetBoneIndex = InTargetSkelMesh->GetRefSkeleton().FindBoneIndex(SourceBoneInfo[SourceBoneIndex].Name);
if (TargetBoneIndex == INDEX_NONE)
{
continue;
}
RefSkelModifier.UpdateRefPoseTransform(TargetBoneIndex, SourceRawBonePose[SourceBoneIndex]);
}
}
InTargetSkelMesh->GetRefBasesInvMatrix().Reset();
InTargetSkelMesh->CalculateInvRefMatrices(); // Needs to be called after RefSkelModifier is destroyed
}
void FMetaHumanCharacterSkelMeshUtils::EnableRecomputeTangents(TNotNull<USkeletalMesh*> InSkelMesh)
{
// Code extracted from PersonaMeshDetails for Recompute Tangents update
auto SetSkelMeshSourceSectionUserData = [](FSkeletalMeshLODModel& LODModel, const int32 SectionIndex, const int32 OriginalSectionIndex)
{
FSkelMeshSourceSectionUserData& SourceSectionUserData = LODModel.UserSectionsData.FindOrAdd(OriginalSectionIndex);
SourceSectionUserData.bDisabled = LODModel.Sections[SectionIndex].bDisabled;
SourceSectionUserData.bCastShadow = LODModel.Sections[SectionIndex].bCastShadow;
SourceSectionUserData.bVisibleInRayTracing = LODModel.Sections[SectionIndex].bVisibleInRayTracing;
SourceSectionUserData.bRecomputeTangent = LODModel.Sections[SectionIndex].bRecomputeTangent;
SourceSectionUserData.RecomputeTangentsVertexMaskChannel = LODModel.Sections[SectionIndex].RecomputeTangentsVertexMaskChannel;
SourceSectionUserData.GenerateUpToLodIndex = LODModel.Sections[SectionIndex].GenerateUpToLodIndex;
SourceSectionUserData.CorrespondClothAssetIndex = LODModel.Sections[SectionIndex].CorrespondClothAssetIndex;
SourceSectionUserData.ClothingData = LODModel.Sections[SectionIndex].ClothingData;
};
// Green mask for recompute tangents is currently set to LODs [0-3]
int32 LODNumberInMesh = InSkelMesh->GetImportedModel()->LODModels.Num();
int32 LODsForRecompute = LODNumberInMesh > 4 ? 4 : LODNumberInMesh;
for (int32 LODIndex = 0; LODIndex < LODsForRecompute; ++LODIndex)
{
FSkeletalMeshModel* ImportedModel = InSkelMesh->GetImportedModel();
if (!ImportedModel || LODIndex >= ImportedModel->LODModels.Num())
{
UE_LOGFMT(LogMetaHumanCharacterEditor, Warning, "No imported model data for LOD {LODIndex}", LODIndex);
continue;
}
FSkeletalMeshLODInfo* LODInfo = InSkelMesh->GetLODInfo(LODIndex);
LODInfo->SkinCacheUsage = ESkinCacheUsage::Enabled;
FSkeletalMeshLODModel& LODModel = ImportedModel->LODModels[LODIndex];
// Recompute tangents from green mask is only valid for section with skin
FSkelMeshSection& Section = LODModel.Sections[0];
Section.bRecomputeTangent = true;
Section.RecomputeTangentsVertexMaskChannel = ESkinVertexColorChannel::Green;
SetSkelMeshSourceSectionUserData(LODModel, 0, Section.OriginalDataSectionIndex);
}
InSkelMesh->Build();
InSkelMesh->PostEditChange();
InSkelMesh->InitResources();
}
void FMetaHumanCharacterSkelMeshUtils::PopulateSkelMeshData(TNotNull<USkeletalMesh*> InSkelMesh, TSharedPtr<IDNAReader> InDNAReader, bool bIsFaceMesh)
{
FInterchangeDnaModule::GetModule().SetSkelMeshDNAData(InSkelMesh, InDNAReader);
constexpr EMetaHumanCharacterTemplateType TemplateType = EMetaHumanCharacterTemplateType::MetaHuman;
if (bIsFaceMesh)
{
// Assign the physics asset to the newly create skeletal mesh
InSkelMesh->SetPhysicsAsset(GetFaceArchetypePhysicsAsset(TemplateType));
// Assign the LOD Settings to the face mesh
InSkelMesh->SetLODSettings(GetFaceArchetypeLODSettings(TemplateType));
// Assign the Face Board Control Rig
InSkelMesh->SetDefaultAnimatingRig(GetFaceArchetypeDefaultAnimatingRig(TemplateType));
TArray<FSkeletalMaterial>& MeshMaterials = InSkelMesh->GetMaterials();
for (FSkeletalMaterial& Material : MeshMaterials)
{
const FString Name = Material.MaterialSlotName.ToString();
// TODO: Do this in a proper way through MetaHumanCharacteSkinMaterials.
if (Name == "eyeshell_shader_shader")
{
UMaterialInterface* EyeShellMaterial = LoadObject<UMaterialInterface>(nullptr, TEXT("/Script/Engine.MaterialInstanceConstant'/" UE_PLUGIN_NAME "/Lookdev_UHM/Eye/Materials/MI_eye_occlusion_unified.MI_eye_occlusion_unified'"));
Material.MaterialInterface = EyeShellMaterial;
}
else if (!Name.Contains("head") && !Name.Contains("teeth") && !Name.Contains("eyeLeft") && !Name.Contains("eyeRight") && !Name.Contains("body") && !Name.Contains("combined"))
{
UMaterialInterface* EmptyMaterial = LoadObject<UMaterialInterface>(nullptr, TEXT("/Script/Engine.MaterialInstanceConstant'/" UE_PLUGIN_NAME "/Materials/M_Hide.M_Hide'"));
Material.MaterialInterface = EmptyMaterial;
}
}
EnableRecomputeTangents(InSkelMesh);
}
else
{
InSkelMesh->SetLODSettings(GetBodyArchetypeLODSettings(TemplateType));
InSkelMesh->SetDefaultAnimatingRig(GetBodyArchetypeDefaultAnimatingRig(TemplateType));
InSkelMesh->PostEditChange();
}
}
TArray<FVector3f> FMetaHumanCharacterSkelMeshUtils::GetComponentSpaceJointTranslations(TNotNull<USkeletalMesh*> InSkelMesh)
{
TArray<FTransform> RawBonePose = InSkelMesh->GetRefSkeleton().GetRawRefBonePose();
TArray<FTransform> ComponentTransforms;
FAnimationRuntime::FillUpComponentSpaceTransforms(InSkelMesh->GetRefSkeleton(), RawBonePose, ComponentTransforms);
TArray<FVector3f> ComponentSpaceTranslations;
ComponentSpaceTranslations.AddUninitialized(ComponentTransforms.Num());
for (int32 I = 0; I < ComponentTransforms.Num(); I++)
{
FVector ComponentTranslation = ComponentTransforms[I].GetTranslation();
ComponentSpaceTranslations[I] = FVector3f(ComponentTranslation.X, ComponentTranslation.Y, ComponentTranslation.Z);
}
return ComponentSpaceTranslations;
}
bool FMetaHumanCharacterSkelMeshUtils::GetStaticMeshVertices(const TNotNull<UStaticMesh*> InTemplateStaticMesh, int32 InLodIndex, TArray<FVector3f>& OutVertices)
{
OutVertices.Empty();
FMeshDescription* MeshDescription = InTemplateStaticMesh->GetMeshDescription(InLodIndex);
if (!MeshDescription)
{
UE_LOG(LogMetaHumanCharacterEditor, Error, TEXT("Template mesh does not contain mesh description for Lod %d"), InLodIndex);
return false;
}
FStaticMeshAttributes Attributes(*MeshDescription);
TVertexAttributesRef<FVector3f> TemplateMeshVertexPositions = Attributes.GetVertexPositions();
OutVertices.Reset(TemplateMeshVertexPositions.GetNumElements());
for (int32 Index = 0; Index < TemplateMeshVertexPositions.GetNumElements(); ++Index)
{
const FVector3f OriginalVertex = TemplateMeshVertexPositions.Get(Index);
OutVertices.Add(OriginalVertex);
}
return true;
}
bool FMetaHumanCharacterSkelMeshUtils::GetSkeletalMeshVertices(
const TNotNull<USkeletalMesh*> InTemplateSkeletalMesh,
int32 InLodIndex,
const TNotNull<FDNAToSkelMeshMap*> InDNAToSkelMeshMap,
int32 InDNAMeshIndex,
TArray<FVector3f>& OutVertices)
{
OutVertices.Empty();
OutVertices.AddUninitialized(InDNAToSkelMeshMap->ImportDNAVtxToUEVtxIndex[InLodIndex][InDNAMeshIndex].Num());
const FSkeletalMeshLODModel& LODModel = InTemplateSkeletalMesh->GetImportedModel()->LODModels[InLodIndex];
TArray<bool> VerticesSet;
VerticesSet.Init(false, OutVertices.Num());
int32 TotalNumSoftVertices = 0;
for (const FSkelMeshSection& Section : LODModel.Sections)
{
TotalNumSoftVertices += Section.GetNumVertices();
}
for (const FSkelMeshSection& Section : LODModel.Sections)
{
const int32& DNAMeshIndex = InDNAToSkelMeshMap->ImportVtxToDNAMeshIndex[InLodIndex][Section.GetVertexBufferIndex()];
if (DNAMeshIndex == InDNAMeshIndex)
{
const int32 NumSoftVertices = Section.GetNumVertices();
int32 VertexBufferIndex = Section.GetVertexBufferIndex();
for (int32 VertexIndex = 0; VertexIndex < NumSoftVertices; VertexIndex++)
{
const int32& DNAVertexIndex = InDNAToSkelMeshMap->ImportVtxToDNAVtxIndex[InLodIndex][VertexBufferIndex++];
if (DNAVertexIndex >= 0 && DNAVertexIndex < OutVertices.Num())
{
const FSoftSkinVertex& Vertex = Section.SoftVertices[VertexIndex];
OutVertices[DNAVertexIndex] = FVector3f{ Vertex.Position.X, Vertex.Position.Y, Vertex.Position.Z };
VerticesSet[DNAVertexIndex] = true;
}
else
{
return false;
}
}
}
}
// Check all vertices have been set
for (int32 V = 0; V < VerticesSet.Num(); ++V)
{
if (!VerticesSet[V])
{
return false;
}
}
return true;
}
bool FMetaHumanCharacterSkelMeshUtils::GetUVCorrespondingStaticMeshVertices(
const TNotNull<UStaticMesh*> InTemplateStaticMesh,
int32 InLodIndex,
const TSharedPtr<IDNAReader>& InArchetypeDNAReader,
int32 InDNAMeshIndex,
TArray<FVector3f>& OutVertices)
{
OutVertices.Empty();
FMeshDescription* MeshDescription = InTemplateStaticMesh->GetMeshDescription(InLodIndex);
if (!MeshDescription)
{
UE_LOG(LogMetaHumanCharacterEditor, Error, TEXT("Template mesh does not contain mesh description for Lod %d"), InLodIndex);
return false;
}
TArray<FVector2f> TemplateUVs;
TArray<FVector3f> TemplateVertexPositions;
TMap<FVector2f, TArray<UE::MetaHuman::DuplicateUVInfo>> DuplicateUVInfos;
UE::MetaHuman::GetTemplateUVsAndPositions(*MeshDescription, nullptr, TemplateUVs, TemplateVertexPositions, DuplicateUVInfos);
return UE::MetaHuman::GetUVCorrespondingVertices(TemplateUVs, TemplateVertexPositions, DuplicateUVInfos, InArchetypeDNAReader, InDNAMeshIndex, OutVertices);
}
bool FMetaHumanCharacterSkelMeshUtils::GetUVCorrespondingSkeletalMeshVertices(
const TNotNull<USkeletalMesh*> InTemplateSkelMesh,
int32 InLodIndex,
const TNotNull<FDNAToSkelMeshMap*> InDNAToSkelMeshMap,
const TSharedPtr<IDNAReader>& InArchetypeDNAReader,
int32 InDNAMeshIndex,
TArray<FVector3f>& OutVertices)
{
OutVertices.Empty();
FSkeletalMeshModel* ImportedModel = InTemplateSkelMesh->GetImportedModel();
if (InLodIndex > ImportedModel->LODModels.Num() - 1)
{
UE_LOG(LogMetaHumanCharacterEditor, Error, TEXT("Template mesh does not contain LOD %d"), InLodIndex);
return false;
}
// Get UVs and positions from template mesh
const FSkeletalMeshLODModel& LODModel = ImportedModel->LODModels[InLodIndex];
FMeshDescription MeshDescription;
LODModel.GetMeshDescription(InTemplateSkelMesh, InLodIndex, MeshDescription);
// Create a vertex id filter to only check vertices that map to the input DNA mesh index
TSet<FVertexID> VertexIdsFilter;
{
TArray<uint32> SourceToTargetVertexMap;
if (LODModel.GetRawPointIndices().Num() == LODModel.NumVertices)
{
SourceToTargetVertexMap.Reserve(LODModel.GetRawPointIndices().Num());
for (const uint32 VertexIndex : LODModel.GetRawPointIndices())
{
SourceToTargetVertexMap.Add(VertexIndex);
}
}
else
{
SourceToTargetVertexMap.Reserve(LODModel.NumVertices);
for (uint32 Index = 0; Index < LODModel.NumVertices; Index++)
{
SourceToTargetVertexMap.Add(Index);
}
}
for (const FSkelMeshSection& Section : LODModel.Sections)
{
const int32 DNAMeshIndex = InDNAToSkelMeshMap->ImportVtxToDNAMeshIndex[InLodIndex][Section.GetVertexBufferIndex()];
if (DNAMeshIndex == InDNAMeshIndex)
{
const uint32 NumSoftVertices = Section.GetNumVertices();
for (uint32 VertexIndex = 0; VertexIndex < NumSoftVertices; VertexIndex++)
{
const FSoftSkinVertex& SoftVertex = Section.SoftVertices[VertexIndex];
const uint32 SourceVertexIndex = VertexIndex + Section.BaseVertexIndex;
const uint32 TargetVertexIndex = SourceToTargetVertexMap[SourceVertexIndex];
const FVertexID VertexID = FVertexID(TargetVertexIndex);
VertexIdsFilter.Add(VertexID);
}
}
}
}
TArray<FVector2f> TemplateUVs;
TArray<FVector3f> TemplateVertexPositions;
TMap<FVector2f, TArray<UE::MetaHuman::DuplicateUVInfo>> DuplicateUVInfos;
UE::MetaHuman::GetTemplateUVsAndPositions(MeshDescription, &VertexIdsFilter, TemplateUVs, TemplateVertexPositions, DuplicateUVInfos);
return UE::MetaHuman::GetUVCorrespondingVertices(TemplateUVs, TemplateVertexPositions, DuplicateUVInfos, InArchetypeDNAReader, InDNAMeshIndex, OutVertices);
}
USkeletalMesh* FMetaHumanCharacterSkelMeshUtils::GetSkeletalMeshAssetFromDNA(const TSharedPtr<IDNAReader>& InDNAReader, const FString& InAssetPath, const FString& InAssetName,
const EMetaHumanImportDNAType InImportDNAType)
{
FInterchangeDnaModule& DNAImportModule = FInterchangeDnaModule::GetModule();
USkeleton* Skeleton = nullptr;
if (InImportDNAType == EMetaHumanImportDNAType::Face)
{
Skeleton = LoadObject<USkeleton>(nullptr, FMetaHumanCommonDataUtils::GetCharacterPluginFaceSkeletonPath());
}
else if (InImportDNAType == EMetaHumanImportDNAType::Body)
{
Skeleton = LoadObject<USkeleton>(nullptr, FMetaHumanCommonDataUtils::GetCharacterPluginBodySkeletonPath());
}
if (USkeletalMesh* SkelMeshAsset = DNAImportModule.ImportSync(InAssetName, InAssetPath, InDNAReader, Skeleton))
{
// Interchange system doesn't make an asset transient when the transient path is supplied
if (InAssetPath.Contains("Engine/Transient") || InAssetPath.Contains("Engine.Transient"))
{
SkelMeshAsset->GetPackage()->SetFlags(RF_Transient);
}
if (UE::MetaHuman::CVarMHCRebuildMeshDescriptionAfterInterchange.GetValueOnAnyThread())
{
// This seems to clear any extra allocated data for blend shapes in the mesh description during the interchange calls
UpdateMeshDescriptionFromLODModel(SkelMeshAsset);
// This frees around 1.5GB of memory after importing the mesh from DNA
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
}
return SkelMeshAsset;
}
return nullptr;
}
USkeletalMesh* FMetaHumanCharacterSkelMeshUtils::CreateArchetypeSkelMeshFromDNA(const EMetaHumanImportDNAType InImportDNAType, TSharedPtr<IDNAReader>& OutArchetypeDnaReader)
{
USkeletalMesh* SkelMeshAsset = nullptr;
const FString DNAPath = FMetaHumanCommonDataUtils::GetArchetypeDNAPath(InImportDNAType);
TArray<uint8> DNADataAsBuffer;
if (FFileHelper::LoadFileToArray(DNADataAsBuffer, *DNAPath))
{
FString ArchetypeAssetName = GetTransientArchetypeMeshAssetName(InImportDNAType);
OutArchetypeDnaReader = ReadDNAFromBuffer(&DNADataAsBuffer, EDNADataLayer::All);
if (OutArchetypeDnaReader)
{
const FString UniqueAssetName = MakeUniqueObjectName(GetTransientPackage(), USkeletalMesh::StaticClass(), FName{ ArchetypeAssetName }, EUniqueObjectNameOptions::GloballyUnique).ToString();
SkelMeshAsset = GetSkeletalMeshAssetFromDNA(OutArchetypeDnaReader, TEXT("/Engine/Transient"), UniqueAssetName, InImportDNAType);
}
}
return SkelMeshAsset;
}
UDNAAsset* FMetaHumanCharacterSkelMeshUtils::GetArchetypeDNAAseet(const EMetaHumanImportDNAType InImportDNAType, UObject* InOuter)
{
const FString DNAPath = FMetaHumanCommonDataUtils::GetArchetypeDNAPath(InImportDNAType);
return GetDNAAssetFromFile(DNAPath, InOuter);
}
FString FMetaHumanCharacterSkelMeshUtils::GetTransientArchetypeMeshAssetName(const EMetaHumanImportDNAType InImportDNAType)
{
FString ArchetypeAssetName;
switch (InImportDNAType)
{
case EMetaHumanImportDNAType::Face:
ArchetypeAssetName = TEXT("Face");
break;
case EMetaHumanImportDNAType::Body:
ArchetypeAssetName = TEXT("Body");
break;
case EMetaHumanImportDNAType::Combined:
ArchetypeAssetName = TEXT("Combined");
break;
default:
ArchetypeAssetName = TEXT("Default");
break;
}
return ArchetypeAssetName;
}
UPhysicsAsset* FMetaHumanCharacterSkelMeshUtils::GetFaceArchetypePhysicsAsset(EMetaHumanCharacterTemplateType InTemplateType)
{
UPhysicsAsset* FaceArchetypePhysics = nullptr;
if (ensureAlways(InTemplateType == EMetaHumanCharacterTemplateType::MetaHuman))
{
FaceArchetypePhysics = LoadObject<UPhysicsAsset>(nullptr, TEXT("/Script/Engine.PhysicsAsset'/" UE_PLUGIN_NAME "/Face/PHYS_Face.PHYS_Face'"));
}
return FaceArchetypePhysics;
}
USkeletalMeshLODSettings* FMetaHumanCharacterSkelMeshUtils::GetFaceArchetypeLODSettings(EMetaHumanCharacterTemplateType InTemplateType)
{
USkeletalMeshLODSettings* FaceArchetypeLODSettings = nullptr;
if (ensureAlways(InTemplateType == EMetaHumanCharacterTemplateType::MetaHuman))
{
FaceArchetypeLODSettings = LoadObject<USkeletalMeshLODSettings>(nullptr, TEXT("/Script/Engine.SkeletalMeshLODSettings'/" UE_PLUGIN_NAME "/Face/Face_LODSettings.Face_LODSettings'"));
}
return FaceArchetypeLODSettings;
}
UControlRigBlueprint* FMetaHumanCharacterSkelMeshUtils::GetFaceArchetypeDefaultAnimatingRig(EMetaHumanCharacterTemplateType InTemplateType)
{
UControlRigBlueprint* FaceArchetypeDefaultAnimatingRig = nullptr;
if (ensureAlways(InTemplateType == EMetaHumanCharacterTemplateType::MetaHuman))
{
const TSoftObjectPtr<UObject> FaceboardControlRigAsset = FMetaHumanCommonDataUtils::GetDefaultFaceControlRig(FMetaHumanCommonDataUtils::GetCharacterPluginFaceControlRigPath());
if (!FaceboardControlRigAsset.IsNull())
{
FaceArchetypeDefaultAnimatingRig = Cast<UControlRigBlueprint>(FaceboardControlRigAsset.LoadSynchronous());
}
}
return FaceArchetypeDefaultAnimatingRig;
}
UPhysicsAsset* FMetaHumanCharacterSkelMeshUtils::GetBodyArchetypePhysicsAsset(EMetaHumanCharacterTemplateType InTemplateType)
{
UPhysicsAsset* BodyArchetypePhysics = nullptr;
if (ensureAlways(InTemplateType == EMetaHumanCharacterTemplateType::MetaHuman))
{
BodyArchetypePhysics = LoadObject<UPhysicsAsset>(nullptr, TEXT("/Script/Engine.PhysicsAsset'/" UE_PLUGIN_NAME "/Body/IdentityTemplate/PHYS_Body.PHYS_Body'"));
}
return BodyArchetypePhysics;
}
USkeletalMeshLODSettings* FMetaHumanCharacterSkelMeshUtils::GetBodyArchetypeLODSettings(EMetaHumanCharacterTemplateType InTemplateType)
{
USkeletalMeshLODSettings* BodyArchetypeLODSettings = nullptr;
if (ensureAlways(InTemplateType == EMetaHumanCharacterTemplateType::MetaHuman))
{
BodyArchetypeLODSettings = LoadObject<USkeletalMeshLODSettings>(nullptr, TEXT("/Script/Engine.SkeletalMeshLODSettings'/" UE_PLUGIN_NAME "/Body/IdentityTemplate/Body_LODSettings.Body_LODSettings'"));
}
return BodyArchetypeLODSettings;
}
UControlRigBlueprint* FMetaHumanCharacterSkelMeshUtils::GetBodyArchetypeDefaultAnimatingRig(EMetaHumanCharacterTemplateType InTemplateType)
{
UControlRigBlueprint* BodyArchetypeDefaultAnimatingRig = nullptr;
if (ensureAlways(InTemplateType == EMetaHumanCharacterTemplateType::MetaHuman))
{
BodyArchetypeDefaultAnimatingRig = LoadObject<UControlRigBlueprint>(nullptr, TEXT("/Script/ControlRigDeveloper.ControlRigBlueprint'/" UE_PLUGIN_NAME "/Common/MetaHuman_ControlRig.MetaHuman_ControlRig'"));
}
return BodyArchetypeDefaultAnimatingRig;
}