// Copyright Epic Games, Inc. All Rights Reserved. /*============================================================================= Functionality for capturing the scene into reflection capture cubemaps, and prefiltering =============================================================================*/ #include "ReflectionEnvironmentCapture.h" #include "Misc/FeedbackContext.h" #include "RenderingThread.h" #include "RenderResource.h" #include "ShowFlags.h" #include "UnrealClient.h" #include "ShaderParameters.h" #include "RendererInterface.h" #include "RHIStaticStates.h" #include "SceneView.h" #include "LegacyScreenPercentageDriver.h" #include "Shader.h" #include "TextureResource.h" #include "StaticBoundShaderState.h" #include "SceneUtils.h" #include "SceneManagement.h" #include "Components/SkyLightComponent.h" #include "Components/ReflectionCaptureComponent.h" #include "Engine/TextureCube.h" #include "PostProcess/SceneRenderTargets.h" #include "GlobalShader.h" #include "SceneRenderTargetParameters.h" #include "SceneRendering.h" #include "ScenePrivate.h" #include "PostProcess/SceneFilterRendering.h" #include "PostProcess/PostProcessing.h" #include "ScreenRendering.h" #include "ReflectionEnvironment.h" #include "OneColorShader.h" #include "PipelineStateCache.h" #include "MobileReflectionEnvironmentCapture.h" #include "Engine/MapBuildDataRegistry.h" #include "Engine/Texture2D.h" #include "EngineUtils.h" #include "UObject/UObjectIterator.h" #include "EngineModule.h" #include "ClearQuad.h" #include "VolumetricCloudRendering.h" #include "VolumetricCloudProxy.h" #include "RenderGraphUtils.h" /** Near plane to use when capturing the scene. */ float GReflectionCaptureNearPlane = 5; constexpr int32 MinSupersampleCaptureFactor = 1; constexpr int32 MaxSupersampleCaptureFactor = 8; int32 GSupersampleCaptureFactor = 1; static FAutoConsoleVariableRef CVarGSupersampleCaptureFactor( TEXT("r.ReflectionCaptureSupersampleFactor"), GSupersampleCaptureFactor, TEXT("Super sample factor when rendering reflection captures.\n") TEXT("Default = 1, no super sampling\n") TEXT("Maximum clamped to 8."), ECVF_RenderThreadSafe ); /** * Mip map used by a Roughness of 0, counting down from the lowest resolution mip (MipCount - 1). * This has been tweaked along with ReflectionCaptureRoughnessMipScale to make good use of the resolution in each mip, especially the highest resolution mips. * This value is duplicated in ReflectionEnvironmentShared.usf! */ float ReflectionCaptureRoughestMip = 1; /** * Scales the log2 of Roughness when computing which mip to use for a given roughness. * Larger values make the higher resolution mips sharper. * This has been tweaked along with ReflectionCaptureRoughnessMipScale to make good use of the resolution in each mip, especially the highest resolution mips. * This value is duplicated in ReflectionEnvironmentShared.usf! */ float ReflectionCaptureRoughnessMipScale = 1.2f; int32 GDiffuseIrradianceCubemapSize = 32; static TAutoConsoleVariable CVarReflectionCaptureGPUArrayCopy( TEXT("r.ReflectionCaptureGPUArrayCopy"), 1, TEXT("Do a fast copy of the reflection capture array when resizing if possible. This avoids hitches on the rendering thread when the cubemap array needs to grow.\n") TEXT(" 0 is off, 1 is on (default)"), ECVF_ReadOnly); // Chaos addition static TAutoConsoleVariable CVarReflectionCaptureStaticSceneOnly( TEXT("r.chaos.ReflectionCaptureStaticSceneOnly"), 1, TEXT("") TEXT(" 0 is off, 1 is on (default)"), ECVF_ReadOnly); static int32 GFreeReflectionScratchAfterUse = 0; static FAutoConsoleVariableRef CVarFreeReflectionScratchAfterUse( TEXT("r.FreeReflectionScratchAfterUse"), GFreeReflectionScratchAfterUse, TEXT("Free reflection scratch render targets after use.")); TGlobalResource GReflectionScratchCubemaps; bool DoGPUArrayCopy() { return GRHISupportsResolveCubemapFaces && CVarReflectionCaptureGPUArrayCopy.GetValueOnAnyThread(); } void FReflectionScratchCubemaps::Allocate(FRHICommandList& RHICmdList, uint32 TargetSize) { if (GSupportsRenderTargetFormat_PF_FloatRGBA) { const int32 NumReflectionCaptureMips = FMath::CeilLogTwo(TargetSize) + 1; if (Color[0] && Color[0]->GetTargetableRHI()->GetNumMips() != NumReflectionCaptureMips) { Color[0].SafeRelease(); Color[1].SafeRelease(); } // Reflection targets are shared between both mobile and deferred shading paths. If we have already allocated for one and are now allocating for the other, // we can skip these targets. bool bSharedReflectionTargetsAllocated = Color[0] != nullptr; if (!bSharedReflectionTargetsAllocated) { // We write to these cubemap faces individually during filtering ETextureCreateFlags CubeTexFlags = TexCreate_TargetArraySlicesIndependently | TexCreate_DisableDCC // todo: temporary disable to avoid DCC copy failure ; { // Create scratch cubemaps for filtering passes FPooledRenderTargetDesc Desc2(FPooledRenderTargetDesc::CreateCubemapDesc(TargetSize, PF_FloatRGBA, FClearValueBinding(FLinearColor(0, 10000, 0, 0)), CubeTexFlags, TexCreate_RenderTargetable, false, 1, NumReflectionCaptureMips)); GRenderTargetPool.FindFreeElement(RHICmdList, Desc2, Color[0], TEXT("ReflectionColorScratchCubemap0"), ERenderTargetTransience::NonTransient); GRenderTargetPool.FindFreeElement(RHICmdList, Desc2, Color[1], TEXT("ReflectionColorScratchCubemap1"), ERenderTargetTransience::NonTransient); } extern int32 GDiffuseIrradianceCubemapSize; const int32 NumDiffuseIrradianceMips = FMath::CeilLogTwo(GDiffuseIrradianceCubemapSize) + 1; { FPooledRenderTargetDesc Desc2(FPooledRenderTargetDesc::CreateCubemapDesc(GDiffuseIrradianceCubemapSize, PF_FloatRGBA, FClearValueBinding(FLinearColor(0, 10000, 0, 0)), CubeTexFlags, TexCreate_RenderTargetable, false, 1, NumDiffuseIrradianceMips)); GRenderTargetPool.FindFreeElement(RHICmdList, Desc2, Irradiance[0], TEXT("DiffuseIrradianceScratchCubemap0"), ERenderTargetTransience::NonTransient); GRenderTargetPool.FindFreeElement(RHICmdList, Desc2, Irradiance[1], TEXT("DiffuseIrradianceScratchCubemap1"), ERenderTargetTransience::NonTransient); } { FPooledRenderTargetDesc Desc(FPooledRenderTargetDesc::Create2DDesc(FIntPoint(FSHVector3::MaxSHBasis, 1), PF_FloatRGBA, FClearValueBinding(FLinearColor(0, 10000, 0, 0)), TexCreate_None, TexCreate_RenderTargetable, false)); GRenderTargetPool.FindFreeElement(RHICmdList, Desc, SkySHIrradiance, TEXT("SkySHIrradiance"), ERenderTargetTransience::NonTransient); } } } } void FReflectionScratchCubemaps::Release() { for (int32 Index = 0; Index < UE_ARRAY_COUNT(Color); Index++) { Color[Index].SafeRelease(); } for (int32 Index = 0; Index < UE_ARRAY_COUNT(Irradiance); Index++) { Irradiance[Index].SafeRelease(); } SkySHIrradiance.SafeRelease(); } void FullyResolveReflectionScratchCubes(FRHICommandListImmediate& RHICmdList) { SCOPED_DRAW_EVENT(RHICmdList, FullyResolveReflectionScratchCubes); FRHITexture* Scratch0 = GReflectionScratchCubemaps.Color[0]->GetTargetableRHI(); FRHITexture* Scratch1 = GReflectionScratchCubemaps.Color[1]->GetTargetableRHI(); FResolveParams ResolveParams(FResolveRect(), CubeFace_PosX, -1, -1, -1); RHICmdList.CopyToResolveTarget(Scratch0, Scratch0, ResolveParams); RHICmdList.CopyToResolveTarget(Scratch1, Scratch1, ResolveParams); } IMPLEMENT_SHADER_TYPE(,FCubeFilterPS,TEXT("/Engine/Private/ReflectionEnvironmentShaders.usf"),TEXT("DownsamplePS"),SF_Pixel); IMPLEMENT_SHADER_TYPE(template<>,TCubeFilterPS<0>,TEXT("/Engine/Private/ReflectionEnvironmentShaders.usf"),TEXT("FilterPS"),SF_Pixel); IMPLEMENT_SHADER_TYPE(template<>,TCubeFilterPS<1>,TEXT("/Engine/Private/ReflectionEnvironmentShaders.usf"),TEXT("FilterPS"),SF_Pixel); /** Computes the average brightness of a 1x1 mip of a cubemap. */ class FComputeBrightnessPS : public FGlobalShader { DECLARE_SHADER_TYPE(FComputeBrightnessPS,Global) public: static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters) { return true; } static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment) { FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment); OutEnvironment.SetDefine(TEXT("COMPUTEBRIGHTNESS_PIXELSHADER"), 1); } FComputeBrightnessPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer) : FGlobalShader(Initializer) { ReflectionEnvironmentColorTexture.Bind(Initializer.ParameterMap,TEXT("ReflectionEnvironmentColorTexture")); ReflectionEnvironmentColorSampler.Bind(Initializer.ParameterMap,TEXT("ReflectionEnvironmentColorSampler")); NumCaptureArrayMips.Bind(Initializer.ParameterMap, TEXT("NumCaptureArrayMips")); } FComputeBrightnessPS() { } void SetParameters(FRHICommandList& RHICmdList, int32 TargetSize, FSceneRenderTargetItem& Cubemap) { const int32 EffectiveTopMipSize = TargetSize; const int32 NumMips = FMath::CeilLogTwo(EffectiveTopMipSize) + 1; // Read from the smallest mip that was downsampled to if (Cubemap.IsValid()) { SetTextureParameter( RHICmdList, RHICmdList.GetBoundPixelShader(), ReflectionEnvironmentColorTexture, ReflectionEnvironmentColorSampler, TStaticSamplerState::GetRHI(), Cubemap.ShaderResourceTexture); } SetShaderValue(RHICmdList, RHICmdList.GetBoundPixelShader(), NumCaptureArrayMips, FMath::CeilLogTwo(TargetSize) + 1); } private: LAYOUT_FIELD(FShaderResourceParameter, ReflectionEnvironmentColorTexture); LAYOUT_FIELD(FShaderResourceParameter, ReflectionEnvironmentColorSampler); LAYOUT_FIELD(FShaderParameter, NumCaptureArrayMips); }; IMPLEMENT_SHADER_TYPE(,FComputeBrightnessPS,TEXT("/Engine/Private/ReflectionEnvironmentShaders.usf"),TEXT("ComputeBrightnessMain"),SF_Pixel); void CreateCubeMips( FRHICommandListImmediate& RHICmdList, ERHIFeatureLevel::Type FeatureLevel, int32 NumMips, FSceneRenderTargetItem& Cubemap ) { SCOPED_DRAW_EVENT(RHICmdList, CreateCubeMips); FRHITexture* CubeRef = Cubemap.TargetableTexture.GetReference(); auto* ShaderMap = GetGlobalShaderMap(FeatureLevel); TArray>> SRVs; SRVs.Empty(NumMips); for (int32 MipIndex = 0; MipIndex < NumMips; MipIndex++) { FRHITextureSRVCreateInfo SRVDesc; SRVDesc.MipLevel = MipIndex; SRVs.Emplace(SRVDesc, RHICreateShaderResourceView(Cubemap.ShaderResourceTexture, SRVDesc)); } FGraphicsPipelineStateInitializer GraphicsPSOInit; GraphicsPSOInit.RasterizerState = TStaticRasterizerState::GetRHI(); GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState::GetRHI(); GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI(); // Downsample all the mips, each one reads from the mip above it for (int32 MipIndex = 1; MipIndex < NumMips; MipIndex++) { // For the first iteration, we don't know what the previous state // of the source mip was, but we *do* for all the other iterations... ERHIAccess Previous = MipIndex == 1 ? ERHIAccess::Unknown : ERHIAccess::RTV; FRHITransitionInfo Transitions[] = { // Make the source mip readable (SRVGraphics) FRHITransitionInfo(CubeRef, Previous, ERHIAccess::SRVGraphics, EResourceTransitionFlags::None, uint32(MipIndex - 1)), // Make the destination mip writable (RTV) FRHITransitionInfo(CubeRef, ERHIAccess::Unknown, ERHIAccess::RTV, EResourceTransitionFlags::None, uint32(MipIndex)) }; RHICmdList.Transition(Transitions); const int32 MipSize = 1 << (NumMips - MipIndex - 1); SCOPED_DRAW_EVENT(RHICmdList, CreateCubeMipsPerFace); for (int32 CubeFace = 0; CubeFace < CubeFace_MAX; CubeFace++) { FRHIRenderPassInfo RPInfo(Cubemap.TargetableTexture, ERenderTargetActions::DontLoad_Store, nullptr, MipIndex, CubeFace); RHICmdList.BeginRenderPass(RPInfo, TEXT("CreateCubeMips")); RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit); const FIntRect ViewRect(0, 0, MipSize, MipSize); RHICmdList.SetViewport(0.0f, 0.0f, 0.0f, (float)MipSize, (float)MipSize, 1.0f); TShaderMapRef VertexShader(ShaderMap); TShaderMapRef PixelShader(ShaderMap); GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GFilterVertexDeclaration.VertexDeclarationRHI; GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader(); GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader(); GraphicsPSOInit.PrimitiveType = PT_TriangleList; SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, 0); { FRHIPixelShader* ShaderRHI = PixelShader.GetPixelShader(); SetShaderValue(RHICmdList, ShaderRHI, PixelShader->CubeFace, CubeFace); SetShaderValue(RHICmdList, ShaderRHI, PixelShader->MipIndex, MipIndex); SetShaderValue(RHICmdList, ShaderRHI, PixelShader->NumMips, NumMips); check(SRVs.IsValidIndex(MipIndex - 1) && SRVs[MipIndex - 1].Key.MipLevel == MipIndex - 1); SetSRVParameter(RHICmdList, ShaderRHI, PixelShader->SourceCubemapTexture, SRVs[MipIndex - 1].Value); SetSamplerParameter(RHICmdList, ShaderRHI, PixelShader->SourceCubemapSampler, TStaticSamplerState::GetRHI()); } DrawRectangle( RHICmdList, ViewRect.Min.X, ViewRect.Min.Y, ViewRect.Width(), ViewRect.Height(), ViewRect.Min.X, ViewRect.Min.Y, ViewRect.Width(), ViewRect.Height(), FIntPoint(ViewRect.Width(), ViewRect.Height()), FIntPoint(MipSize, MipSize), VertexShader); RHICmdList.EndRenderPass(); } } RHICmdList.Transition(FRHITransitionInfo(CubeRef, ERHIAccess::Unknown, ERHIAccess::SRVMask)); SRVs.Empty(); } /** Computes the average brightness of the given reflection capture and stores it in the scene. */ float ComputeSingleAverageBrightnessFromCubemap(FRHICommandListImmediate& RHICmdList, ERHIFeatureLevel::Type FeatureLevel, int32 TargetSize, FSceneRenderTargetItem& Cubemap) { SCOPED_DRAW_EVENT(RHICmdList, ComputeSingleAverageBrightnessFromCubemap); TRefCountPtr ReflectionBrightnessTarget; FPooledRenderTargetDesc Desc(FPooledRenderTargetDesc::Create2DDesc(FIntPoint(1, 1), PF_FloatRGBA, FClearValueBinding::None, TexCreate_None, TexCreate_RenderTargetable, false)); GRenderTargetPool.FindFreeElement(RHICmdList, Desc, ReflectionBrightnessTarget, TEXT("ReflectionBrightness")); FRHITexture* BrightnessTarget = ReflectionBrightnessTarget->GetTargetableRHI(); FRHIRenderPassInfo RPInfo(BrightnessTarget, ERenderTargetActions::Load_Store); TransitionRenderPassTargets(RHICmdList, RPInfo); RHICmdList.BeginRenderPass(RPInfo, TEXT("ReflectionBrightness")); { FGraphicsPipelineStateInitializer GraphicsPSOInit; RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit); GraphicsPSOInit.RasterizerState = TStaticRasterizerState::GetRHI(); GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState::GetRHI(); GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI(); auto ShaderMap = GetGlobalShaderMap(FeatureLevel); TShaderMapRef VertexShader(ShaderMap); TShaderMapRef PixelShader(ShaderMap); GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GFilterVertexDeclaration.VertexDeclarationRHI; GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader(); GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader(); GraphicsPSOInit.PrimitiveType = PT_TriangleList; SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, 0); PixelShader->SetParameters(RHICmdList, TargetSize, Cubemap); DrawRectangle( RHICmdList, 0, 0, 1, 1, 0, 0, 1, 1, FIntPoint(1, 1), FIntPoint(1, 1), VertexShader); } RHICmdList.EndRenderPass(); RHICmdList.CopyToResolveTarget(BrightnessTarget, BrightnessTarget, FResolveParams()); FSceneRenderTargetItem& EffectiveRT = ReflectionBrightnessTarget->GetRenderTargetItem(); check(EffectiveRT.ShaderResourceTexture->GetFormat() == PF_FloatRGBA); TArray SurfaceData; RHICmdList.ReadSurfaceFloatData(EffectiveRT.ShaderResourceTexture, FIntRect(0, 0, 1, 1), SurfaceData, CubeFace_PosX, 0, 0); // Shader outputs luminance to R float AverageBrightness = SurfaceData[0].R.GetFloat(); return AverageBrightness; } void ComputeAverageBrightness(FRHICommandListImmediate& RHICmdList, ERHIFeatureLevel::Type FeatureLevel, int32 CubmapSize, float& OutAverageBrightness) { SCOPED_DRAW_EVENT(RHICmdList, ComputeAverageBrightness); const int32 EffectiveTopMipSize = CubmapSize; const int32 NumMips = FMath::CeilLogTwo(EffectiveTopMipSize) + 1; // necessary to resolve the clears which touched all the mips. scene rendering only resolves mip 0. FullyResolveReflectionScratchCubes(RHICmdList); FSceneRenderTargetItem& DownSampledCube = GReflectionScratchCubemaps.Color[0]->GetRenderTargetItem(); CreateCubeMips( RHICmdList, FeatureLevel, NumMips, DownSampledCube ); OutAverageBrightness = ComputeSingleAverageBrightnessFromCubemap(RHICmdList, FeatureLevel, CubmapSize, DownSampledCube); } void FilterCubeMap(FRHICommandListImmediate& RHICmdList, ERHIFeatureLevel::Type FeatureLevel, int32 NumMips, FSceneRenderTargetItem& DownSampledCube, FSceneRenderTargetItem& FilteredCube) { SCOPED_DRAW_EVENT(RHICmdList, FilterCubeMap); auto ShaderMap = GetGlobalShaderMap(FeatureLevel); FGraphicsPipelineStateInitializer GraphicsPSOInit; GraphicsPSOInit.RasterizerState = TStaticRasterizerState::GetRHI(); GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState::GetRHI(); GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI(); RHICmdList.Transition(FRHITransitionInfo(FilteredCube.TargetableTexture, ERHIAccess::Unknown, ERHIAccess::RTV)); // Filter all the mips for (int32 MipIndex = 0; MipIndex < NumMips; MipIndex++) { const int32 MipSize = 1 << (NumMips - MipIndex - 1); for (int32 CubeFace = 0; CubeFace < CubeFace_MAX; CubeFace++) { FRHIRenderPassInfo RPInfo(FilteredCube.TargetableTexture, ERenderTargetActions::DontLoad_Store, nullptr, MipIndex, CubeFace); RHICmdList.Transition(FRHITransitionInfo(FilteredCube.TargetableTexture, ERHIAccess::Unknown, ERHIAccess::RTV)); RHICmdList.BeginRenderPass(RPInfo, TEXT("FilterMips")); RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit); const FIntRect ViewRect(0, 0, MipSize, MipSize); RHICmdList.SetViewport(0, 0, 0.0f, MipSize, MipSize, 1.0f); //TShaderMapRef> CaptureCubemapArrayPixelShader(GetGlobalShaderMap(FeatureLevel)); TShaderMapRef VertexShader(GetGlobalShaderMap(FeatureLevel)); TShaderMapRef> PixelShader(GetGlobalShaderMap(FeatureLevel)); GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader(); GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader(); GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GFilterVertexDeclaration.VertexDeclarationRHI; GraphicsPSOInit.PrimitiveType = PT_TriangleList; SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, 0); { FRHIPixelShader* ShaderRHI = PixelShader.GetPixelShader(); SetShaderValue(RHICmdList, ShaderRHI, PixelShader->CubeFace, CubeFace); SetShaderValue(RHICmdList, ShaderRHI, PixelShader->MipIndex, MipIndex); SetShaderValue(RHICmdList, ShaderRHI, PixelShader->NumMips, NumMips); SetTextureParameter( RHICmdList, ShaderRHI, PixelShader->SourceCubemapTexture, PixelShader->SourceCubemapSampler, TStaticSamplerState::GetRHI(), DownSampledCube.ShaderResourceTexture); } DrawRectangle( RHICmdList, ViewRect.Min.X, ViewRect.Min.Y, ViewRect.Width(), ViewRect.Height(), ViewRect.Min.X, ViewRect.Min.Y, ViewRect.Width(), ViewRect.Height(), FIntPoint(ViewRect.Width(), ViewRect.Height()), FIntPoint(MipSize, MipSize), VertexShader); RHICmdList.EndRenderPass(); } } } /** Generates mips for glossiness and filters the cubemap for a given reflection. */ void FilterReflectionEnvironment(FRHICommandListImmediate& RHICmdList, ERHIFeatureLevel::Type FeatureLevel, int32 CubmapSize, FSHVectorRGB3* OutIrradianceEnvironmentMap) { SCOPED_DRAW_EVENT(RHICmdList, FilterReflectionEnvironment); const int32 EffectiveTopMipSize = CubmapSize; const int32 NumMips = FMath::CeilLogTwo(EffectiveTopMipSize) + 1; FSceneRenderTargetItem& EffectiveColorRT = GReflectionScratchCubemaps.Color[0]->GetRenderTargetItem(); FGraphicsPipelineStateInitializer GraphicsPSOInit; GraphicsPSOInit.RasterizerState = TStaticRasterizerState::GetRHI(); GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState::GetRHI(); GraphicsPSOInit.BlendState = TStaticBlendState::GetRHI(); RHICmdList.Transition(FRHITransitionInfo(EffectiveColorRT.TargetableTexture, ERHIAccess::Unknown, ERHIAccess::RTV)); // Premultiply alpha in-place using alpha blending for (uint32 CubeFace = 0; CubeFace < CubeFace_MAX; CubeFace++) { FRHIRenderPassInfo RPInfo(EffectiveColorRT.TargetableTexture, ERenderTargetActions::Load_Store, nullptr, 0, CubeFace); RHICmdList.BeginRenderPass(RPInfo, TEXT("FilterReflectionEnvironmentRP")); RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit); const FIntPoint SourceDimensions(CubmapSize, CubmapSize); const FIntRect ViewRect(0, 0, EffectiveTopMipSize, EffectiveTopMipSize); RHICmdList.SetViewport(0, 0, 0.0f, EffectiveTopMipSize, EffectiveTopMipSize, 1.0f); TShaderMapRef VertexShader(GetGlobalShaderMap(FeatureLevel)); TShaderMapRef PixelShader(GetGlobalShaderMap(FeatureLevel)); GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GFilterVertexDeclaration.VertexDeclarationRHI; GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader(); GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader(); GraphicsPSOInit.PrimitiveType = PT_TriangleList; SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit, 0); FLinearColor UnusedColors[1] = { FLinearColor::Black }; PixelShader->SetColors(RHICmdList, UnusedColors, UE_ARRAY_COUNT(UnusedColors)); DrawRectangle( RHICmdList, ViewRect.Min.X, ViewRect.Min.Y, ViewRect.Width(), ViewRect.Height(), 0, 0, SourceDimensions.X, SourceDimensions.Y, FIntPoint(ViewRect.Width(), ViewRect.Height()), SourceDimensions, VertexShader); RHICmdList.EndRenderPass(); } RHICmdList.Transition(FRHITransitionInfo(EffectiveColorRT.TargetableTexture, ERHIAccess::Unknown, ERHIAccess::SRVMask)); auto ShaderMap = GetGlobalShaderMap(FeatureLevel); FSceneRenderTargetItem& DownSampledCube = GReflectionScratchCubemaps.Color[0]->GetRenderTargetItem(); FSceneRenderTargetItem& FilteredCube = GReflectionScratchCubemaps.Color[1]->GetRenderTargetItem(); CreateCubeMips( RHICmdList, FeatureLevel, NumMips, DownSampledCube ); if (OutIrradianceEnvironmentMap) { SCOPED_DRAW_EVENT(RHICmdList, ComputeDiffuseIrradiance); const int32 NumDiffuseMips = FMath::CeilLogTwo( GDiffuseIrradianceCubemapSize ) + 1; const int32 DiffuseConvolutionSourceMip = FMath::Max(0, NumMips - NumDiffuseMips); ComputeDiffuseIrradiance(RHICmdList, FeatureLevel, DownSampledCube.ShaderResourceTexture, DiffuseConvolutionSourceMip, OutIrradianceEnvironmentMap); } FilterCubeMap(RHICmdList, FeatureLevel, NumMips, DownSampledCube, FilteredCube); RHICmdList.CopyToResolveTarget(FilteredCube.TargetableTexture, FilteredCube.ShaderResourceTexture, FResolveParams()); } class FCopyToCubeFaceShader : public FGlobalShader { public: static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters) { return true; } FCopyToCubeFaceShader() = default; FCopyToCubeFaceShader(const CompiledShaderInitializerType& Initializer) : FGlobalShader(Initializer) {} }; /** Vertex shader used when writing to a cubemap. */ class FCopyToCubeFaceVS : public FCopyToCubeFaceShader { public: DECLARE_GLOBAL_SHADER(FCopyToCubeFaceVS); FCopyToCubeFaceVS() = default; FCopyToCubeFaceVS(const CompiledShaderInitializerType & Initializer) : FCopyToCubeFaceShader(Initializer) {} }; IMPLEMENT_GLOBAL_SHADER(FCopyToCubeFaceVS, "/Engine/Private/ReflectionEnvironmentShaders.usf", "CopyToCubeFaceVS", SF_Vertex); /** Pixel shader used when copying scene color from a scene render into a face of a reflection capture cubemap. */ class FCopySceneColorToCubeFacePS : public FCopyToCubeFaceShader { public: DECLARE_GLOBAL_SHADER(FCopySceneColorToCubeFacePS); SHADER_USE_PARAMETER_STRUCT(FCopySceneColorToCubeFacePS, FCopyToCubeFaceShader); static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment) { FCopyToCubeFaceShader::ModifyCompilationEnvironment(Parameters, OutEnvironment); if (IsMobilePlatform(Parameters.Platform)) { // SceneDepth is memoryless on mobile OutEnvironment.SetDefine(TEXT("SCENE_TEXTURES_DISABLED"), 1u); } } BEGIN_SHADER_PARAMETER_STRUCT(FParameters, ) SHADER_PARAMETER_STRUCT_REF(FViewUniformShaderParameters, View) SHADER_PARAMETER_RDG_TEXTURE(Texture2D, SceneColorTexture) SHADER_PARAMETER_SAMPLER(SamplerState, SceneColorSampler) SHADER_PARAMETER_RDG_TEXTURE(Texture2D, SceneDepthTexture) SHADER_PARAMETER_SAMPLER(SamplerState, SceneDepthSampler) SHADER_PARAMETER(FVector4f, SkyLightCaptureParameters) SHADER_PARAMETER(FVector4f, LowerHemisphereColor) RENDER_TARGET_BINDING_SLOTS() END_SHADER_PARAMETER_STRUCT() }; IMPLEMENT_GLOBAL_SHADER(FCopySceneColorToCubeFacePS, "/Engine/Private/ReflectionEnvironmentShaders.usf", "CopySceneColorToCubeFaceColorPS", SF_Pixel); /** Pixel shader used when copying a cubemap into a face of a reflection capture cubemap. */ class FCopyCubemapToCubeFacePS : public FCopyToCubeFaceShader { public: DECLARE_GLOBAL_SHADER(FCopyCubemapToCubeFacePS); SHADER_USE_PARAMETER_STRUCT(FCopyCubemapToCubeFacePS, FCopyToCubeFaceShader); BEGIN_SHADER_PARAMETER_STRUCT(FParameters, ) SHADER_PARAMETER_TEXTURE(TextureCube, SourceCubemapTexture) SHADER_PARAMETER_SAMPLER(SamplerState, SourceCubemapSampler) SHADER_PARAMETER(FVector4f, SkyLightCaptureParameters) SHADER_PARAMETER(int32, CubeFace) SHADER_PARAMETER(FVector4f, LowerHemisphereColor) SHADER_PARAMETER(FVector2f, SinCosSourceCubemapRotation) RENDER_TARGET_BINDING_SLOTS() END_SHADER_PARAMETER_STRUCT() }; IMPLEMENT_GLOBAL_SHADER(FCopyCubemapToCubeFacePS, "/Engine/Private/ReflectionEnvironmentShaders.usf", "CopyCubemapToCubeFaceColorPS", SF_Pixel); int32 FindOrAllocateCubemapIndex(FScene* Scene, const UReflectionCaptureComponent* Component) { int32 CubemapIndex = -1; // Try to find an existing capture index for this component const FCaptureComponentSceneState* CaptureSceneStatePtr = Scene->ReflectionSceneData.AllocatedReflectionCaptureState.Find(Component); if (CaptureSceneStatePtr) { CubemapIndex = CaptureSceneStatePtr->CubemapIndex; } else { // Reuse a freed index if possible CubemapIndex = Scene->ReflectionSceneData.CubemapArraySlotsUsed.FindAndSetFirstZeroBit(); if (CubemapIndex == INDEX_NONE) { // If we didn't find a free index, allocate a new one from the CubemapArraySlotsUsed bitfield CubemapIndex = Scene->ReflectionSceneData.CubemapArraySlotsUsed.Num(); Scene->ReflectionSceneData.CubemapArraySlotsUsed.Add(true); } Scene->ReflectionSceneData.AllocatedReflectionCaptureState.Add(Component, FCaptureComponentSceneState(CubemapIndex)); Scene->ReflectionSceneData.AllocatedReflectionCaptureStateHasChanged = true; check(CubemapIndex < GMaxNumReflectionCaptures); } check(CubemapIndex >= 0); return CubemapIndex; } void ClearScratchCubemaps(FRHICommandListImmediate& RHICmdList, int32 TargetSize) { SCOPED_DRAW_EVENT(RHICmdList, ClearScratchCubemaps); GReflectionScratchCubemaps.Allocate(RHICmdList, TargetSize); FMemMark Mark(FMemStack::Get()); FRDGBuilder GraphBuilder(RHICmdList); for (int32 RenderTargetIndex = 0; RenderTargetIndex < 2; ++RenderTargetIndex) { FRDGTextureRef OutputTexture = GraphBuilder.RegisterExternalTexture(GReflectionScratchCubemaps.Color[RenderTargetIndex], TEXT("OutputCubemap")); RDG_EVENT_SCOPE(GraphBuilder, "ClearScratchCubemapsRT%d", RenderTargetIndex); const int32 NumMips = OutputTexture->Desc.NumMips; for (int32 MipIndex = 0; MipIndex < NumMips; MipIndex++) { for (int32 CubeFace = 0; CubeFace < CubeFace_MAX; CubeFace++) { FRenderTargetParameters* PassParameters = GraphBuilder.AllocParameters(); PassParameters->RenderTargets[0] = FRenderTargetBinding(OutputTexture, ERenderTargetLoadAction::EClear, MipIndex, CubeFace); GraphBuilder.AddPass( RDG_EVENT_NAME("ClearCubeFace(Mip: %d, Face: %d)", MipIndex, CubeFace), PassParameters, ERDGPassFlags::Raster, [](FRHICommandList&) {}); } } } GraphBuilder.Execute(); } /** Captures the scene for a reflection capture by rendering the scene multiple times and copying into a cubemap texture. */ void CaptureSceneToScratchCubemap(FRHICommandListImmediate& RHICmdList, FSceneRenderer* SceneRenderer, ECubeFace CubeFace, int32 CubemapSize, bool bCapturingForSkyLight, bool bLowerHemisphereIsBlack, const FLinearColor& LowerHemisphereColor, bool bCapturingForMobile) { SceneRenderer->RenderThreadBegin(RHICmdList); // update any resources that needed a deferred update FDeferredUpdateResource::UpdateResources(RHICmdList); FMaterialRenderProxy::UpdateDeferredCachedUniformExpressions(); const ERHIFeatureLevel::Type FeatureLevel = SceneRenderer->FeatureLevel; { SCOPED_DRAW_EVENT(RHICmdList, CubeMapCapture); FRDGBuilder GraphBuilder(RHICmdList, RDG_EVENT_NAME("CubeMapCapture"), FSceneRenderer::GetRDGParalelExecuteFlags(FeatureLevel)); // Render the scene normally for one face of the cubemap SceneRenderer->Render(GraphBuilder); AddPass(GraphBuilder, RDG_EVENT_NAME("FlushGPU"), [](FRHICommandListImmediate& InRHICmdList) { QUICK_SCOPE_CYCLE_COUNTER(STAT_CaptureSceneToScratchCubemap_Flush); FRHICommandListExecutor::GetImmediateCommandList().ImmediateFlush(EImmediateFlushType::FlushRHIThread); // some platforms may not be able to keep enqueueing commands like crazy, this will // allow them to restart their command buffers InRHICmdList.SubmitCommandsAndFlushGPU(); }); GReflectionScratchCubemaps.Allocate(RHICmdList, CubemapSize); const FViewInfo& View = SceneRenderer->Views[0]; FRDGTextureRef OutputTexture = GraphBuilder.RegisterExternalTexture(GReflectionScratchCubemaps.Color[0]); auto* PassParameters = GraphBuilder.AllocParameters(); PassParameters->RenderTargets[0] = FRenderTargetBinding(OutputTexture, ERenderTargetLoadAction::ENoAction, 0, CubeFace); PassParameters->LowerHemisphereColor = LowerHemisphereColor; { FVector4f SkyLightParametersValue(ForceInitToZero); FScene* Scene = SceneRenderer->Scene; if (bCapturingForSkyLight) { // When capturing reflection captures, support forcing all low hemisphere lighting to be black SkyLightParametersValue = FVector4f(0, 0, bLowerHemisphereIsBlack ? 1.0f : 0.0f, 0); } else if (!bCapturingForMobile && Scene->SkyLight && !Scene->SkyLight->bHasStaticLighting) { // Mobile renderer can't blend reflections with a sky at runtime, so we dont use this path when capturing for a mobile renderer // When capturing reflection captures and there's a stationary sky light, mask out any pixels whose depth classify it as part of the sky // This will allow changing the stationary sky light at runtime SkyLightParametersValue = FVector4f(1, Scene->SkyLight->SkyDistanceThreshold, 0, 0); } else { // When capturing reflection captures and there's no sky light, or only a static sky light, capture all depth ranges SkyLightParametersValue = FVector4f(2, 0, 0, 0); } PassParameters->SkyLightCaptureParameters = SkyLightParametersValue; } const FMinimalSceneTextures& SceneTextures = FSceneTextures::Get(GraphBuilder); PassParameters->View = View.ViewUniformBuffer; PassParameters->SceneColorSampler = TStaticSamplerState::GetRHI(); PassParameters->SceneColorTexture = SceneTextures.Color.Target; PassParameters->SceneDepthSampler = TStaticSamplerState::GetRHI(); PassParameters->SceneDepthTexture = SceneTextures.Depth.Target; const int32 EffectiveSize = CubemapSize; const FIntPoint SceneTextureExtent = SceneTextures.Config.Extent; GraphBuilder.AddPass( RDG_EVENT_NAME("CopySceneToCubeFace"), PassParameters, ERDGPassFlags::Raster, [EffectiveSize, SceneTextureExtent, FeatureLevel, PassParameters](FRHICommandList& InRHICmdList) { const FIntRect ViewRect(0, 0, EffectiveSize, EffectiveSize); InRHICmdList.SetViewport(0.0f, 0.0f, 0.0f, (float)EffectiveSize, (float)EffectiveSize, 1.0f); FGraphicsPipelineStateInitializer GraphicsPSOInit; InRHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit); GraphicsPSOInit.RasterizerState = TStaticRasterizerState::GetRHI(); GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState::GetRHI(); GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI(); TShaderMapRef VertexShader(GetGlobalShaderMap(FeatureLevel)); TShaderMapRef PixelShader(GetGlobalShaderMap(FeatureLevel)); GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GFilterVertexDeclaration.VertexDeclarationRHI; GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader(); GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader(); GraphicsPSOInit.PrimitiveType = PT_TriangleList; SetGraphicsPipelineState(InRHICmdList, GraphicsPSOInit, 0); SetShaderParameters(InRHICmdList, PixelShader, PixelShader.GetPixelShader(), *PassParameters); const int32 SupersampleCaptureFactor = FMath::Clamp(GSupersampleCaptureFactor, MinSupersampleCaptureFactor, MaxSupersampleCaptureFactor); DrawRectangle( InRHICmdList, ViewRect.Min.X, ViewRect.Min.Y, ViewRect.Width(), ViewRect.Height(), ViewRect.Min.X, ViewRect.Min.Y, ViewRect.Width() * SupersampleCaptureFactor, ViewRect.Height() * SupersampleCaptureFactor, FIntPoint(ViewRect.Width(), ViewRect.Height()), SceneTextureExtent, VertexShader); }); GraphBuilder.Execute(); } SceneRenderer->RenderThreadEnd(RHICmdList); } void CopyCubemapToScratchCubemap(FRHICommandListImmediate& RHICmdList, ERHIFeatureLevel::Type FeatureLevel, UTextureCube* SourceCubemap, int32 CubemapSize, bool bIsSkyLight, bool bLowerHemisphereIsBlack, float SourceCubemapRotation, const FLinearColor& LowerHemisphereColorValue) { SCOPED_DRAW_EVENT(RHICmdList, CopyCubemapToScratchCubemap); check(SourceCubemap); const FTexture* SourceCubemapResource = SourceCubemap->GetResource(); if (SourceCubemapResource == nullptr) { UE_LOG(LogEngine, Warning, TEXT("Unable to copy from cubemap %s, it's RHI resource is null"), *SourceCubemap->GetPathName()); return; } FMemMark Mark(FMemStack::Get()); FRDGBuilder GraphBuilder(RHICmdList); FRDGTextureRef OutputTexture = GraphBuilder.RegisterExternalTexture(GReflectionScratchCubemaps.Color[0], TEXT("Color")); for (uint32 CubeFace = 0; CubeFace < CubeFace_MAX; CubeFace++) { auto* PassParameters = GraphBuilder.AllocParameters(); PassParameters->RenderTargets[0] = FRenderTargetBinding(OutputTexture, ERenderTargetLoadAction::ENoAction, 0, CubeFace); PassParameters->LowerHemisphereColor = LowerHemisphereColorValue; PassParameters->SkyLightCaptureParameters = FVector(bIsSkyLight ? 1.0f : 0.0f, 0.0f, bLowerHemisphereIsBlack ? 1.0f : 0.0f); PassParameters->SourceCubemapSampler = TStaticSamplerState::GetRHI(); PassParameters->SourceCubemapTexture = SourceCubemapResource->TextureRHI; PassParameters->SinCosSourceCubemapRotation = FVector2D(FMath::Sin(SourceCubemapRotation), FMath::Cos(SourceCubemapRotation)); PassParameters->CubeFace = CubeFace; const int32 EffectiveSize = CubemapSize; GraphBuilder.AddPass( RDG_EVENT_NAME("CopyCubemapToCubeFace"), PassParameters, ERDGPassFlags::Raster, [EffectiveSize, SourceCubemapResource, PassParameters, FeatureLevel](FRHICommandList& InRHICmdList) { const FIntPoint SourceDimensions(SourceCubemapResource->GetSizeX(), SourceCubemapResource->GetSizeY()); const FIntRect ViewRect(0, 0, EffectiveSize, EffectiveSize); InRHICmdList.SetViewport(0.0f, 0.0f, 0.0f, (float)EffectiveSize, (float)EffectiveSize, 1.0f); FGraphicsPipelineStateInitializer GraphicsPSOInit; InRHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit); GraphicsPSOInit.RasterizerState = TStaticRasterizerState::GetRHI(); GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState::GetRHI(); GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI(); TShaderMapRef VertexShader(GetGlobalShaderMap(FeatureLevel)); TShaderMapRef PixelShader(GetGlobalShaderMap(FeatureLevel)); GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GFilterVertexDeclaration.VertexDeclarationRHI; GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader(); GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader(); GraphicsPSOInit.PrimitiveType = PT_TriangleList; SetGraphicsPipelineState(InRHICmdList, GraphicsPSOInit, 0); SetShaderParameters(InRHICmdList, PixelShader, PixelShader.GetPixelShader(), *PassParameters); DrawRectangle( InRHICmdList, ViewRect.Min.X, ViewRect.Min.Y, ViewRect.Width(), ViewRect.Height(), 0, 0, SourceDimensions.X, SourceDimensions.Y, FIntPoint(ViewRect.Width(), ViewRect.Height()), SourceDimensions, VertexShader); }); } GraphBuilder.Execute(); } const int32 MinCapturesForSlowTask = 20; void BeginReflectionCaptureSlowTask(int32 NumCaptures, const TCHAR* CaptureReason) { if (NumCaptures > MinCapturesForSlowTask) { FText Status; if (CaptureReason) { Status = FText::Format(NSLOCTEXT("Engine", "UpdateReflectionCapturesForX", "Building reflection captures for {0}"), FText::FromString(FString(CaptureReason))); } else { Status = FText(NSLOCTEXT("Engine", "UpdateReflectionCaptures", "Building reflection captures...")); } GWarn->BeginSlowTask(Status, true); GWarn->StatusUpdate(0, NumCaptures, Status); } } void UpdateReflectionCaptureSlowTask(int32 CaptureIndex, int32 NumCaptures) { const int32 UpdateDivisor = FMath::Max(NumCaptures / 5, 1); if (NumCaptures > MinCapturesForSlowTask && (CaptureIndex % UpdateDivisor) == 0) { GWarn->UpdateProgress(CaptureIndex, NumCaptures); } } void EndReflectionCaptureSlowTask(int32 NumCaptures) { if (NumCaptures > MinCapturesForSlowTask) { GWarn->EndSlowTask(); } } int32 GetReflectionCaptureSizeForArrayCount(int32 InRequestedCaptureSize, int32 InRequestedMaxCubeMaps) { int32 OutCaptureSize = InRequestedCaptureSize; #if WITH_EDITOR if(GIsEditor) { FTextureMemoryStats TextureMemStats; RHIGetTextureMemoryStats(TextureMemStats); SIZE_T TexMemRequired = CalcTextureSize(OutCaptureSize, OutCaptureSize, PF_FloatRGBA, FMath::CeilLogTwo(OutCaptureSize) + 1) * CubeFace_MAX * InRequestedMaxCubeMaps; // Assumption: Texture arrays prefer to be contiguous in memory but not always // Single large cube array allocations can fail on low end systems even if we try to fit it in total avail video and/or avail system memory // Attempt to limit the resource size to within percentage (3/4) of total video memory to give consistant stable results const SIZE_T MaxResourceVideoMemoryFootprint = ((SIZE_T)TextureMemStats.DedicatedVideoMemory * (SIZE_T)3) / (SIZE_T)4; // Bottom out at 128 as that is the default for CVarReflectionCaptureSize while(TexMemRequired > MaxResourceVideoMemoryFootprint && OutCaptureSize > 128) { OutCaptureSize = FMath::RoundUpToPowerOfTwo(OutCaptureSize) >> 1; TexMemRequired = CalcTextureSize(OutCaptureSize, OutCaptureSize, PF_FloatRGBA, FMath::CeilLogTwo(OutCaptureSize) + 1) * CubeFace_MAX * InRequestedMaxCubeMaps; } if(OutCaptureSize != InRequestedCaptureSize) { UE_LOG(LogEngine, Error, TEXT("Requested reflection capture cube size of %d with %d elements results in too large a resource for host machine, limiting reflection capture cube size to %d"), InRequestedCaptureSize, InRequestedMaxCubeMaps, OutCaptureSize); } } #endif // WITH_EDITOR return OutCaptureSize; } /** * Allocates reflection captures in the scene's reflection cubemap array and updates them by recapturing the scene. * Existing captures will only be uploaded. Must be called from the game thread. */ void FScene::AllocateReflectionCaptures(const TArray& NewCaptures, const TCHAR* CaptureReason, bool bVerifyOnlyCapturing, bool bCapturingForMobile) { if (NewCaptures.Num() > 0) { if (SupportsTextureCubeArray(GetFeatureLevel())) { int32_t PlatformMaxNumReflectionCaptures = FMath::Min(FMath::FloorToInt(GMaxTextureArrayLayers / 6.0f), GMaxNumReflectionCaptures); for (int32 CaptureIndex = 0; CaptureIndex < NewCaptures.Num(); CaptureIndex++) { bool bAlreadyExists = false; // Try to find an existing allocation for (TSparseArray::TIterator It(ReflectionSceneData.AllocatedReflectionCapturesGameThread); It; ++It) { UReflectionCaptureComponent* OtherComponent = *It; if (OtherComponent == NewCaptures[CaptureIndex]) { bAlreadyExists = true; } } // Add the capture to the allocated list if (!bAlreadyExists && ReflectionSceneData.AllocatedReflectionCapturesGameThread.Num() < PlatformMaxNumReflectionCaptures) { ReflectionSceneData.AllocatedReflectionCapturesGameThread.Add(NewCaptures[CaptureIndex]); } } // Request the exact amount needed by default int32 DesiredMaxCubemaps = ReflectionSceneData.AllocatedReflectionCapturesGameThread.Num(); const float MaxCubemapsRoundUpBase = 1.5f; // If this is not the first time the scene has allocated the cubemap array, include slack to reduce reallocations if (ReflectionSceneData.MaxAllocatedReflectionCubemapsGameThread > 0) { float Exponent = FMath::LogX(MaxCubemapsRoundUpBase, ReflectionSceneData.AllocatedReflectionCapturesGameThread.Num()); // Round up to the next integer exponent to provide stability and reduce reallocations DesiredMaxCubemaps = FMath::Pow(MaxCubemapsRoundUpBase, FMath::TruncToInt(Exponent) + 1); } DesiredMaxCubemaps = FMath::Min(DesiredMaxCubemaps, PlatformMaxNumReflectionCaptures); const int32 ReflectionCaptureSize = GetReflectionCaptureSizeForArrayCount(UReflectionCaptureComponent::GetReflectionCaptureSize(), DesiredMaxCubemaps); bool bNeedsUpdateAllCaptures = DesiredMaxCubemaps != ReflectionSceneData.MaxAllocatedReflectionCubemapsGameThread || ReflectionCaptureSize != ReflectionSceneData.CubemapArray.GetCubemapSize(); if (DoGPUArrayCopy() && bNeedsUpdateAllCaptures) { // If we're not in the editor, we discard the CPU-side reflection capture data after loading to save memory, so we can't resize if the resolution changes. If this happens, we assert check(GIsEditor || ReflectionCaptureSize == ReflectionSceneData.CubemapArray.GetCubemapSize() || ReflectionSceneData.CubemapArray.GetCubemapSize() == 0); if (ReflectionCaptureSize == ReflectionSceneData.CubemapArray.GetCubemapSize()) { // We can do a fast GPU copy to realloc the array, so we don't need to update all captures ReflectionSceneData.MaxAllocatedReflectionCubemapsGameThread = DesiredMaxCubemaps; FScene* Scene = this; uint32 MaxSize = ReflectionSceneData.MaxAllocatedReflectionCubemapsGameThread; ENQUEUE_RENDER_COMMAND(GPUResizeArrayCommand)( [Scene, MaxSize, ReflectionCaptureSize](FRHICommandListImmediate& RHICmdList) { // Update the scene's cubemap array, preserving the original contents with a GPU-GPU copy Scene->ReflectionSceneData.ResizeCubemapArrayGPU(MaxSize, ReflectionCaptureSize); }); bNeedsUpdateAllCaptures = false; } } if (bNeedsUpdateAllCaptures) { ReflectionSceneData.MaxAllocatedReflectionCubemapsGameThread = DesiredMaxCubemaps; FScene* Scene = this; uint32 MaxSize = ReflectionSceneData.MaxAllocatedReflectionCubemapsGameThread; ENQUEUE_RENDER_COMMAND(ResizeArrayCommand)( [Scene, MaxSize, ReflectionCaptureSize](FRHICommandListImmediate& RHICmdList) { // Update the scene's cubemap array, which will reallocate it, so we no longer have the contents of existing entries Scene->ReflectionSceneData.CubemapArray.UpdateMaxCubemaps(MaxSize, ReflectionCaptureSize); }); // Recapture all reflection captures now that we have reallocated the cubemap array UpdateAllReflectionCaptures(CaptureReason, ReflectionCaptureSize, bVerifyOnlyCapturing, bCapturingForMobile); } else { const int32 NumCapturesForStatus = bVerifyOnlyCapturing ? NewCaptures.Num() : 0; BeginReflectionCaptureSlowTask(NumCapturesForStatus, CaptureReason); // No teardown of the cubemap array was needed, just update the captures that were requested for (int32 CaptureIndex = 0; CaptureIndex < NewCaptures.Num(); CaptureIndex++) { UReflectionCaptureComponent* CurrentComponent = NewCaptures[CaptureIndex]; UpdateReflectionCaptureSlowTask(CaptureIndex, NumCapturesForStatus); bool bAllocated = false; for (TSparseArray::TIterator It(ReflectionSceneData.AllocatedReflectionCapturesGameThread); It; ++It) { if (*It == CurrentComponent) { bAllocated = true; } } if (bAllocated) { CaptureOrUploadReflectionCapture(CurrentComponent, ReflectionCaptureSize, bVerifyOnlyCapturing, bCapturingForMobile); } } EndReflectionCaptureSlowTask(NumCapturesForStatus); } } for (int32 CaptureIndex = 0; CaptureIndex < NewCaptures.Num(); CaptureIndex++) { UReflectionCaptureComponent* Component = NewCaptures[CaptureIndex]; Component->SetCaptureCompleted(); if (Component->SceneProxy) { // Update the transform of the reflection capture // This is not done earlier by the reflection capture when it detects that it is dirty, // To ensure that the RT sees both the new transform and the new contents on the same frame. Component->SendRenderTransform_Concurrent(); } } } } /** Updates the contents of all reflection captures in the scene. Must be called from the game thread. */ void FScene::UpdateAllReflectionCaptures(const TCHAR* CaptureReason, int32 ReflectionCaptureSize, bool bVerifyOnlyCapturing, bool bCapturingForMobile) { if (IsReflectionEnvironmentAvailable(GetFeatureLevel())) { FScene* Scene = this; ENQUEUE_RENDER_COMMAND(CaptureCommand)( [Scene](FRHICommandListImmediate& RHICmdList) { Scene->ReflectionSceneData.AllocatedReflectionCaptureState.Empty(); Scene->ReflectionSceneData.CubemapArraySlotsUsed.Reset(); }); // Only display status during building reflection captures, otherwise we may interrupt a editor widget manipulation of many captures const int32 NumCapturesForStatus = bVerifyOnlyCapturing ? ReflectionSceneData.AllocatedReflectionCapturesGameThread.Num() : 0; BeginReflectionCaptureSlowTask(NumCapturesForStatus, CaptureReason); int32 CaptureIndex = 0; for (TSparseArray::TIterator It(ReflectionSceneData.AllocatedReflectionCapturesGameThread); It; ++It) { UpdateReflectionCaptureSlowTask(CaptureIndex, NumCapturesForStatus); CaptureIndex++; UReflectionCaptureComponent* CurrentComponent = *It; CaptureOrUploadReflectionCapture(CurrentComponent, ReflectionCaptureSize, bVerifyOnlyCapturing, bCapturingForMobile); } EndReflectionCaptureSlowTask(NumCapturesForStatus); } } void GetReflectionCaptureData_RenderingThread(FRHICommandListImmediate& RHICmdList, FScene* Scene, const UReflectionCaptureComponent* Component, FReflectionCaptureData* OutCaptureData) { const FCaptureComponentSceneState* ComponentStatePtr = Scene->ReflectionSceneData.AllocatedReflectionCaptureState.Find(Component); if (ComponentStatePtr) { FSceneRenderTargetItem& EffectiveDest = Scene->ReflectionSceneData.CubemapArray.GetRenderTarget(); const int32 CubemapIndex = ComponentStatePtr->CubemapIndex; const int32 NumMips = EffectiveDest.ShaderResourceTexture->GetNumMips(); const int32 EffectiveTopMipSize = FMath::Pow(2.f, NumMips - 1); int32 CaptureDataSize = 0; for (int32 MipIndex = 0; MipIndex < NumMips; MipIndex++) { const int32 MipSize = 1 << (NumMips - MipIndex - 1); for (int32 CubeFace = 0; CubeFace < CubeFace_MAX; CubeFace++) { CaptureDataSize += MipSize * MipSize * sizeof(FFloat16Color); } } OutCaptureData->FullHDRCapturedData.Empty(CaptureDataSize); OutCaptureData->FullHDRCapturedData.AddZeroed(CaptureDataSize); int32 MipBaseIndex = 0; for (int32 MipIndex = 0; MipIndex < NumMips; MipIndex++) { check(EffectiveDest.ShaderResourceTexture->GetFormat() == PF_FloatRGBA); const int32 MipSize = 1 << (NumMips - MipIndex - 1); const int32 CubeFaceBytes = MipSize * MipSize * sizeof(FFloat16Color); for (int32 CubeFace = 0; CubeFace < CubeFace_MAX; CubeFace++) { TArray SurfaceData; // Read each mip face //@todo - do this without blocking the GPU so many times //@todo - pool the temporary textures in RHIReadSurfaceFloatData instead of always creating new ones RHICmdList.ReadSurfaceFloatData(EffectiveDest.ShaderResourceTexture, FIntRect(0, 0, MipSize, MipSize), SurfaceData, (ECubeFace)CubeFace, CubemapIndex, MipIndex); const int32 DestIndex = MipBaseIndex + CubeFace * CubeFaceBytes; uint8* FaceData = &OutCaptureData->FullHDRCapturedData[DestIndex]; check(SurfaceData.Num() * SurfaceData.GetTypeSize() == CubeFaceBytes); FMemory::Memcpy(FaceData, SurfaceData.GetData(), CubeFaceBytes); } MipBaseIndex += CubeFaceBytes * CubeFace_MAX; } OutCaptureData->CubemapSize = EffectiveTopMipSize; OutCaptureData->AverageBrightness = ComponentStatePtr->AverageBrightness; } } void FScene::GetReflectionCaptureData(UReflectionCaptureComponent* Component, FReflectionCaptureData& OutCaptureData) { check(GetFeatureLevel() >= ERHIFeatureLevel::SM5); FScene* Scene = this; FReflectionCaptureData* OutCaptureDataPtr = &OutCaptureData; ENQUEUE_RENDER_COMMAND(GetReflectionDataCommand)( [Scene, Component, OutCaptureDataPtr](FRHICommandListImmediate& RHICmdList) { GetReflectionCaptureData_RenderingThread(RHICmdList, Scene, Component, OutCaptureDataPtr); }); // Necessary since the RT is writing to OutDerivedData directly FlushRenderingCommands(); // Required for cooking of Encoded HDR data OutCaptureData.Brightness = Component->Brightness; } void UploadReflectionCapture_RenderingThread(FScene* Scene, const FReflectionCaptureData* CaptureData, const UReflectionCaptureComponent* CaptureComponent) { const int32 EffectiveTopMipSize = CaptureData->CubemapSize; const int32 NumMips = FMath::CeilLogTwo(EffectiveTopMipSize) + 1; const int32 CaptureIndex = FindOrAllocateCubemapIndex(Scene, CaptureComponent); check(CaptureData->CubemapSize == Scene->ReflectionSceneData.CubemapArray.GetCubemapSize()); check(CaptureIndex < Scene->ReflectionSceneData.CubemapArray.GetMaxCubemaps()); FTextureCubeRHIRef& CubeMapArray = (FTextureCubeRHIRef&)Scene->ReflectionSceneData.CubemapArray.GetRenderTarget().ShaderResourceTexture; check(CubeMapArray->GetFormat() == PF_FloatRGBA); int32 MipBaseIndex = 0; for (int32 MipIndex = 0; MipIndex < NumMips; MipIndex++) { const int32 MipSize = 1 << (NumMips - MipIndex - 1); const int32 CubeFaceBytes = MipSize * MipSize * sizeof(FFloat16Color); for (int32 CubeFace = 0; CubeFace < CubeFace_MAX; CubeFace++) { uint32 DestStride = 0; uint8* DestBuffer = (uint8*)RHILockTextureCubeFace(CubeMapArray, CubeFace, CaptureIndex, MipIndex, RLM_WriteOnly, DestStride, false); // Handle DestStride by copying each row for (int32 Y = 0; Y < MipSize; Y++) { FFloat16Color* DestPtr = (FFloat16Color*)((uint8*)DestBuffer + Y * DestStride); const int32 SourceIndex = MipBaseIndex + CubeFace * CubeFaceBytes + Y * MipSize * sizeof(FFloat16Color); const uint8* SourcePtr = &CaptureData->FullHDRCapturedData[SourceIndex]; FMemory::Memcpy(DestPtr, SourcePtr, MipSize * sizeof(FFloat16Color)); } RHIUnlockTextureCubeFace(CubeMapArray, CubeFace, CaptureIndex, MipIndex, false); } MipBaseIndex += CubeFaceBytes * CubeFace_MAX; } FCaptureComponentSceneState& FoundState = Scene->ReflectionSceneData.AllocatedReflectionCaptureState.FindChecked(CaptureComponent); FoundState.AverageBrightness = CaptureData->AverageBrightness; } /** Creates a transformation for a cubemap face, following the D3D cubemap layout. */ FMatrix CalcCubeFaceViewRotationMatrix(ECubeFace Face) { FMatrix Result(FMatrix::Identity); static const FVector XAxis(1.f,0.f,0.f); static const FVector YAxis(0.f,1.f,0.f); static const FVector ZAxis(0.f,0.f,1.f); // vectors we will need for our basis FVector vUp(YAxis); FVector vDir; switch( Face ) { case CubeFace_PosX: vDir = XAxis; break; case CubeFace_NegX: vDir = -XAxis; break; case CubeFace_PosY: vUp = -ZAxis; vDir = YAxis; break; case CubeFace_NegY: vUp = ZAxis; vDir = -YAxis; break; case CubeFace_PosZ: vDir = ZAxis; break; case CubeFace_NegZ: vDir = -ZAxis; break; } // derive right vector FVector vRight( vUp ^ vDir ); // create matrix from the 3 axes Result = FBasisVectorMatrix( vRight, vUp, vDir, FVector::ZeroVector ); return Result; } FMatrix GetCubeProjectionMatrix(float HalfFovDeg, float CubeMapSize, float NearPlane) { if ((bool)ERHIZBuffer::IsInverted) { return FReversedZPerspectiveMatrix(HalfFovDeg * float(PI) / 180.0f, CubeMapSize, CubeMapSize, NearPlane); } return FPerspectiveMatrix(HalfFovDeg, CubeMapSize, CubeMapSize, NearPlane); } /** * Render target class required for rendering the scene. * This doesn't actually allocate a render target as we read from scene color to get HDR results directly. */ class FCaptureRenderTarget : public FRenderResource, public FRenderTarget { public: FCaptureRenderTarget() : Size(0) {} virtual const FTexture2DRHIRef& GetRenderTargetTexture() const { static FTexture2DRHIRef DummyTexture; return DummyTexture; } void SetSize(int32 TargetSize) { Size = TargetSize; } virtual FIntPoint GetSizeXY() const { return FIntPoint(Size, Size); } virtual float GetDisplayGamma() const { return 1.0f; } private: int32 Size; }; TGlobalResource GReflectionCaptureRenderTarget; void CaptureSceneIntoScratchCubemap( FScene* Scene, FVector CapturePosition, int32 CubemapSize, bool bCapturingForSkyLight, bool bStaticSceneOnly, float SkyLightNearPlane, bool bLowerHemisphereIsBlack, bool bCaptureEmissiveOnly, const FLinearColor& LowerHemisphereColor, bool bCapturingForMobile ) { int32 SupersampleCaptureFactor = FMath::Clamp(GSupersampleCaptureFactor, MinSupersampleCaptureFactor, MaxSupersampleCaptureFactor); for (int32 CubeFace = 0; CubeFace < CubeFace_MAX; CubeFace++) { if( !bCapturingForSkyLight ) { // Alert the RHI that we're rendering a new frame // Not really a new frame, but it will allow pooling mechanisms to update, like the uniform buffer pool ENQUEUE_RENDER_COMMAND(BeginFrame)( [](FRHICommandList& RHICmdList) { GFrameNumberRenderThread++; RHICmdList.BeginFrame(); }); } GReflectionCaptureRenderTarget.SetSize(CubemapSize); auto ViewFamilyInit = FSceneViewFamily::ConstructionValues( &GReflectionCaptureRenderTarget, Scene, FEngineShowFlags(ESFIM_Game) ) .SetResolveScene(false); if( bStaticSceneOnly ) { ViewFamilyInit.SetTime(FGameTime()); } FSceneViewFamilyContext ViewFamily( ViewFamilyInit ); // Disable features that are not desired when capturing the scene ViewFamily.EngineShowFlags.PostProcessing = 0; ViewFamily.EngineShowFlags.MotionBlur = 0; ViewFamily.EngineShowFlags.SetOnScreenDebug(false); ViewFamily.EngineShowFlags.HMDDistortion = 0; // Exclude particles and light functions as they are usually dynamic, and can't be captured well ViewFamily.EngineShowFlags.Particles = 0; ViewFamily.EngineShowFlags.LightFunctions = 0; ViewFamily.EngineShowFlags.SetCompositeEditorPrimitives(false); // These are highly dynamic and can't be captured effectively ViewFamily.EngineShowFlags.LightShafts = 0; // Don't apply sky lighting diffuse when capturing the sky light source, or we would have feedback ViewFamily.EngineShowFlags.SkyLighting = !bCapturingForSkyLight; // Skip lighting for emissive only ViewFamily.EngineShowFlags.Lighting = !bCaptureEmissiveOnly; // Never do screen percentage in reflection environment capture. ViewFamily.EngineShowFlags.ScreenPercentage = false; FSceneViewInitOptions ViewInitOptions; ViewInitOptions.ViewFamily = &ViewFamily; ViewInitOptions.BackgroundColor = FLinearColor::Black; ViewInitOptions.OverlayColor = FLinearColor::Black; ViewInitOptions.SetViewRectangle(FIntRect(0, 0, CubemapSize * SupersampleCaptureFactor, CubemapSize * SupersampleCaptureFactor)); const float NearPlane = bCapturingForSkyLight ? SkyLightNearPlane : GReflectionCaptureNearPlane; // Projection matrix based on the fov, near / far clip settings // Each face always uses a 90 degree field of view ViewInitOptions.ProjectionMatrix = GetCubeProjectionMatrix(45.0f, (float)CubemapSize * SupersampleCaptureFactor, NearPlane); ViewInitOptions.ViewOrigin = CapturePosition; ViewInitOptions.ViewRotationMatrix = CalcCubeFaceViewRotationMatrix((ECubeFace)CubeFace); FSceneView* View = new FSceneView(ViewInitOptions); // Force all surfaces diffuse View->RoughnessOverrideParameter = FVector2D( 1.0f, 0.0f ); if (bCaptureEmissiveOnly) { View->DiffuseOverrideParameter = FVector4f(0, 0, 0, 0); View->SpecularOverrideParameter = FVector4f(0, 0, 0, 0); } View->bIsReflectionCapture = true; View->bStaticSceneOnly = bStaticSceneOnly; View->StartFinalPostprocessSettings(CapturePosition); View->EndFinalPostprocessSettings(ViewInitOptions); ViewFamily.Views.Add(View); ViewFamily.SetScreenPercentageInterface(new FLegacyScreenPercentageDriver( ViewFamily, /* GlobalResolutionFraction = */ 1.0f)); FSceneRenderer* SceneRenderer = FSceneRenderer::CreateSceneRenderer(&ViewFamily, NULL); ENQUEUE_RENDER_COMMAND(CaptureCommand)( [SceneRenderer, CubeFace, CubemapSize, bCapturingForSkyLight, bLowerHemisphereIsBlack, LowerHemisphereColor, bCapturingForMobile](FRHICommandListImmediate& RHICmdList) { CaptureSceneToScratchCubemap(RHICmdList, SceneRenderer, (ECubeFace)CubeFace, CubemapSize, bCapturingForSkyLight, bLowerHemisphereIsBlack, LowerHemisphereColor, bCapturingForMobile); if (!bCapturingForSkyLight) { RHICmdList.EndFrame(); } }); } } void CopyToSceneArray(FRHICommandListImmediate& RHICmdList, FScene* Scene, FReflectionCaptureProxy* ReflectionProxy) { SCOPED_DRAW_EVENT(RHICmdList, CopyToSceneArray); const int32 EffectiveTopMipSize = Scene->ReflectionSceneData.CubemapArray.GetCubemapSize(); const int32 NumMips = FMath::CeilLogTwo(EffectiveTopMipSize) + 1; const int32 CaptureIndex = FindOrAllocateCubemapIndex(Scene, ReflectionProxy->Component); FSceneRenderTargetItem& FilteredCube = GReflectionScratchCubemaps.Color[1]->GetRenderTargetItem(); FSceneRenderTargetItem& DestCube = Scene->ReflectionSceneData.CubemapArray.GetRenderTarget(); // GPU copy back to the scene's texture array, which is not a render target for (int32 MipIndex = 0; MipIndex < NumMips; MipIndex++) { for (int32 CubeFace = 0; CubeFace < CubeFace_MAX; CubeFace++) { RHICmdList.CopyToResolveTarget(FilteredCube.ShaderResourceTexture, DestCube.ShaderResourceTexture, FResolveParams(FResolveRect(), (ECubeFace)CubeFace, MipIndex, 0, CaptureIndex)); } } } /** * Updates the contents of the given reflection capture by rendering the scene. * This must be called on the game thread. */ void FScene::CaptureOrUploadReflectionCapture(UReflectionCaptureComponent* CaptureComponent, int32 ReflectionCaptureSize, bool bVerifyOnlyCapturing, bool bCapturingForMobile) { if (IsReflectionEnvironmentAvailable(GetFeatureLevel())) { FReflectionCaptureData* CaptureData = CaptureComponent->GetMapBuildData(); // Upload existing derived data if it exists, instead of capturing if (CaptureData) { // Safety check during the reflection capture build, there should not be any map build data ensure(!bVerifyOnlyCapturing); check(SupportsTextureCubeArray(GetFeatureLevel())); FScene* Scene = this; ENQUEUE_RENDER_COMMAND(UploadCaptureCommand) ([Scene, CaptureData, CaptureComponent](FRHICommandListImmediate& RHICmdList) { // After the final upload we cannot upload again because we tossed the source MapBuildData, // After uploading it into the scene's texture array, to guaratee there's only one copy in memory. // This means switching between LightingScenarios only works if the scenario level is reloaded (not simply made hidden / visible again) if (!CaptureData->HasBeenUploadedFinal()) { UploadReflectionCapture_RenderingThread(Scene, CaptureData, CaptureComponent); if (DoGPUArrayCopy()) { CaptureData->OnDataUploadedToGPUFinal(); } } else { const FCaptureComponentSceneState* CaptureSceneStatePtr = Scene->ReflectionSceneData.AllocatedReflectionCaptureState.Find(CaptureComponent); if (!CaptureSceneStatePtr) { ensureMsgf(CaptureSceneStatePtr, TEXT("Reflection capture %s uploaded twice without reloading its lighting scenario level. The Lighting scenario level must be loaded once for each time the reflection capture is uploaded."), *CaptureComponent->GetPathName()); } } }); } // Capturing only supported in the editor. Game can only use built reflection captures. else if (bIsEditorScene) { if (CaptureComponent->ReflectionSourceType == EReflectionSourceType::SpecifiedCubemap && !CaptureComponent->Cubemap) { return; } if (FPlatformProperties::RequiresCookedData()) { UE_LOG(LogEngine, Warning, TEXT("No built data for %s, skipping generation in cooked build."), *CaptureComponent->GetPathName()); return; } // Prefetch all virtual textures so that we have content available if (UseVirtualTexturing(GetFeatureLevel())) { const ERHIFeatureLevel::Type InFeatureLevel = FeatureLevel; const FVector2D ScreenSpaceSize(ReflectionCaptureSize, ReflectionCaptureSize); ENQUEUE_RENDER_COMMAND(LoadTiles)( [InFeatureLevel, ScreenSpaceSize](FRHICommandListImmediate& RHICmdList) { GetRendererModule().RequestVirtualTextureTiles(ScreenSpaceSize, -1); GetRendererModule().LoadPendingVirtualTextureTiles(RHICmdList, InFeatureLevel); }); FlushRenderingCommands(); } ENQUEUE_RENDER_COMMAND(ClearCommand)( [ReflectionCaptureSize](FRHICommandListImmediate& RHICmdList) { ClearScratchCubemaps(RHICmdList, ReflectionCaptureSize); }); if (CaptureComponent->ReflectionSourceType == EReflectionSourceType::CapturedScene) { bool const bCaptureStaticSceneOnly = CVarReflectionCaptureStaticSceneOnly.GetValueOnGameThread() != 0; CaptureSceneIntoScratchCubemap(this, CaptureComponent->GetComponentLocation() + CaptureComponent->CaptureOffset, ReflectionCaptureSize, false, bCaptureStaticSceneOnly, 0, false, false, FLinearColor(), bCapturingForMobile); } else if (CaptureComponent->ReflectionSourceType == EReflectionSourceType::SpecifiedCubemap) { UTextureCube* SourceCubemap = CaptureComponent->Cubemap; float SourceCubemapRotation = CaptureComponent->SourceCubemapAngle * (PI / 180.f); ERHIFeatureLevel::Type InFeatureLevel = FeatureLevel; ENQUEUE_RENDER_COMMAND(CopyCubemapCommand)( [SourceCubemap, ReflectionCaptureSize, SourceCubemapRotation, InFeatureLevel](FRHICommandListImmediate& RHICmdList) { CopyCubemapToScratchCubemap(RHICmdList, InFeatureLevel, SourceCubemap, ReflectionCaptureSize, false, false, SourceCubemapRotation, FLinearColor()); }); } else { check(!TEXT("Unknown reflection source type")); } { ERHIFeatureLevel::Type InFeatureLevel = GetFeatureLevel(); int32 InReflectionCaptureSize = ReflectionCaptureSize; FScene* Scene = this; const UReflectionCaptureComponent* InCaptureComponent = CaptureComponent; ENQUEUE_RENDER_COMMAND(FilterCommand)( [InFeatureLevel, InReflectionCaptureSize, Scene, InCaptureComponent](FRHICommandListImmediate& RHICmdList) { FindOrAllocateCubemapIndex(Scene, InCaptureComponent); FCaptureComponentSceneState& FoundState = Scene->ReflectionSceneData.AllocatedReflectionCaptureState.FindChecked(InCaptureComponent); ComputeAverageBrightness(RHICmdList, InFeatureLevel, InReflectionCaptureSize, FoundState.AverageBrightness); FilterReflectionEnvironment(RHICmdList, InFeatureLevel, InReflectionCaptureSize, nullptr); }); } // Create a proxy to represent the reflection capture to the rendering thread // The rendering thread will be responsible for deleting this when done with the filtering operation // We can't use the component's SceneProxy here because the component may not be registered with the scene FReflectionCaptureProxy* ReflectionProxy = new FReflectionCaptureProxy(CaptureComponent); FScene* Scene = this; ERHIFeatureLevel::Type InFeatureLevel = GetFeatureLevel(); ENQUEUE_RENDER_COMMAND(CopyCommand)( [Scene, ReflectionProxy, InFeatureLevel](FRHICommandListImmediate& RHICmdList) { if (InFeatureLevel == ERHIFeatureLevel::SM5) { CopyToSceneArray(RHICmdList, Scene, ReflectionProxy); } // Clean up the proxy now that the rendering thread is done with it delete ReflectionProxy; }); } //-V773 } } void ReadbackRadianceMap(FRHICommandListImmediate& RHICmdList, int32 CubmapSize, TArray& OutRadianceMap) { OutRadianceMap.Empty(CubmapSize * CubmapSize * 6); OutRadianceMap.AddZeroed(CubmapSize * CubmapSize * 6); const int32 MipIndex = 0; FSceneRenderTargetItem& SourceCube = GReflectionScratchCubemaps.Color[0]->GetRenderTargetItem(); check(SourceCube.ShaderResourceTexture->GetFormat() == PF_FloatRGBA); const int32 CubeFaceBytes = CubmapSize * CubmapSize * OutRadianceMap.GetTypeSize(); for (int32 CubeFace = 0; CubeFace < CubeFace_MAX; CubeFace++) { TArray SurfaceData; // Read each mip face RHICmdList.ReadSurfaceFloatData(SourceCube.ShaderResourceTexture, FIntRect(0, 0, CubmapSize, CubmapSize), SurfaceData, (ECubeFace)CubeFace, 0, MipIndex); const int32 DestIndex = CubeFace * CubmapSize * CubmapSize; FFloat16Color* FaceData = &OutRadianceMap[DestIndex]; check(SurfaceData.Num() * SurfaceData.GetTypeSize() == CubeFaceBytes); FMemory::Memcpy(FaceData, SurfaceData.GetData(), CubeFaceBytes); } } void CopyToSkyTexture(FRHICommandList& RHICmdList, FScene* Scene, FTexture* ProcessedTexture) { SCOPED_DRAW_EVENT(RHICmdList, CopyToSkyTexture); if (ProcessedTexture->TextureRHI) { const int32 EffectiveTopMipSize = ProcessedTexture->GetSizeX(); const int32 NumMips = FMath::CeilLogTwo(EffectiveTopMipSize) + 1; FSceneRenderTargetItem& FilteredCube = GReflectionScratchCubemaps.Color[1]->GetRenderTargetItem(); // GPU copy back to the skylight's texture, which is not a render target FRHICopyTextureInfo CopyInfo; CopyInfo.Size = FilteredCube.ShaderResourceTexture->GetSizeXYZ(); CopyInfo.NumSlices = 6; CopyInfo.NumMips = NumMips; FRHITransitionInfo TransitionsBefore[] = { FRHITransitionInfo(FilteredCube.ShaderResourceTexture, ERHIAccess::Unknown, ERHIAccess::CopySrc), FRHITransitionInfo(ProcessedTexture->TextureRHI, ERHIAccess::Unknown, ERHIAccess::CopyDest) }; RHICmdList.Transition(MakeArrayView(TransitionsBefore, UE_ARRAY_COUNT(TransitionsBefore))); RHICmdList.CopyTexture(FilteredCube.ShaderResourceTexture, ProcessedTexture->TextureRHI, CopyInfo); FRHITransitionInfo TransitionsAfter[] = { FRHITransitionInfo(FilteredCube.ShaderResourceTexture, ERHIAccess::CopySrc, ERHIAccess::SRVMask), FRHITransitionInfo(ProcessedTexture->TextureRHI, ERHIAccess::CopyDest, ERHIAccess::SRVMask) }; RHICmdList.Transition(MakeArrayView(TransitionsAfter, UE_ARRAY_COUNT(TransitionsAfter))); } } // Warning: returns before writes to OutIrradianceEnvironmentMap have completed, as they are queued on the rendering thread void FScene::UpdateSkyCaptureContents( const USkyLightComponent* CaptureComponent, bool bCaptureEmissiveOnly, UTextureCube* SourceCubemap, FTexture* OutProcessedTexture, float& OutAverageBrightness, FSHVectorRGB3& OutIrradianceEnvironmentMap, TArray* OutRadianceMap) { if (GSupportsRenderTargetFormat_PF_FloatRGBA || GetFeatureLevel() >= ERHIFeatureLevel::SM5) { QUICK_SCOPE_CYCLE_COUNTER(STAT_UpdateSkyCaptureContents); { World = GetWorld(); if (World) { //guarantee that all render proxies are up to date before kicking off this render World->SendAllEndOfFrameUpdates(); } } { int32 CubemapSize = CaptureComponent->CubemapResolution; ENQUEUE_RENDER_COMMAND(ClearCommand)( [CubemapSize](FRHICommandListImmediate& RHICmdList) { ClearScratchCubemaps(RHICmdList, CubemapSize); }); } if (CaptureComponent->SourceType == SLS_CapturedScene) { bool bStaticSceneOnly = CaptureComponent->Mobility == EComponentMobility::Static; bool bCapturingForMobile = false; CaptureSceneIntoScratchCubemap(this, CaptureComponent->GetComponentLocation(), CaptureComponent->CubemapResolution, true, bStaticSceneOnly, CaptureComponent->SkyDistanceThreshold, CaptureComponent->bLowerHemisphereIsBlack, bCaptureEmissiveOnly, CaptureComponent->LowerHemisphereColor, bCapturingForMobile); } else if (CaptureComponent->SourceType == SLS_SpecifiedCubemap) { int32 CubemapSize = CaptureComponent->CubemapResolution; bool bLowerHemisphereIsBlack = CaptureComponent->bLowerHemisphereIsBlack; float SourceCubemapRotation = CaptureComponent->SourceCubemapAngle * (PI / 180.f); ERHIFeatureLevel::Type InnerFeatureLevel = FeatureLevel; FLinearColor LowerHemisphereColor = CaptureComponent->LowerHemisphereColor; ENQUEUE_RENDER_COMMAND(CopyCubemapCommand)( [SourceCubemap, CubemapSize, bLowerHemisphereIsBlack, SourceCubemapRotation, InnerFeatureLevel, LowerHemisphereColor](FRHICommandListImmediate& RHICmdList) { CopyCubemapToScratchCubemap(RHICmdList, InnerFeatureLevel, SourceCubemap, CubemapSize, true, bLowerHemisphereIsBlack, SourceCubemapRotation, LowerHemisphereColor); }); } else if (CaptureComponent->IsRealTimeCaptureEnabled()) { ensureMsgf(false, TEXT("A sky light with RealTimeCapture enabled cannot be scheduled for a cubemap update. This will be done dynamically each frame by the renderer.")); return; } else { check(0); } if (OutRadianceMap) { int32 CubemapSize = CaptureComponent->CubemapResolution; ENQUEUE_RENDER_COMMAND(ReadbackCommand)( [CubemapSize, OutRadianceMap](FRHICommandListImmediate& RHICmdList) { ReadbackRadianceMap(RHICmdList, CubemapSize, *OutRadianceMap); }); } { int32 CubemapSize = CaptureComponent->CubemapResolution; float* AverageBrightness = &OutAverageBrightness; FSHVectorRGB3* IrradianceEnvironmentMap = &OutIrradianceEnvironmentMap; ERHIFeatureLevel::Type InFeatureLevel = GetFeatureLevel(); ENQUEUE_RENDER_COMMAND(FilterCommand)( [CubemapSize, AverageBrightness, IrradianceEnvironmentMap, InFeatureLevel](FRHICommandListImmediate& RHICmdList) { if (InFeatureLevel <= ERHIFeatureLevel::ES3_1) { MobileReflectionEnvironmentCapture::ComputeAverageBrightness(RHICmdList, InFeatureLevel, CubemapSize, *AverageBrightness); MobileReflectionEnvironmentCapture::FilterReflectionEnvironment(RHICmdList, InFeatureLevel, CubemapSize, IrradianceEnvironmentMap); } else { ComputeAverageBrightness(RHICmdList, InFeatureLevel, CubemapSize, *AverageBrightness); FilterReflectionEnvironment(RHICmdList, InFeatureLevel, CubemapSize, IrradianceEnvironmentMap); } }); } // Optionally copy the filtered mip chain to the output texture if (OutProcessedTexture) { FScene* Scene = this; ERHIFeatureLevel::Type InFeatureLevel = GetFeatureLevel(); ENQUEUE_RENDER_COMMAND(CopyCommand)( [Scene, OutProcessedTexture, InFeatureLevel](FRHICommandListImmediate& RHICmdList) { if (InFeatureLevel <= ERHIFeatureLevel::ES3_1) { MobileReflectionEnvironmentCapture::CopyToSkyTexture(RHICmdList, Scene, OutProcessedTexture); } else { CopyToSkyTexture(RHICmdList, Scene, OutProcessedTexture); } }); } if (!!GFreeReflectionScratchAfterUse) { ENQUEUE_RENDER_COMMAND(FreeReflectionScratch)( [](FRHICommandListImmediate& RHICmdList) { GReflectionScratchCubemaps.Release(); GRenderTargetPool.FreeUnusedResources(); }); } // These textures should only be manipulated by the render thread, // so enqueue a render command for them to be processed there ENQUEUE_RENDER_COMMAND(ReleasePathTracerSkylightData)( [this](FRHICommandListImmediate& RHICmdList) { PathTracingSkylightTexture.SafeRelease(); PathTracingSkylightPdf.SafeRelease(); }); } }