// Copyright Epic Games, Inc. All Rights Reserved. #include "RectLightTextureManager.h" #include "Engine/Texture.h" #include "Shader.h" #include "GlobalShader.h" #include "ShaderParameters.h" #include "ShaderParameterStruct.h" #include "RenderGraphUtils.h" #include "ShaderPrintParameters.h" #include "ShaderPrint.h" #include "RenderingThread.h" #include "Containers/Array.h" #include "Containers/Queue.h" #include "SceneView.h" #include "RHIDefinitions.h" #include "SceneRendering.h" #include "SystemTextures.h" #include "TextureLayout.h" #include "CommonRenderResources.h" #include "ScreenPass.h" #include "RectLightTexture.h" #include "DataDrivenShaderPlatformInfo.h" /////////////////////////////////////////////////////////////////////////////////////////////////// // Possible improvements: // * When copying & filtering the texture, add RGBE/BCH6 encoding to reduce footprint and improve // fetch perf // * Support non-square atlas static TAutoConsoleVariable CVarRectLightTextureResolution( TEXT("r.RectLightAtlas.MaxResolution"), 4096, TEXT("The maximum resolution for storing rect. light textures.\n"), ECVF_RenderThreadSafe); static TAutoConsoleVariable CVarRectLighTextureDebug( TEXT("r.RectLightAtlas.Debug"), 0, TEXT("Enable rect. light atlas debug information."), ECVF_RenderThreadSafe); static TAutoConsoleVariable CVarRectLighTextureDebugMipLevel( TEXT("r.RectLightAtlas.Debug.MipLevel"), 0, TEXT("Set MIP level for visualizing atlas texture in debug mode."), ECVF_RenderThreadSafe); static TAutoConsoleVariable CVarRectLighForceUpdate( TEXT("r.RectLightAtlas.ForceUpdate"), 0, TEXT("Force rect. light atlas update very frame."), ECVF_RenderThreadSafe); static TAutoConsoleVariable CVarRectLighResetOnChange( TEXT("r.RectLightAtlas.ResetOnChange"), 0, TEXT("Recreate the atlas layout from scratch each time an update is made to the atlas."), ECVF_RenderThreadSafe); static TAutoConsoleVariable CVarRectLighClearAtlas( TEXT("r.RectLightAtlas.ClearAtlas"), 0, TEXT("Clear rect light texture atlas each time the atlas is recreated/updated."), ECVF_RenderThreadSafe); static TAutoConsoleVariable CVarRectLighFilterQuality( TEXT("r.RectLightAtlas.FilterQuality"), 1, TEXT("Define the filtering quality used for filtering texture (0:Box filter, 1:Gaussian filter)."), ECVF_RenderThreadSafe); static TAutoConsoleVariable CVarRectLighMaxTextureRatio( TEXT("r.RectLightAtlas.MaxTextureRatio"), 2, TEXT("Define the max Width/Height or Height/Width ratio that a texture can have."), ECVF_RenderThreadSafe); static TAutoConsoleVariable CVarRectLighAtlasFormat( TEXT("r.RectLightAtlas.Format"), 0, TEXT("Define the atlas format used.\n - 0: R11G11B10 (faster but can caused hue shift). \n - 1: RGA16F (more accurate but slower)"), ECVF_RenderThreadSafe); namespace RectLightAtlas { /////////////////////////////////////////////////////////////////////////////////////////////////// // Configuration // Packing algo. // Reference: "A Thousand Ways to Pack the Bin. A Practical Approach to Two-Dimensional Rectangle Bin Packing" // // 0 : use a Shelf/Row packing algo -> faster, but produces low-quality packing. // 1 : use a Skyline/Horizon packing algo -> more expensive, but provides better packing. // 2 : use a FTextureLayout #define USE_PACKING_MODE 1 // If enabled, track the deallocated free rects (i.e. 'holes' in the atlas), and try to allocate // slots from them when possible rather than expanding the atlas #define USE_WASTE_RECT 1 /////////////////////////////////////////////////////////////////////////////////////////////////// // Structs & constants static const uint32 InvalidSlotIndex = ~0u; static const FIntPoint InvalidOrigin = FIntPoint(-1, -1); static FIntPoint GetSourceResolution(const FRHITexture* Tex, const FVector4f& ScaleOffset) { FIntPoint Out(1, 1); if (Tex) { const FIntVector SourceOriginalResolution = Tex->GetSizeXYZ(); float ScaledResolutionX = ScaleOffset.X * SourceOriginalResolution.X; float ScaledResolutionY = ScaleOffset.Y * SourceOriginalResolution.Y; const float RatioYX = ScaledResolutionY / ScaledResolutionX; const float RatioXY = ScaledResolutionX / ScaledResolutionY; Out = FIntPoint(ScaledResolutionX, ScaledResolutionY); // Max Ratio of 2 const float RatioThreshold = FMath::Clamp(CVarRectLighMaxTextureRatio.GetValueOnAnyThread(), 1, 16); if (RatioYX > RatioThreshold) { Out.X *= RatioYX / RatioThreshold; } else if (RatioXY > RatioThreshold) { Out.Y *= RatioXY / RatioThreshold; } } return Out; } struct FAtlasRect { FIntPoint Origin = InvalidOrigin; FIntPoint Resolution = FIntPoint::ZeroValue; }; struct FAtlasHorizon { FAtlasRect Line; FAtlasRect ExtendedLine; }; // Store the current atlas layout, i.e. data for knowing where next insertion can be done struct FAtlasLayout { FAtlasLayout(const FIntPoint& MaxResolution = FIntPoint(256, 256)) : AtlasResolution(MaxResolution) #if USE_PACKING_MODE == 2 , Packer(256, 256, MaxResolution.X, MaxResolution.Y, true, ETextureLayoutAspectRatio::ForceSquare, true) #endif { } FIntPoint AtlasResolution = FIntPoint::ZeroValue; int32 SourceTextureMIPBias = 0; #if USE_PACKING_MODE == 0 int32 SplitX = 0; int32 SplitY = 0; int32 MaxY = 0; #elif USE_PACKING_MODE == 1 TArray Horizons; #elif USE_PACKING_MODE == 2 FTextureLayout Packer; #endif #if USE_WASTE_RECT TArray FreeRects; #endif }; // Store info of a current slot within the atlas struct FAtlasSlot { uint32 Id = InvalidSlotIndex; FAtlasRect Rect; FVector4f SourceScaleOffset; FTextureReferenceRHIRef SourceTexture = nullptr; FIntPoint CachedSourceTextureResolution = FIntPoint(0, 0); uint32 RefCount = 0; bool bForceRefresh = false; bool IsValid() const { return SourceTexture != nullptr; } FRHITexture* GetTextureRHI() const { return SourceTexture->GetReferencedTexture(); } FIntPoint GetSourceResolution() const { return RectLightAtlas::GetSourceResolution(SourceTexture ? SourceTexture->GetReferencedTexture() : nullptr, SourceScaleOffset); } }; // Store info for copying one atlas slot to another one, when a new layout is created struct FAtlasCopySlot { uint32 Id = InvalidSlotIndex; FIntPoint SrcOrigin = InvalidOrigin; FIntPoint DstOrigin = InvalidOrigin; FIntPoint Resolution = FIntPoint::ZeroValue; FTextureReferenceRHIRef SourceTexture = nullptr; }; // Texture manager, holding all atlas data & description struct FRectLightTextureManager : public FRenderResource { TRefCountPtr AtlasTexture = nullptr; TArray AtlasSlots; TArray DeletedSlots; TQueue FreeSlots; FAtlasLayout AtlasLayout; bool bLock = false; bool bHasPendingAdds = false; bool bHasPendingDeletes = false; virtual void ReleaseRHI() { AtlasTexture.SafeRelease(); } }; // lives on the render thread TGlobalResource GRectLightTextureManager; /////////////////////////////////////////////////////////////////////////////////////////////////// // Utils functions bool CanContain(const FAtlasRect& Outside, const FAtlasRect& Inside) { return Inside.Resolution.X <= Outside.Resolution.X && Inside.Resolution.Y <= Outside.Resolution.Y; } static FIntPoint ToMIP(const FIntPoint& InMip, uint32 MipIndex) { const int32 Div = 1 << MipIndex; FIntPoint Out; Out.X = FMath::DivideAndRoundUp(InMip.X,Div); Out.Y = FMath::DivideAndRoundUp(InMip.Y,Div); return Out; } static int32 GetSlotMaxMIPLevel(const FAtlasSlot& In) { const uint32 MaxMIP = FMath::FloorLog2(FMath::Min(In.Rect.Resolution.X, In.Rect.Resolution.Y)); return MaxMIP > 0u ? MaxMIP-1u : 0u; } // Align the slot resolution to prevent bilinear filtering to leak on neighbors' slot static void AlignSlotResolution(FAtlasSlot& In) { const uint32 MaxMIP = GetSlotMaxMIPLevel(In); const FIntPoint ResolutionMIP = ToMIP(In.Rect.Resolution, MaxMIP); In.Rect.Resolution = FIntPoint(ResolutionMIP.X << MaxMIP, ResolutionMIP.Y << MaxMIP); } static EPixelFormat GetRectLightAtlasFormat() { return CVarRectLighAtlasFormat.GetValueOnRenderThread() > 0 ? PF_FloatRGBA : PF_FloatR11G11B10; } static bool Traits_IsValid(const FAtlasRect& In) { return true; } static bool Traits_IsValid(const FAtlasHorizon& In) { return true; } static bool Traits_IsValid(const FAtlasSlot& In) { return In.IsValid(); } static const FAtlasRect& Traits_GetRect(const FAtlasRect& In) { return In; } static const FAtlasRect& Traits_GetRect(const FAtlasHorizon& In) { return In.ExtendedLine; } static const FAtlasRect& Traits_GetRect(const FAtlasSlot& In) { return In.Rect; } static const FVector4f& Traits_GetScaleOffset(const FAtlasSlot& In) { return In.SourceScaleOffset; } template static FRDGBufferRef CreateSlotBuffer(FRDGBuilder& GraphBuilder, const TArray& In, const TCHAR* InBufferName) { struct FAtlasGPUSlot { uint16 OffsetX; uint16 OffsetY; uint16 ResolutionX; uint16 ResolutionY; }; if (In.IsEmpty()) { return GSystemTextures.GetDefaultBuffer(GraphBuilder, sizeof(FAtlasGPUSlot), 0u); } TArray SlotData; SlotData.Reserve(In.Num()); for (const T& I : In) { if (Traits_IsValid(I)) { const FAtlasRect& Rect = Traits_GetRect(I); FAtlasGPUSlot& GPUSlot = SlotData.AddDefaulted_GetRef(); GPUSlot.OffsetX = Rect.Origin.X; GPUSlot.OffsetY = Rect.Origin.Y; GPUSlot.ResolutionX = Rect.Resolution.X; GPUSlot.ResolutionY = Rect.Resolution.Y; } } FRDGBufferRef Buffer = GraphBuilder.CreateBuffer(FRDGBufferDesc::CreateBufferDesc(sizeof(FAtlasGPUSlot), SlotData.Num()), InBufferName); GraphBuilder.QueueBufferUpload(Buffer, SlotData.GetData(), SlotData.Num() * sizeof(FAtlasGPUSlot)); return Buffer; } template static FRDGBufferRef CreateScaleOffsetBuffer(FRDGBuilder& GraphBuilder, const TArray& In, const TCHAR* InBufferName) { struct FAtlasGPUScaleOffset { float SourceScaleX; float SourceScaleY; float SourceOffsetX; float SourceOffsetY; }; if (In.IsEmpty()) { return GSystemTextures.GetDefaultBuffer(GraphBuilder, sizeof(FAtlasGPUScaleOffset), 0u); } TArray ScaleOffsetData; ScaleOffsetData.Reserve(In.Num()); for (const T& I : In) { if (Traits_IsValid(I)) { //Traits_GetRect(I); const FVector4f& ScaleOffset = Traits_GetScaleOffset(I); FAtlasGPUScaleOffset& GPUSlot = ScaleOffsetData.AddDefaulted_GetRef(); GPUSlot.SourceScaleX = ScaleOffset.X; GPUSlot.SourceScaleY = ScaleOffset.Y; GPUSlot.SourceOffsetX = ScaleOffset.Z; GPUSlot.SourceOffsetY = ScaleOffset.W; } } FRDGBufferRef Buffer = GraphBuilder.CreateBuffer(FRDGBufferDesc::CreateBufferDesc(sizeof(FAtlasGPUScaleOffset), ScaleOffsetData.Num()), InBufferName); GraphBuilder.QueueBufferUpload(Buffer, ScaleOffsetData.GetData(), ScaleOffsetData.Num() * sizeof(FAtlasGPUScaleOffset)); return Buffer; } /////////////////////////////////////////////////////////////////////////////////////////////////// // Clear #define RECT_ATLAS_DEBUG_CLEAR 0 #if RECT_ATLAS_DEBUG_CLEAR class FRectLightAtlasClearCS : public FGlobalShader { DECLARE_GLOBAL_SHADER(FRectLightAtlasClearCS); SHADER_USE_PARAMETER_STRUCT(FRectLightAtlasClearCS, FGlobalShader); BEGIN_SHADER_PARAMETER_STRUCT(FParameters, ) SHADER_PARAMETER(FIntPoint, Resolution) SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture2D, OutputTexture) END_SHADER_PARAMETER_STRUCT() public: static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters) { return true; } static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment) { FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment); OutEnvironment.SetDefine(TEXT("SHADER_CLEAR_TEXTURE"), 1); } static EShaderPermutationPrecacheRequest ShouldPrecachePermutation(const FGlobalShaderPermutationParameters& Parameters) { return EShaderPermutationPrecacheRequest::NotPrecached; } }; IMPLEMENT_GLOBAL_SHADER(FRectLightAtlasClearCS, "/Engine/Private/RectLightAtlas.usf", "MainCS", SF_Compute); static FRDGTextureRef AddRectLightClearPass( FRDGBuilder& GraphBuilder, FGlobalShaderMap* ShaderMap, FRDGTextureRef OutputTexture) { const FIntPoint OutputResolution(OutputTexture->Desc.Extent); const uint32 MipCount = FMath::FloorLog2(OutputResolution.X); for (uint32 MipIt = 0; MipIt < MipCount; ++MipIt) { FRectLightAtlasClearCS::FParameters* Parameters = GraphBuilder.AllocParameters(); Parameters->Resolution.X = OutputResolution.X>>MipIt; Parameters->Resolution.Y = OutputResolution.Y>>MipIt; Parameters->OutputTexture = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(OutputTexture, MipIt)); TShaderMapRef ComputeShader(ShaderMap); const FIntVector DispatchCount = DispatchCount.DivideAndRoundUp(FIntVector(OutputResolution.X, OutputResolution.Y, 1), FIntVector(8, 8, 1)); FComputeShaderUtils::AddPass(GraphBuilder, RDG_EVENT_NAME("RectLightAtlas::Clear"), ComputeShader, Parameters, DispatchCount); } return OutputTexture; } #endif /////////////////////////////////////////////////////////////////////////////////////////////////// // Debug class FRectLightAtlasDebugInfoCS : public FGlobalShader { DECLARE_GLOBAL_SHADER(FRectLightAtlasDebugInfoCS); SHADER_USE_PARAMETER_STRUCT(FRectLightAtlasDebugInfoCS, FGlobalShader); BEGIN_SHADER_PARAMETER_STRUCT(FParameters, ) SHADER_PARAMETER(FIntPoint, AtlasResolution) SHADER_PARAMETER(float, AtlasMaxMipLevel) SHADER_PARAMETER(float, Occupancy) SHADER_PARAMETER(uint32, SlotCount) SHADER_PARAMETER(uint32, HorizonCount) SHADER_PARAMETER(uint32, FreeCount) SHADER_PARAMETER(FIntPoint, OutputResolution) SHADER_PARAMETER(uint32, AtlasMIPIndex) SHADER_PARAMETER(uint32, AtlasSourceTextureMIPBias) SHADER_PARAMETER(uint32, AtlasFormat) SHADER_PARAMETER(uint32, ForceUpdate) SHADER_PARAMETER(uint32, ResetOnChange) SHADER_PARAMETER(uint32, ClearAtlas) SHADER_PARAMETER_SAMPLER(SamplerState, AtlasSampler) SHADER_PARAMETER_RDG_TEXTURE(Texture2D, AtlasTexture) SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture2D, OutputTexture) SHADER_PARAMETER_RDG_BUFFER_SRV(Buffer, SlotBuffer) SHADER_PARAMETER_RDG_BUFFER_SRV(Buffer, HorizonBuffer) SHADER_PARAMETER_RDG_BUFFER_SRV(Buffer, FreeBuffer) SHADER_PARAMETER_STRUCT_INCLUDE(ShaderPrint::FShaderParameters, ShaderPrintParameters) SHADER_PARAMETER_STRUCT_REF(FViewUniformShaderParameters, ViewUniformBuffer) END_SHADER_PARAMETER_STRUCT() public: static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters) { return ShaderPrint::IsSupported(Parameters.Platform); } static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment) { FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment); ShaderPrint::ModifyCompilationEnvironment(Parameters, OutEnvironment); OutEnvironment.SetDefine(TEXT("SHADER_DEBUG"), 1); } static EShaderPermutationPrecacheRequest ShouldPrecachePermutation(const FGlobalShaderPermutationParameters& Parameters) { return EShaderPermutationPrecacheRequest::NotPrecached; } }; IMPLEMENT_GLOBAL_SHADER(FRectLightAtlasDebugInfoCS, "/Engine/Private/RectLightAtlas.usf", "MainCS", SF_Compute); static FRDGTextureRef AddRectLightDebugInfoPass( FRDGBuilder& GraphBuilder, const FViewInfo& View, const FRDGTextureDesc& OutputDesc) { // Force ShaderPrint on. ShaderPrint::SetEnabled(true); FRDGTextureRef OutputTexture = GraphBuilder.CreateTexture(FRDGTextureDesc::Create2D(OutputDesc.Extent, OutputDesc.Format, FClearValueBinding::Black, TexCreate_UAV | TexCreate_ShaderResource), TEXT("RectLight.DebugTexture")); FGlobalShaderMap* ShaderMap = View.ShaderMap; FRDGTextureRef AtlasTexture = GRectLightTextureManager.AtlasTexture ? GraphBuilder.RegisterExternalTexture(GRectLightTextureManager.AtlasTexture) : GSystemTextures.GetBlackDummy(GraphBuilder); uint32 ValidSlotCount = 0; uint32 OccupiedPixels = 0; TArray ValidSlots; ValidSlots.Reserve(GRectLightTextureManager.AtlasSlots.Num()); for (const FAtlasSlot& Slot : GRectLightTextureManager.AtlasSlots) { if (Slot.IsValid()) { ValidSlots.Add(Slot); OccupiedPixels += Slot.Rect.Resolution.X * Slot.Rect.Resolution.Y; } } #if USE_PACKING_MODE == 1 const TArray& Horizons = GRectLightTextureManager.AtlasLayout.Horizons; const TArray& FreeRects = GRectLightTextureManager.AtlasLayout.FreeRects; #else const TArray Horizons; const TArray FreeRects; #endif // Create a buffer with all the valid slot to highlight them on the debug view FRDGBufferRef SlotBuffer = CreateSlotBuffer(GraphBuilder, ValidSlots, TEXT("RectLight.AtlasSlotBuffer")); FRDGBufferRef HorizonBuffer = CreateSlotBuffer(GraphBuilder, Horizons, TEXT("RectLight.HorizonBuffer")); FRDGBufferRef FreeBuffer = CreateSlotBuffer(GraphBuilder, FreeRects, TEXT("RectLight.FreeBuffer")); uint32 AtlasFormat = 2; switch (AtlasTexture->Desc.Format) { case PF_FloatR11G11B10 : AtlasFormat = 0; break; case PF_FloatRGBA : AtlasFormat = 1; break; default: AtlasFormat = 2; break; } const FIntPoint OutputResolution(OutputTexture->Desc.Extent); FRectLightAtlasDebugInfoCS::FParameters* Parameters = GraphBuilder.AllocParameters(); Parameters->AtlasResolution = AtlasTexture->Desc.Extent; Parameters->AtlasMaxMipLevel = AtlasTexture->Desc.NumMips; Parameters->AtlasSourceTextureMIPBias = GRectLightTextureManager.AtlasLayout.SourceTextureMIPBias; Parameters->AtlasFormat = AtlasFormat; Parameters->Occupancy = OccupiedPixels / float(AtlasTexture->Desc.Extent.X * AtlasTexture->Desc.Extent.Y); Parameters->SlotCount = ValidSlots.Num(); Parameters->HorizonCount = Horizons.Num(); Parameters->FreeCount = FreeRects.Num(); Parameters->OutputResolution = OutputResolution; Parameters->ForceUpdate = CVarRectLighForceUpdate.GetValueOnRenderThread() > 0; Parameters->ResetOnChange = CVarRectLighResetOnChange.GetValueOnRenderThread() > 0; Parameters->ClearAtlas = CVarRectLighClearAtlas.GetValueOnRenderThread() > 0; Parameters->AtlasSampler = TStaticSamplerState::GetRHI(); Parameters->AtlasTexture = AtlasTexture; Parameters->SlotBuffer = GraphBuilder.CreateSRV(SlotBuffer, PF_R16G16B16A16_UINT); Parameters->HorizonBuffer = GraphBuilder.CreateSRV(HorizonBuffer, PF_R16G16B16A16_UINT); Parameters->FreeBuffer = GraphBuilder.CreateSRV(FreeBuffer, PF_R16G16B16A16_UINT); Parameters->ViewUniformBuffer = View.ViewUniformBuffer; Parameters->AtlasMIPIndex = FMath::Clamp(CVarRectLighTextureDebugMipLevel.GetValueOnRenderThread(), 0u, Parameters->AtlasMaxMipLevel-1); ShaderPrint::SetParameters(GraphBuilder, View.ShaderPrintData, Parameters->ShaderPrintParameters); Parameters->OutputTexture = GraphBuilder.CreateUAV(OutputTexture); TShaderMapRef ComputeShader(ShaderMap); const FIntVector DispatchCount = DispatchCount.DivideAndRoundUp(FIntVector(OutputResolution.X, OutputResolution.Y, 1), FIntVector(8, 8, 1)); FComputeShaderUtils::AddPass(GraphBuilder, RDG_EVENT_NAME("RectLightAtlas::DebugInfo"), ComputeShader, Parameters, DispatchCount); return OutputTexture; } /////////////////////////////////////////////////////////////////////////////////////////////////// // Rect VS class FRectLightAtlasVS : public FGlobalShader { DECLARE_GLOBAL_SHADER(FRectLightAtlasVS); SHADER_USE_PARAMETER_STRUCT(FRectLightAtlasVS, FGlobalShader); BEGIN_SHADER_PARAMETER_STRUCT(FParameters, ) SHADER_PARAMETER_RDG_BUFFER_SRV(Buffer, SlotBuffer) SHADER_PARAMETER_RDG_BUFFER_SRV(Buffer, ScaleOffsetBuffer) SHADER_PARAMETER(uint32, SlotBufferOffset) SHADER_PARAMETER(FIntPoint, AtlasResolution) SHADER_PARAMETER(int32, bHasScaleOffset) END_SHADER_PARAMETER_STRUCT() public: static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters) { return true; } static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment) { FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment); OutEnvironment.SetDefine(TEXT("SHADER_RECT_VS"), 1); } }; IMPLEMENT_GLOBAL_SHADER(FRectLightAtlasVS, "/Engine/Private/RectLightAtlas.usf", "MainVS", SF_Vertex); /////////////////////////////////////////////////////////////////////////////////////////////////// // Add pass class FRectAtlasAddTexturePS : public FGlobalShader { DECLARE_GLOBAL_SHADER(FRectAtlasAddTexturePS); SHADER_USE_PARAMETER_STRUCT(FRectAtlasAddTexturePS, FGlobalShader); BEGIN_SHADER_PARAMETER_STRUCT(FParameters, ) SHADER_PARAMETER(int32, InTextureMIPBias) SHADER_PARAMETER_TEXTURE(Texture2D, InTexture0) SHADER_PARAMETER_TEXTURE(Texture2D, InTexture1) SHADER_PARAMETER_TEXTURE(Texture2D, InTexture2) SHADER_PARAMETER_TEXTURE(Texture2D, InTexture3) SHADER_PARAMETER_TEXTURE(Texture2D, InTexture4) SHADER_PARAMETER_TEXTURE(Texture2D, InTexture5) SHADER_PARAMETER_TEXTURE(Texture2D, InTexture6) SHADER_PARAMETER_TEXTURE(Texture2D, InTexture7) SHADER_PARAMETER_SAMPLER(SamplerState, InSampler0) SHADER_PARAMETER_SAMPLER(SamplerState, InSampler1) SHADER_PARAMETER_SAMPLER(SamplerState, InSampler2) SHADER_PARAMETER_SAMPLER(SamplerState, InSampler3) SHADER_PARAMETER_SAMPLER(SamplerState, InSampler4) SHADER_PARAMETER_SAMPLER(SamplerState, InSampler5) SHADER_PARAMETER_SAMPLER(SamplerState, InSampler6) SHADER_PARAMETER_SAMPLER(SamplerState, InSampler7) SHADER_PARAMETER_STRUCT_INCLUDE(FRectLightAtlasVS::FParameters, VS) RENDER_TARGET_BINDING_SLOTS() END_SHADER_PARAMETER_STRUCT() public: static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters) { return true; } static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment) { FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment); OutEnvironment.SetDefine(TEXT("SHADER_ADD_TEXTURE"), 1); } }; IMPLEMENT_GLOBAL_SHADER(FRectAtlasAddTexturePS, "/Engine/Private/RectLightAtlas.usf", "MainPS", SF_Pixel); // Insert a texture into the atlas static void AddSlotsPass( FRDGBuilder& GraphBuilder, FGlobalShaderMap* ShaderMap, const int32 TextureMIPBias, const TArray& Slots, FRDGTextureRef& OutAtlas) { const FIntRect Viewport(FIntPoint::ZeroValue, OutAtlas->Desc.Extent); const FIntPoint Resolution(Viewport.Width(), Viewport.Height()); FRDGBufferRef SlotBuffer = CreateSlotBuffer(GraphBuilder, Slots, TEXT("RectLight.AtlasSlotBuffer")); FRDGBufferRef ScaleOffsetBuffer = CreateScaleOffsetBuffer(GraphBuilder, Slots, TEXT("RectLight.AtlasScaleOffsetBuffer")); // Batch new slots into several passes const uint32 SlotCountPerPass = 8u; const uint32 PassCount = FMath::DivideAndRoundUp(uint32(Slots.Num()), SlotCountPerPass); for (uint32 PassIt = 0; PassIt < PassCount; ++PassIt) { const uint32 SlotOffset = PassIt * SlotCountPerPass; const uint32 SlotCount = SlotCountPerPass * (PassIt+1) <= uint32(Slots.Num()) ? SlotCountPerPass : uint32(Slots.Num()) - (SlotCountPerPass * PassIt); FRectAtlasAddTexturePS::FParameters* Parameters = GraphBuilder.AllocParameters(); // Init. source texure { Parameters->InTexture0 = GSystemTextures.BlackDummy->GetRHI(); Parameters->InTexture1 = GSystemTextures.BlackDummy->GetRHI(); Parameters->InTexture2 = GSystemTextures.BlackDummy->GetRHI(); Parameters->InTexture3 = GSystemTextures.BlackDummy->GetRHI(); Parameters->InTexture4 = GSystemTextures.BlackDummy->GetRHI(); Parameters->InTexture5 = GSystemTextures.BlackDummy->GetRHI(); Parameters->InTexture6 = GSystemTextures.BlackDummy->GetRHI(); Parameters->InTexture7 = GSystemTextures.BlackDummy->GetRHI(); } for (uint32 SlotIt = 0; SlotItInTexture0 = Slot.GetTextureRHI(); break; case 1: Parameters->InTexture1 = Slot.GetTextureRHI(); break; case 2: Parameters->InTexture2 = Slot.GetTextureRHI(); break; case 3: Parameters->InTexture3 = Slot.GetTextureRHI(); break; case 4: Parameters->InTexture4 = Slot.GetTextureRHI(); break; case 5: Parameters->InTexture5 = Slot.GetTextureRHI(); break; case 6: Parameters->InTexture6 = Slot.GetTextureRHI(); break; case 7: Parameters->InTexture7 = Slot.GetTextureRHI(); break; } } FRHISamplerState* SamplerState = TStaticSamplerState::GetRHI(); Parameters->InSampler0 = SamplerState; Parameters->InSampler1 = SamplerState; Parameters->InSampler2 = SamplerState; Parameters->InSampler3 = SamplerState; Parameters->InSampler4 = SamplerState; Parameters->InSampler5 = SamplerState; Parameters->InSampler6 = SamplerState; Parameters->InSampler7 = SamplerState; Parameters->InTextureMIPBias = TextureMIPBias; Parameters->VS.AtlasResolution = Resolution; Parameters->VS.SlotBufferOffset = SlotOffset; Parameters->VS.SlotBuffer = GraphBuilder.CreateSRV(SlotBuffer, PF_R16G16B16A16_UINT); Parameters->VS.ScaleOffsetBuffer = GraphBuilder.CreateSRV(ScaleOffsetBuffer, PF_A32B32G32R32F); Parameters->VS.bHasScaleOffset = true; Parameters->RenderTargets[0] = FRenderTargetBinding(OutAtlas, ERenderTargetLoadAction::ELoad, 0); TShaderMapRef VertexShader(ShaderMap); TShaderMapRef PixelShader(ShaderMap); GraphBuilder.AddPass( RDG_EVENT_NAME("RectLightAtlas::AddTexturePass(Slot:%d)", SlotCount), Parameters, ERDGPassFlags::Raster, [Parameters, VertexShader, PixelShader, Viewport, Resolution, SlotCount](FRDGAsyncTask, FRHICommandList& RHICmdList) { FGraphicsPipelineStateInitializer GraphicsPSOInit; RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit); GraphicsPSOInit.BlendState = TStaticBlendState::GetRHI(); GraphicsPSOInit.RasterizerState = TStaticRasterizerState<>::GetRHI(); GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState::GetRHI(); GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GEmptyVertexDeclaration.VertexDeclarationRHI; GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader(); GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader(); GraphicsPSOInit.PrimitiveType = PT_TriangleList; SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, 0); RHICmdList.SetViewport(Viewport.Min.X, Viewport.Min.Y, 0.0f, Viewport.Max.X, Viewport.Max.Y, 1.0f); SetShaderParameters(RHICmdList, PixelShader, PixelShader.GetPixelShader(), *Parameters); SetShaderParameters(RHICmdList, VertexShader, VertexShader.GetVertexShader(), Parameters->VS); RHICmdList.SetStreamSource(0, nullptr, 0); RHICmdList.DrawPrimitive(0, 2, SlotCount); }); } } /////////////////////////////////////////////////////////////////////////////////////////////////// // Copy pass class FRectAtlasCopyTexturePS : public FGlobalShader { DECLARE_GLOBAL_SHADER(FRectAtlasCopyTexturePS); SHADER_USE_PARAMETER_STRUCT(FRectAtlasCopyTexturePS, FGlobalShader); BEGIN_SHADER_PARAMETER_STRUCT(FParameters, ) SHADER_PARAMETER(uint32, MipLevel) SHADER_PARAMETER_RDG_BUFFER_SRV(Buffer, SrcSlotBuffer) SHADER_PARAMETER_RDG_TEXTURE(Texture2D, SourceAtlasTexture) SHADER_PARAMETER_STRUCT_INCLUDE(FRectLightAtlasVS::FParameters, VS) RENDER_TARGET_BINDING_SLOTS() END_SHADER_PARAMETER_STRUCT() public: static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters) { return true; } static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment) { FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment); OutEnvironment.SetDefine(TEXT("SHADER_COPY_TEXTURE"), 1); } }; IMPLEMENT_GLOBAL_SHADER(FRectAtlasCopyTexturePS, "/Engine/Private/RectLightAtlas.usf", "MainPS", SF_Pixel); // Copy all slots (and their mip) from InAtlas to OutAtlas // This is done when atlas need a full repacking, changing the slot layout static void CopySlotsPass( FRDGBuilder& GraphBuilder, FGlobalShaderMap* ShaderMap, const TArray& InSlots, const FRDGTextureRef& InAtlas, FRDGTextureRef& OutAtlas) { FRDGBufferSRVRef DummyScaleOffsetBuffer = GraphBuilder.CreateSRV(GSystemTextures.GetDefaultBuffer(GraphBuilder, 16u, 1u), PF_A32B32G32R32F); // For each MIP, copy the slot from the old atlas (InAtlas) to the new atlas (OutAtlas) const uint32 MipCount = InAtlas->Desc.NumMips; // FMath::Log2(FMath::Min(InAtlas->Desc.Extent.X, InAtlas->Desc.Extent.Y)); for (uint32 MipIt = 0; MipIt < MipCount; ++MipIt) { TArray SrcSlots; TArray DstSlots; SrcSlots.Reserve(InSlots.Num()); DstSlots.Reserve(InSlots.Num()); for (const FAtlasCopySlot& Slot : InSlots) { const bool bIsValidMIP = (Slot.Resolution.X >> MipIt) > 1 && (Slot.Resolution.Y >> MipIt) > 1; if (bIsValidMIP) { { FAtlasSlot& DstSlot = DstSlots.AddDefaulted_GetRef(); DstSlot.Rect.Origin = ToMIP(Slot.DstOrigin, MipIt); DstSlot.Rect.Resolution = ToMIP(Slot.Resolution, MipIt); DstSlot.SourceTexture = Slot.SourceTexture; } { FAtlasSlot& SrcSlot = SrcSlots.AddDefaulted_GetRef(); SrcSlot.Rect.Origin = ToMIP(Slot.SrcOrigin, MipIt); SrcSlot.Rect.Resolution = ToMIP(Slot.Resolution, MipIt); SrcSlot.SourceTexture = Slot.SourceTexture; } } } const uint32 SlotCount = DstSlots.Num(); if (SlotCount > 0) { FRDGBufferRef SrcSlotBuffer = CreateSlotBuffer(GraphBuilder, SrcSlots, TEXT("RectLight.SrcSlotBuffer")); FRDGBufferRef DstSlotBuffer = CreateSlotBuffer(GraphBuilder, DstSlots, TEXT("RectLight.DstSlotBuffer")); const FIntPoint Resolution = ToMIP(OutAtlas->Desc.Extent.X, MipIt); const FIntRect Viewport(FIntPoint::ZeroValue, Resolution); FRectAtlasCopyTexturePS::FParameters* Parameters = GraphBuilder.AllocParameters(); Parameters->SourceAtlasTexture = InAtlas; Parameters->MipLevel = MipIt; Parameters->SrcSlotBuffer = GraphBuilder.CreateSRV(SrcSlotBuffer, PF_R16G16B16A16_UINT); Parameters->VS.AtlasResolution = Resolution; Parameters->VS.SlotBufferOffset = 0; Parameters->VS.SlotBuffer = GraphBuilder.CreateSRV(DstSlotBuffer, PF_R16G16B16A16_UINT); Parameters->VS.ScaleOffsetBuffer = DummyScaleOffsetBuffer; Parameters->VS.bHasScaleOffset = false; Parameters->RenderTargets[0] = FRenderTargetBinding(OutAtlas, ERenderTargetLoadAction::ELoad, MipIt); TShaderMapRef VertexShader(ShaderMap); TShaderMapRef PixelShader(ShaderMap); GraphBuilder.AddPass( RDG_EVENT_NAME("RectLightAtlas::CopyTexturePass(MIP:%d,Slots:%d)", MipIt, SlotCount), Parameters, ERDGPassFlags::Raster, [Parameters, VertexShader, PixelShader, Viewport, Resolution, SlotCount](FRDGAsyncTask, FRHICommandList& RHICmdList) { FGraphicsPipelineStateInitializer GraphicsPSOInit; RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit); GraphicsPSOInit.BlendState = TStaticBlendState::GetRHI(); GraphicsPSOInit.RasterizerState = TStaticRasterizerState<>::GetRHI(); GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState::GetRHI(); GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GEmptyVertexDeclaration.VertexDeclarationRHI; GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader(); GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader(); GraphicsPSOInit.PrimitiveType = PT_TriangleList; SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, 0); RHICmdList.SetViewport(Viewport.Min.X, Viewport.Min.Y, 0.0f, Viewport.Max.X, Viewport.Max.Y, 1.0f); SetShaderParameters(RHICmdList, PixelShader, PixelShader.GetPixelShader(), *Parameters); SetShaderParameters(RHICmdList, VertexShader, VertexShader.GetVertexShader(), Parameters->VS); RHICmdList.SetStreamSource(0, nullptr, 0); RHICmdList.DrawPrimitive(0, 2, SlotCount); }); } } } /////////////////////////////////////////////////////////////////////////////////////////////////// // Filtering pass class FRectAtlasFilterTexturePS : public FGlobalShader { DECLARE_GLOBAL_SHADER(FRectAtlasFilterTexturePS); SHADER_USE_PARAMETER_STRUCT(FRectAtlasFilterTexturePS, FGlobalShader); BEGIN_SHADER_PARAMETER_STRUCT(FParameters, ) SHADER_PARAMETER(FIntPoint, SrcAtlasResolution) SHADER_PARAMETER(uint32, FilterQuality) SHADER_PARAMETER_RDG_BUFFER_SRV(Buffer, DstSlotBuffer) SHADER_PARAMETER_RDG_BUFFER_SRV(Buffer, SrcSlotBuffer) SHADER_PARAMETER_SAMPLER(SamplerState, SourceAtlasSampler) SHADER_PARAMETER_RDG_TEXTURE_SRV(Texture2D, SourceAtlasTexture) SHADER_PARAMETER_STRUCT_INCLUDE(FRectLightAtlasVS::FParameters, VS) RENDER_TARGET_BINDING_SLOTS() END_SHADER_PARAMETER_STRUCT() public: static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters) { return true; } static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment) { FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment); OutEnvironment.SetDefine(TEXT("SHADER_FILTER_TEXTURE"), 1); } }; IMPLEMENT_GLOBAL_SHADER(FRectAtlasFilterTexturePS, "/Engine/Private/RectLightAtlas.usf", "MainPS", SF_Pixel); // Filter all provided slots static void FilterSlotsPass( FRDGBuilder& GraphBuilder, FGlobalShaderMap* ShaderMap, const TArray& InSlots, FRDGTextureRef& InAtlas) { FRDGBufferSRVRef DummyScaleOffsetBuffer = GraphBuilder.CreateSRV(GSystemTextures.GetDefaultBuffer(GraphBuilder, 16u, 1u), PF_A32B32G32R32F); const uint32 MipCount = InAtlas->Desc.NumMips; // FMath::Log2(FMath::Min(InAtlas->Desc.Extent.X, InAtlas->Desc.Extent.Y)); for (uint32 DstMip = 1; DstMip < MipCount; ++DstMip) { const uint32 SrcMip = DstMip-1; TArray SrcMIPSlots; TArray DstMIPSlots; SrcMIPSlots.Reserve(InSlots.Num()); DstMIPSlots.Reserve(InSlots.Num()); for (const FAtlasSlot& Slot : InSlots) { const bool bIsValidMIP = (Slot.Rect.Resolution.X >> DstMip) > 1 && (Slot.Rect.Resolution.Y >> DstMip) > 1; if (bIsValidMIP) { { FAtlasSlot& DstMIPSlot = DstMIPSlots.AddDefaulted_GetRef(); DstMIPSlot.Rect.Origin = ToMIP(Slot.Rect.Origin, DstMip); DstMIPSlot.Rect.Resolution = ToMIP(Slot.Rect.Resolution, DstMip); DstMIPSlot.SourceTexture = Slot.SourceTexture; } { FAtlasSlot& SrcMIPSlot = SrcMIPSlots.AddDefaulted_GetRef(); SrcMIPSlot.Rect.Origin = ToMIP(Slot.Rect.Origin, SrcMip); SrcMIPSlot.Rect.Resolution = ToMIP(Slot.Rect.Resolution, SrcMip); SrcMIPSlot.SourceTexture = Slot.SourceTexture; } } } const uint32 SlotCount = DstMIPSlots.Num(); if (SlotCount > 0) { FRDGBufferRef SrcSlotBuffer = CreateSlotBuffer(GraphBuilder, SrcMIPSlots, TEXT("RectLight.SrcMIPSlotBuffer")); FRDGBufferRef DstSlotBuffer = CreateSlotBuffer(GraphBuilder, DstMIPSlots, TEXT("RectLight.DstMIPSlotBuffer")); FRectAtlasFilterTexturePS::FParameters* Parameters = GraphBuilder.AllocParameters(); Parameters->FilterQuality = FMath::Clamp(CVarRectLighFilterQuality.GetValueOnRenderThread(), 0, 1); Parameters->SrcAtlasResolution = ToMIP(InAtlas->Desc.Extent, SrcMip); Parameters->DstSlotBuffer = GraphBuilder.CreateSRV(DstSlotBuffer, PF_R16G16B16A16_UINT); Parameters->SrcSlotBuffer = GraphBuilder.CreateSRV(SrcSlotBuffer, PF_R16G16B16A16_UINT); Parameters->SourceAtlasTexture = GraphBuilder.CreateSRV(FRDGTextureSRVDesc::CreateForMipLevel(InAtlas, SrcMip)); Parameters->SourceAtlasSampler = TStaticSamplerState::GetRHI(); Parameters->VS.AtlasResolution = ToMIP(InAtlas->Desc.Extent, DstMip); Parameters->VS.SlotBufferOffset = 0; Parameters->VS.SlotBuffer = GraphBuilder.CreateSRV(DstSlotBuffer, PF_R16G16B16A16_UINT); Parameters->VS.ScaleOffsetBuffer = DummyScaleOffsetBuffer; Parameters->VS.bHasScaleOffset = false; Parameters->RenderTargets[0] = FRenderTargetBinding(InAtlas, ERenderTargetLoadAction::ELoad, DstMip); TShaderMapRef VertexShader(ShaderMap); TShaderMapRef PixelShader(ShaderMap); GraphBuilder.AddPass( RDG_EVENT_NAME("RectLightAtlas::FilterTexturePass(Mip:%d,Slots:%d)", DstMip, SlotCount), Parameters, ERDGPassFlags::Raster, [Parameters, VertexShader, PixelShader, SlotCount](FRDGAsyncTask, FRHICommandList& RHICmdList) { const FIntPoint Resolution = Parameters->VS.AtlasResolution; const FIntRect Viewport(FIntPoint::ZeroValue, Resolution); FGraphicsPipelineStateInitializer GraphicsPSOInit; RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit); GraphicsPSOInit.BlendState = TStaticBlendState::GetRHI(); GraphicsPSOInit.RasterizerState = TStaticRasterizerState<>::GetRHI(); GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState::GetRHI(); GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GEmptyVertexDeclaration.VertexDeclarationRHI; GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader(); GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader(); GraphicsPSOInit.PrimitiveType = PT_TriangleList; SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, 0); RHICmdList.SetViewport(Viewport.Min.X, Viewport.Min.Y, 0.0f, Viewport.Max.X, Viewport.Max.Y, 1.0f); SetShaderParameters(RHICmdList, PixelShader, PixelShader.GetPixelShader(), *Parameters); SetShaderParameters(RHICmdList, VertexShader, VertexShader.GetVertexShader(), Parameters->VS); RHICmdList.SetStreamSource(0, nullptr, 0); RHICmdList.DrawPrimitive(0, 2, SlotCount); }); } } } /////////////////////////////////////////////////////////////////////////////////////////////////// // Internal update // Compute an aligned version of a rect. // The output rect will be smaller, but its origin will respect the alignement requirement static FAtlasRect ComputeAlignedRect(const FAtlasRect& In, int32 Alignement) { FAtlasRect Out; Out.Origin = FIntPoint( FMath::DivideAndRoundUp(In.Origin.X, Alignement) * Alignement, FMath::DivideAndRoundUp(In.Origin.Y, Alignement) * Alignement); Out.Resolution = (In.Resolution + In.Origin) - Out.Origin; return Out; } // Find a valid slot among the free rects static bool FindFreeRect(FAtlasLayout& Layout, FAtlasSlot& Slot, int32 Alignement) { #if USE_WASTE_RECT // 1. Find the smallest free rect than can contains the slot request int32 BestFreeRectIt = -1; int32 BestFreeRectResX = Layout.AtlasResolution.X * 2; FAtlasRect BestAlignedFreeRect; for (int32 FreeIt = 0, FreeCount = Layout.FreeRects.Num(); FreeIt < FreeCount; ++FreeIt) { // Compute the aligned free rect, to respect the slot alignement requirement const FAtlasRect& FreeRect = Layout.FreeRects[FreeIt]; const FAtlasRect AlignedFreeRect = ComputeAlignedRect(FreeRect, Alignement); if (CanContain(AlignedFreeRect, Slot.Rect) && AlignedFreeRect.Resolution.X < BestFreeRectResX) { BestFreeRectIt = FreeIt; BestFreeRectResX = FreeRect.Resolution.X; BestAlignedFreeRect = AlignedFreeRect; } } // 2. If found, split the rest of the valid free rect into two parts // --------- ----- // |Slot| | | | // | | | -> |Free0| // |---- | ----- | | // | | |Free1| | | // --------- ----- ----- // Fitted new New free // slot rects const bool bFit = BestFreeRectIt != -1; if (bFit) { // Assign the slot origin Slot.Rect.Origin = BestAlignedFreeRect.Origin; // Find the split for the two new rects const FAtlasRect BestFreeRect = Layout.FreeRects[BestFreeRectIt]; check(BestFreeRect.Origin.X >= 0 && BestFreeRect.Origin.Y >= 0); Layout.FreeRects.RemoveAtSwap(BestFreeRectIt); // If the new Slot doesn't fit the rect, split the remaining empty space into new free rects #if 1 const bool bFitX = Slot.Rect.Origin.X + Slot.Rect.Resolution.X == BestFreeRect.Origin.X + BestFreeRect.Resolution.X; const bool bFitY = Slot.Rect.Origin.Y + Slot.Rect.Resolution.Y == BestFreeRect.Origin.Y + BestFreeRect.Resolution.Y; // 1. Perfect fit, or right-fit // --------- // ||'''''''|| // || || // || || // ||.......|| // --------- if (bFitX && bFitY) { // The slot takes all the free. rect space. Nothing to do. } // 2. Width fit // --------- // ||'''''''|| // ||.......|| // | | // | | // --------- else if (bFitX) { FAtlasRect& FreeRect = Layout.FreeRects.AddDefaulted_GetRef(); FreeRect.Origin.X = Slot.Rect.Origin.X + Slot.Rect.Resolution.X; FreeRect.Origin.Y = BestFreeRect.Origin.Y; FreeRect.Resolution.X =(BestFreeRect.Origin.X+ BestFreeRect.Resolution.X) - FreeRect.Origin.X; FreeRect.Resolution.Y = BestFreeRect.Resolution.Y; } // 3. Height fit // --------- // ||''''| | // || | | // || | | // ||....| | // --------- else if (bFitY) { FAtlasRect& FreeRect = Layout.FreeRects.AddDefaulted_GetRef(); FreeRect.Origin.X = BestFreeRect.Origin.X; FreeRect.Origin.Y = Slot.Rect.Origin.Y + Slot.Rect.Resolution.Y; FreeRect.Resolution.X = BestFreeRect.Resolution.X; FreeRect.Resolution.Y = (BestFreeRect.Origin.Y + BestFreeRect.Resolution.Y) - FreeRect.Origin.Y; } // 4. Two splits // --------- // ||''''| | // || | | // ||....| | // | | // --------- else { { FAtlasRect& FreeRect = Layout.FreeRects.AddDefaulted_GetRef(); FreeRect.Origin.X = BestFreeRect.Origin.X; FreeRect.Origin.Y = Slot.Rect.Origin.Y + Slot.Rect.Resolution.Y; FreeRect.Resolution.X =(Slot.Rect.Origin.X + Slot.Rect.Resolution.X) - FreeRect.Origin.X; FreeRect.Resolution.Y =(BestFreeRect.Origin.Y + BestFreeRect.Resolution.Y) - FreeRect.Origin.Y; } { FAtlasRect& FreeRect = Layout.FreeRects.AddDefaulted_GetRef(); FreeRect.Origin.X = Slot.Rect.Origin.X + Slot.Rect.Resolution.X; FreeRect.Origin.Y = BestFreeRect.Origin.Y; FreeRect.Resolution.X =(BestFreeRect.Origin.X + BestFreeRect.Resolution.X) - FreeRect.Origin.X; FreeRect.Resolution.Y = BestFreeRect.Resolution.Y; } } #endif } return bFit; #endif // USE_WASTE_RECT } // Fit a new slot into the curent layout. Returns true if the slot can be fitted, false otherwise static bool FitAlignedSlot(FAtlasLayout& Layout, FAtlasSlot& Slot) #if USE_PACKING_MODE == 0 // Use Shelf packing algo. { // Due to MIPs we need to ensure that the rect is aligned onto its pow2 resolution const uint32 MaxMIP = GetSlotMaxMIPLevel(Slot); const int32 Alignement = 1u << MaxMIP; // 1. Find a valid slot among the free rects bool bFit = FindFreeRect(Layout, Slot, Alignement); // 2. Try to fit slot if (!bFit) { const FIntPoint AlignedSplit( FMath::DivideAndRoundUp(Layout.SplitX, Alignement) * Alignement, FMath::DivideAndRoundUp(Layout.SplitY, Alignement) * Alignement); const int32 AlignedMaxY = FMath::DivideAndRoundUp(Layout.MaxY, Alignement) * Alignement; // 2.1. Try to fit into the current row if ( AlignedSplit.X + Slot.Rect.Resolution.X <= Layout.AtlasResolution.X && AlignedSplit.Y + Slot.Rect.Resolution.Y <= Layout.AtlasResolution.Y) { //Slot.Origin = FIntPoint(Layout.SplitX, Layout.SplitY); Slot.Rect.Origin = FIntPoint(AlignedSplit.X, AlignedSplit.Y); Layout.SplitX += Slot.Rect.Resolution.X; Layout.MaxY = FMath::Max(Layout.MaxY, AlignedSplit.Y + Slot.Rect.Resolution.Y); } // 2.2. Try to fit in a the next row else if ( Slot.Rect.Resolution.X <= Layout.AtlasResolution.X && AlignedMaxY + Slot.Rect.Resolution.Y <= Layout.AtlasResolution.Y) { Layout.SplitX = 0; Layout.SplitY = Layout.MaxY; Slot.Rect.Origin = FIntPoint(Layout.SplitX, AlignedMaxY); Layout.SplitX = Slot.Rect.Resolution.X; Layout.MaxY = AlignedMaxY + Slot.Rect.Resolution.Y; } // 2.3. It doesn't fit with the current atlas resolution else { bFit = false; } } return bFit; } #elif USE_PACKING_MODE == 1 // Use skyline packing algo. { // Due to MIPs we need to ensure that the rect is aligned onto its pow2 resolution const uint32 MaxMIP = GetSlotMaxMIPLevel(Slot); const int32 Alignement = 1u << MaxMIP; // 0. If the layout is verbatim, initialize the horizon data if (Layout.Horizons.IsEmpty()) { FAtlasHorizon& H = Layout.Horizons.AddDefaulted_GetRef(); H.Line.Origin = FIntPoint(0, 0); H.Line.Resolution = Layout.AtlasResolution; H.ExtendedLine = H.Line; } // 1. Find a valid slot among the free rects bool bFit = FindFreeRect(Layout, Slot, Alignement); // 2. Fit rectangle within the existing horizon if (!bFit) { // 2.2 Find the horizon line which result in the lowest horizon after update int32 MinHeight = Layout.AtlasResolution.Y * 2; // *2 to ensure MinHeight is initialized with a larger height than the default horizon (i.e., full atlas) int32 BestHorizonIt = -1; FAtlasRect BestAlignedHorizon; for (int32 HorizonIt = 0, HorizonCount = Layout.Horizons.Num(); HorizonIt < HorizonCount; ++HorizonIt) { // Compute the aligned horizon for taking into account filtering/MIP-mapping requirement const FAtlasRect AlignedHorizon = ComputeAlignedRect(Layout.Horizons[HorizonIt].ExtendedLine, Alignement); if (CanContain(AlignedHorizon, Slot.Rect)) { if (AlignedHorizon.Origin.Y + Slot.Rect.Resolution.Y < MinHeight) { MinHeight = AlignedHorizon.Origin.Y + Slot.Rect.Resolution.Y; BestAlignedHorizon = AlignedHorizon; BestHorizonIt = HorizonIt; } } } // 2.3. Insert & update horizon bFit = BestHorizonIt != -1; if (bFit) { // The slot is inserted with Left-most insertion. // TODO compute waste space heuristic left/right to pick the best Slot.Rect.Origin = BestAlignedHorizon.Origin; // 2.3.1 Update Horizon segments const uint32 SlotY = Slot.Rect.Origin.Y + Slot.Rect.Resolution.Y; const uint32 StartX = Slot.Rect.Origin.X; const uint32 EndX = Slot.Rect.Origin.X + Slot.Rect.Resolution.X; { TArray NewHorizonRects; NewHorizonRects.Reserve(Layout.Horizons.Num()); for (int32 HorizonIt = 0, HorizonCount = Layout.Horizons.Num(); HorizonIt < HorizonCount; ++HorizonIt) { const FAtlasHorizon& Horizon = Layout.Horizons[HorizonIt]; const uint32 HorizonStartX = Horizon.Line.Origin.X; const uint32 HorizonEndX = Horizon.Line.Origin.X + Horizon.Line.Resolution.X; const uint32 HorizonY = Horizon.Line.Origin.Y; const bool bIntersection = !(EndX < HorizonStartX || StartX > HorizonEndX); if (bIntersection) { // Sanity check //check(HorizonY <= SlotY); // 1. Complete cover // Slot |----------| -> Output |----------| // Horizon |---| -> if (StartX <= HorizonStartX && EndX >= HorizonEndX) { if ((StartX == HorizonStartX && EndX == HorizonEndX) || StartX == HorizonStartX) { FAtlasHorizon& H = NewHorizonRects.AddDefaulted_GetRef(); H.Line.Origin.X = StartX; H.Line.Origin.Y = SlotY; H.Line.Resolution.X = EndX - StartX; H.Line.Resolution.Y = Layout.AtlasResolution.Y - SlotY; H.ExtendedLine = H.Line; } // Skip horizon line, as it is totally occluded } // 2. Inside cover // Slot |---| -> Output |---| // Horizon |----------| -> |--| |---| else if (StartX > HorizonStartX && EndX < HorizonEndX) { { FAtlasHorizon& H = NewHorizonRects.AddDefaulted_GetRef(); H.Line.Origin.X = HorizonStartX; H.Line.Origin.Y = HorizonY; H.Line.Resolution.X = StartX - HorizonStartX; H.Line.Resolution.Y = Layout.AtlasResolution.Y - HorizonY; H.ExtendedLine = H.Line; } { FAtlasHorizon& H = NewHorizonRects.AddDefaulted_GetRef(); H.Line.Origin.X = StartX; H.Line.Origin.Y = SlotY; H.Line.Resolution.X = EndX - StartX; H.Line.Resolution.Y = Layout.AtlasResolution.Y - SlotY; H.ExtendedLine = H.Line; } { FAtlasHorizon& H = NewHorizonRects.AddDefaulted_GetRef(); H.Line.Origin.X = EndX; H.Line.Origin.Y = HorizonY; H.Line.Resolution.X = HorizonEndX - EndX; H.Line.Resolution.Y = Layout.AtlasResolution.Y - HorizonY; H.ExtendedLine = H.Line; } } // 3. Left cover // Slot |---| -> Output |--| // Horizon |----------| -> |-------| else if (StartX <= HorizonStartX && EndX < HorizonEndX) { //if (StartX == HorizonStartX && SlotY < uint32(Layout.AtlasResolution.Y)) { FAtlasHorizon& H = NewHorizonRects.AddDefaulted_GetRef(); H.Line.Origin.X = StartX; H.Line.Origin.Y = SlotY; H.Line.Resolution.X = EndX - StartX; H.Line.Resolution.Y = Layout.AtlasResolution.Y - SlotY; H.ExtendedLine = H.Line; } { FAtlasHorizon& H = NewHorizonRects.AddDefaulted_GetRef(); H.Line.Origin.X = EndX; H.Line.Origin.Y = HorizonY; H.Line.Resolution.X = HorizonEndX - EndX; H.Line.Resolution.Y = Layout.AtlasResolution.Y - HorizonY; H.ExtendedLine = H.Line; } } // 4. Right cover // Slot |---| -> Output |--| // Horizon |----------| -> |-------| else if (StartX > HorizonStartX && EndX <= HorizonEndX) { { FAtlasHorizon& H = NewHorizonRects.AddDefaulted_GetRef(); H.Line.Origin.X = HorizonStartX; H.Line.Origin.Y = HorizonY; H.Line.Resolution.X = StartX - HorizonStartX; H.Line.Resolution.Y = Layout.AtlasResolution.Y - HorizonY; H.ExtendedLine = H.Line; } { FAtlasHorizon& H = NewHorizonRects.AddDefaulted_GetRef(); H.Line.Origin.X = StartX; H.Line.Origin.Y = SlotY; H.Line.Resolution.X = EndX - StartX; H.Line.Resolution.Y = Layout.AtlasResolution.Y - SlotY; H.ExtendedLine = H.Line; } } // 5. Right cover with overflow (merge this case with 4.) // Slot |------| -> Output |------| // Horizon |----------| -> |------| else if (StartX > HorizonStartX && StartX < HorizonEndX) { { FAtlasHorizon& H = NewHorizonRects.AddDefaulted_GetRef(); H.Line.Origin.X = HorizonStartX; H.Line.Origin.Y = HorizonY; H.Line.Resolution.X = StartX - HorizonStartX; H.Line.Resolution.Y = Layout.AtlasResolution.Y - HorizonY; H.ExtendedLine = H.Line; } { FAtlasHorizon& H = NewHorizonRects.AddDefaulted_GetRef(); H.Line.Origin.X = StartX; H.Line.Origin.Y = SlotY; H.Line.Resolution.X = EndX - StartX; H.Line.Resolution.Y = Layout.AtlasResolution.Y - SlotY; H.ExtendedLine = H.Line; } } // 6. Left cover with overflow (merge this case with 3.) // Slot |-----| -> Output |-----| // Horizon |----------| -> |----| else if (EndX > StartX && EndX < HorizonEndX) { { FAtlasHorizon& H = NewHorizonRects.AddDefaulted_GetRef(); H.Line.Origin.X = EndX; H.Line.Origin.Y = HorizonY; H.Line.Resolution.X = HorizonEndX - EndX; H.Line.Resolution.Y = Layout.AtlasResolution.Y - HorizonY; H.ExtendedLine = H.Line; } } else { // Sanity check check(StartX == HorizonEndX); NewHorizonRects.Add(Horizon); } } else { FAtlasHorizon& NewHorizon = NewHorizonRects.Add_GetRef(Horizon); NewHorizon.ExtendedLine = NewHorizon.Line; } } Layout.Horizons = NewHorizonRects; // 2.3.2 Rebuild extended horizon data // The first step (Left->Right) could be done during the horizon building) if (Layout.Horizons.Num()) { // Extend left-horizon by sweeping: Left -> Right { TArray VisibilityStack; VisibilityStack.Add(FIntPoint(0, Layout.AtlasResolution.Y+1)); for (int32 HorizonIt = 0, HorizonCount = Layout.Horizons.Num(); HorizonIt < HorizonCount; ++HorizonIt) { FAtlasHorizon& H = Layout.Horizons[HorizonIt]; if (H.Line.Origin.Y >= VisibilityStack.Last().Y) { while (H.Line.Origin.Y >= VisibilityStack.Last().Y) { VisibilityStack.Pop(EAllowShrinking::No); } // Sanity check(!VisibilityStack.IsEmpty()); } H.ExtendedLine.Origin.X = VisibilityStack.Last().X; H.ExtendedLine.Resolution.X += (H.Line.Origin.X - H.ExtendedLine.Origin.X); VisibilityStack.Add(FIntPoint(H.Line.Origin.X + H.Line.Resolution.X, H.Line.Origin.Y)); } } // Extend right-horizon by sweeping: Right -> Left { TArray VisibilityStack; VisibilityStack.Add(FIntPoint(Layout.AtlasResolution.X, Layout.AtlasResolution.Y+1)); for (int32 HorizonIt = Layout.Horizons.Num() - 1; HorizonIt >= 0; --HorizonIt) { FAtlasHorizon& H = Layout.Horizons[HorizonIt]; if (H.Line.Origin.Y >= VisibilityStack.Last().Y) { while (H.Line.Origin.Y >= VisibilityStack.Last().Y) { VisibilityStack.Pop(EAllowShrinking::No); } // Sanity check(!VisibilityStack.IsEmpty()); } H.ExtendedLine.Resolution.X = VisibilityStack.Last().X - H.ExtendedLine.Origin.X; VisibilityStack.Add(H.Line.Origin); } } } } } } return bFit; } #elif USE_PACKING_MODE == 2 // Use FTextureLayout packer { uint32 OriginX = 0; uint32 OriginY = 0; const bool bFit = Layout.Packer.AddElement(OriginX, OriginY, Slot.Rect.Resolution.X, Slot.Rect.Resolution.Y); Slot.Rect.Origin.X = OriginX; Slot.Rect.Origin.Y = OriginY; return bFit; } #endif // Update the atlas layout (book-keeping) with the remove slots static void CleanAtlas( FAtlasLayout& InLayout, const TArray& InDeleteSlots) { for (const FAtlasRect& Rect : InDeleteSlots) { #if USE_WASTE_RECT && (USE_PACKING_MODE == 1 || USE_PACKING_MODE == 2) if (Rect.Origin.X >= 0 && Rect.Origin.Y >= 0) { InLayout.FreeRects.Add(Rect); } #elif USE_PACKING_MODE ==2 InLayout.Packer.RemoveElement(Rect.Origin.X, Rect.Origin.Y, Rect.Resolution.X, Rect.Resolution.Y); #endif } } // Fit a set of slots within an existing altas layout, or create a new layout. // * Try to fit the new slot within the existing layout. // * If it fails, produce a new atlas layout. static void PackAtlas( FAtlasLayout& Layout, const TArray& InSlots, TArray& CopySlots, TArray& NewSlots) { const int32 MaxAtlasResolution = CVarRectLightTextureResolution.GetValueOnRenderThread(); // Extract slots which are already parts of the current layout from the new/requested slots. // Estimate of the target resolution; uint32 TargetPixelCount = 0; TArray ValidSlots; TArray ValidNewSlots; ValidSlots.Reserve(InSlots.Num()); ValidNewSlots.Reserve(InSlots.Num()); for (const FAtlasSlot& Slot : InSlots) { if (Slot.IsValid()) { FAtlasSlot& ValidSlot = ValidSlots.Add_GetRef(Slot); // Check if the texture resolution has changed (due to streaming) // If the texture resolution has change, reset the slot to be handled as a new slot const FIntPoint TextureResolution = ValidSlot.GetSourceResolution(); const bool bHasTextureResolutionChanged = ValidSlot.CachedSourceTextureResolution != TextureResolution; if (bHasTextureResolutionChanged || ValidSlot.bForceRefresh) { ValidSlot.Rect.Origin = InvalidOrigin; ValidSlot.Rect.Resolution = TextureResolution; AlignSlotResolution(ValidSlot); ValidSlot.CachedSourceTextureResolution = TextureResolution; } TargetPixelCount += ValidSlot.Rect.Resolution.X * ValidSlot.Rect.Resolution.Y; if (ValidSlot.Rect.Origin == InvalidOrigin) { ValidNewSlots.Add(ValidSlot); } } } // Initialize atlas resolution the first time if (Layout.AtlasResolution == FIntPoint::ZeroValue) { const float HeuristicFactor = 1.0f; Layout.AtlasResolution = FMath::RoundUpToPowerOfTwo(FMath::Sqrt(float(TargetPixelCount)) * HeuristicFactor); Layout.AtlasResolution.X = FMath::Min(MaxAtlasResolution, Layout.AtlasResolution.X); Layout.AtlasResolution.Y = FMath::Min(MaxAtlasResolution, Layout.AtlasResolution.Y); } // Sort rect by perimeter ValidSlots.Sort ([](const FAtlasSlot& A, const FAtlasSlot& B) { return (A.Rect.Resolution.X + A.Rect.Resolution.Y) > (B.Rect.Resolution.X + B.Rect.Resolution.Y); }); ValidNewSlots.Sort ([](const FAtlasSlot& A, const FAtlasSlot& B) { return (A.Rect.Resolution.X + A.Rect.Resolution.Y) > (B.Rect.Resolution.X + B.Rect.Resolution.Y); }); // 1. Try to fit the new slots into the current layout bool bNeedRefit = false; { for (const FAtlasSlot& Slot : ValidNewSlots) { FAtlasSlot NewSlot = Slot; if (!FitAlignedSlot(Layout, NewSlot)) { bNeedRefit = true; break; } NewSlots.Add(NewSlot); } } // 2. Couldn't fit the new textures into the current layout. Refit the atlas layout // * Use Simple row packing algo. // * First iteration try to repack the texure within the existing resolution // * Subsequent iteration increaes atlas resolution to fit the textures if (bNeedRefit) { CopySlots.Reserve(ValidSlots.Num()); CopySlots.SetNum(0, EAllowShrinking::No); NewSlots.SetNum(0, EAllowShrinking::No); FIntPoint CurrentAtlasResolution = Layout.AtlasResolution; int32 CurrentSourceTextureMIPBias = 0; // When a refit is done, restart with a MIP bias of 0, to ensure we use the full potential space of the atlas bool bIsPackingValid = false; while (!bIsPackingValid) { // 2.0 Reset atlas layout Layout = FAtlasLayout(CurrentAtlasResolution); Layout.AtlasResolution = CurrentAtlasResolution; Layout.SourceTextureMIPBias = CurrentSourceTextureMIPBias; // 2.1 Try to fit all slots bool bFit = true; for (const FAtlasSlot& SrcSlot : ValidSlots) { FAtlasSlot DstSlot = SrcSlot; if (!FitAlignedSlot(Layout, DstSlot)) { bFit = false; break; } const bool bIsNewSlot = SrcSlot.Rect.Origin == InvalidOrigin; if (bIsNewSlot) { NewSlots.Add(DstSlot); } else { FAtlasCopySlot& CopySlot = CopySlots.AddDefaulted_GetRef(); CopySlot.Id = SrcSlot.Id; CopySlot.Resolution = SrcSlot.Rect.Resolution; CopySlot.SrcOrigin = SrcSlot.Rect.Origin; CopySlot.DstOrigin = DstSlot.Rect.Origin; CopySlot.SourceTexture = SrcSlot.SourceTexture; } } // 2.2 If the current slots don't fit, increase atlas resolution if (!bFit) { // Ensure the atlas resolution fit under the user requirement. // Otherwise starts to drop SourceTexture resolution if (Layout.AtlasResolution.X * 2 <= MaxAtlasResolution && Layout.AtlasResolution.Y * 2 <= MaxAtlasResolution) { CurrentAtlasResolution = Layout.AtlasResolution * 2; } else { // Mark all textures as invalid to force copy them into the atlas with a lower mip index CurrentSourceTextureMIPBias++; for (FAtlasSlot& Slot : ValidSlots) { const FIntPoint SourceResolution = Slot.GetSourceResolution(); Slot.Rect.Origin = InvalidOrigin; Slot.Rect.Resolution = ToMIP(SourceResolution, CurrentSourceTextureMIPBias); Slot.CachedSourceTextureResolution = Slot.Rect.Resolution; AlignSlotResolution(Slot); } } CopySlots.SetNum(0, EAllowShrinking::No); NewSlots.SetNum(0, EAllowShrinking::No); } else { bIsPackingValid = true; } } } } /////////////////////////////////////////////////////////////////////////////////////////////////// // API uint32 AddTexture(UTexture* In, const FVector4f& ScaleOffset) { check(IsInRenderingThread()); uint32 SlotIndex = InvalidSlotIndex; if (In) { // 1. Find if the texture already exist (simple linear search assuming a low number of textures) bool bFound = false; for (FAtlasSlot& Slot : GRectLightTextureManager.AtlasSlots) { if ((Slot.SourceTexture == In->TextureReference.TextureReferenceRHI) && (Slot.SourceScaleOffset == ScaleOffset)) { Slot.RefCount++; SlotIndex = Slot.Id; bFound = true; break; } } // 2. If not found, then add an atlas slot for this new texture if (!bFound) { FAtlasSlot* Slot = nullptr; if (GRectLightTextureManager.FreeSlots.Dequeue(SlotIndex)) { Slot = &GRectLightTextureManager.AtlasSlots[SlotIndex]; } else { SlotIndex = GRectLightTextureManager.AtlasSlots.Num(); Slot = &GRectLightTextureManager.AtlasSlots.AddDefaulted_GetRef(); } const FRHITexture* Tex = In->TextureReference.TextureReferenceRHI; *Slot = FAtlasSlot(); Slot->SourceTexture = In->TextureReference.TextureReferenceRHI; Slot->SourceScaleOffset = ScaleOffset; Slot->Id = SlotIndex; Slot->Rect.Origin = InvalidOrigin; Slot->Rect.Resolution = GetSourceResolution(Tex, ScaleOffset); Slot->CachedSourceTextureResolution = Slot->Rect.Resolution; Slot->RefCount = 1; // Force alignement based on the max. MIP-level AlignSlotResolution(*Slot); GRectLightTextureManager.bHasPendingAdds = true; } } return SlotIndex; } void RemoveTexture(uint32 InSlotIndex) { check(IsInRenderingThread()); if (InSlotIndex != InvalidSlotIndex && InSlotIndex < uint32(GRectLightTextureManager.AtlasSlots.Num())) { // If it is the last light referencing this texture, we retires the atlas slot if (--GRectLightTextureManager.AtlasSlots[InSlotIndex].RefCount == 0) { // Add pending slots to clean-up the layout during the next update call GRectLightTextureManager.DeletedSlots.Add(GRectLightTextureManager.AtlasSlots[InSlotIndex].Rect); const int32 SlotCount = GRectLightTextureManager.AtlasSlots.Num(); if (InSlotIndex == SlotCount-1) { GRectLightTextureManager.AtlasSlots.SetNum(SlotCount-1); } else { GRectLightTextureManager.FreeSlots.Enqueue(InSlotIndex); GRectLightTextureManager.AtlasSlots[InSlotIndex] = FAtlasSlot(); } GRectLightTextureManager.bHasPendingDeletes = true; } } } FAtlasSlotDesc GetAtlasSlot(uint32 InSlotIndex) { FAtlasSlotDesc Out; Out.UVOffset = FVector2f(0,0); Out.UVScale = FVector2f(0, 0); Out.MaxMipLevel = FLightRenderParameters::GetRectLightAtlasInvalidMIPLevel(); if (InSlotIndex < uint32(GRectLightTextureManager.AtlasSlots.Num()) && GRectLightTextureManager.AtlasTexture) { const FAtlasSlot& Slot = GRectLightTextureManager.AtlasSlots[InSlotIndex]; const FIntPoint Resolution = GRectLightTextureManager.AtlasTexture->GetDesc().Extent; const FVector2f InvResolution(1.f / Resolution.X, 1.f / Resolution.Y); // UV rect shrinking, to avoid leaking during tri/bi-linear filtering, is done within the shader, as it depends on the selected MIP value. // UVOffset/UVScale gives the rect value without any border/clamping Out.UVOffset = FVector2f(Slot.Rect.Origin.X * InvResolution.X, Slot.Rect.Origin.Y * InvResolution.Y); Out.UVScale = FVector2f(Slot.Rect.Resolution.X * InvResolution.X, Slot.Rect.Resolution.Y * InvResolution.Y); Out.MaxMipLevel = GetSlotMaxMIPLevel(Slot); } return Out; } static FRDGTextureRef CreateRectLightAtlasTexture(FRDGBuilder& GraphBuilder, const FIntPoint& Resolution, EPixelFormat AtlasFormat) { const uint32 MipCount = FMath::Log2(float(FMath::Min(Resolution.X, Resolution.Y))); return GraphBuilder.CreateTexture(FRDGTextureDesc::Create2D( Resolution, AtlasFormat, FClearValueBinding::Transparent, ETextureCreateFlags::UAV | ETextureCreateFlags::ShaderResource | ETextureCreateFlags::RenderTargetable, MipCount), TEXT("RectLight.AtlasTexture"), ERDGTextureFlags::MultiFrame); } void UpdateAtlasTexture(FRDGBuilder& GraphBuilder, const ERHIFeatureLevel::Type FeatureLevel) { if (GRectLightTextureManager.bLock) { return; } // Force update by resetting the atlas layout static int32 CachedMaxAtlasResolution = CVarRectLightTextureResolution.GetValueOnRenderThread(); static EPixelFormat CachedAtlasFormat = GetRectLightAtlasFormat(); const EPixelFormat AtlasFormat = GetRectLightAtlasFormat(); const bool bForceUpdate = CVarRectLighForceUpdate.GetValueOnRenderThread() > 0 || CachedMaxAtlasResolution != CVarRectLightTextureResolution.GetValueOnRenderThread() || CachedAtlasFormat != AtlasFormat; const bool bResetOnChange = CVarRectLighResetOnChange.GetValueOnRenderThread() > 0; const bool bClearAtlas = CVarRectLighClearAtlas.GetValueOnRenderThread() > 0; auto ResetAtlas = [&]() { CachedMaxAtlasResolution = CVarRectLightTextureResolution.GetValueOnRenderThread(); CachedAtlasFormat = AtlasFormat; GRectLightTextureManager.bHasPendingAdds = true; GRectLightTextureManager.AtlasLayout = FAtlasLayout(FIntPoint(CachedMaxAtlasResolution, CachedMaxAtlasResolution)); GRectLightTextureManager.DeletedSlots.Empty(); GRectLightTextureManager.FreeSlots.Empty(); for (FAtlasSlot& Slot : GRectLightTextureManager.AtlasSlots) { Slot.Rect.Origin = InvalidOrigin; } }; if (bForceUpdate) { ResetAtlas(); } // Force update if among the existing valid slot a streamed a higher resolution than the existing one uint32 RefreshRequestCount = 0; { for (FAtlasSlot& Slot : GRectLightTextureManager.AtlasSlots) { if (Slot.IsValid() && Slot.GetTextureRHI()) { const FIntPoint SourceResolutionMIPed = ToMIP(Slot.GetSourceResolution(), GRectLightTextureManager.AtlasLayout.SourceTextureMIPBias); if (SourceResolutionMIPed.X > Slot.Rect.Resolution.X || SourceResolutionMIPed.Y > Slot.Rect.Resolution.Y) { // Invalid the slot Slot.Rect.Origin = InvalidOrigin; GRectLightTextureManager.bHasPendingAdds = true; } if (Slot.bForceRefresh) { ++RefreshRequestCount; } } } } // Force the atlas to be reset when some change are detected (optional) if (GRectLightTextureManager.bHasPendingAdds && !bForceUpdate && bResetOnChange) { ResetAtlas(); } // Update the atlas layout with the delete slots if (GRectLightTextureManager.DeletedSlots.Num() > 0) { CleanAtlas(GRectLightTextureManager.AtlasLayout, GRectLightTextureManager.DeletedSlots); GRectLightTextureManager.DeletedSlots.SetNum(0); GRectLightTextureManager.bHasPendingDeletes = false; } // Process new atlas entries if (GRectLightTextureManager.bHasPendingAdds) { FGlobalShaderMap* ShaderMap = GetGlobalShaderMap(FeatureLevel); // 1. Compute new layout TArray NewSlots; TArray CopySlots; PackAtlas( GRectLightTextureManager.AtlasLayout, GRectLightTextureManager.AtlasSlots, CopySlots, NewSlots); // 2. Apply layout modification const bool bHasChanged = !NewSlots.IsEmpty() || !CopySlots.IsEmpty(); if (bHasChanged) { // 2.0 Register or create the texture atlas FRDGTextureRef AtlasTexture = nullptr; bool bNeedExtraction = false; const bool bRecreateAtlasTexture = GRectLightTextureManager.AtlasTexture == nullptr || (CopySlots.Num() == 0 && GRectLightTextureManager.AtlasTexture->GetDesc().Extent != GRectLightTextureManager.AtlasLayout.AtlasResolution); if (bRecreateAtlasTexture) { AtlasTexture = CreateRectLightAtlasTexture(GraphBuilder, GRectLightTextureManager.AtlasLayout.AtlasResolution, AtlasFormat); #if RECT_ATLAS_DEBUG_CLEAR AddRectLightClearPass(GraphBuilder, ShaderMap, AtlasTexture); #endif bNeedExtraction = true; // Sanity check check(CopySlots.Num() == 0); } else { AtlasTexture = GraphBuilder.RegisterExternalTexture(GRectLightTextureManager.AtlasTexture); // Clear the atlas (optional) if (bClearAtlas && bResetOnChange && CopySlots.Num() == 0) { #if RECT_ATLAS_DEBUG_CLEAR AddRectLightClearPass(GraphBuilder, ShaderMap, AtlasTexture); #else AddClearRenderTargetPass(GraphBuilder, AtlasTexture, FLinearColor::Transparent); #endif } } // 2.1 Copy slots from previous to new atlas texture if (CopySlots.Num() > 0) { FRDGTextureRef NewAtlasTexture = CreateRectLightAtlasTexture(GraphBuilder, GRectLightTextureManager.AtlasLayout.AtlasResolution, AtlasFormat); bNeedExtraction = true; #if RECT_ATLAS_DEBUG_CLEAR AddRectLightClearPass(GraphBuilder, ShaderMap, NewAtlasTexture); #endif CopySlotsPass(GraphBuilder, ShaderMap, CopySlots, AtlasTexture, NewAtlasTexture); for (FAtlasCopySlot& Slot : CopySlots) { // Update: // * the slot Origin based on the new layout & Resolution with new layout data // * the slot Resolution in case the texture has streamed a higher-res version GRectLightTextureManager.AtlasSlots[Slot.Id].Rect.Origin = Slot.DstOrigin; GRectLightTextureManager.AtlasSlots[Slot.Id].Rect.Resolution = Slot.Resolution; GRectLightTextureManager.AtlasSlots[Slot.Id].bForceRefresh = false; } AtlasTexture = NewAtlasTexture; } // 2.2 Insert & filter new slots into the atlas texture if (NewSlots.Num() > 0) { AddSlotsPass(GraphBuilder, ShaderMap, GRectLightTextureManager.AtlasLayout.SourceTextureMIPBias, NewSlots, AtlasTexture); FilterSlotsPass(GraphBuilder, ShaderMap, NewSlots, AtlasTexture); for (FAtlasSlot& Slot : NewSlots) { // Update: // * the slot Origin based on the new layout & Resolution with new layout data // * the slot Resolution in case the texture has streamed a higher-res version GRectLightTextureManager.AtlasSlots[Slot.Id].Rect.Origin = Slot.Rect.Origin; GRectLightTextureManager.AtlasSlots[Slot.Id].Rect.Resolution = Slot.Rect.Resolution; GRectLightTextureManager.AtlasSlots[Slot.Id].bForceRefresh = false; } } if (bNeedExtraction) { GRectLightTextureManager.AtlasTexture = GraphBuilder.ConvertToExternalTexture(AtlasTexture); } } GRectLightTextureManager.bHasPendingAdds = false; } // Process forced refresh slots else if (RefreshRequestCount > 0) { TArray RefreshSlots; RefreshSlots.Reserve(FMath::Max(1u, RefreshRequestCount)); for (FAtlasSlot& Slot : GRectLightTextureManager.AtlasSlots) { if (Slot.bForceRefresh) { Slot.bForceRefresh = false; RefreshSlots.Add(Slot); } } FGlobalShaderMap* ShaderMap = GetGlobalShaderMap(FeatureLevel); FRDGTextureRef AtlasTexture = GraphBuilder.RegisterExternalTexture(GRectLightTextureManager.AtlasTexture); AddSlotsPass(GraphBuilder, ShaderMap, GRectLightTextureManager.AtlasLayout.SourceTextureMIPBias, RefreshSlots, AtlasTexture); FilterSlotsPass(GraphBuilder, ShaderMap, RefreshSlots, AtlasTexture); } } void AddDebugPass(FRDGBuilder& GraphBuilder, const FViewInfo& View, FRDGTextureRef OutputTexture) { if (CVarRectLighTextureDebug.GetValueOnRenderThread() > 0 && ShaderPrint::IsSupported(View.Family->GetShaderPlatform())) { if (FRDGTextureRef DebugOutput = AddRectLightDebugInfoPass(GraphBuilder, View, OutputTexture->Desc)) { // Debug output is blend on top of the SceneColor/OutputTexture, as debug pass is a CS pass, and SceneColor/OutputTexture might not have a UAV flag FCopyRectPS::FParameters* Parameters = GraphBuilder.AllocParameters(); Parameters->InputTexture = DebugOutput; Parameters->InputSampler = TStaticSamplerState::GetRHI(); Parameters->RenderTargets[0] = FRenderTargetBinding(OutputTexture, ERenderTargetLoadAction::ELoad); const FScreenPassTextureViewport InputViewport(DebugOutput->Desc.Extent); const FScreenPassTextureViewport OutputViewport(OutputTexture); TShaderMapRef PixelShader(View.ShaderMap); TShaderMapRef VertexShader(View.ShaderMap); FRHIBlendState* BlendState = TStaticBlendState::GetRHI(); FRHIDepthStencilState* DepthStencilState = FScreenPassPipelineState::FDefaultDepthStencilState::GetRHI(); AddDrawScreenPass(GraphBuilder, RDG_EVENT_NAME("RectLightAtlas::BlitDebug"), View, OutputViewport, InputViewport, VertexShader, PixelShader, BlendState, DepthStencilState, Parameters, EScreenPassDrawFlags::None); } } } FRHITexture* GetAtlasTexture() { return GRectLightTextureManager.AtlasTexture ? GRectLightTextureManager.AtlasTexture->GetRHI() : nullptr; } // Scope object allowing to force refresh of a slot texture, and locking/preventing // the atlas update during the 'update'/'capture' FAtlasTextureInvalidationScope::FAtlasTextureInvalidationScope(const UTexture* In) { if (In) { // Several rect lights can reference the same animated texture, but with different offset/scale. // In this case we need to refresh all rects referencing this texture. bool bHasBeenLockedLocally = false; for (FAtlasSlot& Slot : GRectLightTextureManager.AtlasSlots) { // If the input texture is actually part of the atlas, // then we lock the atlas to not update the atlas during // the capture, and force the target texture to refresh // its data during the next update if (Slot.SourceTexture == In->TextureReference.TextureReferenceRHI) { if (!bHasBeenLockedLocally) { // Sanity check, allow a single lock/capture refresh at a time. check(GRectLightTextureManager.bLock == false); GRectLightTextureManager.bLock = true; bHasBeenLockedLocally = true; } bLocked = true; Slot.bForceRefresh = true; } } } } FAtlasTextureInvalidationScope::~FAtlasTextureInvalidationScope() { if (bLocked) { // Sanity check, allow a single lock/capture refresh at a time. check(GRectLightTextureManager.bLock == true); GRectLightTextureManager.bLock = false; bLocked = false; } } } // namespace RectLightAtlas