// Copyright (c) Wojciech Figat. All rights reserved. #include "ScreenSpaceReflectionsPass.h" #include "ReflectionsPass.h" #include "GBufferPass.h" #include "RenderList.h" #include "GlobalSignDistanceFieldPass.h" #include "GI/GlobalSurfaceAtlasPass.h" #include "Utils/MultiScaler.h" #include "Engine/Engine/Engine.h" #include "Engine/Platform/Window.h" #include "Engine/Content/Content.h" #include "Engine/Graphics/Graphics.h" #include "Engine/Graphics/RenderTools.h" #include "Engine/Graphics/RenderTask.h" #include "Engine/Graphics/RenderTargetPool.h" #include "Engine/Graphics/RenderBuffers.h" #include "Engine/Graphics/GPUContext.h" // Shader input texture slots mapping #define TEXTURE0 4 #define TEXTURE1 5 #define TEXTURE2 6 #define SSR_USE_HZB 1 GPU_CB_STRUCT(Data { ShaderGBufferData GBuffer; float MaxColorMiplevel; float TraceSizeMax; float MaxTraceSamples; float RoughnessFade; Float2 SSRTexelSize; float TemporalTime; float BRDFBias; float WorldAntiSelfOcclusionBias; float EdgeFadeFactor; float TemporalResponse; uint32 DepthMips; float RayTraceStep; float TemporalEffect; float Intensity; float FadeOutDistance; Matrix ViewMatrix; Matrix ViewProjectionMatrix; GlobalSignDistanceFieldPass::ConstantsData GlobalSDF; GlobalSurfaceAtlasPass::ConstantsData GlobalSurfaceAtlas; }); String ScreenSpaceReflectionsPass::ToString() const { return TEXT("ScreenSpaceReflectionsPass"); } bool ScreenSpaceReflectionsPass::Init() { // Create pipeline states _psRayTracePass.CreatePipelineStates(); _psResolvePass.CreatePipelineStates(); _psCombinePass = GPUDevice::Instance->CreatePipelineState(); _psTemporalPass = GPUDevice::Instance->CreatePipelineState(); // Load assets _shader = Content::LoadAsyncInternal(TEXT("Shaders/SSR")); _preIntegratedGF = Content::LoadAsyncInternal(PRE_INTEGRATED_GF_ASSET_NAME); if (_shader == nullptr || _preIntegratedGF == nullptr) return true; #if COMPILE_WITH_DEV_ENV _shader.Get()->OnReloading.Bind(this); #endif return false; } bool ScreenSpaceReflectionsPass::setupResources() { if (!_preIntegratedGF->IsLoaded()) return true; if (!_shader->IsLoaded()) return true; const auto shader = _shader->GetShader(); CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); // Create pipeline stages GPUPipelineState::Description psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle; if (!_psRayTracePass.IsValid()) { if (_psRayTracePass.Create(psDesc, shader, "PS_RayTracePass")) return true; } if (!_psCombinePass->IsValid()) { psDesc.PS = shader->GetPS("PS_CombinePass"); if (_psCombinePass->Init(psDesc)) return true; } if (!_psResolvePass.IsValid()) { if (_psResolvePass.Create(psDesc, shader, "PS_ResolvePass")) return true; } if (!_psTemporalPass->IsValid()) { psDesc.PS = shader->GetPS("PS_TemporalPass"); if (_psTemporalPass->Init(psDesc)) return true; } return false; } void ScreenSpaceReflectionsPass::Dispose() { // Base RendererPass::Dispose(); // Cleanup SAFE_DELETE_GPU_RESOURCE(_psCombinePass); SAFE_DELETE_GPU_RESOURCE(_psTemporalPass); _psRayTracePass.Delete(); _psResolvePass.Delete(); _shader = nullptr; _preIntegratedGF = nullptr; } GPUTexture* ScreenSpaceReflectionsPass::Render(RenderContext& renderContext, GPUTextureView* reflectionsRT, GPUTextureView* lightBuffer) { // Skip pass if resources aren't ready if (checkIfSkipPass()) return nullptr; const RenderView& view = renderContext.View; RenderBuffers* buffers = renderContext.Buffers; // TODO: add support for SSR in ortho projection if (view.IsOrthographicProjection()) return nullptr; PROFILE_GPU_CPU("Screen Space Reflections"); // Cache data auto device = GPUDevice::Instance; auto context = device->GetMainContext(); const auto shader = _shader->GetShader(); auto cb = shader->GetCB(0); auto& settings = renderContext.List->Settings.ScreenSpaceReflections; const bool useTemporal = settings.TemporalEffect && !renderContext.Task->IsCameraCut && renderContext.List->Setup.UseMotionVectors; // Prepare resolutions for passes const int32 width = buffers->GetWidth(); const int32 height = buffers->GetHeight(); if (width < 4 || height < 4) return nullptr; const int32 traceWidth = RenderTools::GetResolution(width, settings.RayTracePassResolution); const int32 traceHeight = RenderTools::GetResolution(height, settings.RayTracePassResolution); const int32 resolveWidth = RenderTools::GetResolution(width, settings.ResolvePassResolution); const int32 resolveHeight = RenderTools::GetResolution(height, settings.ResolvePassResolution); const int32 colorBufferWidth = RenderTools::GetResolution(width, ResolutionMode::Half); const int32 colorBufferHeight = RenderTools::GetResolution(height, ResolutionMode::Half); const auto colorBufferMips = MipLevelsCount(colorBufferWidth, colorBufferHeight); // Prepare buffers GPUTexture* colorBuffer0, *colorBuffer1; if (settings.UseColorBufferMips) { auto tempDesc = GPUTextureDescription::New2D(colorBufferWidth, colorBufferHeight, 0, PixelFormat::R11G11B10_Float, GPUTextureFlags::ShaderResource | GPUTextureFlags::RenderTarget | GPUTextureFlags::PerMipViews); colorBuffer0 = RenderTargetPool::Get(tempDesc); RENDER_TARGET_POOL_SET_NAME(colorBuffer0, "SSR.ColorBuffer0"); // TODO: maybe allocate colorBuffer1 smaller because mip0 is not used (the same as PostProcessingPass for Bloom), keep in sync to use the same buffer in frame colorBuffer1 = RenderTargetPool::Get(tempDesc); RENDER_TARGET_POOL_SET_NAME(colorBuffer1, "SSR.ColorBuffer1"); } else { // Single mip auto tempDesc = GPUTextureDescription::New2D(colorBufferWidth, colorBufferHeight, 1, PixelFormat::R11G11B10_Float); colorBuffer0 = RenderTargetPool::Get(tempDesc); colorBuffer1 = nullptr; } GPUTexture* traceBuffer, *resolveBuffer; { auto tempDesc = GPUTextureDescription::New2D(traceWidth, traceHeight, PixelFormat::R16G16B16A16_Float); traceBuffer = RenderTargetPool::Get(tempDesc); RENDER_TARGET_POOL_SET_NAME(traceBuffer, "SSR.TraceBuffer"); tempDesc.Width = resolveWidth; tempDesc.Height = resolveHeight; resolveBuffer = RenderTargetPool::Get(tempDesc); RENDER_TARGET_POOL_SET_NAME(resolveBuffer, "SSR.ResolveBuffer"); } // Pick effect settings int32 maxTraceSamples = 60; int32 resolveSamples = settings.ResolveSamples; switch (Graphics::SSRQuality) { case Quality::Low: maxTraceSamples = 40; resolveSamples = Math::Min(resolveSamples, 2); break; case Quality::Medium: maxTraceSamples = 55; resolveSamples = Math::Min(resolveSamples, 4); break; case Quality::High: maxTraceSamples = 70; break; case Quality::Ultra: maxTraceSamples = 120; break; } int32 resolvePassIndex = 0; if (resolveSamples >= 8) resolvePassIndex = 3; else if (resolveSamples >= 4) resolvePassIndex = 2; else if (resolveSamples >= 2) resolvePassIndex = 1; // Setup data Data data; GBufferPass::SetInputs(view, data.GBuffer); data.RoughnessFade = Math::Saturate(settings.RoughnessThreshold); data.MaxTraceSamples = static_cast(maxTraceSamples); data.BRDFBias = settings.BRDFBias; data.WorldAntiSelfOcclusionBias = settings.WorldAntiSelfOcclusionBias * (int32)settings.DepthResolution; data.EdgeFadeFactor = settings.EdgeFadeFactor; data.SSRTexelSize = Float2(1.0f / (float)traceWidth, 1.0f / (float)traceHeight); data.TraceSizeMax = (float)Math::Max(traceWidth, traceHeight); data.MaxColorMiplevel = settings.UseColorBufferMips ? (float)(colorBufferMips - 2) : 0.0f; data.RayTraceStep = (float)settings.DepthResolution / (float)width; data.Intensity = settings.Intensity; data.FadeOutDistance = Math::Max(settings.FadeOutDistance, 100.0f); data.TemporalResponse = settings.TemporalResponse; data.TemporalEffect = useTemporal ? 1.0f : 0.0f; data.TemporalTime = useTemporal ? RenderTools::ComputeTemporalTime() : 0; Matrix::Transpose(view.View, data.ViewMatrix); Matrix::Transpose(view.ViewProjection(), data.ViewProjectionMatrix); // Try to use Global Surface Atlas (with rendered GI) to perform full-scene tracing (not only screen-space) bool useGlobalSurfaceAtlas = false; GlobalSignDistanceFieldPass::BindingData bindingDataSDF; GlobalSurfaceAtlasPass::BindingData bindingDataSurfaceAtlas; if (settings.TraceMode == ReflectionsTraceMode::SoftwareTracing && EnumHasAnyFlags(view.Flags, ViewFlags::GI) && renderContext.List->Settings.GlobalIllumination.Mode == GlobalIlluminationMode::DDGI) { if (!GlobalSignDistanceFieldPass::Instance()->Render(renderContext, context, bindingDataSDF) && !GlobalSurfaceAtlasPass::Instance()->Render(renderContext, context, bindingDataSurfaceAtlas)) { useGlobalSurfaceAtlas = true; data.GlobalSDF = bindingDataSDF.Constants; data.GlobalSurfaceAtlas = bindingDataSurfaceAtlas.Constants; } } // Prepare depth buffer #if SSR_USE_HZB int32 hzbMips = settings.DepthResolution == ResolutionMode::Full ? 5 : 4; // Using lower mips in tracing introduces blocky artifacts bool hzbFullRes = settings.DepthResolution == ResolutionMode::Full; GPUTexture* depthBufferTrace = buffers->RequestHiZ(context, hzbFullRes, hzbMips); data.DepthMips = hzbMips - 1; // Offset to improve SSR range #else GPUTexture* depthBufferTrace = settings.DepthResolution == ResolutionMode::Half ? buffers->RequestHalfResDepth(context) : buffers->DepthBuffer; data.DepthMips = 1; #endif // Prepare constants context->UpdateCB(cb, &data); context->BindCB(0, cb); // Bind GBuffer inputs context->BindSR(0, buffers->GBuffer0); context->BindSR(1, buffers->GBuffer1); context->BindSR(2, buffers->GBuffer2); context->BindSR(3, depthBufferTrace); // Combine pass { PROFILE_GPU("Combine"); context->BindSR(TEXTURE0, lightBuffer); context->BindSR(TEXTURE1, reflectionsRT); context->BindSR(TEXTURE2, _preIntegratedGF->GetTexture()); context->SetViewportAndScissors((float)colorBufferWidth, (float)colorBufferHeight); context->SetRenderTarget(colorBuffer0->View(0)); context->SetState(_psCombinePass); context->DrawFullscreenTriangle(); context->UnBindSR(TEXTURE1); context->UnBindSR(TEXTURE2); context->ResetRenderTarget(); } // Blur Pass if (settings.UseColorBufferMips) { // Note: using color buffer mips maps helps with reducing artifacts // and improves resolve pass performance (faster color texture lookups, less cache misses) // Also for high surface roughness values it adds more blur to the reflection tail which looks more realistic. // Downscale with gaussian blur auto filterMode = PLATFORM_ANDROID || PLATFORM_IOS || PLATFORM_SWITCH ? MultiScaler::FilterMode::GaussianBlur5 : MultiScaler::FilterMode::GaussianBlur9; for (int32 mipLevel = 1; mipLevel < colorBufferMips; mipLevel++) { const int32 mipWidth = Math::Max(colorBufferWidth >> mipLevel, 1); const int32 mipHeight = Math::Max(colorBufferHeight >> mipLevel, 1); const auto srcMip = colorBuffer0->View(0, mipLevel - 1); const auto tmpMip = colorBuffer1->View(0, mipLevel); const auto dstMip = colorBuffer0->View(0, mipLevel); MultiScaler::Instance()->Filter(filterMode, context, mipWidth, mipHeight, srcMip, dstMip, tmpMip); } // Restore state context->BindCB(0, cb); context->BindSR(0, buffers->GBuffer0); } RenderTargetPool::Release(colorBuffer1); // Ray Trace Pass { PROFILE_GPU("RayTrace"); context->SetViewportAndScissors((float)traceWidth, (float)traceHeight); context->SetRenderTarget(*traceBuffer); context->BindSR(TEXTURE0, colorBuffer0->View()); if (useGlobalSurfaceAtlas) { context->BindSR(7, bindingDataSDF.Texture ? bindingDataSDF.Texture->ViewVolume() : nullptr); context->BindSR(8, bindingDataSDF.TextureMip ? bindingDataSDF.TextureMip->ViewVolume() : nullptr); context->BindSR(9, bindingDataSurfaceAtlas.Chunks ? bindingDataSurfaceAtlas.Chunks->View() : nullptr); context->BindSR(10, bindingDataSurfaceAtlas.CulledObjects ? bindingDataSurfaceAtlas.CulledObjects->View() : nullptr); context->BindSR(11, bindingDataSurfaceAtlas.Objects ? bindingDataSurfaceAtlas.Objects->View() : nullptr); context->BindSR(12, bindingDataSurfaceAtlas.AtlasDepth->View()); context->BindSR(13, bindingDataSurfaceAtlas.AtlasLighting->View()); } context->SetState(_psRayTracePass.Get(useGlobalSurfaceAtlas ? 1 : 0)); context->DrawFullscreenTriangle(); context->ResetRenderTarget(); RenderTargetPool::Release(colorBuffer0); } // Resolve Pass { PROFILE_GPU("Resolve"); context->SetViewportAndScissors((float)resolveWidth, (float)resolveHeight); context->SetRenderTarget(resolveBuffer->View()); context->BindSR(TEXTURE0, traceBuffer->View()); context->SetState(_psResolvePass.Get(resolvePassIndex)); context->DrawFullscreenTriangle(); context->ResetRenderTarget(); RenderTargetPool::Release(traceBuffer); } // Temporal Pass GPUTexture* reflectionsBuffer = resolveBuffer; if (useTemporal) { PROFILE_GPU("Temporal"); buffers->LastFrameTemporalSSR = Engine::FrameCount; bool resetHistory = false; if (!buffers->TemporalSSR || buffers->TemporalSSR->Width() != resolveWidth || buffers->TemporalSSR->Height() != resolveHeight) { resetHistory = true; if (buffers->TemporalSSR) RenderTargetPool::Release(buffers->TemporalSSR); auto tempDesc = GPUTextureDescription::New2D(resolveWidth, resolveHeight, PixelFormat::R16G16B16A16_Float); buffers->TemporalSSR = RenderTargetPool::Get(tempDesc); RENDER_TARGET_POOL_SET_NAME(buffers->TemporalSSR, "SSR.TemporalSSR"); } auto newTemporal = RenderTargetPool::Get(buffers->TemporalSSR->GetDescription()); RENDER_TARGET_POOL_SET_NAME(newTemporal, "SSR.TemporalSSR"); if (resetHistory) { context->Draw(newTemporal, resolveBuffer); } else { context->SetRenderTarget(newTemporal->View()); context->BindSR(TEXTURE0, resolveBuffer); context->BindSR(TEXTURE1, buffers->TemporalSSR); context->BindSR(TEXTURE2, buffers->MotionVectors && buffers->MotionVectors->IsAllocated() ? buffers->MotionVectors->View() : nullptr); context->SetState(_psTemporalPass); context->DrawFullscreenTriangle(); } context->ResetRenderTarget(); context->UnBindSR(TEXTURE1); context->UnBindSR(TEXTURE2); RenderTargetPool::Release(resolveBuffer); context->CopyResource(buffers->TemporalSSR, newTemporal); reflectionsBuffer = newTemporal; } return reflectionsBuffer; } #if COMPILE_WITH_DEV_ENV void ScreenSpaceReflectionsPass::OnShaderReloading(Asset* obj) { _psCombinePass->ReleaseGPU(); _psTemporalPass->ReleaseGPU(); _psRayTracePass.Release(); _psResolvePass.Release(); invalidateResources(); } #endif