Files
UnrealEngine/Engine/Source/Developer/SkeletalMeshUtilitiesCommon/Private/LODUtilities.cpp
2025-05-18 13:04:45 +08:00

4700 lines
184 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "LODUtilities.h"
#if WITH_EDITOR
#include "Algo/Accumulate.h"
#include "Animation/MorphTarget.h"
#include "Animation/SkinWeightProfile.h"
#include "Async/ParallelFor.h"
#include "Async/TaskGraphInterfaces.h"
#include "BoneWeights.h"
#include "ClothingAsset.h"
#include "ComponentReregisterContext.h"
#include "Components/SkeletalMeshComponent.h"
#include "Components/SkinnedMeshComponent.h"
#include "EditorFramework/AssetImportData.h"
#include "Engine/SkeletalMesh.h"
#include "Engine/SkeletalMeshLODSettings.h"
#include "Engine/SkeletalMeshSocket.h"
#include "Engine/SkinnedAssetAsyncCompileUtils.h"
#include "Engine/SkinnedAssetCommon.h"
#include "Engine/Texture2D.h"
#include "Framework/Notifications/NotificationManager.h"
#include "GenericQuadTree.h"
#include "IMeshReductionManagerModule.h"
#include "ImageCore.h"
#include "ImageCoreUtils.h"
#include "ImportUtils/SkelImport.h"
#include "Interfaces/ITargetPlatform.h"
#include "Logging/StructuredLog.h"
#include "MeshUtilities.h"
#include "MeshUtilitiesCommon.h"
#include "Misc/FeedbackContext.h"
#include "Misc/MessageDialog.h"
#include "Modules/ModuleManager.h"
#include "ObjectTools.h"
#include "OverlappingCorners.h"
#include "Rendering/SkeletalMeshLODImporterData.h"
#include "Rendering/SkeletalMeshLODModel.h"
#include "Rendering/SkeletalMeshModel.h"
#include "SkeletalMeshAttributes.h"
#include "Tasks/Task.h"
#include "UObject/GarbageCollection.h"
#include "UObject/UObjectIterator.h"
#include "Widgets/Notifications/SNotificationList.h"
#include <limits>
IMPLEMENT_MODULE(FDefaultModuleImpl, SkeletalMeshUtilitiesCommon)
#define LOCTEXT_NAMESPACE "LODUtilities"
DEFINE_LOG_CATEGORY_STATIC(LogLODUtilities, Log, All);
/**
* Process and update the vertex Influences using the predefined wedges
*
* @param VertexCount - The number of vertices in the corresponding mesh.
* @param Influences - BoneWeights and Ids for the corresponding vertices.
*/
void FLODUtilities::ProcessImportMeshInfluences(const int32 VertexCount, TArray<SkeletalMeshImportData::FRawBoneInfluence>& Influences, const FString& MeshName)
{
// Sort influences by vertex index.
struct FCompareVertexIndex
{
bool operator()(const SkeletalMeshImportData::FRawBoneInfluence& A, const SkeletalMeshImportData::FRawBoneInfluence& B) const
{
if (A.VertexIndex > B.VertexIndex) return false;
else if (A.VertexIndex < B.VertexIndex) return true;
else if (A.Weight < B.Weight) return false;
else if (A.Weight > B.Weight) return true;
else if (A.BoneIndex > B.BoneIndex) return false;
else if (A.BoneIndex < B.BoneIndex) return true;
else return false;
}
};
Influences.Sort(FCompareVertexIndex());
TArray <SkeletalMeshImportData::FRawBoneInfluence> NewInfluences;
int32 LastNewInfluenceIndex = 0;
int32 LastVertexIndex = INDEX_NONE;
int32 InfluenceCount = 0;
float TotalWeight = 0.f;
int MaxVertexInfluence = 0;
float MaxIgnoredWeight = 0.0f;
//We have to normalize the data before filtering influences
//Because influence filtering is base on the normalize value.
//Some DCC like Daz studio don't have normalized weight
for (int32 i = 0; i < Influences.Num(); i++)
{
// if less than min weight, or it's more than 8, then we clear it to use weight
InfluenceCount++;
TotalWeight += Influences[i].Weight;
// we have all influence for the same vertex, normalize it now
if (i + 1 >= Influences.Num() || Influences[i].VertexIndex != Influences[i + 1].VertexIndex)
{
// Normalize the last set of influences.
if (InfluenceCount && (TotalWeight != 1.0f))
{
float OneOverTotalWeight = 1.f / TotalWeight;
for (int r = 0; r < InfluenceCount; r++)
{
Influences[i - r].Weight *= OneOverTotalWeight;
}
}
if (MaxVertexInfluence < InfluenceCount)
{
MaxVertexInfluence = InfluenceCount;
}
// clear to count next one
InfluenceCount = 0;
TotalWeight = 0.f;
}
if (InfluenceCount > MAX_TOTAL_INFLUENCES && Influences[i].Weight > MaxIgnoredWeight)
{
MaxIgnoredWeight = Influences[i].Weight;
}
}
// warn about too many influences
if (MaxVertexInfluence > MAX_TOTAL_INFLUENCES)
{
UE_LOG(LogLODUtilities, Display, TEXT("Skeletal mesh (%s) influence count of %d exceeds max count of %d. Influence truncation will occur. Maximum Ignored Weight %f"), *MeshName, MaxVertexInfluence, MAX_TOTAL_INFLUENCES, MaxIgnoredWeight);
}
for (int32 i = 0; i < Influences.Num(); i++)
{
// we found next verts, normalize it now
if (LastVertexIndex != Influences[i].VertexIndex)
{
// Normalize the last set of influences.
if (InfluenceCount && (TotalWeight != 1.0f))
{
float OneOverTotalWeight = 1.f / TotalWeight;
for (int r = 0; r < InfluenceCount; r++)
{
NewInfluences[LastNewInfluenceIndex - r].Weight *= OneOverTotalWeight;
}
}
// now we insert missing verts
if (LastVertexIndex != INDEX_NONE)
{
int32 CurrentVertexIndex = Influences[i].VertexIndex;
for (int32 j = LastVertexIndex + 1; j < CurrentVertexIndex; j++)
{
// Add a 0-bone weight if none other present (known to happen with certain MAX skeletal setups).
LastNewInfluenceIndex = NewInfluences.AddUninitialized();
NewInfluences[LastNewInfluenceIndex].VertexIndex = j;
NewInfluences[LastNewInfluenceIndex].BoneIndex = 0;
NewInfluences[LastNewInfluenceIndex].Weight = 1.f;
}
}
// clear to count next one
InfluenceCount = 0;
TotalWeight = 0.f;
LastVertexIndex = Influences[i].VertexIndex;
}
// if less than min weight, or it's more than 8, then we clear it to use weight
if (Influences[i].Weight >= UE::AnimationCore::BoneWeightThreshold &&
InfluenceCount < MAX_TOTAL_INFLUENCES)
{
LastNewInfluenceIndex = NewInfluences.Add(Influences[i]);
InfluenceCount++;
TotalWeight += Influences[i].Weight;
}
}
Influences = NewInfluences;
// Ensure that each vertex has at least one influence as e.g. CreateSkinningStream relies on it.
// The below code relies on influences being sorted by vertex index.
if (Influences.Num() == 0)
{
// warn about no influences
UE_LOG(LogLODUtilities, Display, TEXT("Warning skeletal mesh (%s) has no vertex influences"), *MeshName);
// add one for each wedge entry
Influences.AddUninitialized(VertexCount);
for (int32 VertexIdx = 0; VertexIdx < VertexCount; VertexIdx++)
{
Influences[VertexIdx].VertexIndex = VertexIdx;
Influences[VertexIdx].BoneIndex = 0;
Influences[VertexIdx].Weight = 1.0f;
}
for (int32 i = 0; i < Influences.Num(); i++)
{
int32 CurrentVertexIndex = Influences[i].VertexIndex;
if (LastVertexIndex != CurrentVertexIndex)
{
for (int32 j = LastVertexIndex + 1; j < CurrentVertexIndex; j++)
{
// Add a 0-bone weight if none other present (known to happen with certain MAX skeletal setups).
Influences.InsertUninitialized(i, 1);
Influences[i].VertexIndex = j;
Influences[i].BoneIndex = 0;
Influences[i].Weight = 1.f;
}
LastVertexIndex = CurrentVertexIndex;
}
}
}
}
bool FLODUtilities::RegenerateLOD(USkeletalMesh* SkeletalMesh, const ITargetPlatform* TargetPlatform, int32 NewLODCount /*= 0*/, bool bRegenerateEvenIfImported /*= false*/, bool bGenerateBaseLOD /*= false*/)
{
if (SkeletalMesh)
{
FScopedSkeletalMeshPostEditChange ScopedPostEditChange(SkeletalMesh);
// Unbind any existing clothing assets before we regenerate all LODs
TArray<ClothingAssetUtils::FClothingAssetMeshBinding> ClothingBindings;
FLODUtilities::UnbindClothingAndBackup(SkeletalMesh, ClothingBindings);
int32 LODCount = SkeletalMesh->GetLODNum();
if (NewLODCount > 0)
{
LODCount = NewLODCount;
}
SkeletalMesh->Modify();
FSkeletalMeshUpdateContext UpdateContext;
UpdateContext.SkeletalMesh = SkeletalMesh;
//If we force a regenerate, we want to invalidate the DCC so the render data get rebuilded
SkeletalMesh->InvalidateDeriveDataCacheGUID();
// remove LODs
int32 CurrentNumLODs = SkeletalMesh->GetLODNum();
if (LODCount < CurrentNumLODs)
{
for (int32 LODIdx = CurrentNumLODs - 1; LODIdx >= LODCount; LODIdx--)
{
FLODUtilities::RemoveLOD(UpdateContext, LODIdx);
}
}
// we need to add more
else if (LODCount > CurrentNumLODs)
{
// Only create new skeletal mesh LOD level entries, we cannot multi thread since the LOD will be create here
//TArray are not thread safe.
for (int32 LODIdx = CurrentNumLODs; LODIdx < LODCount; LODIdx++)
{
// if no previous setting found, it will use default setting.
FLODUtilities::SimplifySkeletalMeshLOD(UpdateContext, LODIdx, TargetPlatform, false);
}
}
else
{
for (int32 LODIdx = 0; LODIdx < LODCount; LODIdx++)
{
FSkeletalMeshLODInfo& CurrentLODInfo = *(SkeletalMesh->GetLODInfo(LODIdx));
if ((bRegenerateEvenIfImported && LODIdx > 0) || (bGenerateBaseLOD && LODIdx == 0) || CurrentLODInfo.bHasBeenSimplified )
{
FLODUtilities::SimplifySkeletalMeshLOD(UpdateContext, LODIdx, TargetPlatform, false);
}
}
}
//Restore all clothing we can
FLODUtilities::RestoreClothingFromBackup(SkeletalMesh, ClothingBindings);
return true;
}
return false;
}
namespace RemoveLODHelper
{
void GetDependentLODs(USkeletalMesh* SkeletalMesh, const int32 RefLODIndex, TArray<int32>& DependentLODs)
{
if (!SkeletalMesh || RefLODIndex >= SkeletalMesh->GetLODNum()-1)
{
return;
}
int32 LODCount = SkeletalMesh->GetLODNum();
FSkeletalMeshModel* SkelMeshModel = SkeletalMesh->GetImportedModel();
for (int32 LODIndex = RefLODIndex + 1; LODIndex < LODCount; ++LODIndex)
{
if (!SkeletalMesh->IsReductionActive(LODIndex))
{
continue;
}
const FSkeletalMeshLODInfo* LODInfo = SkeletalMesh->GetLODInfo(LODIndex);
if (!LODInfo)
{
continue;
}
if (LODInfo->ReductionSettings.BaseLOD == RefLODIndex)
{
DependentLODs.Add(LODIndex);
}
}
}
void AdjustReductionSettings(USkeletalMesh* SkeletalMesh, const int32 DestinationLODIndex, const int32 SourceLODIndex)
{
FSkeletalMeshLODInfo* DestinationLODInfo = SkeletalMesh->GetLODInfo(DestinationLODIndex);
const FSkeletalMeshLODInfo* SourceLODInfo = SkeletalMesh->GetLODInfo(SourceLODIndex);
if (!DestinationLODInfo || !SourceLODInfo)
{
return;
}
//Adjust percent so we end up with the same amount.
DestinationLODInfo->ReductionSettings.NumOfTrianglesPercentage /= SourceLODInfo->ReductionSettings.NumOfTrianglesPercentage;
DestinationLODInfo->ReductionSettings.NumOfVertPercentage /= SourceLODInfo->ReductionSettings.NumOfVertPercentage;
}
} //End namspace RemoveLODHelper
void FLODUtilities::RemoveLOD(FSkeletalMeshUpdateContext& UpdateContext, int32 DesiredLOD )
{
USkeletalMesh* SkeletalMesh = UpdateContext.SkeletalMesh;
FSkeletalMeshModel* SkelMeshModel = SkeletalMesh->GetImportedModel();
if(SkelMeshModel->LODModels.Num() <= 1)
{
if(!FApp::IsUnattended())
{
FMessageDialog::Open( EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "NoLODToRemove", "No LODs to remove!") );
}
UE_LOG(LogLODUtilities, Warning, TEXT("Cannot remove LOD {0}, there must be at least one LOD after the removal."), DesiredLOD);
return;
}
check( SkeletalMesh->GetLODNum() == SkelMeshModel->LODModels.Num() );
// If its a valid LOD, remove it.
if(DesiredLOD < SkelMeshModel->LODModels.Num() )
{
FScopedSkeletalMeshPostEditChange ScopedPostEditChange(SkeletalMesh);
//Get the dependent generated LODs
TArray<int32> DependentLODs;
RemoveLODHelper::GetDependentLODs(SkeletalMesh, DesiredLOD, DependentLODs);
//Adjust LODInfo properties to be in sync with the LOD removal. We reverse iterate because we want to restore some LOD info property from the previous LOD
for (int32 NextLODIndex = SkeletalMesh->GetLODNum() -1; NextLODIndex > DesiredLOD; NextLODIndex--)
{
const FSkeletalMeshLODInfo* PreviousLODInfo = SkeletalMesh->GetLODInfo(NextLODIndex-1);
FSkeletalMeshLODInfo* NextLODInfo = SkeletalMesh->GetLODInfo(NextLODIndex);
if (!NextLODInfo)
{
continue;
}
//Adjust the reduction baseLOD
if(SkeletalMesh->IsReductionActive(NextLODIndex) && NextLODInfo->ReductionSettings.BaseLOD > DesiredLOD)
{
NextLODInfo->ReductionSettings.BaseLOD--;
}
//Propagate someproperties we need to take from the previous LOD
if (PreviousLODInfo)
{
//Screen size
NextLODInfo->ScreenSize = PreviousLODInfo->ScreenSize;
}
}
//Adjust the imported data so it point on the correct LOD index
if (DependentLODs.Num() > 0 && SkeletalMesh->HasMeshDescription(DesiredLOD))
{
const int32 FirstDepLODIndex = DependentLODs[0];
if (FSkeletalMeshLODInfo* FirstDepLODInfo = SkeletalMesh->GetLODInfo(FirstDepLODIndex))
{
SkeletalMesh->ModifyMeshDescription(DesiredLOD);
FMeshDescription* SourceMeshDescription = SkeletalMesh->GetMeshDescription(DesiredLOD);
SkeletalMesh->ModifyMeshDescription(FirstDepLODIndex);
SkeletalMesh->CreateMeshDescription(FirstDepLODIndex, MoveTemp(*SourceMeshDescription));
SkeletalMesh->CommitMeshDescription(FirstDepLODIndex);
//Manage the override original reduction source mesh data
if (SkelMeshModel->InlineReductionCacheDatas.IsValidIndex(FirstDepLODIndex))
{
//The inline reduction cache data will be recache by the build
SkelMeshModel->InlineReductionCacheDatas[FirstDepLODIndex].SetCacheGeometryInfo(MAX_uint32, MAX_uint32);
}
//Adjust Reduction settings
FirstDepLODInfo->ReductionSettings.BaseLOD = FirstDepLODIndex - 1;
RemoveLODHelper::AdjustReductionSettings(SkeletalMesh, FirstDepLODIndex, DesiredLOD);
//Do the adjustment for the other dependent LODs
for (int32 DependentLODsIndex = 1; DependentLODsIndex < DependentLODs.Num(); ++DependentLODsIndex)
{
const int32 DepLODIndex = DependentLODs[DependentLODsIndex];
FSkeletalMeshLODInfo* DepLODInfo = SkeletalMesh->GetLODInfo(DepLODIndex);
if (!DepLODInfo)
{
continue;
}
//Adjust Reduction settings
DepLODInfo->ReductionSettings.BaseLOD = FirstDepLODIndex - 1;
RemoveLODHelper::AdjustReductionSettings(SkeletalMesh, DepLODIndex, FirstDepLODIndex);
}
}
}
//remove all Morph target data for this LOD
for (UMorphTarget* MorphTarget : SkeletalMesh->GetMorphTargets())
{
if (MorphTarget->HasDataForLOD(DesiredLOD))
{
MorphTarget->GetMorphLODModels().RemoveAt(DesiredLOD);
}
}
SkelMeshModel->LODModels.RemoveAt(DesiredLOD);
SkeletalMesh->RemoveLODInfo(DesiredLOD);
RefreshLODChange(SkeletalMesh);
// Adjust the force LOD to point on the same one, if we are forcing a LOD greater then the one we delete, we want to continue pointing on it
// If we delete the LOD we are loking at, we fall back on auto LOD
for(auto Iter = UpdateContext.AssociatedComponents.CreateIterator(); Iter; ++Iter)
{
USkinnedMeshComponent* SkinnedComponent = Cast<USkinnedMeshComponent>(*Iter);
if(SkinnedComponent)
{
int32 CurrentForceLOD = SkinnedComponent->GetForcedLOD();
CurrentForceLOD = CurrentForceLOD == 0 ? 0 : CurrentForceLOD-1;
if(CurrentForceLOD == DesiredLOD)
{
SkinnedComponent->SetForcedLOD(0);
}
else if (CurrentForceLOD > DesiredLOD)
{
//Set back the force LOD, CurrentForceLOD was reduce by one so we simply set it.
SkinnedComponent->SetForcedLOD(CurrentForceLOD);
}
}
}
//Notify calling system of change
UpdateContext.OnLODChanged.ExecuteIfBound();
// Mark things for saving.
SkeletalMesh->MarkPackageDirty();
}
}
void FLODUtilities::RemoveLODs(FSkeletalMeshUpdateContext& UpdateContext, const TArray<int32>& DesiredLODs)
{
USkeletalMesh* SkeletalMesh = UpdateContext.SkeletalMesh;
FSkeletalMeshModel* SkelMeshModel = SkeletalMesh->GetImportedModel();
auto NoLODToRemoveDialog = []()
{
if (!FApp::IsUnattended())
{
FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "NoLODToRemove", "No LODs to remove!"));
}
UE_LOG(LogLODUtilities, Warning, TEXT("No LOD to remove or there must be at least one LOD after we remove this one."));
};
if (SkelMeshModel->LODModels.Num() <= 1 || DesiredLODs.Num() < 1)
{
NoLODToRemoveDialog();
return;
}
check(SkeletalMesh->GetLODNum() == SkelMeshModel->LODModels.Num());
TArray<int32> SortedDesiredLODs;
for (int32 DesiredLODIndex = 0; DesiredLODIndex < DesiredLODs.Num(); ++DesiredLODIndex)
{
int32 DesiredLOD = DesiredLODs[DesiredLODIndex];
if (SkelMeshModel->LODModels.Num() > 1 && SkelMeshModel->LODModels.IsValidIndex(DesiredLOD))
{
SortedDesiredLODs.Add(DesiredLOD);
}
else
{
UE_LOG(LogLODUtilities, Warning, TEXT("Cannot remove LOD {0}"), DesiredLOD);
}
}
if (SortedDesiredLODs.Num() < 1)
{
NoLODToRemoveDialog();
return;
}
{
FScopedSkeletalMeshPostEditChange ScopedPostEditChange(SkeletalMesh);
//Sort the LODs and reverse iterate the sorted array to remove the LODs from the end to avoid having to remap LODs index in the sortedDesiredLODs array
SortedDesiredLODs.Sort();
for (int32 SortedDesiredLODIndex = SortedDesiredLODs.Num()-1; SortedDesiredLODIndex >= 0 ; SortedDesiredLODIndex--)
{
int32 LODToRemove = SortedDesiredLODs[SortedDesiredLODIndex];
check(SkelMeshModel->LODModels.IsValidIndex(LODToRemove))
FLODUtilities::RemoveLOD(UpdateContext, LODToRemove);
}
}
}
namespace UE::Private
{
struct FScopedLockProperties
{
public:
FScopedLockProperties(USkeletalMesh* SkeletalMesh)
{
check(SkeletalMesh);
Lock = SkeletalMesh->LockPropertiesUntil();
}
~FScopedLockProperties()
{
Lock->Trigger();
Lock = nullptr;
}
private:
FEvent* Lock = nullptr;
};
}
bool FLODUtilities::SetCustomLOD(USkeletalMesh* DestinationSkeletalMesh, USkeletalMesh* SourceSkeletalMesh, const int32 LodIndex, const FString& SourceDataFilename)
{
if(!DestinationSkeletalMesh || !SourceSkeletalMesh)
{
return false;
}
FScopedSkeletalMeshPostEditChange ScopePostEditChange(DestinationSkeletalMesh);
//Lock the skeletal mesh
UE::Private::FScopedLockProperties ScopedLock(DestinationSkeletalMesh);
FSkinnedAssetAsyncBuildScope AsyncBuildScope(DestinationSkeletalMesh);
// Get a list of all the clothing assets affecting this LOD so we can re-apply later
TArray<ClothingAssetUtils::FClothingAssetMeshBinding> ClothingBindings;
TArray<UClothingAssetBase*> ClothingAssetsInUse;
TArray<int32> ClothingAssetSectionIndices;
TArray<int32> ClothingAssetInternalLodIndices;
//SAve custom imported morph targets
TMap<FString, TArray<FMorphTargetLodBackupData>> BackupImportedMorphTargetData;
TArray<FSkinWeightProfileInfo> ExistingSkinWeightProfileInfos;
TArray<FSkeletalMeshImportData> ExistingAlternateImportDataPerLOD;
FSkeletalMeshModel* const SourceImportedResource = SourceSkeletalMesh->GetImportedModel();
FSkeletalMeshModel* const DestImportedResource = DestinationSkeletalMesh->GetImportedModel();
if (!SourceImportedResource || !DestImportedResource)
{
return false;
}
if (DestImportedResource->LODModels.IsValidIndex(LodIndex))
{
FLODUtilities::UnbindClothingAndBackup(DestinationSkeletalMesh, ClothingBindings, LodIndex);
//Backup the lod custom imported morph
BackupCustomImportedMorphTargetData(DestinationSkeletalMesh, BackupImportedMorphTargetData);
int32 ExistingLodCount = DestinationSkeletalMesh->GetLODNum();
//Extract all LOD Skin weight profiles data, we will re-apply them at the end
ExistingSkinWeightProfileInfos = DestinationSkeletalMesh->GetSkinWeightProfiles();
DestinationSkeletalMesh->GetSkinWeightProfiles().Reset();
for (int32 AllLodIndex = 0; AllLodIndex < ExistingLodCount; ++AllLodIndex)
{
FSkeletalMeshLODModel& BuildLODModel = DestinationSkeletalMesh->GetImportedModel()->LODModels[AllLodIndex];
BuildLODModel.SkinWeightProfiles.Reset();
//Store the LOD alternate skinning profile data
FSkeletalMeshImportData& SkeletalMeshImportData = ExistingAlternateImportDataPerLOD.AddDefaulted_GetRef();
if (DestinationSkeletalMesh->HasMeshDescription(AllLodIndex))
{
PRAGMA_DISABLE_DEPRECATION_WARNINGS
DestinationSkeletalMesh->LoadLODImportedData(AllLodIndex, SkeletalMeshImportData);
PRAGMA_ENABLE_DEPRECATION_WARNINGS
}
}
}
//Lambda to call to re-apply the clothing
auto ReapplyClothing = [&DestinationSkeletalMesh, &ClothingBindings, &SourceImportedResource, &LodIndex]()
{
if (SourceImportedResource->LODModels.IsValidIndex(LodIndex))
{
// Re-apply our clothing assets
FLODUtilities::RestoreClothingFromBackup(DestinationSkeletalMesh, ClothingBindings, LodIndex);
}
};
auto ReapplyCustomImportedMorphTarget = [&DestinationSkeletalMesh, &BackupImportedMorphTargetData, &LodIndex]()
{
if (FMeshDescription* MeshDescription = DestinationSkeletalMesh->GetMeshDescription(LodIndex))
{
if (FLODUtilities::RestoreCustomImportedMorphTargetData(DestinationSkeletalMesh, LodIndex, *MeshDescription, BackupImportedMorphTargetData))
{
USkeletalMesh::FCommitMeshDescriptionParams CommitParams;
CommitParams.bForceUpdate = false;
CommitParams.bMarkPackageDirty = false;
DestinationSkeletalMesh->CommitMeshDescription(LodIndex, CommitParams);
}
}
};
auto ReapplyAlternateSkinning = [&DestinationSkeletalMesh, &ExistingSkinWeightProfileInfos, &ExistingAlternateImportDataPerLOD, &LodIndex]()
{
if (ExistingSkinWeightProfileInfos.Num() > 0)
{
TArray<FSkinWeightProfileInfo>& SkinProfiles = DestinationSkeletalMesh->GetSkinWeightProfiles();
SkinProfiles = ExistingSkinWeightProfileInfos;
for (const FSkinWeightProfileInfo& ProfileInfo : SkinProfiles)
{
const int32 LodCount = DestinationSkeletalMesh->GetLODNum();
for(int32 AllLodIndex = 0; AllLodIndex < LodCount; ++AllLodIndex)
{
if (!ExistingAlternateImportDataPerLOD.IsValidIndex(AllLodIndex))
{
continue;
}
if (!DestinationSkeletalMesh->HasMeshDescription(AllLodIndex))
{
continue;
}
const FSkeletalMeshLODInfo* LodInfo = DestinationSkeletalMesh->GetLODInfo(AllLodIndex);
if (!LodInfo)
{
continue;
}
const FSkeletalMeshImportData& ExistingImportDataSrc = ExistingAlternateImportDataPerLOD[AllLodIndex];
const FString ProfileNameStr = ProfileInfo.Name.ToString();
FSkeletalMeshImportData ImportDataDest;
PRAGMA_DISABLE_DEPRECATION_WARNINGS
DestinationSkeletalMesh->LoadLODImportedData(AllLodIndex, ImportDataDest);
PRAGMA_ENABLE_DEPRECATION_WARNINGS
int32 PointNumberDest = ImportDataDest.Points.Num();
int32 VertexNumberDest = ImportDataDest.Points.Num();
if (ExistingImportDataSrc.Points.Num() != PointNumberDest)
{
UE_LOG(LogLODUtilities, Error, TEXT("Alternate skinning mesh vertex number is different from the mesh LOD, we cannot apply the existing alternate skinning [%s] on the re-import skeletal mesh LOD %d [%s]"), *ProfileNameStr, LodIndex, *DestinationSkeletalMesh->GetName());
continue;
}
//Replace the data into the destination bulk data and save it
int32 ProfileIndex = 0;
if (ImportDataDest.AlternateInfluenceProfileNames.Find(ProfileNameStr, ProfileIndex))
{
ImportDataDest.AlternateInfluenceProfileNames.RemoveAt(ProfileIndex);
ImportDataDest.AlternateInfluences.RemoveAt(ProfileIndex);
}
int32 SrcProfileIndex = 0;
if (ExistingImportDataSrc.AlternateInfluenceProfileNames.Find(ProfileNameStr, SrcProfileIndex))
{
ImportDataDest.AlternateInfluenceProfileNames.Add(ProfileNameStr);
ImportDataDest.AlternateInfluences.Add(ExistingImportDataSrc.AlternateInfluences[SrcProfileIndex]);
}
//Resave the bulk data with the new or refreshed data
PRAGMA_DISABLE_DEPRECATION_WARNINGS
DestinationSkeletalMesh->SaveLODImportedData(AllLodIndex, ImportDataDest);
PRAGMA_ENABLE_DEPRECATION_WARNINGS
}
}
}
};
// Now we copy the base FSkeletalMeshLODModel from the imported skeletal mesh as the new LOD in the selected mesh.
if (SourceImportedResource->LODModels.Num() == 0)
{
return false;
}
// Names of root bones must match.
// If the names of root bones don't match, the LOD Mesh does not share skeleton with base Mesh.
if (SourceSkeletalMesh->GetRefSkeleton().GetBoneName(0) != DestinationSkeletalMesh->GetRefSkeleton().GetBoneName(0))
{
UE_LOG(LogLODUtilities, Error, TEXT("SkeletalMesh [%s] FLODUtilities::SetCustomLOD: Root bone in LOD is '%s' instead of '%s'.\nImport failed..")
, *DestinationSkeletalMesh->GetName()
, *SourceSkeletalMesh->GetRefSkeleton().GetBoneName(0).ToString()
, *DestinationSkeletalMesh->GetRefSkeleton().GetBoneName(0).ToString());
return false;
}
// We do some checking here that for every bone in the mesh we just imported, it's in our base ref skeleton, and the parent is the same.
for (int32 i = 0; i < SourceSkeletalMesh->GetRefSkeleton().GetRawBoneNum(); i++)
{
int32 LODBoneIndex = i;
FName LODBoneName = SourceSkeletalMesh->GetRefSkeleton().GetBoneName(LODBoneIndex);
int32 BaseBoneIndex = DestinationSkeletalMesh->GetRefSkeleton().FindBoneIndex(LODBoneName);
if (BaseBoneIndex == INDEX_NONE)
{
UE_LOG(LogLODUtilities, Error, TEXT("FLODUtilities::SetCustomLOD: Bone '%s' not found in destination SkeletalMesh '%s'.\nImport failed.")
, *LODBoneName.ToString()
, *DestinationSkeletalMesh->GetName());
return false;
}
if (i > 0)
{
int32 LODParentIndex = SourceSkeletalMesh->GetRefSkeleton().GetParentIndex(LODBoneIndex);
FName LODParentName = SourceSkeletalMesh->GetRefSkeleton().GetBoneName(LODParentIndex);
int32 BaseParentIndex = DestinationSkeletalMesh->GetRefSkeleton().GetParentIndex(BaseBoneIndex);
FName BaseParentName = DestinationSkeletalMesh->GetRefSkeleton().GetBoneName(BaseParentIndex);
if (LODParentName != BaseParentName)
{
UE_LOG(LogLODUtilities, Error, TEXT("SkeletalMesh [%s] FLODUtilities::SetCustomLOD: Bone '%s' in LOD has parent '%s' instead of '%s'")
, *DestinationSkeletalMesh->GetName()
, *LODBoneName.ToString()
, *LODParentName.ToString()
, *BaseParentName.ToString());
return false;
}
}
}
FSkeletalMeshLODModel& NewLODModel = SourceImportedResource->LODModels[0];
// If this LOD is not the base LOD, we check all bones we have sockets on are present in it.
if (LodIndex > 0)
{
const TArray<USkeletalMeshSocket*>& Sockets = DestinationSkeletalMesh->GetMeshOnlySocketList();
for (int32 i = 0; i < Sockets.Num(); i++)
{
// Find bone index the socket is attached to.
USkeletalMeshSocket* Socket = Sockets[i];
int32 SocketBoneIndex = SourceSkeletalMesh->GetRefSkeleton().FindBoneIndex(Socket->BoneName);
// If this LOD does not contain the socket bone, abort import.
if (SocketBoneIndex == INDEX_NONE)
{
UE_LOG(LogLODUtilities, Error, TEXT("FLODUtilities::SetCustomLOD: This LOD is missing bone '%s' used by socket '%s'.\nAborting import.")
, *Socket->BoneName.ToString()
, *Socket->SocketName.ToString());
return false;
}
}
}
{
//The imported LOD is always in LOD 0 of the SourceSkeletalMesh
const int32 SourceLODIndex = 0;
if (SourceSkeletalMesh->HasMeshDescription(SourceLODIndex))
{
// Fix up the imported data bone indexes
FSkeletalMeshImportData LODImportData;
PRAGMA_DISABLE_DEPRECATION_WARNINGS
SourceSkeletalMesh->LoadLODImportedData(SourceLODIndex, LODImportData);
PRAGMA_ENABLE_DEPRECATION_WARNINGS
const int32 LODImportDataBoneNumber = LODImportData.RefBonesBinary.Num();
//We want to create a remap array so we can fix all influence easily
TArray<int32> ImportDataBoneRemap;
ImportDataBoneRemap.AddZeroed(LODImportDataBoneNumber);
//We generate a new RefBonesBinary array to replace the existing one
TArray<SkeletalMeshImportData::FBone> RemapedRefBonesBinary;
RemapedRefBonesBinary.AddZeroed(DestinationSkeletalMesh->GetRefSkeleton().GetNum());
for (int32 ImportBoneIndex = 0; ImportBoneIndex < LODImportDataBoneNumber; ++ImportBoneIndex)
{
SkeletalMeshImportData::FBone& ImportedBone = LODImportData.RefBonesBinary[ImportBoneIndex];
int32 LODBoneIndex = ImportBoneIndex;
FName LODBoneName = FName(*FSkeletalMeshImportData::FixupBoneName(ImportedBone.Name));
int32 BaseBoneIndex = DestinationSkeletalMesh->GetRefSkeleton().FindBoneIndex(LODBoneName);
ImportDataBoneRemap[ImportBoneIndex] = BaseBoneIndex;
if (BaseBoneIndex != INDEX_NONE)
{
RemapedRefBonesBinary[BaseBoneIndex] = ImportedBone;
if (RemapedRefBonesBinary[BaseBoneIndex].ParentIndex != INDEX_NONE)
{
RemapedRefBonesBinary[BaseBoneIndex].ParentIndex = ImportDataBoneRemap[RemapedRefBonesBinary[BaseBoneIndex].ParentIndex];
}
}
}
//Copy the new RefBonesBinary over the existing one
LODImportData.RefBonesBinary = RemapedRefBonesBinary;
//Fix the influences
bool bNeedShrinking = false;
const int32 InfluenceNumber = LODImportData.Influences.Num();
for (int32 InfluenceIndex = InfluenceNumber - 1; InfluenceIndex >= 0; --InfluenceIndex)
{
SkeletalMeshImportData::FRawBoneInfluence& Influence = LODImportData.Influences[InfluenceIndex];
Influence.BoneIndex = ImportDataBoneRemap[Influence.BoneIndex];
if (Influence.BoneIndex == INDEX_NONE)
{
const int32 DeleteCount = 1;
LODImportData.Influences.RemoveAt(InfluenceIndex, DeleteCount, EAllowShrinking::No);
bNeedShrinking = true;
}
}
//Shrink the array if we have deleted at least one entry
if (bNeedShrinking)
{
LODImportData.Influences.Shrink();
}
//Save the fix up remap bone index
PRAGMA_DISABLE_DEPRECATION_WARNINGS
SourceSkeletalMesh->SaveLODImportedData(SourceLODIndex, LODImportData);
PRAGMA_ENABLE_DEPRECATION_WARNINGS
}
// Fix up the ActiveBoneIndices array.
for (int32 ActiveIndex = 0; ActiveIndex < NewLODModel.ActiveBoneIndices.Num(); ActiveIndex++)
{
int32 LODBoneIndex = NewLODModel.ActiveBoneIndices[ActiveIndex];
FName LODBoneName = SourceSkeletalMesh->GetRefSkeleton().GetBoneName(LODBoneIndex);
int32 BaseBoneIndex = DestinationSkeletalMesh->GetRefSkeleton().FindBoneIndex(LODBoneName);
checkSlow(BaseBoneIndex != INDEX_NONE);
NewLODModel.ActiveBoneIndices[ActiveIndex] = static_cast<FBoneIndexType>(BaseBoneIndex);
}
// Fix up the chunk BoneMaps.
for (int32 SectionIndex = 0; SectionIndex < NewLODModel.Sections.Num(); SectionIndex++)
{
FSkelMeshSection& Section = NewLODModel.Sections[SectionIndex];
for (int32 BoneMapIndex = 0; BoneMapIndex < Section.BoneMap.Num(); BoneMapIndex++)
{
int32 LODBoneIndex = Section.BoneMap[BoneMapIndex];
FName LODBoneName = SourceSkeletalMesh->GetRefSkeleton().GetBoneName(LODBoneIndex);
int32 BaseBoneIndex = DestinationSkeletalMesh->GetRefSkeleton().FindBoneIndex(LODBoneName);
checkSlow(BaseBoneIndex != INDEX_NONE);
Section.BoneMap[BoneMapIndex] = static_cast<FBoneIndexType>(BaseBoneIndex);
}
}
// Create the RequiredBones array in the LODModel from the ref skeleton.
for (int32 RequiredBoneIndex = 0; RequiredBoneIndex < NewLODModel.RequiredBones.Num(); RequiredBoneIndex++)
{
FName LODBoneName = SourceSkeletalMesh->GetRefSkeleton().GetBoneName(NewLODModel.RequiredBones[RequiredBoneIndex]);
int32 BaseBoneIndex = DestinationSkeletalMesh->GetRefSkeleton().FindBoneIndex(LODBoneName);
if (BaseBoneIndex != INDEX_NONE)
{
NewLODModel.RequiredBones[RequiredBoneIndex] = static_cast<FBoneIndexType>(BaseBoneIndex);
}
else
{
NewLODModel.RequiredBones.RemoveAt(RequiredBoneIndex--);
}
}
// Also sort the RequiredBones array to be strictly increasing.
NewLODModel.RequiredBones.Sort();
DestinationSkeletalMesh->GetRefSkeleton().EnsureParentsExistAndSort(NewLODModel.ActiveBoneIndices);
}
// To be extra-nice, we apply the difference between the root transform of the meshes to the verts.
FMatrix44f LODToBaseTransform = FMatrix44f(SourceSkeletalMesh->GetRefPoseMatrix(0).InverseFast() * DestinationSkeletalMesh->GetRefPoseMatrix(0));
for (int32 SectionIndex = 0; SectionIndex < NewLODModel.Sections.Num(); SectionIndex++)
{
FSkelMeshSection& Section = NewLODModel.Sections[SectionIndex];
// Fix up soft verts.
for (int32 i = 0; i < Section.SoftVertices.Num(); i++)
{
Section.SoftVertices[i].Position = LODToBaseTransform.TransformPosition(Section.SoftVertices[i].Position);
Section.SoftVertices[i].TangentX = LODToBaseTransform.TransformVector(Section.SoftVertices[i].TangentX);
Section.SoftVertices[i].TangentY = LODToBaseTransform.TransformVector(Section.SoftVertices[i].TangentY);
Section.SoftVertices[i].TangentZ = LODToBaseTransform.TransformVector(Section.SoftVertices[i].TangentZ);
}
}
TArray<FName> ExistingOriginalPerSectionMaterialImportName;
bool bIsReImport = false;
//Restore the LOD section data in case this LOD was reimport and some material match
if (DestImportedResource->LODModels.IsValidIndex(LodIndex) && SourceImportedResource->LODModels.IsValidIndex(0))
{
if (DestinationSkeletalMesh->HasMeshDescription(LodIndex))
{
FSkeletalMeshImportData DestinationLODImportData;
PRAGMA_DISABLE_DEPRECATION_WARNINGS
DestinationSkeletalMesh->LoadLODImportedData(LodIndex, DestinationLODImportData);
PRAGMA_ENABLE_DEPRECATION_WARNINGS
for (int32 SectionIndex = 0; SectionIndex < DestinationLODImportData.Materials.Num(); ++SectionIndex)
{
ExistingOriginalPerSectionMaterialImportName.Add(FName(*DestinationLODImportData.Materials[SectionIndex].MaterialImportName));
}
}
bIsReImport = true;
}
// If we want to add this as a new LOD to this mesh - add to LODModels/LODInfo array.
if (LodIndex == DestImportedResource->LODModels.Num())
{
DestImportedResource->LODModels.Add(new FSkeletalMeshLODModel());
// Add element to LODInfo array.
DestinationSkeletalMesh->AddLODInfo();
check(DestinationSkeletalMesh->GetLODNum() == DestImportedResource->LODModels.Num());
}
const int32 SourceLODIndex = 0;
if (SourceSkeletalMesh->HasMeshDescription(SourceLODIndex))
{
// Fix up the imported data bone indexes
FSkeletalMeshImportData SourceLODImportData;
PRAGMA_DISABLE_DEPRECATION_WARNINGS
SourceSkeletalMesh->LoadLODImportedData(SourceLODIndex, SourceLODImportData);
PRAGMA_ENABLE_DEPRECATION_WARNINGS
FLODUtilities::FSkeletalMeshMatchImportedMaterialsParameters Parameters;
Parameters.bIsReImport = bIsReImport;
Parameters.LodIndex = LodIndex;
Parameters.SkeletalMesh = DestinationSkeletalMesh;
Parameters.ImportedMaterials = &SourceLODImportData.Materials;
Parameters.ExistingOriginalPerSectionMaterialImportName = &ExistingOriginalPerSectionMaterialImportName;
Parameters.CustomImportedLODModel = &NewLODModel;
FLODUtilities::MatchImportedMaterials(Parameters);
}
// Set up LODMaterialMap to number of materials in new mesh.
FSkeletalMeshLODInfo& LODInfo = *(DestinationSkeletalMesh->GetLODInfo(LodIndex));
//Copy the build settings
if (SourceSkeletalMesh->GetLODInfo(0))
{
const FSkeletalMeshLODInfo& ImportedLODInfo = *(SourceSkeletalMesh->GetLODInfo(0));
LODInfo.BuildSettings = ImportedLODInfo.BuildSettings;
}
// Release all resources before replacing the model
DestinationSkeletalMesh->PreEditChange(NULL);
// Assign new FSkeletalMeshLODModel to desired slot in selected skeletal mesh.
FSkeletalMeshLODModel::CopyStructure(&(DestImportedResource->LODModels[LodIndex]), &NewLODModel);
//Copy the import data into the base skeletalmesh for the imported LOD
FMeshDescription SourceMeshDescription;
if (SourceSkeletalMesh->CloneMeshDescription(0, SourceMeshDescription))
{
DestinationSkeletalMesh->ModifyMeshDescription(LodIndex);
DestinationSkeletalMesh->CreateMeshDescription(LodIndex, MoveTemp(SourceMeshDescription));
DestinationSkeletalMesh->CommitMeshDescription(LodIndex);
}
// If this LOD had been generated previously by automatic mesh reduction, clear that flag.
LODInfo.bHasBeenSimplified = false;
if (DestinationSkeletalMesh->GetLODSettings() == nullptr || !DestinationSkeletalMesh->GetLODSettings()->HasValidSettings() || DestinationSkeletalMesh->GetLODSettings()->GetNumberOfSettings() <= LodIndex)
{
//Make sure any custom LOD have correct settings (no reduce)
LODInfo.ReductionSettings.NumOfTrianglesPercentage = 1.0f;
LODInfo.ReductionSettings.MaxNumOfTriangles = MAX_uint32;
LODInfo.ReductionSettings.MaxNumOfTrianglesPercentage = MAX_uint32;
LODInfo.ReductionSettings.NumOfVertPercentage = 1.0f;
LODInfo.ReductionSettings.MaxNumOfVerts = MAX_uint32;
LODInfo.ReductionSettings.MaxNumOfVertsPercentage = MAX_uint32;
LODInfo.ReductionSettings.MaxDeviationPercentage = 0.0f;
}
// Set LOD source filename
DestinationSkeletalMesh->GetLODInfo(LodIndex)->SourceImportFilename = UAssetImportData::SanitizeImportFilename(SourceDataFilename, nullptr);
DestinationSkeletalMesh->GetLODInfo(LodIndex)->bImportWithBaseMesh = false;
ReapplyClothing();
ReapplyCustomImportedMorphTarget();
ReapplyAlternateSkinning();
// Notification of success
FNotificationInfo NotificationInfo(FText::GetEmpty());
NotificationInfo.Text = FText::Format(NSLOCTEXT("UnrealEd", "LODImportSuccessful", "Mesh for LOD {0} imported successfully!"), FText::AsNumber(LodIndex));
NotificationInfo.ExpireDuration = 5.0f;
FSlateNotificationManager::Get().AddNotification(NotificationInfo);
return true;
}
/** Given three direction vectors, indicates if A and B are on the same 'side' of Vec. */
bool VectorsOnSameSide(const FVector2D& Vec, const FVector2D& A, const FVector2D& B)
{
return !(((B.Y - A.Y)*(Vec.X - A.X)) + ((A.X - B.X)*(Vec.Y - A.Y)) < 0.0f);
}
double PointToSegmentDistanceSquare(const FVector2D& A, const FVector2D& B, const FVector2D& P)
{
return FVector2D::DistSquared(P, FMath::ClosestPointOnSegment2D(P, A, B));
}
/** Return true if P is within triangle created by A, B and C. */
bool PointInTriangle(const FVector2D& A, const FVector2D& B, const FVector2D& C, const FVector2D& P)
{
//If the point is on a triangle point we consider the point inside the triangle
if (P.Equals(A) || P.Equals(B) || P.Equals(C))
{
return true;
}
// If its on the same side as the remaining vert for all edges, then its inside.
if (VectorsOnSameSide(A, B, P) &&
VectorsOnSameSide(B, C, P) &&
VectorsOnSameSide(C, A, P))
{
return true;
}
//Make sure point on the edge are count inside the triangle
if (PointToSegmentDistanceSquare(A, B, P) <= KINDA_SMALL_NUMBER)
{
return true;
}
if (PointToSegmentDistanceSquare(B, C, P) <= KINDA_SMALL_NUMBER)
{
return true;
}
if (PointToSegmentDistanceSquare(C, A, P) <= KINDA_SMALL_NUMBER)
{
return true;
}
return false;
}
/** Given three direction vectors, indicates if A and B are on the same 'side' of Vec. */
bool VectorsOnSameSide(const FVector3f& Vec, const FVector3f& A, const FVector3f& B, const float SameSideDotProductEpsilon)
{
const FVector CrossA = FVector(Vec ^ A);
const FVector CrossB = FVector(Vec ^ B);
double DotWithEpsilon = SameSideDotProductEpsilon + (CrossA | CrossB);
return !(DotWithEpsilon < 0.0f);
}
/** Util to see if P lies within triangle created by A, B and C. */
bool PointInTriangle(const FVector3f& A, const FVector3f& B, const FVector3f& C, const FVector3f& P)
{
// Cross product indicates which 'side' of the vector the point is on
// If its on the same side as the remaining vert for all edges, then its inside.
if (VectorsOnSameSide(B - A, P - A, C - A, KINDA_SMALL_NUMBER) &&
VectorsOnSameSide(C - B, P - B, A - B, KINDA_SMALL_NUMBER) &&
VectorsOnSameSide(A - C, P - C, B - C, KINDA_SMALL_NUMBER))
{
return true;
}
return false;
}
FVector3f GetBaryCentric(const FVector3f& Point, const FVector3f& A, const FVector3f& B, const FVector3f& C, const TCHAR* DebugContext)
{
// Compute the normal of the triangle
const FVector3f TriNorm = (B - A) ^ (C - A);
//check collinearity of A,B,C
if (TriNorm.SizeSquared() <= SMALL_NUMBER)
{
float DistA = FVector3f::DistSquared(Point, A);
float DistB = FVector3f::DistSquared(Point, B);
float DistC = FVector3f::DistSquared(Point, C);
if(DistA <= DistB && DistA <= DistC)
{
return FVector3f(1.0f, 0.0f, 0.0f);
}
if (DistB <= DistC)
{
return FVector3f(0.0f, 1.0f, 0.0f);
}
return FVector3f(0.0f, 0.0f, 1.0f);
}
FVector ToRet(0, 0, 0);
if (!FMath::ComputeBarycentricTri((FVector)Point, (FVector)A, (FVector)B, (FVector)C, ToRet, UE_DOUBLE_SMALL_NUMBER))
{
UE_LOG(LogUnrealMath, Warning, TEXT("SkeletalMesh [%s]: Small triangle detected in FMath::ComputeBaryCentric2D(); can't compute valid barycentric coordinate."), DebugContext ? DebugContext : TEXT("Unknown Source"));
}
return (FVector3f)ToRet;
}
struct FTriangleElement
{
FBox2D UVsBound;
FBoxCenterAndExtent PositionBound;
TArray<FSoftSkinVertex> Vertices;
TArray<uint32> Indexes;
uint32 TriangleIndex;
};
bool FindTriangleUVMatch(const FVector2D& TargetUV, const TArray<FTriangleElement>& Triangles, const TArray<uint32>& QuadTreeTriangleResults, TArray<uint32>& MatchTriangleIndexes)
{
for (uint32 TriangleIndex : QuadTreeTriangleResults)
{
const FTriangleElement& TriangleElement = Triangles[TriangleIndex];
if (PointInTriangle(FVector2D(TriangleElement.Vertices[0].UVs[0]), FVector2D(TriangleElement.Vertices[1].UVs[0]), FVector2D(TriangleElement.Vertices[2].UVs[0]), TargetUV))
{
MatchTriangleIndexes.Add(TriangleIndex);
}
TriangleIndex++;
}
return MatchTriangleIndexes.Num() == 0 ? false : true;
}
bool FindTrianglePositionMatch(const FVector& Position, const TArray<FTriangleElement>& Triangles, const TArray<FTriangleElement>& OcTreeTriangleResults, TArray<uint32>& MatchTriangleIndexes)
{
for (const FTriangleElement& Triangle : OcTreeTriangleResults)
{
uint32 TriangleIndex = Triangle.TriangleIndex;
const FTriangleElement& TriangleElement = Triangles[TriangleIndex];
if (PointInTriangle((FVector3f)TriangleElement.Vertices[0].Position, (FVector3f)TriangleElement.Vertices[1].Position, (FVector3f)TriangleElement.Vertices[2].Position, (FVector3f)Position))
{
MatchTriangleIndexes.Add(TriangleIndex);
}
TriangleIndex++;
}
return MatchTriangleIndexes.Num() == 0 ? false : true;
}
struct FTargetMatch
{
float BarycentricWeight[3]; //The weight we use to interpolate the TARGET data
uint32 Indices[3]; //BASE Index of the triangle vertice
//Default constructor
FTargetMatch()
{
BarycentricWeight[0] = BarycentricWeight[1] = BarycentricWeight[2] = 0.0f;
Indices[0] = Indices[1] = Indices[2] = INDEX_NONE;
}
};
void ProjectTargetOnBase(const TArray<FSoftSkinVertex>& BaseVertices, const TArray<TArray<uint32>>& PerSectionBaseTriangleIndices,
TArray<FTargetMatch>& TargetMatchData, const TArray<FSkelMeshSection>& TargetSections, const TArray<int32>& TargetSectionMatchBaseIndex, const TCHAR* DebugContext)
{
bool bNoMatchMsgDone = false;
bool bNoUVsMsgDisplayed = false;
TArray<FTriangleElement> Triangles;
//Project section target vertices on match base section using the UVs coordinates
for (int32 SectionIndex = 0; SectionIndex < TargetSections.Num(); ++SectionIndex)
{
//Use the remap base index in case some sections disappear during the reduce phase
int32 BaseSectionIndex = TargetSectionMatchBaseIndex[SectionIndex];
if (BaseSectionIndex == INDEX_NONE || !PerSectionBaseTriangleIndices.IsValidIndex(BaseSectionIndex) || PerSectionBaseTriangleIndices[BaseSectionIndex].Num() < 1)
{
continue;
}
//Target vertices for the Section
const TArray<FSoftSkinVertex>& TargetVertices = TargetSections[SectionIndex].SoftVertices;
//Base Triangle indices for the matched base section
const TArray<uint32>& BaseTriangleIndices = PerSectionBaseTriangleIndices[BaseSectionIndex];
FBox2D BaseMeshUVBound(EForceInit::ForceInit);
FBox BaseMeshPositionBound(EForceInit::ForceInit);
//Fill the triangle element to speed up the triangle research
Triangles.Reset(BaseTriangleIndices.Num() / 3);
for (uint32 TriangleIndex = 0; TriangleIndex < (uint32)BaseTriangleIndices.Num(); TriangleIndex += 3)
{
FTriangleElement TriangleElement;
TriangleElement.UVsBound.Init();
for (int32 Corner = 0; Corner < 3; ++Corner)
{
uint32 CornerIndice = BaseTriangleIndices[TriangleIndex + Corner];
check(BaseVertices.IsValidIndex(CornerIndice));
const FSoftSkinVertex& BaseVertex = BaseVertices[CornerIndice];
TriangleElement.Indexes.Add(CornerIndice);
TriangleElement.Vertices.Add(BaseVertex);
TriangleElement.UVsBound += FVector2D(BaseVertex.UVs[0]);
BaseMeshPositionBound += (FVector)BaseVertex.Position;
}
BaseMeshUVBound += TriangleElement.UVsBound;
TriangleElement.TriangleIndex = Triangles.Num();
Triangles.Add(TriangleElement);
}
if (BaseMeshUVBound.GetExtent().IsNearlyZero())
{
if(!bNoUVsMsgDisplayed)
{
UE_LOG(LogLODUtilities, Display, TEXT("SkeletalMesh [%s] Remap morph target: Cannot remap morph target because source UVs are missings."), DebugContext ? DebugContext : TEXT("Unknown Source"));
bNoUVsMsgDisplayed = true;
}
continue;
}
//Setup the Quad tree
float UVsQuadTreeMinSize = 0.001f;
TQuadTree<uint32, 100> QuadTree(BaseMeshUVBound, UVsQuadTreeMinSize);
for (FTriangleElement& TriangleElement : Triangles)
{
QuadTree.Insert(TriangleElement.TriangleIndex, TriangleElement.UVsBound, DebugContext);
}
//Retrieve all triangle that are close to our point, let get 5% of UV extend
double DistanceThreshold = BaseMeshUVBound.GetExtent().Size()*0.05;
//Find a match triangle for every target vertices
TArray<uint32> QuadTreeTriangleResults;
QuadTreeTriangleResults.Reserve(Triangles.Num() / 10); //Reserve 10% to speed up the query
for (uint32 TargetVertexIndex = 0; TargetVertexIndex < (uint32)TargetVertices.Num(); ++TargetVertexIndex)
{
FVector2D TargetUV = FVector2D(TargetVertices[TargetVertexIndex].UVs[0]);
//Reset the last data without flushing the memmery allocation
QuadTreeTriangleResults.Reset();
const uint32 FullTargetIndex = TargetSections[SectionIndex].BaseVertexIndex + TargetVertexIndex;
//Make sure the array is allocate properly
if (!TargetMatchData.IsValidIndex(FullTargetIndex))
{
continue;
}
//Set default data for the target match, in case we cannot found a match
FTargetMatch& TargetMatch = TargetMatchData[FullTargetIndex];
for (int32 Corner = 0; Corner < 3; ++Corner)
{
TargetMatch.Indices[Corner] = INDEX_NONE;
TargetMatch.BarycentricWeight[Corner] = 0.3333f; //The weight will be use to found the proper delta
}
FVector2D Extent(DistanceThreshold, DistanceThreshold);
FBox2D CurBox(TargetUV - Extent, TargetUV + Extent);
while (QuadTreeTriangleResults.Num() <= 0)
{
QuadTree.GetElements(CurBox, QuadTreeTriangleResults);
Extent *= 2;
CurBox = FBox2D(TargetUV - Extent, TargetUV + Extent);
}
auto GetDistancePointToBaseTriangle = [&Triangles, &TargetVertices, &TargetVertexIndex](const uint32 BaseTriangleIndex)->double
{
FTriangleElement& CandidateTriangle = Triangles[BaseTriangleIndex];
return FVector::DistSquared(FMath::ClosestPointOnTriangleToPoint((FVector)TargetVertices[TargetVertexIndex].Position, (FVector)CandidateTriangle.Vertices[0].Position, (FVector)CandidateTriangle.Vertices[1].Position, (FVector)CandidateTriangle.Vertices[2].Position), (FVector)TargetVertices[TargetVertexIndex].Position);
};
auto FailSafeUnmatchVertex = [&GetDistancePointToBaseTriangle, &QuadTreeTriangleResults](uint32 &OutIndexMatch)->bool
{
bool bFoundMatch = false;
double ClosestTriangleDistSquared = std::numeric_limits<double>::max();
for (uint32 MatchTriangleIndex : QuadTreeTriangleResults)
{
double TriangleDistSquared = GetDistancePointToBaseTriangle(MatchTriangleIndex);
if (TriangleDistSquared < ClosestTriangleDistSquared)
{
ClosestTriangleDistSquared = TriangleDistSquared;
OutIndexMatch = MatchTriangleIndex;
bFoundMatch = true;
}
}
return bFoundMatch;
};
//Find all Triangles that contain the Target UV
if (QuadTreeTriangleResults.Num() > 0)
{
TArray<uint32> MatchTriangleIndexes;
uint32 FoundIndexMatch = INDEX_NONE;
if(!FindTriangleUVMatch(TargetUV, Triangles, QuadTreeTriangleResults, MatchTriangleIndexes))
{
if (!FailSafeUnmatchVertex(FoundIndexMatch))
{
//We should always have a match
if (!bNoMatchMsgDone)
{
UE_LOG(LogLODUtilities, Warning, TEXT("Reduce LOD, remap morph target: Cannot find a triangle from the base LOD that contain a vertex UV in the target LOD. Remap morph target quality will be lower."));
bNoMatchMsgDone = true;
}
continue;
}
}
double ClosestTriangleDistSquared = std::numeric_limits<double>::max();
if (MatchTriangleIndexes.Num() == 1)
{
//One match, this mean no mirror UVs simply take the single match
FoundIndexMatch = MatchTriangleIndexes[0];
ClosestTriangleDistSquared = GetDistancePointToBaseTriangle(FoundIndexMatch);
}
else
{
//Geometry can use mirror so the UVs are not unique. Use the closest match triangle to the point to find the best match
for (uint32 MatchTriangleIndex : MatchTriangleIndexes)
{
double TriangleDistSquared = GetDistancePointToBaseTriangle(MatchTriangleIndex);
if (TriangleDistSquared < ClosestTriangleDistSquared)
{
ClosestTriangleDistSquared = TriangleDistSquared;
FoundIndexMatch = MatchTriangleIndex;
}
}
}
//FAIL SAFE, make sure we have a match that make sense
//Use the mesh section geometry bound extent (10% of it) to validate we are close enough.
if (ClosestTriangleDistSquared > BaseMeshPositionBound.GetExtent().SizeSquared()*0.1f)
{
//Executing fail safe, if the UVs are too much off because of the reduction, use the closest distance to polygons to find the match
//This path is not optimize and should not happen often.
FailSafeUnmatchVertex(FoundIndexMatch);
}
//We should always have a valid match at this point
check(FoundIndexMatch != INDEX_NONE);
FTriangleElement& BestTriangle = Triangles[FoundIndexMatch];
//Found the surface area of the 3 barycentric triangles from the UVs
FVector3f BarycentricWeight;
BarycentricWeight = GetBaryCentric(FVector3f(FVector2f(TargetUV), 0.0f), FVector3f(BestTriangle.Vertices[0].UVs[0], 0.0f), FVector3f(BestTriangle.Vertices[1].UVs[0], 0.0f), FVector3f(BestTriangle.Vertices[2].UVs[0], 0.0f), DebugContext); // LWC_TODO: Precision loss
//Fill the target match
for (int32 Corner = 0; Corner < 3; ++Corner)
{
TargetMatch.Indices[Corner] = BestTriangle.Indexes[Corner];
TargetMatch.BarycentricWeight[Corner] = BarycentricWeight[Corner]; //The weight will be use to found the proper delta
}
}
else
{
if (!bNoMatchMsgDone)
{
UE_LOG(LogLODUtilities, Warning, TEXT("Reduce LOD, remap morph target: Cannot find a triangle from the base LOD that contain a vertex UV in the target LOD. Remap morph target quality will be lower."));
bNoMatchMsgDone = true;
}
continue;
}
}
}
}
void CreateLODMorphTarget(USkeletalMesh* SkeletalMesh, const FInlineReductionDataParameter& InlineReductionDataParameter, int32 SourceLOD, int32 DestinationLOD, const TMap<UMorphTarget *, TMap<uint32, uint32>>& PerMorphTargetBaseIndexToMorphTargetDelta, const TMap<uint32, TArray<uint32>>& BaseMorphIndexToTargetIndexList, const TArray<FSoftSkinVertex>& TargetVertices, const TArray<FTargetMatch>& TargetMatchData)
{
FSkeletalMeshModel* SkeletalMeshModel = SkeletalMesh->GetImportedModel();
const FSkeletalMeshLODModel& TargetLODModel = SkeletalMeshModel->LODModels[DestinationLOD];
bool bInitializeMorphData = false;
for (UMorphTarget *MorphTarget : SkeletalMesh->GetMorphTargets())
{
if (!MorphTarget->HasDataForLOD(SourceLOD))
{
continue;
}
bool bUseBaseMorphDelta = SourceLOD == DestinationLOD && InlineReductionDataParameter.bIsDataValid && InlineReductionDataParameter.InlineOriginalSrcMorphTargetData.Contains(MorphTarget->GetFullName());
const TArray<FMorphTargetDelta> *BaseMorphDeltas = bUseBaseMorphDelta ? InlineReductionDataParameter.InlineOriginalSrcMorphTargetData.Find(MorphTarget->GetFullName()) : nullptr;
if (BaseMorphDeltas == nullptr || BaseMorphDeltas->Num() <= 0)
{
bUseBaseMorphDelta = false;
}
const TMap<uint32, uint32>& BaseIndexToMorphTargetDelta = PerMorphTargetBaseIndexToMorphTargetDelta[MorphTarget];
TArray<FMorphTargetDelta> NewMorphTargetDeltas;
TSet<uint32> CreatedTargetIndex;
TMap<FVector3f, TArray<uint32>> MorphTargetPerPosition;
const FMorphTargetLODModel& BaseMorphModel = MorphTarget->GetMorphLODModels()[SourceLOD];
//Iterate each original morph target source index to fill the NewMorphTargetDeltas array with the TargetMatchData.
const TArray<FMorphTargetDelta>& Vertices = bUseBaseMorphDelta ? *BaseMorphDeltas : BaseMorphModel.Vertices;
for (uint32 MorphDeltaIndex = 0; MorphDeltaIndex < (uint32)(Vertices.Num()); ++MorphDeltaIndex)
{
const FMorphTargetDelta& MorphDelta = Vertices[MorphDeltaIndex];
const TArray<uint32>* TargetIndexesPtr = BaseMorphIndexToTargetIndexList.Find(MorphDelta.SourceIdx);
if (TargetIndexesPtr == nullptr)
{
continue;
}
const TArray<uint32>& TargetIndexes = *TargetIndexesPtr;
for (int32 MorphTargetIndex = 0; MorphTargetIndex < TargetIndexes.Num(); ++MorphTargetIndex)
{
uint32 TargetIndex = TargetIndexes[MorphTargetIndex];
if (CreatedTargetIndex.Contains(TargetIndex))
{
continue;
}
CreatedTargetIndex.Add(TargetIndex);
const FVector3f& SearchPosition = TargetVertices[TargetIndex].Position;
FMorphTargetDelta MatchMorphDelta;
MatchMorphDelta.SourceIdx = TargetIndex;
const FTargetMatch& TargetMatch = TargetMatchData[TargetIndex];
//Find the Position/tangent delta for the MatchMorphDelta using the barycentric weight
MatchMorphDelta.PositionDelta = FVector3f::ZeroVector;
MatchMorphDelta.TangentZDelta = FVector3f::ZeroVector;
for (int32 Corner = 0; Corner < 3; ++Corner)
{
const uint32* BaseMorphTargetIndexPtr = BaseIndexToMorphTargetDelta.Find(TargetMatch.Indices[Corner]);
if (BaseMorphTargetIndexPtr != nullptr && Vertices.IsValidIndex(*BaseMorphTargetIndexPtr))
{
const FMorphTargetDelta& BaseMorphTargetDelta = Vertices[*BaseMorphTargetIndexPtr];
FVector3f BasePositionDelta = !BaseMorphTargetDelta.PositionDelta.ContainsNaN() ? BaseMorphTargetDelta.PositionDelta : FVector3f(0.0f);
FVector3f BaseTangentZDelta = !BaseMorphTargetDelta.TangentZDelta.ContainsNaN() ? BaseMorphTargetDelta.TangentZDelta : FVector3f(0.0f);
MatchMorphDelta.PositionDelta += BasePositionDelta * TargetMatch.BarycentricWeight[Corner];
MatchMorphDelta.TangentZDelta += BaseTangentZDelta * TargetMatch.BarycentricWeight[Corner];
}
ensure(!MatchMorphDelta.PositionDelta.ContainsNaN());
ensure(!MatchMorphDelta.TangentZDelta.ContainsNaN());
}
//Make sure all morph delta that are at the same position use the same delta to avoid hole in the geometry
TArray<uint32> *MorphTargetsIndexUsingPosition = nullptr;
MorphTargetsIndexUsingPosition = MorphTargetPerPosition.Find(SearchPosition);
if (MorphTargetsIndexUsingPosition != nullptr)
{
//Get the maximum position/tangent delta for the existing matched morph delta
FVector3f PositionDelta = MatchMorphDelta.PositionDelta;
FVector3f TangentZDelta = MatchMorphDelta.TangentZDelta;
for (uint32 ExistingMorphTargetIndex : *MorphTargetsIndexUsingPosition)
{
const FMorphTargetDelta& ExistingMorphDelta = NewMorphTargetDeltas[ExistingMorphTargetIndex];
PositionDelta = PositionDelta.SizeSquared() > ExistingMorphDelta.PositionDelta.SizeSquared() ? PositionDelta : ExistingMorphDelta.PositionDelta;
TangentZDelta = TangentZDelta.SizeSquared() > ExistingMorphDelta.TangentZDelta.SizeSquared() ? TangentZDelta : ExistingMorphDelta.TangentZDelta;
}
//Update all MorphTarget that share the same position.
for (uint32 ExistingMorphTargetIndex : *MorphTargetsIndexUsingPosition)
{
FMorphTargetDelta& ExistingMorphDelta = NewMorphTargetDeltas[ExistingMorphTargetIndex];
ExistingMorphDelta.PositionDelta = PositionDelta;
ExistingMorphDelta.TangentZDelta = TangentZDelta;
}
MatchMorphDelta.PositionDelta = PositionDelta;
MatchMorphDelta.TangentZDelta = TangentZDelta;
MorphTargetsIndexUsingPosition->Add(NewMorphTargetDeltas.Num());
}
else
{
MorphTargetPerPosition.Add(TargetVertices[TargetIndex].Position).Add(NewMorphTargetDeltas.Num());
}
NewMorphTargetDeltas.Add(MatchMorphDelta);
}
}
//Register the new morph target on the target LOD
MorphTarget->PopulateDeltas(NewMorphTargetDeltas, DestinationLOD, TargetLODModel.Sections, false, true);
if (MorphTarget->HasValidData())
{
bInitializeMorphData |= SkeletalMesh->RegisterMorphTarget(MorphTarget, false);
}
}
if (bInitializeMorphData)
{
SkeletalMesh->InitMorphTargetsAndRebuildRenderData();
}
}
void FLODUtilities::ClearGeneratedMorphTarget(USkeletalMesh* SkeletalMesh, int32 TargetLOD)
{
check(SkeletalMesh);
FSkeletalMeshModel* SkeletalMeshResource = SkeletalMesh->GetImportedModel();
if (!SkeletalMeshResource ||
!SkeletalMeshResource->LODModels.IsValidIndex(TargetLOD))
{
//Abort clearing
return;
}
const FSkeletalMeshLODModel& TargetLODModel = SkeletalMeshResource->LODModels[TargetLOD];
//Make sure we have some morph for this LOD
for (UMorphTarget *MorphTarget : SkeletalMesh->GetMorphTargets())
{
if (!MorphTarget->HasDataForLOD(TargetLOD))
{
continue;
}
//if (MorphTarget->MorphLODModels[TargetLOD].bGeneratedByEngine)
{
MorphTarget->GetMorphLODModels()[TargetLOD].Reset();
// if this is the last one, we can remove empty ones
if (TargetLOD == MorphTarget->GetMorphLODModels().Num() - 1)
{
MorphTarget->RemoveEmptyMorphTargets();
}
}
}
}
void FLODUtilities::ApplyMorphTargetsToLOD(USkeletalMesh* SkeletalMesh, int32 SourceLOD, int32 DestinationLOD, const FInlineReductionDataParameter& InlineReductionDataParameter)
{
check(SkeletalMesh);
FSkeletalMeshModel* SkeletalMeshResource = SkeletalMesh->GetImportedModel();
if (!SkeletalMeshResource ||
!SkeletalMeshResource->LODModels.IsValidIndex(SourceLOD) ||
!SkeletalMeshResource->LODModels.IsValidIndex(DestinationLOD) ||
SourceLOD > DestinationLOD)
{
//Cannot reduce if the source model is missing or we reduce from a higher index LOD
return;
}
FSkeletalMeshLODModel& SourceLODModel = SkeletalMeshResource->LODModels[SourceLOD];
bool bReduceBaseLOD = DestinationLOD == SourceLOD && InlineReductionDataParameter.bIsDataValid;
if (!bReduceBaseLOD && SourceLOD == DestinationLOD)
{
//Abort remapping of morph target since the data is missing
return;
}
//Make sure we have some morph for this LOD
bool bContainsMorphTargets = false;
for (UMorphTarget* MorphTarget : SkeletalMesh->GetMorphTargets())
{
if (MorphTarget->HasDataForLOD(SourceLOD))
{
bContainsMorphTargets = true;
}
}
if (!bContainsMorphTargets)
{
//No morph target to remap
return;
}
const FSkeletalMeshLODModel& BaseLODModel = bReduceBaseLOD ? InlineReductionDataParameter.InlineOriginalSrcModel : SkeletalMeshResource->LODModels[SourceLOD];
const FSkeletalMeshLODInfo* BaseLODInfo = SkeletalMesh->GetLODInfo(SourceLOD);
const FSkeletalMeshLODModel& TargetLODModel = SkeletalMeshResource->LODModels[DestinationLOD];
const FSkeletalMeshLODInfo* TargetLODInfo = SkeletalMesh->GetLODInfo(DestinationLOD);
TArray<int32> BaseLODMaterialMap = BaseLODInfo ? BaseLODInfo->LODMaterialMap : TArray<int32>();
TArray<int32> TargetLODMaterialMap = TargetLODInfo ? TargetLODInfo->LODMaterialMap : TArray<int32>();
auto InternalGetSectionMaterialIndex = [](const FSkeletalMeshLODModel& LODModel, int32 SectionIndex)->int32
{
if (!LODModel.Sections.IsValidIndex(SectionIndex))
{
return 0;
}
return LODModel.Sections[SectionIndex].MaterialIndex;
};
auto GetBaseSectionMaterialIndex = [&BaseLODModel, &InternalGetSectionMaterialIndex](int32 SectionIndex)->int32
{
return InternalGetSectionMaterialIndex(BaseLODModel, SectionIndex);
};
auto GetTargetSectionMaterialIndex = [&TargetLODModel, &InternalGetSectionMaterialIndex](int32 SectionIndex)->int32
{
return InternalGetSectionMaterialIndex(TargetLODModel, SectionIndex);
};
//We have to match target sections index with the correct base section index. Reduced LODs can contain a different number of sections than the base LOD
TArray<int32> TargetSectionMatchBaseIndex;
//Initialize the array to INDEX_NONE
TargetSectionMatchBaseIndex.AddUninitialized(TargetLODModel.Sections.Num());
for (int32 TargetSectionIndex = 0; TargetSectionIndex < TargetLODModel.Sections.Num(); ++TargetSectionIndex)
{
TargetSectionMatchBaseIndex[TargetSectionIndex] = INDEX_NONE;
}
TBitArray<> BaseSectionMatch;
BaseSectionMatch.Init(false, BaseLODModel.Sections.Num());
//Find corresponding section indices from Source LOD for Target LOD
for (int32 TargetSectionIndex = 0; TargetSectionIndex < TargetLODModel.Sections.Num(); ++TargetSectionIndex)
{
int32 TargetSectionMaterialIndex = GetTargetSectionMaterialIndex(TargetSectionIndex);
for (int32 BaseSectionIndex = 0; BaseSectionIndex < BaseLODModel.Sections.Num(); ++BaseSectionIndex)
{
if (BaseSectionMatch[BaseSectionIndex])
{
continue;
}
int32 BaseSectionMaterialIndex = GetBaseSectionMaterialIndex(BaseSectionIndex);
if (TargetSectionMaterialIndex == BaseSectionMaterialIndex)
{
TargetSectionMatchBaseIndex[TargetSectionIndex] = BaseSectionIndex;
BaseSectionMatch[BaseSectionIndex] = true;
break;
}
}
}
//We should have match all the target sections
if (TargetSectionMatchBaseIndex.Contains(INDEX_NONE))
{
//This case is not fatal but need attention.
//Because of the chunking its possible a generated LOD end up with more sections.
UE_ASSET_LOG(LogLODUtilities, Display, SkeletalMesh, TEXT("FLODUtilities::ApplyMorphTargetsToLOD: The target contain more section then the source. Extra sections will not be affected by morph targets remap"));
}
TArray<FSoftSkinVertex> BaseVertices;
TArray<FSoftSkinVertex> TargetVertices;
BaseLODModel.GetVertices(BaseVertices);
TargetLODModel.GetVertices(TargetVertices);
//Create the base triangle indices per section
TArray<TArray<uint32>> BaseTriangleIndices;
int32 SectionCount = BaseLODModel.Sections.Num();
BaseTriangleIndices.AddDefaulted(SectionCount);
for (int32 SectionIndex = 0; SectionIndex < SectionCount; ++SectionIndex)
{
const FSkelMeshSection& Section = BaseLODModel.Sections[SectionIndex];
uint32 TriangleCount = Section.NumTriangles;
for (uint32 TriangleIndex = 0; TriangleIndex < TriangleCount; ++TriangleIndex)
{
for (uint32 PointIndex = 0; PointIndex < 3; PointIndex++)
{
uint32 IndexBufferValue = BaseLODModel.IndexBuffer[Section.BaseIndex + ((TriangleIndex * 3) + PointIndex)];
BaseTriangleIndices[SectionIndex].Add(IndexBufferValue);
}
}
}
//Every target vertices match a Base LOD triangle, we also want the barycentric weight of the triangle match. All this done using the UVs
TArray<FTargetMatch> TargetMatchData;
TargetMatchData.AddDefaulted(TargetVertices.Num());
//Match all target vertices to a Base triangle Using UVs.
ProjectTargetOnBase(BaseVertices, BaseTriangleIndices, TargetMatchData, TargetLODModel.Sections, TargetSectionMatchBaseIndex, *SkeletalMesh->GetName());
//Helper to retrieve the FMorphTargetDelta from the BaseIndex
TMap<UMorphTarget *, TMap<uint32, uint32>> PerMorphTargetBaseIndexToMorphTargetDelta;
//Create a map from BaseIndex to a list of match target index for all base morph target point
TMap<uint32, TArray<uint32>> BaseMorphIndexToTargetIndexList;
for (UMorphTarget *MorphTarget : SkeletalMesh->GetMorphTargets())
{
if (!MorphTarget->HasDataForLOD(SourceLOD))
{
continue;
}
bool bUseTempMorphDelta = SourceLOD == DestinationLOD && bReduceBaseLOD && InlineReductionDataParameter.InlineOriginalSrcMorphTargetData.Contains(MorphTarget->GetFullName());
const TArray<FMorphTargetDelta> *TempMorphDeltas = bUseTempMorphDelta ? InlineReductionDataParameter.InlineOriginalSrcMorphTargetData.Find(MorphTarget->GetFullName()) : nullptr;
if (TempMorphDeltas == nullptr || TempMorphDeltas->Num() <= 0)
{
bUseTempMorphDelta = false;
}
TMap<uint32, uint32>& BaseIndexToMorphTargetDelta = PerMorphTargetBaseIndexToMorphTargetDelta.FindOrAdd(MorphTarget);
const FMorphTargetLODModel& BaseMorphModel = MorphTarget->GetMorphLODModels()[SourceLOD];
const TArray<FMorphTargetDelta>& Vertices = bUseTempMorphDelta ? *TempMorphDeltas : BaseMorphModel.Vertices;
for (uint32 MorphDeltaIndex = 0; MorphDeltaIndex < (uint32)(Vertices.Num()); ++MorphDeltaIndex)
{
const FMorphTargetDelta& MorphDelta = Vertices[MorphDeltaIndex];
BaseIndexToMorphTargetDelta.Add(MorphDelta.SourceIdx, MorphDeltaIndex);
//Iterate the targetmatch data so we can store which target indexes is impacted by this morph delta.
for (int32 TargetIndex = 0; TargetIndex < TargetMatchData.Num(); ++TargetIndex)
{
const FTargetMatch& TargetMatch = TargetMatchData[TargetIndex];
if (TargetMatch.Indices[0] == INDEX_NONE)
{
//In case this vertex did not found a triangle match
continue;
}
if (TargetMatch.Indices[0] == MorphDelta.SourceIdx || TargetMatch.Indices[1] == MorphDelta.SourceIdx || TargetMatch.Indices[2] == MorphDelta.SourceIdx)
{
TArray<uint32>& TargetIndexes = BaseMorphIndexToTargetIndexList.FindOrAdd(MorphDelta.SourceIdx);
TargetIndexes.AddUnique(TargetIndex);
}
}
}
}
//Create the target morph target
CreateLODMorphTarget(SkeletalMesh, InlineReductionDataParameter, SourceLOD, DestinationLOD, PerMorphTargetBaseIndexToMorphTargetDelta, BaseMorphIndexToTargetIndexList, TargetVertices, TargetMatchData);
}
void FLODUtilities::SimplifySkeletalMeshLOD(
USkeletalMesh* SkeletalMesh,
int32 DesiredLOD,
const ITargetPlatform* TargetPlatform,
bool bRestoreClothing,
TArray<FMorphTargetUpdateRequest>& OutMorphRequests,
std::atomic<bool>* OutNeedsPackageDirtied
)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FLODUtilities::SimplifySkeletalMeshLOD);
IMeshReductionModule& ReductionModule = FModuleManager::Get().LoadModuleChecked<IMeshReductionModule>("MeshReductionInterface");
IMeshReduction* MeshReduction = ReductionModule.GetSkeletalMeshReductionInterface();
if (!MeshReduction)
{
UE_ASSET_LOG(LogLODUtilities, Warning, SkeletalMesh, TEXT("Cannot reduce skeletalmesh LOD because there is no active reduction plugin."));
return;
}
check (MeshReduction->IsSupported());
if (DesiredLOD == 0
&& SkeletalMesh->GetLODInfo(DesiredLOD) != nullptr
&& SkeletalMesh->GetLODInfo(DesiredLOD)->bHasBeenSimplified
&& !SkeletalMesh->GetImportedModel()->InlineReductionCacheDatas.IsValidIndex(0))
{
//The base LOD was reduce and there is no valid data, we cannot regenerate this lod it must be re-import before
FFormatNamedArguments Args;
Args.Add(TEXT("SkeletalMeshName"), FText::FromString(SkeletalMesh->GetName()));
Args.Add(TEXT("LODIndex"), FText::AsNumber(DesiredLOD));
FText Message = FText::Format(NSLOCTEXT("UnrealEd", "MeshSimp_GenerateLODCannotGenerateMissingData", "Cannot generate LOD {LODIndex} for skeletal mesh '{SkeletalMeshName}'. This LOD must be re-import to create the necessary data"), Args);
if (FApp::IsUnattended() || !IsInGameThread())
{
UE_LOG(LogLODUtilities, Warning, TEXT("%s"), *(Message.ToString()));
}
else
{
FMessageDialog::Open(EAppMsgType::Ok, Message);
}
return;
}
if (IsInGameThread())
{
FFormatNamedArguments Args;
Args.Add(TEXT("DesiredLOD"), DesiredLOD);
Args.Add(TEXT("SkeletalMeshName"), FText::FromString(SkeletalMesh->GetName()));
const FText StatusUpdate = FText::Format(NSLOCTEXT("UnrealEd", "MeshSimp_GeneratingLOD_F", "Generating LOD{DesiredLOD} for {SkeletalMeshName}..."), Args);
GWarn->BeginSlowTask(StatusUpdate, true);
}
FScopedSkeletalMeshPostEditChange ScopedPostEditChange(SkeletalMesh);
// Unbind DesiredLOD existing clothing assets before we simplify this LOD
TArray<ClothingAssetUtils::FClothingAssetMeshBinding> ClothingBindings;
if (bRestoreClothing && SkeletalMesh->GetImportedModel() && SkeletalMesh->GetImportedModel()->LODModels.IsValidIndex(DesiredLOD))
{
FLODUtilities::UnbindClothingAndBackup(SkeletalMesh, ClothingBindings, DesiredLOD);
}
FInlineReductionDataParameter InlineReductionDataParameter;
if (SkeletalMesh->GetLODInfo(DesiredLOD) != nullptr)
{
FSkeletalMeshModel* SkeletalMeshResource = SkeletalMesh->GetImportedModel();
FSkeletalMeshOptimizationSettings& Settings = SkeletalMesh->GetLODInfo(DesiredLOD)->ReductionSettings;
//We must save the original reduction data, special case when we reduce inline we save even if its already simplified
if (SkeletalMeshResource->LODModels.IsValidIndex(DesiredLOD) && (!SkeletalMesh->GetLODInfo(DesiredLOD)->bHasBeenSimplified || DesiredLOD == Settings.BaseLOD))
{
FSkeletalMeshLODModel& SrcModel = SkeletalMeshResource->LODModels[DesiredLOD];
if (!SkeletalMeshResource->InlineReductionCacheDatas.IsValidIndex(DesiredLOD))
{
//We should not do that in a worker thread, the serialization of the SkeletalMeshResource is suppose to allocate the correct number of inline data caches
//If the user add LOD in person editor, the simplification will be call in the game thread, see FLODUtilities::RegenerateLOD
if (!ensure(IsInGameThread()))
{
UE_ASSET_LOG(LogLODUtilities, Error, SkeletalMesh, TEXT("FLODUtilities::SimplifySkeletalMeshLOD: InlineReductionCacheDatas was not added in the game thread."));
}
SkeletalMeshResource->InlineReductionCacheDatas.AddDefaulted((DesiredLOD + 1) - SkeletalMeshResource->InlineReductionCacheDatas.Num());
}
check(SkeletalMeshResource->InlineReductionCacheDatas.IsValidIndex(DesiredLOD));
SkeletalMeshResource->InlineReductionCacheDatas[DesiredLOD].SetCacheGeometryInfo(SrcModel);
InlineReductionDataParameter.InlineOriginalSrcMorphTargetData.Empty(SkeletalMesh->GetMorphTargets().Num());
for (UMorphTarget* MorphTarget : SkeletalMesh->GetMorphTargets())
{
if (!MorphTarget->HasDataForLOD(DesiredLOD))
{
continue;
}
TArray<FMorphTargetDelta>& MorphDeltasArray = InlineReductionDataParameter.InlineOriginalSrcMorphTargetData.FindOrAdd(MorphTarget->GetFullName());
const FMorphTargetLODModel& BaseMorphModel = MorphTarget->GetMorphLODModels()[DesiredLOD];
//Iterate each original morph target source index to fill the NewMorphTargetDeltas array with the TargetMatchData.
int32 NumDeltas = 0;
const FMorphTargetDelta* BaseDeltaArray = MorphTarget->GetMorphTargetDelta(DesiredLOD, NumDeltas);
for (int32 DeltaIndex = 0; DeltaIndex < NumDeltas; DeltaIndex++)
{
MorphDeltasArray.Add(BaseDeltaArray[DeltaIndex]);
}
}
// Copy the original SkeletalMesh LODModel
// Unbind clothing before saving the original data, we must not restore clothing to do inline reduction
{
TArray<ClothingAssetUtils::FClothingAssetMeshBinding> TemporaryRemoveClothingBindings;
FLODUtilities::UnbindClothingAndBackup(SkeletalMesh, TemporaryRemoveClothingBindings, DesiredLOD);
FSkeletalMeshLODModel::CopyStructure(&InlineReductionDataParameter.InlineOriginalSrcModel, &SrcModel);
if (TemporaryRemoveClothingBindings.Num() > 0)
{
FLODUtilities::RestoreClothingFromBackup(SkeletalMesh, TemporaryRemoveClothingBindings, DesiredLOD);
}
}
InlineReductionDataParameter.bIsDataValid = true;
if (DesiredLOD == 0)
{
SkeletalMesh->GetLODInfo(DesiredLOD)->SourceImportFilename = SkeletalMesh->GetAssetImportData()->GetFirstFilename();
}
}
}
if (MeshReduction->ReduceSkeletalMesh(SkeletalMesh, DesiredLOD, TargetPlatform))
{
check(SkeletalMesh->GetLODNum() >= 1);
//Manage morph target after the reduction. either apply to the reduce LOD or clear them all
{
FSkeletalMeshOptimizationSettings& ReductionSettings = SkeletalMesh->GetLODInfo(DesiredLOD)->ReductionSettings;
//Apply morph to the new LOD. Force it if we reduce the base LOD, base LOD must apply the morph target
if (ReductionSettings.bRemapMorphTargets)
{
OutMorphRequests.Emplace(SkeletalMesh, ReductionSettings.BaseLOD, DesiredLOD, MoveTemp(InlineReductionDataParameter));
}
else
{
OutMorphRequests.Emplace(SkeletalMesh, DesiredLOD);
}
// Update the vertex attribute information in the LOD info.
UpdateLODInfoVertexAttributes(SkeletalMesh, ReductionSettings.BaseLOD, DesiredLOD, false);
}
if (IsInGameThread())
{
(void)SkeletalMesh->MarkPackageDirty();
}
else if(OutNeedsPackageDirtied)
{
(*OutNeedsPackageDirtied) = true;
}
}
else
{
// Simplification failed! Warn the user.
FFormatNamedArguments Args;
Args.Add(TEXT("SkeletalMeshName"), FText::FromString(SkeletalMesh->GetName()));
const FText Message = FText::Format(NSLOCTEXT("UnrealEd", "MeshSimp_GenerateLODFailed_F", "An error occurred while simplifying the geometry for mesh '{SkeletalMeshName}'. Consider adjusting simplification parameters and re-simplifying the mesh."), Args);
if (FApp::IsUnattended() || !IsInGameThread())
{
UE_LOG(LogLODUtilities, Warning, TEXT("%s"), *(Message.ToString()));
}
else
{
FMessageDialog::Open(EAppMsgType::Ok, Message);
}
}
//Put back the clothing for the DesiredLOD
if (bRestoreClothing && ClothingBindings.Num() > 0 && SkeletalMesh->GetImportedModel()->LODModels.IsValidIndex(DesiredLOD))
{
FLODUtilities::RestoreClothingFromBackup(SkeletalMesh, ClothingBindings, DesiredLOD);
}
if (IsInGameThread())
{
GWarn->EndSlowTask();
}
}
void FLODUtilities::SimplifySkeletalMeshLOD(FSkeletalMeshUpdateContext& UpdateContext, int32 DesiredLOD, const ITargetPlatform* TargetPlatform, bool bRestoreClothing /*= false*/, std::atomic<bool>* OutNeedsPackageDirtied/*= nullptr*/)
{
USkeletalMesh* SkeletalMesh = UpdateContext.SkeletalMesh;
IMeshReductionModule& ReductionModule = FModuleManager::Get().LoadModuleChecked<IMeshReductionModule>("MeshReductionInterface");
IMeshReduction* MeshReduction = ReductionModule.GetSkeletalMeshReductionInterface();
if (MeshReduction && MeshReduction->IsSupported() && SkeletalMesh)
{
TArray<FMorphTargetUpdateRequest> MorphTargetUpdateRequests;
SimplifySkeletalMeshLOD(SkeletalMesh, DesiredLOD, TargetPlatform, bRestoreClothing, MorphTargetUpdateRequests, OutNeedsPackageDirtied);
for (FMorphTargetUpdateRequest& Request: MorphTargetUpdateRequests)
{
Request.Run();
}
if (UpdateContext.OnLODChanged.IsBound())
{
//Notify calling system of change
UpdateContext.OnLODChanged.ExecuteIfBound();
}
}
}
bool FLODUtilities::RestoreSkeletalMeshLODImportedData_DEPRECATED(USkeletalMesh* SkeletalMesh, int32 LodIndex)
{
const bool bThisFunctionIsDeprecated = true;
ensure(!bThisFunctionIsDeprecated);
UE_ASSET_LOG(LogLODUtilities, Error, SkeletalMesh, TEXT("FLODUtilities::RestoreSkeletalMeshLODImportedData_DEPRECATED: This function is deprecated."));
return false;
}
void FLODUtilities::RefreshLODChange(const USkeletalMesh* SkeletalMesh)
{
for (FThreadSafeObjectIterator Iter(USkeletalMeshComponent::StaticClass()); Iter; ++Iter)
{
USkeletalMeshComponent* SkeletalMeshComponent = Cast<USkeletalMeshComponent>(*Iter);
if (SkeletalMeshComponent->GetSkeletalMeshAsset() == SkeletalMesh)
{
// it needs to recreate IF it already has been created
if (SkeletalMeshComponent->IsRegistered())
{
SkeletalMeshComponent->UpdateLODStatus();
SkeletalMeshComponent->MarkRenderStateDirty();
}
}
}
}
bool ValidateAlternateSkeleton(const FSkeletalMeshImportData& ImportDataSrc, const FSkeletalMeshImportData& ImportDataDest, const FString& SkeletalMeshDestName, const int32 LODIndexDest)
{
bool bIsunattended = GIsRunningUnattendedScript || FApp::IsUnattended();
int32 BoneNumberDest = ImportDataDest.RefBonesBinary.Num();
int32 BoneNumberSrc = ImportDataSrc.RefBonesBinary.Num();
//We also want to report any missing bone, because skinning quality will be impacted if bones are missing
TArray<FString> DestBonesNotUsedBySrc;
TArray<FString> SrcBonesNotUsedByDest;
for (int32 BoneIndexSrc = 0; BoneIndexSrc < BoneNumberSrc; ++BoneIndexSrc)
{
FString BoneNameSrc = ImportDataSrc.RefBonesBinary[BoneIndexSrc].Name;
bool bFoundMatch = false;
for (int32 BoneIndexDest = 0; BoneIndexDest < BoneNumberDest; ++BoneIndexDest)
{
if (ImportDataDest.RefBonesBinary[BoneIndexDest].Name.Equals(BoneNameSrc))
{
bFoundMatch = true;
break;
}
}
if (!bFoundMatch)
{
SrcBonesNotUsedByDest.Add(BoneNameSrc);
}
}
for (int32 BoneIndexDest = 0; BoneIndexDest < BoneNumberDest; ++BoneIndexDest)
{
FString BoneNameDest = ImportDataDest.RefBonesBinary[BoneIndexDest].Name;
bool bFound = false;
for (int32 BoneIndexSrc = 0; BoneIndexSrc < BoneNumberSrc; ++BoneIndexSrc)
{
FString BoneNameSrc = ImportDataSrc.RefBonesBinary[BoneIndexSrc].Name;
if (BoneNameDest.Equals(BoneNameSrc))
{
bFound = true;
break;
}
}
if (!bFound)
{
DestBonesNotUsedBySrc.Add(BoneNameDest);
}
}
if (SrcBonesNotUsedByDest.Num() > 0)
{
//Let the user know
if (!bIsunattended)
{
FString BoneList;
for (FString& BoneName : SrcBonesNotUsedByDest)
{
BoneList += BoneName;
BoneList += TEXT("\n");
}
FFormatNamedArguments Args;
Args.Add(TEXT("SkeletalMeshName"), FText::FromString(SkeletalMeshDestName));
Args.Add(TEXT("LODIndex"), FText::AsNumber(LODIndexDest));
Args.Add(TEXT("BoneList"), FText::FromString(BoneList));
FText Message = FText::Format(NSLOCTEXT("UnrealEd", "AlternateSkinningImport_SourceBoneNotUseByDestination", "Not all the alternate mesh bones are used by the LOD {LODIndex} when importing alternate weights for skeletal mesh '{SkeletalMeshName}'.\nBones List:\n{BoneList}"), Args);
if(FMessageDialog::Open(EAppMsgType::OkCancel, Message) == EAppReturnType::Cancel)
{
return false;
}
}
else
{
UE_LOG(LogLODUtilities, Error, TEXT("Alternate skinning import: Not all the alternate mesh bones are used by the mesh."));
return false;
}
}
else if (DestBonesNotUsedBySrc.Num() > 0) //Do a else here since the DestBonesNotUsedBySrc is less prone to give a bad alternate influence result.
{
//Let the user know
if (!bIsunattended)
{
FString BoneList;
for (FString& BoneName : DestBonesNotUsedBySrc)
{
BoneList += BoneName;
BoneList += TEXT("\n");
}
FFormatNamedArguments Args;
Args.Add(TEXT("SkeletalMeshName"), FText::FromString(SkeletalMeshDestName));
Args.Add(TEXT("LODIndex"), FText::AsNumber(LODIndexDest));
Args.Add(TEXT("BoneList"), FText::FromString(BoneList));
FText Message = FText::Format(NSLOCTEXT("UnrealEd", "AlternateSkinningImport_DestinationBoneNotUseBySource", "Not all the LOD {LODIndex} bones are used by the alternate mesh when importing alternate weights for skeletal mesh '{SkeletalMeshName}'.\nBones List:\n{BoneList}"), Args);
if (FMessageDialog::Open(EAppMsgType::OkCancel, Message) == EAppReturnType::Cancel)
{
return false;
}
}
else
{
UE_LOG(LogLODUtilities, Display, TEXT("Alternate skinning import: Not all the mesh bones are used by the alternate mesh."));
return false;
}
}
return true;
}
/*
* The remap use the name to find the corresponding bone index between the source and destination skeleton
*/
void FillRemapBoneIndexSrcToDest(const FSkeletalMeshImportData& ImportDataSrc, const FSkeletalMeshImportData& ImportDataDest, TMap<int32, int32>& RemapBoneIndexSrcToDest)
{
RemapBoneIndexSrcToDest.Empty(ImportDataSrc.RefBonesBinary.Num());
int32 BoneNumberDest = ImportDataDest.RefBonesBinary.Num();
int32 BoneNumberSrc = ImportDataSrc.RefBonesBinary.Num();
for (int32 BoneIndexSrc = 0; BoneIndexSrc < BoneNumberSrc; ++BoneIndexSrc)
{
FString BoneNameSrc = ImportDataSrc.RefBonesBinary[BoneIndexSrc].Name;
for (int32 BoneIndexDest = 0; BoneIndexDest < BoneNumberDest; ++BoneIndexDest)
{
if (ImportDataDest.RefBonesBinary[BoneIndexDest].Name.Equals(BoneNameSrc))
{
RemapBoneIndexSrcToDest.Add(BoneIndexSrc, BoneIndexDest);
break;
}
}
if (!RemapBoneIndexSrcToDest.Contains(BoneIndexSrc))
{
RemapBoneIndexSrcToDest.Add(BoneIndexSrc, INDEX_NONE);
}
}
}
namespace VertexMatchNameSpace
{
struct FVertexMatchResult
{
TArray<uint32> VertexIndexes;
TArray<float> Ratios;
};
}
struct FTriangleOctreeSemantics
{
// When a leaf gets more than this number of elements, it will split itself into a node with multiple child leaves
enum { MaxElementsPerLeaf = 10 };
// This is used for incremental updates. When removing a polygon, larger values will cause leaves to be removed and collapsed into a parent node.
enum { MinInclusiveElementsPerNode = 5 };
// How deep the tree can go.
enum { MaxNodeDepth = 20 };
typedef TInlineAllocator<MaxElementsPerLeaf> ElementAllocator;
FORCEINLINE static FBoxCenterAndExtent GetBoundingBox(const FTriangleElement& Element)
{
return Element.PositionBound;
}
FORCEINLINE static bool AreElementsEqual(const FTriangleElement& A, const FTriangleElement& B)
{
return (A.TriangleIndex == B.TriangleIndex);
}
FORCEINLINE static void SetElementId(const FTriangleElement& Element, FOctreeElementId2 OctreeElementID)
{
}
};
typedef TOctree2<FTriangleElement, FTriangleOctreeSemantics> TTriangleElementOctree;
void MatchVertexIndexUsingPosition(
const FSkeletalMeshImportData& ImportDataDest
, const FSkeletalMeshImportData& ImportDataSrc
, TSortedMap<uint32, VertexMatchNameSpace::FVertexMatchResult>& VertexIndexSrcToVertexIndexDestMatches
, const TArray<uint32>& VertexIndexToMatchWithPositions
, bool& bNoMatchMsgDone
, const TCHAR* DebugContext)
{
if (VertexIndexToMatchWithPositions.Num() <= 0)
{
return;
}
int32 FaceNumberDest = ImportDataDest.Faces.Num();
//Setup the Position Octree with the destination faces so we can match the source vertex index
TArray<FTriangleElement> TrianglesDest;
FBox BaseMeshPositionBound(EForceInit::ForceInit);
for (int32 FaceIndexDest = 0; FaceIndexDest < FaceNumberDest; ++FaceIndexDest)
{
const SkeletalMeshImportData::FTriangle& Triangle = ImportDataDest.Faces[FaceIndexDest];
FTriangleElement TriangleElement;
TriangleElement.UVsBound.Init();
FBox TrianglePositionBound;
TrianglePositionBound.Init();
for (int32 Corner = 0; Corner < 3; ++Corner)
{
const uint32 WedgeIndexDest = Triangle.WedgeIndex[Corner];
const uint32 VertexIndexDest = ImportDataDest.Wedges[WedgeIndexDest].VertexIndex;
TriangleElement.Indexes.Add(WedgeIndexDest);
FSoftSkinVertex SoftSkinVertex;
SoftSkinVertex.Position = ImportDataDest.Points[VertexIndexDest];
SoftSkinVertex.UVs[0] = ImportDataDest.Wedges[WedgeIndexDest].UVs[0];
TriangleElement.Vertices.Add(SoftSkinVertex);
TriangleElement.UVsBound += FVector2D(SoftSkinVertex.UVs[0]);
TrianglePositionBound += (FVector)SoftSkinVertex.Position;
BaseMeshPositionBound += (FVector)SoftSkinVertex.Position;
}
BaseMeshPositionBound += TrianglePositionBound;
TriangleElement.PositionBound = FBoxCenterAndExtent(TrianglePositionBound);
TriangleElement.TriangleIndex = FaceIndexDest;
TrianglesDest.Add(TriangleElement);
}
TTriangleElementOctree OcTree(BaseMeshPositionBound.GetCenter(), BaseMeshPositionBound.GetExtent().Size());
for (FTriangleElement& TriangleElement : TrianglesDest)
{
OcTree.AddElement(TriangleElement);
}
//Retrieve all triangles that are close to our point, start at 0.25% of OcTree extend
double DistanceThreshold = BaseMeshPositionBound.GetExtent().Size()*0.0025;
//Find a match triangle for every target vertices
TArray<FTriangleElement> OcTreeTriangleResults;
OcTreeTriangleResults.Reserve(TrianglesDest.Num() / 50); //Reserve 2% to speed up the query
//This lambda store a source vertex index -> source wedge index destination triangle.
//It use a barycentric function to determine the impact on the 3 corner of the triangle.
auto AddMatchTriangle = [&ImportDataDest, &TrianglesDest, &VertexIndexSrcToVertexIndexDestMatches, DebugContext](const FTriangleElement& BestTriangle, const FVector3f& Position, const uint32 VertexIndexSrc)
{
//Found the surface area of the 3 barycentric triangles from the UVs
FVector3f BarycentricWeight;
BarycentricWeight = GetBaryCentric(Position, BestTriangle.Vertices[0].Position, BestTriangle.Vertices[1].Position, BestTriangle.Vertices[2].Position, DebugContext);
//Fill the match
VertexMatchNameSpace::FVertexMatchResult& VertexMatchDest = VertexIndexSrcToVertexIndexDestMatches.FindOrAdd(VertexIndexSrc);
for (int32 CornerIndex = 0; CornerIndex < 3; ++CornerIndex)
{
int32 VertexIndexDest = ImportDataDest.Wedges[BestTriangle.Indexes[CornerIndex]].VertexIndex;
float Ratio = BarycentricWeight[CornerIndex];
int32 FindIndex = INDEX_NONE;
if (!VertexMatchDest.VertexIndexes.Find(VertexIndexDest, FindIndex))
{
VertexMatchDest.VertexIndexes.Add(VertexIndexDest);
VertexMatchDest.Ratios.Add(Ratio);
}
else
{
check(VertexMatchDest.Ratios.IsValidIndex(FindIndex));
VertexMatchDest.Ratios[FindIndex] = FMath::Max(VertexMatchDest.Ratios[FindIndex], Ratio);
}
}
};
for (int32 VertexIndexSrc : VertexIndexToMatchWithPositions)
{
FVector3f PositionSrc = ImportDataSrc.Points[VertexIndexSrc];
OcTreeTriangleResults.Reset();
//Use the OcTree to find closest triangle
FVector Extent(DistanceThreshold, DistanceThreshold, DistanceThreshold);
FBoxCenterAndExtent CurBox((FVector)PositionSrc, Extent);
while (OcTreeTriangleResults.Num() <= 0)
{
OcTree.FindElementsWithBoundsTest(CurBox, [&OcTreeTriangleResults](const FTriangleElement& Element)
{
// Add all of the elements in the current node to the list of points to consider for closest point calculations
OcTreeTriangleResults.Add(Element);
});
//Increase the extend so we try to found in a larger area
Extent *= 2;
if (Extent.SizeSquared() >= BaseMeshPositionBound.GetSize().SizeSquared())
{
//Extend must not be bigger then the whole mesh, its acceptable to have error at this point
break;
}
CurBox = FBox((FVector)PositionSrc - Extent, (FVector)PositionSrc + Extent);
}
//Get the 3D distance between a point and a destination triangle
auto GetDistanceSrcPointToDestTriangle = [&TrianglesDest, &PositionSrc](const uint32 DestTriangleIndex)->double
{
FTriangleElement& CandidateTriangle = TrianglesDest[DestTriangleIndex];
return FVector::DistSquared(FMath::ClosestPointOnTriangleToPoint((FVector)PositionSrc, (FVector)CandidateTriangle.Vertices[0].Position, (FVector)CandidateTriangle.Vertices[1].Position, (FVector)CandidateTriangle.Vertices[2].Position), (FVector)PositionSrc);
};
//Brute force finding of closest triangle using 3D position
auto FailSafeUnmatchVertex = [&GetDistanceSrcPointToDestTriangle, &OcTreeTriangleResults](uint32 &OutIndexMatch)->bool
{
bool bFoundMatch = false;
double ClosestTriangleDistSquared = std::numeric_limits<double>::max();
for (const FTriangleElement& MatchTriangle : OcTreeTriangleResults)
{
const int32 MatchTriangleIndex = MatchTriangle.TriangleIndex;
const double TriangleDistSquared = GetDistanceSrcPointToDestTriangle(MatchTriangleIndex);
if (TriangleDistSquared < ClosestTriangleDistSquared)
{
ClosestTriangleDistSquared = TriangleDistSquared;
OutIndexMatch = MatchTriangleIndex;
bFoundMatch = true;
}
}
return bFoundMatch;
};
//Find all Triangles that contain the Target UV
if (OcTreeTriangleResults.Num() > 0)
{
TArray<uint32> MatchTriangleIndexes;
uint32 FoundIndexMatch = INDEX_NONE;
if (!FindTrianglePositionMatch((FVector)PositionSrc, TrianglesDest, OcTreeTriangleResults, MatchTriangleIndexes))
{
//There is no Position match possible, use brute force fail safe
if (!FailSafeUnmatchVertex(FoundIndexMatch))
{
//We should always have a match
if (!bNoMatchMsgDone)
{
UE_LOG(LogLODUtilities, Warning, TEXT("Alternate skinning import: Cannot find a triangle from the destination LOD that contain a vertex UV in the imported alternate skinning LOD mesh. Alternate skinning quality will be lower."));
bNoMatchMsgDone = true;
}
continue;
}
}
double ClosestTriangleDistSquared = std::numeric_limits<double>::max();
if (MatchTriangleIndexes.Num() == 1)
{
//One match, this mean no mirror UVs simply take the single match
FoundIndexMatch = MatchTriangleIndexes[0];
ClosestTriangleDistSquared = GetDistanceSrcPointToDestTriangle(FoundIndexMatch);
}
else
{
//Geometry can use mirror so the UVs are not unique. Use the closest match triangle to the point to find the best match
for (uint32 MatchTriangleIndex : MatchTriangleIndexes)
{
double TriangleDistSquared = GetDistanceSrcPointToDestTriangle(MatchTriangleIndex);
if (TriangleDistSquared < ClosestTriangleDistSquared)
{
ClosestTriangleDistSquared = TriangleDistSquared;
FoundIndexMatch = MatchTriangleIndex;
}
}
}
//FAIL SAFE, make sure we have a match that make sense
//Use the mesh geometry bound extent (1% of it) to validate we are close enough.
if (ClosestTriangleDistSquared > BaseMeshPositionBound.GetExtent().SizeSquared()*0.01f)
{
//Executing fail safe, if the UVs are too much off because of the reduction, use the closest distance to polygons to find the match
//This path is not optimize and should not happen often.
FailSafeUnmatchVertex(FoundIndexMatch);
}
//We should always have a valid match at this point
check(TrianglesDest.IsValidIndex(FoundIndexMatch));
AddMatchTriangle(TrianglesDest[FoundIndexMatch], PositionSrc, VertexIndexSrc);
}
else
{
if (!bNoMatchMsgDone)
{
UE_LOG(LogLODUtilities, Warning, TEXT("Alternate skinning import: Cannot find a triangle from the destination LOD that contain a vertex UV in the imported alternate skinning LOD mesh. Alternate skinning quality will be lower."));
bNoMatchMsgDone = true;
}
}
}
}
bool FLODUtilities::UpdateAlternateSkinWeights(USkeletalMesh* SkeletalMeshDest, const FName& ProfileNameDest, int32 LODIndexDest, const IMeshUtilities::MeshBuildOptions& Options)
{
//Grab all the destination structure
check(SkeletalMeshDest);
check(SkeletalMeshDest->GetImportedModel());
check(SkeletalMeshDest->GetImportedModel()->LODModels.IsValidIndex(LODIndexDest));
FSkeletalMeshLODModel& LODModelDest = SkeletalMeshDest->GetImportedModel()->LODModels[LODIndexDest];
if (!SkeletalMeshDest->HasMeshDescription(LODIndexDest))
{
UE_LOG(LogLODUtilities, Error, TEXT("Failed to import Skin Weight Profile as the target skeletal mesh (%s) requires reimporting first."), *SkeletalMeshDest->GetName());
//Very old asset will not have this data, we cannot add alternate until the asset is reimported
return false;
}
FSkeletalMeshImportData ImportDataDest;
PRAGMA_DISABLE_DEPRECATION_WARNINGS
SkeletalMeshDest->LoadLODImportedData(LODIndexDest, ImportDataDest);
PRAGMA_ENABLE_DEPRECATION_WARNINGS
return UpdateAlternateSkinWeights(
LODModelDest,
ImportDataDest,
SkeletalMeshDest,
SkeletalMeshDest->GetRefSkeleton(),
ProfileNameDest,
LODIndexDest,
Options);
}
bool FLODUtilities::UpdateAlternateSkinWeights(
USkeletalMesh* SkeletalMeshDest,
const FName& ProfileNameDest,
int32 LODIndexDest,
FOverlappingThresholds OverlappingThresholds,
bool ShouldImportNormals,
bool ShouldImportTangents,
bool bUseMikkTSpace,
bool bComputeWeightedNormals)
{
IMeshUtilities::MeshBuildOptions Options;
Options.OverlappingThresholds = OverlappingThresholds;
Options.bComputeNormals = !ShouldImportNormals;
Options.bComputeTangents = !ShouldImportTangents;
Options.bUseMikkTSpace = bUseMikkTSpace;
Options.bComputeWeightedNormals = bComputeWeightedNormals;
return UpdateAlternateSkinWeights(SkeletalMeshDest, ProfileNameDest, LODIndexDest, Options);
}
static bool IsSameImportMeshStructure(const FSkeletalMeshImportData& MeshA, const FSkeletalMeshImportData& MeshB)
{
// This equivalency testing above will suffice and is only for sanity, since both of the import data objects
// passed in should have been generated from the same FMeshDescription (see FSkeletalMeshImportData::GetMeshDescription)
return MeshA.Points.Num() == MeshB.Points.Num() &&
MeshA.Wedges.Num() == MeshB.Wedges.Num() &&
MeshA.Faces.Num() == MeshB.Faces.Num() &&
MeshA.RefBonesBinary.Num() == MeshB.RefBonesBinary.Num();
}
bool FLODUtilities::UpdateAlternateSkinWeights(
FSkeletalMeshLODModel& LODModelDest,
FSkeletalMeshImportData& ImportDataDest,
USkeletalMesh* SkeletalMeshDest,
const FReferenceSkeleton& RefSkeleton,
const FName& ProfileNameDest,
int32 LODIndexDest,
const IMeshUtilities::MeshBuildOptions& Options)
{
int32 ProfileIndex = 0;
if (!ImportDataDest.AlternateInfluenceProfileNames.Find(ProfileNameDest.ToString(), ProfileIndex))
{
UE_LOGFMT(LogLODUtilities, Display, "Can't update alternative profile {ProfileName} on {SkeletalMesh} at LOD {0}. Profile doesn't exist.",
ProfileNameDest, SkeletalMeshDest->GetName(), LODIndexDest);
return false;
}
check(ImportDataDest.AlternateInfluences.IsValidIndex(ProfileIndex));
//The data must be there and must be verified before getting here
const FSkeletalMeshImportData& ImportDataSrc = ImportDataDest.AlternateInfluences[ProfileIndex];
// Double-check the mesh has the same structure.
if (!IsSameImportMeshStructure(ImportDataSrc, ImportDataDest))
{
UE_LOGFMT(LogLODUtilities, Warning, "Can't update alternative profile {ProfileName} on {SkeletalMesh} at LOD {0}. Profile source mesh and the base mesh are different.",
ProfileNameDest, SkeletalMeshDest->GetName(), LODIndexDest);
return false;
}
//Sort and normalize weights for alternate influences
TArray<SkeletalMeshImportData::FRawBoneInfluence> AlternateInfluences{ImportDataSrc.Influences};
ProcessImportMeshInfluences(ImportDataDest.Wedges.Num(), AlternateInfluences, SkeletalMeshDest->GetPathName());
//Store the remapped influence into the profile, the function SkeletalMeshTools::ChunkSkinnedVertices will use all profiles including this one to chunk the sections
FImportedSkinWeightProfileData& ImportedProfileData = LODModelDest.SkinWeightProfiles.Add(ProfileNameDest);
ImportedProfileData.SourceModelInfluences.Empty(AlternateInfluences.Num());
for (int32 InfluenceIndex = 0; InfluenceIndex < AlternateInfluences.Num(); ++InfluenceIndex)
{
const SkeletalMeshImportData::FRawBoneInfluence& RawInfluence = AlternateInfluences[InfluenceIndex];
SkeletalMeshImportData::FVertInfluence LODAlternateInfluence;
LODAlternateInfluence.BoneIndex = static_cast<FBoneIndexType>(RawInfluence.BoneIndex);
LODAlternateInfluence.VertIndex = RawInfluence.VertexIndex;
LODAlternateInfluence.Weight = RawInfluence.Weight;
ImportedProfileData.SourceModelInfluences.Add(LODAlternateInfluence);
}
//
//////////////////////////////////////////////////////////////////////////
//Prepare the build data to rebuild the asset with the alternate influences
//The chunking can be different when we have alternate influences
//Grab the build data from ImportDataDest
TArray<FVector3f> LODPointsDest;
TArray<SkeletalMeshImportData::FMeshWedge> LODWedgesDest;
TArray<SkeletalMeshImportData::FMeshFace> LODFacesDest;
TArray<SkeletalMeshImportData::FVertInfluence> LODInfluencesDest;
TArray<int32> LODPointToRawMapDest;
ImportDataDest.CopyLODImportData(LODPointsDest, LODWedgesDest, LODFacesDest, LODInfluencesDest, LODPointToRawMapDest);
//Set the options with the current asset build options
IMeshUtilities::MeshBuildOptions BuildOptions = Options;
BuildOptions.bComputeNormals = Options.bComputeNormals || !ImportDataDest.bHasNormals;
BuildOptions.bComputeTangents = Options.bComputeTangents || !ImportDataDest.bHasTangents;
BuildOptions.bUseMikkTSpace = (Options.bUseMikkTSpace) && (Options.bComputeNormals || Options.bComputeTangents);
//Build the skeletal mesh asset
IMeshUtilities& MeshUtilities = FModuleManager::Get().LoadModuleChecked<IMeshUtilities>("MeshUtilities");
TArray<FText> WarningMessages;
TArray<FName> WarningNames;
//BaseLOD need to make sure the source data fit with the skeletalmesh materials array before using meshutilities.BuildSkeletalMesh
AdjustImportDataFaceMaterialIndex(SkeletalMeshDest->GetMaterials(), ImportDataDest.Materials, LODFacesDest, LODIndexDest);
//Build the destination mesh with the Alternate influences, so the chunking is done properly.
bool bBuildSuccess = MeshUtilities.BuildSkeletalMesh(LODModelDest, SkeletalMeshDest->GetName(), RefSkeleton, LODInfluencesDest, LODWedgesDest, LODFacesDest, LODPointsDest, LODPointToRawMapDest, BuildOptions, &WarningMessages, &WarningNames);
//Re-Apply the user section changes, the UserSectionsData is map to original section and should match the builded LODModel
LODModelDest.SyncronizeUserSectionsDataArray();
RegenerateAllImportSkinWeightProfileData(LODModelDest, Options.BoneInfluenceLimit, Options.TargetPlatform);
return bBuildSuccess;
}
bool FLODUtilities::UpdateAlternateSkinWeights(
FSkeletalMeshLODModel& LODModelDest,
FSkeletalMeshImportData& ImportDataDest,
USkeletalMesh* SkeletalMeshDest,
const FReferenceSkeleton& RefSkeleton,
const FName& ProfileNameDest,
int32 LODIndexDest,
FOverlappingThresholds OverlappingThresholds,
bool ShouldImportNormals,
bool ShouldImportTangents,
bool bUseMikkTSpace,
bool bComputeWeightedNormals)
{
IMeshUtilities::MeshBuildOptions Options;
Options.OverlappingThresholds = OverlappingThresholds;
Options.bComputeNormals = !ShouldImportNormals;
Options.bComputeTangents = !ShouldImportTangents;
Options.bUseMikkTSpace = bUseMikkTSpace;
Options.bComputeWeightedNormals = bComputeWeightedNormals;
return UpdateAlternateSkinWeights(LODModelDest, ImportDataDest, SkeletalMeshDest, RefSkeleton, ProfileNameDest, LODIndexDest, Options);
}
bool FLODUtilities::UpdateAlternateSkinWeights(
USkeletalMesh* SkeletalMeshDest,
const FName& ProfileNameDest,
USkeletalMesh* SkeletalMeshSrc,
int32 LODIndexDest,
int32 LODIndexSrc,
const IMeshUtilities::MeshBuildOptions& Options)
{
//Grab all the destination structure
check(SkeletalMeshDest);
check(SkeletalMeshDest->GetImportedModel());
check(SkeletalMeshDest->GetImportedModel()->LODModels.IsValidIndex(LODIndexDest));
FSkeletalMeshLODModel& LODModelDest = SkeletalMeshDest->GetImportedModel()->LODModels[LODIndexDest];
if (!SkeletalMeshDest->HasMeshDescription(LODIndexDest))
{
UE_LOG(LogLODUtilities, Error, TEXT("Failed to import Skin Weight Profile as the target skeletal mesh (%s) requires reimporting first."), SkeletalMeshDest ? *SkeletalMeshDest->GetName() : TEXT("NULL"));
//Very old asset will not have this data, we cannot add alternate until the asset is reimported
return false;
}
FSkeletalMeshImportData ImportDataDest;
PRAGMA_DISABLE_DEPRECATION_WARNINGS
SkeletalMeshDest->LoadLODImportedData(LODIndexDest, ImportDataDest);
PRAGMA_ENABLE_DEPRECATION_WARNINGS
int32 PointNumberDest = ImportDataDest.Points.Num();
int32 VertexNumberDest = ImportDataDest.Points.Num();
//Grab all the source structure
check(SkeletalMeshSrc);
//The source model is a fresh import and the data need to be there
check(SkeletalMeshSrc->HasMeshDescription(LODIndexSrc));
FSkeletalMeshImportData ImportDataSrc;
PRAGMA_DISABLE_DEPRECATION_WARNINGS
SkeletalMeshSrc->LoadLODImportedData(LODIndexSrc, ImportDataSrc);
PRAGMA_ENABLE_DEPRECATION_WARNINGS
//Remove all unnecessary array data from the structure (this will save a lot of memory)
ImportDataSrc.KeepAlternateSkinningBuildDataOnly();
FString SkeletalMeshDestName = SkeletalMeshDest->GetName();
if (ImportDataSrc.Points.Num() != PointNumberDest)
{
UE_LOG(LogLODUtilities, Error, TEXT("Asset %s failed to import Skin Weight Profile as the incomming alternate influence model vertex number is different. LOD model vertex count: %d Alternate model vertex count: %d"), *SkeletalMeshDestName, PointNumberDest, ImportDataSrc.Points.Num());
return false;
}
if (!ValidateAlternateSkeleton(ImportDataSrc, ImportDataDest, SkeletalMeshDestName, LODIndexDest))
{
//Log are print in the validate function
return false;
}
//Replace the data into the destination bulk data and save it
int32 ProfileIndex = 0;
if (ImportDataDest.AlternateInfluenceProfileNames.Find(ProfileNameDest.ToString(), ProfileIndex))
{
ImportDataDest.AlternateInfluenceProfileNames.RemoveAt(ProfileIndex);
ImportDataDest.AlternateInfluences.RemoveAt(ProfileIndex);
}
ImportDataDest.AlternateInfluenceProfileNames.Add(ProfileNameDest.ToString());
ImportDataDest.AlternateInfluences.Add(ImportDataSrc);
//Resave the bulk data with the new or refreshed data
PRAGMA_DISABLE_DEPRECATION_WARNINGS
SkeletalMeshDest->SaveLODImportedData(LODIndexDest, ImportDataDest);
PRAGMA_ENABLE_DEPRECATION_WARNINGS
return true;
}
bool FLODUtilities::UpdateAlternateSkinWeights(
USkeletalMesh* SkeletalMeshDest,
const FName& ProfileNameDest,
USkeletalMesh* SkeletalMeshSrc,
int32 LODIndexDest,
int32 LODIndexSrc,
FOverlappingThresholds OverlappingThresholds,
bool ShouldImportNormals,
bool ShouldImportTangents,
bool bUseMikkTSpace,
bool bComputeWeightedNormals)
{
IMeshUtilities::MeshBuildOptions Options;
Options.OverlappingThresholds = OverlappingThresholds;
Options.bComputeNormals = !ShouldImportNormals;
Options.bComputeTangents = !ShouldImportTangents;
Options.bUseMikkTSpace = bUseMikkTSpace;
Options.bComputeWeightedNormals = bComputeWeightedNormals;
return UpdateAlternateSkinWeights(SkeletalMeshDest, ProfileNameDest, SkeletalMeshSrc, LODIndexDest, LODIndexSrc, Options);
}
void FLODUtilities::GenerateImportedSkinWeightProfileData(
FSkeletalMeshLODModel& LODModelDest,
FImportedSkinWeightProfileData& ImportedProfileData,
int32 BoneInfluenceLimit,
const ITargetPlatform* TargetPlatform)
{
using namespace UE::AnimationCore;
//Add the override buffer with the alternate influence data
TArray<FSoftSkinVertex> DestinationSoftVertices;
LODModelDest.GetVertices(DestinationSoftVertices);
//Get the SkinWeights buffer allocated before filling it
TArray<FRawSkinWeight>& SkinWeights = ImportedProfileData.SkinWeights;
SkinWeights.Empty(DestinationSoftVertices.Num());
const int32 MaxBoneInfluencesFromProjectSettings = FGPUBaseSkinVertexFactory::UseUnlimitedBoneInfluences(MAX_TOTAL_INFLUENCES, TargetPlatform) ? MAX_TOTAL_INFLUENCES : EXTRA_BONE_INFLUENCES;
const int32 MaxBoneInfluencesFromAsset = FGPUBaseSkinVertexFactory::GetBoneInfluenceLimitForAsset(BoneInfluenceLimit, TargetPlatform);
//Get the maximum allow bone influence, so we can cut lowest weight properly and get the same result has the sk build
const int32 MaxInfluenceCount = FMath::Min(MaxBoneInfluencesFromProjectSettings, MaxBoneInfluencesFromAsset);
int32 MaxNumInfluences = 0;
TMultiMap<int32, const SkeletalMeshImportData::FVertInfluence*> VertexToInfluenceMap;
for (const SkeletalMeshImportData::FVertInfluence& VertInfluence : ImportedProfileData.SourceModelInfluences)
{
VertexToInfluenceMap.Add(VertInfluence.VertIndex, &VertInfluence);
}
TMap<FBoneIndexType, float> WeightForBone;
TArray<const SkeletalMeshImportData::FVertInfluence*> FoundInfluences;
// cf. FCompareVertexIndex (should be factorized for next versions)
auto VertInfluencePredicate = [](const SkeletalMeshImportData::FVertInfluence* A, const SkeletalMeshImportData::FVertInfluence* B)
{
if (A->VertIndex > B->VertIndex) return false;
if (A->VertIndex < B->VertIndex) return true;
if (A->Weight < B->Weight) return false;
if (A->Weight > B->Weight) return true;
if (A->BoneIndex > B->BoneIndex) return false;
if (A->BoneIndex < B->BoneIndex) return true;
return false;
};
for (int32 VertexInstanceIndex = 0; VertexInstanceIndex < DestinationSoftVertices.Num(); ++VertexInstanceIndex)
{
int32 SectionIndex = INDEX_NONE;
int32 OutVertexIndexGarb = INDEX_NONE;
LODModelDest.GetSectionFromVertexIndex(VertexInstanceIndex, SectionIndex, OutVertexIndexGarb);
if (!LODModelDest.Sections.IsValidIndex(SectionIndex))
{
continue;
}
FSkelMeshSection& Section = LODModelDest.Sections[SectionIndex];
const TArray<FBoneIndexType>& SectionBoneMap = Section.BoneMap;
const int32 VertexIndex = LODModelDest.GetRawPointIndices()[VertexInstanceIndex];
FRawSkinWeight& SkinWeight = SkinWeights.AddDefaulted_GetRef();
// get the bone weights for that vertex and sort them (mostly by weights here as VertIndex are similar)
FoundInfluences.Reset();
VertexToInfluenceMap.MultiFind(VertexIndex, FoundInfluences);
Algo::Sort(FoundInfluences, VertInfluencePredicate);
WeightForBone.Reset();
for (const SkeletalMeshImportData::FVertInfluence* VertInfluence : FoundInfluences)
{
//Use the section bone map to remap the bone index
int32 BoneMapIndex = INDEX_NONE;
SectionBoneMap.Find(VertInfluence->BoneIndex, BoneMapIndex);
if (BoneMapIndex == INDEX_NONE)
{
//Map to root of the section
BoneMapIndex = 0;
}
WeightForBone.Add(static_cast<FBoneIndexType>(BoneMapIndex), VertInfluence->Weight);
}
//Add the prepared alternate influences for this skin vertex (cf. MeshUtilities::GenerateSkeletalRenderMesh)
int32 InfluenceBoneCount = FMath::Min<uint32>(WeightForBone.Num(), MaxBoneInfluencesFromAsset);
if (InfluenceBoneCount > EXTRA_BONE_INFLUENCES && !FGPUBaseSkinVertexFactory::UseUnlimitedBoneInfluences(InfluenceBoneCount, TargetPlatform))
{
InfluenceBoneCount = EXTRA_BONE_INFLUENCES;
}
FBoneIndexType InfluenceBones[MAX_TOTAL_INFLUENCES];
float InfluenceWeights[MAX_TOTAL_INFLUENCES];
int32 ActualInfluenceCount = 0;
for (const TTuple<FBoneIndexType, float>& BoneAndWeight : WeightForBone)
{
if (ActualInfluenceCount < InfluenceBoneCount)
{
InfluenceBones[ActualInfluenceCount] = BoneAndWeight.Key;
InfluenceWeights[ActualInfluenceCount] = BoneAndWeight.Value;
ActualInfluenceCount++;
}
}
const FBoneWeights BoneWeights = FBoneWeights::Create(InfluenceBones, InfluenceWeights, ActualInfluenceCount);
FMemory::Memzero(SkinWeight.InfluenceBones);
FMemory::Memzero(SkinWeight.InfluenceWeights);
if (BoneWeights.Num() == 0)
{
SkinWeight.InfluenceWeights[0] = std::numeric_limits<uint16>::max();
}
else
{
int32 Index = 0;
for (const FBoneWeight& BoneWeight: BoneWeights)
{
SkinWeight.InfluenceBones[Index] = BoneWeight.GetBoneIndex();
SkinWeight.InfluenceWeights[Index] = BoneWeight.GetRawWeight();
Index++;
}
}
//Adjust section influence count if the alternate influence bone count is greater
if (ActualInfluenceCount > MaxNumInfluences)
{
MaxNumInfluences = ActualInfluenceCount;
if (MaxNumInfluences > Section.GetMaxBoneInfluences())
{
Section.MaxBoneInfluences = MaxNumInfluences;
}
}
check(MaxNumInfluences <= MaxInfluenceCount);
}
}
void FLODUtilities::RegenerateAllImportSkinWeightProfileData(FSkeletalMeshLODModel& LODModelDest, int32 BoneInfluenceLimit, const ITargetPlatform* TargetPlatform)
{
for (TPair<FName, FImportedSkinWeightProfileData>& ProfilePair : LODModelDest.SkinWeightProfiles)
{
GenerateImportedSkinWeightProfileData(LODModelDest, ProfilePair.Value, BoneInfluenceLimit, TargetPlatform);
}
}
bool FLODUtilities::UpdateLODInfoVertexAttributes(
USkeletalMesh *InSkeletalMesh,
int32 InSourceLODIndex,
int32 InTargetLODIndex,
bool bInCopyAttributeValues
)
{
if (!InSkeletalMesh->HasMeshDescription(InSourceLODIndex))
{
return false;
}
FSkeletalMeshLODModel& TargetLODModel = InSkeletalMesh->GetImportedModel()->LODModels[InTargetLODIndex];
const TArray<FSkeletalMeshVertexAttributeInfo>& SkelMeshAttributeInfos = InSkeletalMesh->GetLODInfo(InTargetLODIndex)->VertexAttributes;
// Retain any existing attribute infos and match based on names.
TMap<FName, const FSkeletalMeshVertexAttributeInfo*> ExistingAttributeInfos;
for (const FSkeletalMeshVertexAttributeInfo& AttributeInfo: SkelMeshAttributeInfos)
{
ExistingAttributeInfos.Add(AttributeInfo.Name, &AttributeInfo);
}
const FMeshDescription* MeshDescription = InSkeletalMesh->GetMeshDescription(InSourceLODIndex);
// NOTE: There's a current limitation that we only support single-channel attributes for rendering.
TMap<FName, TVertexAttributesConstRef<float>> SourceAttributes;
MeshDescription->VertexAttributes().ForEachByType<float>([&SourceAttributes](const FName InAttributeName, TVertexAttributesConstRef<float> InAttributeRef)
{
if (!FSkeletalMeshAttributes::IsReservedAttributeName(InAttributeName))
{
SourceAttributes.Add(InAttributeName, InAttributeRef);
}
});
// If we're not copying the values, leave the existing data in place.
if (bInCopyAttributeValues)
{
TargetLODModel.VertexAttributes.Reset();
}
TArray<UE::Tasks::FTask> ConversionTasks;
for (const TPair<FName, TVertexAttributesConstRef<float>>& SourceAttributeInfo: SourceAttributes)
{
const TVertexAttributesConstRef<float> SourceAttribute = SourceAttributeInfo.Value;
const FName AttributeName(SourceAttributeInfo.Key);
// Did this definition already exist? Try to retain as much of the existing information as possible.
const FSkeletalMeshVertexAttributeInfo* AttributeInfo = nullptr;
if (ExistingAttributeInfos.Contains(AttributeName))
{
AttributeInfo = ExistingAttributeInfos[AttributeName];
}
if (AttributeInfo && AttributeInfo->IsEnabledForRender() && bInCopyAttributeValues)
{
FSkeletalMeshModelVertexAttribute& ModelAttribute = TargetLODModel.VertexAttributes.FindOrAdd(AttributeName);
ModelAttribute.DataType = AttributeInfo->DataType;
ModelAttribute.ComponentCount = 1;
if (InTargetLODIndex == InSourceLODIndex)
{
ConversionTasks.Add(
UE::Tasks::Launch(UE_SOURCE_LOCATION, [&TargetLODModel, AttributeName, SourceAttribute]()
{
FSkeletalMeshModelVertexAttribute& ModelAttribute = TargetLODModel.VertexAttributes.FindChecked(AttributeName);
ModelAttribute.Values.SetNumUninitialized(TargetLODModel.NumVertices);
for(uint32 VertexIndex = 0; VertexIndex < TargetLODModel.NumVertices; VertexIndex++)
{
const int32 ImportVertexIndex = TargetLODModel.MeshToImportVertexMap[VertexIndex];
ModelAttribute.Values[VertexIndex] = SourceAttribute.Get(FVertexID(ImportVertexIndex));
}
})
);
}
else
{
// Initialize with zero data, matching the vertex count.
ModelAttribute.Values.SetNumZeroed(TargetLODModel.NumVertices);
}
}
}
// Wait for all the attribute conversion tasks to complete.
UE::Tasks::Wait(ConversionTasks);
return true;
}
void FLODUtilities::RegenerateDependentLODs(USkeletalMesh* SkeletalMesh, int32 LODIndex, const ITargetPlatform* TargetPlatform)
{
int32 LODNumber = SkeletalMesh->GetLODNum();
TMap<int32, TArray<int32>> Dependencies;
TBitArray<> DependentLOD;
DependentLOD.Init(false, LODNumber);
DependentLOD[LODIndex] = true;
for (int32 DependentLODIndex = LODIndex + 1; DependentLODIndex < LODNumber; ++DependentLODIndex)
{
const FSkeletalMeshLODInfo* LODInfo = SkeletalMesh->GetLODInfo(DependentLODIndex);
//Only add active reduction LOD that are not inline reducted (inline mean they do not depend on LODIndex)
if (LODInfo && (SkeletalMesh->IsReductionActive(DependentLODIndex) || LODInfo->bHasBeenSimplified) && DependentLODIndex > LODInfo->ReductionSettings.BaseLOD && DependentLOD[LODInfo->ReductionSettings.BaseLOD])
{
TArray<int32>& LODDependencies = Dependencies.FindOrAdd(LODInfo->ReductionSettings.BaseLOD);
LODDependencies.Add(DependentLODIndex);
DependentLOD[DependentLODIndex] = true;
}
}
if (Dependencies.Contains(LODIndex))
{
//Load the necessary module before going multithreaded
IMeshReductionModule& ReductionModule = FModuleManager::Get().LoadModuleChecked<IMeshReductionModule>("MeshReductionInterface");
//This will load all necessary module before kicking the multi threaded reduction
IMeshReduction* MeshReduction = ReductionModule.GetSkeletalMeshReductionInterface();
if (!MeshReduction)
{
UE_ASSET_LOG(LogLODUtilities, Warning, SkeletalMesh, TEXT("Cannot reduce skeletalmesh LOD because there is no active reduction plugin."));
return;
}
check(MeshReduction->IsSupported());
FScopedSkeletalMeshPostEditChange ScopedPostEditChange(SkeletalMesh);
if (IsInGameThread())
{
FFormatNamedArguments Args;
Args.Add(TEXT("DesiredLOD"), LODIndex);
Args.Add(TEXT("SkeletalMeshName"), FText::FromString(SkeletalMesh->GetName()));
const FText StatusUpdate = FText::Format(NSLOCTEXT("UnrealEd", "MeshSimp_GeneratingDependentLODs_F", "Generating All Dependent LODs from LOD {DesiredLOD} for {SkeletalMeshName}..."), Args);
GWarn->BeginSlowTask(StatusUpdate, true);
}
for (const auto& Kvp : Dependencies)
{
int32 MaxDependentLODIndex = 0;
//Use a TQueue which is thread safe, this Queue will be fill by some delegate call from other threads
TQueue<FSkeletalMeshLODModel*> LODModelReplaceByReduction;
const TArray<int32>& DependentLODs = Kvp.Value;
//Clothing do not play well with multithread, backup it here. Also bind the LODModel delete delegates
TMap<int32, TArray<ClothingAssetUtils::FClothingAssetMeshBinding>> PerLODClothingBindings;
for (int32 DependentLODIndex : DependentLODs)
{
MaxDependentLODIndex = FMath::Max(MaxDependentLODIndex, DependentLODIndex);
TArray<ClothingAssetUtils::FClothingAssetMeshBinding>& ClothingBindings = PerLODClothingBindings.FindOrAdd(DependentLODIndex);
FLODUtilities::UnbindClothingAndBackup(SkeletalMesh, ClothingBindings, DependentLODIndex);
const FSkeletalMeshLODInfo* LODInfo = SkeletalMesh->GetLODInfo(DependentLODIndex);
check(LODInfo);
LODInfo->ReductionSettings.OnDeleteLODModelDelegate.BindLambda([&LODModelReplaceByReduction](FSkeletalMeshLODModel* ReplacedLODModel)
{
LODModelReplaceByReduction.Enqueue(ReplacedLODModel);
});
}
//Reduce all dependent LODs
std::atomic<bool> bNeedsPackageDirtied{false};
//Adjust the InlineReductionCacheDatas before simplifying dependent LODs
if (SkeletalMesh->GetImportedModel()->InlineReductionCacheDatas.Num() < LODNumber)
{
SkeletalMesh->GetImportedModel()->InlineReductionCacheDatas.AddDefaulted(LODNumber - SkeletalMesh->GetImportedModel()->InlineReductionCacheDatas.Num());
}
else if (SkeletalMesh->GetImportedModel()->InlineReductionCacheDatas.Num() > LODNumber)
{
//If we have too much entry simply shrink the array to valid LODModel size
SkeletalMesh->GetImportedModel()->InlineReductionCacheDatas.SetNum(LODNumber);
}
struct FSimplifyThreadContext
{
TArray<FMorphTargetUpdateRequest> MorphTargetUpdateRequests;
};
// Reduce LODs in parallel (reduction is multithread safe)
const bool bHasAccessToLockedProperties = !FSkinnedAssetAsyncBuildScope::ShouldWaitOnLockedProperties(SkeletalMesh);
TArray<FSimplifyThreadContext> SimplifyThreadContexts;
ParallelForWithTaskContext(SimplifyThreadContexts, DependentLODs.Num(),
[&DependentLODs, &SkeletalMesh, &bNeedsPackageDirtied, bHasAccessToLockedProperties, &TargetPlatform](FSimplifyThreadContext& Context, int32 IterationIndex)
{
TUniquePtr<FSkinnedAssetAsyncBuildScope> AsyncBuildScope(bHasAccessToLockedProperties ? MakeUnique<FSkinnedAssetAsyncBuildScope>(SkeletalMesh) : nullptr);
check(DependentLODs.IsValidIndex(IterationIndex));
int32 DependentLODIndex = DependentLODs[IterationIndex];
check(SkeletalMesh->GetLODInfo(DependentLODIndex)); //We cannot add a LOD when reducing with multi thread, so check we already have one
FLODUtilities::SimplifySkeletalMeshLOD(SkeletalMesh, DependentLODIndex, TargetPlatform, false, Context.MorphTargetUpdateRequests, &bNeedsPackageDirtied);
});
// Perform the morph target updates that we cannot do in a separate thread.
for (FSimplifyThreadContext& Context: SimplifyThreadContexts)
{
for (FMorphTargetUpdateRequest& Request: Context.MorphTargetUpdateRequests)
{
Request.Run();
}
}
if (bNeedsPackageDirtied && IsInGameThread())
{
SkeletalMesh->MarkPackageDirty();
}
//Restore the clothings and unbind the delegates
for (int32 DependentLODIndex : DependentLODs)
{
TArray<ClothingAssetUtils::FClothingAssetMeshBinding>& ClothingBindings = PerLODClothingBindings.FindChecked(DependentLODIndex);
FLODUtilities::RestoreClothingFromBackup(SkeletalMesh, ClothingBindings);
FSkeletalMeshLODInfo* LODInfo = SkeletalMesh->GetLODInfo(DependentLODIndex);
check(LODInfo);
LODInfo->ReductionSettings.OnDeleteLODModelDelegate.Unbind();
}
while (!LODModelReplaceByReduction.IsEmpty())
{
FSkeletalMeshLODModel* ReplacedLODModel = nullptr;
LODModelReplaceByReduction.Dequeue(ReplacedLODModel);
if (ReplacedLODModel)
{
delete ReplacedLODModel;
}
}
check(LODModelReplaceByReduction.IsEmpty());
}
if (IsInGameThread())
{
GWarn->EndSlowTask();
}
}
}
//////////////////////////////////////////////////////////////////////////
// Morph targets build code
//
struct FMeshDataBundle
{
TArray< FVector3f > Vertices;
TArray< uint32 > Indices;
TArray< FVector2f > UVs;
TArray< uint32 > SmoothingGroups;
TArray<SkeletalMeshImportData::FTriangle> Faces;
};
static void ConvertImportDataToMeshData(const FSkeletalMeshImportData& ImportData, FMeshDataBundle& MeshDataBundle)
{
for (const SkeletalMeshImportData::FTriangle& Face : ImportData.Faces)
{
SkeletalMeshImportData::FTriangle FaceTriangle;
FaceTriangle = Face;
for (int32 i = 0; i < 3; ++i)
{
const SkeletalMeshImportData::FVertex& Wedge = ImportData.Wedges[Face.WedgeIndex[i]];
int32 FaceWedgeIndex = MeshDataBundle.Indices.Add(Wedge.VertexIndex);
MeshDataBundle.UVs.Add(Wedge.UVs[0]);
FaceTriangle.WedgeIndex[i] = FaceWedgeIndex;
}
MeshDataBundle.Faces.Add(FaceTriangle);
MeshDataBundle.SmoothingGroups.Add(Face.SmoothingGroups);
}
MeshDataBundle.Vertices = ImportData.Points;
}
/**
* A class encapsulating morph target processing that occurs during import on a separate thread
*/
class FAsyncImportMorphTargetWork : public FNonAbandonableTask
{
public:
FAsyncImportMorphTargetWork(
const FName InMorphName,
FSkeletalMeshLODModel* InLODModel,
const FReferenceSkeleton& InRefSkeleton,
const FSkeletalMeshImportData& InBaseImportData,
const FMeshDescription* InSkeletalMeshModel,
TArray<FVector3f>&& InMorphLODPoints,
TArray< FMorphTargetDelta >& InMorphDeltas,
TArray<uint32>& InBaseIndexData,
const TArray< uint32 >& InBaseWedgePointIndices,
TMap<uint32, uint32>& InWedgePointToVertexIndexMap,
const FOverlappingCorners& InOverlappingCorners,
const TSet<uint32>& InModifiedPoints,
const TMultiMap< int32, int32 >& InWedgeToFaces,
const FMeshDataBundle& InMeshDataBundle,
const TArray<FVector3f>& InTangentZ,
bool InShouldImportNormals,
bool InShouldImportTangents,
bool InbUseMikkTSpace,
const FOverlappingThresholds InThresholds
)
: LODModel(InLODModel)
, RefSkeleton(InRefSkeleton)
, BaseImportData(InBaseImportData)
, SkeletalMeshModel(InSkeletalMeshModel)
, CompressMorphLODPoints(InMorphLODPoints)
, MorphTargetDeltas(InMorphDeltas)
, BaseIndexData(InBaseIndexData)
, BaseWedgePointIndices(InBaseWedgePointIndices)
, WedgePointToVertexIndexMap(InWedgePointToVertexIndexMap)
, OverlappingCorners(InOverlappingCorners)
, ModifiedPoints(InModifiedPoints)
, WedgeToFaces(InWedgeToFaces)
, MeshDataBundle(InMeshDataBundle)
, BaseTangentZ(InTangentZ)
, TangentZ(InTangentZ)
, ShouldImportNormals(InShouldImportNormals)
, ShouldImportTangents(InShouldImportTangents)
, bUseMikkTSpace(InbUseMikkTSpace)
, Thresholds(InThresholds)
{
MeshUtilities = &FModuleManager::Get().LoadModuleChecked<IMeshUtilities>("MeshUtilities");
if (SkeletalMeshModel)
{
FSkeletalMeshConstAttributes SkeletalMeshAttributes(*SkeletalMeshModel);
BaseNormalAttribute = SkeletalMeshAttributes.GetVertexInstanceNormals();
MorphNormalDeltaAttribute = SkeletalMeshAttributes.GetVertexInstanceMorphNormalDelta(InMorphName);
ShouldImportNormals = false;
}
}
//Decompress the shape points data
void DecompressData()
{
const TArray<FVector3f>& BaseMeshPoints = BaseImportData.Points;
MorphLODPoints = BaseMeshPoints;
int32 ModifiedPointIndex = 0;
for (uint32 PointIndex : ModifiedPoints)
{
MorphLODPoints[PointIndex] = CompressMorphLODPoints[ModifiedPointIndex];
ModifiedPointIndex++;
}
check(MorphLODPoints.Num() == MeshDataBundle.Vertices.Num());
}
void PrepareTangents()
{
TArray<bool> WasProcessed;
WasProcessed.Empty(MeshDataBundle.Indices.Num());
WasProcessed.AddZeroed(MeshDataBundle.Indices.Num());
TArray< int32 > WedgeFaces;
TArray< int32 > OtherWedgeFaces;
TArray< int32 > OverlappingWedgesDummy;
TArray< int32 > OtherOverlappingWedgesDummy;
// For each ModifiedPoints, reset the tangents for the affected wedges
for (int32 WedgeIdx = 0; WedgeIdx < MeshDataBundle.Indices.Num(); ++WedgeIdx)
{
int32 PointIdx = MeshDataBundle.Indices[WedgeIdx];
if (ModifiedPoints.Find(PointIdx) != nullptr)
{
TangentZ[WedgeIdx] = FVector3f::ZeroVector;
const TArray<int32>& OverlappingWedges = FindIncludingNoOverlapping(OverlappingCorners, WedgeIdx, OverlappingWedgesDummy);
for (const int32 OverlappingWedgeIndex : OverlappingWedges)
{
if (WasProcessed[OverlappingWedgeIndex])
{
continue;
}
WasProcessed[OverlappingWedgeIndex] = true;
WedgeFaces.Reset();
WedgeToFaces.MultiFind(OverlappingWedgeIndex, WedgeFaces);
for (const int32 FaceIndex : WedgeFaces)
{
for (int32 CornerIndex = 0; CornerIndex < 3; ++CornerIndex)
{
int32 WedgeIndex = MeshDataBundle.Faces[FaceIndex].WedgeIndex[CornerIndex];
TangentZ[WedgeIndex] = FVector3f::ZeroVector;
const TArray<int32>& OtherOverlappingWedges = FindIncludingNoOverlapping(OverlappingCorners, WedgeIndex, OtherOverlappingWedgesDummy);
for (const int32 OtherDupVert : OtherOverlappingWedges)
{
OtherWedgeFaces.Reset();
WedgeToFaces.MultiFind(OtherDupVert, OtherWedgeFaces);
for (const int32 OtherFaceIndex : OtherWedgeFaces)
{
for (int32 OtherCornerIndex = 0; OtherCornerIndex < 3; ++OtherCornerIndex)
{
int32 OtherWedgeIndex = MeshDataBundle.Faces[OtherFaceIndex].WedgeIndex[OtherCornerIndex];
TangentZ[OtherWedgeIndex] = FVector3f::ZeroVector;
}
}
}
}
}
}
}
}
}
void ComputeTangents()
{
bool bComputeNormals = !ShouldImportNormals || !BaseImportData.bHasNormals;
bool bComputeTangents = !ShouldImportTangents || !BaseImportData.bHasTangents;
bool bUseMikkTSpaceFinal = bUseMikkTSpace && (!ShouldImportNormals || !ShouldImportTangents);
check(MorphLODPoints.Num() == MeshDataBundle.Vertices.Num());
ETangentOptions::Type TangentOptions = ETangentOptions::BlendOverlappingNormals;
// MikkTSpace should be use only when the user want to recompute the normals or tangents otherwise should always fallback on builtin
if (bUseMikkTSpaceFinal && (bComputeNormals || bComputeTangents))
{
TangentOptions = (ETangentOptions::Type)(TangentOptions | ETangentOptions::UseMikkTSpace);
}
MeshUtilities->CalculateNormals(MorphLODPoints, MeshDataBundle.Indices, MeshDataBundle.UVs, MeshDataBundle.SmoothingGroups, TangentOptions, TangentZ);
}
void ComputeMorphDeltas()
{
TArray<bool> WasProcessed;
WasProcessed.Empty(LODModel->NumVertices);
WasProcessed.AddZeroed(LODModel->NumVertices);
const bool bUseAttributeForNormal = MorphNormalDeltaAttribute.IsValid();
for (int32 Idx = 0; Idx < BaseIndexData.Num(); ++Idx)
{
uint32 BaseVertIdx = BaseIndexData[Idx];
// check for duplicate processing
if (WasProcessed[BaseVertIdx])
{
continue;
}
// mark this base vertex as already processed
WasProcessed[BaseVertIdx] = true;
// clothing can add extra verts, and we won't have source point, so we ignore those
if (!BaseWedgePointIndices.IsValidIndex(BaseVertIdx))
{
continue;
}
// get the base mesh's original wedge point index
uint32 BasePointIdx = BaseWedgePointIndices[BaseVertIdx];
if (MeshDataBundle.Vertices.IsValidIndex(BasePointIdx) && MorphLODPoints.IsValidIndex(BasePointIdx))
{
const FVector3f& BasePosition = MeshDataBundle.Vertices[BasePointIdx];
const FVector3f& TargetPosition = MorphLODPoints[BasePointIdx];
const FVector3f PositionDelta = TargetPosition - BasePosition;
uint32* VertexIdx = WedgePointToVertexIndexMap.Find(BasePointIdx);
FVector3f NormalDeltaZ = FVector3f::ZeroVector;
if (VertexIdx != nullptr)
{
FVector3f BaseNormal = BaseTangentZ[*VertexIdx];
if (bUseAttributeForNormal)
{
// BasePointIdx is the index into the import data points. These map directly to the mesh description's vertex ids.
const FVertexID VertexID(BasePointIdx);
const FVertexInstanceID VertexInstanceID = SkeletalMeshModel->GetVertexVertexInstanceIDs(VertexID)[0];
// Use the first one. They all carry the same information for now due to the only place we get these normals are from
// rebuilding from a FSkeletalMeshLODModel + UMorphTargets.
NormalDeltaZ = MorphNormalDeltaAttribute.Get(VertexInstanceID);
}
else
{
NormalDeltaZ = TangentZ[*VertexIdx] - BaseNormal;
}
}
// check if position actually changed much
if (PositionDelta.SizeSquared() > FMath::Square(Thresholds.MorphThresholdPosition) ||
// since we can't get imported morph target normal from FBX
// we can't compare normal unless it's calculated
// this is special flag to ignore normal diff
((ShouldImportNormals == false) && NormalDeltaZ.SizeSquared() > 0.01f))
{
// create a new entry
FMorphTargetDelta NewVertex;
// position delta
NewVertex.PositionDelta = PositionDelta;
// normal delta
NewVertex.TangentZDelta = NormalDeltaZ;
// index of base mesh vert this entry is to modify
NewVertex.SourceIdx = BaseVertIdx;
// add it to the list of changed verts
MorphTargetDeltas.Add(NewVertex);
}
}
}
}
void DoWork()
{
DecompressData();
if (!MorphNormalDeltaAttribute.IsValid())
{
PrepareTangents();
ComputeTangents();
}
ComputeMorphDeltas();
}
FORCEINLINE TStatId GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FAsyncImportMorphTargetWork, STATGROUP_ThreadPoolAsyncTasks);
}
private:
const TArray<int32>& FindIncludingNoOverlapping(const FOverlappingCorners& Corners, int32 Key, TArray<int32>& NoOverlapping)
{
const TArray<int32>& Found = Corners.FindIfOverlapping(Key);
if (Found.Num() > 0)
{
return Found;
}
else
{
NoOverlapping.Reset(1);
NoOverlapping.Add(Key);
return NoOverlapping;
}
}
FSkeletalMeshLODModel* LODModel;
// @todo not thread safe
const FReferenceSkeleton& RefSkeleton;
const FSkeletalMeshImportData& BaseImportData;
const FMeshDescription* SkeletalMeshModel;
TVertexInstanceAttributesConstRef<FVector3f> BaseNormalAttribute;
TVertexInstanceAttributesConstRef<FVector3f> MorphNormalDeltaAttribute;
const TArray<FVector3f> CompressMorphLODPoints;
TArray<FVector3f> MorphLODPoints;
IMeshUtilities* MeshUtilities;
TArray< FMorphTargetDelta >& MorphTargetDeltas;
TArray< uint32 >& BaseIndexData;
const TArray< uint32 >& BaseWedgePointIndices;
TMap<uint32, uint32>& WedgePointToVertexIndexMap;
const FOverlappingCorners& OverlappingCorners;
const TSet<uint32>& ModifiedPoints;
const TMultiMap< int32, int32 >& WedgeToFaces;
const FMeshDataBundle& MeshDataBundle;
const TArray<FVector3f>& BaseTangentZ;
TArray<FVector3f> TangentZ;
bool ShouldImportNormals;
bool ShouldImportTangents;
bool bUseMikkTSpace;
const FOverlappingThresholds Thresholds;
};
static void BuildMorphTargetsInternal(
USkeletalMesh* BaseSkelMesh,
const FMeshDescription* SkeletalMeshModel,
FSkeletalMeshImportData &BaseImportData,
int32 LODIndex,
bool ShouldImportNormals,
bool ShouldImportTangents,
bool bUseMikkTSpace,
const FOverlappingThresholds& Thresholds
)
{
bool bComputeNormals = !ShouldImportNormals || !BaseImportData.bHasNormals;
bool bComputeTangents = !ShouldImportTangents || !BaseImportData.bHasTangents;
bool bUseMikkTSpaceFinal = bUseMikkTSpace && (!ShouldImportNormals || !ShouldImportTangents);
// Prepare base data
FSkeletalMeshLODModel& BaseLODModel = BaseSkelMesh->GetImportedModel()->LODModels[LODIndex];
const FSkeletalMeshLODInfo* BaseLodInfo = BaseSkelMesh->GetLODInfo(LODIndex);
FMeshDataBundle MeshDataBundle;
ConvertImportDataToMeshData(BaseImportData, MeshDataBundle);
IMeshUtilities& MeshUtilities = FModuleManager::Get().LoadModuleChecked<IMeshUtilities>("MeshUtilities");
ETangentOptions::Type TangentOptions = ETangentOptions::BlendOverlappingNormals;
// MikkTSpace should be use only when the user want to recompute the normals or tangents otherwise should always fallback on builtin
if (bUseMikkTSpaceFinal && (bComputeNormals || bComputeTangents))
{
TangentOptions = (ETangentOptions::Type)(TangentOptions | ETangentOptions::UseMikkTSpace);
}
FOverlappingCorners OverlappingVertices;
MeshUtilities.CalculateOverlappingCorners(MeshDataBundle.Vertices, MeshDataBundle.Indices, false, OverlappingVertices);
TArray<FVector3f> TangentZ;
MeshUtilities.CalculateNormals(MeshDataBundle.Vertices, MeshDataBundle.Indices, MeshDataBundle.UVs, MeshDataBundle.SmoothingGroups, TangentOptions, TangentZ);
TArray<uint32> BaseIndexData = BaseLODModel.IndexBuffer;
TMap<uint32, uint32> WedgePointToVertexIndexMap;
// Build a mapping of wedge point indices to vertex indices for fast lookup later.
for (int32 Idx = 0; Idx < MeshDataBundle.Indices.Num(); ++Idx)
{
WedgePointToVertexIndexMap.Add(MeshDataBundle.Indices[Idx], Idx);
}
// Create a map from wedge indices to faces
TMultiMap< int32, int32 > WedgeToFaces;
for (int32 FaceIndex = 0; FaceIndex < MeshDataBundle.Faces.Num(); FaceIndex++)
{
const SkeletalMeshImportData::FTriangle& Face = MeshDataBundle.Faces[FaceIndex];
for (int32 CornerIndex = 0; CornerIndex < 3; ++CornerIndex)
{
WedgeToFaces.AddUnique(Face.WedgeIndex[CornerIndex], FaceIndex);
}
}
// Temp arrays to keep track of data being used by threads
TArray< TArray< FMorphTargetDelta >* > Results;
TArray<UMorphTarget*> MorphTargets;
// Array of pending tasks that are not complete
TIndirectArray<FAsyncTask<FAsyncImportMorphTargetWork> > PendingWork;
int32 NumCompleted = 0;
int32 NumTasks = 0;
int32 MaxShapeInProcess = FPlatformMisc::NumberOfCoresIncludingHyperthreads();
int32 ShapeIndex = 0;
int32 TotalShapeCount = BaseImportData.MorphTargetNames.Num();
TMap<FName, UMorphTarget*> ExistingMorphTargets;
for (UMorphTarget* MorphTarget : BaseSkelMesh->GetMorphTargets())
{
ExistingMorphTargets.Add(MorphTarget->GetFName(), MorphTarget);
}
// iterate through shapename, and create morphtarget
for (int32 MorphTargetIndex = 0; MorphTargetIndex < BaseImportData.MorphTargetNames.Num(); ++MorphTargetIndex)
{
int32 CurrentNumTasks = PendingWork.Num();
while (CurrentNumTasks >= MaxShapeInProcess)
{
//Wait until the first slot is available
PendingWork[0].EnsureCompletion();
for (int32 TaskIndex = PendingWork.Num() - 1; TaskIndex >= 0; --TaskIndex)
{
if (PendingWork[TaskIndex].IsDone())
{
PendingWork.RemoveAt(TaskIndex);
++NumCompleted;
if (IsInGameThread())
{
FFormatNamedArguments Args;
Args.Add(TEXT("NumCompleted"), NumCompleted);
Args.Add(TEXT("NumTasks"), TotalShapeCount);
GWarn->StatusUpdate(NumCompleted, TotalShapeCount, FText::Format(LOCTEXT("ImportingMorphTargetStatus", "Importing Morph Target: {NumCompleted} of {NumTasks}"), Args));
}
}
}
CurrentNumTasks = PendingWork.Num();
}
check(BaseImportData.MorphTargetNames.IsValidIndex(MorphTargetIndex));
check(BaseImportData.MorphTargetModifiedPoints.IsValidIndex(MorphTargetIndex));
check(BaseImportData.MorphTargets.IsValidIndex(MorphTargetIndex));
FString& ShapeName = BaseImportData.MorphTargetNames[MorphTargetIndex];
FSkeletalMeshImportData& ShapeImportData = BaseImportData.MorphTargets[MorphTargetIndex];
TSet<uint32>& ModifiedPoints = BaseImportData.MorphTargetModifiedPoints[MorphTargetIndex];
UMorphTarget* MorphTarget = nullptr;
{
FName ObjectName = *ShapeName;
MorphTarget = ExistingMorphTargets.FindRef(ObjectName);
// we only create new one for LOD0, otherwise don't create new one
if (!MorphTarget)
{
if (LODIndex == 0)
{
//Garbage collect must be delayed until the skeletal mesh build is done by registering to delegate
//FCoreUObjectDelegates::GetPreGarbageCollectDelegate.
if (ensure(!GIsGarbageCollecting))
{
if (!IsInGameThread())
{
//TODO remove this code when overriding a UObject will be allow outside of the game thread
//We currently need to avoid overriding an existing asset outside of the game thread
UObject* ExistingMorphTarget = StaticFindObject(UMorphTarget::StaticClass(), BaseSkelMesh, *ShapeName);
if (ExistingMorphTarget)
{
//make sure the object is not standalone or transactional
ExistingMorphTarget->ClearFlags(RF_Standalone | RF_Transactional);
//Move this object in the transient package
ExistingMorphTarget->Rename(nullptr, GetTransientPackage(), REN_DoNotDirty | REN_DontCreateRedirectors | REN_NonTransactional);
ExistingMorphTarget = nullptr;
}
}
MorphTarget = NewObject<UMorphTarget>(BaseSkelMesh, ObjectName);
}
else
{
UE_ASSET_LOG(LogLODUtilities, Error, BaseSkelMesh, TEXT("FLODUtilities::BuildMorphTargets: Garbage collection is running during the skeletal build. Morph target [%s] cannot be built properly and will be missing."), *ShapeName);
}
}
else
{
/*AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(FText::FromString("Could not find the {0} morphtarget for LOD {1}. \
Make sure the name for morphtarget matches with LOD 0"), FText::FromString(ShapeName), FText::FromString(FString::FromInt(LODIndex)))),
FFbxErrors::SkeletalMesh_LOD_MissingMorphTarget);*/
}
}
}
if (MorphTarget)
{
check(IsValid(MorphTarget));
MorphTargets.Add(MorphTarget);
int32 NewMorphDeltasIdx = Results.Add(new TArray< FMorphTargetDelta >());
TArray< FMorphTargetDelta >* Deltas = Results[NewMorphDeltasIdx];
FAsyncTask<FAsyncImportMorphTargetWork>* NewWork = new FAsyncTask<FAsyncImportMorphTargetWork>(MorphTarget->GetFName(), &BaseLODModel, BaseSkelMesh->GetRefSkeleton(), BaseImportData, SkeletalMeshModel,
MoveTemp(ShapeImportData.Points), *Deltas, BaseIndexData, BaseLODModel.GetRawPointIndices(), WedgePointToVertexIndexMap, OverlappingVertices, ModifiedPoints, WedgeToFaces, MeshDataBundle, TangentZ,
ShouldImportNormals, ShouldImportTangents, bUseMikkTSpace, Thresholds);
PendingWork.Add(NewWork);
NewWork->StartBackgroundTask(GLargeThreadPool);
CurrentNumTasks++;
NumTasks++;
}
++ShapeIndex;
}
// Wait for all importing tasks to complete
for (int32 TaskIndex = 0; TaskIndex < PendingWork.Num(); ++TaskIndex)
{
PendingWork[TaskIndex].EnsureCompletion();
++NumCompleted;
if (IsInGameThread())
{
FFormatNamedArguments Args;
Args.Add(TEXT("NumCompleted"), NumCompleted);
Args.Add(TEXT("NumTasks"), TotalShapeCount);
GWarn->StatusUpdate(NumCompleted, NumTasks, FText::Format(LOCTEXT("ImportingMorphTargetStatus", "Importing Morph Target: {NumCompleted} of {NumTasks}"), Args));
}
}
bool bNeedToInvalidateRegisteredMorph = false;
// Create morph streams for each morph target we are importing.
// This has to happen on a single thread since the skeletal meshes' bulk data is locked and cant be accessed by multiple threads simultaneously
for (int32 Index = 0; Index < MorphTargets.Num(); Index++)
{
if (IsInGameThread())
{
FFormatNamedArguments Args;
Args.Add(TEXT("NumCompleted"), Index + 1);
Args.Add(TEXT("NumTasks"), MorphTargets.Num());
GWarn->StatusUpdate(Index + 1, MorphTargets.Num(), FText::Format(LOCTEXT("BuildingMorphTargetRenderDataStatus", "Building Morph Target Render Data: {NumCompleted} of {NumTasks}"), Args));
}
UMorphTarget* MorphTarget = MorphTargets[Index];
check(IsValid(MorphTarget));
constexpr bool bCompareNormals = true; // Ensure we include morphs that only modify normals and not positions.
constexpr bool bGeneratedByReduction = false;
MorphTarget->PopulateDeltas(*Results[Index], LODIndex, BaseLODModel.Sections, bCompareNormals, bGeneratedByReduction, Thresholds.MorphThresholdPosition);
// register does mark package as dirty
if (MorphTarget->HasValidData())
{
bNeedToInvalidateRegisteredMorph |= BaseSkelMesh->RegisterMorphTarget(MorphTarget, false);
}
delete Results[Index];
Results[Index] = nullptr;
//Reset the morph target imported source filename
MorphTarget->SetCustomImportedSourceFilename(LODIndex, FString());
MorphTarget->SetGeneratedByEngine(LODIndex, false);
//Now set the imported source filename from the imported data
if (BaseLodInfo)
{
if (const FMorphTargetImportedSourceFileInfo* MorphTargetImportedSourceFileInfo = BaseLodInfo->ImportedMorphTargetSourceFilename.Find(MorphTarget->GetName()))
{
const FString& SourceFilename = MorphTargetImportedSourceFileInfo->GetSourceFilename();
if (MorphTargetImportedSourceFileInfo->IsGeneratedByEngine())
{
MorphTarget->SetGeneratedByEngine(LODIndex, true);
}
else if (!SourceFilename.IsEmpty())
{
MorphTarget->SetCustomImportedSourceFilename(LODIndex, *SourceFilename);
}
}
}
// We might have created new MorphTarget in an async thread, so we need to remove the async flag so they can get
// garbage collected in the future now that their references are properly setup and reachable by the GC.
MorphTarget->ClearInternalFlags(EInternalObjectFlags::Async);
}
if (bNeedToInvalidateRegisteredMorph)
{
BaseSkelMesh->InitMorphTargetsAndRebuildRenderData();
}
}
void FLODUtilities::BuildMorphTargets(USkeletalMesh* SkeletalMesh, FSkeletalMeshImportData& ImportData, int32 LODIndex, bool ShouldImportNormals, bool ShouldImportTangents, bool bUseMikkTSpace, const FOverlappingThresholds& Thresholds)
{
BuildMorphTargetsInternal(SkeletalMesh, nullptr, ImportData, LODIndex, ShouldImportNormals, ShouldImportTangents, bUseMikkTSpace, Thresholds);
}
void FLODUtilities::BuildMorphTargets(USkeletalMesh* SkeletalMesh, const FMeshDescription& SkeletalMeshModel, FSkeletalMeshImportData& ImportData, int32 LODIndex, bool ShouldImportNormals, bool ShouldImportTangents, bool bUseMikkTSpace, const FOverlappingThresholds& Thresholds)
{
BuildMorphTargetsInternal(SkeletalMesh, &SkeletalMeshModel, ImportData, LODIndex, ShouldImportNormals, ShouldImportTangents, bUseMikkTSpace, Thresholds);
}
void FLODUtilities::UnbindClothingAndBackup(USkeletalMesh* SkeletalMesh, TArray<ClothingAssetUtils::FClothingAssetMeshBinding>& ClothingBindings)
{
for (int32 LODIndex = 0; LODIndex < SkeletalMesh->GetImportedModel()->LODModels.Num(); ++LODIndex)
{
TArray<ClothingAssetUtils::FClothingAssetMeshBinding> LODBindings;
UnbindClothingAndBackup(SkeletalMesh, LODBindings, LODIndex);
ClothingBindings.Append(LODBindings);
}
}
void FLODUtilities::UnbindClothingAndBackup(USkeletalMesh* SkeletalMesh, TArray<ClothingAssetUtils::FClothingAssetMeshBinding>& ClothingBindings, const int32 LODIndex)
{
if (!SkeletalMesh->GetImportedModel()->LODModels.IsValidIndex(LODIndex))
{
return;
}
FSkeletalMeshLODModel& LODModel = SkeletalMesh->GetImportedModel()->LODModels[LODIndex];
//Store the clothBinding
ClothingAssetUtils::GetAllLodMeshClothingAssetBindings(SkeletalMesh, ClothingBindings, LODIndex);
//Unbind the Cloth for this LOD before we reduce it, we will put back the cloth after the reduction, if it still match the sections
for (ClothingAssetUtils::FClothingAssetMeshBinding& Binding : ClothingBindings)
{
if (Binding.LODIndex == LODIndex)
{
//Use the UserSectionsData original section index, this will ensure we remap correctly the cloth if the reduction has change the number of sections
int32 OriginalDataSectionIndex = LODModel.Sections[Binding.SectionIndex].OriginalDataSectionIndex;
if (Binding.Asset)
{
Binding.Asset->UnbindFromSkeletalMesh(SkeletalMesh, Binding.LODIndex);
Binding.SectionIndex = OriginalDataSectionIndex;
}
FSkelMeshSourceSectionUserData& SectionUserData = LODModel.UserSectionsData.FindChecked(OriginalDataSectionIndex);
SectionUserData.ClothingData.AssetGuid = FGuid();
SectionUserData.ClothingData.AssetLodIndex = INDEX_NONE;
SectionUserData.CorrespondClothAssetIndex = INDEX_NONE;
}
}
}
void FLODUtilities::RestoreClothingFromBackup(USkeletalMesh* SkeletalMesh, TArray<ClothingAssetUtils::FClothingAssetMeshBinding>& ClothingBindings)
{
for (int32 LODIndex = 0; LODIndex < SkeletalMesh->GetImportedModel()->LODModels.Num(); ++LODIndex)
{
RestoreClothingFromBackup(SkeletalMesh, ClothingBindings, LODIndex);
}
}
void FLODUtilities::RestoreClothingFromBackup(USkeletalMesh* SkeletalMesh, TArray<ClothingAssetUtils::FClothingAssetMeshBinding>& ClothingBindings, const int32 LODIndex)
{
if (!SkeletalMesh->GetImportedModel()->LODModels.IsValidIndex(LODIndex))
{
return;
}
FSkeletalMeshLODModel& LODModel = SkeletalMesh->GetImportedModel()->LODModels[LODIndex];
for (ClothingAssetUtils::FClothingAssetMeshBinding& Binding : ClothingBindings)
{
for (int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); ++SectionIndex)
{
if (LODModel.Sections[SectionIndex].OriginalDataSectionIndex != Binding.SectionIndex)
{
continue;
}
if (Binding.LODIndex == LODIndex && Binding.Asset)
{
if (Binding.Asset->BindToSkeletalMesh(SkeletalMesh, Binding.LODIndex, SectionIndex, Binding.AssetInternalLodIndex))
{
//If successfull set back the section user data
FSkelMeshSourceSectionUserData& SectionUserData = LODModel.UserSectionsData.FindChecked(Binding.SectionIndex);
SectionUserData.CorrespondClothAssetIndex = LODModel.Sections[SectionIndex].CorrespondClothAssetIndex;
SectionUserData.ClothingData = LODModel.Sections[SectionIndex].ClothingData;
}
}
break;
}
}
}
void FLODUtilities::BackupCustomImportedMorphTargetData(USkeletalMesh* SkeletalMesh, TMap<FString, TArray<FMorphTargetLodBackupData>>& BackupImportedMorphTargetData)
{
const int32 LodCount = SkeletalMesh->GetLODNum();
constexpr int32 LodIndex0 = 0;
//Find all imported morph targets
if (const FSkeletalMeshLODInfo* LodInfo = SkeletalMesh->GetLODInfo(LodIndex0))
{
TSet<FString> ImportedMorphTargetNames;
LodInfo->ImportedMorphTargetSourceFilename.GetKeys(ImportedMorphTargetNames);
for (const FString& MorphTargetNameStr : ImportedMorphTargetNames)
{
FName MorphTargetName(*MorphTargetNameStr);
TArray<FMorphTargetLodBackupData>& MorphTargetLodDatas = BackupImportedMorphTargetData.FindOrAdd(MorphTargetNameStr);
for (int32 LodIndex = 0; LodIndex < LodCount; ++LodIndex)
{
FMorphTargetLodBackupData& MorphTargetLodData = MorphTargetLodDatas.AddDefaulted_GetRef();
if (SkeletalMesh->HasMeshDescription(LodIndex))
{
FMeshDescription& MeshDescription = *(SkeletalMesh->GetMeshDescription(LodIndex));
FSkeletalMeshConstAttributes SkeletalMeshAttributes(MeshDescription);
if (SkeletalMeshAttributes.HasMorphTargetPositionsAttribute(MorphTargetName))
{
MorphTargetLodData.bIsEmpty = false;
MorphTargetLodData.MorphPositionDeltas.Reserve(MeshDescription.Vertices().Num());
TVertexAttributesConstRef<FVector3f> VertexPositionDeltas = SkeletalMeshAttributes.GetVertexMorphPositionDelta(MorphTargetName);
for (FVertexID VertexID : MeshDescription.Vertices().GetElementIDs())
{
MorphTargetLodData.MorphPositionDeltas.Add(VertexPositionDeltas[VertexID]);
}
if (SkeletalMeshAttributes.HasMorphTargetNormalsAttribute(MorphTargetName))
{
MorphTargetLodData.MorphNormalDeltas.Reserve(MeshDescription.VertexInstances().Num());
TVertexInstanceAttributesConstRef<FVector3f> VertexInstanceNormalDeltas = SkeletalMeshAttributes.GetVertexInstanceMorphNormalDelta(MorphTargetName);
for (FVertexInstanceID VertexInstanceID : MeshDescription.VertexInstances().GetElementIDs())
{
MorphTargetLodData.MorphNormalDeltas.Add(VertexInstanceNormalDeltas[VertexInstanceID]);
}
}
}
}
}
}
}
}
bool FLODUtilities::RestoreCustomImportedMorphTargetData(USkeletalMesh* SkeletalMesh, const int32 LodIndex, FMeshDescription& LodMeshDescription, const TMap<FString, TArray<FMorphTargetLodBackupData>>& BackupImportedMorphTargetData)
{
TMap<FString, FMorphTargetImportedSourceFileInfo> RemovedImportedMorphTargetInfo;
auto RemoveInvalidMorphTargetLOD = [SkeletalMesh, LodIndex, &RemovedImportedMorphTargetInfo](const FString& MorphTargetNameStr)
{
FName MorphTargetName(*MorphTargetNameStr);
if(UMorphTarget* MorphTarget = SkeletalMesh->FindMorphTarget(MorphTargetName))
{
if (MorphTarget)
{
MorphTarget->RemoveFromRoot();
MorphTarget->ClearFlags(RF_Standalone);
if (SkeletalMesh->HasMeshDescription(LodIndex))
{
//Remove the morph target from the raw import data
FMeshDescription* MeshDescription = SkeletalMesh->GetMeshDescription(LodIndex);
FSkeletalMeshAttributes MeshAttributes(*MeshDescription);
if (MeshAttributes.GetMorphTargetNames().Contains(MorphTargetName))
{
SkeletalMesh->ModifyMeshDescription(LodIndex);
MeshAttributes.UnregisterMorphTargetAttribute(MorphTargetName);
SkeletalMesh->CommitMeshDescription(LodIndex);
}
}
SkeletalMesh->UnregisterMorphTarget(MorphTarget, false);
}
}
if (FSkeletalMeshLODInfo* LodInfo = SkeletalMesh->GetLODInfo(LodIndex))
{
if (LodInfo->ImportedMorphTargetSourceFilename.Contains(MorphTargetNameStr))
{
FMorphTargetImportedSourceFileInfo& RemoveBackup = RemovedImportedMorphTargetInfo.FindOrAdd(MorphTargetNameStr);
RemoveBackup = LodInfo->ImportedMorphTargetSourceFilename.FindChecked(MorphTargetNameStr);
LodInfo->ImportedMorphTargetSourceFilename.Remove(MorphTargetNameStr);
}
}
};
bool bIsMeshDescriptionModified = false;
for (const TPair<FString, TArray<FMorphTargetLodBackupData>>& ImportedMorphTargetPair : BackupImportedMorphTargetData)
{
const FString& MorphTargetNameStr = ImportedMorphTargetPair.Key;
FName MorphTargetName(*MorphTargetNameStr);
const TArray<FMorphTargetLodBackupData>& MorphTargetLodDatas = ImportedMorphTargetPair.Value;
if (!MorphTargetLodDatas.IsValidIndex(LodIndex))
{
continue;
}
const FMorphTargetLodBackupData& MorphTargetLodData = MorphTargetLodDatas[LodIndex];
if (MorphTargetLodData.bIsEmpty)
{
//In case the Source Skeletal didn't have MorphTarget for a given LodIndex, it would show up as an Empty.
continue;
}
FSkeletalMeshAttributes SkeletalMeshAttributes(LodMeshDescription);
if (SkeletalMeshAttributes.IsMorphTargetAttribute(MorphTargetName))
{
//The morph target already exist, this mean the re-import added this morph target
continue;
}
if (MorphTargetLodData.MorphPositionDeltas.Num() != LodMeshDescription.Vertices().Num())
{
RemoveInvalidMorphTargetLOD(MorphTargetNameStr);
//Error out that the morph target is not valid anymore for this LOD
UE_ASSET_LOG(LogLODUtilities, Error, SkeletalMesh, TEXT("Cannot keep custom morph target %s for LOD %d, because the re-import LOD now have a different mesh geometry topology."), *MorphTargetNameStr, LodIndex);
continue;
}
//We are good to go and we can add the data to the mesh description
bIsMeshDescriptionModified = true;
const bool bHasNormal = MorphTargetLodData.MorphNormalDeltas.Num() > 0;
SkeletalMeshAttributes.RegisterMorphTargetAttribute(MorphTargetName, bHasNormal);
TVertexAttributesRef<FVector3f> VertexPositionDeltas = SkeletalMeshAttributes.GetVertexMorphPositionDelta(MorphTargetName);
for (int32 VertexIndex = 0; VertexIndex < MorphTargetLodData.MorphPositionDeltas.Num(); ++VertexIndex)
{
FVertexID VertexID(VertexIndex);
VertexPositionDeltas[VertexID] = MorphTargetLodData.MorphPositionDeltas[VertexIndex];
}
if (bHasNormal)
{
TVertexInstanceAttributesRef<FVector3f> VertexInstanceNormalDeltas = SkeletalMeshAttributes.GetVertexInstanceMorphNormalDelta(MorphTargetName);
for (int32 VertexInstanceIndex = 0; VertexInstanceIndex < MorphTargetLodData.MorphPositionDeltas.Num(); ++VertexInstanceIndex)
{
FVertexInstanceID VertexInstanceID(VertexInstanceIndex);
VertexInstanceNormalDeltas[VertexInstanceID] = MorphTargetLodData.MorphNormalDeltas[VertexInstanceIndex];
}
}
}
if (!GIsRunningUnattendedScript && !FApp::IsUnattended() && !RemovedImportedMorphTargetInfo.IsEmpty())
{
FTextBuilder ErrorMessage;
ErrorMessage.AppendLine(LOCTEXT("BadTopologyMorphRemoveMessage", "Morph target(s) removed because of topology changes:"));
ErrorMessage.Indent();
for (const TPair<FString, FMorphTargetImportedSourceFileInfo>& RemovedMorphTargetInfo : RemovedImportedMorphTargetInfo)
{
if (RemovedMorphTargetInfo.Value.IsGeneratedByEngine())
{
ErrorMessage.AppendLine(FText::Format(LOCTEXT("BadTopologyMorphRemoveMessagePerMorphGenerated", "{0} [LOD {1}] Generate by Engine tools"), FText::FromString(RemovedMorphTargetInfo.Key), FText::AsNumber(LodIndex)));
}
else
{
ErrorMessage.AppendLine(FText::Format(LOCTEXT("BadTopologyMorphRemoveMessagePerMorphCustomImport", "{0} [LOD {1}] SourceFile:{2}"), FText::FromString(RemovedMorphTargetInfo.Key), FText::AsNumber(LodIndex), FText::FromString(RemovedMorphTargetInfo.Value.GetSourceFilename())));
}
}
ErrorMessage.Unindent();
FMessageDialog::Open(EAppMsgType::Ok, ErrorMessage.ToText());
}
return bIsMeshDescriptionModified;
}
void FLODUtilities::AdjustImportDataFaceMaterialIndex(const TArray<FSkeletalMaterial>& Materials, TArray<SkeletalMeshImportData::FMaterial>& RawMeshMaterials, TArray<SkeletalMeshImportData::FMeshFace>& LODFaces, int32 LODIndex)
{
if ((Materials.Num() <= 1 && RawMeshMaterials.Num() <= 1) || LODIndex != 0)
{
//Nothing to fix if we have 1 or less material or we are not adjusting the base LOD
return;
}
//Fix the material for the faces
TArray<int32> MaterialRemap;
MaterialRemap.Reserve(RawMeshMaterials.Num());
//Optimization to avoid doing the remap if no material have to change
bool bNeedRemapping = false;
bool bFoundMaterial = false;
for (int32 MaterialIndex = 0; MaterialIndex < RawMeshMaterials.Num(); ++MaterialIndex)
{
MaterialRemap.Add(MaterialIndex);
FName MaterialImportName = *(RawMeshMaterials[MaterialIndex].MaterialImportName);
for (int32 MeshMaterialIndex = 0; MeshMaterialIndex < Materials.Num(); ++MeshMaterialIndex)
{
FName MeshMaterialName = Materials[MeshMaterialIndex].ImportedMaterialSlotName;
if (MaterialImportName == MeshMaterialName)
{
bNeedRemapping |= (MaterialRemap[MaterialIndex] != MeshMaterialIndex);
MaterialRemap[MaterialIndex] = MeshMaterialIndex;
bFoundMaterial = true;
break;
}
}
//A material slot was deleted by the user, we cannot remap anything in that case, the LODMaterialMap should be active for the LOD 0 and will match the sections
if (!bFoundMaterial)
{
return;
}
}
if (bNeedRemapping)
{
//Make sure the data is good before doing the change, We cannot do the remap if we
//have a bad synchronization between the face data and the Materials data.
for (int32 FaceIndex = 0; FaceIndex < LODFaces.Num(); ++FaceIndex)
{
if (!MaterialRemap.IsValidIndex(LODFaces[FaceIndex].MeshMaterialIndex))
{
return;
}
}
//Update all the faces
for (int32 FaceIndex = 0; FaceIndex < LODFaces.Num(); ++FaceIndex)
{
LODFaces[FaceIndex].MeshMaterialIndex = static_cast<uint16>(MaterialRemap[LODFaces[FaceIndex].MeshMaterialIndex]);
}
}
}
void FLODUtilities::MatchImportedMaterials(FLODUtilities::FSkeletalMeshMatchImportedMaterialsParameters& Parameters)
{
//Make sure we have valid parameters
if (!ensure(Parameters.SkeletalMesh))
{
UE_LOG(LogLODUtilities, Warning, TEXT("FLODUtilities::MatchImportedMaterials: Bad parameters, SkeletalMesh is null"));
return;
}
if (!ensure(Parameters.ImportedMaterials))
{
UE_ASSET_LOG(LogLODUtilities, Warning, Parameters.SkeletalMesh, TEXT("FLODUtilities::MatchImportedMaterials: Bad parameters, ImportedMaterials is null"));
return;
}
if (Parameters.bIsReImport)
{
if (!ensure(Parameters.ExistingOriginalPerSectionMaterialImportName))
{
UE_ASSET_LOG(LogLODUtilities, Warning, Parameters.SkeletalMesh, TEXT("FLODUtilities::MatchImportedMaterials: Bad parameters, ExistingOriginalPerSectionMaterialImportName is null"));
return;
}
if (Parameters.SkeletalMesh->GetMaterials().Num() == 0)
{
return;
}
}
FSkeletalMeshLODInfo* LodInfo = Parameters.SkeletalMesh->GetLODInfo(Parameters.LodIndex);
if (!LodInfo)
{
UE_ASSET_LOG(LogLODUtilities, Warning, Parameters.SkeletalMesh, TEXT("FLODUtilities::MatchImportedMaterials: LodIndex %d, LodInfo is null"), Parameters.LodIndex);
return;
}
//Grab some parameters pointer into references
const TArray<SkeletalMeshImportData::FMaterial>& ImportedMaterials = *Parameters.ImportedMaterials;
TArray<FSkeletalMaterial>& Materials = Parameters.SkeletalMesh->GetMaterials();
TMap<FName, int32> LODMaterialMapRedirection;
TMap<FName, FSkelMeshSourceSectionUserData> NewUserSectionsDataMap;
//If we reimport we have to keep the existing user section info data. We use the material name to match the existing
if (Parameters.bIsReImport)
{
const TArray<FName>& ExistingOriginalPerSectionMaterialImportName = *Parameters.ExistingOriginalPerSectionMaterialImportName;
auto GetImportMaterialSlotName = [&Parameters, &LodInfo](const FSkelMeshSection& Section
, int32 SectionIndex
, int32& OutMaterialIndex
, const TArray<FName>& PerSectionMaterialImportName)->FName
{
const TArray<FSkeletalMaterial>& MeshMaterials = Parameters.SkeletalMesh->GetMaterials();
if (!ensure(MeshMaterials.Num() > 0))
{
OutMaterialIndex = -1;
return NAME_None;
}
if (PerSectionMaterialImportName.IsValidIndex(SectionIndex))
{
for (int32 MaterialIndex = 0; MaterialIndex < MeshMaterials.Num(); ++MaterialIndex)
{
const FSkeletalMaterial& Material = MeshMaterials[MaterialIndex];
if (PerSectionMaterialImportName[SectionIndex] == Material.ImportedMaterialSlotName)
{
OutMaterialIndex = MaterialIndex;
break;
}
}
return PerSectionMaterialImportName[SectionIndex];
}
OutMaterialIndex = Section.MaterialIndex;
if (LodInfo->LODMaterialMap.IsValidIndex(SectionIndex) && LodInfo->LODMaterialMap[SectionIndex] != INDEX_NONE)
{
OutMaterialIndex = LodInfo->LODMaterialMap[SectionIndex];
}
FName ImportedMaterialSlotName = NAME_None;
if (MeshMaterials.IsValidIndex(OutMaterialIndex))
{
ImportedMaterialSlotName = MeshMaterials[OutMaterialIndex].ImportedMaterialSlotName;
}
else
{
ImportedMaterialSlotName = MeshMaterials[0].ImportedMaterialSlotName;
OutMaterialIndex = 0;
}
return ImportedMaterialSlotName;
};
FSkeletalMeshLODModel& ExistLODModel = Parameters.SkeletalMesh->GetImportedModel()->LODModels[Parameters.LodIndex];
for (int32 NewSectionIndex = 0; NewSectionIndex < ImportedMaterials.Num(); NewSectionIndex++)
{
FName ImportedMaterialName = *(ImportedMaterials[NewSectionIndex].MaterialImportName);
FSkelMeshSourceSectionUserData& SourceSectionUserData = NewUserSectionsDataMap.FindOrAdd(ImportedMaterialName);
for (int32 ExistSectionIndex = 0; ExistSectionIndex < ExistLODModel.Sections.Num(); ExistSectionIndex++)
{
if (ExistLODModel.Sections[ExistSectionIndex].ChunkedParentSectionIndex != INDEX_NONE)
{
continue;
}
const FSkelMeshSection& ExistingSection = ExistLODModel.Sections[ExistSectionIndex];
//Skip chunked section
if (ExistingSection.ChunkedParentSectionIndex != INDEX_NONE)
continue;
int32 ExistingMaterialIndex = 0;
FName ExistingImportedMaterialSlotName = GetImportMaterialSlotName(ExistingSection
, ExistingSection.OriginalDataSectionIndex
, ExistingMaterialIndex
, ExistingOriginalPerSectionMaterialImportName);
TArray<FName> EmptyArray;
int32 ExistingCurrentMaterialIndex = 0;
FName ExistingCurrentImportedMaterialSlotName = GetImportMaterialSlotName(ExistingSection
, ExistingSection.OriginalDataSectionIndex
, ExistingCurrentMaterialIndex
, EmptyArray);
auto CopySectionData = [&ExistingSection, &SourceSectionUserData]()
{
//Set the user section data to reflect the existing settings
SourceSectionUserData.bCastShadow = ExistingSection.bCastShadow;
SourceSectionUserData.bVisibleInRayTracing = ExistingSection.bVisibleInRayTracing;
SourceSectionUserData.bRecomputeTangent = ExistingSection.bRecomputeTangent;
SourceSectionUserData.RecomputeTangentsVertexMaskChannel = ExistingSection.RecomputeTangentsVertexMaskChannel;
SourceSectionUserData.bDisabled = ExistingSection.bDisabled;
SourceSectionUserData.GenerateUpToLodIndex = ExistingSection.GenerateUpToLodIndex;
};
if (ExistingImportedMaterialSlotName != NAME_None)
{
if (ImportedMaterialName == ExistingImportedMaterialSlotName)
{
CopySectionData();
//If user has edit the material slot pointer, store the data so we can modify the LODMaterialMap
if (ExistingCurrentImportedMaterialSlotName != ExistingImportedMaterialSlotName)
{
LODMaterialMapRedirection.FindOrAdd(ImportedMaterialName) = ExistingCurrentMaterialIndex;
}
break;
}
}
else if (Parameters.SkeletalMesh->GetMaterials().IsValidIndex(ExistingCurrentMaterialIndex) &&
ImportedMaterials[NewSectionIndex].Material.Get() == Parameters.SkeletalMesh->GetMaterials()[ExistingCurrentMaterialIndex].MaterialInterface.Get()) //Use material slot compare to match in case the name is none
{
CopySectionData();
break;
}
}
}
}
LodInfo->LODMaterialMap.Empty();
// Now set up the material mapping array.
if (Parameters.LodIndex != 0)
{
for (int32 ImportedMaterialIndex = 0; ImportedMaterialIndex < ImportedMaterials.Num(); ImportedMaterialIndex++)
{
int32 LODMatIndex = INDEX_NONE;
FName ImportedMaterialName = *(ImportedMaterials[ImportedMaterialIndex].MaterialImportName);
//If the current lod section material slot was edited, we must keep the edition.
if (int32* MaterialRedirection = LODMaterialMapRedirection.Find(ImportedMaterialName))
{
LODMatIndex = *MaterialRedirection;
}
else
{
//Match by name
for (int32 MaterialIndex = 0; MaterialIndex < Materials.Num(); ++MaterialIndex)
{
const FSkeletalMaterial& SkeletalMaterial = Materials[MaterialIndex];
if (SkeletalMaterial.ImportedMaterialSlotName != NAME_None && SkeletalMaterial.ImportedMaterialSlotName == ImportedMaterialName)
{
LODMatIndex = MaterialIndex;
break;
}
}
// If we don't have a match, add a new entry to the material list.
if (LODMatIndex == INDEX_NONE)
{
LODMatIndex = Materials.Add(FSkeletalMaterial(ImportedMaterials[ImportedMaterialIndex].Material.Get(), true, false, ImportedMaterialName, ImportedMaterialName));
}
}
LodInfo->LODMaterialMap.Add(LODMatIndex);
}
}
else if (LODMaterialMapRedirection.Num() > 0)
{
for (int32 ExistMaterialIndex = 0; ExistMaterialIndex < Materials.Num(); ExistMaterialIndex++)
{
int32 LODMatIndex = INDEX_NONE;
FName ExistMaterialName = Materials[ExistMaterialIndex].ImportedMaterialSlotName;
//If the current lod section material slot was edited, we must keep the edition.
if (int32* MaterialRedirection = LODMaterialMapRedirection.Find(ExistMaterialName))
{
LODMatIndex = *MaterialRedirection;
}
LodInfo->LODMaterialMap.Add(LODMatIndex);
}
}
if (Parameters.bIsReImport)
{
FSkeletalMeshLODModel& ExistLODModel = Parameters.SkeletalMesh->GetImportedModel()->LODModels[Parameters.LodIndex];
for (int32 NewSectionIndex = 0; NewSectionIndex < ImportedMaterials.Num(); NewSectionIndex++)
{
FName ImportedMaterialName = *(ImportedMaterials[NewSectionIndex].MaterialImportName);
if (!NewUserSectionsDataMap.Contains(ImportedMaterialName))
{
NewUserSectionsDataMap.Add(ImportedMaterialName, FSkelMeshSourceSectionUserData());
}
}
if (Parameters.LodIndex == 0)
{
//Since LOD 0 data will be re-arrange to follow the Material list order, sort the map accordingly.
NewUserSectionsDataMap.KeySort([&Materials](const FName& A, const FName& B)
{
int32 MatAIndex = INDEX_NONE;
int32 MatBIndex = INDEX_NONE;
for (int32 MaterialIndex = 0; MaterialIndex < Materials.Num(); ++MaterialIndex)
{
if (Materials[MaterialIndex].ImportedMaterialSlotName == A)
{
MatAIndex = MaterialIndex;
}
if (Materials[MaterialIndex].ImportedMaterialSlotName == B)
{
MatBIndex = MaterialIndex;
}
}
if (MatBIndex == INDEX_NONE)
{
return true;
}
if (MatAIndex == INDEX_NONE)
{
return false;
}
return MatAIndex < MatBIndex;
});
}
//If the caller provide a CustomImportedLODModel we must apply the user section data to the provided lod model.
if (Parameters.CustomImportedLODModel)
{
Parameters.CustomImportedLODModel->UserSectionsData.Reset();
int32 RemapSectionIndex = 0;
for (TPair<FName, FSkelMeshSourceSectionUserData> UserSectionsData : NewUserSectionsDataMap)
{
Parameters.CustomImportedLODModel->UserSectionsData.Add(RemapSectionIndex++, UserSectionsData.Value);
}
Parameters.CustomImportedLODModel->SyncronizeUserSectionsDataArray(true);
}
else
{
ExistLODModel.UserSectionsData.Reset();
TBitArray<> MatchedMaterial;
MatchedMaterial.Init(false, Materials.Num());
TSet<FName> MatchedUserSections;
MatchedUserSections.Reserve(NewUserSectionsDataMap.Num());
for (TPair<FName, FSkelMeshSourceSectionUserData> UserSectionsData : NewUserSectionsDataMap)
{
for (int32 MaterialIndex = 0; MaterialIndex < Materials.Num(); ++MaterialIndex)
{
if (Materials[MaterialIndex].ImportedMaterialSlotName == UserSectionsData.Key)
{
MatchedMaterial[MaterialIndex] = true;
MatchedUserSections.Add(UserSectionsData.Key);
ExistLODModel.UserSectionsData.Add(MaterialIndex, UserSectionsData.Value);
break;
}
}
}
//Add any missing user section data
for (TPair<FName, FSkelMeshSourceSectionUserData> UserSectionsData : NewUserSectionsDataMap)
{
if (!MatchedUserSections.Contains(UserSectionsData.Key))
{
for (int32 MaterialIndex = 0; MaterialIndex < Materials.Num(); ++MaterialIndex)
{
if (!MatchedMaterial[MaterialIndex])
{
MatchedMaterial[MaterialIndex] = true;
ExistLODModel.UserSectionsData.Add(MaterialIndex, UserSectionsData.Value);
break;
}
}
}
}
//Reorder the user section from the keys
ExistLODModel.UserSectionsData.KeySort([](const int32& A, const int32& B)
{
return A < B;
});
}
}
}
void FLODUtilities::ReorderMaterialSlotToBaseLod(USkeletalMesh* SkeletalMesh)
{
//Make sure we have valid parameters
if (!ensure(SkeletalMesh))
{
UE_ASSET_LOG(LogLODUtilities, Warning, SkeletalMesh, TEXT("FLODUtilities::ReorderMaterialSlotToBaseLod: Bad parameters, SkeletalMesh is null"));
return;
}
TArray<FSkeletalMaterial>& Materials = SkeletalMesh->GetMaterials();
if (Materials.Num() < 2)
{
return;
}
TArray<int32> MaterialSlotRemap;
MaterialSlotRemap.AddUninitialized(Materials.Num());
for (int32 MaterialIndex = 0; MaterialIndex < Materials.Num(); ++MaterialIndex)
{
MaterialSlotRemap[MaterialIndex] = INDEX_NONE;
}
TArray<FSkeletalMaterial> ReorderMaterialArray;
ReorderMaterialArray.Reserve(Materials.Num());
//Unique imported material list for all lods
TArray<FName> AllLodSortedImportedMaterials;
AllLodSortedImportedMaterials.Reserve(Materials.Num());
TBitArray<> MatchedMaterials;
MatchedMaterials.Init(false, Materials.Num());
for (int32 LodIndex = 0; LodIndex < SkeletalMesh->GetLODNum(); ++LodIndex)
{
if (!SkeletalMesh->HasMeshDescription(LodIndex))
{
if (LodIndex == 0)
{
UE_ASSET_LOG(LogLODUtilities, Warning, SkeletalMesh, TEXT("FLODUtilities::ReorderMaterialSlotToBaseLod: Skeletal mesh invalid import data for LOD 0"));
return;
}
continue;
}
FSkeletalMeshImportData LodImportData;
PRAGMA_DISABLE_DEPRECATION_WARNINGS
SkeletalMesh->LoadLODImportedData(LodIndex, LodImportData);
PRAGMA_ENABLE_DEPRECATION_WARNINGS
const TArray<SkeletalMeshImportData::FMaterial>& ImportedMaterials = LodImportData.Materials;
for (int32 ImportedMaterialIndex = 0; ImportedMaterialIndex < ImportedMaterials.Num(); ++ImportedMaterialIndex)
{
FName ImportedMaterialSlotName = FName(*ImportedMaterials[ImportedMaterialIndex].MaterialImportName);
//add Unique Material
if (!AllLodSortedImportedMaterials.Contains(ImportedMaterialSlotName))
{
AllLodSortedImportedMaterials.Add(ImportedMaterialSlotName);
//Make sure matched material is filled
for (int32 MaterialIndex = 0; MaterialIndex < Materials.Num(); ++MaterialIndex)
{
FName MaterialSlotName = Materials[MaterialIndex].ImportedMaterialSlotName;
if (MaterialSlotName == ImportedMaterialSlotName)
{
MatchedMaterials[MaterialIndex] = true;
break;
}
}
}
}
}
//Now fill the ReorderMaterialArray using the AllLodSortedImportedMaterials order
for (int32 ImportedMaterialIndex = 0; ImportedMaterialIndex < AllLodSortedImportedMaterials.Num(); ++ImportedMaterialIndex)
{
FName ImportedMaterialSlotName = AllLodSortedImportedMaterials[ImportedMaterialIndex];
int32 TargetMaterialIndex = INDEX_NONE;
for (int32 MaterialIndex = 0; MaterialIndex < Materials.Num(); ++MaterialIndex)
{
//If we have a match we should find back the material index and assign it to the target material index
if (MatchedMaterials[MaterialIndex])
{
FName MaterialSlotName = Materials[MaterialIndex].ImportedMaterialSlotName;
if (MaterialSlotName == ImportedMaterialSlotName)
{
TargetMaterialIndex = MaterialIndex;
break;
}
}
}
//If we have an unmatched imported material we use the next unmatched material in the original list
if (TargetMaterialIndex == INDEX_NONE)
{
for (int32 MaterialIndex = 0; MaterialIndex < Materials.Num(); ++MaterialIndex)
{
if (MatchedMaterials[MaterialIndex])
{
continue;
}
ensure(MaterialSlotRemap[MaterialIndex] == INDEX_NONE);
TargetMaterialIndex = MaterialIndex;
MatchedMaterials[MaterialIndex] = true;
break;
}
}
//We should have found a valid material index at this point
if (ensure(MaterialSlotRemap.IsValidIndex(TargetMaterialIndex)))
{
if (MaterialSlotRemap[TargetMaterialIndex] == INDEX_NONE)
{
MaterialSlotRemap[TargetMaterialIndex] = ReorderMaterialArray.Add(Materials[TargetMaterialIndex]);
}
}
}
//Unmatched material are appended to the end of the material slots
for (int32 MaterialIndex = 0; MaterialIndex < Materials.Num(); ++MaterialIndex)
{
if (MaterialSlotRemap[MaterialIndex] == INDEX_NONE)
{
MaterialSlotRemap[MaterialIndex] = ReorderMaterialArray.Add(Materials[MaterialIndex]);
}
}
check(ReorderMaterialArray.Num() == Materials.Num());
//Reorder the skeletal mesh material slot array
SkeletalMesh->SetMaterials(ReorderMaterialArray);
//We now need to adjust all LODs data to fit the re-order
for (int32 LodIndex = 0; LodIndex < SkeletalMesh->GetLODNum(); ++LodIndex)
{
if (ensure(SkeletalMesh->GetImportedModel()->LODModels.IsValidIndex(LodIndex)))
{
FSkeletalMeshLODModel& LODModel = SkeletalMesh->GetImportedModel()->LODModels[LodIndex];
TMap<int32, FSkelMeshSourceSectionUserData> RemapSectionsDataMap;
for (TPair<int32, FSkelMeshSourceSectionUserData>& UserSectionDataPair : LODModel.UserSectionsData)
{
RemapSectionsDataMap.Add(MaterialSlotRemap[UserSectionDataPair.Key], UserSectionDataPair.Value);
}
//Sort the remap section so its in the proper order. Optional but easier to debug ordered data
RemapSectionsDataMap.KeySort([](const int32& A, const int32& B)
{
return A < B;
});
//Rebuild a remap with the new order starting from 0 to follow the section order
LODModel.UserSectionsData.Empty(RemapSectionsDataMap.Num());
int32 KeyIndex = 0;
for (TPair<int32, FSkelMeshSourceSectionUserData>& UserSectionDataPair : RemapSectionsDataMap)
{
LODModel.UserSectionsData.Add(KeyIndex++, UserSectionDataPair.Value);
}
}
else
{
UE_ASSET_LOG(LogLODUtilities, Warning, SkeletalMesh, TEXT("FLODUtilities::ReorderMaterialSlotToBaseLod: Skeletal mesh invalid LODModel %d"), LodIndex);
}
//Remap the LODMaterialMap
if (FSkeletalMeshLODInfo* LodInfo = SkeletalMesh->GetLODInfo(LodIndex))
{
for (int32& MaterialMap : LodInfo->LODMaterialMap)
{
if (MaterialSlotRemap.IsValidIndex(MaterialMap))
{
MaterialMap = MaterialSlotRemap[MaterialMap];
}
}
}
else
{
UE_ASSET_LOG(LogLODUtilities, Warning, SkeletalMesh, TEXT("FLODUtilities::ReorderMaterialSlotToBaseLod: Skeletal mesh invalid LODInfo %d"), LodIndex);
}
}
}
void FLODUtilities::RemoveUnusedMaterialSlot(USkeletalMesh* SkeletalMesh)
{
if (!SkeletalMesh || !SkeletalMesh->HasMeshDescription(0))
{
return;
}
TArray<FSkeletalMaterial>& Materials = SkeletalMesh->GetMaterials();
if (Materials.Num() < 2)
{
return;
}
FSkeletalMeshModel* ImportedResource = SkeletalMesh->GetImportedModel();
TArray<int32> UsedIndexes;
for (int32 LodIndex = 0, LODNum = SkeletalMesh->GetLODNum(); LodIndex < LODNum; ++LodIndex)
{
const TArray<int32>& LODMaterialMap = SkeletalMesh->GetLODInfo(LodIndex)->LODMaterialMap;
if (!LODMaterialMap.IsEmpty())
{
for (int32 MaterialIndex : LODMaterialMap)
{
if (MaterialIndex != INDEX_NONE)
{
UsedIndexes.AddUnique(MaterialIndex);
}
}
}
if (LodIndex == 0 && ImportedResource && ImportedResource->LODModels.IsValidIndex(LodIndex))
{
//Since LOD 0 is always reordering the material slot array, we expect section index to match UserSectionsData order
//We cannot use the section since section are built by the DDC cache.
FSkeletalMeshLODModel& Model = ImportedResource->LODModels[LodIndex];
for (TPair<int32, FSkelMeshSourceSectionUserData> UserSectionsData : Model.UserSectionsData)
{
if (LODMaterialMap.IsEmpty() || !LODMaterialMap.IsValidIndex(UserSectionsData.Key) || LODMaterialMap[UserSectionsData.Key] == INDEX_NONE)
{
UsedIndexes.AddUnique(UserSectionsData.Key);
}
}
}
}
//Clean up the trailing unused material
for (int32 MaterialIndex = Materials.Num() - 1; MaterialIndex >= 0; MaterialIndex--)
{
if (UsedIndexes.Contains(MaterialIndex))
{
//Only delete extra material at the end of the material slot list, because MaterialMap and UserSectionData will need to be patch otherwise
break;
}
Materials.RemoveAt(MaterialIndex);
}
}
namespace TriangleStripHelper
{
struct FTriangle2D
{
FVector2D Vertices[3];
};
bool IntersectTriangleAndAABB(const FTriangle2D& Triangle, const FBox2D& Box)
{
FBox2D TriangleBox(Triangle.Vertices[0], Triangle.Vertices[0]);
TriangleBox += Triangle.Vertices[1];
TriangleBox += Triangle.Vertices[2];
auto IntersectBoxes = [&TriangleBox, &Box]()-> bool
{
if ((FMath::RoundToInt(TriangleBox.Min.X) >= FMath::RoundToInt(Box.Max.X)) || (FMath::RoundToInt(Box.Min.X) >= FMath::RoundToInt(TriangleBox.Max.X)))
{
return false;
}
if ((FMath::RoundToInt(TriangleBox.Min.Y) >= FMath::RoundToInt(Box.Max.Y)) || (FMath::RoundToInt(Box.Min.Y) >= FMath::RoundToInt(TriangleBox.Max.Y)))
{
return false;
}
return true;
};
//If the triangle box do not intersect, return false
if (!IntersectBoxes())
{
return false;
}
auto IsInsideBox = [&Box](const FVector2D& TestPoint)->bool
{
return ((FMath::RoundToInt(TestPoint.X) >= FMath::RoundToInt(Box.Min.X)) &&
(FMath::RoundToInt(TestPoint.X) <= FMath::RoundToInt(Box.Max.X)) &&
(FMath::RoundToInt(TestPoint.Y) >= FMath::RoundToInt(Box.Min.Y)) &&
(FMath::RoundToInt(TestPoint.Y) <= FMath::RoundToInt(Box.Max.Y)) );
};
if( IsInsideBox(Triangle.Vertices[0]) ||
IsInsideBox(Triangle.Vertices[1]) ||
IsInsideBox(Triangle.Vertices[2]) )
{
return true;
}
auto SegmentIntersection2D = [](const FVector2D & SegmentStartA, const FVector2D & SegmentEndA, const FVector2D & SegmentStartB, const FVector2D & SegmentEndB)
{
const FVector2D VectorA = SegmentEndA - SegmentStartA;
const FVector2D VectorB = SegmentEndB - SegmentStartB;
const double S = (-VectorA.Y * (SegmentStartA.X - SegmentStartB.X) + VectorA.X * (SegmentStartA.Y - SegmentStartB.Y)) / (-VectorB.X * VectorA.Y + VectorA.X * VectorB.Y);
const double T = (VectorB.X * (SegmentStartA.Y - SegmentStartB.Y) - VectorB.Y * (SegmentStartA.X - SegmentStartB.X)) / (-VectorB.X * VectorA.Y + VectorA.X * VectorB.Y);
return (S >= 0 && S <= 1 && T >= 0 && T <= 1);
};
auto IsInsideTriangle = [&Triangle, &SegmentIntersection2D, &Box, &TriangleBox](const FVector2D& TestPoint)->bool
{
double Extent = (2.0 * Box.GetSize().Size()) + (2.0 * TriangleBox.GetSize().Size());
FVector2D TestPointExtend(Extent, Extent);
int32 IntersectionCount = SegmentIntersection2D(Triangle.Vertices[0], Triangle.Vertices[1], TestPoint, TestPoint + TestPointExtend) ? 1 : 0;
IntersectionCount += SegmentIntersection2D(Triangle.Vertices[1], Triangle.Vertices[2], TestPoint, TestPoint + TestPointExtend) ? 1 : 0;
IntersectionCount += SegmentIntersection2D(Triangle.Vertices[2], Triangle.Vertices[0], TestPoint, TestPoint + TestPointExtend) ? 1 : 0;
return (IntersectionCount == 1);
};
if (IsInsideTriangle(Box.Min) ||
IsInsideTriangle(Box.Max) ||
IsInsideTriangle(FVector2D(Box.Min.X, Box.Max.Y)) ||
IsInsideTriangle(FVector2D(Box.Max.X, Box.Min.Y)))
{
return true;
}
auto IsTriangleEdgeIntersectBoxEdges = [&SegmentIntersection2D, &Box]( const FVector2D& EdgeStart, const FVector2D& EdgeEnd)->bool
{
//Triangle Edges 0-1 intersection with box
if( SegmentIntersection2D(EdgeStart, EdgeEnd, Box.Min, FVector2D(Box.Min.X, Box.Max.Y)) ||
SegmentIntersection2D(EdgeStart, EdgeEnd, Box.Max, FVector2D(Box.Min.X, Box.Max.Y)) ||
SegmentIntersection2D(EdgeStart, EdgeEnd, Box.Max, FVector2D(Box.Max.X, Box.Min.Y)) ||
SegmentIntersection2D(EdgeStart, EdgeEnd, Box.Min, FVector2D(Box.Max.X, Box.Min.Y)) )
{
return true;
}
return false;
};
if( IsTriangleEdgeIntersectBoxEdges(Triangle.Vertices[0], Triangle.Vertices[1]) ||
IsTriangleEdgeIntersectBoxEdges(Triangle.Vertices[1], Triangle.Vertices[2]) ||
IsTriangleEdgeIntersectBoxEdges(Triangle.Vertices[2], Triangle.Vertices[0]))
{
return true;
}
return false;
}
} //End namespace TriangleStripHelper
bool FLODUtilities::StripLODGeometry(USkeletalMesh* SkeletalMesh, const int32 LODIndex, UTexture2D* TextureMask, const float Threshold)
{
if (LODIndex < 0 || LODIndex >= SkeletalMesh->GetLODNum() || !SkeletalMesh->GetImportedModel() || !SkeletalMesh->GetImportedModel()->LODModels.IsValidIndex(LODIndex) || !TextureMask)
{
UE_LOG(LogLODUtilities, Warning, TEXT("Cannot strip triangle for skeletalmesh %s LOD %d."), *SkeletalMesh->GetPathName(), LODIndex);
return false;
}
//Grab the reference data
FSkeletalMeshLODModel& LODModel = SkeletalMesh->GetImportedModel()->LODModels[LODIndex];
const FSkeletalMeshLODInfo& LODInfo = *(SkeletalMesh->GetLODInfo(LODIndex));
const bool bIsReductionActive = SkeletalMesh->IsReductionActive(LODIndex);
if (bIsReductionActive && LODInfo.ReductionSettings.BaseLOD < LODIndex)
{
//No need to strip if the LOD is reduce using another LOD as the source data
UE_LOG(LogLODUtilities, Warning, TEXT("Cannot strip triangle for skeletalmesh %s LOD %d. Because this LOD is generated, strip the source instead."), *SkeletalMesh->GetPathName(), LODIndex);
return false;
}
//Check the texture mask source data, it must be valid
FTextureSource& InitialSource = TextureMask->Source;
const int32 ResX = InitialSource.GetSizeX();
const int32 ResY = InitialSource.GetSizeY();
const int32 FormatDataSize = InitialSource.GetBytesPerPixel();
if (FormatDataSize <= 0)
{
UE_LOG(LogLODUtilities, Warning, TEXT("Cannot strip triangle for skeletalmesh %s LOD %d. Because the texture format size is 0."), *SkeletalMesh->GetPathName(), LODIndex);
return false;
}
ETextureSourceFormat SourceFormat = InitialSource.GetFormat();
if (SourceFormat <= TSF_Invalid || SourceFormat >= TSF_MAX)
{
UE_LOG(LogLODUtilities, Warning, TEXT("Cannot strip triangle for skeletalmesh %s LOD %d. Because the texture format is invalid."), *SkeletalMesh->GetPathName(), LODIndex);
return false;
}
TArray64<uint8> Ref2DData;
if (!InitialSource.GetMipData(Ref2DData, 0, nullptr))
{
UE_LOG(LogLODUtilities, Warning, TEXT("Cannot strip triangle for skeletalmesh %s LOD %d. Because the texture data cannot be extracted."), *SkeletalMesh->GetPathName(), LODIndex);
return false;
}
//Post edit change scope
{
FScopedSkeletalMeshPostEditChange ScopePostEditChange(SkeletalMesh);
//This is like a re-import, we must force to use a new DDC
SkeletalMesh->InvalidateDeriveDataCacheGUID();
const bool bBuildAvailable = SkeletalMesh->HasMeshDescription(LODIndex);
FSkeletalMeshImportData ImportedData;
//Get the imported data if available
if (bBuildAvailable)
{
PRAGMA_DISABLE_DEPRECATION_WARNINGS
SkeletalMesh->LoadLODImportedData(LODIndex, ImportedData);
PRAGMA_ENABLE_DEPRECATION_WARNINGS
}
SkeletalMesh->Modify();
ERawImageFormat::Type RawFormat = FImageCoreUtils::ConvertToRawImageFormat(SourceFormat);
bool bSRGB = TextureMask->SRGB;
auto ShouldStripTriangle = [&](const FVector2D& UvA, const FVector2D& UvB, const FVector2D& UvC)->bool
{
const FIntVector2 PixelUvA(FMath::FloorToInt32(UvA.X * ResX) % (ResX + 1), FMath::FloorToInt32(UvA.Y * ResY) % (ResY + 1));
const FIntVector2 PixelUvB(FMath::FloorToInt32(UvB.X * ResX) % (ResX + 1), FMath::FloorToInt32(UvB.Y * ResY) % (ResY + 1));
const FIntVector2 PixelUvC(FMath::FloorToInt32(UvC.X * ResX) % (ResX + 1), FMath::FloorToInt32(UvC.Y * ResY) % (ResY + 1));
const int32 MinU = FMath::Clamp(FMath::Min3<int32>(PixelUvA.X, PixelUvB.X, PixelUvC.X), 0, ResX);
const int32 MinV = FMath::Clamp(FMath::Min3<int32>(PixelUvA.Y, PixelUvB.Y, PixelUvC.Y), 0, ResY);
const int32 MaxU = FMath::Clamp(FMath::Max3<int32>(PixelUvA.X, PixelUvB.X, PixelUvC.X), 0, ResX);
const int32 MaxV = FMath::Clamp(FMath::Max3<int32>(PixelUvA.Y, PixelUvB.Y, PixelUvC.Y), 0, ResY);
//Do not read the alpha value when testing the texture value
auto IsPixelZero = [&](int32 PosX, int32 PosY) -> bool
{
const int32 RefPos = PosX + (PosY * InitialSource.GetSizeX());
const void * PixelPtr = Ref2DData.GetData() + RefPos * FormatDataSize;
const FLinearColor Color = ERawImageFormat::GetOnePixelLinear(PixelPtr,RawFormat,bSRGB);
const bool bPixelIsZero =
FMath::IsNearlyZero(Color.R,Threshold) &&
FMath::IsNearlyZero(Color.G,Threshold) &&
FMath::IsNearlyZero(Color.B,Threshold);
return bPixelIsZero;
};
//Triangle smaller or equal to one pixel just need to test the pixel color value
if (MinU == MaxU || MinV == MaxV)
{
return IsPixelZero(MinU, MinV);
}
for (int32 PosY = MinV; PosY < MaxV; ++PosY)
{
for (int32 PosX = MinU; PosX < MaxU; ++PosX)
{
const bool bStripPixel = IsPixelZero(PosX, PosY);
//if any none zeroed pixel intersect the triangle, prevent stripping of this triangle
if (!bStripPixel)
{
FVector2D StartPixel(PosX, PosY);
FVector2D EndPixel(PosX + 1, PosY + 1);
FBox2D Box2D(StartPixel, EndPixel);
//Test if the triangle UV touch this pixel
TriangleStripHelper::FTriangle2D Triangle;
Triangle.Vertices[0] = FVector2D(PixelUvA.X, PixelUvA.Y);
Triangle.Vertices[1] = FVector2D(PixelUvB.X, PixelUvB.Y);
Triangle.Vertices[2] = FVector2D(PixelUvC.X, PixelUvC.Y);
if(TriangleStripHelper::IntersectTriangleAndAABB(Triangle, Box2D))
{
return false;
}
}
}
}
return true;
};
const TArray< uint32 >& SoftVertexIndexToImportDataPointIndex = LODModel.GetRawPointIndices();
TMap<uint64, TArray<int32>> OptimizedFaceFinder;
auto GetMatchFaceIndex = [&OptimizedFaceFinder, &ImportedData](const int32 FaceVertexA, const int32 FaceVertexB, int32 FaceVertexC)->int32
{
uint64 Key = (uint64)FaceVertexA | ((uint64)FaceVertexB >> 32) | (((uint64)FaceVertexC & 0xFFFF) >> 48);
TArray<int32>& FaceIndices = OptimizedFaceFinder.FindChecked(Key);
for (int32 PossibleFaceIndex = 0; PossibleFaceIndex < FaceIndices.Num(); ++PossibleFaceIndex)
{
int32 FaceIndex = FaceIndices[PossibleFaceIndex];
const SkeletalMeshImportData::FTriangle& Face = ImportedData.Faces[FaceIndex];
if (FaceVertexA == ImportedData.Wedges[Face.WedgeIndex[0]].VertexIndex)
{
if (FaceVertexB == ImportedData.Wedges[Face.WedgeIndex[1]].VertexIndex)
{
if (FaceVertexC == ImportedData.Wedges[Face.WedgeIndex[2]].VertexIndex)
{
return FaceIndex;
}
}
}
}
return INDEX_NONE;
};
for (int32 FaceIndex = 0; FaceIndex < ImportedData.Faces.Num(); ++FaceIndex)
{
const SkeletalMeshImportData::FTriangle& Face = ImportedData.Faces[FaceIndex];
int32 FaceVertexA = ImportedData.Wedges[Face.WedgeIndex[0]].VertexIndex;
int32 FaceVertexB = ImportedData.Wedges[Face.WedgeIndex[1]].VertexIndex;
int32 FaceVertexC = ImportedData.Wedges[Face.WedgeIndex[2]].VertexIndex;
uint64 Key = (uint64)FaceVertexA | ((uint64)FaceVertexB >> 32) | (((uint64)FaceVertexC & 0xFFFF) >> 48);
TArray<int32>& FaceIndices = OptimizedFaceFinder.FindOrAdd(Key);
FaceIndices.Add(FaceIndex);
}
int32 RemovedFaceCount = 0;
TBitArray<> FaceToRemove;
FaceToRemove.Init(false, ImportedData.Faces.Num());
int32 NumTriangleIndex = LODModel.IndexBuffer.Num();
for (int32 TriangleIndex = NumTriangleIndex - 1; TriangleIndex >= 0; TriangleIndex -= 3)
{
int32 VertexIndexA = LODModel.IndexBuffer[TriangleIndex - 2];
int32 VertexIndexB = LODModel.IndexBuffer[TriangleIndex - 1];
int32 VertexIndexC = LODModel.IndexBuffer[TriangleIndex];
int32 SectionIndex;
int32 SectionVertexIndexA;
int32 SectionVertexIndexB;
int32 SectionVertexIndexC;
LODModel.GetSectionFromVertexIndex(VertexIndexA, SectionIndex, SectionVertexIndexA);
LODModel.GetSectionFromVertexIndex(VertexIndexB, SectionIndex, SectionVertexIndexB);
LODModel.GetSectionFromVertexIndex(VertexIndexC, SectionIndex, SectionVertexIndexC);
FSkelMeshSection& Section = LODModel.Sections[SectionIndex];
//Get the UV triangle, add the small number that will act like threshold when converting the UV into pixel coordinate.
FVector2D UvA = FVector2D(Section.SoftVertices[SectionVertexIndexA].UVs[0]) + KINDA_SMALL_NUMBER;
FVector2D UvB = FVector2D(Section.SoftVertices[SectionVertexIndexB].UVs[0]) + KINDA_SMALL_NUMBER;
FVector2D UvC = FVector2D(Section.SoftVertices[SectionVertexIndexC].UVs[0]) + KINDA_SMALL_NUMBER;
if (ShouldStripTriangle(UvA, UvB, UvC))
{
//Find the face in the imported data
if (bBuildAvailable)
{
//Findback the face in the import data
int32 ImportedPointIndexA = SoftVertexIndexToImportDataPointIndex[VertexIndexA];
int32 ImportedPointIndexB = SoftVertexIndexToImportDataPointIndex[VertexIndexB];
int32 ImportedPointIndexC = SoftVertexIndexToImportDataPointIndex[VertexIndexC];
int32 FaceIndex = GetMatchFaceIndex(ImportedPointIndexA, ImportedPointIndexB, ImportedPointIndexC);
if (FaceIndex != INDEX_NONE)
{
if (!FaceToRemove[FaceIndex])
{
FaceToRemove[FaceIndex] = true;
RemovedFaceCount++;
}
}
}
else
{
//Remove the source model vertex if there is no build data
LODModel.IndexBuffer.RemoveAt(TriangleIndex - 2, 3, EAllowShrinking::No);
}
}
}
if(bBuildAvailable && RemovedFaceCount > 0)
{
//Recreate a new imported data with only the remaining faces
FSkeletalMeshImportData StrippedImportedData;
StrippedImportedData = ImportedData;
StrippedImportedData.Faces.Reset();
StrippedImportedData.Wedges.Reset();
StrippedImportedData.Points.Reset();
StrippedImportedData.PointToRawMap.Reset();
StrippedImportedData.Influences.Reset();
TArray<int32> RemapVertexIndex;
RemapVertexIndex.AddZeroed(ImportedData.Points.Num());
for (int32 VertexIndex = 0; VertexIndex < ImportedData.Points.Num(); ++VertexIndex)
{
RemapVertexIndex[VertexIndex] = INDEX_NONE;
}
StrippedImportedData.Faces.AddDefaulted(ImportedData.Faces.Num() - RemovedFaceCount);
StrippedImportedData.Wedges.AddDefaulted(StrippedImportedData.Faces.Num()*3);
int32 NewFaceIndex = 0;
int32 NewWedgeIndex = 0;
for (int32 FaceIndex = 0; FaceIndex < ImportedData.Faces.Num(); ++FaceIndex)
{
//Skip removed faces
if (FaceToRemove[FaceIndex])
{
continue;
}
SkeletalMeshImportData::FTriangle& NewFace = StrippedImportedData.Faces[NewFaceIndex++];
NewFace = ImportedData.Faces[FaceIndex];
for(int32 FaceWedgeIndex = 0; FaceWedgeIndex < 3; ++FaceWedgeIndex)
{
SkeletalMeshImportData::FVertex& NewWedge = StrippedImportedData.Wedges[NewWedgeIndex];
NewWedge = ImportedData.Wedges[NewFace.WedgeIndex[FaceWedgeIndex]];
NewFace.WedgeIndex[FaceWedgeIndex] = NewWedgeIndex;
int32 VertexIndex = NewWedge.VertexIndex;
if(RemapVertexIndex[VertexIndex] == INDEX_NONE)
{
StrippedImportedData.PointToRawMap.Add(ImportedData.PointToRawMap[VertexIndex]);
NewWedge.VertexIndex = StrippedImportedData.Points.Add(ImportedData.Points[VertexIndex]);
RemapVertexIndex[VertexIndex] = NewWedge.VertexIndex;
}
else
{
NewWedge.VertexIndex = RemapVertexIndex[VertexIndex];
}
NewWedgeIndex++;
}
}
//Fix the influences with the RemapVertexIndex
for (int32 InfluenceIndex = 0; InfluenceIndex < ImportedData.Influences.Num(); ++InfluenceIndex)
{
int32 VertexIndex = ImportedData.Influences[InfluenceIndex].VertexIndex;
int32 RemappedVertexIndex = RemapVertexIndex[VertexIndex];
if(RemappedVertexIndex != INDEX_NONE)
{
SkeletalMeshImportData::FRawBoneInfluence& Influence = StrippedImportedData.Influences.Add_GetRef(ImportedData.Influences[InfluenceIndex]);
Influence.VertexIndex = RemapVertexIndex[VertexIndex];
}
}
PRAGMA_DISABLE_DEPRECATION_WARNINGS
SkeletalMesh->SaveLODImportedData(LODIndex, StrippedImportedData);
PRAGMA_ENABLE_DEPRECATION_WARNINGS
}
}
return true;
}
#undef LOCTEXT_NAMESPACE // "LODUtilities"
#endif //WITH_EDITOR