// 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, Log, All); #define LOCTEXT_NAMESPACE "MaterialBakingModule" /** Cvars for advanced features */ static TAutoConsoleVariable 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 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 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 CVarMaterialBakingVTWarmupFrames( TEXT("MaterialBaking.VTWarmupFrames"), 5, TEXT("Number of frames to render for virtual texture warmup when material baking.")); static TAutoConsoleVariable 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 IndexBuffers; TArray VertexBuffers; template RefType GetSmallestFit(uint32 SizeInBytes, TArray& 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(FRHICommandListBase& RHICmdList, uint32 NumElements) override { uint32 BufferSize = GetIndexBufferSize(NumElements); if (BufferSize > SmallestPooledBufferSize) { FBufferRHIRef Ref = GetSmallestFit(GetIndexBufferSize(NumElements), IndexBuffers); if (Ref.IsValid()) { return Ref; } } return FDynamicMeshBufferAllocator::AllocIndexBuffer(RHICmdList, NumElements); } virtual void ReleaseIndexBuffer(FBufferRHIRef& IndexBufferRHI) override { if (IndexBufferRHI->GetSize() > SmallestPooledBufferSize) { IndexBuffers.Add(MoveTemp(IndexBufferRHI)); } IndexBufferRHI = nullptr; } virtual FBufferRHIRef AllocVertexBuffer(FRHICommandListBase& RHICmdList, 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(RHICmdList, Stride, NumElements); } virtual void ReleaseVertexBuffer(FBufferRHIRef& VertexBufferRHI) override { if (VertexBufferRHI->GetSize() > SmallestPooledBufferSize) { VertexBuffers.Add(MoveTemp(VertexBufferRHI)); } VertexBufferRHI = nullptr; } }; class FStagingBufferPool { public: FTextureRHIRef 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 FTextureRHIRef& 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 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) { FTextureRHIRef 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(FTextureRHIRef& 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 (FTextureRHIRef& StagingSurface : ToUnmap) { RHICmdList.UnmapStagingSurface(StagingSurface); } ToUnmap.Empty(); Pool.Empty(); } ~FStagingBufferPool() { check(Pool.Num() == 0); } private: TArray Pool; // Not contented enough to warrant the use of lockless structures. FCriticalSection ToUnmapLock; TArray 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& Output, float& EmissiveScale, const FColor& BackgroundColor); void FMaterialBakingModule::StartupModule() { bEmissiveHDR = false; bIsBakingMaterials = 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("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("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& PropertySizePair : MaterialData.PropertySizes) { MaterialDataEx.PropertySizes.Add(PropertySizePair.Key, PropertySizePair.Value); } return MaterialDataEx; } FBakeOutput ToBakeOutput(FBakeOutputEx& BakeOutputEx) { FBakeOutput BakeOutput; BakeOutput.EmissiveScale = BakeOutputEx.EmissiveScale; for (TPair& PropertySizePair : BakeOutputEx.PropertySizes) { BakeOutput.PropertySizes.Add(PropertySizePair.Key.Type, PropertySizePair.Value); } for (TPair>& PropertyDataPair : BakeOutputEx.PropertyData) { BakeOutput.PropertyData.Add(PropertyDataPair.Key.Type, MoveTemp(PropertyDataPair.Value)); } for (TPair>& PropertyDataPair : BakeOutputEx.HDRPropertyData) { BakeOutput.HDRPropertyData.Add(PropertyDataPair.Key.Type, MoveTemp(PropertyDataPair.Value)); } for (TPair& PropertyDataPair : BakeOutputEx.PropertyIsLinearColor) { BakeOutput.PropertyIsLinearColor.Add(PropertyDataPair.Key.Type, PropertyDataPair.Value); } return BakeOutput; } void FMaterialBakingModule::BakeMaterials(const TArray& MaterialSettings, const TArray& MeshSettings, TArray& Output) { // Translate old material data to extended types TArray MaterialDataExs; MaterialDataExs.Reserve(MaterialSettings.Num()); for (const FMaterialData* MaterialData : MaterialSettings) { MaterialDataExs.Emplace(ToMaterialDataEx(*MaterialData)); } // Build an array of pointers to the extended type TArray MaterialSettingsEx; MaterialSettingsEx.Reserve(MaterialDataExs.Num()); for (FMaterialDataEx& MaterialDataEx : MaterialDataExs) { MaterialSettingsEx.Add(&MaterialDataEx); } TArray 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& MaterialSettings, const TArray& MeshSettings, FBakeOutput& BakeOutput) { // Translate old material data to extended types TArray MaterialDataExs; MaterialDataExs.Reserve(MaterialSettings.Num()); for (const FMaterialData* MaterialData : MaterialSettings) { MaterialDataExs.Emplace(ToMaterialDataEx(*MaterialData)); } // Build an array of pointers to the extended type TArray 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& InMaterialSettings, const TArray& InMeshSettings) : MaterialBakingModule(InMaterialBakingModule) , MaterialSettings(InMaterialSettings) , MeshSettings(InMeshSettings) , 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(); NumMaterials = ProcessingOrder.Num(); } 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]; check(!CurrentMaterialSettings.PropertySizes.IsEmpty()); const FMeshData* CurrentMeshSettings = MeshSettings[MaterialIndex]; FBakeOutputEx& CurrentOutput = GetBakeOutput(MaterialIndex); TMap* 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); } BakeOutput.PropertyIsLinearColor.Add(Property, MaterialBakingModule.IsLinearBake(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) { if (!MaterialSettings[Index]->PropertySizes.IsEmpty()) { 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* PrepareRenderItems_AnyThread(int32 MaterialIndex) { TRACE_CPUPROFILER_EVENT_SCOPE(PrepareRenderItems); TMap* RenderItems = new TMap(); const FMaterialDataEx* CurrentMaterialSettings = MaterialSettings[MaterialIndex]; const FMeshData* CurrentMeshSettings = MeshSettings[MaterialIndex]; check(!CurrentMaterialSettings->PropertySizes.IsEmpty()); 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, Index=NextRenderItem]() { return PrepareRenderItems_AnyThread(ProcessingOrder[Index]); } ); } } TMap* GetRenderItems(int32 InIndex) { TRACE_CPUPROFILER_EVENT_SCOPE(WaitOnPreparedRenderItems) TMap* 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 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(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::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 { const double STREAMING_WAIT_DT = 0.1; while (IStreamingManager::Get().StreamAllResources(STREAMING_WAIT_DT) > 0) { // Application tick. FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread); FTSTicker::GetCoreTicker().Tick(FApp::GetDeltaTime()); } } } 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) { RenderCaptureInterface::FScopedCapture RenderCapture(CVarMaterialBakingRDOCCapture.GetValueOnAnyThread() == 1, &RHICmdList, TEXT("MaterialBaking")); FSceneViewFamily ViewFamily(FSceneViewFamily::ConstructionValues(RenderTarget->GetRenderTargetResource(), nullptr, FEngineShowFlags(ESFIM_Game)) .SetTime(FGameTime())); 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.GetShaderPlatform())) { 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& MaterialSettings; const TArray& MeshSettings; 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: int32 NumMaterials; TArray ProcessingOrder; FMaterialBakingDynamicMeshBufferAllocator MaterialBakingDynamicMeshBufferAllocator; // We reuse the pipeline depth to prepare render items in advance to avoid stalling the game thread int NextRenderItem = 0; TFuture*> PreparedRenderItems[PipelineDepth]; }; class FMaterialBakingProcessorSingleOutput : public FMaterialBakingProcessor { public: FMaterialBakingProcessorSingleOutput(FMaterialBakingModule& InMaterialBakingModule, const TArray& InMaterialSettings, const TArray& 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.Emplace(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 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].Get(); } private: TMap> RenderTargets; FBakeOutputEx& Output; }; class FMaterialBakingProcessorMultiOutput : public FMaterialBakingProcessor { public: FMaterialBakingProcessorMultiOutput(FMaterialBakingModule& InMaterialBakingModule, const TArray& InMaterialSettings, const TArray& InMeshSettings, TArray& 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, InPipelineIndex=PipelineIndex](FRHICommandListImmediate& RHICmdList) { // Enqueue remaining reads for (int32 Index = 0; Index < PipelineDepth; Index++) { int32 LocalPipelineIndex = (InPipelineIndex + 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, InPipelineIndex=PipelineIndex](FRHICommandListImmediate& RHICmdList) { FTextureRenderTargetResource* RenderTargetResource = RenderTarget->GetRenderTargetResource(); FTextureRHIRef 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](FTextureRHIRef& StagingBuffer, void* Data, int32 DataWidth, int32 DataHeight) { TRACE_CPUPROFILER_EVENT_SCOPE(FinalProcessing) TArray& 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& 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[InPipelineIndex].ReadCommand) { PipelineContext[InPipelineIndex].ReadCommand(RHICmdList); } // Generate a texture reading command that will be executed once it reaches the end of the pipeline PipelineContext[InPipelineIndex].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 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 NumTasks = 0; FStagingBufferPool StagingBufferPool; TArray& Output; }; void FMaterialBakingModule::BakeMaterials(const TArray& MaterialSettings, const TArray& MeshSettings, FBakeOutputEx& Output) { TRACE_CPUPROFILER_EVENT_SCOPE(FMaterialBakingModule::BakeMaterials) TGuardValue GuardIsBaking(bIsBakingMaterials, true); FMaterialBakingProcessorSingleOutput MaterialBakingProcessor(*this, MaterialSettings, MeshSettings, Output); MaterialBakingProcessor.BakeMaterials(); } void FMaterialBakingModule::BakeMaterials(const TArray& MaterialSettings, const TArray& MeshSettings, TArray& Output) { TRACE_CPUPROFILER_EVENT_SCOPE(FMaterialBakingModule::BakeMaterials) TGuardValue GuardIsBaking(bIsBakingMaterials, true); FMaterialBakingProcessorMultiOutput MaterialBakingProcessor(*this, MaterialSettings, MeshSettings, Output); MaterialBakingProcessor.BakeMaterials(); } bool FMaterialBakingModule::SetupMaterialBakeSettings(TArray>& OptionObjects, int32 NumLODs) { TSharedRef Window = SNew(SWindow) .Title(LOCTEXT("WindowTitle", "Material Baking Options")) .SizingRule(ESizingRule::Autosized); TSharedPtr Options; Window->SetContent ( SAssignNew(Options, SMaterialOptions) .WidgetWindow(Window) .NumLODs(NumLODs) .SettingsObjects(OptionObjects) ); TSharedPtr ParentWindow; if (FModuleManager::Get().IsModuleLoaded("MainFrame")) { IMainFrameModule& MainFrame = FModuleManager::LoadModuleChecked("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; } void FMaterialBakingModule::CleanupMaterialProxies() { TArray ResourcesToFree; for (auto Iterator : MaterialProxyPool) { ResourcesToFree.Add(Iterator.Value.Value); } FMaterial::DeferredDeleteArray(ResourcesToFree); MaterialProxyPool.Reset(); } void FMaterialBakingModule::CleanupRenderTargets() { 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 TStrongObjectPtr& 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. TStrongObjectPtr* FindResult = RenderTargetPool.FindByPredicate(RenderTargetComparison); if (FindResult) { RenderTarget = FindResult->Get(); } } // 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(); check(RenderTarget); RenderTarget->ClearColor = bForceLinearGamma ? BackgroundColor.ReinterpretAsLinear() : FLinearColor(BackgroundColor); RenderTarget->TargetGamma = 0.0f; RenderTarget->InitCustomFormat(ClampedTargetSize.X, ClampedTargetSize.Y, PixelFormat, bForceLinearGamma); if (bInUsePooledRenderTargets) { RenderTargetPool.Emplace(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 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, false); MaterialProxyPool.Add(MaterialSettings->Material, FMaterialPoolValue(Property, Proxy)); } return Proxy; } void ProcessEmissiveOutput(const FFloat16Color* Color16, int32 Color16PitchPixels, const FIntPoint& OutputSize, TArray& 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(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(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 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(PoolMaterialPtr); while (!bMustDelete && MaterialInstance && MaterialInstance->Parent != nullptr) { bMustDelete = MaterialInstance->Parent == MaterialToInvalidate; MaterialInstance = Cast(MaterialInstance->Parent); } } // We have a match, remove the entry from our pool if (bMustDelete) { TArray ResourcesToFree; ResourcesToFree.Add(It.Value().Value); FMaterial::DeferredDeleteArray(ResourcesToFree); It.RemoveCurrent(); } } } } } void FMaterialBakingModule::OnPreGarbageCollect() { // Do not cleanup material proxies while baking materials. if (!bIsBakingMaterials) { CleanupMaterialProxies(); } } #undef LOCTEXT_NAMESPACE //"MaterialBakingModule"