Files
UnrealEngineUWP/Engine/Source/Developer/MaterialBaking/Private/MaterialBakingModule.cpp
aidin abedi 21543249dd Fix material baking incorrectly stating that EmissiveColor is baked as sRGB while in reality it's always baked as linear because of the HDR rendertarget.
Please note that this change doesn't actually affect any baking results, because EmissiveColor always reads linear data from its HDR rendertarget regardless of PerPropertyColorSpace. The fix only affects the value returned by FMaterialBakingModule::IsLinearBake for EmissiveColor, which until now has been incorrect due to the missmatch between PerPropertyColorSpace and how EmissiveColor is actually baked.

#jira UE-184310
#review-25298709 @Jason.Nadro
#preflight 6450d1a70e33f2d51d22b142

[CL 25301160 by aidin abedi in ue5-main branch]
2023-05-02 10:11:29 -04:00

1408 lines
51 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "MaterialBakingModule.h"
#include "ContentStreaming.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 "Serialization/ArchiveCrc32.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."));
static TAutoConsoleVariable<bool> CVarMaterialBakingForceDisableEmissiveScaling(
TEXT("MaterialBaking.ForceDisableEmissiveScaling"),
false,
TEXT("If set to true, values stored in the emissive textures will be clamped to the [0, 1] range rather than being normalized and scaled back using the EmissiveScale material static parameter."));
namespace
{
// 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));
}
}
/** Helper for emissive color conversion to Output */
static void ProcessEmissiveOutput(const FFloat16Color* Color16, int32 Color16Pitch, const FIntPoint& OutputSize, TArray<FColor>& Output, float& EmissiveScale, const FColor& BackgroundColor);
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_Refraction, 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(MP_ShadingModel, PF_B8G8R8A8);
PerPropertyFormat.Add(FMaterialPropertyEx::ClearCoatBottomNormal, PF_B8G8R8A8);
PerPropertyFormat.Add(FMaterialPropertyEx::TransmittanceColor, 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();
CleanupRenderTargets();
}
uint32 FMaterialBakingModule::GetCRC() const
{
FArchiveCrc32 Ar;
// Base key, changing this will force a rebuild of all HLODs that are relying on material baking.
FString ModuleBaseKey = "4167B9A126CA47B3A6EAB520B40A66BB";
Ar << ModuleBaseKey;
bool bUseEmissiveHDR = bEmissiveHDR;
Ar << bUseEmissiveHDR;
uint8 ColorSpace = DefaultColorSpace;
Ar << ColorSpace;
int32 VTWarmupFrames = CVarMaterialBakingVTWarmupFrames.GetValueOnAnyThread();
Ar << VTWarmupFrames;
bool ForceDisableEmissiveScaling = CVarMaterialBakingForceDisableEmissiveScaling.GetValueOnAnyThread();
Ar << ForceDisableEmissiveScaling;
return Ar.GetCrc();
}
FMaterialDataEx ToMaterialDataEx(const FMaterialData& MaterialData)
{
FMaterialDataEx MaterialDataEx;
MaterialDataEx.Material = MaterialData.Material;
MaterialDataEx.bPerformBorderSmear = MaterialData.bPerformBorderSmear;
MaterialDataEx.bPerformShrinking = MaterialData.bPerformShrinking;
MaterialDataEx.bTangentSpaceNormal = MaterialData.bTangentSpaceNormal;
MaterialDataEx.BlendMode = MaterialData.BlendMode;
MaterialDataEx.BackgroundColor = MaterialData.BackgroundColor;
for (const TPair<EMaterialProperty, FIntPoint>& PropertySizePair : MaterialData.PropertySizes)
{
MaterialDataEx.PropertySizes.Add(PropertySizePair.Key, PropertySizePair.Value);
}
return MaterialDataEx;
}
FBakeOutput ToBakeOutput(FBakeOutputEx& BakeOutputEx)
{
FBakeOutput BakeOutput;
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));
}
return BakeOutput;
}
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)
{
MaterialDataExs.Emplace(ToMaterialDataEx(*MaterialData));
}
// 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)
{
Output.Emplace(ToBakeOutput(BakeOutputEx));
}
}
void FMaterialBakingModule::BakeMaterials(const TArray<FMaterialData*>& MaterialSettings, const TArray<FMeshData*>& MeshSettings, FBakeOutput& BakeOutput)
{
// Translate old material data to extended types
TArray<FMaterialDataEx> MaterialDataExs;
MaterialDataExs.Reserve(MaterialSettings.Num());
for (const FMaterialData* MaterialData : MaterialSettings)
{
MaterialDataExs.Emplace(ToMaterialDataEx(*MaterialData));
}
// Build an array of pointers to the extended type
TArray<FMaterialDataEx*> MaterialSettingsEx;
MaterialSettingsEx.Reserve(MaterialDataExs.Num());
for (FMaterialDataEx& MaterialDataEx : MaterialDataExs)
{
MaterialSettingsEx.Add(&MaterialDataEx);
}
FBakeOutputEx BakeOutputEx;
BakeMaterials(MaterialSettingsEx, MeshSettings, BakeOutputEx);
// Translate extended bake output to old type
BakeOutput = ToBakeOutput(BakeOutputEx);
}
class FMaterialBakingProcessor
{
public:
FMaterialBakingProcessor(FMaterialBakingModule& InMaterialBakingModule, const TArray<FMaterialDataEx*>& InMaterialSettings, const TArray<FMeshData*>& InMeshSettings)
: MaterialBakingModule(InMaterialBakingModule)
, MaterialSettings(InMaterialSettings)
, MeshSettings(InMeshSettings)
, NumMaterials(MaterialSettings.Num())
, bSaveIntermediateTextures(CVarSaveIntermediateTextures.GetValueOnAnyThread() == 1)
, bEmissiveHDR(InMaterialBakingModule.bEmissiveHDR)
{
checkf(MaterialSettings.Num() == MeshSettings.Num(), TEXT("Number of material settings does not match that of MeshSettings"));
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());
}
}
ComputeMeshProcessingOrder();
}
virtual ~FMaterialBakingProcessor() = default;
void BakeMaterials()
{
TRACE_CPUPROFILER_EVENT_SCOPE(FMaterialBakingModule::BakeMaterials)
// We reuse the pipeline depth to prepare render items in advance to avoid stalling the game thread
LaunchAsyncPrepareRenderItems(PipelineDepth);
// Create all material proxies right away to start compiling shaders asynchronously and avoid stalling the baking process as much as possible
CreateMaterialProxies();
// For each material
for (int32 Index = 0; Index < NumMaterials; ++Index)
{
const int32 MaterialIndex = ProcessingOrder[Index];
const FMaterialDataEx& CurrentMaterialSettings = *MaterialSettings[MaterialIndex];
if (!CurrentMaterialSettings.PropertySizes.IsEmpty())
{
const FMeshData* CurrentMeshSettings = MeshSettings[MaterialIndex];
FBakeOutputEx& CurrentOutput = GetBakeOutput(MaterialIndex);
TMap<FRenderItemKey, FMeshMaterialRenderItem*>* RenderItems = GetRenderItems(Index);
check(RenderItems && !RenderItems->IsEmpty());
// For each property
for (const auto& [Property, Size] : CurrentMaterialSettings.PropertySizes)
{
TRACE_CPUPROFILER_EVENT_SCOPE_TEXT(*Property.ToString())
FExportMaterialProxy* ExportMaterialProxy = MaterialBakingModule.CreateMaterialProxy(&CurrentMaterialSettings, Property);
if (!ExportMaterialProxy->IsCompilationFinished())
{
TRACE_CPUPROFILER_EVENT_SCOPE(WaitForMaterialProxyCompilation)
ExportMaterialProxy->FinishCompilation();
}
UTextureRenderTarget2D* RenderTarget = GetRenderTarget(Property, Size, CurrentMaterialSettings);
FMeshMaterialRenderItem* RenderItem = RenderItems->FindChecked(FRenderItemKey(CurrentMeshSettings, Size));
BakeMaterialProperty(CurrentMaterialSettings, Property, RenderItem, RenderTarget, ExportMaterialProxy, CurrentOutput);
}
// 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;
}
);
}
}
}
protected:
void PrepareBakeOutput(FMaterialDataEx* InMaterialSettings, FBakeOutputEx& BakeOutput)
{
BakeOutput.PropertySizes = InMaterialSettings->PropertySizes;
for (const auto& [Property, Size] : BakeOutput.PropertySizes)
{
BakeOutput.PropertyData.Add(Property);
if (bEmissiveHDR && Property == MP_EmissiveColor)
{
BakeOutput.HDRPropertyData.Add(Property);
}
}
}
UTextureRenderTarget2D* CreateRenderTarget(FMaterialPropertyEx InProperty, const FIntPoint& InTargetSize, bool bInUsePooledRenderTargets, const FColor& BackgroundColor)
{
return MaterialBakingModule.CreateRenderTarget(InProperty, InTargetSize, bInUsePooledRenderTargets, BackgroundColor);
}
void SaveIntermediateTextures(const FBakeOutputEx& BakeOutput, const FMaterialPropertyEx& Property, const FString& FilenameString)
{
#if WITH_EDITOR
// If saving intermediates is turned on
if (bSaveIntermediateTextures)
{
if (!BakeOutput.PropertyData[Property].IsEmpty())
{
static int32 SaveCount = 0;
TRACE_CPUPROFILER_EVENT_SCOPE(SaveIntermediateTextures)
FString TrimmedPropertyName = Property.ToString();
const FString DirectoryPath = FPaths::ConvertRelativePathToFull(FPaths::ProjectIntermediateDir() + TEXT("MaterialBaking/"));
FString FullPath = FString::Printf(TEXT("%s%s-%d-%s.bmp"), *DirectoryPath, *FilenameString, SaveCount++, *TrimmedPropertyName);
FFileHelper::CreateBitmap(*FullPath, BakeOutput.PropertySizes[Property].X, BakeOutput.PropertySizes[Property].Y, BakeOutput.PropertyData[Property].GetData());
}
}
#endif
}
private:
virtual FBakeOutputEx& GetBakeOutput(int32 InMaterialIndex) = 0;
virtual UTextureRenderTarget2D* GetRenderTarget(FMaterialPropertyEx InMaterialProperty, const FIntPoint& InRequiredSize, const FMaterialDataEx& InMaterialSettings) = 0;
virtual void OnMaterialPropertyBaked(const FMaterialDataEx& CurrentMaterialSettings, const FMaterialPropertyEx& Property, FMeshMaterialRenderItem* RenderItem, UTextureRenderTarget2D* RenderTarget, FExportMaterialProxy* ExportMaterialProxy, FBakeOutputEx& CurrentOutput) {}
void ComputeMeshProcessingOrder()
{
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,
[this](const uint32 Index) { return MeshSettings[Index]->MeshDescription ? MeshSettings[Index]->MeshDescription->Vertices().Num() : 0; },
TGreater<>()
);
}
// This will create and prepare FMeshMaterialRenderItem for each property sizes we're going to need
TMap<FRenderItemKey, FMeshMaterialRenderItem*>* PrepareRenderItems_AnyThread(int32 MaterialIndex)
{
TRACE_CPUPROFILER_EVENT_SCOPE(PrepareRenderItems);
TMap<FRenderItemKey, FMeshMaterialRenderItem*>* RenderItems = new TMap<FRenderItemKey, FMeshMaterialRenderItem*>();
const FMaterialDataEx* CurrentMaterialSettings = MaterialSettings[MaterialIndex];
const FMeshData* CurrentMeshSettings = MeshSettings[MaterialIndex];
for (const auto& [Property, Size] : CurrentMaterialSettings->PropertySizes)
{
FRenderItemKey RenderItemKey(CurrentMeshSettings, Size);
if (RenderItems->Find(RenderItemKey) == nullptr)
{
RenderItems->Add(RenderItemKey, new FMeshMaterialRenderItem(Size, CurrentMeshSettings, &MaterialBakingDynamicMeshBufferAllocator));
}
}
return RenderItems;
}
void LaunchAsyncPrepareRenderItems(int32 InLaunchCount)
{
int32 LastRenderItem = NextRenderItem + InLaunchCount;
for (; NextRenderItem < NumMaterials && NextRenderItem < LastRenderItem; NextRenderItem++)
{
PreparedRenderItems[NextRenderItem % PipelineDepth] =
Async(
EAsyncExecution::ThreadPool,
[this, NextRenderItem=NextRenderItem]()
{
return PrepareRenderItems_AnyThread(ProcessingOrder[NextRenderItem]);
}
);
}
}
TMap<FRenderItemKey, FMeshMaterialRenderItem*>* GetRenderItems(int32 InIndex)
{
TRACE_CPUPROFILER_EVENT_SCOPE(WaitOnPreparedRenderItems)
TMap<FRenderItemKey, FMeshMaterialRenderItem*>* RenderItems = PreparedRenderItems[InIndex % PipelineDepth].Get();
// Prepare the next render item in advance
if (NextRenderItem < NumMaterials)
{
check((NextRenderItem % PipelineDepth) == (InIndex % PipelineDepth));
LaunchAsyncPrepareRenderItems(1);
}
return RenderItems;
}
void CreateMaterialProxies()
{
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 (UTexture2D* Texture2D = Cast<UTexture2D>(Texture))
{
if (Texture2D->IsStreamable())
{
// Force LODs in with high priority, including cinematic ones.
Texture2D->SetForceMipLevelsToBeResident(30.f, 0xFFFFFFFF);
Texture2D->StreamIn(FStreamableRenderResourceState::MAX_LOD_COUNT, true);
}
}
}
}
for (TMap<FMaterialPropertyEx, FIntPoint>::TConstIterator PropertySizeIterator = CurrentMaterialSettings->PropertySizes.CreateConstIterator(); PropertySizeIterator; ++PropertySizeIterator)
{
// They will be stored in the pool and compiled asynchronously
MaterialBakingModule.CreateMaterialProxy(CurrentMaterialSettings, PropertySizeIterator.Key());
}
}
// Force all mip maps to load before baking the materials
UTexture::ForceUpdateTextureStreaming();
}
void BakeMaterialProperty(const FMaterialDataEx& CurrentMaterialSettings, const FMaterialPropertyEx& Property, FMeshMaterialRenderItem* RenderItem, UTextureRenderTarget2D* RenderTarget, FExportMaterialProxy* ExportMaterialProxy, FBakeOutputEx& CurrentOutput)
{
// 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)(
[RenderItem, RenderTarget, ExportMaterialProxy](FRHICommandListImmediate& RHICmdList)
{
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
for (int WarmupIndex = 0; WarmupIndex < WarmupIterationCount; ++WarmupIndex)
{
FCanvas::FCanvasSortElement& SortElement = Canvas.GetSortElement(Canvas.TopDepthSortKey());
SortElement.RenderBatchArray.Add(RenderItem);
Canvas.Flush_RenderThread(RHICmdList);
SortElement.RenderBatchArray.Empty();
RHICmdList.ImmediateFlush(EImmediateFlushType::FlushRHIThreadFlushResources);
}
}
);
OnMaterialPropertyBaked(CurrentMaterialSettings, Property, RenderItem, RenderTarget, ExportMaterialProxy, CurrentOutput);
}
protected:
FMaterialBakingModule& MaterialBakingModule;
const TArray<FMaterialDataEx*>& MaterialSettings;
const TArray<FMeshData*>& MeshSettings;
const int32 NumMaterials;
const bool bSaveIntermediateTextures;
const bool bEmissiveHDR;
// Distance between the command sent to rendering and the GPU read-back of the result
// to minimize sync time waiting on GPU.
static const int32 PipelineDepth = 16;
private:
TArray<uint32> ProcessingOrder;
FMaterialBakingDynamicMeshBufferAllocator MaterialBakingDynamicMeshBufferAllocator;
// 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];
};
class FMaterialBakingProcessorSingleOutput : public FMaterialBakingProcessor
{
public:
FMaterialBakingProcessorSingleOutput(FMaterialBakingModule& InMaterialBakingModule, const TArray<FMaterialDataEx*>& InMaterialSettings, const TArray<FMeshData*>& InMeshSettings, FBakeOutputEx& InOutput)
: FMaterialBakingProcessor(InMaterialBakingModule, InMaterialSettings, InMeshSettings)
, Output(InOutput)
{
if (!MaterialSettings.IsEmpty())
{
FMaterialDataEx* DefaultMaterialData = MaterialSettings[0];
// Single output path can only work if all materials settings share the same properties
for (const FMaterialDataEx* MaterialData : MaterialSettings)
{
for (const auto& [Property, Size] : MaterialData->PropertySizes)
{
check(DefaultMaterialData->PropertySizes.Contains(Property) && DefaultMaterialData->PropertySizes[Property] == Size);
}
check(MaterialData->bPerformBorderSmear == DefaultMaterialData->bPerformBorderSmear);
check(MaterialData->bPerformShrinking == DefaultMaterialData->bPerformShrinking);
check(MaterialData->bTangentSpaceNormal == DefaultMaterialData->bTangentSpaceNormal);
check(MaterialData->BlendMode == DefaultMaterialData->BlendMode);
check(MaterialData->BackgroundColor == DefaultMaterialData->BackgroundColor);
}
PrepareBakeOutput(DefaultMaterialData, Output);
// Create render targets for all properties
for (const auto& [Property, Size] : Output.PropertySizes)
{
// Skip pooling, all materials will be baked to the same set of render targets
const bool bUsePooledRenderTargets = false;
UTextureRenderTarget2D* RenderTarget = CreateRenderTarget(Property, Size, bUsePooledRenderTargets, DefaultMaterialData->BackgroundColor);
RenderTargets.Add(Property, RenderTarget);
}
}
}
~FMaterialBakingProcessorSingleOutput()
{
if (!MaterialSettings.IsEmpty())
{
// Wait until every tasks have been queued so that NumTasks is only decreasing
FlushRenderingCommands();
FMaterialDataEx* DefaultMaterialData = MaterialSettings[0];
// Read back from the render targets
for (auto& [Property, RenderTarget] : RenderTargets)
{
FRenderTarget* RTResource = RenderTarget->GameThread_GetRenderTargetResource();
check(RTResource);
if (Property.Type != MP_EmissiveColor)
{
RTResource->ReadPixels(Output.PropertyData[Property]);
}
else
{
TArray<FFloat16Color> OutputHDRColor;
RTResource->ReadFloat16Pixels(OutputHDRColor);
int32 Color16PitchPixels = Output.PropertySizes[Property].X;
ProcessEmissiveOutput(OutputHDRColor.GetData(), Color16PitchPixels, Output.PropertySizes[Property], Output.PropertyData[Property], Output.EmissiveScale, DefaultMaterialData->BackgroundColor);
if (bEmissiveHDR)
{
Output.HDRPropertyData[Property] = MoveTemp(OutputHDRColor);
}
}
if (DefaultMaterialData->bPerformBorderSmear)
{
// This will resize the output to a single pixel if the result is monochrome.
FMaterialBakingHelpers::PerformUVBorderSmearAndShrink(Output.PropertyData[Property], Output.PropertySizes[Property].X, Output.PropertySizes[Property].Y, DefaultMaterialData->BackgroundColor);
}
SaveIntermediateTextures(Output, Property, TEXT("Final"));
}
}
}
virtual FBakeOutputEx& GetBakeOutput(int32 InMaterialIndex) override
{
return Output;
}
virtual UTextureRenderTarget2D* GetRenderTarget(FMaterialPropertyEx InMaterialProperty, const FIntPoint& InRequiredSize, const FMaterialDataEx& InMaterialSettings) override
{
return RenderTargets[InMaterialProperty];
}
private:
TMap<FMaterialPropertyEx, UTextureRenderTarget2D*> RenderTargets;
FBakeOutputEx& Output;
};
class FMaterialBakingProcessorMultiOutput : public FMaterialBakingProcessor
{
public:
FMaterialBakingProcessorMultiOutput(FMaterialBakingModule& InMaterialBakingModule, const TArray<FMaterialDataEx*>& InMaterialSettings, const TArray<FMeshData*>& InMeshSettings, TArray<FBakeOutputEx>& InOutput)
: FMaterialBakingProcessor(InMaterialBakingModule, InMaterialSettings, InMeshSettings)
, Output(InOutput)
{
Output.SetNum(InMaterialSettings.Num());
int32 NumOutputs = InOutput.Num();
if (NumOutputs != 0)
{
if (ensure(NumOutputs == InMaterialSettings.Num()))
{
for (int32 Idx = 0; Idx < NumOutputs; ++Idx)
{
PrepareBakeOutput(InMaterialSettings[Idx], Output[Idx]);
}
}
}
}
~FMaterialBakingProcessorMultiOutput()
{
ENQUEUE_RENDER_COMMAND(ProcessRemainingReads)(
[this, PipelineIndex=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)(
[this](FRHICommandListImmediate& RHICmdList)
{
StagingBufferPool.Clear_RenderThread(RHICmdList);
}
);
// Wait for StagingBufferPool clear to have executed before exiting the function
FlushRenderingCommands();
}
private:
virtual void OnMaterialPropertyBaked(const FMaterialDataEx& CurrentMaterialSettings, const FMaterialPropertyEx& Property, FMeshMaterialRenderItem* RenderItem, UTextureRenderTarget2D* RenderTarget, FExportMaterialProxy* ExportMaterialProxy, FBakeOutputEx& CurrentOutput) override
{
ENQUEUE_RENDER_COMMAND(CopyStagingBuffer)(
[this, RenderTarget, Property, ExportMaterialProxy, &CurrentMaterialSettings, &CurrentOutput, PipelineIndex=PipelineIndex](FRHICommandListImmediate& RHICmdList)
{
FTextureRenderTargetResource* RenderTargetResource = RenderTarget->GetRenderTargetResource();
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 =
[this, CurrentMaterialSettings, &CurrentOutput, Property](FTexture2DRHIRef& StagingBuffer, void* Data, int32 DataWidth, int32 DataHeight)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FinalProcessing)
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
ProcessEmissiveOutput((const FFloat16Color*)Data, DataWidth, OutputSize, OutputColor, CurrentOutput.EmissiveScale, CurrentMaterialSettings.BackgroundColor);
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, CurrentMaterialSettings.BackgroundColor);
}
if (CurrentMaterialSettings.bPerformBorderSmear)
{
// This will resize the output to a single pixel if the result is monochrome.
FMaterialBakingHelpers::PerformUVBorderSmearAndShrink(OutputColor, OutputSize.X, OutputSize.Y, CurrentMaterialSettings.BackgroundColor);
}
SaveIntermediateTextures(CurrentOutput, Property, *CurrentMaterialSettings.Material->GetName());
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;
}
virtual FBakeOutputEx& GetBakeOutput(int32 InMaterialIndex) override
{
return Output[InMaterialIndex];
}
virtual UTextureRenderTarget2D* GetRenderTarget(FMaterialPropertyEx InMaterialProperty, const FIntPoint& InRequiredSize, const FMaterialDataEx& InMaterialSettings) override
{
// It is safe to reuse the same render targets for each draw pass since they all execute sequentially on the GPU and are copied to staging buffers before
// being reused.
const bool bUsePooledRenderTargets = true;
return CreateRenderTarget(InMaterialProperty, InRequiredSize, bUsePooledRenderTargets, InMaterialSettings.BackgroundColor);
}
private:
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.
int32 PipelineIndex = 0;
FPipelineContext PipelineContext[PipelineDepth];
TAtomic<uint32> NumTasks = 0;
FStagingBufferPool StagingBufferPool;
TArray<FBakeOutputEx>& Output;
};
void FMaterialBakingModule::BakeMaterials(const TArray<FMaterialDataEx*>& MaterialSettings, const TArray<FMeshData*>& MeshSettings, FBakeOutputEx& Output)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FMaterialBakingModule::BakeMaterials)
FMaterialBakingProcessorSingleOutput MaterialBakingProcessor(*this, MaterialSettings, MeshSettings, Output);
MaterialBakingProcessor.BakeMaterials();
}
void FMaterialBakingModule::BakeMaterials(const TArray<FMaterialDataEx*>& MaterialSettings, const TArray<FMeshData*>& MeshSettings, TArray<FBakeOutputEx>& Output)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FMaterialBakingModule::BakeMaterials)
FMaterialBakingProcessorMultiOutput MaterialBakingProcessor(*this, MaterialSettings, MeshSettings, Output);
MaterialBakingProcessor.BakeMaterials();
}
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_SubsurfaceColor, EPropertyColorSpace::sRGB);
PerPropertyColorSpace.Add(FMaterialPropertyEx::TransmittanceColor, EPropertyColorSpace::sRGB);
}
else
{
DefaultColorSpace = EPropertyColorSpace::sRGB;
PerPropertyColorSpace.Add(MP_EmissiveColor, EPropertyColorSpace::Linear); // Always linear because it uses HDR
PerPropertyColorSpace.Add(MP_Normal, EPropertyColorSpace::Linear);
PerPropertyColorSpace.Add(MP_Refraction, EPropertyColorSpace::Linear);
PerPropertyColorSpace.Add(MP_Opacity, EPropertyColorSpace::Linear);
PerPropertyColorSpace.Add(MP_OpacityMask, EPropertyColorSpace::Linear);
PerPropertyColorSpace.Add(MP_ShadingModel, EPropertyColorSpace::Linear);
PerPropertyColorSpace.Add(FMaterialPropertyEx::ClearCoatBottomNormal, EPropertyColorSpace::Linear);
}
}
bool FMaterialBakingModule::IsLinearBake(FMaterialPropertyEx Property)
{
const EPropertyColorSpace* OverrideColorSpace = PerPropertyColorSpace.Find(Property);
const EPropertyColorSpace ColorSpace = OverrideColorSpace ? *OverrideColorSpace : DefaultColorSpace;
return ColorSpace == 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();
}
void FMaterialBakingModule::CleanupRenderTargets()
{
for (auto RenderTarget : RenderTargetPool)
{
RenderTarget->RemoveFromRoot();
}
RenderTargetPool.Empty();
}
UTextureRenderTarget2D* FMaterialBakingModule::CreateRenderTarget(FMaterialPropertyEx InProperty, const FIntPoint& InTargetSize, bool bInUsePooledRenderTargets, const FColor& BackgroundColor)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FMaterialBakingModule::CreateRenderTarget)
// Lookup gamma and format settings for property, if not found use default values
// Gamma
const EPropertyColorSpace* OverrideColorSpace = PerPropertyColorSpace.Find(InProperty);
const EPropertyColorSpace ColorSpace = OverrideColorSpace ? *OverrideColorSpace : DefaultColorSpace;
const bool bForceLinearGamma = ColorSpace == EPropertyColorSpace::Linear;
// Pixel format
const EPixelFormat PixelFormat = PerPropertyFormat.Contains(InProperty) ? PerPropertyFormat[InProperty] : PF_B8G8R8A8;
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));
UTextureRenderTarget2D* RenderTarget = nullptr;
// First, look in pool
if (bInUsePooledRenderTargets)
{
auto RenderTargetComparison = [bForceLinearGamma, PixelFormat, ClampedTargetSize](const UTextureRenderTarget2D* CompareRenderTarget) -> bool
{
return (CompareRenderTarget->SizeX == ClampedTargetSize.X && CompareRenderTarget->SizeY == ClampedTargetSize.Y && CompareRenderTarget->OverrideFormat == PixelFormat && CompareRenderTarget->bForceLinearGamma == bForceLinearGamma);
};
// Find any pooled render target with suitable properties.
UTextureRenderTarget2D** FindResult = RenderTargetPool.FindByPredicate(RenderTargetComparison);
if (FindResult)
{
RenderTarget = *FindResult;
}
}
// If we want to avoid pooling, or no render target was found in the pool, create a new one
if (!RenderTarget)
{
TRACE_CPUPROFILER_EVENT_SCOPE(CreateNewRenderTarget)
// Not found - create a new one.
RenderTarget = NewObject<UTextureRenderTarget2D>();
check(RenderTarget);
RenderTarget->ClearColor = bForceLinearGamma ? BackgroundColor.ReinterpretAsLinear() : FLinearColor(BackgroundColor);
RenderTarget->TargetGamma = 0.0f;
RenderTarget->InitCustomFormat(ClampedTargetSize.X, ClampedTargetSize.Y, PixelFormat, bForceLinearGamma);
if (bInUsePooledRenderTargets)
{
RenderTarget->AddToRoot();
RenderTargetPool.Add(RenderTarget);
}
}
const bool bClearRenderTarget = true;
RenderTarget->UpdateResourceImmediate(bClearRenderTarget);
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 && Entry.Value->ProxyBlendMode == MaterialSettings->BlendMode)
{
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, MaterialSettings->BlendMode);
MaterialProxyPool.Add(MaterialSettings->Material, FMaterialPoolValue(Property, Proxy));
}
return Proxy;
}
void ProcessEmissiveOutput(const FFloat16Color* Color16, int32 Color16PitchPixels, const FIntPoint& OutputSize, TArray<FColor>& OutputColor, float& EmissiveScale, const FColor& BackgroundColor)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FMaterialBakingModule::ProcessEmissiveOutput)
const int32 NumThreads = [&]()
{
return FPlatformProcess::SupportsMultithreading() ? FPlatformMisc::NumberOfCores() : 1;
}();
const int32 LinesPerThread = FMath::CeilToInt((float)OutputSize.Y / (float)NumThreads);
const FFloat16Color BackgroundColor16 = FFloat16Color(FLinearColor(BackgroundColor)); // Can assume emissive always uses sRGB
const bool bShouldNormalize = CVarMaterialBakingForceDisableEmissiveScaling.GetValueOnAnyThread() == 0;
float GlobalMaxValue = 1.0f;
if (bShouldNormalize)
{
float* MaxValue = new float[NumThreads];
FMemory::Memset(MaxValue, 0, NumThreads * sizeof(MaxValue[0]));
// Find maximum float value across texture
ParallelFor(NumThreads, [&Color16, LinesPerThread, MaxValue, OutputSize, Color16PitchPixels, BackgroundColor16](int32 Index)
{
const int32 EndY = FMath::Min((Index + 1) * LinesPerThread, OutputSize.Y);
float& CurrentMaxValue = MaxValue[Index];
for (int32 PixelY = Index * LinesPerThread; PixelY < EndY; ++PixelY)
{
const int32 SrcYOffset = PixelY * Color16PitchPixels;
for (int32 PixelX = 0; PixelX < OutputSize.X; PixelX++)
{
const FFloat16Color& Pixel16 = Color16[PixelX + SrcYOffset];
// Find maximum channel value across texture
if (!(Pixel16 == BackgroundColor16))
{
CurrentMaxValue = FMath::Max(CurrentMaxValue, FMath::Max3(Pixel16.R.GetFloat(), Pixel16.G.GetFloat(), Pixel16.B.GetFloat()));
}
}
}
});
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, no need to scale
GlobalMaxValue = 1.0f;
}
}
// 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, Color16PitchPixels, Scale, BackgroundColor16, BackgroundColor](int32 Index)
{
const int32 EndY = FMath::Min((Index + 1) * LinesPerThread, OutputSize.Y);
for (int32 PixelY = Index * LinesPerThread; PixelY < EndY; ++PixelY)
{
const int32 SrcYOffset = PixelY * Color16PitchPixels;
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 == BackgroundColor16)
{
Pixel8 = BackgroundColor;
}
else
{
Pixel8.R = (uint8)FMath::Clamp(FMath::RoundToInt(Pixel16.R.GetFloat() * Scale), 0, 255);
Pixel8.G = (uint8)FMath::Clamp(FMath::RoundToInt(Pixel16.G.GetFloat() * Scale), 0, 255);
Pixel8.B = (uint8)FMath::Clamp(FMath::RoundToInt(Pixel16.B.GetFloat() * Scale), 0, 255);
}
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"