Files
UnrealEngineUWP/Engine/Source/Developer/MeshMergeUtilities/Private/ProxyGenerationProcessor.cpp
Rune Stubbe 9dbd92093b New Nanite vertex quantization code that replaces the cluster-relative coordinates with quantization to an absolute po2-sized grid centered around the mesh center.
The scheme solves issues where meshes would no longer align properly after converting them to Nanite. It also solves the precision issues in clusters with triangles of non-uniform size.
By default the precision is heuristically selected by Nanite, but the user also has the ability to override with an explicit precision to solve issues or optimize for disk size.
Clusters store the coordinate components using the minimal number of bits required to span the range of values in the cluster.
Apart from fixing issues, the default quality seems no worse than before and is typically ~5-10% smaller than before.

Added Position Precision to the top left of Mesh viewer along with the other mesh stats.
Added Position Precision dropdown to Nanite import settings.
Added debug mode "r.nanite.visualize PositionBits" that shows the vertex position bit sizes for clusters.
New quantization code now updates float positions and cluster bounds to reflect the quantized coordinates.

#rb brian.karis, graham.wihlidal
#JIRA UE-102722
#preflight 607d56774df3b60001ef477c

[CL 16049335 by Rune Stubbe in ue5-main branch]
2021-04-19 06:58:00 -04:00

