You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
#preflight 63b5e0bfff7b9ad7030f0f81 [CL 23581920 by christopher waters in ue5-main branch]
1088 lines
39 KiB
C++
1088 lines
39 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "MaterialBakingModule.h"
|
|
#include "MaterialRenderItem.h"
|
|
#include "Engine/TextureRenderTarget2D.h"
|
|
#include "ExportMaterialProxy.h"
|
|
#include "Interfaces/IMainFrameModule.h"
|
|
#include "MaterialOptionsWindow.h"
|
|
#include "MaterialOptions.h"
|
|
#include "PropertyEditorModule.h"
|
|
#include "MaterialOptionsCustomization.h"
|
|
#include "UObject/UObjectGlobals.h"
|
|
#include "MaterialBakingStructures.h"
|
|
#include "Framework/Application/SlateApplication.h"
|
|
#include "MaterialBakingHelpers.h"
|
|
#include "Async/Async.h"
|
|
#include "Async/ParallelFor.h"
|
|
#include "Materials/MaterialInstance.h"
|
|
#include "Materials/MaterialInstanceConstant.h"
|
|
#include "MaterialEditor/MaterialEditorInstanceConstant.h"
|
|
#include "RenderingThread.h"
|
|
#include "RHISurfaceDataConversion.h"
|
|
#include "SceneView.h"
|
|
#include "Misc/ScopedSlowTask.h"
|
|
#include "MeshDescription.h"
|
|
#include "TextureCompiler.h"
|
|
#include "TextureResource.h"
|
|
#include "RenderCaptureInterface.h"
|
|
#if WITH_EDITOR
|
|
#include "Misc/FileHelper.h"
|
|
#endif
|
|
|
|
IMPLEMENT_MODULE(FMaterialBakingModule, MaterialBaking);
|
|
|
|
DEFINE_LOG_CATEGORY_STATIC(LogMaterialBaking, All, All);
|
|
|
|
#define LOCTEXT_NAMESPACE "MaterialBakingModule"
|
|
|
|
/** Cvars for advanced features */
|
|
static TAutoConsoleVariable<int32> CVarUseMaterialProxyCaching(
|
|
TEXT("MaterialBaking.UseMaterialProxyCaching"),
|
|
1,
|
|
TEXT("Determines whether or not Material Proxies should be cached to speed up material baking.\n")
|
|
TEXT("0: Turned Off\n")
|
|
TEXT("1: Turned On"),
|
|
ECVF_Default);
|
|
|
|
static TAutoConsoleVariable<int32> CVarSaveIntermediateTextures(
|
|
TEXT("MaterialBaking.SaveIntermediateTextures"),
|
|
0,
|
|
TEXT("Determines whether or not to save out intermediate BMP images for each flattened material property.\n")
|
|
TEXT("0: Turned Off\n")
|
|
TEXT("1: Turned On"),
|
|
ECVF_Default);
|
|
|
|
static TAutoConsoleVariable<int32> CVarMaterialBakingRDOCCapture(
|
|
TEXT("MaterialBaking.RenderDocCapture"),
|
|
0,
|
|
TEXT("Determines whether or not to trigger a RenderDoc capture.\n")
|
|
TEXT("0: Turned Off\n")
|
|
TEXT("1: Turned On"),
|
|
ECVF_Default);
|
|
|
|
static TAutoConsoleVariable<int32> CVarMaterialBakingVTWarmupFrames(
|
|
TEXT("MaterialBaking.VTWarmupFrames"),
|
|
5,
|
|
TEXT("Number of frames to render for virtual texture warmup when material baking."));
|
|
|
|
|
|
namespace FMaterialBakingModuleImpl
|
|
{
|
|
// Custom dynamic mesh allocator specifically tailored for Material Baking.
|
|
// This will always reuse the same couple buffers, so searching linearly is not a problem.
|
|
class FMaterialBakingDynamicMeshBufferAllocator : public FDynamicMeshBufferAllocator
|
|
{
|
|
// This must be smaller than the large allocation blocks on Windows 10 which is currently ~508K.
|
|
// Large allocations uses VirtualAlloc directly without any kind of buffering before
|
|
// releasing pages to the kernel, so it causes lots of soft page fault when
|
|
// memory is first initialized.
|
|
const uint32 SmallestPooledBufferSize = 256*1024;
|
|
|
|
TArray<FBufferRHIRef> IndexBuffers;
|
|
TArray<FBufferRHIRef> VertexBuffers;
|
|
|
|
template <typename RefType>
|
|
RefType GetSmallestFit(uint32 SizeInBytes, TArray<RefType>& Array)
|
|
{
|
|
uint32 SmallestFitIndex = UINT32_MAX;
|
|
uint32 SmallestFitSize = UINT32_MAX;
|
|
for (int32 Index = 0; Index < Array.Num(); ++Index)
|
|
{
|
|
uint32 Size = Array[Index]->GetSize();
|
|
if (Size >= SizeInBytes && (SmallestFitIndex == UINT32_MAX || Size < SmallestFitSize))
|
|
{
|
|
SmallestFitIndex = Index;
|
|
SmallestFitSize = Size;
|
|
}
|
|
}
|
|
|
|
RefType Ref;
|
|
// Do not reuse the smallest fit if it's a lot bigger than what we requested
|
|
if (SmallestFitIndex != UINT32_MAX && SmallestFitSize < SizeInBytes*2)
|
|
{
|
|
Ref = Array[SmallestFitIndex];
|
|
Array.RemoveAtSwap(SmallestFitIndex);
|
|
}
|
|
|
|
return Ref;
|
|
}
|
|
|
|
virtual FBufferRHIRef AllocIndexBuffer(uint32 NumElements) override
|
|
{
|
|
uint32 BufferSize = GetIndexBufferSize(NumElements);
|
|
if (BufferSize > SmallestPooledBufferSize)
|
|
{
|
|
FBufferRHIRef Ref = GetSmallestFit(GetIndexBufferSize(NumElements), IndexBuffers);
|
|
if (Ref.IsValid())
|
|
{
|
|
return Ref;
|
|
}
|
|
}
|
|
|
|
return FDynamicMeshBufferAllocator::AllocIndexBuffer(NumElements);
|
|
}
|
|
|
|
virtual void ReleaseIndexBuffer(FBufferRHIRef& IndexBufferRHI) override
|
|
{
|
|
if (IndexBufferRHI->GetSize() > SmallestPooledBufferSize)
|
|
{
|
|
IndexBuffers.Add(MoveTemp(IndexBufferRHI));
|
|
}
|
|
|
|
IndexBufferRHI = nullptr;
|
|
}
|
|
|
|
virtual FBufferRHIRef AllocVertexBuffer(uint32 Stride, uint32 NumElements) override
|
|
{
|
|
uint32 BufferSize = GetVertexBufferSize(Stride, NumElements);
|
|
if (BufferSize > SmallestPooledBufferSize)
|
|
{
|
|
FBufferRHIRef Ref = GetSmallestFit(BufferSize, VertexBuffers);
|
|
if (Ref.IsValid())
|
|
{
|
|
return Ref;
|
|
}
|
|
}
|
|
|
|
return FDynamicMeshBufferAllocator::AllocVertexBuffer(Stride, NumElements);
|
|
}
|
|
|
|
virtual void ReleaseVertexBuffer(FBufferRHIRef& VertexBufferRHI) override
|
|
{
|
|
if (VertexBufferRHI->GetSize() > SmallestPooledBufferSize)
|
|
{
|
|
VertexBuffers.Add(MoveTemp(VertexBufferRHI));
|
|
}
|
|
|
|
VertexBufferRHI = nullptr;
|
|
}
|
|
};
|
|
|
|
class FStagingBufferPool
|
|
{
|
|
public:
|
|
FTexture2DRHIRef CreateStagingBuffer_RenderThread(FRHICommandListImmediate& RHICmdList, int32 Width, int32 Height, EPixelFormat Format, bool bIsSRGB)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(CreateStagingBuffer_RenderThread)
|
|
|
|
auto StagingBufferPredicate =
|
|
[Width, Height, Format, bIsSRGB](const FTexture2DRHIRef& Texture2DRHIRef)
|
|
{
|
|
return Texture2DRHIRef->GetSizeX() == Width && Texture2DRHIRef->GetSizeY() == Height && Texture2DRHIRef->GetFormat() == Format && bool(Texture2DRHIRef->GetFlags() & TexCreate_SRGB) == bIsSRGB;
|
|
};
|
|
|
|
// Process any staging buffers available for unmapping
|
|
{
|
|
TArray<FTexture2DRHIRef> ToUnmapLocal;
|
|
{
|
|
FScopeLock Lock(&ToUnmapLock);
|
|
ToUnmapLocal = MoveTemp(ToUnmap);
|
|
}
|
|
|
|
for (int32 Index = 0, Num = ToUnmapLocal.Num(); Index < Num; ++Index)
|
|
{
|
|
RHICmdList.UnmapStagingSurface(ToUnmapLocal[Index]);
|
|
Pool.Add(MoveTemp(ToUnmapLocal[Index]));
|
|
}
|
|
}
|
|
|
|
// Find any pooled staging buffer with suitable properties.
|
|
int32 Index = Pool.IndexOfByPredicate(StagingBufferPredicate);
|
|
|
|
if (Index != -1)
|
|
{
|
|
FTexture2DRHIRef StagingBuffer = MoveTemp(Pool[Index]);
|
|
Pool.RemoveAtSwap(Index);
|
|
return StagingBuffer;
|
|
}
|
|
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(RHICreateTexture2D)
|
|
|
|
FRHITextureCreateDesc Desc =
|
|
FRHITextureCreateDesc::Create2D(TEXT("FStagingBufferPool_StagingBuffer"), Width, Height, Format)
|
|
.SetFlags(ETextureCreateFlags::CPUReadback);
|
|
|
|
if (bIsSRGB)
|
|
{
|
|
Desc.AddFlags(ETextureCreateFlags::SRGB);
|
|
}
|
|
|
|
return RHICreateTexture(Desc);
|
|
}
|
|
|
|
void ReleaseStagingBufferForUnmap_AnyThread(FTexture2DRHIRef& Texture2DRHIRef)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(ReleaseStagingBufferForUnmap_AnyThread)
|
|
FScopeLock Lock(&ToUnmapLock);
|
|
ToUnmap.Emplace(MoveTemp(Texture2DRHIRef));
|
|
}
|
|
|
|
void Clear_RenderThread(FRHICommandListImmediate& RHICmdList)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(Clear_RenderThread)
|
|
for (FTexture2DRHIRef& StagingSurface : ToUnmap)
|
|
{
|
|
RHICmdList.UnmapStagingSurface(StagingSurface);
|
|
}
|
|
|
|
ToUnmap.Empty();
|
|
Pool.Empty();
|
|
}
|
|
|
|
~FStagingBufferPool()
|
|
{
|
|
check(Pool.Num() == 0);
|
|
}
|
|
|
|
private:
|
|
TArray<FTexture2DRHIRef> Pool;
|
|
|
|
// Not contented enough to warrant the use of lockless structures.
|
|
FCriticalSection ToUnmapLock;
|
|
TArray<FTexture2DRHIRef> ToUnmap;
|
|
};
|
|
|
|
struct FRenderItemKey
|
|
{
|
|
const FMeshData* RenderData;
|
|
const FIntPoint RenderSize;
|
|
|
|
FRenderItemKey(const FMeshData* InRenderData, const FIntPoint& InRenderSize)
|
|
: RenderData(InRenderData)
|
|
, RenderSize(InRenderSize)
|
|
{
|
|
}
|
|
|
|
bool operator == (const FRenderItemKey& Other) const
|
|
{
|
|
return RenderData == Other.RenderData &&
|
|
RenderSize == Other.RenderSize;
|
|
}
|
|
};
|
|
|
|
uint32 GetTypeHash(const FRenderItemKey& Key)
|
|
{
|
|
return HashCombine(GetTypeHash(Key.RenderData), GetTypeHash(Key.RenderSize));
|
|
}
|
|
}
|
|
|
|
void FMaterialBakingModule::StartupModule()
|
|
{
|
|
bEmissiveHDR = false;
|
|
|
|
// Set which properties should enforce gamma correction
|
|
SetLinearBake(true);
|
|
|
|
// Set which pixel format should be used for the possible baked out material properties
|
|
PerPropertyFormat.Add(MP_EmissiveColor, PF_FloatRGBA);
|
|
PerPropertyFormat.Add(MP_Opacity, PF_B8G8R8A8);
|
|
PerPropertyFormat.Add(MP_OpacityMask, PF_B8G8R8A8);
|
|
PerPropertyFormat.Add(MP_BaseColor, PF_B8G8R8A8);
|
|
PerPropertyFormat.Add(MP_Metallic, PF_B8G8R8A8);
|
|
PerPropertyFormat.Add(MP_Specular, PF_B8G8R8A8);
|
|
PerPropertyFormat.Add(MP_Roughness, PF_B8G8R8A8);
|
|
PerPropertyFormat.Add(MP_Anisotropy, PF_B8G8R8A8);
|
|
PerPropertyFormat.Add(MP_Normal, PF_B8G8R8A8);
|
|
PerPropertyFormat.Add(MP_Tangent, PF_B8G8R8A8);
|
|
PerPropertyFormat.Add(MP_AmbientOcclusion, PF_B8G8R8A8);
|
|
PerPropertyFormat.Add(MP_SubsurfaceColor, PF_B8G8R8A8);
|
|
PerPropertyFormat.Add(MP_CustomData0, PF_B8G8R8A8);
|
|
PerPropertyFormat.Add(MP_CustomData1, PF_B8G8R8A8);
|
|
PerPropertyFormat.Add(TEXT("ClearCoatBottomNormal"), PF_B8G8R8A8);
|
|
|
|
// Register property customization
|
|
FPropertyEditorModule& Module = FModuleManager::Get().LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
|
|
Module.RegisterCustomPropertyTypeLayout(TEXT("PropertyEntry"), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FPropertyEntryCustomization::MakeInstance));
|
|
|
|
// Register callback for modified objects
|
|
FCoreUObjectDelegates::OnObjectModified.AddRaw(this, &FMaterialBakingModule::OnObjectModified);
|
|
|
|
// Register callback on garbage collection
|
|
FCoreUObjectDelegates::GetPreGarbageCollectDelegate().AddRaw(this, &FMaterialBakingModule::OnPreGarbageCollect);
|
|
}
|
|
|
|
void FMaterialBakingModule::ShutdownModule()
|
|
{
|
|
// Unregister customization and callback
|
|
FPropertyEditorModule* PropertyEditorModule = FModuleManager::GetModulePtr<FPropertyEditorModule>("PropertyEditor");
|
|
|
|
if (PropertyEditorModule)
|
|
{
|
|
PropertyEditorModule->UnregisterCustomPropertyTypeLayout(TEXT("PropertyEntry"));
|
|
}
|
|
|
|
FCoreUObjectDelegates::OnObjectModified.RemoveAll(this);
|
|
FCoreUObjectDelegates::GetPreGarbageCollectDelegate().RemoveAll(this);
|
|
|
|
CleanupMaterialProxies();
|
|
}
|
|
|
|
void FMaterialBakingModule::BakeMaterials(const TArray<FMaterialData*>& MaterialSettings, const TArray<FMeshData*>& MeshSettings, TArray<FBakeOutput>& Output)
|
|
{
|
|
// Translate old material data to extended types
|
|
TArray<FMaterialDataEx> MaterialDataExs;
|
|
MaterialDataExs.Reserve(MaterialSettings.Num());
|
|
for (const FMaterialData* MaterialData : MaterialSettings)
|
|
{
|
|
FMaterialDataEx& MaterialDataEx = MaterialDataExs.AddDefaulted_GetRef();
|
|
MaterialDataEx.Material = MaterialData->Material;
|
|
MaterialDataEx.bPerformBorderSmear = MaterialData->bPerformBorderSmear;
|
|
MaterialDataEx.bPerformShrinking = MaterialData->bPerformShrinking;
|
|
MaterialDataEx.bTangentSpaceNormal = MaterialData->bTangentSpaceNormal;
|
|
|
|
for (const TPair<EMaterialProperty, FIntPoint>& PropertySizePair : MaterialData->PropertySizes)
|
|
{
|
|
MaterialDataEx.PropertySizes.Add(PropertySizePair.Key, PropertySizePair.Value);
|
|
}
|
|
}
|
|
|
|
// Build an array of pointers to the extended type
|
|
TArray<FMaterialDataEx*> MaterialSettingsEx;
|
|
MaterialSettingsEx.Reserve(MaterialDataExs.Num());
|
|
for (FMaterialDataEx& MaterialDataEx : MaterialDataExs)
|
|
{
|
|
MaterialSettingsEx.Add(&MaterialDataEx);
|
|
}
|
|
|
|
TArray<FBakeOutputEx> BakeOutputExs;
|
|
BakeMaterials(MaterialSettingsEx, MeshSettings, BakeOutputExs);
|
|
|
|
// Translate extended bake output to old types
|
|
Output.Reserve(BakeOutputExs.Num());
|
|
for (FBakeOutputEx& BakeOutputEx : BakeOutputExs)
|
|
{
|
|
FBakeOutput& BakeOutput = Output.AddDefaulted_GetRef();
|
|
BakeOutput.EmissiveScale = BakeOutputEx.EmissiveScale;
|
|
|
|
for (TPair<FMaterialPropertyEx, FIntPoint>& PropertySizePair : BakeOutputEx.PropertySizes)
|
|
{
|
|
BakeOutput.PropertySizes.Add(PropertySizePair.Key.Type, PropertySizePair.Value);
|
|
}
|
|
|
|
for (TPair<FMaterialPropertyEx, TArray<FColor>>& PropertyDataPair : BakeOutputEx.PropertyData)
|
|
{
|
|
BakeOutput.PropertyData.Add(PropertyDataPair.Key.Type, MoveTemp(PropertyDataPair.Value));
|
|
}
|
|
|
|
for (TPair<FMaterialPropertyEx, TArray<FFloat16Color>>& PropertyDataPair : BakeOutputEx.HDRPropertyData)
|
|
{
|
|
BakeOutput.HDRPropertyData.Add(PropertyDataPair.Key.Type, MoveTemp(PropertyDataPair.Value));
|
|
}
|
|
}
|
|
}
|
|
|
|
void FMaterialBakingModule::BakeMaterials(const TArray<FMaterialDataEx*>& MaterialSettings, const TArray<FMeshData*>& MeshSettings, TArray<FBakeOutputEx>& Output)
|
|
{
|
|
UE_LOG(LogMaterialBaking, Verbose, TEXT("Performing material baking for %d materials"), MaterialSettings.Num());
|
|
for (int32 i = 0; i < MaterialSettings.Num(); i++)
|
|
{
|
|
if (MaterialSettings[i]->Material && MeshSettings[i]->MeshDescription)
|
|
{
|
|
UE_LOG(LogMaterialBaking, Verbose, TEXT(" [%5d] Material: %-50s Vertices: %8d Triangles: %8d"), i, *MaterialSettings[i]->Material->GetName(), MeshSettings[i]->MeshDescription->Vertices().Num(), MeshSettings[i]->MeshDescription->Triangles().Num());
|
|
}
|
|
}
|
|
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FMaterialBakingModule::BakeMaterials)
|
|
|
|
checkf(MaterialSettings.Num() == MeshSettings.Num(), TEXT("Number of material settings does not match that of MeshSettings"));
|
|
const int32 NumMaterials = MaterialSettings.Num();
|
|
const bool bSaveIntermediateTextures = CVarSaveIntermediateTextures.GetValueOnAnyThread() == 1;
|
|
|
|
using namespace FMaterialBakingModuleImpl;
|
|
FMaterialBakingDynamicMeshBufferAllocator MaterialBakingDynamicMeshBufferAllocator;
|
|
|
|
FScopedSlowTask Progress(NumMaterials, LOCTEXT("BakeMaterials", "Baking Materials..."), true );
|
|
Progress.MakeDialog(true);
|
|
|
|
TArray<uint32> ProcessingOrder;
|
|
ProcessingOrder.Reserve(MeshSettings.Num());
|
|
for (int32 Index = 0; Index < MeshSettings.Num(); ++Index)
|
|
{
|
|
ProcessingOrder.Add(Index);
|
|
}
|
|
|
|
// Start with the biggest mesh first so we can always reuse the same vertex/index buffers.
|
|
// This will decrease the number of allocations backed by newly allocated memory from the OS,
|
|
// which will reduce soft page faults while copying into that memory.
|
|
// Soft page faults are now incredibly expensive on Windows 10.
|
|
Algo::SortBy(
|
|
ProcessingOrder,
|
|
[&MeshSettings](const uint32 Index){ return MeshSettings[Index]->MeshDescription ? MeshSettings[Index]->MeshDescription->Vertices().Num() : 0; },
|
|
TGreater<>()
|
|
);
|
|
|
|
Output.SetNum(NumMaterials);
|
|
|
|
struct FPipelineContext
|
|
{
|
|
typedef TFunction<void (FRHICommandListImmediate& RHICmdList)> FReadCommand;
|
|
FReadCommand ReadCommand;
|
|
};
|
|
|
|
// Distance between the command sent to rendering and the GPU read-back of the result
|
|
// to minimize sync time waiting on GPU.
|
|
const int32 PipelineDepth = 16;
|
|
int32 PipelineIndex = 0;
|
|
FPipelineContext PipelineContext[PipelineDepth];
|
|
|
|
// This will create and prepare FMeshMaterialRenderItem for each property sizes we're going to need
|
|
auto PrepareRenderItems_AnyThread =
|
|
[&](int32 MaterialIndex)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(PrepareRenderItems);
|
|
|
|
TMap<FMaterialBakingModuleImpl::FRenderItemKey, FMeshMaterialRenderItem*>* RenderItems = new TMap<FRenderItemKey, FMeshMaterialRenderItem *>();
|
|
const FMaterialDataEx* CurrentMaterialSettings = MaterialSettings[MaterialIndex];
|
|
const FMeshData* CurrentMeshSettings = MeshSettings[MaterialIndex];
|
|
|
|
for (TMap<FMaterialPropertyEx, FIntPoint>::TConstIterator PropertySizeIterator = CurrentMaterialSettings->PropertySizes.CreateConstIterator(); PropertySizeIterator; ++PropertySizeIterator)
|
|
{
|
|
FRenderItemKey RenderItemKey(CurrentMeshSettings, PropertySizeIterator.Value());
|
|
if (RenderItems->Find(RenderItemKey) == nullptr)
|
|
{
|
|
RenderItems->Add(RenderItemKey, new FMeshMaterialRenderItem(PropertySizeIterator.Value(), CurrentMeshSettings, &MaterialBakingDynamicMeshBufferAllocator));
|
|
}
|
|
}
|
|
|
|
return RenderItems;
|
|
};
|
|
|
|
// We reuse the pipeline depth to prepare render items in advance to avoid stalling the game thread
|
|
int NextRenderItem = 0;
|
|
TFuture<TMap<FRenderItemKey, FMeshMaterialRenderItem*>*> PreparedRenderItems[PipelineDepth];
|
|
for (; NextRenderItem < NumMaterials && NextRenderItem < PipelineDepth; ++NextRenderItem)
|
|
{
|
|
PreparedRenderItems[NextRenderItem] =
|
|
Async(
|
|
EAsyncExecution::ThreadPool,
|
|
[&PrepareRenderItems_AnyThread, &ProcessingOrder, NextRenderItem]()
|
|
{
|
|
return PrepareRenderItems_AnyThread(ProcessingOrder[NextRenderItem]);
|
|
}
|
|
);
|
|
}
|
|
|
|
// Create all material proxies right away to start compiling shaders asynchronously and avoid stalling the baking process as much as possible
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(CreateMaterialProxies)
|
|
|
|
for (int32 Index = 0; Index < NumMaterials; ++Index)
|
|
{
|
|
int32 MaterialIndex = ProcessingOrder[Index];
|
|
const FMaterialDataEx* CurrentMaterialSettings = MaterialSettings[MaterialIndex];
|
|
|
|
TArray<UTexture*> MaterialTextures;
|
|
CurrentMaterialSettings->Material->GetUsedTextures(MaterialTextures, EMaterialQualityLevel::Num, true, GMaxRHIFeatureLevel, true);
|
|
|
|
// Force load materials used by the current material
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(LoadTexturesForMaterial)
|
|
|
|
FTextureCompilingManager::Get().FinishCompilation(MaterialTextures);
|
|
|
|
for (UTexture* Texture : MaterialTextures)
|
|
{
|
|
if (Texture != NULL)
|
|
{
|
|
UTexture2D* Texture2D = Cast<UTexture2D>(Texture);
|
|
if (Texture2D)
|
|
{
|
|
Texture2D->SetForceMipLevelsToBeResident(30.0f);
|
|
Texture2D->WaitForStreaming();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (TMap<FMaterialPropertyEx, FIntPoint>::TConstIterator PropertySizeIterator = CurrentMaterialSettings->PropertySizes.CreateConstIterator(); PropertySizeIterator; ++PropertySizeIterator)
|
|
{
|
|
// They will be stored in the pool and compiled asynchronously
|
|
CreateMaterialProxy(CurrentMaterialSettings, PropertySizeIterator.Key());
|
|
}
|
|
}
|
|
}
|
|
|
|
TAtomic<uint32> NumTasks(0);
|
|
FStagingBufferPool StagingBufferPool;
|
|
|
|
for (int32 Index = 0; Index < NumMaterials; ++Index)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(BakeOneMaterial)
|
|
|
|
Progress.EnterProgressFrame(1.0f, FText::Format(LOCTEXT("BakingMaterial", "Baking Material {0}/{1}"), Index, NumMaterials));
|
|
|
|
int32 MaterialIndex = ProcessingOrder[Index];
|
|
TMap<FRenderItemKey, FMeshMaterialRenderItem*>* RenderItems;
|
|
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(WaitOnPreparedRenderItems)
|
|
RenderItems = PreparedRenderItems[Index % PipelineDepth].Get();
|
|
}
|
|
|
|
// Prepare the next render item in advance
|
|
if (NextRenderItem < NumMaterials)
|
|
{
|
|
check((NextRenderItem % PipelineDepth) == (Index % PipelineDepth));
|
|
PreparedRenderItems[NextRenderItem % PipelineDepth] =
|
|
Async(
|
|
EAsyncExecution::ThreadPool,
|
|
[&PrepareRenderItems_AnyThread, NextMaterialIndex = ProcessingOrder[NextRenderItem]]()
|
|
{
|
|
return PrepareRenderItems_AnyThread(NextMaterialIndex);
|
|
}
|
|
);
|
|
NextRenderItem++;
|
|
}
|
|
|
|
const FMaterialDataEx* CurrentMaterialSettings = MaterialSettings[MaterialIndex];
|
|
const FMeshData* CurrentMeshSettings = MeshSettings[MaterialIndex];
|
|
FBakeOutputEx& CurrentOutput = Output[MaterialIndex];
|
|
|
|
TRACE_CPUPROFILER_EVENT_SCOPE_TEXT(*CurrentMaterialSettings->Material->GetName())
|
|
|
|
TArray<FMaterialPropertyEx> MaterialPropertiesToBakeOut;
|
|
CurrentMaterialSettings->PropertySizes.GenerateKeyArray(MaterialPropertiesToBakeOut);
|
|
|
|
const int32 NumPropertiesToRender = MaterialPropertiesToBakeOut.Num();
|
|
if (NumPropertiesToRender > 0)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(RenderOneMaterial)
|
|
|
|
// Ensure data in memory will not change place passed this point to avoid race conditions
|
|
CurrentOutput.PropertySizes = CurrentMaterialSettings->PropertySizes;
|
|
for (int32 PropertyIndex = 0; PropertyIndex < NumPropertiesToRender; ++PropertyIndex)
|
|
{
|
|
const FMaterialPropertyEx& Property = MaterialPropertiesToBakeOut[PropertyIndex];
|
|
CurrentOutput.PropertyData.Add(Property);
|
|
if (bEmissiveHDR && Property == MP_EmissiveColor)
|
|
{
|
|
CurrentOutput.HDRPropertyData.Add(Property);
|
|
}
|
|
}
|
|
|
|
for (int32 PropertyIndex = 0; PropertyIndex < NumPropertiesToRender; ++PropertyIndex)
|
|
{
|
|
const FMaterialPropertyEx& Property = MaterialPropertiesToBakeOut[PropertyIndex];
|
|
|
|
TRACE_CPUPROFILER_EVENT_SCOPE_TEXT(*Property.ToString())
|
|
|
|
FExportMaterialProxy* ExportMaterialProxy = CreateMaterialProxy(CurrentMaterialSettings, Property);
|
|
|
|
if (!ExportMaterialProxy->IsCompilationFinished())
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(WaitForMaterialProxyCompilation)
|
|
ExportMaterialProxy->FinishCompilation();
|
|
}
|
|
|
|
// Lookup gamma and format settings for property, if not found use default values
|
|
const EPropertyColorSpace* OverrideColorSpace = PerPropertyColorSpace.Find(Property);
|
|
const EPropertyColorSpace ColorSpace = OverrideColorSpace ? *OverrideColorSpace : DefaultColorSpace;
|
|
const EPixelFormat PixelFormat = PerPropertyFormat.Contains(Property) ? PerPropertyFormat[Property] : PF_B8G8R8A8;
|
|
|
|
// It is safe to reuse the same render target for each draw pass since they all execute sequentially on the GPU and are copied to staging buffers before
|
|
// being reused.
|
|
UTextureRenderTarget2D* RenderTarget = CreateRenderTarget((ColorSpace == EPropertyColorSpace::Linear), PixelFormat, CurrentOutput.PropertySizes[Property]);
|
|
if (RenderTarget != nullptr)
|
|
{
|
|
// Perform everything left of the operation directly on the render thread since we need to modify some RenderItem's properties
|
|
// for each render pass and we can't do that without costly synchronization (flush) between the game thread and render thread.
|
|
// Everything slow to execute has already been prepared on the game thread anyway.
|
|
ENQUEUE_RENDER_COMMAND(RenderOneMaterial)(
|
|
[this, RenderItems, RenderTarget, Property, ExportMaterialProxy, &PipelineContext, PipelineIndex, &StagingBufferPool, &NumTasks, bSaveIntermediateTextures, &MaterialSettings, &MeshSettings, MaterialIndex, &Output](FRHICommandListImmediate& RHICmdList)
|
|
{
|
|
const FMaterialDataEx* CurrentMaterialSettings = MaterialSettings[MaterialIndex];
|
|
const FMeshData* CurrentMeshSettings = MeshSettings[MaterialIndex];
|
|
|
|
FMeshMaterialRenderItem& RenderItem = *RenderItems->FindChecked(FRenderItemKey(CurrentMeshSettings, FIntPoint(RenderTarget->GetSurfaceWidth(), RenderTarget->GetSurfaceHeight())));
|
|
|
|
FSceneViewFamily ViewFamily(FSceneViewFamily::ConstructionValues(RenderTarget->GetRenderTargetResource(), nullptr,
|
|
FEngineShowFlags(ESFIM_Game))
|
|
.SetTime(FGameTime())
|
|
.SetGammaCorrection(RenderTarget->GetRenderTargetResource()->GetDisplayGamma()));
|
|
|
|
RenderItem.MaterialRenderProxy = ExportMaterialProxy;
|
|
RenderItem.ViewFamily = &ViewFamily;
|
|
|
|
FTextureRenderTargetResource* RenderTargetResource = RenderTarget->GetRenderTargetResource();
|
|
FCanvas Canvas(RenderTargetResource, nullptr, FGameTime::GetTimeSinceAppStart(), GMaxRHIFeatureLevel);
|
|
Canvas.SetAllowedModes(FCanvas::Allow_Flush);
|
|
Canvas.SetRenderTargetRect(FIntRect(0, 0, RenderTarget->GetSurfaceWidth(), RenderTarget->GetSurfaceHeight()));
|
|
Canvas.SetBaseTransform(Canvas.CalcBaseTransform2D(RenderTarget->GetSurfaceWidth(), RenderTarget->GetSurfaceHeight()));
|
|
|
|
// Virtual textures may require repeated rendering to warm up.
|
|
int32 WarmupIterationCount = 1;
|
|
if (UseVirtualTexturing(ViewFamily.GetFeatureLevel()))
|
|
{
|
|
const FMaterial& MeshMaterial = ExportMaterialProxy->GetIncompleteMaterialWithFallback(ViewFamily.GetFeatureLevel());
|
|
if (!MeshMaterial.GetUniformVirtualTextureExpressions().IsEmpty())
|
|
{
|
|
WarmupIterationCount = CVarMaterialBakingVTWarmupFrames.GetValueOnAnyThread();
|
|
}
|
|
}
|
|
|
|
// Do rendering
|
|
{
|
|
RenderCaptureInterface::FScopedCapture RenderCapture(CVarMaterialBakingRDOCCapture.GetValueOnAnyThread() == 1, &RHICmdList, TEXT("MaterialBaking"));
|
|
|
|
for (int WarmupIndex = 0; WarmupIndex < WarmupIterationCount; ++WarmupIndex)
|
|
{
|
|
Canvas.Clear(RenderTarget->ClearColor);
|
|
FCanvas::FCanvasSortElement& SortElement = Canvas.GetSortElement(Canvas.TopDepthSortKey());
|
|
SortElement.RenderBatchArray.Add(&RenderItem);
|
|
Canvas.Flush_RenderThread(RHICmdList);
|
|
SortElement.RenderBatchArray.Empty();
|
|
|
|
RHICmdList.ImmediateFlush(EImmediateFlushType::FlushRHIThreadFlushResources);
|
|
}
|
|
}
|
|
|
|
FTexture2DRHIRef StagingBufferRef = StagingBufferPool.CreateStagingBuffer_RenderThread(RHICmdList, RenderTargetResource->GetSizeX(), RenderTargetResource->GetSizeY(), RenderTarget->GetFormat(), RenderTarget->IsSRGB());
|
|
FGPUFenceRHIRef GPUFence = RHICreateGPUFence(TEXT("MaterialBackingFence"));
|
|
TransitionAndCopyTexture(RHICmdList, RenderTargetResource->GetRenderTargetTexture(), StagingBufferRef, {});
|
|
RHICmdList.WriteGPUFence(GPUFence);
|
|
|
|
// Prepare a lambda for final processing that will be executed asynchronously
|
|
NumTasks++;
|
|
auto FinalProcessing_AnyThread =
|
|
[&NumTasks, bSaveIntermediateTextures, CurrentMaterialSettings, &StagingBufferPool, &Output, Property, MaterialIndex, bEmissiveHDR = bEmissiveHDR](FTexture2DRHIRef& StagingBuffer, void * Data, int32 DataWidth, int32 DataHeight)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FinalProcessing)
|
|
|
|
FBakeOutputEx& CurrentOutput = Output[MaterialIndex];
|
|
TArray<FColor>& OutputColor = CurrentOutput.PropertyData[Property];
|
|
FIntPoint& OutputSize = CurrentOutput.PropertySizes[Property];
|
|
|
|
OutputColor.SetNum(OutputSize.X * OutputSize.Y);
|
|
|
|
if (Property.Type == MP_EmissiveColor)
|
|
{
|
|
// Only one thread will write to CurrentOutput.EmissiveScale since there can be only one emissive channel property per FBakeOutputEx
|
|
FMaterialBakingModule::ProcessEmissiveOutput((const FFloat16Color*)Data, DataWidth, OutputSize, OutputColor, CurrentOutput.EmissiveScale);
|
|
|
|
if (bEmissiveHDR)
|
|
{
|
|
TArray<FFloat16Color>& OutputHDRColor = CurrentOutput.HDRPropertyData[Property];
|
|
OutputHDRColor.SetNum(OutputSize.X * OutputSize.Y);
|
|
ConvertRawR16G16B16A16FDataToFFloat16Color(OutputSize.X, OutputSize.Y, (uint8*)Data, DataWidth * sizeof(FFloat16Color), OutputHDRColor.GetData());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(ConvertRawB8G8R8A8DataToFColor)
|
|
|
|
check(StagingBuffer->GetFormat() == PF_B8G8R8A8);
|
|
ConvertRawB8G8R8A8DataToFColor(OutputSize.X, OutputSize.Y, (uint8*)Data, DataWidth * sizeof(FColor), OutputColor.GetData());
|
|
}
|
|
|
|
// We can't unmap ourself since we're not on the render thread
|
|
StagingBufferPool.ReleaseStagingBufferForUnmap_AnyThread(StagingBuffer);
|
|
|
|
if (CurrentMaterialSettings->bPerformShrinking)
|
|
{
|
|
FMaterialBakingHelpers::PerformShrinking(OutputColor, OutputSize.X, OutputSize.Y);
|
|
}
|
|
|
|
if (CurrentMaterialSettings->bPerformBorderSmear)
|
|
{
|
|
FMaterialBakingHelpers::PerformUVBorderSmear(OutputColor, OutputSize.X, OutputSize.Y);
|
|
}
|
|
#if WITH_EDITOR
|
|
// If saving intermediates is turned on
|
|
if (bSaveIntermediateTextures)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(SaveIntermediateTextures)
|
|
FString TrimmedPropertyName = Property.ToString();
|
|
|
|
const FString DirectoryPath = FPaths::ConvertRelativePathToFull(FPaths::ProjectIntermediateDir() + TEXT("MaterialBaking/"));
|
|
FString FilenameString = FString::Printf(TEXT("%s%s-%d-%s.bmp"), *DirectoryPath, *CurrentMaterialSettings->Material->GetName(), MaterialIndex, *TrimmedPropertyName);
|
|
FFileHelper::CreateBitmap(*FilenameString, CurrentOutput.PropertySizes[Property].X, CurrentOutput.PropertySizes[Property].Y, CurrentOutput.PropertyData[Property].GetData());
|
|
}
|
|
#endif // WITH_EDITOR
|
|
NumTasks--;
|
|
};
|
|
|
|
// Run previous command if we're going to overwrite it meaning pipeline depth has been reached
|
|
if (PipelineContext[PipelineIndex].ReadCommand)
|
|
{
|
|
PipelineContext[PipelineIndex].ReadCommand(RHICmdList);
|
|
}
|
|
|
|
// Generate a texture reading command that will be executed once it reaches the end of the pipeline
|
|
PipelineContext[PipelineIndex].ReadCommand =
|
|
[FinalProcessing_AnyThread, StagingBufferRef = MoveTemp(StagingBufferRef), GPUFence = MoveTemp(GPUFence)](FRHICommandListImmediate& RHICmdList) mutable
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(MapAndEnqueue)
|
|
|
|
void * Data = nullptr;
|
|
int32 Width; int32 Height;
|
|
RHICmdList.MapStagingSurface(StagingBufferRef, GPUFence.GetReference(), Data, Width, Height);
|
|
|
|
// Schedule the copy and processing on another thread to free up the render thread as much as possible
|
|
Async(
|
|
EAsyncExecution::ThreadPool,
|
|
[FinalProcessing_AnyThread, Data, Width, Height, StagingBufferRef = MoveTemp(StagingBufferRef)]() mutable
|
|
{
|
|
FinalProcessing_AnyThread(StagingBufferRef, Data, Width, Height);
|
|
}
|
|
);
|
|
};
|
|
}
|
|
);
|
|
|
|
PipelineIndex = (PipelineIndex + 1) % PipelineDepth;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Destroying Render Items must happen on the render thread to ensure
|
|
// they are not used anymore.
|
|
ENQUEUE_RENDER_COMMAND(DestroyRenderItems)(
|
|
[RenderItems](FRHICommandListImmediate& RHICmdList)
|
|
{
|
|
for (auto RenderItem : (*RenderItems))
|
|
{
|
|
delete RenderItem.Value;
|
|
}
|
|
|
|
delete RenderItems;
|
|
}
|
|
);
|
|
}
|
|
|
|
ENQUEUE_RENDER_COMMAND(ProcessRemainingReads)(
|
|
[&PipelineContext, PipelineDepth, PipelineIndex](FRHICommandListImmediate& RHICmdList)
|
|
{
|
|
// Enqueue remaining reads
|
|
for (int32 Index = 0; Index < PipelineDepth; Index++)
|
|
{
|
|
int32 LocalPipelineIndex = (PipelineIndex + Index) % PipelineDepth;
|
|
|
|
if (PipelineContext[LocalPipelineIndex].ReadCommand)
|
|
{
|
|
PipelineContext[LocalPipelineIndex].ReadCommand(RHICmdList);
|
|
}
|
|
}
|
|
}
|
|
);
|
|
|
|
// Wait until every tasks have been queued so that NumTasks is only decreasing
|
|
FlushRenderingCommands();
|
|
|
|
// Wait for any remaining final processing tasks
|
|
while (NumTasks.Load(EMemoryOrder::Relaxed) > 0)
|
|
{
|
|
FPlatformProcess::Sleep(0.1f);
|
|
}
|
|
|
|
// Wait for all tasks to have been processed before clearing the staging buffers
|
|
FlushRenderingCommands();
|
|
|
|
ENQUEUE_RENDER_COMMAND(ClearStagingBufferPool)(
|
|
[&StagingBufferPool](FRHICommandListImmediate& RHICmdList)
|
|
{
|
|
StagingBufferPool.Clear_RenderThread(RHICmdList);
|
|
}
|
|
);
|
|
|
|
// Wait for StagingBufferPool clear to have executed before exiting the function
|
|
FlushRenderingCommands();
|
|
|
|
if (!CVarUseMaterialProxyCaching.GetValueOnAnyThread())
|
|
{
|
|
CleanupMaterialProxies();
|
|
}
|
|
}
|
|
|
|
bool FMaterialBakingModule::SetupMaterialBakeSettings(TArray<TWeakObjectPtr<UObject>>& OptionObjects, int32 NumLODs)
|
|
{
|
|
TSharedRef<SWindow> Window = SNew(SWindow)
|
|
.Title(LOCTEXT("WindowTitle", "Material Baking Options"))
|
|
.SizingRule(ESizingRule::Autosized);
|
|
|
|
TSharedPtr<SMaterialOptions> Options;
|
|
|
|
Window->SetContent
|
|
(
|
|
SAssignNew(Options, SMaterialOptions)
|
|
.WidgetWindow(Window)
|
|
.NumLODs(NumLODs)
|
|
.SettingsObjects(OptionObjects)
|
|
);
|
|
|
|
TSharedPtr<SWindow> ParentWindow;
|
|
if (FModuleManager::Get().IsModuleLoaded("MainFrame"))
|
|
{
|
|
IMainFrameModule& MainFrame = FModuleManager::LoadModuleChecked<IMainFrameModule>("MainFrame");
|
|
ParentWindow = MainFrame.GetParentWindow();
|
|
FSlateApplication::Get().AddModalWindow(Window, ParentWindow, false);
|
|
return !Options->WasUserCancelled();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void FMaterialBakingModule::SetEmissiveHDR(bool bHDR)
|
|
{
|
|
bEmissiveHDR = bHDR;
|
|
}
|
|
|
|
void FMaterialBakingModule::SetLinearBake(bool bCorrectLinear)
|
|
{
|
|
// PerPropertyGamma ultimately sets whether the render target is linear
|
|
PerPropertyColorSpace.Reset();
|
|
if (bCorrectLinear)
|
|
{
|
|
DefaultColorSpace = EPropertyColorSpace::Linear;
|
|
PerPropertyColorSpace.Add(MP_BaseColor, EPropertyColorSpace::sRGB);
|
|
PerPropertyColorSpace.Add(MP_EmissiveColor, EPropertyColorSpace::sRGB);
|
|
PerPropertyColorSpace.Add(MP_SubsurfaceColor, EPropertyColorSpace::sRGB);
|
|
}
|
|
else
|
|
{
|
|
DefaultColorSpace = EPropertyColorSpace::sRGB;
|
|
PerPropertyColorSpace.Add(MP_Normal, EPropertyColorSpace::Linear);
|
|
PerPropertyColorSpace.Add(MP_Opacity, EPropertyColorSpace::Linear);
|
|
PerPropertyColorSpace.Add(MP_OpacityMask, EPropertyColorSpace::Linear);
|
|
PerPropertyColorSpace.Add(TEXT("ClearCoatBottomNormal"), EPropertyColorSpace::Linear);
|
|
}
|
|
}
|
|
|
|
static void DeleteCachedMaterialProxy(FExportMaterialProxy* Proxy)
|
|
{
|
|
ENQUEUE_RENDER_COMMAND(DeleteCachedMaterialProxy)(
|
|
[Proxy](FRHICommandListImmediate& RHICmdList)
|
|
{
|
|
delete Proxy;
|
|
});
|
|
}
|
|
|
|
void FMaterialBakingModule::CleanupMaterialProxies()
|
|
{
|
|
for (auto Iterator : MaterialProxyPool)
|
|
{
|
|
DeleteCachedMaterialProxy(Iterator.Value.Value);
|
|
}
|
|
MaterialProxyPool.Reset();
|
|
}
|
|
|
|
UTextureRenderTarget2D* FMaterialBakingModule::CreateRenderTarget(bool bInForceLinearGamma, EPixelFormat InPixelFormat, const FIntPoint& InTargetSize)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FMaterialBakingModule::CreateRenderTarget)
|
|
|
|
UTextureRenderTarget2D* RenderTarget = nullptr;
|
|
const int32 MaxTextureSize = 1 << (MAX_TEXTURE_MIP_COUNT - 1); // Don't use GetMax2DTextureDimension() as this is for the RHI only.
|
|
const FIntPoint ClampedTargetSize(FMath::Clamp(InTargetSize.X, 1, MaxTextureSize), FMath::Clamp(InTargetSize.Y, 1, MaxTextureSize));
|
|
auto RenderTargetComparison = [bInForceLinearGamma, InPixelFormat, ClampedTargetSize](const UTextureRenderTarget2D* CompareRenderTarget) -> bool
|
|
{
|
|
return (CompareRenderTarget->SizeX == ClampedTargetSize.X && CompareRenderTarget->SizeY == ClampedTargetSize.Y && CompareRenderTarget->OverrideFormat == InPixelFormat && CompareRenderTarget->bForceLinearGamma == bInForceLinearGamma);
|
|
};
|
|
|
|
// Find any pooled render target with suitable properties.
|
|
UTextureRenderTarget2D** FindResult = RenderTargetPool.FindByPredicate(RenderTargetComparison);
|
|
|
|
if (FindResult)
|
|
{
|
|
RenderTarget = *FindResult;
|
|
}
|
|
else
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(CreateNewRenderTarget)
|
|
|
|
// Not found - create a new one.
|
|
RenderTarget = NewObject<UTextureRenderTarget2D>();
|
|
check(RenderTarget);
|
|
RenderTarget->AddToRoot();
|
|
RenderTarget->ClearColor = FLinearColor(1.0f, 0.0f, 1.0f);
|
|
RenderTarget->ClearColor.A = 1.0f;
|
|
RenderTarget->TargetGamma = 0.0f;
|
|
RenderTarget->InitCustomFormat(ClampedTargetSize.X, ClampedTargetSize.Y, InPixelFormat, bInForceLinearGamma);
|
|
|
|
RenderTargetPool.Add(RenderTarget);
|
|
}
|
|
|
|
checkf(RenderTarget != nullptr, TEXT("Unable to create or find valid render target"));
|
|
return RenderTarget;
|
|
}
|
|
|
|
FExportMaterialProxy* FMaterialBakingModule::CreateMaterialProxy(const FMaterialDataEx* MaterialSettings, const FMaterialPropertyEx& Property)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FMaterialBakingModule::CreateMaterialProxy)
|
|
|
|
FExportMaterialProxy* Proxy = nullptr;
|
|
|
|
// Find all pooled material proxy matching this material
|
|
TArray<FMaterialPoolValue> Entries;
|
|
MaterialProxyPool.MultiFind(MaterialSettings->Material, Entries);
|
|
|
|
// Look for the matching property
|
|
for (FMaterialPoolValue& Entry : Entries)
|
|
{
|
|
if (Entry.Key == Property && Entry.Value->bTangentSpaceNormal == MaterialSettings->bTangentSpaceNormal)
|
|
{
|
|
Proxy = Entry.Value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Not found, create a new entry
|
|
if (Proxy == nullptr)
|
|
{
|
|
Proxy = new FExportMaterialProxy(MaterialSettings->Material, Property.Type, Property.CustomOutput.ToString(), false /* bInSynchronousCompilation */, MaterialSettings->bTangentSpaceNormal);
|
|
MaterialProxyPool.Add(MaterialSettings->Material, FMaterialPoolValue(Property, Proxy));
|
|
}
|
|
|
|
return Proxy;
|
|
}
|
|
|
|
void FMaterialBakingModule::ProcessEmissiveOutput(const FFloat16Color* Color16, int32 Color16Pitch, const FIntPoint& OutputSize, TArray<FColor>& OutputColor, float& EmissiveScale)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FMaterialBakingModule::ProcessEmissiveOutput)
|
|
|
|
const int32 NumThreads = [&]()
|
|
{
|
|
return FPlatformProcess::SupportsMultithreading() ? FPlatformMisc::NumberOfCores() : 1;
|
|
}();
|
|
|
|
float* MaxValue = new float[NumThreads];
|
|
FMemory::Memset(MaxValue, 0, NumThreads * sizeof(MaxValue[0]));
|
|
const int32 LinesPerThread = FMath::CeilToInt((float)OutputSize.Y / (float)NumThreads);
|
|
|
|
// Find maximum float value across texture
|
|
ParallelFor(NumThreads, [&Color16, LinesPerThread, MaxValue, OutputSize, Color16Pitch](int32 Index)
|
|
{
|
|
const int32 EndY = FMath::Min((Index + 1) * LinesPerThread, OutputSize.Y);
|
|
float& CurrentMaxValue = MaxValue[Index];
|
|
const FFloat16Color MagentaFloat16 = FFloat16Color(FLinearColor(1.0f, 0.0f, 1.0f));
|
|
for (int32 PixelY = Index * LinesPerThread; PixelY < EndY; ++PixelY)
|
|
{
|
|
const int32 SrcYOffset = PixelY * Color16Pitch;
|
|
for (int32 PixelX = 0; PixelX < OutputSize.X; PixelX++)
|
|
{
|
|
const FFloat16Color& Pixel16 = Color16[PixelX + SrcYOffset];
|
|
// Find maximum channel value across texture
|
|
if (!(Pixel16 == MagentaFloat16))
|
|
{
|
|
CurrentMaxValue = FMath::Max(CurrentMaxValue, FMath::Max3(Pixel16.R.GetFloat(), Pixel16.G.GetFloat(), Pixel16.B.GetFloat()));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
const float GlobalMaxValue = [&MaxValue, NumThreads]
|
|
{
|
|
float TempValue = 0.0f;
|
|
for (int32 ThreadIndex = 0; ThreadIndex < NumThreads; ++ThreadIndex)
|
|
{
|
|
TempValue = FMath::Max(TempValue, MaxValue[ThreadIndex]);
|
|
}
|
|
|
|
return TempValue;
|
|
}();
|
|
|
|
if (GlobalMaxValue <= 0.01f)
|
|
{
|
|
// Black emissive, drop it
|
|
}
|
|
|
|
// Now convert Float16 to Color using the scale
|
|
OutputColor.SetNumUninitialized(OutputSize.X * OutputSize.Y);
|
|
const float Scale = 255.0f / GlobalMaxValue;
|
|
ParallelFor(NumThreads, [&Color16, LinesPerThread, &OutputColor, OutputSize, Color16Pitch, Scale](int32 Index)
|
|
{
|
|
const FFloat16Color MagentaFloat16 = FFloat16Color(FLinearColor(1.0f, 0.0f, 1.0f));
|
|
|
|
const int32 EndY = FMath::Min((Index + 1) * LinesPerThread, OutputSize.Y);
|
|
for (int32 PixelY = Index * LinesPerThread; PixelY < EndY; ++PixelY)
|
|
{
|
|
const int32 SrcYOffset = PixelY * Color16Pitch;
|
|
const int32 DstYOffset = PixelY * OutputSize.X;
|
|
|
|
for (int32 PixelX = 0; PixelX < OutputSize.X; PixelX++)
|
|
{
|
|
const FFloat16Color& Pixel16 = Color16[PixelX + SrcYOffset];
|
|
FColor& Pixel8 = OutputColor[PixelX + DstYOffset];
|
|
|
|
if (Pixel16 == MagentaFloat16)
|
|
{
|
|
Pixel8.R = 255;
|
|
Pixel8.G = 0;
|
|
Pixel8.B = 255;
|
|
}
|
|
else
|
|
{
|
|
Pixel8.R = (uint8)FMath::RoundToInt(Pixel16.R.GetFloat() * Scale);
|
|
Pixel8.G = (uint8)FMath::RoundToInt(Pixel16.G.GetFloat() * Scale);
|
|
Pixel8.B = (uint8)FMath::RoundToInt(Pixel16.B.GetFloat() * Scale);
|
|
}
|
|
|
|
Pixel8.A = 255;
|
|
}
|
|
}
|
|
});
|
|
|
|
// This scale will be used in the proxy material to get the original range of emissive values outside of 0-1
|
|
EmissiveScale = GlobalMaxValue;
|
|
}
|
|
|
|
void FMaterialBakingModule::OnObjectModified(UObject* Object)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FMaterialBakingModule::OnObjectModified)
|
|
|
|
if (CVarUseMaterialProxyCaching.GetValueOnAnyThread())
|
|
{
|
|
UMaterialInterface* MaterialToInvalidate = Cast<UMaterialInterface>(Object);
|
|
if (!MaterialToInvalidate)
|
|
{
|
|
// Check to see if the object is a material editor instance constant and if so, retrieve its source instance
|
|
UMaterialEditorInstanceConstant* EditorInstance = Cast<UMaterialEditorInstanceConstant>(Object);
|
|
if (EditorInstance && EditorInstance->SourceInstance)
|
|
{
|
|
MaterialToInvalidate = EditorInstance->SourceInstance;
|
|
}
|
|
}
|
|
|
|
if (MaterialToInvalidate)
|
|
{
|
|
// Search our proxy pool for materials or material instances that refer to MaterialToInvalidate
|
|
for (auto It = MaterialProxyPool.CreateIterator(); It; ++It)
|
|
{
|
|
TWeakObjectPtr<UMaterialInterface> PoolMaterialPtr = It.Key();
|
|
|
|
// Remove stale entries from the pool
|
|
bool bMustDelete = PoolMaterialPtr.IsValid();
|
|
if (!bMustDelete)
|
|
{
|
|
bMustDelete = PoolMaterialPtr == MaterialToInvalidate;
|
|
}
|
|
|
|
// No match - Test the MaterialInstance hierarchy
|
|
if (!bMustDelete)
|
|
{
|
|
UMaterialInstance* MaterialInstance = Cast<UMaterialInstance>(PoolMaterialPtr);
|
|
while (!bMustDelete && MaterialInstance && MaterialInstance->Parent != nullptr)
|
|
{
|
|
bMustDelete = MaterialInstance->Parent == MaterialToInvalidate;
|
|
MaterialInstance = Cast<UMaterialInstance>(MaterialInstance->Parent);
|
|
}
|
|
}
|
|
|
|
// We have a match, remove the entry from our pool
|
|
if (bMustDelete)
|
|
{
|
|
DeleteCachedMaterialProxy(It.Value().Value);
|
|
It.RemoveCurrent();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FMaterialBakingModule::OnPreGarbageCollect()
|
|
{
|
|
CleanupMaterialProxies();
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE //"MaterialBakingModule"
|