You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
Phase 1 adds the async compilation without introducing any higher-level awareness. This step provides the biggest performance improvements by allowing skeletal mesh to be compiled in multiple threads but stalls are expected to occur before the level has finished loading as soon as properties of skeletal meshes are accessed by the game-thread. - Make skeletal mesh reduction and renderdata building thread-safe where needed - Add protection on all USkeletalMesh accessors so the game-thread can wait on data still being built if needed - Modify both PostLoad and Build to be async when the feature is enabled - Make FRawSkeletalMeshBulkData::LoadRawMesh thread-safe - Fix some PropertyChangedEvent name comparison to use strongly typed GET_MEMBER_NAME_CHECKED - Add an experimental setting options to enable the feature (disabled by default for now) DEBUGGING - Can be forcibly enabled/disabled through command-line via -asyncskeletalmeshcompilation=[off, on, paused] - Can pause skeletal mesh compilation using Editor.AsyncSkeletalMeshCompilation = 2 or -asyncskeletalmeshcompilation=paused - Can manually resume a specified amount of paused compilation using Editor.AsyncSkeletalMeshCompilationResume [Num] - Can forcibly wait on all compilation using Editor.AsyncSkeletalMeshCompilationFlushAll BENCHMARKS Tested on AMD TR 3970X with 256GB RAM - 6m50s to 2m58s for loading P_Construct with a local-only DDC that doesn't contains any prebuilt skeletal mesh - 3m10s to 39s for importing 299 SK with the Interchange framework TESTS - Opened all 5769 SK from FortniteGame to exercise all legacy conversion code paths - Opened all 102 SK from QAGame - Opened P_Construct, Apollo_Terrain, LumenTest, P_World - Imported 299 SK with and without Interchange activated - Ensure no new regression introduced in any of the 70 SK related tests from EngineTest - Run all FBX import test in EngineTest - Cook / Run ShooterGame Client #rb Alexis.Matte [CL 15452895 by danny couture in ue5-main branch]
222 lines
9.0 KiB
C++
222 lines
9.0 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "SkeletalMeshBuilder.h"
|
|
#include "Modules/ModuleManager.h"
|
|
#include "MeshBoneReduction.h"
|
|
#include "Engine/EngineTypes.h"
|
|
#include "Engine/SkeletalMesh.h"
|
|
#include "PhysicsEngine/BodySetup.h"
|
|
#include "MeshDescription.h"
|
|
#include "MeshAttributes.h"
|
|
#include "MeshDescriptionHelper.h"
|
|
#include "MeshBuild.h"
|
|
#include "Rendering/SkeletalMeshModel.h"
|
|
#include "Rendering/SkeletalMeshLODModel.h"
|
|
#include "GPUSkinVertexFactory.h"
|
|
#include "ThirdPartyBuildOptimizationHelper.h"
|
|
#include "Misc/ScopedSlowTask.h"
|
|
#include "LODUtilities.h"
|
|
#include "ClothingAsset.h"
|
|
#include "MeshUtilities.h"
|
|
#include "EditorFramework/AssetImportData.h"
|
|
|
|
DEFINE_LOG_CATEGORY(LogSkeletalMeshBuilder);
|
|
|
|
namespace SkeletalMeshBuilderOptimization
|
|
{
|
|
void CacheOptimizeIndexBuffer(TArray<uint16>& Indices)
|
|
{
|
|
BuildOptimizationThirdParty::CacheOptimizeIndexBuffer(Indices);
|
|
}
|
|
|
|
void CacheOptimizeIndexBuffer(TArray<uint32>& Indices)
|
|
{
|
|
BuildOptimizationThirdParty::CacheOptimizeIndexBuffer(Indices);
|
|
}
|
|
}
|
|
|
|
struct InfluenceMap
|
|
{
|
|
FORCEINLINE bool operator()(const float& A, const float& B) const
|
|
{
|
|
return B > A;
|
|
}
|
|
};
|
|
|
|
struct FSkeletalMeshVertInstanceIDAndZ
|
|
{
|
|
FVertexInstanceID Index;
|
|
float Z;
|
|
};
|
|
|
|
FSkeletalMeshBuilder::FSkeletalMeshBuilder()
|
|
{
|
|
|
|
}
|
|
|
|
|
|
bool FSkeletalMeshBuilder::Build(USkeletalMesh* SkeletalMesh, const int32 LODIndex, const bool bRegenDepLODs)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FSkeletalMeshBuilder::Build);
|
|
|
|
check(SkeletalMesh->GetImportedModel());
|
|
check(SkeletalMesh->GetImportedModel()->LODModels.IsValidIndex(LODIndex));
|
|
check(SkeletalMesh->GetLODInfo(LODIndex) != nullptr);
|
|
|
|
FSkeletalMeshLODInfo* LODInfo = SkeletalMesh->GetLODInfo(LODIndex);
|
|
//We want to backup in case the LODModel is regenerated, this data is use to validate in the UI if the ddc must be rebuild
|
|
const FString BackupBuildStringID = SkeletalMesh->GetImportedModel()->LODModels[LODIndex].BuildStringID;
|
|
|
|
const FReferenceSkeleton& RefSkeleton = SkeletalMesh->GetRefSkeleton();
|
|
|
|
FScopedSlowTask SlowTask(6.01f, NSLOCTEXT("SkeltalMeshBuilder", "BuildingSkeletalMeshLOD", "Building skeletal mesh LOD"));
|
|
SlowTask.MakeDialog();
|
|
|
|
//Prevent any PostEdit change during the build
|
|
FScopedSkeletalMeshPostEditChange ScopedPostEditChange(SkeletalMesh, false, false);
|
|
// Unbind any existing clothing assets before we reimport the geometry
|
|
TArray<ClothingAssetUtils::FClothingAssetMeshBinding> ClothingBindings;
|
|
FLODUtilities::UnbindClothingAndBackup(SkeletalMesh, ClothingBindings, LODIndex);
|
|
|
|
FSkeletalMeshImportData SkeletalMeshImportData;
|
|
int32 NumTextCoord = 1; //We need to send rendering at least one tex coord buffer
|
|
|
|
//This scope define where we can use the LODModel, after a reduction the LODModel must be requery since it is a new instance
|
|
{
|
|
FSkeletalMeshLODModel& BuildLODModel = SkeletalMesh->GetImportedModel()->LODModels[LODIndex];
|
|
|
|
//Load the imported data
|
|
SkeletalMesh->LoadLODImportedData(LODIndex, SkeletalMeshImportData);
|
|
|
|
TArray<FVector> LODPoints;
|
|
TArray<SkeletalMeshImportData::FMeshWedge> LODWedges;
|
|
TArray<SkeletalMeshImportData::FMeshFace> LODFaces;
|
|
TArray<SkeletalMeshImportData::FVertInfluence> LODInfluences;
|
|
TArray<int32> LODPointToRawMap;
|
|
SkeletalMeshImportData.CopyLODImportData(LODPoints, LODWedges, LODFaces, LODInfluences, LODPointToRawMap);
|
|
|
|
//Use the max because we need to have at least one texture coordinnate
|
|
NumTextCoord = FMath::Max<int32>(NumTextCoord, SkeletalMeshImportData.NumTexCoords);
|
|
|
|
//BaseLOD need to make sure the source data fit with the skeletalmesh materials array before using meshutilities.BuildSkeletalMesh
|
|
FLODUtilities::AdjustImportDataFaceMaterialIndex(SkeletalMesh->GetMaterials(), SkeletalMeshImportData.Materials, LODFaces, LODIndex);
|
|
|
|
//Build the skeletalmesh using mesh utilities module
|
|
IMeshUtilities::MeshBuildOptions Options;
|
|
Options.FillOptions(LODInfo->BuildSettings);
|
|
//Force the normals or tangent in case the data is missing
|
|
Options.bComputeNormals |= !SkeletalMeshImportData.bHasNormals;
|
|
Options.bComputeTangents |= !SkeletalMeshImportData.bHasTangents;
|
|
IMeshUtilities& MeshUtilities = FModuleManager::Get().LoadModuleChecked<IMeshUtilities>("MeshUtilities");
|
|
|
|
// Create skinning streams for NewModel.
|
|
SlowTask.EnterProgressFrame(1.0f);
|
|
MeshUtilities.BuildSkeletalMesh(
|
|
BuildLODModel,
|
|
SkeletalMesh->GetPathName(),
|
|
RefSkeleton,
|
|
LODInfluences,
|
|
LODWedges,
|
|
LODFaces,
|
|
LODPoints,
|
|
LODPointToRawMap,
|
|
Options
|
|
);
|
|
|
|
//Re-Apply the user section changes, the UserSectionsData is map to original section and should match the builded LODModel
|
|
BuildLODModel.SyncronizeUserSectionsDataArray();
|
|
|
|
// Set texture coordinate count on the new model.
|
|
BuildLODModel.NumTexCoords = NumTextCoord;
|
|
|
|
//Re-apply the morph target
|
|
SlowTask.EnterProgressFrame(1.0f, NSLOCTEXT("SkeltalMeshBuilder", "RebuildMorphTarget", "Rebuilding morph targets..."));
|
|
if (SkeletalMeshImportData.MorphTargetNames.Num() > 0)
|
|
{
|
|
FLODUtilities::BuildMorphTargets(SkeletalMesh, SkeletalMeshImportData, LODIndex, !Options.bComputeNormals, !Options.bComputeTangents, Options.bUseMikkTSpace, Options.OverlappingThresholds);
|
|
}
|
|
|
|
//Re-apply the alternate skinning it must be after the inline reduction
|
|
SlowTask.EnterProgressFrame(1.0f, NSLOCTEXT("SkeltalMeshBuilder", "RebuildAlternateSkinning", "Rebuilding alternate skinning..."));
|
|
const TArray<FSkinWeightProfileInfo>& SkinProfiles = SkeletalMesh->GetSkinWeightProfiles();
|
|
for (int32 SkinProfileIndex = 0; SkinProfileIndex < SkinProfiles.Num(); ++SkinProfileIndex)
|
|
{
|
|
const FSkinWeightProfileInfo& ProfileInfo = SkinProfiles[SkinProfileIndex];
|
|
FLODUtilities::UpdateAlternateSkinWeights(SkeletalMesh, ProfileInfo.Name, LODIndex, Options.OverlappingThresholds, !Options.bComputeNormals, !Options.bComputeTangents, Options.bUseMikkTSpace, Options.bComputeWeightedNormals);
|
|
}
|
|
|
|
|
|
FSkeletalMeshUpdateContext UpdateContext;
|
|
UpdateContext.SkeletalMesh = SkeletalMesh;
|
|
//We are reduce ourself in this case we reduce ourself from the original data and return true.
|
|
if (SkeletalMesh->IsReductionActive(LODIndex))
|
|
{
|
|
SlowTask.EnterProgressFrame(1.0f, NSLOCTEXT("SkeltalMeshBuilder", "RegenerateLOD", "Regenerate LOD..."));
|
|
//Update the original reduction data since we just build a new LODModel.
|
|
if (LODInfo->ReductionSettings.BaseLOD == LODIndex && SkeletalMesh->GetImportedModel()->OriginalReductionSourceMeshData.IsValidIndex(LODIndex))
|
|
{
|
|
// Inplace reduction is not currently thread-safe
|
|
check(IsInGameThread());
|
|
|
|
//Make the copy of the data only once until the ImportedModel change (re-imported)
|
|
SkeletalMesh->GetImportedModel()->OriginalReductionSourceMeshData[LODIndex]->EmptyBulkData();
|
|
TMap<FString, TArray<FMorphTargetDelta>> BaseLODMorphTargetData;
|
|
BaseLODMorphTargetData.Empty(SkeletalMesh->GetMorphTargets().Num());
|
|
for (UMorphTarget *MorphTarget : SkeletalMesh->GetMorphTargets())
|
|
{
|
|
if (!MorphTarget->HasDataForLOD(LODIndex))
|
|
{
|
|
continue;
|
|
}
|
|
TArray<FMorphTargetDelta>& MorphDeltasArray = BaseLODMorphTargetData.FindOrAdd(MorphTarget->GetFullName());
|
|
const FMorphTargetLODModel& BaseMorphModel = MorphTarget->MorphLODModels[LODIndex];
|
|
//Iterate each original morph target source index to fill the NewMorphTargetDeltas array with the TargetMatchData.
|
|
for (const FMorphTargetDelta& MorphDelta : BaseMorphModel.Vertices)
|
|
{
|
|
MorphDeltasArray.Add(MorphDelta);
|
|
}
|
|
}
|
|
//Copy the original SkeletalMesh LODModel
|
|
SkeletalMesh->GetImportedModel()->OriginalReductionSourceMeshData[LODIndex]->SaveReductionData(BuildLODModel, BaseLODMorphTargetData, SkeletalMesh);
|
|
|
|
if (LODIndex == 0)
|
|
{
|
|
SkeletalMesh->GetLODInfo(LODIndex)->SourceImportFilename = SkeletalMesh->GetAssetImportData()->GetFirstFilename();
|
|
}
|
|
}
|
|
FLODUtilities::SimplifySkeletalMeshLOD(UpdateContext, LODIndex, false);
|
|
}
|
|
else
|
|
{
|
|
if (LODInfo->BonesToRemove.Num() > 0 && SkeletalMesh->GetSkeleton())
|
|
{
|
|
TArray<FName> BonesToRemove;
|
|
BonesToRemove.Reserve(LODInfo->BonesToRemove.Num());
|
|
for (const FBoneReference& BoneReference : LODInfo->BonesToRemove)
|
|
{
|
|
BonesToRemove.Add(BoneReference.BoneName);
|
|
}
|
|
MeshUtilities.RemoveBonesFromMesh(SkeletalMesh, LODIndex, &BonesToRemove);
|
|
}
|
|
}
|
|
}
|
|
|
|
FSkeletalMeshLODModel& LODModelAfterReduction = SkeletalMesh->GetImportedModel()->LODModels[LODIndex];
|
|
|
|
//Re-apply the clothing using the UserSectionsData, this will ensure we remap correctly the cloth if the reduction has change the number of sections
|
|
SlowTask.EnterProgressFrame(1.0f, NSLOCTEXT("SkeltalMeshBuilder", "RebuildClothing", "Rebuilding clothing..."));
|
|
FLODUtilities::RestoreClothingFromBackup(SkeletalMesh, ClothingBindings, LODIndex);
|
|
|
|
LODModelAfterReduction.SyncronizeUserSectionsDataArray();
|
|
LODModelAfterReduction.NumTexCoords = NumTextCoord;
|
|
LODModelAfterReduction.BuildStringID = BackupBuildStringID;
|
|
|
|
SlowTask.EnterProgressFrame(1.0f, NSLOCTEXT("SkeltalMeshBuilder", "RegenerateDependentLODs", "Regenerate Dependent LODs..."));
|
|
if (bRegenDepLODs)
|
|
{
|
|
//Regenerate dependent LODs
|
|
FLODUtilities::RegenerateDependentLODs(SkeletalMesh, LODIndex);
|
|
}
|
|
return true;
|
|
}
|