354 lines
13 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "ProxyGenerationProcessor.h"
#include "MaterialUtilities.h"
#include "MeshMergeUtilities.h"
#include "IMeshMergeExtension.h"
#include "ProxyMaterialUtilities.h"
#include "IMeshReductionInterfaces.h"
#include "IMeshReductionManagerModule.h"
#include "Modules/ModuleManager.h"
#include "StaticMeshAttributes.h"
#include "Stats/Stats.h"
#if WITH_EDITOR
#include "Editor.h"
#include "MeshMergeHelpers.h"
#endif // WITH_EDITOR
FProxyGenerationProcessor::FProxyGenerationProcessor(const FMeshMergeUtilities* InOwner)
: Owner(InOwner)
{
#if WITH_EDITOR
FEditorDelegates::MapChange.AddRaw(this, &FProxyGenerationProcessor::OnMapChange);
FEditorDelegates::NewCurrentLevel.AddRaw(this, &FProxyGenerationProcessor::OnNewCurrentLevel);
IMeshReductionManagerModule& Module = FModuleManager::Get().LoadModuleChecked<IMeshReductionManagerModule>("MeshReductionInterface");
IMeshMerging* MeshMerging = Module.GetMeshMergingInterface();
if (!MeshMerging)
{
UE_LOG(LogMeshMerging, Log, TEXT("No automatic mesh merging module available"));
}
else
{
MeshMerging->CompleteDelegate.BindRaw(this, &FProxyGenerationProcessor::ProxyGenerationComplete);
MeshMerging->FailedDelegate.BindRaw(this, &FProxyGenerationProcessor::ProxyGenerationFailed);
}
IMeshMerging* DistributedMeshMerging = Module.GetDistributedMeshMergingInterface();
if (!DistributedMeshMerging)
{
UE_LOG(LogMeshMerging, Log, TEXT("No distributed automatic mesh merging module available"));
}
else
{
DistributedMeshMerging->CompleteDelegate.BindRaw(this, &FProxyGenerationProcessor::ProxyGenerationComplete);
DistributedMeshMerging->FailedDelegate.BindRaw(this, &FProxyGenerationProcessor::ProxyGenerationFailed);
}
#endif // WITH_EDITOR
}
FProxyGenerationProcessor::~FProxyGenerationProcessor()
{
#if WITH_EDITOR
FEditorDelegates::MapChange.RemoveAll(this);
FEditorDelegates::NewCurrentLevel.RemoveAll(this);
#endif // WITH_EDITOR
}
void FProxyGenerationProcessor::AddProxyJob(FGuid InJobGuid, FMergeCompleteData* InCompleteData)
{
FScopeLock Lock(&StateLock);
ProxyMeshJobs.Add(InJobGuid, InCompleteData);
}
bool FProxyGenerationProcessor::Tick(float DeltaTime)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_FProxyGenerationProcessor_Tick);
FScopeLock Lock(&StateLock);
for (const auto& Entry : ToProcessJobDataMap)
{
FGuid JobGuid = Entry.Key;
FProxyGenerationData* Data = Entry.Value;
// Process the job
ProcessJob(JobGuid, Data);
// Data retrieved so can now remove the job from the map
ProxyMeshJobs.Remove(JobGuid);
delete Data->MergeData;
delete Data;
}
ToProcessJobDataMap.Reset();
return true;
}
void FProxyGenerationProcessor::ProxyGenerationComplete(FMeshDescription& OutProxyMesh, struct FFlattenMaterial& OutMaterial, const FGuid OutJobGUID)
{
FScopeLock Lock(&StateLock);
FMergeCompleteData** FindData = ProxyMeshJobs.Find(OutJobGUID);
if (FindData && *FindData)
{
FMergeCompleteData* Data = *FindData;
FProxyGenerationData* GenerationData = new FProxyGenerationData();
GenerationData->Material = OutMaterial;
GenerationData->RawMesh = OutProxyMesh;
GenerationData->MergeData = Data;
ToProcessJobDataMap.Add(OutJobGUID, GenerationData);
}
}
void FProxyGenerationProcessor::ProxyGenerationFailed(const FGuid OutJobGUID, const FString& ErrorMessage)
{
FScopeLock Lock(&StateLock);
FMergeCompleteData** FindData = ProxyMeshJobs.Find(OutJobGUID);
if (FindData && *FindData)
{
UE_LOG(LogMeshMerging, Log, TEXT("Failed to generate proxy mesh for cluster %s, %s"), *(*FindData)->ProxyBasePackageName, *ErrorMessage);
ProxyMeshJobs.Remove(OutJobGUID);
TArray<UObject*> OutAssetsToSync;
(*FindData)->CallbackDelegate.ExecuteIfBound(OutJobGUID, OutAssetsToSync);
}
}
void FProxyGenerationProcessor::OnMapChange(uint32 MapFlags)
{
ClearProcessingData();
}
void FProxyGenerationProcessor::OnNewCurrentLevel()
{
ClearProcessingData();
}
void FProxyGenerationProcessor::ClearProcessingData()
{
FScopeLock Lock(&StateLock);
ProxyMeshJobs.Empty();
ToProcessJobDataMap.Empty();
}
void FProxyGenerationProcessor::ProcessJob(const FGuid& JobGuid, FProxyGenerationData* Data)
{
TArray<UObject*> OutAssetsToSync;
const FString AssetBaseName = FPackageName::GetShortName(Data->MergeData->ProxyBasePackageName);
const FString AssetBasePath = Data->MergeData->InOuter ? TEXT("") : FPackageName::GetLongPackagePath(Data->MergeData->ProxyBasePackageName) + TEXT("/");
UMaterialInstanceConstant* ProxyMaterial = nullptr;
if (!Data->RawMesh.IsEmpty())
{
// Don't recreate render states with the material update context as we will manually do it through
// the FStaticMeshComponentRecreateRenderStateContext below
FMaterialUpdateContext MaterialUpdateContext(FMaterialUpdateContext::EOptions::Default & ~FMaterialUpdateContext::EOptions::RecreateRenderStates);
// Retrieve flattened material data
FFlattenMaterial& FlattenMaterial = Data->Material;
// Resize flattened material
FMaterialUtilities::ResizeFlattenMaterial(FlattenMaterial, Data->MergeData->InProxySettings);
// Optimize flattened material
FMaterialUtilities::OptimizeFlattenMaterial(FlattenMaterial);
// Create a new proxy material instance
ProxyMaterial = ProxyMaterialUtilities::CreateProxyMaterialInstance(Data->MergeData->InOuter, Data->MergeData->InProxySettings.MaterialSettings, Data->MergeData->BaseMaterial, FlattenMaterial, AssetBasePath, AssetBaseName, OutAssetsToSync, &MaterialUpdateContext);
for (IMeshMergeExtension* Extension : Owner->MeshMergeExtensions)
{
Extension->OnCreatedProxyMaterial(Data->MergeData->StaticMeshComponents, ProxyMaterial);
}
// Set material static lighting usage flag if project has static lighting enabled
static const auto AllowStaticLightingVar = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.AllowStaticLighting"));
const bool bAllowStaticLighting = (!AllowStaticLightingVar || AllowStaticLightingVar->GetValueOnGameThread() != 0);
if (bAllowStaticLighting)
{
ProxyMaterial->CheckMaterialUsage(MATUSAGE_StaticLighting);
}
}
// Construct proxy static mesh
UPackage* MeshPackage = Data->MergeData->InOuter;
FString MeshAssetName = TEXT("SM_") + AssetBaseName;
if (MeshPackage == nullptr)
{
MeshPackage = CreatePackage( *(AssetBasePath + MeshAssetName));
MeshPackage->FullyLoad();
MeshPackage->Modify();
}
FStaticMeshComponentRecreateRenderStateContext RecreateRenderStateContext(FindObject<UStaticMesh>(MeshPackage, *MeshAssetName));
UStaticMesh* StaticMesh = NewObject<UStaticMesh>(MeshPackage, FName(*MeshAssetName), RF_Public | RF_Standalone);
StaticMesh->InitResources();
FString OutputPath = StaticMesh->GetPathName();
// make sure it has a new lighting guid
StaticMesh->SetLightingGuid();
// Set it to use textured lightmaps. Note that Build Lighting will do the error-checking (texcoordindex exists for all LODs, etc).
StaticMesh->SetLightMapResolution(Data->MergeData->InProxySettings.LightMapResolution);
StaticMesh->SetLightMapCoordinateIndex(1);
FStaticMeshSourceModel& SrcModel = StaticMesh->AddSourceModel();
/*Don't allow the engine to recalculate normals*/
SrcModel.BuildSettings.bRecomputeNormals = false;
SrcModel.BuildSettings.bRecomputeTangents = false;
SrcModel.BuildSettings.bComputeWeightedNormals = true;
SrcModel.BuildSettings.bRemoveDegenerates = true;
SrcModel.BuildSettings.bUseHighPrecisionTangentBasis = false;
SrcModel.BuildSettings.bUseFullPrecisionUVs = false;
SrcModel.BuildSettings.bGenerateLightmapUVs = Data->MergeData->InProxySettings.bGenerateLightmapUVs;
SrcModel.BuildSettings.bBuildReversedIndexBuffer = false;
if (!Data->MergeData->InProxySettings.bAllowDistanceField)
{
SrcModel.BuildSettings.DistanceFieldResolutionScale = 0.0f;
}
const bool bContainsImposters = Data->MergeData->ImposterComponents.Num() > 0;
FBox ImposterBounds(EForceInit::ForceInit);
TPolygonGroupAttributesConstRef<FName> PolygonGroupMaterialSlotName = FStaticMeshAttributes(Data->RawMesh).GetPolygonGroupMaterialSlotNames();
auto RemoveVertexColorAndCommitMeshDescription = [&StaticMesh, &Data, &ProxyMaterial, &PolygonGroupMaterialSlotName]()
{
if (!Data->MergeData->InProxySettings.bAllowVertexColors)
{
//We cannot remove the vertex color with the mesh description so we assign a white value to all color
TVertexInstanceAttributesRef<FVector4> VertexInstanceColors = FStaticMeshAttributes(Data->RawMesh).GetVertexInstanceColors();
//set all value to white
for (const FVertexInstanceID VertexInstanceID : Data->RawMesh.VertexInstances().GetElementIDs())
{
VertexInstanceColors[VertexInstanceID] = FVector4(1.0f, 1.0f, 1.0f);
}
}
//Commit the FMeshDescription to the source model we just created
int32 SourceModelIndex = StaticMesh->GetNumSourceModels() - 1;
FMeshDescription* MeshDescription = StaticMesh->CreateMeshDescription(SourceModelIndex, Data->RawMesh);
if (ensure(MeshDescription))
{
if (ProxyMaterial)
{
// Make sure the Proxy material have a valid ImportedMaterialSlotName
//The proxy material must be add only once and is always the first slot of the HLOD mesh
FStaticMaterial NewMaterial(ProxyMaterial);
if (MeshDescription->PolygonGroups().Num() > 0)
{
NewMaterial.ImportedMaterialSlotName = PolygonGroupMaterialSlotName[MeshDescription->PolygonGroups().GetFirstValidID()];
}
StaticMesh->GetStaticMaterials().Add(NewMaterial);
}
UStaticMesh::FCommitMeshDescriptionParams CommitParams;
CommitParams.bUseHashAsGuid = true;
StaticMesh->CommitMeshDescription(SourceModelIndex, CommitParams);
}
};
if (bContainsImposters)
{
TArray<UMaterialInterface*> ImposterMaterials;
// Merge imposter meshes to rawmesh
// The base material index is always one here as we assume we only have one HLOD material
FMeshMergeHelpers::MergeImpostersToMesh(Data->MergeData->ImposterComponents, Data->RawMesh, FVector::ZeroVector, 1, ImposterMaterials);
for (const UStaticMeshComponent* Component : Data->MergeData->ImposterComponents)
{
if (Component->GetStaticMesh())
{
ImposterBounds += Component->GetStaticMesh()->GetBoundingBox().TransformBy(Component->GetComponentToWorld());
}
}
RemoveVertexColorAndCommitMeshDescription();
for (UMaterialInterface* Material : ImposterMaterials)
{
//Set the ImportedMaterialSlotName in each imposter material
FStaticMaterial NewMaterial(Material);
if (Data->RawMesh.PolygonGroups().Num() > 0)
{
NewMaterial.ImportedMaterialSlotName = PolygonGroupMaterialSlotName[Data->RawMesh.PolygonGroups().GetFirstValidID()];
}
StaticMesh->GetStaticMaterials().Add(NewMaterial);
}
}
else
{
RemoveVertexColorAndCommitMeshDescription();
}
// Nanite settings
StaticMesh->NaniteSettings.bEnabled = Data->MergeData->InProxySettings.bGenerateNaniteEnabledMesh;
StaticMesh->NaniteSettings.PositionPrecision = MIN_int32;
StaticMesh->NaniteSettings.PercentTriangles = 1.0f; // For now, assume meshes generated here are low poly enough to be used directly as the nanite proxy.
//Set the Imported version before calling the build
StaticMesh->ImportVersion = EImportStaticMeshVersion::LastVersion;
// setup section info map
TPolygonGroupAttributesConstRef<FName> PolygonGroupImportedMaterialSlotNames = Data->RawMesh.PolygonGroupAttributes().GetAttributesRef<FName>(MeshAttribute::PolygonGroup::ImportedMaterialSlotName);
TArray<int32> UniqueMaterialIndices;
for (const FPolygonGroupID PolygonGroupID : Data->RawMesh.PolygonGroups().GetElementIDs())
{
int32 PolygonGroupMaterialIndex = PolygonGroupID.GetValue();
FName PolygonGroupName = PolygonGroupImportedMaterialSlotNames[PolygonGroupID];
if (PolygonGroupName != NAME_None)
{
for (int32 MaterialIndex = 0; MaterialIndex < StaticMesh->GetStaticMaterials().Num(); ++MaterialIndex)
{
if (StaticMesh->GetStaticMaterials()[MaterialIndex].ImportedMaterialSlotName == PolygonGroupName)
{
PolygonGroupMaterialIndex = MaterialIndex;
break;
}
}
}
if (!StaticMesh->GetStaticMaterials().IsValidIndex(PolygonGroupMaterialIndex))
{
PolygonGroupMaterialIndex = 0;
}
UniqueMaterialIndices.AddUnique(PolygonGroupMaterialIndex);
}
int32 SectionIndex = 0;
for (int32 UniqueMaterialIndex : UniqueMaterialIndices)
{
FMeshSectionInfo MeshSectionInfo(UniqueMaterialIndex);
// enable/disable section collision according to settings
MeshSectionInfo.bEnableCollision = Data->MergeData->InProxySettings.bCreateCollision;
StaticMesh->GetSectionInfoMap().Set(0, SectionIndex, MeshSectionInfo);
SectionIndex++;
}
StaticMesh->Build();
if (ImposterBounds.IsValid)
{
const FBox StaticMeshBox = StaticMesh->GetBoundingBox();
const FBox CombinedBox = StaticMeshBox + ImposterBounds;
StaticMesh->SetPositiveBoundsExtension((CombinedBox.Max - StaticMeshBox.Max));
StaticMesh->SetNegativeBoundsExtension((StaticMeshBox.Min - CombinedBox.Min));
StaticMesh->CalculateExtendedBounds();
}
StaticMesh->PostEditChange();
OutAssetsToSync.Add(StaticMesh);
// Execute the delegate received from the user
Data->MergeData->CallbackDelegate.ExecuteIfBound(JobGuid, OutAssetsToSync);
}