From d2b80bf41953aa16cbf8e48d833254ab740888c0 Mon Sep 17 00:00:00 2001 From: Anand-Ramnarain-27 <142887960+Anand-Ramnarain-27@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:54:39 +0200 Subject: [PATCH 01/14] Add HDR post-process (bloom, LUT, tonemap) Introduce a new HDR post-processing pipeline: adds BloomPass and PostProcessPass with supporting HLSL shaders (bloom downsample/upsample/threshold, postprocess) and a CubeLut loader for 3D LUT color grading. Integrates an HDR render target (R16G16B16A16_FLOAT) into RenderSurface and ModuleResources, updates PSOs (mesh & particles) to render to HDR, and wires bloom/LUT into ModuleRender so the scene is rendered to an HDR target, blurred, color graded and resolved to the LDR output. Also: adjust shaders to output linear HDR (LightPixelShader), simplify particle tinting, add project file entries and filters, and include editor_windows.ini. Adds helper Texture and LUT creation utilities and pipeline/root-signature setup for the new passes. --- .../BloomDownsamplePixelShader.hlsl | 17 ++ Engine_Master_UPC/BloomPass.cpp | 172 ++++++++++++++++++ Engine_Master_UPC/BloomPass.h | 48 +++++ .../BloomThresholdPixelShader.hlsl | 20 ++ .../BloomUpsamplePixelShader.hlsl | 20 ++ Engine_Master_UPC/CubeLut.cpp | 110 +++++++++++ Engine_Master_UPC/CubeLut.h | 13 ++ Engine_Master_UPC/Engine.vcxproj | 43 +++++ Engine_Master_UPC/Engine.vcxproj.filters | 44 +++++ Engine_Master_UPC/LightPixelShader.hlsl | 10 +- Engine_Master_UPC/MeshRendererPass.cpp | 2 +- Engine_Master_UPC/ModuleRender.cpp | 111 ++++++++--- Engine_Master_UPC/ModuleRender.h | 18 +- Engine_Master_UPC/ModuleResources.cpp | 20 ++ Engine_Master_UPC/ModuleResources.h | 1 + Engine_Master_UPC/ParticlePixelShader.hlsl | 4 +- Engine_Master_UPC/ParticlesPass.cpp | 2 +- Engine_Master_UPC/PostProcessPass.cpp | 161 ++++++++++++++++ Engine_Master_UPC/PostProcessPass.h | 56 ++++++ Engine_Master_UPC/PostProcessPixelShader.hlsl | 61 +++++++ Engine_Master_UPC/PostProcessSettings.h | 37 ++++ Engine_Master_UPC/Scene.cpp | 4 + Engine_Master_UPC/Scene.h | 4 + Engine_Master_UPC/SceneConfig.cpp | 33 ++++ Engine_Master_UPC/SceneConfig.h | 2 + Engine_Master_UPC/SkyboxPass.cpp | 2 +- Engine_Master_UPC/SwapChain.cpp | 4 + Engine_Master_UPC/Texture.cpp | 20 +- Engine_Master_UPC/Texture.h | 1 + Engine_Master_UPC/editor_windows.ini | 10 + 30 files changed, 1004 insertions(+), 46 deletions(-) create mode 100644 Engine_Master_UPC/BloomDownsamplePixelShader.hlsl create mode 100644 Engine_Master_UPC/BloomPass.cpp create mode 100644 Engine_Master_UPC/BloomPass.h create mode 100644 Engine_Master_UPC/BloomThresholdPixelShader.hlsl create mode 100644 Engine_Master_UPC/BloomUpsamplePixelShader.hlsl create mode 100644 Engine_Master_UPC/CubeLut.cpp create mode 100644 Engine_Master_UPC/CubeLut.h create mode 100644 Engine_Master_UPC/PostProcessPass.cpp create mode 100644 Engine_Master_UPC/PostProcessPass.h create mode 100644 Engine_Master_UPC/PostProcessPixelShader.hlsl create mode 100644 Engine_Master_UPC/PostProcessSettings.h create mode 100644 Engine_Master_UPC/editor_windows.ini diff --git a/Engine_Master_UPC/BloomDownsamplePixelShader.hlsl b/Engine_Master_UPC/BloomDownsamplePixelShader.hlsl new file mode 100644 index 00000000..cde87e1d --- /dev/null +++ b/Engine_Master_UPC/BloomDownsamplePixelShader.hlsl @@ -0,0 +1,17 @@ +Texture2D inputTexture : register(t0); +SamplerState bilinearClamp : register(s0); + +float4 main(float2 uv : TEXCOORD) : SV_TARGET +{ + float2 texSize; + inputTexture.GetDimensions(texSize.x, texSize.y); + float2 halfPixel = 0.5 / texSize; + + float4 sum = inputTexture.Sample(bilinearClamp, uv) * 4.0; + sum += inputTexture.Sample(bilinearClamp, uv + float2(-halfPixel.x, halfPixel.y)); + sum += inputTexture.Sample(bilinearClamp, uv + float2( halfPixel.x, halfPixel.y)); + sum += inputTexture.Sample(bilinearClamp, uv + float2(-halfPixel.x, -halfPixel.y)); + sum += inputTexture.Sample(bilinearClamp, uv + float2( halfPixel.x, -halfPixel.y)); + + return sum / 8.0; +} diff --git a/Engine_Master_UPC/BloomPass.cpp b/Engine_Master_UPC/BloomPass.cpp new file mode 100644 index 00000000..f90aba2f --- /dev/null +++ b/Engine_Master_UPC/BloomPass.cpp @@ -0,0 +1,172 @@ +#include "Globals.h" +#include "BloomPass.h" + +#include "RenderContext.h" + +#include "Application.h" +#include "ModuleDescriptors.h" +#include "ModuleScene.h" + +#include "Scene.h" +#include "PostProcessSettings.h" +#include "Texture.h" +#include "UID.h" + +#include +#include +#include +#include + +BloomPass::BloomPass(ComPtr device) : m_device(device) +{ + CD3DX12_DESCRIPTOR_RANGE srvRange; + srvRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0, 0); + + CD3DX12_ROOT_PARAMETER rootParameters[2] = {}; + rootParameters[0].InitAsConstants(sizeof(BloomParams) / sizeof(UINT32), 0, 0, D3D12_SHADER_VISIBILITY_PIXEL); + rootParameters[1].InitAsDescriptorTable(1, &srvRange, D3D12_SHADER_VISIBILITY_PIXEL); + + D3D12_STATIC_SAMPLER_DESC sampler = {}; + sampler.Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR; + sampler.AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + sampler.AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + sampler.AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + sampler.MaxLOD = D3D12_FLOAT32_MAX; + sampler.ShaderRegister = 0; + sampler.RegisterSpace = 0; + sampler.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + + CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc; + rootSignatureDesc.Init(_countof(rootParameters), rootParameters, 1, &sampler, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT); + + ComPtr signature; + ComPtr error; + DXCall(D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error)); + DXCall(m_device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&m_rootSignature))); + + m_thresholdPSO = buildPSO(L"BloomThresholdPixelShader.cso", /*additiveBlend=*/false); + m_downsamplePSO = buildPSO(L"BloomDownsamplePixelShader.cso", /*additiveBlend=*/false); + m_upsamplePSO = buildPSO(L"BloomUpsamplePixelShader.cso", /*additiveBlend=*/true); + + for (int i = 0; i < LEVELS; ++i) + { + m_width[i] = std::max(1u, BASE_WIDTH >> i); + m_height[i] = std::max(1u, BASE_HEIGHT >> i); + + TextureDesc desc{}; + desc.format = DXGI_FORMAT_R16G16B16A16_FLOAT; + desc.width = m_width[i]; + desc.height = m_height[i]; + desc.mipLevels = 1; + desc.views = TextureView::SRV | TextureView::RTV; + desc.initialState = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; + desc.shaderVisibleSRV = true; + + m_chain[i] = std::make_shared(GenerateUID(), *m_device.Get(), desc); + m_chain[i]->setName(L"BloomChain"); + } +} + +ComPtr BloomPass::buildPSO(const wchar_t* pixelShaderCso, bool additiveBlend) +{ + ComPtr vertexShaderBlob; + ThrowIfFailed(D3DReadFileToBlob(L"BRDFVertexShader.cso", &vertexShaderBlob)); + + ComPtr pixelShaderBlob; + ThrowIfFailed(D3DReadFileToBlob(pixelShaderCso, &pixelShaderBlob)); + + D3D12_GRAPHICS_PIPELINE_STATE_DESC desc{}; + desc.InputLayout = { nullptr, 0 }; + desc.pRootSignature = m_rootSignature.Get(); + desc.VS = CD3DX12_SHADER_BYTECODE(vertexShaderBlob.Get()); + desc.PS = CD3DX12_SHADER_BYTECODE(pixelShaderBlob.Get()); + desc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT); + if (additiveBlend) + { + desc.BlendState.RenderTarget[0].BlendEnable = TRUE; + desc.BlendState.RenderTarget[0].SrcBlend = D3D12_BLEND_ONE; + desc.BlendState.RenderTarget[0].DestBlend = D3D12_BLEND_ONE; + desc.BlendState.RenderTarget[0].BlendOp = D3D12_BLEND_OP_ADD; + desc.BlendState.RenderTarget[0].SrcBlendAlpha = D3D12_BLEND_ONE; + desc.BlendState.RenderTarget[0].DestBlendAlpha = D3D12_BLEND_ONE; + desc.BlendState.RenderTarget[0].BlendOpAlpha = D3D12_BLEND_OP_ADD; + } + desc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT); + desc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; + desc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT); + desc.DepthStencilState.DepthEnable = FALSE; + desc.DepthStencilState.StencilEnable = FALSE; + desc.SampleMask = UINT_MAX; + desc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; + desc.NumRenderTargets = 1; + desc.RTVFormats[0] = DXGI_FORMAT_R16G16B16A16_FLOAT; + desc.SampleDesc = { 1, 0 }; + + ComPtr pso; + DXCall(m_device->CreateGraphicsPipelineState(&desc, IID_PPV_ARGS(&pso))); + return pso; +} + +void BloomPass::prepare(const RenderContext& ctx) +{ + (void)ctx; + + if (Scene* scene = app->getModuleScene()->getScene()) + { + m_params.threshold = scene->getPostProcessSettings().bloomThreshold; + } +} + +void BloomPass::renderLevel(ID3D12GraphicsCommandList4* commandList, ID3D12PipelineState* pso, D3D12_GPU_DESCRIPTOR_HANDLE inputSrv, int level) +{ + Texture* tex = m_chain[level].get(); + + D3D12_VIEWPORT viewport = { 0.0f, 0.0f, static_cast(m_width[level]), static_cast(m_height[level]), 0.0f, 1.0f }; + D3D12_RECT scissor = { 0, 0, static_cast(m_width[level]), static_cast(m_height[level]) }; + + CD3DX12_RESOURCE_BARRIER toRT = CD3DX12_RESOURCE_BARRIER::Transition(tex->getD3D12Resource().Get(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET); + commandList->ResourceBarrier(1, &toRT); + + commandList->SetPipelineState(pso); + commandList->SetGraphicsRootSignature(m_rootSignature.Get()); + + ID3D12DescriptorHeap* descriptorHeaps[] = { app->getModuleDescriptors()->getHeap(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV).getHeap() }; + commandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps); + + D3D12_CPU_DESCRIPTOR_HANDLE rtv = tex->getRTV(0).cpu; + commandList->OMSetRenderTargets(1, &rtv, FALSE, nullptr); + commandList->RSSetViewports(1, &viewport); + commandList->RSSetScissorRects(1, &scissor); + + commandList->SetGraphicsRoot32BitConstants(0, sizeof(BloomParams) / sizeof(UINT32), &m_params, 0); + commandList->SetGraphicsRootDescriptorTable(1, inputSrv); + + commandList->IASetVertexBuffers(0, 0, nullptr); + commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + commandList->DrawInstanced(3, 1, 0, 0); + + CD3DX12_RESOURCE_BARRIER toSRV = CD3DX12_RESOURCE_BARRIER::Transition(tex->getD3D12Resource().Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + commandList->ResourceBarrier(1, &toSRV); +} + +void BloomPass::apply(ID3D12GraphicsCommandList4* commandList, D3D12_GPU_DESCRIPTOR_HANDLE sceneHDRSrv) +{ + // Bright-pass: HDR scene -> chain[0]. + renderLevel(commandList, m_thresholdPSO.Get(), sceneHDRSrv, 0); + + // Downsample down the chain. + for (int i = 1; i < LEVELS; ++i) + { + renderLevel(commandList, m_downsamplePSO.Get(), m_chain[i - 1]->getSRV().gpu, i); + } + + for (int i = LEVELS - 2; i >= 0; --i) + { + renderLevel(commandList, m_upsamplePSO.Get(), m_chain[i + 1]->getSRV().gpu, i); + } +} + +D3D12_GPU_DESCRIPTOR_HANDLE BloomPass::getBloomSRV() const +{ + return m_chain[0]->getSRV().gpu; +} diff --git a/Engine_Master_UPC/BloomPass.h b/Engine_Master_UPC/BloomPass.h new file mode 100644 index 00000000..7fcdb5f7 --- /dev/null +++ b/Engine_Master_UPC/BloomPass.h @@ -0,0 +1,48 @@ +#pragma once +#include +#include +#include + +using Microsoft::WRL::ComPtr; + +struct RenderContext; +class Texture; + +class BloomPass +{ +public: + explicit BloomPass(ComPtr device); + + void prepare(const RenderContext& ctx); + void apply(ID3D12GraphicsCommandList4* commandList, D3D12_GPU_DESCRIPTOR_HANDLE sceneHDRSrv); + + D3D12_GPU_DESCRIPTOR_HANDLE getBloomSRV() const; + +private: + static constexpr int LEVELS = 5; + static constexpr uint32_t BASE_WIDTH = 640; + static constexpr uint32_t BASE_HEIGHT = 360; + + struct BloomParams + { + float threshold = 1.0f; + float pad0 = 0.0f; + float pad1 = 0.0f; + float pad2 = 0.0f; + }; + + ComPtr buildPSO(const wchar_t* pixelShaderCso, bool additiveBlend); + void renderLevel(ID3D12GraphicsCommandList4* commandList, ID3D12PipelineState* pso, D3D12_GPU_DESCRIPTOR_HANDLE inputSrv, int level); + + ComPtr m_device; + ComPtr m_rootSignature; + ComPtr m_thresholdPSO; + ComPtr m_downsamplePSO; + ComPtr m_upsamplePSO; + + std::shared_ptr m_chain[LEVELS]; + uint32_t m_width[LEVELS]{}; + uint32_t m_height[LEVELS]{}; + + BloomParams m_params; +}; diff --git a/Engine_Master_UPC/BloomThresholdPixelShader.hlsl b/Engine_Master_UPC/BloomThresholdPixelShader.hlsl new file mode 100644 index 00000000..42b8198c --- /dev/null +++ b/Engine_Master_UPC/BloomThresholdPixelShader.hlsl @@ -0,0 +1,20 @@ +Texture2D inputTexture : register(t0); +SamplerState bilinearClamp : register(s0); + +cbuffer BloomParams : register(b0) +{ + float threshold; + float pad0; + float pad1; + float pad2; +}; + +float4 main(float2 uv : TEXCOORD) : SV_TARGET +{ + float3 c = inputTexture.Sample(bilinearClamp, uv).rgb; + + float brightness = max(c.r, max(c.g, c.b)); + float contribution = max(brightness - threshold, 0.0) / max(brightness, 1e-5); + + return float4(c * contribution, 1.0); +} diff --git a/Engine_Master_UPC/BloomUpsamplePixelShader.hlsl b/Engine_Master_UPC/BloomUpsamplePixelShader.hlsl new file mode 100644 index 00000000..9860d994 --- /dev/null +++ b/Engine_Master_UPC/BloomUpsamplePixelShader.hlsl @@ -0,0 +1,20 @@ +Texture2D inputTexture : register(t0); +SamplerState bilinearClamp : register(s0); + +float4 main(float2 uv : TEXCOORD) : SV_TARGET +{ + float2 texSize; + inputTexture.GetDimensions(texSize.x, texSize.y); + float2 halfPixel = 0.5 / texSize; + + float4 sum = inputTexture.Sample(bilinearClamp, uv + float2(-halfPixel.x * 2.0, 0.0)); + sum += inputTexture.Sample(bilinearClamp, uv + float2(-halfPixel.x, halfPixel.y)) * 2.0; + sum += inputTexture.Sample(bilinearClamp, uv + float2(0.0, halfPixel.y * 2.0)); + sum += inputTexture.Sample(bilinearClamp, uv + float2( halfPixel.x, halfPixel.y)) * 2.0; + sum += inputTexture.Sample(bilinearClamp, uv + float2( halfPixel.x * 2.0, 0.0)); + sum += inputTexture.Sample(bilinearClamp, uv + float2( halfPixel.x, -halfPixel.y)) * 2.0; + sum += inputTexture.Sample(bilinearClamp, uv + float2(0.0, -halfPixel.y * 2.0)); + sum += inputTexture.Sample(bilinearClamp, uv + float2(-halfPixel.x, -halfPixel.y)) * 2.0; + + return sum / 12.0; +} diff --git a/Engine_Master_UPC/CubeLut.cpp b/Engine_Master_UPC/CubeLut.cpp new file mode 100644 index 00000000..56f386cb --- /dev/null +++ b/Engine_Master_UPC/CubeLut.cpp @@ -0,0 +1,110 @@ +#include "Globals.h" +#include "CubeLut.h" + +#include "Texture.h" +#include "Application.h" +#include "ModuleResources.h" +#include "UID.h" + +#include +#include + +namespace +{ + // Creates the 3D texture and uploads the RGBA float data into it. + std::shared_ptr buildLutTexture(ID3D12Device4& device, int size, const std::vector& rgba) + { + TextureDesc desc{}; + desc.format = DXGI_FORMAT_R32G32B32A32_FLOAT; + desc.width = static_cast(size); + desc.height = static_cast(size); + desc.depth = static_cast(size); + desc.mipLevels = 1; + desc.views = TextureView::SRV; + desc.initialState = D3D12_RESOURCE_STATE_COPY_DEST; + desc.shaderVisibleSRV = true; + + auto tex = std::make_shared(GenerateUID(), device, desc); + tex->setName(L"ColorGradingLUT"); + + D3D12_SUBRESOURCE_DATA sub{}; + sub.pData = rgba.data(); + sub.RowPitch = static_cast(size) * sizeof(float) * 4; + sub.SlicePitch = sub.RowPitch * size; + + app->getModuleResources()->uploadTextureAndTransition(tex->getD3D12Resource().Get(), { sub }); + return tex; + } +} + +std::shared_ptr CubeLut::createIdentity(ID3D12Device4& device, int size) +{ + if (size < 2) size = 2; + + std::vector rgba(static_cast(size) * size * size * 4); + size_t idx = 0; + const float inv = 1.0f / static_cast(size - 1); + + for (int b = 0; b < size; ++b) + for (int g = 0; g < size; ++g) + for (int r = 0; r < size; ++r) + { + rgba[idx++] = r * inv; + rgba[idx++] = g * inv; + rgba[idx++] = b * inv; + rgba[idx++] = 1.0f; + } + + return buildLutTexture(device, size, rgba); +} + +std::shared_ptr CubeLut::load(ID3D12Device4& device, const std::string& path) +{ + FILE* file = nullptr; + fopen_s(&file, path.c_str(), "r"); + if (!file) + { + DEBUG_ERROR("CubeLut: cannot open '%s'", path.c_str()); + return nullptr; + } + + int size = 0; + std::vector rgba; + size_t writeIdx = 0; + char line[256]; + + while (fgets(line, sizeof(line), file)) + { + if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') + continue; + + int parsedSize = 0; + if (size == 0 && sscanf_s(line, "LUT_3D_SIZE %d", &parsedSize) == 1 && parsedSize > 0) + { + size = parsedSize; + rgba.assign(static_cast(size) * size * size * 4, 1.0f); + continue; + } + + if (size > 0) + { + float r = 0.0f, g = 0.0f, b = 0.0f; + if (sscanf_s(line, "%f %f %f", &r, &g, &b) == 3 && writeIdx + 4 <= rgba.size()) + { + rgba[writeIdx++] = r; + rgba[writeIdx++] = g; + rgba[writeIdx++] = b; + rgba[writeIdx++] = 1.0f; + } + } + } + fclose(file); + + if (size == 0 || writeIdx != rgba.size()) + { + DEBUG_ERROR("CubeLut: malformed or incomplete .CUBE file '%s'", path.c_str()); + return nullptr; + } + + return buildLutTexture(device, size, rgba); +} diff --git a/Engine_Master_UPC/CubeLut.h b/Engine_Master_UPC/CubeLut.h new file mode 100644 index 00000000..0c495267 --- /dev/null +++ b/Engine_Master_UPC/CubeLut.h @@ -0,0 +1,13 @@ +#pragma once +#include +#include +#include + +class Texture; + +namespace CubeLut +{ + std::shared_ptr load(ID3D12Device4& device, const std::string& path); + + std::shared_ptr createIdentity(ID3D12Device4& device, int size); +} diff --git a/Engine_Master_UPC/Engine.vcxproj b/Engine_Master_UPC/Engine.vcxproj index 7459d193..8c2f9019 100644 --- a/Engine_Master_UPC/Engine.vcxproj +++ b/Engine_Master_UPC/Engine.vcxproj @@ -416,6 +416,10 @@ $(SolutionDir)3rdParty\RecastNavigation\DebugUtils\Include; + + + + @@ -805,6 +809,9 @@ $(SolutionDir)3rdParty\RecastNavigation\DebugUtils\Include; + + + @@ -982,6 +989,42 @@ $(SolutionDir)3rdParty\RecastNavigation\DebugUtils\Include; Pixel 6.0 + + Pixel + 6.0 + -Qembed_debug %(AdditionalOptions) + Pixel + 6.0 + Pixel + 6.0 + + + Pixel + 6.0 + -Qembed_debug %(AdditionalOptions) + Pixel + 6.0 + Pixel + 6.0 + + + Pixel + 6.0 + -Qembed_debug %(AdditionalOptions) + Pixel + 6.0 + Pixel + 6.0 + + + Pixel + 6.0 + -Qembed_debug %(AdditionalOptions) + Pixel + 6.0 + Pixel + 6.0 + Pixel Pixel diff --git a/Engine_Master_UPC/Engine.vcxproj.filters b/Engine_Master_UPC/Engine.vcxproj.filters index 635a2489..374a15e1 100644 --- a/Engine_Master_UPC/Engine.vcxproj.filters +++ b/Engine_Master_UPC/Engine.vcxproj.filters @@ -1720,4 +1720,48 @@ + + + {b7e2f1a4-9c3d-4f8a-b6e1-5a2c8d0f3e91} + + + + + D3D12\Rendering\PostProcessing + + + D3D12\Rendering\PostProcessing + + + D3D12\Rendering\PostProcessing + + + + + D3D12\Rendering\PostProcessing + + + D3D12\Rendering\PostProcessing + + + D3D12\Rendering\PostProcessing + + + D3D12\Rendering\PostProcessing + + + + + D3D12\Rendering\PostProcessing + + + D3D12\Rendering\PostProcessing + + + D3D12\Rendering\PostProcessing + + + D3D12\Rendering\PostProcessing + + \ No newline at end of file diff --git a/Engine_Master_UPC/LightPixelShader.hlsl b/Engine_Master_UPC/LightPixelShader.hlsl index b18126da..8324bf22 100644 --- a/Engine_Master_UPC/LightPixelShader.hlsl +++ b/Engine_Master_UPC/LightPixelShader.hlsl @@ -350,11 +350,11 @@ float4 main(float3 worldPos : POSITION, float3 normal : NORMAL, float3 tangent : - //Calculate final color - float3 colorMapped = PBRNeutralToneMapping(directLighting + indirectLighting + emissive); - float3 finalColor = LinearToSRGB(colorMapped); + //Output linear HDR colour. Exposure, tone mapping and gamma correction are + //applied later by the post-process pass. + float3 finalColor = directLighting + indirectLighting + emissive; + + - - return float4(finalColor, 1.0f); } \ No newline at end of file diff --git a/Engine_Master_UPC/MeshRendererPass.cpp b/Engine_Master_UPC/MeshRendererPass.cpp index 9dd08535..982da783 100644 --- a/Engine_Master_UPC/MeshRendererPass.cpp +++ b/Engine_Master_UPC/MeshRendererPass.cpp @@ -113,7 +113,7 @@ MeshRendererPass::MeshRendererPass(ComPtr device): m_device(devic psoDesc.SampleMask = UINT_MAX; psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; psoDesc.NumRenderTargets = 1; - psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; + psoDesc.RTVFormats[0] = DXGI_FORMAT_R16G16B16A16_FLOAT; psoDesc.SampleDesc = { 1,0 }; DXCall(m_device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&m_pipelineState))); diff --git a/Engine_Master_UPC/ModuleRender.cpp b/Engine_Master_UPC/ModuleRender.cpp index 789583d7..50f80737 100644 --- a/Engine_Master_UPC/ModuleRender.cpp +++ b/Engine_Master_UPC/ModuleRender.cpp @@ -33,6 +33,9 @@ #include "StaticTexturesPass.h" #include "SkinningComputePass.h" #include "ShadowMapPass.h" +#include "PostProcessPass.h" +#include "BloomPass.h" +#include "PostProcessSettings.h" #include "Quadtree.h" #include "RenderContext.h" #include "WindowSceneEditor.h" @@ -71,12 +74,17 @@ bool ModuleRender::init() auto skyBoxPass = std::make_unique(device, app->getModuleScene()->getScene()->getSkyBoxSettings()); m_skyBoxPass = skyBoxPass.get(); - m_renderPasses.push_back(std::move(skyBoxPass)); - m_renderPasses.push_back(std::unique_ptr(m_meshRenderPass)); - m_renderPasses.push_back(std::make_unique(device)); - m_renderPasses.push_back(std::move(debugDrawPass)); - m_renderPasses.push_back(std::make_unique(device)); - m_renderPasses.push_back(std::make_unique(device)); + m_scenePasses.push_back(std::move(skyBoxPass)); + m_scenePasses.push_back(std::unique_ptr(m_meshRenderPass)); + m_scenePasses.push_back(std::make_unique(device)); + + // Overlay passes draw into the LDR target after post-processing. + m_overlayPasses.push_back(std::move(debugDrawPass)); + m_overlayPasses.push_back(std::make_unique(device)); + m_overlayPasses.push_back(std::make_unique(device)); + + m_bloomPass = std::make_unique(device); + m_postProcessPass = std::make_unique(device); // ImGui lives outside the pass list because startFrame() / apply() must // bracket the entire editor render, not just the scene render. @@ -114,17 +122,17 @@ void ModuleRender::preRender() PERF_RENDER("ModuleRender::RenderViewports"); for (const ViewportEntry& entry : m_viewports) { - renderToSurface(commandList, *entry.surface, [&](D3D12_CPU_DESCRIPTOR_HANDLE rtv, D3D12_CPU_DESCRIPTOR_HANDLE dsv) + renderToSurface(commandList, *entry.surface, [&](const RenderSurface& surface) { if (entry.type == ViewportType::EDITOR) { PERF_RENDER("ModuleRender::RenderEditorScene"); - renderEditorScene(commandList, rtv, dsv, entry.width, entry.height); + renderEditorScene(commandList, surface, entry.width, entry.height); } else { PERF_RENDER("ModuleRender::RenderPlayScene"); - renderPlayScene(commandList, rtv, dsv, entry.width, entry.height); + renderPlayScene(commandList, surface, entry.width, entry.height); } }); } @@ -156,8 +164,7 @@ void ModuleRender::render() #else renderGameToBackbuffer(commandList, - swapChain->getRenderSurface().getTexture(RenderSurface::COLOR_0)->getRTV().cpu, - swapChain->getRenderSurface().getTexture(RenderSurface::DEPTH_STENCIL)->getDSV().cpu, + swapChain->getRenderSurface(), swapChain->getViewport(), swapChain->getScissorRect()); @@ -179,8 +186,11 @@ bool ModuleRender::cleanUp() m_shadowMapPass.reset(); m_skinningComputePass.reset(); + m_postProcessPass.reset(); + m_bloomPass.reset(); - m_renderPasses.clear(); + m_scenePasses.clear(); + m_overlayPasses.clear(); m_meshRenderPass = nullptr; m_debugDrawPass = nullptr; @@ -232,14 +242,14 @@ D3D12_GPU_VIRTUAL_ADDRESS ModuleRender::allocateInRingBuffer(const void* data, s return m_ringBuffer->allocate(data, size, app->getModuleD3D12()->getCurrentFrame()); } -void ModuleRender::renderToSurface(ID3D12GraphicsCommandList4* commandList,RenderSurface& surface,std::function renderFunc) +void ModuleRender::renderToSurface(ID3D12GraphicsCommandList4* commandList,RenderSurface& surface,std::function renderFunc) { + // final destination of post-processing + overlays. auto colorTex = surface.getTexture(RenderSurface::COLOR_0); - auto depthTex = surface.getTexture(RenderSurface::DEPTH_STENCIL); transitionResource(commandList,colorTex->getD3D12Resource(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET); - renderFunc(colorTex->getRTV(0).cpu, depthTex->getDSV().cpu); + renderFunc(surface); transitionResource(commandList, colorTex->getD3D12Resource(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); } @@ -288,10 +298,18 @@ void ModuleRender::transitionResource( ComPtr command commandList->ResourceBarrier(1, &barrier); } -void ModuleRender::renderScene(ID3D12GraphicsCommandList4* commandList, const RenderCamera& camera, D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle, D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle, D3D12_VIEWPORT viewport, D3D12_RECT scissorRect, bool renderDebug, RenderViewType viewType) +void ModuleRender::renderScene(ID3D12GraphicsCommandList4* commandList, const RenderCamera& camera, const RenderSurface& surface, D3D12_VIEWPORT viewport, D3D12_RECT scissorRect, bool renderDebug, RenderViewType viewType) { PERF_RENDER(renderDebug ? "ModuleRender::renderScene(Editor)" : "ModuleRender::renderScene(Game)"); + auto hdrTex = surface.getTexture(RenderSurface::COLOR_1); + auto ldrTex = surface.getTexture(RenderSurface::COLOR_0); + auto depthTex = surface.getTexture(RenderSurface::DEPTH_STENCIL); + + const D3D12_CPU_DESCRIPTOR_HANDLE hdrRtv = hdrTex->getRTV(0).cpu; + const D3D12_CPU_DESCRIPTOR_HANDLE ldrRtv = ldrTex->getRTV(0).cpu; + const D3D12_CPU_DESCRIPTOR_HANDLE dsv = depthTex->getDSV().cpu; + RenderContext ctx{ .view = camera.view, .projection = camera.projection, @@ -336,23 +354,60 @@ void ModuleRender::renderScene(ID3D12GraphicsCommandList4* commandList, const Re } } + //Render the scene into the HDR target. + transitionResource(commandList, hdrTex->getD3D12Resource(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET); + { PERF_RENDER("ModuleRender::renderScene::Background"); - renderBackground(commandList, rtvHandle, dsvHandle, viewport, scissorRect); + renderBackground(commandList, hdrRtv, dsv, viewport, scissorRect); } - { - PERF_RENDER("ModuleRender::renderScene::PreparePasses"); - for (auto& pass : m_renderPasses) + PERF_RENDER("ModuleRender::renderScene::ScenePasses"); + for (auto& pass : m_scenePasses) { pass->prepare(ctx); } + for (auto& pass : m_scenePasses) + { + pass->apply(commandList); + } } + transitionResource(commandList, hdrTex->getD3D12Resource(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + + //blur the bright areas of the HDR scene. + const bool bloomEnabled = app->getModuleScene()->getScene()->getPostProcessSettings().bloomEnabled; + if (bloomEnabled && m_bloomPass) + { + PERF_RENDER("ModuleRender::renderScene::Bloom"); + m_bloomPass->prepare(ctx); + m_bloomPass->apply(commandList, hdrTex->getSRV().gpu); + m_postProcessPass->setBloomSource(m_bloomPass->getBloomSRV(), true); + } + + // Resolve HDR -> LDR into COLOR_0. { - PERF_RENDER("ModuleRender::renderScene::ApplyPasses"); - for (auto& pass : m_renderPasses) + PERF_RENDER("ModuleRender::renderScene::PostProcess"); + if (m_postProcessPass) + { + m_postProcessPass->prepare(ctx); + m_postProcessPass->apply(commandList, hdrTex->getSRV().gpu, ldrRtv, viewport, scissorRect); + } + } + + //Overlays draw on top of the resolved image. + { + PERF_RENDER("ModuleRender::renderScene::OverlayPasses"); + commandList->OMSetRenderTargets(1, &ldrRtv, FALSE, &dsv); + commandList->RSSetViewports(1, &viewport); + commandList->RSSetScissorRects(1, &scissorRect); + + for (auto& pass : m_overlayPasses) + { + pass->prepare(ctx); + } + for (auto& pass : m_overlayPasses) { pass->apply(commandList); } @@ -371,15 +426,15 @@ void ModuleRender::renderBackground(ID3D12GraphicsCommandList4* commandList, D3D } #pragma region Wrappers -void ModuleRender::renderEditorScene(ID3D12GraphicsCommandList4* commandList, D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle, D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle, float width, float height) +void ModuleRender::renderEditorScene(ID3D12GraphicsCommandList4* commandList, const RenderSurface& surface, float width, float height) { D3D12_VIEWPORT viewport = { 0, 0, width, height, 0, 1 }; D3D12_RECT scissorRect = { 0, 0, static_cast(width), static_cast(height) }; - renderScene(commandList, getEditorCamera(), rtvHandle, dsvHandle, viewport, scissorRect, /*debug=*/true, RenderViewType::Editor); + renderScene(commandList, getEditorCamera(), surface, viewport, scissorRect, /*debug=*/true, RenderViewType::Editor); } -void ModuleRender::renderPlayScene(ID3D12GraphicsCommandList4* commandList, D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle, D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle, float width, float height) +void ModuleRender::renderPlayScene(ID3D12GraphicsCommandList4* commandList, const RenderSurface& surface, float width, float height) { const RenderCamera camera = getGameCamera(); if (!camera.valid) return; @@ -387,15 +442,15 @@ void ModuleRender::renderPlayScene(ID3D12GraphicsCommandList4* commandList, D3D1 D3D12_VIEWPORT viewport = { 0, 0, width, height, 0, 1 }; D3D12_RECT scissorRect = { 0, 0, static_cast(width), static_cast(height) }; - renderScene(commandList, camera, rtvHandle, dsvHandle, viewport, scissorRect, m_moduleGameView->getShowDebugWindow(), RenderViewType::Game); + renderScene(commandList, camera, surface, viewport, scissorRect, m_moduleGameView->getShowDebugWindow(), RenderViewType::Game); } -void ModuleRender::renderGameToBackbuffer(ID3D12GraphicsCommandList4* commandList, D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle, D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle, D3D12_VIEWPORT viewport, D3D12_RECT scissorRect) +void ModuleRender::renderGameToBackbuffer(ID3D12GraphicsCommandList4* commandList, const RenderSurface& surface, D3D12_VIEWPORT viewport, D3D12_RECT scissorRect) { const RenderCamera camera = getGameCamera(); if (!camera.valid) return; - renderScene(commandList, camera, rtvHandle, dsvHandle, viewport, scissorRect, m_moduleGameView->getShowDebugWindow(), RenderViewType::Game); + renderScene(commandList, camera, surface, viewport, scissorRect, m_moduleGameView->getShowDebugWindow(), RenderViewType::Game); } #pragma endregion diff --git a/Engine_Master_UPC/ModuleRender.h b/Engine_Master_UPC/ModuleRender.h index f935afef..768664e4 100644 --- a/Engine_Master_UPC/ModuleRender.h +++ b/Engine_Master_UPC/ModuleRender.h @@ -11,6 +11,8 @@ #include "RenderViewType.h" #include "SkinningComputePass.h" #include "ShadowMapPass.h" +#include "PostProcessPass.h" +#include "BloomPass.h" using Microsoft::WRL::ComPtr; @@ -57,8 +59,10 @@ class ModuleRender : public Module RingBuffer* m_ringBuffer = nullptr; - // Ordered list of passes that are called every frame. - std::vector> m_renderPasses; + std::vector> m_scenePasses; + std::vector> m_overlayPasses; + std::unique_ptr m_bloomPass; + std::unique_ptr m_postProcessPass; // ImGui straddles the frame (startFrame / apply), so it lives separately. std::unique_ptr m_imGuiPass; @@ -99,14 +103,14 @@ class ModuleRender : public Module void markDebugDrawCacheDirty(); private: - void renderToSurface( ID3D12GraphicsCommandList4* commandList, RenderSurface& surface, std::function renderFunc); + void renderToSurface( ID3D12GraphicsCommandList4* commandList, RenderSurface& surface, std::function renderFunc); // Scene rendering - void renderScene( ID3D12GraphicsCommandList4* commandList, const RenderCamera& camera, D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle, D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle, D3D12_VIEWPORT viewport, D3D12_RECT scissorRect, bool renderDebug, RenderViewType viewType); + void renderScene( ID3D12GraphicsCommandList4* commandList, const RenderCamera& camera, const RenderSurface& surface, D3D12_VIEWPORT viewport, D3D12_RECT scissorRect, bool renderDebug, RenderViewType viewType); void renderBackground(ID3D12GraphicsCommandList4* commandList, D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle, D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle, D3D12_VIEWPORT viewport, D3D12_RECT scissorRect); - void renderEditorScene(ID3D12GraphicsCommandList4* commandList, D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle, D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle, float width, float height); - void renderPlayScene(ID3D12GraphicsCommandList4* commandList, D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle, D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle,float width, float height); - void renderGameToBackbuffer( ID3D12GraphicsCommandList4* commandList, D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle, D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle, D3D12_VIEWPORT viewport, D3D12_RECT scissorRect); + void renderEditorScene(ID3D12GraphicsCommandList4* commandList, const RenderSurface& surface, float width, float height); + void renderPlayScene(ID3D12GraphicsCommandList4* commandList, const RenderSurface& surface, float width, float height); + void renderGameToBackbuffer( ID3D12GraphicsCommandList4* commandList, const RenderSurface& surface, D3D12_VIEWPORT viewport, D3D12_RECT scissorRect); // Camera helpers RenderCamera getEditorCamera(); diff --git a/Engine_Master_UPC/ModuleResources.cpp b/Engine_Master_UPC/ModuleResources.cpp index 9e28bce9..616dafd9 100644 --- a/Engine_Master_UPC/ModuleResources.cpp +++ b/Engine_Master_UPC/ModuleResources.cpp @@ -182,15 +182,35 @@ Texture* ModuleResources::createRenderTexture(float width, float height) return new Texture(GenerateUID(), *m_device.Get(), desc); } +Texture* ModuleResources::createHDRRenderTexture(float width, float height) +{ + TextureDesc desc{}; + desc.format = DXGI_FORMAT_R16G16B16A16_FLOAT; + desc.srvFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; + desc.rtvFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; + desc.width = static_cast(width); + desc.height = static_cast(height); + desc.views = TextureView::SRV | TextureView::RTV; + desc.initialState = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; + desc.hasClearValue = true; + desc.clearValue = CD3DX12_CLEAR_VALUE(DXGI_FORMAT_R16G16B16A16_FLOAT, Color(0.0f, 0.2f, 0.4f, 1.0f)); + desc.shaderVisibleSRV = true; + + return new Texture(GenerateUID(), *m_device.Get(), desc); +} + RenderSurface* ModuleResources::createRenderSurface(float width, float height) { auto surface = new RenderSurface(); auto colorTex = std::shared_ptr(app->getModuleResources()->createRenderTexture(width, height)); colorTex->setName(L"RenderSurface_Color"); + auto hdrTex = std::shared_ptr(app->getModuleResources()->createHDRRenderTexture(width, height)); + hdrTex->setName(L"RenderSurface_SceneHDR"); auto depthTex = std::shared_ptr(app->getModuleResources()->createDepthBuffer(width, height)); depthTex->setName(L"RenderSurface_Depth"); surface->attachTexture(RenderSurface::COLOR_0, colorTex); + surface->attachTexture(RenderSurface::COLOR_1, hdrTex); surface->attachTexture(RenderSurface::DEPTH_STENCIL, depthTex); return surface; diff --git a/Engine_Master_UPC/ModuleResources.h b/Engine_Master_UPC/ModuleResources.h index df6a6421..238f8a06 100644 --- a/Engine_Master_UPC/ModuleResources.h +++ b/Engine_Master_UPC/ModuleResources.h @@ -57,6 +57,7 @@ class ModuleResources : public Module Texture* createDepthBuffer(float width, float height); Texture* createShadowMap(uint32_t size); Texture* createRenderTexture(float width, float height); + Texture* createHDRRenderTexture(float width, float height); RenderSurface* createRenderSurface(float width, float height); static constexpr const char* NULL_TEXTURE_HASH = "__NULL_TEXTURE__"; diff --git a/Engine_Master_UPC/ParticlePixelShader.hlsl b/Engine_Master_UPC/ParticlePixelShader.hlsl index f3d84188..663c4989 100644 --- a/Engine_Master_UPC/ParticlePixelShader.hlsl +++ b/Engine_Master_UPC/ParticlePixelShader.hlsl @@ -39,7 +39,7 @@ float4 main(PSInput input) : SV_TARGET clip(texColor.a - 0.001f); float4 particleColorInfo = instanceDataBuffer[input.instanceID].colorAndAlpha; - float3 resultingColor = LinearToSRGB(Tint(texColor.rgb, particleColorInfo.rgb) ); - + float3 resultingColor = Tint(texColor.rgb, particleColorInfo.rgb); + return float4(resultingColor, texColor.a * particleColorInfo.a); } \ No newline at end of file diff --git a/Engine_Master_UPC/ParticlesPass.cpp b/Engine_Master_UPC/ParticlesPass.cpp index 72095996..684afcc2 100644 --- a/Engine_Master_UPC/ParticlesPass.cpp +++ b/Engine_Master_UPC/ParticlesPass.cpp @@ -78,7 +78,7 @@ ParticlesPass::ParticlesPass(ComPtr device) psoDesc.SampleMask = UINT_MAX; psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; psoDesc.NumRenderTargets = 1; - psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; + psoDesc.RTVFormats[0] = DXGI_FORMAT_R16G16B16A16_FLOAT; // HDR scene target psoDesc.SampleDesc = { 1, 0 }; DXCall(m_device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&m_pipelineState))); diff --git a/Engine_Master_UPC/PostProcessPass.cpp b/Engine_Master_UPC/PostProcessPass.cpp new file mode 100644 index 00000000..d322ff02 --- /dev/null +++ b/Engine_Master_UPC/PostProcessPass.cpp @@ -0,0 +1,161 @@ +#include "Globals.h" +#include "PostProcessPass.h" + +#include "RenderContext.h" + +#include "Application.h" +#include "ModuleDescriptors.h" +#include "ModuleScene.h" + +#include "Scene.h" +#include "PostProcessSettings.h" +#include "Texture.h" +#include "CubeLut.h" +#include "UID.h" + +#include +#include +#include + +PostProcessPass::PostProcessPass(ComPtr device) : m_device(device) +{ + // Root signature: b0 (params) + t0 (HDR scene) + t1 (bloom) + t2 (LUT 3D), + CD3DX12_DESCRIPTOR_RANGE sceneRange, bloomRange, lutRange; + sceneRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0, 0); + bloomRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 1, 0); + lutRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 2, 0); + + CD3DX12_ROOT_PARAMETER rootParameters[4] = {}; + rootParameters[0].InitAsConstants(sizeof(PostProcessParams) / sizeof(UINT32), 0, 0, D3D12_SHADER_VISIBILITY_PIXEL); + rootParameters[1].InitAsDescriptorTable(1, &sceneRange, D3D12_SHADER_VISIBILITY_PIXEL); + rootParameters[2].InitAsDescriptorTable(1, &bloomRange, D3D12_SHADER_VISIBILITY_PIXEL); + rootParameters[3].InitAsDescriptorTable(1, &lutRange, D3D12_SHADER_VISIBILITY_PIXEL); + + D3D12_STATIC_SAMPLER_DESC sampler = {}; + sampler.Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR; + sampler.AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + sampler.AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + sampler.AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + sampler.MaxLOD = D3D12_FLOAT32_MAX; + sampler.ShaderRegister = 0; + sampler.RegisterSpace = 0; + sampler.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + + CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc; + rootSignatureDesc.Init(_countof(rootParameters), rootParameters, 1, &sampler, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT); + + ComPtr signature; + ComPtr error; + DXCall(D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error)); + DXCall(m_device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&m_rootSignature))); + + // Reuse the shared fullscreen-triangle vertex shader. + ComPtr vertexShaderBlob; + ThrowIfFailed(D3DReadFileToBlob(L"BRDFVertexShader.cso", &vertexShaderBlob)); + + ComPtr pixelShaderBlob; + ThrowIfFailed(D3DReadFileToBlob(L"PostProcessPixelShader.cso", &pixelShaderBlob)); + + D3D12_GRAPHICS_PIPELINE_STATE_DESC desc{}; + desc.InputLayout = { nullptr, 0 }; + desc.pRootSignature = m_rootSignature.Get(); + desc.VS = CD3DX12_SHADER_BYTECODE(vertexShaderBlob.Get()); + desc.PS = CD3DX12_SHADER_BYTECODE(pixelShaderBlob.Get()); + desc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT); + desc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT); + desc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; + desc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT); + desc.DepthStencilState.DepthEnable = FALSE; + desc.DepthStencilState.StencilEnable = FALSE; + desc.SampleMask = UINT_MAX; + desc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; + desc.NumRenderTargets = 1; + desc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; + desc.SampleDesc = { 1, 0 }; + + DXCall(m_device->CreateGraphicsPipelineState(&desc, IID_PPV_ARGS(&m_pipelineState))); + + m_identityLut = CubeLut::createIdentity(*m_device.Get(), 2); + + TextureDesc dummyDesc{}; + dummyDesc.format = DXGI_FORMAT_R16G16B16A16_FLOAT; + dummyDesc.width = 1; + dummyDesc.height = 1; + dummyDesc.mipLevels = 1; + dummyDesc.views = TextureView::SRV; + dummyDesc.initialState = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; + dummyDesc.shaderVisibleSRV = true; + m_dummyTexture = std::make_shared(GenerateUID(), *m_device.Get(), dummyDesc); + m_dummyTexture->setName(L"PostProcess_DummyBloom"); +} + +void PostProcessPass::setBloomSource(D3D12_GPU_DESCRIPTOR_HANDLE bloomSrv, bool valid) +{ + m_bloomSrv = bloomSrv; + m_bloomValid = valid; +} + +void PostProcessPass::prepare(const RenderContext& ctx) +{ + (void)ctx; + + Scene* scene = app->getModuleScene()->getScene(); + if (!scene) + return; + + const PostProcessSettings& settings = scene->getPostProcessSettings(); + + m_params.exposure = settings.exposure; + + if (settings.lutEnabled && !settings.lutPath.empty()) + { + if (settings.lutPath != m_loadedLutPath) + { + m_lutTexture = CubeLut::load(*m_device.Get(), settings.lutPath); + m_loadedLutPath = settings.lutPath; + if (m_lutTexture) + m_lutSize = static_cast(m_lutTexture->getDesc().depth); + } + } + + const bool lutActive = settings.lutEnabled && m_lutTexture != nullptr; + m_params.enableLUT = lutActive ? 1u : 0u; + m_params.lutSize = static_cast(m_lutSize); + + m_params.enableCA = settings.chromaticAberrationEnabled ? 1u : 0u; + m_params.caStrength = settings.chromaticAberrationStrength; + + m_params.enableBloom = (settings.bloomEnabled && m_bloomValid) ? 1u : 0u; + m_params.bloomIntensity = settings.bloomIntensity; +} + +void PostProcessPass::apply(ID3D12GraphicsCommandList4* commandList, + D3D12_GPU_DESCRIPTOR_HANDLE sceneHDRSrv, + D3D12_CPU_DESCRIPTOR_HANDLE targetRTV, + const D3D12_VIEWPORT& viewport, + const D3D12_RECT& scissorRect) +{ + commandList->SetPipelineState(m_pipelineState.Get()); + commandList->SetGraphicsRootSignature(m_rootSignature.Get()); + + ID3D12DescriptorHeap* descriptorHeaps[] = { app->getModuleDescriptors()->getHeap(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV).getHeap() }; + commandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps); + + commandList->OMSetRenderTargets(1, &targetRTV, FALSE, nullptr); + commandList->RSSetViewports(1, &viewport); + commandList->RSSetScissorRects(1, &scissorRect); + + const D3D12_GPU_DESCRIPTOR_HANDLE bloomHandle = m_bloomValid ? m_bloomSrv : m_dummyTexture->getSRV().gpu; + const std::shared_ptr& lut = m_lutTexture ? m_lutTexture : m_identityLut; + + commandList->SetGraphicsRoot32BitConstants(0, sizeof(PostProcessParams) / sizeof(UINT32), &m_params, 0); + commandList->SetGraphicsRootDescriptorTable(1, sceneHDRSrv); + commandList->SetGraphicsRootDescriptorTable(2, bloomHandle); + commandList->SetGraphicsRootDescriptorTable(3, lut->getSRV().gpu); + + commandList->IASetVertexBuffers(0, 0, nullptr); + commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + commandList->DrawInstanced(3, 1, 0, 0); + + m_bloomValid = false; +} diff --git a/Engine_Master_UPC/PostProcessPass.h b/Engine_Master_UPC/PostProcessPass.h new file mode 100644 index 00000000..b399c6f2 --- /dev/null +++ b/Engine_Master_UPC/PostProcessPass.h @@ -0,0 +1,56 @@ +#pragma once +#include +#include +#include +#include + +using Microsoft::WRL::ComPtr; + +struct RenderContext; +class Texture; + +class PostProcessPass +{ +public: + explicit PostProcessPass(ComPtr device); + + void prepare(const RenderContext& ctx); + + void setBloomSource(D3D12_GPU_DESCRIPTOR_HANDLE bloomSrv, bool valid); + + void apply(ID3D12GraphicsCommandList4* commandList, + D3D12_GPU_DESCRIPTOR_HANDLE sceneHDRSrv, + D3D12_CPU_DESCRIPTOR_HANDLE targetRTV, + const D3D12_VIEWPORT& viewport, + const D3D12_RECT& scissorRect); + +private: + struct PostProcessParams + { + float exposure = 0.0f; + float bloomIntensity = 0.5f; + float lutSize = 2.0f; + float caStrength = 1.0f; + + uint32_t enableBloom = 0; + uint32_t enableLUT = 0; + uint32_t enableCA = 0; + uint32_t pad0 = 0; + }; + + ComPtr m_device; + ComPtr m_rootSignature; + ComPtr m_pipelineState; + + PostProcessParams m_params; + + std::shared_ptr m_lutTexture; + std::shared_ptr m_identityLut; + std::string m_loadedLutPath; + int m_lutSize = 2; + + std::shared_ptr m_dummyTexture; + + D3D12_GPU_DESCRIPTOR_HANDLE m_bloomSrv{}; + bool m_bloomValid = false; +}; diff --git a/Engine_Master_UPC/PostProcessPixelShader.hlsl b/Engine_Master_UPC/PostProcessPixelShader.hlsl new file mode 100644 index 00000000..1dd47d31 --- /dev/null +++ b/Engine_Master_UPC/PostProcessPixelShader.hlsl @@ -0,0 +1,61 @@ +#include "General.hlsli" +#include "PBRGeneral.hlsli" + +Texture2D sceneTexture : register(t0); +Texture2D bloomTexture : register(t1); +Texture3D lutTexture : register(t2); +SamplerState bilinearClamp : register(s0); + +cbuffer PostProcessParams : register(b0) +{ + float exposure; + float bloomIntensity; + float lutSize; + float caStrength; + + uint enableBloom; + uint enableLUT; + uint enableCA; + uint pad0; +}; + +float3 sampleScene(float2 uv) +{ + if (enableCA == 0) + return sceneTexture.Sample(bilinearClamp, uv).rgb; + + const float3 colourOffset = float3(0.015, 0.008, -0.008) * caStrength; + float2 dir = float2(0.5, 0.5) - uv; + + float3 c; + c.r = sceneTexture.Sample(bilinearClamp, uv + dir * colourOffset.r).r; + c.g = sceneTexture.Sample(bilinearClamp, uv + dir * colourOffset.g).g; + c.b = sceneTexture.Sample(bilinearClamp, uv + dir * colourOffset.b).b; + return c; +} + +float3 applyLUT(float3 color) +{ + float3 uvw = (saturate(color) * (lutSize - 1.0) + 0.5) / lutSize; + return lutTexture.SampleLevel(bilinearClamp, uvw, 0).rgb; +} + +float4 main(float2 uv : TEXCOORD) : SV_TARGET +{ + float3 hdr = sampleScene(uv); + + if (enableBloom != 0) + { + float3 bloom = bloomTexture.Sample(bilinearClamp, uv).rgb; + hdr += bloom * bloomIntensity; + } + + hdr *= 1.2 * pow(2.0, exposure); + + float3 mapped = PBRNeutralToneMapping(hdr); + + if (enableLUT != 0) + mapped = applyLUT(mapped); + + return float4(LinearToSRGB(mapped), 1.0); +} diff --git a/Engine_Master_UPC/PostProcessSettings.h b/Engine_Master_UPC/PostProcessSettings.h new file mode 100644 index 00000000..7c24950d --- /dev/null +++ b/Engine_Master_UPC/PostProcessSettings.h @@ -0,0 +1,37 @@ +#pragma once +#include "ISerializable.h" +#include "IArchive.h" +#include + +struct PostProcessSettings : public ISerializable +{ + float exposure = 0.0f; + + // Bloom + bool bloomEnabled = false; + float bloomThreshold = 1.0f; + float bloomIntensity = 0.5f; + + // Colour grading + bool lutEnabled = false; + std::string lutPath; + + // Chromatic aberration + bool chromaticAberrationEnabled = false; + float chromaticAberrationStrength = 1.0f; + + void serialize(IArchive& archive) override + { + archive.serialize(exposure, "exposure"); + + archive.serialize(bloomEnabled, "bloomEnabled"); + archive.serialize(bloomThreshold, "bloomThreshold"); + archive.serialize(bloomIntensity, "bloomIntensity"); + + archive.serialize(lutEnabled, "lutEnabled"); + archive.serialize(lutPath, "lutPath"); + + archive.serialize(chromaticAberrationEnabled, "chromaticAberrationEnabled"); + archive.serialize(chromaticAberrationStrength, "chromaticAberrationStrength"); + } +}; diff --git a/Engine_Master_UPC/Scene.cpp b/Engine_Master_UPC/Scene.cpp index 01a3e9e4..1d63cb83 100644 --- a/Engine_Master_UPC/Scene.cpp +++ b/Engine_Master_UPC/Scene.cpp @@ -638,6 +638,10 @@ void Scene::serialize(IArchive& archive) m_skybox.serialize(archive); archive.endObject(); + archive.beginObject("PostProcess"); + m_postProcess.serialize(archive); + archive.endObject(); + { SoundBanksData soundData; if (archive.mode() == ArchiveMode::Output) diff --git a/Engine_Master_UPC/Scene.h b/Engine_Master_UPC/Scene.h index d4a50571..dc4fc161 100644 --- a/Engine_Master_UPC/Scene.h +++ b/Engine_Master_UPC/Scene.h @@ -7,6 +7,7 @@ #include "SceneLightingSettings.h" #include "SceneDataCB.h" #include "SkyBoxSettings.h" +#include "PostProcessSettings.h" #include "SoundBanksData.h" #include "SceneReferenceResolver.h" #include "UID.h" @@ -31,6 +32,7 @@ class Scene: public Asset SceneLightingSettings m_lighting; SceneDataCB m_sceneDataCB; SkyBoxSettings m_skybox; + PostProcessSettings m_postProcess; CameraComponent* m_defaultCamera; std::vector m_rootObjects; @@ -95,6 +97,8 @@ class Scene: public Asset const SceneDataCB& getCBData() const { return m_sceneDataCB; } SkyBoxSettings& getSkyBoxSettings() { return m_skybox; } const SkyBoxSettings& getSkyBoxSettings() const { return m_skybox; } + PostProcessSettings& getPostProcessSettings() { return m_postProcess; } + const PostProcessSettings& getPostProcessSettings() const { return m_postProcess; } CameraComponent* getDefaultCamera() const { return m_defaultCamera; } diff --git a/Engine_Master_UPC/SceneConfig.cpp b/Engine_Master_UPC/SceneConfig.cpp index 806693e7..13f74172 100644 --- a/Engine_Master_UPC/SceneConfig.cpp +++ b/Engine_Master_UPC/SceneConfig.cpp @@ -28,6 +28,8 @@ void SceneConfig::drawInternal() ImGui::Separator(); drawLightSettings(); ImGui::Separator(); + drawPostProcessSettings(); + ImGui::Separator(); drawMusicBanksSettings(); ImGui::Separator(); } @@ -184,6 +186,37 @@ void SceneConfig::drawLightSettings() } } +void SceneConfig::drawPostProcessSettings() +{ + PostProcessSettings& pp = m_moduleScene->getScene()->getPostProcessSettings(); + + if (ImGui::CollapsingHeader("Post Process")) + { + ImGui::DragFloat("Exposure (EV)###PPExposure", &pp.exposure, 0.05f, -10.0f, 10.0f); + ImGui::TextDisabled("One stop (EV +1) doubles luminance."); + + ImGui::Separator(); + ImGui::Checkbox("Bloom###PPBloomEnabled", &pp.bloomEnabled); + ImGui::DragFloat("Bloom Threshold###PPBloomThreshold", &pp.bloomThreshold, 0.01f, 0.0f, 10.0f); + ImGui::DragFloat("Bloom Intensity###PPBloomIntensity", &pp.bloomIntensity, 0.01f, 0.0f, 5.0f); + + ImGui::Separator(); + ImGui::Checkbox("Colour Grading (LUT)###PPLutEnabled", &pp.lutEnabled); + + char lutBuffer[260]; + strcpy_s(lutBuffer, pp.lutPath.c_str()); + if (ImGui::InputText(".CUBE Path###PPLutPath", lutBuffer, IM_ARRAYSIZE(lutBuffer))) + { + pp.lutPath = lutBuffer; + } + ImGui::TextDisabled("Path to a .CUBE LUT (relative to the working directory)."); + + ImGui::Separator(); + ImGui::Checkbox("Chromatic Aberration###PPCAEnabled", &pp.chromaticAberrationEnabled); + ImGui::DragFloat("CA Strength###PPCAStrength", &pp.chromaticAberrationStrength, 0.05f, 0.0f, 10.0f); + } +} + void SceneConfig::drawMusicBanksSettings() { const std::vector loadedBanks = m_moduleScene->getScene()->getLoadedBanks(); diff --git a/Engine_Master_UPC/SceneConfig.h b/Engine_Master_UPC/SceneConfig.h index 8f39b47b..562c8f23 100644 --- a/Engine_Master_UPC/SceneConfig.h +++ b/Engine_Master_UPC/SceneConfig.h @@ -46,5 +46,7 @@ class SceneConfig : public EditorWindow void drawLightSettings(); + void drawPostProcessSettings(); + void drawMusicBanksSettings(); }; diff --git a/Engine_Master_UPC/SkyboxPass.cpp b/Engine_Master_UPC/SkyboxPass.cpp index 0a3e85b4..01d04478 100644 --- a/Engine_Master_UPC/SkyboxPass.cpp +++ b/Engine_Master_UPC/SkyboxPass.cpp @@ -81,7 +81,7 @@ SkyBoxPass::SkyBoxPass(ComPtr device, SkyBoxSettings& settings) : desc.SampleMask = UINT_MAX; desc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; desc.NumRenderTargets = 1; - desc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; + desc.RTVFormats[0] = DXGI_FORMAT_R16G16B16A16_FLOAT; // HDR scene target desc.SampleDesc = { 1, 0 }; DXCall(m_device->CreateGraphicsPipelineState(&desc, IID_PPV_ARGS(&m_pipelineState))); diff --git a/Engine_Master_UPC/SwapChain.cpp b/Engine_Master_UPC/SwapChain.cpp index 13700d24..9a074e62 100644 --- a/Engine_Master_UPC/SwapChain.cpp +++ b/Engine_Master_UPC/SwapChain.cpp @@ -60,6 +60,10 @@ SwapChain::SwapChain(HWND hWnd, ComPtr device, CommandQueue* queu auto* depthTexture = app->getModuleResources()->createDepthBuffer(float(m_windowWidth), float(m_windowHeight)); m_renderSurface.attachTexture( RenderSurface::DEPTH_STENCIL, std::shared_ptr(depthTexture)); + + // HDR scene target used by the post-process pipeline (resolved into the backbuffer). + auto* hdrTexture = app->getModuleResources()->createHDRRenderTexture(float(m_windowWidth), float(m_windowHeight)); + m_renderSurface.attachTexture( RenderSurface::COLOR_1, std::shared_ptr(hdrTexture)); } SwapChain::~SwapChain() diff --git a/Engine_Master_UPC/Texture.cpp b/Engine_Master_UPC/Texture.cpp index db8e2b29..bc29b366 100644 --- a/Engine_Master_UPC/Texture.cpp +++ b/Engine_Master_UPC/Texture.cpp @@ -27,6 +27,17 @@ namespace flags |= D3D12_RESOURCE_FLAG_DENY_SHADER_RESOURCE; } + if (desc.depth > 1) + { + return CD3DX12_RESOURCE_DESC::Tex3D( + desc.format, + static_cast(desc.width), + static_cast(desc.height), + static_cast(desc.depth), + desc.mipLevels, + flags); + } + return CD3DX12_RESOURCE_DESC::Tex2D( desc.format, static_cast(desc.width), @@ -198,7 +209,14 @@ void Texture::createSRV() srvDesc.Format = resolvedSRVFormat(); srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; - if (m_desc.arraySize == 6) + if (m_desc.depth > 1) + { + srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE3D; + srvDesc.Texture3D.MostDetailedMip = 0; + srvDesc.Texture3D.MipLevels = m_mipCount; + srvDesc.Texture3D.ResourceMinLODClamp = 0.0f; + } + else if (m_desc.arraySize == 6) { srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE; srvDesc.TextureCube.MostDetailedMip = 0; diff --git a/Engine_Master_UPC/Texture.h b/Engine_Master_UPC/Texture.h index fcb4e552..33796998 100644 --- a/Engine_Master_UPC/Texture.h +++ b/Engine_Master_UPC/Texture.h @@ -27,6 +27,7 @@ struct TextureDesc uint32_t width{ 1 }; uint32_t height{ 1 }; uint16_t arraySize{ 1 }; + uint16_t depth{ 1 }; // > 1 makes this a 3D (volume) texture uint16_t mipLevels{ 1 }; TextureView views{ TextureView::SRV }; D3D12_RESOURCE_STATES initialState{ D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE }; diff --git a/Engine_Master_UPC/editor_windows.ini b/Engine_Master_UPC/editor_windows.ini new file mode 100644 index 00000000..f7b1851b --- /dev/null +++ b/Engine_Master_UPC/editor_windows.ini @@ -0,0 +1,10 @@ +Console|1|1 +Hardware Info|2|1 +Editor Settings|3|1 +FileDialog|4|1 +Scene Configuration|5|1 +Scene Editor|6|1 +Hierarchy|7|1 +WindowInspector|8|1 +Game|9|1 +Animation State Machine|10|1 From 3557623a49d6cb5c11857cfafe4f585f457ebe63 Mon Sep 17 00:00:00 2001 From: Anand-Ramnarain-27 <142887960+Anand-Ramnarain-27@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:20:31 +0200 Subject: [PATCH 02/14] Add HDR render target and post-process resolve Convert the renderer to produce a linear HDR scene target and resolve it via a post-process pass. Deferred and skybox passes now render into a new SCENE_HDR attachment using DXGI_FORMAT_R16G16B16A16_FLOAT and shaders output linear HDR (tone mapping/gamma removed). ModuleResources adds createHDRRenderTexture and attaches SCENE_HDR to RenderSurface (enum expanded). PostProcessPass was refactored to implement IRenderPass, own a BloomPass, handle LUTs/dummy bloom SRV, perform resource-state transitions, and resolve exposure/tone-mapping/gamma into the COMPOSITE target. ModuleRender and SwapChain updated to create and schedule the post-process pass and to attach the HDR texture to the surface. --- Engine_Master_UPC/DeferredShadingPass.cpp | 9 ++- Engine_Master_UPC/LightPixelShader.hlsl | 10 ++-- Engine_Master_UPC/ModuleRender.cpp | 6 ++ Engine_Master_UPC/ModuleResources.cpp | 20 +++++++ Engine_Master_UPC/PostProcessPass.cpp | 72 ++++++++++++++++------- Engine_Master_UPC/PostProcessPass.h | 29 +++++---- Engine_Master_UPC/RenderSurface.h | 3 +- Engine_Master_UPC/SkyboxPass.cpp | 2 +- Engine_Master_UPC/SwapChain.cpp | 2 +- 9 files changed, 109 insertions(+), 44 deletions(-) diff --git a/Engine_Master_UPC/DeferredShadingPass.cpp b/Engine_Master_UPC/DeferredShadingPass.cpp index 91313c8e..60c9898e 100644 --- a/Engine_Master_UPC/DeferredShadingPass.cpp +++ b/Engine_Master_UPC/DeferredShadingPass.cpp @@ -26,6 +26,7 @@ #include "RenderSurface.h" #include "SimpleMath.h" +#include #include #include "PlatformHelpers.h" #include "OptickProfiler.h" @@ -104,7 +105,7 @@ DeferredShadingPass::DeferredShadingPass(ComPtr device): m_device psoDesc.SampleMask = UINT_MAX; psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; psoDesc.NumRenderTargets = 1; - psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; + psoDesc.RTVFormats[0] = DXGI_FORMAT_R16G16B16A16_FLOAT; // HDR scene target psoDesc.SampleDesc.Count = 1; psoDesc.SampleDesc.Quality = 0; @@ -173,7 +174,11 @@ void DeferredShadingPass::apply(ID3D12GraphicsCommandList4* commandList) BEGIN_EVENT(commandList, "Deferred"); - auto colorTex = m_renderSurface->getTexture(RenderSurface::COMPOSITE); + // Write the lit scene into the HDR target; post-processing resolves it to COMPOSITE. + auto colorTex = m_renderSurface->getTexture(RenderSurface::SCENE_HDR); + CD3DX12_RESOURCE_BARRIER toRT = CD3DX12_RESOURCE_BARRIER::Transition(colorTex->getD3D12Resource().Get(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET); + commandList->ResourceBarrier(1, &toRT); + D3D12_CPU_DESCRIPTOR_HANDLE rtv = colorTex->getRTV(0).cpu; commandList->OMSetRenderTargets(1, &rtv, FALSE, nullptr); commandList->RSSetViewports(1, &m_viewport); diff --git a/Engine_Master_UPC/LightPixelShader.hlsl b/Engine_Master_UPC/LightPixelShader.hlsl index bc4a8c04..0c0e43e6 100644 --- a/Engine_Master_UPC/LightPixelShader.hlsl +++ b/Engine_Master_UPC/LightPixelShader.hlsl @@ -316,11 +316,11 @@ float4 main(float4 position : SV_Position, float2 coord : TEXCOORD0) : SV_TARGET float3 indirectLighting = computeIndirectLighting(reflection, NdotV, finalWorldNormal, F0Metallic, alphaRoughness, 11, metallic, ao, specularAO); - //Calculate final color - float3 colorMapped = PBRNeutralToneMapping(directLighting + indirectLighting + emissive); - float3 finalColor = LinearToSRGB(colorMapped); + //Output linear HDR colour. Exposure, tone mapping and gamma correction are + //applied later by the post-process pass. + float3 finalColor = directLighting + indirectLighting + emissive; + + - - return float4(finalColor, 1.0f); } \ No newline at end of file diff --git a/Engine_Master_UPC/ModuleRender.cpp b/Engine_Master_UPC/ModuleRender.cpp index 992c52c5..5f32fdd0 100644 --- a/Engine_Master_UPC/ModuleRender.cpp +++ b/Engine_Master_UPC/ModuleRender.cpp @@ -34,6 +34,7 @@ #include "StaticTexturesPass.h" #include "SkinningComputePass.h" #include "ShadowMapPass.h" +#include "PostProcessPass.h" #include "Quadtree.h" #include "RenderContext.h" #include "WindowSceneEditor.h" @@ -81,6 +82,11 @@ bool ModuleRender::init() m_renderPasses.push_back(std::move(skyBoxPass)); m_renderPasses.push_back(std::make_unique(device)); + + // Resolve HDR scene fors bloom, exposure, tone mapping, LUT, gamma + // before the overlay passes draw on top. + m_renderPasses.push_back(std::make_unique(device)); + m_renderPasses.push_back(std::move(debugDrawPass)); m_renderPasses.push_back(std::make_unique(device)); m_renderPasses.push_back(std::make_unique(device)); diff --git a/Engine_Master_UPC/ModuleResources.cpp b/Engine_Master_UPC/ModuleResources.cpp index 0e256a65..a0c5b377 100644 --- a/Engine_Master_UPC/ModuleResources.cpp +++ b/Engine_Master_UPC/ModuleResources.cpp @@ -201,15 +201,35 @@ Texture* ModuleResources::createGBuffer(float width, float height, const DXGI_FO return new Texture(GenerateUID(), *m_device.Get(), desc); } +Texture* ModuleResources::createHDRRenderTexture(float width, float height) +{ + // Floating-point colour target so the lit scene can be stored in HDR + // (unclamped) and tone-mapped later as a post-process step. + TextureDesc desc{}; + desc.format = DXGI_FORMAT_R16G16B16A16_FLOAT; + desc.srvFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; + desc.rtvFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; + desc.width = static_cast(width); + desc.height = static_cast(height); + desc.views = TextureView::SRV | TextureView::RTV; + desc.initialState = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; + desc.shaderVisibleSRV = true; + + return new Texture(GenerateUID(), *m_device.Get(), desc); +} + RenderSurface* ModuleResources::createRenderSurface(float width, float height) { auto surface = new RenderSurface(); auto colorTex = std::shared_ptr(app->getModuleResources()->createRenderTexture(width, height)); colorTex->setName(L"RenderSurface_Color"); + auto hdrTex = std::shared_ptr(app->getModuleResources()->createHDRRenderTexture(width, height)); + hdrTex->setName(L"RenderSurface_SceneHDR"); auto depthTex = std::shared_ptr(app->getModuleResources()->createDepthBuffer(width, height)); depthTex->setName(L"RenderSurface_Depth"); surface->attachTexture(RenderSurface::COMPOSITE, colorTex); + surface->attachTexture(RenderSurface::SCENE_HDR, hdrTex); surface->attachTexture(RenderSurface::DEPTH_STENCIL, depthTex); return surface; diff --git a/Engine_Master_UPC/PostProcessPass.cpp b/Engine_Master_UPC/PostProcessPass.cpp index d322ff02..08893ee4 100644 --- a/Engine_Master_UPC/PostProcessPass.cpp +++ b/Engine_Master_UPC/PostProcessPass.cpp @@ -2,6 +2,7 @@ #include "PostProcessPass.h" #include "RenderContext.h" +#include "RenderSurface.h" #include "Application.h" #include "ModuleDescriptors.h" @@ -11,6 +12,7 @@ #include "PostProcessSettings.h" #include "Texture.h" #include "CubeLut.h" +#include "BloomPass.h" #include "UID.h" #include @@ -20,9 +22,10 @@ PostProcessPass::PostProcessPass(ComPtr device) : m_device(device) { // Root signature: b0 (params) + t0 (HDR scene) + t1 (bloom) + t2 (LUT 3D), + // with a static bilinear-clamp sampler at s0. CD3DX12_DESCRIPTOR_RANGE sceneRange, bloomRange, lutRange; sceneRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0, 0); - bloomRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 1, 0); + bloomRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 1, 0); lutRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 2, 0); CD3DX12_ROOT_PARAMETER rootParameters[4] = {}; @@ -75,8 +78,13 @@ PostProcessPass::PostProcessPass(ComPtr device) : m_device(device DXCall(m_device->CreateGraphicsPipelineState(&desc, IID_PPV_ARGS(&m_pipelineState))); + // Bloom is produced internally and fed into the resolve. + m_bloomPass = std::make_unique(m_device); + + // Neutral identity LUT used whenever colour grading is disabled. m_identityLut = CubeLut::createIdentity(*m_device.Get(), 2); + // 1x1 placeholder for the bloom slot when bloom is inactive. TextureDesc dummyDesc{}; dummyDesc.format = DXGI_FORMAT_R16G16B16A16_FLOAT; dummyDesc.width = 1; @@ -89,15 +97,13 @@ PostProcessPass::PostProcessPass(ComPtr device) : m_device(device m_dummyTexture->setName(L"PostProcess_DummyBloom"); } -void PostProcessPass::setBloomSource(D3D12_GPU_DESCRIPTOR_HANDLE bloomSrv, bool valid) -{ - m_bloomSrv = bloomSrv; - m_bloomValid = valid; -} +PostProcessPass::~PostProcessPass() = default; void PostProcessPass::prepare(const RenderContext& ctx) { - (void)ctx; + m_surface = &ctx.renderSurface; + m_viewport = ctx.viewport; + m_scissorRect = ctx.scissorRect; Scene* scene = app->getModuleScene()->getScene(); if (!scene) @@ -107,6 +113,7 @@ void PostProcessPass::prepare(const RenderContext& ctx) m_params.exposure = settings.exposure; + // Colour-grading LUT if (settings.lutEnabled && !settings.lutPath.empty()) { if (settings.lutPath != m_loadedLutPath) @@ -125,31 +132,51 @@ void PostProcessPass::prepare(const RenderContext& ctx) m_params.enableCA = settings.chromaticAberrationEnabled ? 1u : 0u; m_params.caStrength = settings.chromaticAberrationStrength; - m_params.enableBloom = (settings.bloomEnabled && m_bloomValid) ? 1u : 0u; + m_runBloom = settings.bloomEnabled; + m_params.enableBloom = settings.bloomEnabled ? 1u : 0u; m_params.bloomIntensity = settings.bloomIntensity; + + if (m_runBloom) + m_bloomPass->prepare(ctx); } -void PostProcessPass::apply(ID3D12GraphicsCommandList4* commandList, - D3D12_GPU_DESCRIPTOR_HANDLE sceneHDRSrv, - D3D12_CPU_DESCRIPTOR_HANDLE targetRTV, - const D3D12_VIEWPORT& viewport, - const D3D12_RECT& scissorRect) +void PostProcessPass::apply(ID3D12GraphicsCommandList4* commandList) { + if (!m_surface) + return; + + auto sceneHDR = m_surface->getTexture(RenderSurface::SCENE_HDR); + auto composite = m_surface->getTexture(RenderSurface::COMPOSITE); + if (!sceneHDR || !composite) + return; + + // The scene passes left SCENE_HDR as a render target; make it readable. + CD3DX12_RESOURCE_BARRIER toSRV = CD3DX12_RESOURCE_BARRIER::Transition(sceneHDR->getD3D12Resource().Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + commandList->ResourceBarrier(1, &toSRV); + + // Bloom reads SCENE_HDR and produces its own blurred texture. + D3D12_GPU_DESCRIPTOR_HANDLE bloomHandle = m_dummyTexture->getSRV().gpu; + if (m_runBloom) + { + m_bloomPass->apply(commandList, sceneHDR->getSRV().gpu); + bloomHandle = m_bloomPass->getBloomSRV(); + } + + const std::shared_ptr& lut = m_lutTexture ? m_lutTexture : m_identityLut; + + D3D12_CPU_DESCRIPTOR_HANDLE targetRTV = composite->getRTV(0).cpu; + commandList->OMSetRenderTargets(1, &targetRTV, FALSE, nullptr); + commandList->RSSetViewports(1, &m_viewport); + commandList->RSSetScissorRects(1, &m_scissorRect); + commandList->SetPipelineState(m_pipelineState.Get()); commandList->SetGraphicsRootSignature(m_rootSignature.Get()); ID3D12DescriptorHeap* descriptorHeaps[] = { app->getModuleDescriptors()->getHeap(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV).getHeap() }; commandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps); - commandList->OMSetRenderTargets(1, &targetRTV, FALSE, nullptr); - commandList->RSSetViewports(1, &viewport); - commandList->RSSetScissorRects(1, &scissorRect); - - const D3D12_GPU_DESCRIPTOR_HANDLE bloomHandle = m_bloomValid ? m_bloomSrv : m_dummyTexture->getSRV().gpu; - const std::shared_ptr& lut = m_lutTexture ? m_lutTexture : m_identityLut; - commandList->SetGraphicsRoot32BitConstants(0, sizeof(PostProcessParams) / sizeof(UINT32), &m_params, 0); - commandList->SetGraphicsRootDescriptorTable(1, sceneHDRSrv); + commandList->SetGraphicsRootDescriptorTable(1, sceneHDR->getSRV().gpu); commandList->SetGraphicsRootDescriptorTable(2, bloomHandle); commandList->SetGraphicsRootDescriptorTable(3, lut->getSRV().gpu); @@ -157,5 +184,6 @@ void PostProcessPass::apply(ID3D12GraphicsCommandList4* commandList, commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); commandList->DrawInstanced(3, 1, 0, 0); - m_bloomValid = false; + // Leave SCENE_HDR in PIXEL_SHADER_RESOURCE for the next frame's scene pass, + // which will transition it back to RENDER_TARGET. } diff --git a/Engine_Master_UPC/PostProcessPass.h b/Engine_Master_UPC/PostProcessPass.h index b399c6f2..ec28fd2b 100644 --- a/Engine_Master_UPC/PostProcessPass.h +++ b/Engine_Master_UPC/PostProcessPass.h @@ -1,4 +1,6 @@ #pragma once +#include "IRenderPass.h" + #include #include #include @@ -7,22 +9,18 @@ using Microsoft::WRL::ComPtr; struct RenderContext; +class RenderSurface; class Texture; +class BloomPass; -class PostProcessPass +class PostProcessPass : public IRenderPass { public: explicit PostProcessPass(ComPtr device); + ~PostProcessPass() override; - void prepare(const RenderContext& ctx); - - void setBloomSource(D3D12_GPU_DESCRIPTOR_HANDLE bloomSrv, bool valid); - - void apply(ID3D12GraphicsCommandList4* commandList, - D3D12_GPU_DESCRIPTOR_HANDLE sceneHDRSrv, - D3D12_CPU_DESCRIPTOR_HANDLE targetRTV, - const D3D12_VIEWPORT& viewport, - const D3D12_RECT& scissorRect); + void prepare(const RenderContext& ctx) override; + void apply(ID3D12GraphicsCommandList4* commandList) override; private: struct PostProcessParams @@ -44,13 +42,20 @@ class PostProcessPass PostProcessParams m_params; + std::unique_ptr m_bloomPass; + + // Colour-grading LUT and a neutral identity LUT fallback. std::shared_ptr m_lutTexture; std::shared_ptr m_identityLut; std::string m_loadedLutPath; int m_lutSize = 2; + // 1x1 placeholder bound to the bloom slot when bloom is inactive. std::shared_ptr m_dummyTexture; - D3D12_GPU_DESCRIPTOR_HANDLE m_bloomSrv{}; - bool m_bloomValid = false; + // Per-frame state captured in prepare(). + RenderSurface* m_surface = nullptr; + D3D12_VIEWPORT m_viewport{}; + D3D12_RECT m_scissorRect{}; + bool m_runBloom = false; }; diff --git a/Engine_Master_UPC/RenderSurface.h b/Engine_Master_UPC/RenderSurface.h index ff2b822f..a7c78e32 100644 --- a/Engine_Master_UPC/RenderSurface.h +++ b/Engine_Master_UPC/RenderSurface.h @@ -17,8 +17,9 @@ class RenderSurface GBUFFER_EMISSIVE = 4, DEPTH_STENCIL = 5, COMPOSITE = 6, + SCENE_HDR = 7, - NUM_ATTACHMENT_POINTS = 7 + NUM_ATTACHMENT_POINTS = 8 }; diff --git a/Engine_Master_UPC/SkyboxPass.cpp b/Engine_Master_UPC/SkyboxPass.cpp index 0ea4ec91..efacad0e 100644 --- a/Engine_Master_UPC/SkyboxPass.cpp +++ b/Engine_Master_UPC/SkyboxPass.cpp @@ -124,7 +124,7 @@ void SkyBoxPass::apply(ID3D12GraphicsCommandList4* commandList) params.flipX = 0; params.flipZ = 0; - auto colorTex = m_renderSurface->getTexture(RenderSurface::COMPOSITE); + auto colorTex = m_renderSurface->getTexture(RenderSurface::SCENE_HDR); auto dsTex = m_renderSurface->getTexture(RenderSurface::DEPTH_STENCIL); D3D12_CPU_DESCRIPTOR_HANDLE rtv = colorTex->getRTV(0).cpu; D3D12_CPU_DESCRIPTOR_HANDLE dsv = dsTex->getDSV().cpu; diff --git a/Engine_Master_UPC/SwapChain.cpp b/Engine_Master_UPC/SwapChain.cpp index aca4e9d2..4912b98d 100644 --- a/Engine_Master_UPC/SwapChain.cpp +++ b/Engine_Master_UPC/SwapChain.cpp @@ -63,7 +63,7 @@ SwapChain::SwapChain(HWND hWnd, ComPtr device, CommandQueue* queu // HDR scene target used by the post-process pipeline (resolved into the backbuffer). auto* hdrTexture = app->getModuleResources()->createHDRRenderTexture(float(m_windowWidth), float(m_windowHeight)); - m_renderSurface.attachTexture( RenderSurface::COLOR_1, std::shared_ptr(hdrTexture)); + m_renderSurface.attachTexture( RenderSurface::SCENE_HDR, std::shared_ptr(hdrTexture)); } SwapChain::~SwapChain() From 0d3d18e093709af1c72c8330b9aa72cec76ed7a5 Mon Sep 17 00:00:00 2001 From: Anand-Ramnarain-27 <142887960+Anand-Ramnarain-27@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:20:46 +0200 Subject: [PATCH 03/14] Revert "Add HDR render target and post-process resolve" This reverts commit 3557623a49d6cb5c11857cfafe4f585f457ebe63. --- Engine_Master_UPC/DeferredShadingPass.cpp | 9 +-- Engine_Master_UPC/LightPixelShader.hlsl | 10 ++-- Engine_Master_UPC/ModuleRender.cpp | 6 -- Engine_Master_UPC/ModuleResources.cpp | 20 ------- Engine_Master_UPC/PostProcessPass.cpp | 72 +++++++---------------- Engine_Master_UPC/PostProcessPass.h | 29 ++++----- Engine_Master_UPC/RenderSurface.h | 3 +- Engine_Master_UPC/SkyboxPass.cpp | 2 +- Engine_Master_UPC/SwapChain.cpp | 2 +- 9 files changed, 44 insertions(+), 109 deletions(-) diff --git a/Engine_Master_UPC/DeferredShadingPass.cpp b/Engine_Master_UPC/DeferredShadingPass.cpp index 60c9898e..91313c8e 100644 --- a/Engine_Master_UPC/DeferredShadingPass.cpp +++ b/Engine_Master_UPC/DeferredShadingPass.cpp @@ -26,7 +26,6 @@ #include "RenderSurface.h" #include "SimpleMath.h" -#include #include #include "PlatformHelpers.h" #include "OptickProfiler.h" @@ -105,7 +104,7 @@ DeferredShadingPass::DeferredShadingPass(ComPtr device): m_device psoDesc.SampleMask = UINT_MAX; psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; psoDesc.NumRenderTargets = 1; - psoDesc.RTVFormats[0] = DXGI_FORMAT_R16G16B16A16_FLOAT; // HDR scene target + psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; psoDesc.SampleDesc.Count = 1; psoDesc.SampleDesc.Quality = 0; @@ -174,11 +173,7 @@ void DeferredShadingPass::apply(ID3D12GraphicsCommandList4* commandList) BEGIN_EVENT(commandList, "Deferred"); - // Write the lit scene into the HDR target; post-processing resolves it to COMPOSITE. - auto colorTex = m_renderSurface->getTexture(RenderSurface::SCENE_HDR); - CD3DX12_RESOURCE_BARRIER toRT = CD3DX12_RESOURCE_BARRIER::Transition(colorTex->getD3D12Resource().Get(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET); - commandList->ResourceBarrier(1, &toRT); - + auto colorTex = m_renderSurface->getTexture(RenderSurface::COMPOSITE); D3D12_CPU_DESCRIPTOR_HANDLE rtv = colorTex->getRTV(0).cpu; commandList->OMSetRenderTargets(1, &rtv, FALSE, nullptr); commandList->RSSetViewports(1, &m_viewport); diff --git a/Engine_Master_UPC/LightPixelShader.hlsl b/Engine_Master_UPC/LightPixelShader.hlsl index 0c0e43e6..bc4a8c04 100644 --- a/Engine_Master_UPC/LightPixelShader.hlsl +++ b/Engine_Master_UPC/LightPixelShader.hlsl @@ -316,11 +316,11 @@ float4 main(float4 position : SV_Position, float2 coord : TEXCOORD0) : SV_TARGET float3 indirectLighting = computeIndirectLighting(reflection, NdotV, finalWorldNormal, F0Metallic, alphaRoughness, 11, metallic, ao, specularAO); - //Output linear HDR colour. Exposure, tone mapping and gamma correction are - //applied later by the post-process pass. - float3 finalColor = directLighting + indirectLighting + emissive; - - + //Calculate final color + float3 colorMapped = PBRNeutralToneMapping(directLighting + indirectLighting + emissive); + float3 finalColor = LinearToSRGB(colorMapped); + + return float4(finalColor, 1.0f); } \ No newline at end of file diff --git a/Engine_Master_UPC/ModuleRender.cpp b/Engine_Master_UPC/ModuleRender.cpp index 5f32fdd0..992c52c5 100644 --- a/Engine_Master_UPC/ModuleRender.cpp +++ b/Engine_Master_UPC/ModuleRender.cpp @@ -34,7 +34,6 @@ #include "StaticTexturesPass.h" #include "SkinningComputePass.h" #include "ShadowMapPass.h" -#include "PostProcessPass.h" #include "Quadtree.h" #include "RenderContext.h" #include "WindowSceneEditor.h" @@ -82,11 +81,6 @@ bool ModuleRender::init() m_renderPasses.push_back(std::move(skyBoxPass)); m_renderPasses.push_back(std::make_unique(device)); - - // Resolve HDR scene fors bloom, exposure, tone mapping, LUT, gamma - // before the overlay passes draw on top. - m_renderPasses.push_back(std::make_unique(device)); - m_renderPasses.push_back(std::move(debugDrawPass)); m_renderPasses.push_back(std::make_unique(device)); m_renderPasses.push_back(std::make_unique(device)); diff --git a/Engine_Master_UPC/ModuleResources.cpp b/Engine_Master_UPC/ModuleResources.cpp index a0c5b377..0e256a65 100644 --- a/Engine_Master_UPC/ModuleResources.cpp +++ b/Engine_Master_UPC/ModuleResources.cpp @@ -201,35 +201,15 @@ Texture* ModuleResources::createGBuffer(float width, float height, const DXGI_FO return new Texture(GenerateUID(), *m_device.Get(), desc); } -Texture* ModuleResources::createHDRRenderTexture(float width, float height) -{ - // Floating-point colour target so the lit scene can be stored in HDR - // (unclamped) and tone-mapped later as a post-process step. - TextureDesc desc{}; - desc.format = DXGI_FORMAT_R16G16B16A16_FLOAT; - desc.srvFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; - desc.rtvFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; - desc.width = static_cast(width); - desc.height = static_cast(height); - desc.views = TextureView::SRV | TextureView::RTV; - desc.initialState = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; - desc.shaderVisibleSRV = true; - - return new Texture(GenerateUID(), *m_device.Get(), desc); -} - RenderSurface* ModuleResources::createRenderSurface(float width, float height) { auto surface = new RenderSurface(); auto colorTex = std::shared_ptr(app->getModuleResources()->createRenderTexture(width, height)); colorTex->setName(L"RenderSurface_Color"); - auto hdrTex = std::shared_ptr(app->getModuleResources()->createHDRRenderTexture(width, height)); - hdrTex->setName(L"RenderSurface_SceneHDR"); auto depthTex = std::shared_ptr(app->getModuleResources()->createDepthBuffer(width, height)); depthTex->setName(L"RenderSurface_Depth"); surface->attachTexture(RenderSurface::COMPOSITE, colorTex); - surface->attachTexture(RenderSurface::SCENE_HDR, hdrTex); surface->attachTexture(RenderSurface::DEPTH_STENCIL, depthTex); return surface; diff --git a/Engine_Master_UPC/PostProcessPass.cpp b/Engine_Master_UPC/PostProcessPass.cpp index 08893ee4..d322ff02 100644 --- a/Engine_Master_UPC/PostProcessPass.cpp +++ b/Engine_Master_UPC/PostProcessPass.cpp @@ -2,7 +2,6 @@ #include "PostProcessPass.h" #include "RenderContext.h" -#include "RenderSurface.h" #include "Application.h" #include "ModuleDescriptors.h" @@ -12,7 +11,6 @@ #include "PostProcessSettings.h" #include "Texture.h" #include "CubeLut.h" -#include "BloomPass.h" #include "UID.h" #include @@ -22,10 +20,9 @@ PostProcessPass::PostProcessPass(ComPtr device) : m_device(device) { // Root signature: b0 (params) + t0 (HDR scene) + t1 (bloom) + t2 (LUT 3D), - // with a static bilinear-clamp sampler at s0. CD3DX12_DESCRIPTOR_RANGE sceneRange, bloomRange, lutRange; sceneRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0, 0); - bloomRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 1, 0); + bloomRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 1, 0); lutRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 2, 0); CD3DX12_ROOT_PARAMETER rootParameters[4] = {}; @@ -78,13 +75,8 @@ PostProcessPass::PostProcessPass(ComPtr device) : m_device(device DXCall(m_device->CreateGraphicsPipelineState(&desc, IID_PPV_ARGS(&m_pipelineState))); - // Bloom is produced internally and fed into the resolve. - m_bloomPass = std::make_unique(m_device); - - // Neutral identity LUT used whenever colour grading is disabled. m_identityLut = CubeLut::createIdentity(*m_device.Get(), 2); - // 1x1 placeholder for the bloom slot when bloom is inactive. TextureDesc dummyDesc{}; dummyDesc.format = DXGI_FORMAT_R16G16B16A16_FLOAT; dummyDesc.width = 1; @@ -97,13 +89,15 @@ PostProcessPass::PostProcessPass(ComPtr device) : m_device(device m_dummyTexture->setName(L"PostProcess_DummyBloom"); } -PostProcessPass::~PostProcessPass() = default; +void PostProcessPass::setBloomSource(D3D12_GPU_DESCRIPTOR_HANDLE bloomSrv, bool valid) +{ + m_bloomSrv = bloomSrv; + m_bloomValid = valid; +} void PostProcessPass::prepare(const RenderContext& ctx) { - m_surface = &ctx.renderSurface; - m_viewport = ctx.viewport; - m_scissorRect = ctx.scissorRect; + (void)ctx; Scene* scene = app->getModuleScene()->getScene(); if (!scene) @@ -113,7 +107,6 @@ void PostProcessPass::prepare(const RenderContext& ctx) m_params.exposure = settings.exposure; - // Colour-grading LUT if (settings.lutEnabled && !settings.lutPath.empty()) { if (settings.lutPath != m_loadedLutPath) @@ -132,51 +125,31 @@ void PostProcessPass::prepare(const RenderContext& ctx) m_params.enableCA = settings.chromaticAberrationEnabled ? 1u : 0u; m_params.caStrength = settings.chromaticAberrationStrength; - m_runBloom = settings.bloomEnabled; - m_params.enableBloom = settings.bloomEnabled ? 1u : 0u; + m_params.enableBloom = (settings.bloomEnabled && m_bloomValid) ? 1u : 0u; m_params.bloomIntensity = settings.bloomIntensity; - - if (m_runBloom) - m_bloomPass->prepare(ctx); } -void PostProcessPass::apply(ID3D12GraphicsCommandList4* commandList) +void PostProcessPass::apply(ID3D12GraphicsCommandList4* commandList, + D3D12_GPU_DESCRIPTOR_HANDLE sceneHDRSrv, + D3D12_CPU_DESCRIPTOR_HANDLE targetRTV, + const D3D12_VIEWPORT& viewport, + const D3D12_RECT& scissorRect) { - if (!m_surface) - return; - - auto sceneHDR = m_surface->getTexture(RenderSurface::SCENE_HDR); - auto composite = m_surface->getTexture(RenderSurface::COMPOSITE); - if (!sceneHDR || !composite) - return; - - // The scene passes left SCENE_HDR as a render target; make it readable. - CD3DX12_RESOURCE_BARRIER toSRV = CD3DX12_RESOURCE_BARRIER::Transition(sceneHDR->getD3D12Resource().Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); - commandList->ResourceBarrier(1, &toSRV); - - // Bloom reads SCENE_HDR and produces its own blurred texture. - D3D12_GPU_DESCRIPTOR_HANDLE bloomHandle = m_dummyTexture->getSRV().gpu; - if (m_runBloom) - { - m_bloomPass->apply(commandList, sceneHDR->getSRV().gpu); - bloomHandle = m_bloomPass->getBloomSRV(); - } - - const std::shared_ptr& lut = m_lutTexture ? m_lutTexture : m_identityLut; - - D3D12_CPU_DESCRIPTOR_HANDLE targetRTV = composite->getRTV(0).cpu; - commandList->OMSetRenderTargets(1, &targetRTV, FALSE, nullptr); - commandList->RSSetViewports(1, &m_viewport); - commandList->RSSetScissorRects(1, &m_scissorRect); - commandList->SetPipelineState(m_pipelineState.Get()); commandList->SetGraphicsRootSignature(m_rootSignature.Get()); ID3D12DescriptorHeap* descriptorHeaps[] = { app->getModuleDescriptors()->getHeap(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV).getHeap() }; commandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps); + commandList->OMSetRenderTargets(1, &targetRTV, FALSE, nullptr); + commandList->RSSetViewports(1, &viewport); + commandList->RSSetScissorRects(1, &scissorRect); + + const D3D12_GPU_DESCRIPTOR_HANDLE bloomHandle = m_bloomValid ? m_bloomSrv : m_dummyTexture->getSRV().gpu; + const std::shared_ptr& lut = m_lutTexture ? m_lutTexture : m_identityLut; + commandList->SetGraphicsRoot32BitConstants(0, sizeof(PostProcessParams) / sizeof(UINT32), &m_params, 0); - commandList->SetGraphicsRootDescriptorTable(1, sceneHDR->getSRV().gpu); + commandList->SetGraphicsRootDescriptorTable(1, sceneHDRSrv); commandList->SetGraphicsRootDescriptorTable(2, bloomHandle); commandList->SetGraphicsRootDescriptorTable(3, lut->getSRV().gpu); @@ -184,6 +157,5 @@ void PostProcessPass::apply(ID3D12GraphicsCommandList4* commandList) commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); commandList->DrawInstanced(3, 1, 0, 0); - // Leave SCENE_HDR in PIXEL_SHADER_RESOURCE for the next frame's scene pass, - // which will transition it back to RENDER_TARGET. + m_bloomValid = false; } diff --git a/Engine_Master_UPC/PostProcessPass.h b/Engine_Master_UPC/PostProcessPass.h index ec28fd2b..b399c6f2 100644 --- a/Engine_Master_UPC/PostProcessPass.h +++ b/Engine_Master_UPC/PostProcessPass.h @@ -1,6 +1,4 @@ #pragma once -#include "IRenderPass.h" - #include #include #include @@ -9,18 +7,22 @@ using Microsoft::WRL::ComPtr; struct RenderContext; -class RenderSurface; class Texture; -class BloomPass; -class PostProcessPass : public IRenderPass +class PostProcessPass { public: explicit PostProcessPass(ComPtr device); - ~PostProcessPass() override; - void prepare(const RenderContext& ctx) override; - void apply(ID3D12GraphicsCommandList4* commandList) override; + void prepare(const RenderContext& ctx); + + void setBloomSource(D3D12_GPU_DESCRIPTOR_HANDLE bloomSrv, bool valid); + + void apply(ID3D12GraphicsCommandList4* commandList, + D3D12_GPU_DESCRIPTOR_HANDLE sceneHDRSrv, + D3D12_CPU_DESCRIPTOR_HANDLE targetRTV, + const D3D12_VIEWPORT& viewport, + const D3D12_RECT& scissorRect); private: struct PostProcessParams @@ -42,20 +44,13 @@ class PostProcessPass : public IRenderPass PostProcessParams m_params; - std::unique_ptr m_bloomPass; - - // Colour-grading LUT and a neutral identity LUT fallback. std::shared_ptr m_lutTexture; std::shared_ptr m_identityLut; std::string m_loadedLutPath; int m_lutSize = 2; - // 1x1 placeholder bound to the bloom slot when bloom is inactive. std::shared_ptr m_dummyTexture; - // Per-frame state captured in prepare(). - RenderSurface* m_surface = nullptr; - D3D12_VIEWPORT m_viewport{}; - D3D12_RECT m_scissorRect{}; - bool m_runBloom = false; + D3D12_GPU_DESCRIPTOR_HANDLE m_bloomSrv{}; + bool m_bloomValid = false; }; diff --git a/Engine_Master_UPC/RenderSurface.h b/Engine_Master_UPC/RenderSurface.h index a7c78e32..ff2b822f 100644 --- a/Engine_Master_UPC/RenderSurface.h +++ b/Engine_Master_UPC/RenderSurface.h @@ -17,9 +17,8 @@ class RenderSurface GBUFFER_EMISSIVE = 4, DEPTH_STENCIL = 5, COMPOSITE = 6, - SCENE_HDR = 7, - NUM_ATTACHMENT_POINTS = 8 + NUM_ATTACHMENT_POINTS = 7 }; diff --git a/Engine_Master_UPC/SkyboxPass.cpp b/Engine_Master_UPC/SkyboxPass.cpp index efacad0e..0ea4ec91 100644 --- a/Engine_Master_UPC/SkyboxPass.cpp +++ b/Engine_Master_UPC/SkyboxPass.cpp @@ -124,7 +124,7 @@ void SkyBoxPass::apply(ID3D12GraphicsCommandList4* commandList) params.flipX = 0; params.flipZ = 0; - auto colorTex = m_renderSurface->getTexture(RenderSurface::SCENE_HDR); + auto colorTex = m_renderSurface->getTexture(RenderSurface::COMPOSITE); auto dsTex = m_renderSurface->getTexture(RenderSurface::DEPTH_STENCIL); D3D12_CPU_DESCRIPTOR_HANDLE rtv = colorTex->getRTV(0).cpu; D3D12_CPU_DESCRIPTOR_HANDLE dsv = dsTex->getDSV().cpu; diff --git a/Engine_Master_UPC/SwapChain.cpp b/Engine_Master_UPC/SwapChain.cpp index 4912b98d..aca4e9d2 100644 --- a/Engine_Master_UPC/SwapChain.cpp +++ b/Engine_Master_UPC/SwapChain.cpp @@ -63,7 +63,7 @@ SwapChain::SwapChain(HWND hWnd, ComPtr device, CommandQueue* queu // HDR scene target used by the post-process pipeline (resolved into the backbuffer). auto* hdrTexture = app->getModuleResources()->createHDRRenderTexture(float(m_windowWidth), float(m_windowHeight)); - m_renderSurface.attachTexture( RenderSurface::SCENE_HDR, std::shared_ptr(hdrTexture)); + m_renderSurface.attachTexture( RenderSurface::COLOR_1, std::shared_ptr(hdrTexture)); } SwapChain::~SwapChain() From 5b5e3b8bc4e335aad37eb0de0127d668fa13c1c4 Mon Sep 17 00:00:00 2001 From: Anand-Ramnarain-27 <142887960+Anand-Ramnarain-27@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:23:08 +0200 Subject: [PATCH 04/14] Reapply "Add HDR render target and post-process resolve" This reverts commit 0d3d18e093709af1c72c8330b9aa72cec76ed7a5. --- Engine_Master_UPC/DeferredShadingPass.cpp | 9 ++- Engine_Master_UPC/LightPixelShader.hlsl | 10 ++-- Engine_Master_UPC/ModuleRender.cpp | 6 ++ Engine_Master_UPC/ModuleResources.cpp | 20 +++++++ Engine_Master_UPC/PostProcessPass.cpp | 72 ++++++++++++++++------- Engine_Master_UPC/PostProcessPass.h | 29 +++++---- Engine_Master_UPC/RenderSurface.h | 3 +- Engine_Master_UPC/SkyboxPass.cpp | 2 +- Engine_Master_UPC/SwapChain.cpp | 2 +- 9 files changed, 109 insertions(+), 44 deletions(-) diff --git a/Engine_Master_UPC/DeferredShadingPass.cpp b/Engine_Master_UPC/DeferredShadingPass.cpp index 91313c8e..60c9898e 100644 --- a/Engine_Master_UPC/DeferredShadingPass.cpp +++ b/Engine_Master_UPC/DeferredShadingPass.cpp @@ -26,6 +26,7 @@ #include "RenderSurface.h" #include "SimpleMath.h" +#include #include #include "PlatformHelpers.h" #include "OptickProfiler.h" @@ -104,7 +105,7 @@ DeferredShadingPass::DeferredShadingPass(ComPtr device): m_device psoDesc.SampleMask = UINT_MAX; psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; psoDesc.NumRenderTargets = 1; - psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; + psoDesc.RTVFormats[0] = DXGI_FORMAT_R16G16B16A16_FLOAT; // HDR scene target psoDesc.SampleDesc.Count = 1; psoDesc.SampleDesc.Quality = 0; @@ -173,7 +174,11 @@ void DeferredShadingPass::apply(ID3D12GraphicsCommandList4* commandList) BEGIN_EVENT(commandList, "Deferred"); - auto colorTex = m_renderSurface->getTexture(RenderSurface::COMPOSITE); + // Write the lit scene into the HDR target; post-processing resolves it to COMPOSITE. + auto colorTex = m_renderSurface->getTexture(RenderSurface::SCENE_HDR); + CD3DX12_RESOURCE_BARRIER toRT = CD3DX12_RESOURCE_BARRIER::Transition(colorTex->getD3D12Resource().Get(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET); + commandList->ResourceBarrier(1, &toRT); + D3D12_CPU_DESCRIPTOR_HANDLE rtv = colorTex->getRTV(0).cpu; commandList->OMSetRenderTargets(1, &rtv, FALSE, nullptr); commandList->RSSetViewports(1, &m_viewport); diff --git a/Engine_Master_UPC/LightPixelShader.hlsl b/Engine_Master_UPC/LightPixelShader.hlsl index bc4a8c04..0c0e43e6 100644 --- a/Engine_Master_UPC/LightPixelShader.hlsl +++ b/Engine_Master_UPC/LightPixelShader.hlsl @@ -316,11 +316,11 @@ float4 main(float4 position : SV_Position, float2 coord : TEXCOORD0) : SV_TARGET float3 indirectLighting = computeIndirectLighting(reflection, NdotV, finalWorldNormal, F0Metallic, alphaRoughness, 11, metallic, ao, specularAO); - //Calculate final color - float3 colorMapped = PBRNeutralToneMapping(directLighting + indirectLighting + emissive); - float3 finalColor = LinearToSRGB(colorMapped); + //Output linear HDR colour. Exposure, tone mapping and gamma correction are + //applied later by the post-process pass. + float3 finalColor = directLighting + indirectLighting + emissive; + + - - return float4(finalColor, 1.0f); } \ No newline at end of file diff --git a/Engine_Master_UPC/ModuleRender.cpp b/Engine_Master_UPC/ModuleRender.cpp index 992c52c5..5f32fdd0 100644 --- a/Engine_Master_UPC/ModuleRender.cpp +++ b/Engine_Master_UPC/ModuleRender.cpp @@ -34,6 +34,7 @@ #include "StaticTexturesPass.h" #include "SkinningComputePass.h" #include "ShadowMapPass.h" +#include "PostProcessPass.h" #include "Quadtree.h" #include "RenderContext.h" #include "WindowSceneEditor.h" @@ -81,6 +82,11 @@ bool ModuleRender::init() m_renderPasses.push_back(std::move(skyBoxPass)); m_renderPasses.push_back(std::make_unique(device)); + + // Resolve HDR scene fors bloom, exposure, tone mapping, LUT, gamma + // before the overlay passes draw on top. + m_renderPasses.push_back(std::make_unique(device)); + m_renderPasses.push_back(std::move(debugDrawPass)); m_renderPasses.push_back(std::make_unique(device)); m_renderPasses.push_back(std::make_unique(device)); diff --git a/Engine_Master_UPC/ModuleResources.cpp b/Engine_Master_UPC/ModuleResources.cpp index 0e256a65..a0c5b377 100644 --- a/Engine_Master_UPC/ModuleResources.cpp +++ b/Engine_Master_UPC/ModuleResources.cpp @@ -201,15 +201,35 @@ Texture* ModuleResources::createGBuffer(float width, float height, const DXGI_FO return new Texture(GenerateUID(), *m_device.Get(), desc); } +Texture* ModuleResources::createHDRRenderTexture(float width, float height) +{ + // Floating-point colour target so the lit scene can be stored in HDR + // (unclamped) and tone-mapped later as a post-process step. + TextureDesc desc{}; + desc.format = DXGI_FORMAT_R16G16B16A16_FLOAT; + desc.srvFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; + desc.rtvFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; + desc.width = static_cast(width); + desc.height = static_cast(height); + desc.views = TextureView::SRV | TextureView::RTV; + desc.initialState = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; + desc.shaderVisibleSRV = true; + + return new Texture(GenerateUID(), *m_device.Get(), desc); +} + RenderSurface* ModuleResources::createRenderSurface(float width, float height) { auto surface = new RenderSurface(); auto colorTex = std::shared_ptr(app->getModuleResources()->createRenderTexture(width, height)); colorTex->setName(L"RenderSurface_Color"); + auto hdrTex = std::shared_ptr(app->getModuleResources()->createHDRRenderTexture(width, height)); + hdrTex->setName(L"RenderSurface_SceneHDR"); auto depthTex = std::shared_ptr(app->getModuleResources()->createDepthBuffer(width, height)); depthTex->setName(L"RenderSurface_Depth"); surface->attachTexture(RenderSurface::COMPOSITE, colorTex); + surface->attachTexture(RenderSurface::SCENE_HDR, hdrTex); surface->attachTexture(RenderSurface::DEPTH_STENCIL, depthTex); return surface; diff --git a/Engine_Master_UPC/PostProcessPass.cpp b/Engine_Master_UPC/PostProcessPass.cpp index d322ff02..08893ee4 100644 --- a/Engine_Master_UPC/PostProcessPass.cpp +++ b/Engine_Master_UPC/PostProcessPass.cpp @@ -2,6 +2,7 @@ #include "PostProcessPass.h" #include "RenderContext.h" +#include "RenderSurface.h" #include "Application.h" #include "ModuleDescriptors.h" @@ -11,6 +12,7 @@ #include "PostProcessSettings.h" #include "Texture.h" #include "CubeLut.h" +#include "BloomPass.h" #include "UID.h" #include @@ -20,9 +22,10 @@ PostProcessPass::PostProcessPass(ComPtr device) : m_device(device) { // Root signature: b0 (params) + t0 (HDR scene) + t1 (bloom) + t2 (LUT 3D), + // with a static bilinear-clamp sampler at s0. CD3DX12_DESCRIPTOR_RANGE sceneRange, bloomRange, lutRange; sceneRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0, 0); - bloomRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 1, 0); + bloomRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 1, 0); lutRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 2, 0); CD3DX12_ROOT_PARAMETER rootParameters[4] = {}; @@ -75,8 +78,13 @@ PostProcessPass::PostProcessPass(ComPtr device) : m_device(device DXCall(m_device->CreateGraphicsPipelineState(&desc, IID_PPV_ARGS(&m_pipelineState))); + // Bloom is produced internally and fed into the resolve. + m_bloomPass = std::make_unique(m_device); + + // Neutral identity LUT used whenever colour grading is disabled. m_identityLut = CubeLut::createIdentity(*m_device.Get(), 2); + // 1x1 placeholder for the bloom slot when bloom is inactive. TextureDesc dummyDesc{}; dummyDesc.format = DXGI_FORMAT_R16G16B16A16_FLOAT; dummyDesc.width = 1; @@ -89,15 +97,13 @@ PostProcessPass::PostProcessPass(ComPtr device) : m_device(device m_dummyTexture->setName(L"PostProcess_DummyBloom"); } -void PostProcessPass::setBloomSource(D3D12_GPU_DESCRIPTOR_HANDLE bloomSrv, bool valid) -{ - m_bloomSrv = bloomSrv; - m_bloomValid = valid; -} +PostProcessPass::~PostProcessPass() = default; void PostProcessPass::prepare(const RenderContext& ctx) { - (void)ctx; + m_surface = &ctx.renderSurface; + m_viewport = ctx.viewport; + m_scissorRect = ctx.scissorRect; Scene* scene = app->getModuleScene()->getScene(); if (!scene) @@ -107,6 +113,7 @@ void PostProcessPass::prepare(const RenderContext& ctx) m_params.exposure = settings.exposure; + // Colour-grading LUT if (settings.lutEnabled && !settings.lutPath.empty()) { if (settings.lutPath != m_loadedLutPath) @@ -125,31 +132,51 @@ void PostProcessPass::prepare(const RenderContext& ctx) m_params.enableCA = settings.chromaticAberrationEnabled ? 1u : 0u; m_params.caStrength = settings.chromaticAberrationStrength; - m_params.enableBloom = (settings.bloomEnabled && m_bloomValid) ? 1u : 0u; + m_runBloom = settings.bloomEnabled; + m_params.enableBloom = settings.bloomEnabled ? 1u : 0u; m_params.bloomIntensity = settings.bloomIntensity; + + if (m_runBloom) + m_bloomPass->prepare(ctx); } -void PostProcessPass::apply(ID3D12GraphicsCommandList4* commandList, - D3D12_GPU_DESCRIPTOR_HANDLE sceneHDRSrv, - D3D12_CPU_DESCRIPTOR_HANDLE targetRTV, - const D3D12_VIEWPORT& viewport, - const D3D12_RECT& scissorRect) +void PostProcessPass::apply(ID3D12GraphicsCommandList4* commandList) { + if (!m_surface) + return; + + auto sceneHDR = m_surface->getTexture(RenderSurface::SCENE_HDR); + auto composite = m_surface->getTexture(RenderSurface::COMPOSITE); + if (!sceneHDR || !composite) + return; + + // The scene passes left SCENE_HDR as a render target; make it readable. + CD3DX12_RESOURCE_BARRIER toSRV = CD3DX12_RESOURCE_BARRIER::Transition(sceneHDR->getD3D12Resource().Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + commandList->ResourceBarrier(1, &toSRV); + + // Bloom reads SCENE_HDR and produces its own blurred texture. + D3D12_GPU_DESCRIPTOR_HANDLE bloomHandle = m_dummyTexture->getSRV().gpu; + if (m_runBloom) + { + m_bloomPass->apply(commandList, sceneHDR->getSRV().gpu); + bloomHandle = m_bloomPass->getBloomSRV(); + } + + const std::shared_ptr& lut = m_lutTexture ? m_lutTexture : m_identityLut; + + D3D12_CPU_DESCRIPTOR_HANDLE targetRTV = composite->getRTV(0).cpu; + commandList->OMSetRenderTargets(1, &targetRTV, FALSE, nullptr); + commandList->RSSetViewports(1, &m_viewport); + commandList->RSSetScissorRects(1, &m_scissorRect); + commandList->SetPipelineState(m_pipelineState.Get()); commandList->SetGraphicsRootSignature(m_rootSignature.Get()); ID3D12DescriptorHeap* descriptorHeaps[] = { app->getModuleDescriptors()->getHeap(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV).getHeap() }; commandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps); - commandList->OMSetRenderTargets(1, &targetRTV, FALSE, nullptr); - commandList->RSSetViewports(1, &viewport); - commandList->RSSetScissorRects(1, &scissorRect); - - const D3D12_GPU_DESCRIPTOR_HANDLE bloomHandle = m_bloomValid ? m_bloomSrv : m_dummyTexture->getSRV().gpu; - const std::shared_ptr& lut = m_lutTexture ? m_lutTexture : m_identityLut; - commandList->SetGraphicsRoot32BitConstants(0, sizeof(PostProcessParams) / sizeof(UINT32), &m_params, 0); - commandList->SetGraphicsRootDescriptorTable(1, sceneHDRSrv); + commandList->SetGraphicsRootDescriptorTable(1, sceneHDR->getSRV().gpu); commandList->SetGraphicsRootDescriptorTable(2, bloomHandle); commandList->SetGraphicsRootDescriptorTable(3, lut->getSRV().gpu); @@ -157,5 +184,6 @@ void PostProcessPass::apply(ID3D12GraphicsCommandList4* commandList, commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); commandList->DrawInstanced(3, 1, 0, 0); - m_bloomValid = false; + // Leave SCENE_HDR in PIXEL_SHADER_RESOURCE for the next frame's scene pass, + // which will transition it back to RENDER_TARGET. } diff --git a/Engine_Master_UPC/PostProcessPass.h b/Engine_Master_UPC/PostProcessPass.h index b399c6f2..ec28fd2b 100644 --- a/Engine_Master_UPC/PostProcessPass.h +++ b/Engine_Master_UPC/PostProcessPass.h @@ -1,4 +1,6 @@ #pragma once +#include "IRenderPass.h" + #include #include #include @@ -7,22 +9,18 @@ using Microsoft::WRL::ComPtr; struct RenderContext; +class RenderSurface; class Texture; +class BloomPass; -class PostProcessPass +class PostProcessPass : public IRenderPass { public: explicit PostProcessPass(ComPtr device); + ~PostProcessPass() override; - void prepare(const RenderContext& ctx); - - void setBloomSource(D3D12_GPU_DESCRIPTOR_HANDLE bloomSrv, bool valid); - - void apply(ID3D12GraphicsCommandList4* commandList, - D3D12_GPU_DESCRIPTOR_HANDLE sceneHDRSrv, - D3D12_CPU_DESCRIPTOR_HANDLE targetRTV, - const D3D12_VIEWPORT& viewport, - const D3D12_RECT& scissorRect); + void prepare(const RenderContext& ctx) override; + void apply(ID3D12GraphicsCommandList4* commandList) override; private: struct PostProcessParams @@ -44,13 +42,20 @@ class PostProcessPass PostProcessParams m_params; + std::unique_ptr m_bloomPass; + + // Colour-grading LUT and a neutral identity LUT fallback. std::shared_ptr m_lutTexture; std::shared_ptr m_identityLut; std::string m_loadedLutPath; int m_lutSize = 2; + // 1x1 placeholder bound to the bloom slot when bloom is inactive. std::shared_ptr m_dummyTexture; - D3D12_GPU_DESCRIPTOR_HANDLE m_bloomSrv{}; - bool m_bloomValid = false; + // Per-frame state captured in prepare(). + RenderSurface* m_surface = nullptr; + D3D12_VIEWPORT m_viewport{}; + D3D12_RECT m_scissorRect{}; + bool m_runBloom = false; }; diff --git a/Engine_Master_UPC/RenderSurface.h b/Engine_Master_UPC/RenderSurface.h index ff2b822f..a7c78e32 100644 --- a/Engine_Master_UPC/RenderSurface.h +++ b/Engine_Master_UPC/RenderSurface.h @@ -17,8 +17,9 @@ class RenderSurface GBUFFER_EMISSIVE = 4, DEPTH_STENCIL = 5, COMPOSITE = 6, + SCENE_HDR = 7, - NUM_ATTACHMENT_POINTS = 7 + NUM_ATTACHMENT_POINTS = 8 }; diff --git a/Engine_Master_UPC/SkyboxPass.cpp b/Engine_Master_UPC/SkyboxPass.cpp index 0ea4ec91..efacad0e 100644 --- a/Engine_Master_UPC/SkyboxPass.cpp +++ b/Engine_Master_UPC/SkyboxPass.cpp @@ -124,7 +124,7 @@ void SkyBoxPass::apply(ID3D12GraphicsCommandList4* commandList) params.flipX = 0; params.flipZ = 0; - auto colorTex = m_renderSurface->getTexture(RenderSurface::COMPOSITE); + auto colorTex = m_renderSurface->getTexture(RenderSurface::SCENE_HDR); auto dsTex = m_renderSurface->getTexture(RenderSurface::DEPTH_STENCIL); D3D12_CPU_DESCRIPTOR_HANDLE rtv = colorTex->getRTV(0).cpu; D3D12_CPU_DESCRIPTOR_HANDLE dsv = dsTex->getDSV().cpu; diff --git a/Engine_Master_UPC/SwapChain.cpp b/Engine_Master_UPC/SwapChain.cpp index aca4e9d2..4912b98d 100644 --- a/Engine_Master_UPC/SwapChain.cpp +++ b/Engine_Master_UPC/SwapChain.cpp @@ -63,7 +63,7 @@ SwapChain::SwapChain(HWND hWnd, ComPtr device, CommandQueue* queu // HDR scene target used by the post-process pipeline (resolved into the backbuffer). auto* hdrTexture = app->getModuleResources()->createHDRRenderTexture(float(m_windowWidth), float(m_windowHeight)); - m_renderSurface.attachTexture( RenderSurface::COLOR_1, std::shared_ptr(hdrTexture)); + m_renderSurface.attachTexture( RenderSurface::SCENE_HDR, std::shared_ptr(hdrTexture)); } SwapChain::~SwapChain() From a31f65fe63f92da4364f17a423a1613076b8178c Mon Sep 17 00:00:00 2001 From: Anand-Ramnarain-27 <142887960+Anand-Ramnarain-27@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:39:44 +0200 Subject: [PATCH 05/14] Bind depth-stencil DSV in PostProcessPass Set desc.DSVFormat to DXGI_FORMAT_D32_FLOAT and use the surface depth-stencil texture's DSV when calling OMSetRenderTargets in PostProcessPass so the composite pass has a proper depth-stencil view. Also remove the obsolete Engine_OUT/GameScripts_runtime_1.dll. --- Engine_Master_UPC/PostProcessPass.cpp | 5 ++++- Engine_OUT/GameScripts_runtime_1.dll | Bin 367616 -> 0 bytes 2 files changed, 4 insertions(+), 1 deletion(-) delete mode 100644 Engine_OUT/GameScripts_runtime_1.dll diff --git a/Engine_Master_UPC/PostProcessPass.cpp b/Engine_Master_UPC/PostProcessPass.cpp index 08893ee4..bfe77c51 100644 --- a/Engine_Master_UPC/PostProcessPass.cpp +++ b/Engine_Master_UPC/PostProcessPass.cpp @@ -70,6 +70,7 @@ PostProcessPass::PostProcessPass(ComPtr device) : m_device(device desc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT); desc.DepthStencilState.DepthEnable = FALSE; desc.DepthStencilState.StencilEnable = FALSE; + desc.DSVFormat = DXGI_FORMAT_D32_FLOAT; desc.SampleMask = UINT_MAX; desc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; desc.NumRenderTargets = 1; @@ -164,8 +165,10 @@ void PostProcessPass::apply(ID3D12GraphicsCommandList4* commandList) const std::shared_ptr& lut = m_lutTexture ? m_lutTexture : m_identityLut; + auto depthTex = m_surface->getTexture(RenderSurface::DEPTH_STENCIL); D3D12_CPU_DESCRIPTOR_HANDLE targetRTV = composite->getRTV(0).cpu; - commandList->OMSetRenderTargets(1, &targetRTV, FALSE, nullptr); + D3D12_CPU_DESCRIPTOR_HANDLE dsv = depthTex->getDSV().cpu; + commandList->OMSetRenderTargets(1, &targetRTV, FALSE, &dsv); commandList->RSSetViewports(1, &m_viewport); commandList->RSSetScissorRects(1, &m_scissorRect); diff --git a/Engine_OUT/GameScripts_runtime_1.dll b/Engine_OUT/GameScripts_runtime_1.dll deleted file mode 100644 index 1c6bca25e9574659e8bd9667bb5416bea60df9ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 367616 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 JSoy#8`#)6JDL4QC From 4e537d6f2cd4bbd82900f96e20a818cf5fe4add4 Mon Sep 17 00:00:00 2001 From: Anand-Ramnarain-27 <142887960+Anand-Ramnarain-27@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:09:20 +0200 Subject: [PATCH 06/14] Add HDR render target; defer tone mapping Introduce an explicit HDR render target and move tone mapping/gamma to the post-process pass. Pixel shader now outputs linear HDR color (no tone mapping/gamma). Added createHDRRenderTexture and attach a R16G16B16A16_FLOAT COLOR_1 target to render surfaces and the swapchain. Updated ModuleRender API to operate on RenderSurface references (renderToSurface, renderScene, wrapper calls, renderGameToBackbuffer) and made resource transitions/clears for the HDR target. PostProcessPass now reads from COLOR_1 and writes to COLOR_0. Skybox pipeline RTV format updated to R16G16B16A16_FLOAT. RenderContext now carries a RenderSurface& for passes that need access to surface textures. These changes implement an HDR pipeline where scene lighting is rendered in HDR and tone mapping/exposure/gamma are applied later in post-processing. --- Engine_Master_UPC/LightPixelShader.hlsl | 5 +-- Engine_Master_UPC/ModuleRender.cpp | 42 +++++++++++++++---------- Engine_Master_UPC/ModuleRender.h | 10 +++--- Engine_Master_UPC/ModuleResources.cpp | 22 +++++++++++++ Engine_Master_UPC/PostProcessPass.cpp | 4 +-- Engine_Master_UPC/RenderContext.h | 2 ++ Engine_Master_UPC/SkyboxPass.cpp | 2 +- Engine_Master_UPC/SwapChain.cpp | 2 +- 8 files changed, 62 insertions(+), 27 deletions(-) diff --git a/Engine_Master_UPC/LightPixelShader.hlsl b/Engine_Master_UPC/LightPixelShader.hlsl index b18126da..4cb6774a 100644 --- a/Engine_Master_UPC/LightPixelShader.hlsl +++ b/Engine_Master_UPC/LightPixelShader.hlsl @@ -351,8 +351,9 @@ float4 main(float3 worldPos : POSITION, float3 normal : NORMAL, float3 tangent : //Calculate final color - float3 colorMapped = PBRNeutralToneMapping(directLighting + indirectLighting + emissive); - float3 finalColor = LinearToSRGB(colorMapped); + // Output linear HDR colour. Exposure, tone mapping and gamma correction are + // applied later by the post-process pass. + float3 finalColor = directLighting + indirectLighting + emissive; diff --git a/Engine_Master_UPC/ModuleRender.cpp b/Engine_Master_UPC/ModuleRender.cpp index 4681128a..6ab07960 100644 --- a/Engine_Master_UPC/ModuleRender.cpp +++ b/Engine_Master_UPC/ModuleRender.cpp @@ -120,17 +120,17 @@ void ModuleRender::preRender() PERF_RENDER("ModuleRender::RenderViewports"); for (const ViewportEntry& entry : m_viewports) { - renderToSurface(commandList, *entry.surface, [&](D3D12_CPU_DESCRIPTOR_HANDLE rtv, D3D12_CPU_DESCRIPTOR_HANDLE dsv) + renderToSurface(commandList, *entry.surface, [&](RenderSurface& surface) { if (entry.type == ViewportType::EDITOR) { PERF_RENDER("ModuleRender::RenderEditorScene"); - renderEditorScene(commandList, rtv, dsv, entry.width, entry.height); + renderEditorScene(commandList, surface, entry.width, entry.height); } else { PERF_RENDER("ModuleRender::RenderPlayScene"); - renderPlayScene(commandList, rtv, dsv, entry.width, entry.height); + renderPlayScene(commandList, surface, entry.width, entry.height); } }); } @@ -162,8 +162,7 @@ void ModuleRender::render() #else renderGameToBackbuffer(commandList, - swapChain->getRenderSurface().getTexture(RenderSurface::COLOR_0)->getRTV().cpu, - swapChain->getRenderSurface().getTexture(RenderSurface::DEPTH_STENCIL)->getDSV().cpu, + const_cast(swapChain->getRenderSurface()), swapChain->getViewport(), swapChain->getScissorRect()); @@ -238,14 +237,15 @@ D3D12_GPU_VIRTUAL_ADDRESS ModuleRender::allocateInRingBuffer(const void* data, s return m_ringBuffer->allocate(data, size, app->getModuleD3D12()->getCurrentFrame()); } -void ModuleRender::renderToSurface(ID3D12GraphicsCommandList4* commandList,RenderSurface& surface,std::function renderFunc) +void ModuleRender::renderToSurface(ID3D12GraphicsCommandList4* commandList,RenderSurface& surface,std::function renderFunc) { + // COLOR_0 is the LDR display target (sampled afterwards by ImGui). It is the + // final destination of post-processing + overlays. auto colorTex = surface.getTexture(RenderSurface::COLOR_0); - auto depthTex = surface.getTexture(RenderSurface::DEPTH_STENCIL); transitionResource(commandList,colorTex->getD3D12Resource(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET); - renderFunc(colorTex->getRTV(0).cpu, depthTex->getDSV().cpu); + renderFunc(surface); transitionResource(commandList, colorTex->getD3D12Resource(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); } @@ -294,10 +294,16 @@ void ModuleRender::transitionResource( ComPtr command commandList->ResourceBarrier(1, &barrier); } -void ModuleRender::renderScene(ID3D12GraphicsCommandList4* commandList, const RenderCamera& camera, D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle, D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle, D3D12_VIEWPORT viewport, D3D12_RECT scissorRect, bool renderDebug, RenderViewType viewType) +void ModuleRender::renderScene(ID3D12GraphicsCommandList4* commandList, const RenderCamera& camera, RenderSurface& surface, D3D12_VIEWPORT viewport, D3D12_RECT scissorRect, bool renderDebug, RenderViewType viewType) { PERF_RENDER(renderDebug ? "ModuleRender::renderScene(Editor)" : "ModuleRender::renderScene(Game)"); + // The scene is rendered into the HDR target (COLOR_1); the PostProcessPass + // resolves it into the LDR display target (COLOR_0) and the overlay passes + // draw on top of that. + auto hdrTex = surface.getTexture(RenderSurface::COLOR_1); + auto depthTex = surface.getTexture(RenderSurface::DEPTH_STENCIL); + RenderContext ctx{ .view = camera.view, .projection = camera.projection, @@ -311,6 +317,7 @@ void ModuleRender::renderScene(ID3D12GraphicsCommandList4* commandList, const Re .uiImageCommands = &app->getModuleUI()->getImageCommands(), .particleCommands = &app->getModuleParticleSystem()->getParticleCommands(), .skyBoxSettings = &app->getModuleScene()->getScene()->getSkyBoxSettings(), + .renderSurface = surface, .shadowData = nullptr, }; @@ -342,9 +349,12 @@ void ModuleRender::renderScene(ID3D12GraphicsCommandList4* commandList, const Re } } + // Make the HDR target writable and clear it (the scene passes render here). + transitionResource(commandList, hdrTex->getD3D12Resource(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_RENDER_TARGET); + { PERF_RENDER("ModuleRender::renderScene::Background"); - renderBackground(commandList, rtvHandle, dsvHandle, viewport, scissorRect); + renderBackground(commandList, hdrTex->getRTV(0).cpu, depthTex->getDSV().cpu, viewport, scissorRect); } @@ -377,15 +387,15 @@ void ModuleRender::renderBackground(ID3D12GraphicsCommandList4* commandList, D3D } #pragma region Wrappers -void ModuleRender::renderEditorScene(ID3D12GraphicsCommandList4* commandList, D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle, D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle, float width, float height) +void ModuleRender::renderEditorScene(ID3D12GraphicsCommandList4* commandList, RenderSurface& surface, float width, float height) { D3D12_VIEWPORT viewport = { 0, 0, width, height, 0, 1 }; D3D12_RECT scissorRect = { 0, 0, static_cast(width), static_cast(height) }; - renderScene(commandList, getEditorCamera(), rtvHandle, dsvHandle, viewport, scissorRect, /*debug=*/true, RenderViewType::Editor); + renderScene(commandList, getEditorCamera(), surface, viewport, scissorRect, /*debug=*/true, RenderViewType::Editor); } -void ModuleRender::renderPlayScene(ID3D12GraphicsCommandList4* commandList, D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle, D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle, float width, float height) +void ModuleRender::renderPlayScene(ID3D12GraphicsCommandList4* commandList, RenderSurface& surface, float width, float height) { const RenderCamera camera = getGameCamera(); if (!camera.valid) return; @@ -393,15 +403,15 @@ void ModuleRender::renderPlayScene(ID3D12GraphicsCommandList4* commandList, D3D1 D3D12_VIEWPORT viewport = { 0, 0, width, height, 0, 1 }; D3D12_RECT scissorRect = { 0, 0, static_cast(width), static_cast(height) }; - renderScene(commandList, camera, rtvHandle, dsvHandle, viewport, scissorRect, m_moduleGameView->getShowDebugWindow(), RenderViewType::Game); + renderScene(commandList, camera, surface, viewport, scissorRect, m_moduleGameView->getShowDebugWindow(), RenderViewType::Game); } -void ModuleRender::renderGameToBackbuffer(ID3D12GraphicsCommandList4* commandList, D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle, D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle, D3D12_VIEWPORT viewport, D3D12_RECT scissorRect) +void ModuleRender::renderGameToBackbuffer(ID3D12GraphicsCommandList4* commandList, RenderSurface& surface, D3D12_VIEWPORT viewport, D3D12_RECT scissorRect) { const RenderCamera camera = getGameCamera(); if (!camera.valid) return; - renderScene(commandList, camera, rtvHandle, dsvHandle, viewport, scissorRect, m_moduleGameView->getShowDebugWindow(), RenderViewType::Game); + renderScene(commandList, camera, surface, viewport, scissorRect, m_moduleGameView->getShowDebugWindow(), RenderViewType::Game); } #pragma endregion diff --git a/Engine_Master_UPC/ModuleRender.h b/Engine_Master_UPC/ModuleRender.h index f935afef..44bb26de 100644 --- a/Engine_Master_UPC/ModuleRender.h +++ b/Engine_Master_UPC/ModuleRender.h @@ -99,14 +99,14 @@ class ModuleRender : public Module void markDebugDrawCacheDirty(); private: - void renderToSurface( ID3D12GraphicsCommandList4* commandList, RenderSurface& surface, std::function renderFunc); + void renderToSurface( ID3D12GraphicsCommandList4* commandList, RenderSurface& surface, std::function renderFunc); // Scene rendering - void renderScene( ID3D12GraphicsCommandList4* commandList, const RenderCamera& camera, D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle, D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle, D3D12_VIEWPORT viewport, D3D12_RECT scissorRect, bool renderDebug, RenderViewType viewType); + void renderScene( ID3D12GraphicsCommandList4* commandList, const RenderCamera& camera, RenderSurface& surface, D3D12_VIEWPORT viewport, D3D12_RECT scissorRect, bool renderDebug, RenderViewType viewType); void renderBackground(ID3D12GraphicsCommandList4* commandList, D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle, D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle, D3D12_VIEWPORT viewport, D3D12_RECT scissorRect); - void renderEditorScene(ID3D12GraphicsCommandList4* commandList, D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle, D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle, float width, float height); - void renderPlayScene(ID3D12GraphicsCommandList4* commandList, D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle, D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle,float width, float height); - void renderGameToBackbuffer( ID3D12GraphicsCommandList4* commandList, D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle, D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle, D3D12_VIEWPORT viewport, D3D12_RECT scissorRect); + void renderEditorScene(ID3D12GraphicsCommandList4* commandList, RenderSurface& surface, float width, float height); + void renderPlayScene(ID3D12GraphicsCommandList4* commandList, RenderSurface& surface, float width, float height); + void renderGameToBackbuffer( ID3D12GraphicsCommandList4* commandList, RenderSurface& surface, D3D12_VIEWPORT viewport, D3D12_RECT scissorRect); // Camera helpers RenderCamera getEditorCamera(); diff --git a/Engine_Master_UPC/ModuleResources.cpp b/Engine_Master_UPC/ModuleResources.cpp index 9e28bce9..3b500e7a 100644 --- a/Engine_Master_UPC/ModuleResources.cpp +++ b/Engine_Master_UPC/ModuleResources.cpp @@ -182,15 +182,37 @@ Texture* ModuleResources::createRenderTexture(float width, float height) return new Texture(GenerateUID(), *m_device.Get(), desc); } +Texture* ModuleResources::createHDRRenderTexture(float width, float height) +{ + // Floating-point colour target so the lit scene can be stored in HDR + // (unclamped) and tone-mapped later as a post-process step. + TextureDesc desc{}; + desc.format = DXGI_FORMAT_R16G16B16A16_FLOAT; + desc.srvFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; + desc.rtvFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; + desc.width = static_cast(width); + desc.height = static_cast(height); + desc.views = TextureView::SRV | TextureView::RTV; + desc.initialState = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; + desc.hasClearValue = true; + desc.clearValue = CD3DX12_CLEAR_VALUE(DXGI_FORMAT_R16G16B16A16_FLOAT, Color(0.0f, 0.2f, 0.4f, 1.0f)); + desc.shaderVisibleSRV = true; + + return new Texture(GenerateUID(), *m_device.Get(), desc); +} + RenderSurface* ModuleResources::createRenderSurface(float width, float height) { auto surface = new RenderSurface(); auto colorTex = std::shared_ptr(app->getModuleResources()->createRenderTexture(width, height)); colorTex->setName(L"RenderSurface_Color"); + auto hdrTex = std::shared_ptr(app->getModuleResources()->createHDRRenderTexture(width, height)); + hdrTex->setName(L"RenderSurface_SceneHDR"); auto depthTex = std::shared_ptr(app->getModuleResources()->createDepthBuffer(width, height)); depthTex->setName(L"RenderSurface_Depth"); surface->attachTexture(RenderSurface::COLOR_0, colorTex); + surface->attachTexture(RenderSurface::COLOR_1, hdrTex); surface->attachTexture(RenderSurface::DEPTH_STENCIL, depthTex); return surface; diff --git a/Engine_Master_UPC/PostProcessPass.cpp b/Engine_Master_UPC/PostProcessPass.cpp index bfe77c51..87cae498 100644 --- a/Engine_Master_UPC/PostProcessPass.cpp +++ b/Engine_Master_UPC/PostProcessPass.cpp @@ -146,8 +146,8 @@ void PostProcessPass::apply(ID3D12GraphicsCommandList4* commandList) if (!m_surface) return; - auto sceneHDR = m_surface->getTexture(RenderSurface::SCENE_HDR); - auto composite = m_surface->getTexture(RenderSurface::COMPOSITE); + auto sceneHDR = m_surface->getTexture(RenderSurface::COLOR_1); + auto composite = m_surface->getTexture(RenderSurface::COLOR_0); if (!sceneHDR || !composite) return; diff --git a/Engine_Master_UPC/RenderContext.h b/Engine_Master_UPC/RenderContext.h index 65233e86..93c3eae5 100644 --- a/Engine_Master_UPC/RenderContext.h +++ b/Engine_Master_UPC/RenderContext.h @@ -9,6 +9,7 @@ struct UITextCommand; struct UIImageCommand; struct SkyBoxSettings; struct ParticleEmitterCommand; +class RenderSurface; struct RenderContext { @@ -27,6 +28,7 @@ struct RenderContext const SkyBoxSettings* skyBoxSettings = nullptr; + RenderSurface& renderSurface; const ShadowFrameData* shadowData = nullptr; }; \ No newline at end of file diff --git a/Engine_Master_UPC/SkyboxPass.cpp b/Engine_Master_UPC/SkyboxPass.cpp index 0a3e85b4..01d04478 100644 --- a/Engine_Master_UPC/SkyboxPass.cpp +++ b/Engine_Master_UPC/SkyboxPass.cpp @@ -81,7 +81,7 @@ SkyBoxPass::SkyBoxPass(ComPtr device, SkyBoxSettings& settings) : desc.SampleMask = UINT_MAX; desc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; desc.NumRenderTargets = 1; - desc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; + desc.RTVFormats[0] = DXGI_FORMAT_R16G16B16A16_FLOAT; // HDR scene target desc.SampleDesc = { 1, 0 }; DXCall(m_device->CreateGraphicsPipelineState(&desc, IID_PPV_ARGS(&m_pipelineState))); diff --git a/Engine_Master_UPC/SwapChain.cpp b/Engine_Master_UPC/SwapChain.cpp index 21ae43b4..9a074e62 100644 --- a/Engine_Master_UPC/SwapChain.cpp +++ b/Engine_Master_UPC/SwapChain.cpp @@ -63,7 +63,7 @@ SwapChain::SwapChain(HWND hWnd, ComPtr device, CommandQueue* queu // HDR scene target used by the post-process pipeline (resolved into the backbuffer). auto* hdrTexture = app->getModuleResources()->createHDRRenderTexture(float(m_windowWidth), float(m_windowHeight)); - m_renderSurface.attachTexture( RenderSurface::SCENE_HDR, std::shared_ptr(hdrTexture)); + m_renderSurface.attachTexture( RenderSurface::COLOR_1, std::shared_ptr(hdrTexture)); } SwapChain::~SwapChain() From c4dd97bbb1864001d19720bdc0fba9ceaa1129f0 Mon Sep 17 00:00:00 2001 From: Anand-Ramnarain-27 <142887960+Anand-Ramnarain-27@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:33:17 +0200 Subject: [PATCH 07/14] Add PostProcessCommon and refactor postprocessing Introduce PostProcessCommon.{h,cpp} with shared helpers (bilinearClampSampler, createFullscreenPSO, drawFullscreenTriangle) to centralize common post-process setup. Refactor BloomPass.cpp and PostProcessPass.cpp to use these helpers (remove duplicated sampler/PSO/draw code) and update comments clarifying HDR/LDR targets. Add new sources to Engine.vcxproj and filters. --- Engine_Master_UPC/BloomPass.cpp | 53 ++---------------- Engine_Master_UPC/Engine.vcxproj | 2 + Engine_Master_UPC/Engine.vcxproj.filters | 6 ++ Engine_Master_UPC/PostProcessCommon.cpp | 71 ++++++++++++++++++++++++ Engine_Master_UPC/PostProcessCommon.h | 21 +++++++ Engine_Master_UPC/PostProcessPass.cpp | 52 ++++------------- 6 files changed, 115 insertions(+), 90 deletions(-) create mode 100644 Engine_Master_UPC/PostProcessCommon.cpp create mode 100644 Engine_Master_UPC/PostProcessCommon.h diff --git a/Engine_Master_UPC/BloomPass.cpp b/Engine_Master_UPC/BloomPass.cpp index f90aba2f..09415855 100644 --- a/Engine_Master_UPC/BloomPass.cpp +++ b/Engine_Master_UPC/BloomPass.cpp @@ -9,6 +9,7 @@ #include "Scene.h" #include "PostProcessSettings.h" +#include "PostProcessCommon.h" #include "Texture.h" #include "UID.h" @@ -26,15 +27,7 @@ BloomPass::BloomPass(ComPtr device) : m_device(device) rootParameters[0].InitAsConstants(sizeof(BloomParams) / sizeof(UINT32), 0, 0, D3D12_SHADER_VISIBILITY_PIXEL); rootParameters[1].InitAsDescriptorTable(1, &srvRange, D3D12_SHADER_VISIBILITY_PIXEL); - D3D12_STATIC_SAMPLER_DESC sampler = {}; - sampler.Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR; - sampler.AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - sampler.AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - sampler.AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - sampler.MaxLOD = D3D12_FLOAT32_MAX; - sampler.ShaderRegister = 0; - sampler.RegisterSpace = 0; - sampler.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + D3D12_STATIC_SAMPLER_DESC sampler = PostProcess::bilinearClampSampler(); CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc; rootSignatureDesc.Init(_countof(rootParameters), rootParameters, 1, &sampler, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT); @@ -69,42 +62,8 @@ BloomPass::BloomPass(ComPtr device) : m_device(device) ComPtr BloomPass::buildPSO(const wchar_t* pixelShaderCso, bool additiveBlend) { - ComPtr vertexShaderBlob; - ThrowIfFailed(D3DReadFileToBlob(L"BRDFVertexShader.cso", &vertexShaderBlob)); - - ComPtr pixelShaderBlob; - ThrowIfFailed(D3DReadFileToBlob(pixelShaderCso, &pixelShaderBlob)); - - D3D12_GRAPHICS_PIPELINE_STATE_DESC desc{}; - desc.InputLayout = { nullptr, 0 }; - desc.pRootSignature = m_rootSignature.Get(); - desc.VS = CD3DX12_SHADER_BYTECODE(vertexShaderBlob.Get()); - desc.PS = CD3DX12_SHADER_BYTECODE(pixelShaderBlob.Get()); - desc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT); - if (additiveBlend) - { - desc.BlendState.RenderTarget[0].BlendEnable = TRUE; - desc.BlendState.RenderTarget[0].SrcBlend = D3D12_BLEND_ONE; - desc.BlendState.RenderTarget[0].DestBlend = D3D12_BLEND_ONE; - desc.BlendState.RenderTarget[0].BlendOp = D3D12_BLEND_OP_ADD; - desc.BlendState.RenderTarget[0].SrcBlendAlpha = D3D12_BLEND_ONE; - desc.BlendState.RenderTarget[0].DestBlendAlpha = D3D12_BLEND_ONE; - desc.BlendState.RenderTarget[0].BlendOpAlpha = D3D12_BLEND_OP_ADD; - } - desc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT); - desc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; - desc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT); - desc.DepthStencilState.DepthEnable = FALSE; - desc.DepthStencilState.StencilEnable = FALSE; - desc.SampleMask = UINT_MAX; - desc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; - desc.NumRenderTargets = 1; - desc.RTVFormats[0] = DXGI_FORMAT_R16G16B16A16_FLOAT; - desc.SampleDesc = { 1, 0 }; - - ComPtr pso; - DXCall(m_device->CreateGraphicsPipelineState(&desc, IID_PPV_ARGS(&pso))); - return pso; + // Bloom chain renders into HDR targets; no depth. + return PostProcess::createFullscreenPSO(m_device.Get(), m_rootSignature.Get(), pixelShaderCso, DXGI_FORMAT_R16G16B16A16_FLOAT, DXGI_FORMAT_UNKNOWN, additiveBlend); } void BloomPass::prepare(const RenderContext& ctx) @@ -141,9 +100,7 @@ void BloomPass::renderLevel(ID3D12GraphicsCommandList4* commandList, ID3D12Pipel commandList->SetGraphicsRoot32BitConstants(0, sizeof(BloomParams) / sizeof(UINT32), &m_params, 0); commandList->SetGraphicsRootDescriptorTable(1, inputSrv); - commandList->IASetVertexBuffers(0, 0, nullptr); - commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); - commandList->DrawInstanced(3, 1, 0, 0); + PostProcess::drawFullscreenTriangle(commandList); CD3DX12_RESOURCE_BARRIER toSRV = CD3DX12_RESOURCE_BARRIER::Transition(tex->getD3D12Resource().Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); commandList->ResourceBarrier(1, &toSRV); diff --git a/Engine_Master_UPC/Engine.vcxproj b/Engine_Master_UPC/Engine.vcxproj index bd8549f0..ea0ce1fc 100644 --- a/Engine_Master_UPC/Engine.vcxproj +++ b/Engine_Master_UPC/Engine.vcxproj @@ -420,6 +420,7 @@ $(SolutionDir)3rdParty\RecastNavigation\DebugUtils\Include; + @@ -814,6 +815,7 @@ $(SolutionDir)3rdParty\RecastNavigation\DebugUtils\Include; + diff --git a/Engine_Master_UPC/Engine.vcxproj.filters b/Engine_Master_UPC/Engine.vcxproj.filters index 2c259a92..9655bd26 100644 --- a/Engine_Master_UPC/Engine.vcxproj.filters +++ b/Engine_Master_UPC/Engine.vcxproj.filters @@ -1745,6 +1745,9 @@ D3D12\Rendering\PostProcessing + + D3D12\Rendering\PostProcessing + @@ -1759,6 +1762,9 @@ D3D12\Rendering\PostProcessing + + D3D12\Rendering\PostProcessing + diff --git a/Engine_Master_UPC/PostProcessCommon.cpp b/Engine_Master_UPC/PostProcessCommon.cpp new file mode 100644 index 00000000..a63db65c --- /dev/null +++ b/Engine_Master_UPC/PostProcessCommon.cpp @@ -0,0 +1,71 @@ +#include "Globals.h" +#include "PostProcessCommon.h" + +#include +#include +#include + +namespace PostProcess +{ + D3D12_STATIC_SAMPLER_DESC bilinearClampSampler(UINT shaderRegister) + { + D3D12_STATIC_SAMPLER_DESC sampler = {}; + sampler.Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR; + sampler.AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + sampler.AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + sampler.AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + sampler.MaxLOD = D3D12_FLOAT32_MAX; + sampler.ShaderRegister = shaderRegister; + sampler.RegisterSpace = 0; + sampler.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + return sampler; + } + + ComPtr createFullscreenPSO(ID3D12Device4* device, ID3D12RootSignature* rootSignature, const wchar_t* pixelShaderCso, DXGI_FORMAT rtvFormat, DXGI_FORMAT dsvFormat, bool additiveBlend) + { + ComPtr vertexShaderBlob; + ThrowIfFailed(D3DReadFileToBlob(L"BRDFVertexShader.cso", &vertexShaderBlob)); + + ComPtr pixelShaderBlob; + ThrowIfFailed(D3DReadFileToBlob(pixelShaderCso, &pixelShaderBlob)); + + D3D12_GRAPHICS_PIPELINE_STATE_DESC desc{}; + desc.InputLayout = { nullptr, 0 }; + desc.pRootSignature = rootSignature; + desc.VS = CD3DX12_SHADER_BYTECODE(vertexShaderBlob.Get()); + desc.PS = CD3DX12_SHADER_BYTECODE(pixelShaderBlob.Get()); + desc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT); + if (additiveBlend) + { + desc.BlendState.RenderTarget[0].BlendEnable = TRUE; + desc.BlendState.RenderTarget[0].SrcBlend = D3D12_BLEND_ONE; + desc.BlendState.RenderTarget[0].DestBlend = D3D12_BLEND_ONE; + desc.BlendState.RenderTarget[0].BlendOp = D3D12_BLEND_OP_ADD; + desc.BlendState.RenderTarget[0].SrcBlendAlpha = D3D12_BLEND_ONE; + desc.BlendState.RenderTarget[0].DestBlendAlpha = D3D12_BLEND_ONE; + desc.BlendState.RenderTarget[0].BlendOpAlpha = D3D12_BLEND_OP_ADD; + } + desc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT); + desc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; + desc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT); + desc.DepthStencilState.DepthEnable = FALSE; + desc.DepthStencilState.StencilEnable = FALSE; + desc.DSVFormat = dsvFormat; + desc.SampleMask = UINT_MAX; + desc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; + desc.NumRenderTargets = 1; + desc.RTVFormats[0] = rtvFormat; + desc.SampleDesc = { 1, 0 }; + + ComPtr pso; + DXCall(device->CreateGraphicsPipelineState(&desc, IID_PPV_ARGS(&pso))); + return pso; + } + + void drawFullscreenTriangle(ID3D12GraphicsCommandList* commandList) + { + commandList->IASetVertexBuffers(0, 0, nullptr); + commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + commandList->DrawInstanced(3, 1, 0, 0); + } +} diff --git a/Engine_Master_UPC/PostProcessCommon.h b/Engine_Master_UPC/PostProcessCommon.h new file mode 100644 index 00000000..9d4b3fec --- /dev/null +++ b/Engine_Master_UPC/PostProcessCommon.h @@ -0,0 +1,21 @@ +#pragma once +#include +#include +#include + +using Microsoft::WRL::ComPtr; + +namespace PostProcess +{ + D3D12_STATIC_SAMPLER_DESC bilinearClampSampler(UINT shaderRegister = 0); + + ComPtr createFullscreenPSO( + ID3D12Device4* device, + ID3D12RootSignature* rootSignature, + const wchar_t* pixelShaderCso, + DXGI_FORMAT rtvFormat, + DXGI_FORMAT dsvFormat = DXGI_FORMAT_UNKNOWN, + bool additiveBlend = false); + + void drawFullscreenTriangle(ID3D12GraphicsCommandList* commandList); +} diff --git a/Engine_Master_UPC/PostProcessPass.cpp b/Engine_Master_UPC/PostProcessPass.cpp index 87cae498..95d8e5c9 100644 --- a/Engine_Master_UPC/PostProcessPass.cpp +++ b/Engine_Master_UPC/PostProcessPass.cpp @@ -10,6 +10,7 @@ #include "Scene.h" #include "PostProcessSettings.h" +#include "PostProcessCommon.h" #include "Texture.h" #include "CubeLut.h" #include "BloomPass.h" @@ -34,15 +35,7 @@ PostProcessPass::PostProcessPass(ComPtr device) : m_device(device rootParameters[2].InitAsDescriptorTable(1, &bloomRange, D3D12_SHADER_VISIBILITY_PIXEL); rootParameters[3].InitAsDescriptorTable(1, &lutRange, D3D12_SHADER_VISIBILITY_PIXEL); - D3D12_STATIC_SAMPLER_DESC sampler = {}; - sampler.Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR; - sampler.AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - sampler.AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - sampler.AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; - sampler.MaxLOD = D3D12_FLOAT32_MAX; - sampler.ShaderRegister = 0; - sampler.RegisterSpace = 0; - sampler.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + D3D12_STATIC_SAMPLER_DESC sampler = PostProcess::bilinearClampSampler(); CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc; rootSignatureDesc.Init(_countof(rootParameters), rootParameters, 1, &sampler, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT); @@ -52,32 +45,9 @@ PostProcessPass::PostProcessPass(ComPtr device) : m_device(device DXCall(D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error)); DXCall(m_device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&m_rootSignature))); - // Reuse the shared fullscreen-triangle vertex shader. - ComPtr vertexShaderBlob; - ThrowIfFailed(D3DReadFileToBlob(L"BRDFVertexShader.cso", &vertexShaderBlob)); - - ComPtr pixelShaderBlob; - ThrowIfFailed(D3DReadFileToBlob(L"PostProcessPixelShader.cso", &pixelShaderBlob)); - - D3D12_GRAPHICS_PIPELINE_STATE_DESC desc{}; - desc.InputLayout = { nullptr, 0 }; - desc.pRootSignature = m_rootSignature.Get(); - desc.VS = CD3DX12_SHADER_BYTECODE(vertexShaderBlob.Get()); - desc.PS = CD3DX12_SHADER_BYTECODE(pixelShaderBlob.Get()); - desc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT); - desc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT); - desc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; - desc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT); - desc.DepthStencilState.DepthEnable = FALSE; - desc.DepthStencilState.StencilEnable = FALSE; - desc.DSVFormat = DXGI_FORMAT_D32_FLOAT; - desc.SampleMask = UINT_MAX; - desc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; - desc.NumRenderTargets = 1; - desc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; - desc.SampleDesc = { 1, 0 }; - - DXCall(m_device->CreateGraphicsPipelineState(&desc, IID_PPV_ARGS(&m_pipelineState))); + // Resolve into the LDR display target (COLOR_0); keep a matching depth + // format so the depth view can stay bound for the overlay passes. + m_pipelineState = PostProcess::createFullscreenPSO(m_device.Get(), m_rootSignature.Get(), L"PostProcessPixelShader.cso", DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_D32_FLOAT); // Bloom is produced internally and fed into the resolve. m_bloomPass = std::make_unique(m_device); @@ -151,11 +121,11 @@ void PostProcessPass::apply(ID3D12GraphicsCommandList4* commandList) if (!sceneHDR || !composite) return; - // The scene passes left SCENE_HDR as a render target; make it readable. + // The scene passes left the HDR target (COLOR_1) as a render target; make it readable. CD3DX12_RESOURCE_BARRIER toSRV = CD3DX12_RESOURCE_BARRIER::Transition(sceneHDR->getD3D12Resource().Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); commandList->ResourceBarrier(1, &toSRV); - // Bloom reads SCENE_HDR and produces its own blurred texture. + // Bloom reads the HDR scene and produces its own blurred texture. D3D12_GPU_DESCRIPTOR_HANDLE bloomHandle = m_dummyTexture->getSRV().gpu; if (m_runBloom) { @@ -183,10 +153,8 @@ void PostProcessPass::apply(ID3D12GraphicsCommandList4* commandList) commandList->SetGraphicsRootDescriptorTable(2, bloomHandle); commandList->SetGraphicsRootDescriptorTable(3, lut->getSRV().gpu); - commandList->IASetVertexBuffers(0, 0, nullptr); - commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); - commandList->DrawInstanced(3, 1, 0, 0); + PostProcess::drawFullscreenTriangle(commandList); - // Leave SCENE_HDR in PIXEL_SHADER_RESOURCE for the next frame's scene pass, - // which will transition it back to RENDER_TARGET. + // Leave COLOR_1 (HDR) in PIXEL_SHADER_RESOURCE for the next frame's scene + // pass, which will transition it back to RENDER_TARGET. } From 0ec4a6ede2e03159558cc721684052fba88dd2e8 Mon Sep 17 00:00:00 2001 From: Anand-Ramnarain-27 <142887960+Anand-Ramnarain-27@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:28:37 +0200 Subject: [PATCH 08/14] Add heartbeat and death-fade postprocess effects Implement heartbeat (low-health damage) and death-fade postprocess effects. - PostProcessPass: advance time-based effects once per frame, clamp dt; add heartbeat state machine and death-fade timer; expose new PostProcessParams fields and internal state for pulse/sway/blur; update parameter packing and per-frame updates. - PostProcessPixelShader.hlsl: extend constant buffer with heartbeat and death-fade inputs; add scene blur helper, heartbeat color/tint/darken logic, sample sway, and apply desaturation/fade to black; ensure bloom samples follow sway. - PostProcessSettings: add editable knobs for enabling heartbeat, health threshold, runtime health/separation inputs (test-only), and death-fade durations/trigger. - SceneConfig: add ImGui controls for heartbeat and death-fade testing (health, separation, grey/black durations). These changes add CPU-driven heartbeat visuals (vignette, pulse, sway, desaturation) and layered death fade (blur -> grey -> black), with authorable settings and editor test UI. --- Engine_Master_UPC/PostProcessPass.cpp | 112 ++++++++++++++++++ Engine_Master_UPC/PostProcessPass.h | 43 ++++++- Engine_Master_UPC/PostProcessPixelShader.hlsl | 109 +++++++++++++++-- Engine_Master_UPC/PostProcessSettings.h | 20 ++++ Engine_Master_UPC/SceneConfig.cpp | 13 ++ 5 files changed, 286 insertions(+), 11 deletions(-) diff --git a/Engine_Master_UPC/PostProcessPass.cpp b/Engine_Master_UPC/PostProcessPass.cpp index 95d8e5c9..08429788 100644 --- a/Engine_Master_UPC/PostProcessPass.cpp +++ b/Engine_Master_UPC/PostProcessPass.cpp @@ -7,6 +7,7 @@ #include "Application.h" #include "ModuleDescriptors.h" #include "ModuleScene.h" +#include "ModuleTime.h" #include "Scene.h" #include "PostProcessSettings.h" @@ -19,6 +20,8 @@ #include #include #include +#include +#include PostProcessPass::PostProcessPass(ComPtr device) : m_device(device) { @@ -109,6 +112,115 @@ void PostProcessPass::prepare(const RenderContext& ctx) if (m_runBloom) m_bloomPass->prepare(ctx); + + // Advance time-based effects once per frame (even with several viewports). + // Unscaled time keeps them animating in the editor / when the game is paused. + const uint32_t frame = app->getModuleTime()->frameCount(); + const float dt = (frame != m_lastFrame) ? std::min(app->getModuleTime()->unscaledDeltaTime(), 0.05f) : 0.0f; + m_lastFrame = frame; + + // Heartbeat / low-health damage screen effect. + m_params.enableHeartbeat = settings.heartbeatEnabled ? 1u : 0u; + if (settings.heartbeatEnabled) + updateHeartbeat(settings, ctx, dt); + + // Death fade (grey then black) — independent of the heartbeat. + updateDeathFade(settings, dt); +} + +void PostProcessPass::updateDeathFade(const PostProcessSettings& settings, float dt) +{ + if (settings.deathFadeActive) + m_deathTime += dt; + else + m_deathTime = 0.0f; + + const float grey = std::max(0.01f, settings.deathGreyDuration); + const float black = std::max(0.01f, settings.deathBlackDuration); + + // First desaturate to full grey (and blur out of focus), then fade from + // grey to black. + m_params.deathDesat = std::min(1.0f, m_deathTime / grey); + m_params.deathBlur = m_params.deathDesat; + m_params.deathFade = std::min(1.0f, std::max(0.0f, (m_deathTime - grey) / black)); +} + +void PostProcessPass::updateHeartbeat(const PostProcessSettings& settings, const RenderContext& ctx, float dt) +{ + auto saturate01 = [](float v) { return std::max(0.0f, std::min(1.0f, v)); }; + + const float health = saturate01(settings.health); + const float sep = saturate01(settings.separation); + + const float hpDanger = std::max(0.0f, 1.0f - health); + const float sepDanger = sep; + const bool healthActive = health < settings.healthThreshold; + const bool sepActive = sepDanger > 0.05f; + + // Faster heart + sharper diastole as danger rises (ported from the preview). + const float danger = healthActive ? hpDanger : (sepActive ? sepDanger * 0.8f : 0.0f); + auto interBeatSeconds = [](float t) { return 0.12f + (1.0f - t) * 0.55f; }; + auto diastoleSeconds = [](float t) { return 0.18f + (1.0f - t) * 0.80f; }; + auto fireLub = [&](float t) + { + m_hbDubTimer = interBeatSeconds(t); + m_hbLubTimer = -1.0f; + m_hbPulseAnim = 1.0f; + m_hbPulseType = 0; + }; + + // Screen sway (only when critically low health). + m_hbSwayAngle += dt * 0.4f; + const float critT = healthActive ? std::max(0.0f, (hpDanger - 0.5f) / 0.5f) : 0.0f; + const float swayAmt = critT * 4.0f; // pixels + const float swayXpx = std::sin(m_hbSwayAngle) * swayAmt; + const float swayYpx = std::cos(m_hbSwayAngle * 0.7f) * swayAmt * 0.5f; + + // Lub-dub state machine. + if (healthActive) + { + if (m_hbDubTimer < 0.0f && m_hbLubTimer < 0.0f) + fireLub(hpDanger); + + if (m_hbDubTimer >= 0.0f) + { + m_hbDubTimer -= dt; + if (m_hbDubTimer < 0.0f) + { + m_hbPulseAnim = 0.6f; + m_hbPulseType = 1; + m_hbLubTimer = diastoleSeconds(hpDanger); + } + } + if (m_hbLubTimer >= 0.0f) + { + m_hbLubTimer -= dt; + if (m_hbLubTimer < 0.0f) + fireLub(hpDanger); + } + } + else + { + m_hbDubTimer = -1.0f; + m_hbLubTimer = -1.0f; + m_hbPulseAnim = std::max(0.0f, m_hbPulseAnim - dt * 4.0f); + } + + if (sepActive && m_hbDubTimer < 0.0f && m_hbLubTimer < 0.0f && !healthActive) + fireLub(sepDanger * 0.8f); + + m_hbPulseAnim = std::max(0.0f, m_hbPulseAnim - dt * 3.5f); + (void)danger; + + // Derived per-frame outputs fed to the shader. + m_params.hbHealthVignette = healthActive ? std::pow(hpDanger, 1.4f) * 0.7f : 0.0f; + m_params.hbSepVignette = sepActive ? std::pow(sepDanger, 1.3f) * 0.55f : 0.0f; + m_params.hbPulse = m_hbPulseAnim * (m_hbPulseType == 0 ? 1.0f : 0.55f) * std::max(hpDanger, sepDanger * 0.7f); + m_params.hbPulseIsLub = (m_hbPulseType == 0) ? 1u : 0u; + m_params.hbCrit = critT * 0.75f; + m_params.hbDesat = hpDanger * 0.6f + sepDanger * 0.25f; + m_params.hbSwayX = (ctx.viewport.Width > 0.0f) ? swayXpx / ctx.viewport.Width : 0.0f; + m_params.hbSwayY = (ctx.viewport.Height > 0.0f) ? swayYpx / ctx.viewport.Height : 0.0f; } void PostProcessPass::apply(ID3D12GraphicsCommandList4* commandList) diff --git a/Engine_Master_UPC/PostProcessPass.h b/Engine_Master_UPC/PostProcessPass.h index ec28fd2b..1a8a0e32 100644 --- a/Engine_Master_UPC/PostProcessPass.h +++ b/Engine_Master_UPC/PostProcessPass.h @@ -9,6 +9,7 @@ using Microsoft::WRL::ComPtr; struct RenderContext; +struct PostProcessSettings; class RenderSurface; class Texture; class BloomPass; @@ -25,17 +26,44 @@ class PostProcessPass : public IRenderPass private: struct PostProcessParams { + // row 0 float exposure = 0.0f; float bloomIntensity = 0.5f; float lutSize = 2.0f; float caStrength = 1.0f; + // row 1 uint32_t enableBloom = 0; uint32_t enableLUT = 0; uint32_t enableCA = 0; - uint32_t pad0 = 0; + uint32_t enableHeartbeat = 0; + + // row 2 — heartbeat outputs + float hbHealthVignette = 0.0f; + float hbSepVignette = 0.0f; + float hbPulse = 0.0f; + uint32_t hbPulseIsLub = 1; + + // row 3 — heartbeat outputs + float hbCrit = 0.0f; + float hbDesat = 0.0f; + float hbSwayX = 0.0f; + float hbSwayY = 0.0f; + + // row 4 — death fade outputs + float deathDesat = 0.0f; // 0..1 desaturation towards grey + float deathFade = 0.0f; // 0..1 fade towards black + float deathBlur = 0.0f; // 0..1 out-of-focus blur amount + float deathPad0 = 0.0f; }; + // Advances the heartbeat state machine and fills the heartbeat fields of + // m_params from the current health/separation. dt is the per-frame delta. + void updateHeartbeat(const PostProcessSettings& settings, const RenderContext& ctx, float dt); + + // Advances the death-fade timer and fills the death fields of m_params. + void updateDeathFade(const PostProcessSettings& settings, float dt); + ComPtr m_device; ComPtr m_rootSignature; ComPtr m_pipelineState; @@ -58,4 +86,17 @@ class PostProcessPass : public IRenderPass D3D12_VIEWPORT m_viewport{}; D3D12_RECT m_scissorRect{}; bool m_runBloom = false; + + // Time-based effects are advanced once per frame, guarded by this index. + uint32_t m_lastFrame = 0xFFFFFFFFu; + + // Heartbeat timing state. + float m_hbDubTimer = -1.0f; + float m_hbLubTimer = -1.0f; + float m_hbPulseAnim = 0.0f; + int m_hbPulseType = 0; // 0 = lub, 1 = dub + float m_hbSwayAngle = 0.0f; + + // Death-fade timing state. + float m_deathTime = 0.0f; }; diff --git a/Engine_Master_UPC/PostProcessPixelShader.hlsl b/Engine_Master_UPC/PostProcessPixelShader.hlsl index 1dd47d31..770311a1 100644 --- a/Engine_Master_UPC/PostProcessPixelShader.hlsl +++ b/Engine_Master_UPC/PostProcessPixelShader.hlsl @@ -10,13 +10,30 @@ cbuffer PostProcessParams : register(b0) { float exposure; float bloomIntensity; - float lutSize; - float caStrength; + float lutSize; + float caStrength; uint enableBloom; uint enableLUT; uint enableCA; - uint pad0; + uint enableHeartbeat; + + // Heartbeat / damage screen effect (computed on the CPU each frame). + float hbHealthVignette; + float hbSepVignette; + float hbPulse; + uint hbPulseIsLub; + + float hbCrit; + float hbDesat; + float hbSwayX; + float hbSwayY; + + // Death fade. + float deathDesat; + float deathFade; + float deathBlur; + float deathPad0; }; float3 sampleScene(float2 uv) @@ -40,22 +57,94 @@ float3 applyLUT(float3 color) return lutTexture.SampleLevel(bilinearClamp, uvw, 0).rgb; } +// Out-of-focus disc blur of the scene, used by the death fade. 17 taps in two +// rings; radius grows with the blur amount. +float3 sampleSceneBlurred(float2 uv, float blur) +{ + if (blur <= 0.001) + return sampleScene(uv); + + const float2 dirs[8] = + { + float2( 1.0, 0.0), float2(-1.0, 0.0), float2(0.0, 1.0), float2(0.0, -1.0), + float2( 0.707, 0.707), float2(-0.707, 0.707), float2(0.707, -0.707), float2(-0.707, -0.707) + }; + + float2 radius = blur * 0.02; // up to ~2% of the screen + float3 sum = sceneTexture.Sample(bilinearClamp, uv).rgb; + + [unroll] + for (int i = 0; i < 8; ++i) + { + sum += sceneTexture.Sample(bilinearClamp, uv + dirs[i] * radius).rgb; + sum += sceneTexture.Sample(bilinearClamp, uv + dirs[i] * radius * 0.5).rgb; + } + + return sum / 17.0; +} + +// Layered low-health "damage screen": desaturation, red/blue vignettes, +// heartbeat pulse tint and critical edge darkening. Applied to the final +// displayed colour (after tone mapping + gamma). +float3 applyHeartbeat(float3 color, float2 uv) +{ + // Desaturate towards luminance. + float lum = dot(color, float3(0.299, 0.587, 0.114)); + color = lerp(color, lum.xxx, saturate(hbDesat)); + + // Radial distance from the screen centre (0 centre .. ~1 corner). + float r = saturate(length(uv - 0.5) * 1.4); + float edge = smoothstep(0.3, 1.0, r); + + // Health vignette (red): tint + darken the edges. + float hv = edge * hbHealthVignette; + color = lerp(color, float3(0.5, 0.0, 0.0), hv); + color *= 1.0 - hv * 0.5; + + // Separation vignette (blue). + float sv = edge * hbSepVignette; + color = lerp(color, float3(0.0, 0.1, 0.4), sv); + + // Heartbeat pulse: warm flash on the "lub", cooler on the "dub". + float3 pulseCol = hbPulseIsLub ? float3(1.0, 0.78, 0.70) : float3(0.70, 0.86, 1.0); + color += pulseCol * (hbPulse * (hbPulseIsLub ? 0.25 : 0.18)); + + // Critical edge darkening. + color *= 1.0 - edge * hbCrit; + + return color; +} + float4 main(float2 uv : TEXCOORD) : SV_TARGET { - float3 hdr = sampleScene(uv); + // Heartbeat sway shifts the scene sampling (zero when the effect is off). + float2 suv = uv + float2(hbSwayX, hbSwayY); + + // Scene sample, blurred out of focus during the death fade. + float3 hdr = sampleSceneBlurred(suv, deathBlur); if (enableBloom != 0) { - float3 bloom = bloomTexture.Sample(bilinearClamp, uv).rgb; + float3 bloom = bloomTexture.Sample(bilinearClamp, suv).rgb; hdr += bloom * bloomIntensity; } - + hdr *= 1.2 * pow(2.0, exposure); - + float3 mapped = PBRNeutralToneMapping(hdr); - + if (enableLUT != 0) mapped = applyLUT(mapped); - - return float4(LinearToSRGB(mapped), 1.0); + + float3 outColor = LinearToSRGB(mapped); + + if (enableHeartbeat != 0) + outColor = applyHeartbeat(outColor, uv); + + // Death fade: desaturate to grey, then fade fully to black (no-op when both 0). + float deathLum = dot(outColor, float3(0.299, 0.587, 0.114)); + outColor = lerp(outColor, deathLum.xxx, saturate(deathDesat)); + outColor *= 1.0 - saturate(deathFade); + + return float4(outColor, 1.0); } diff --git a/Engine_Master_UPC/PostProcessSettings.h b/Engine_Master_UPC/PostProcessSettings.h index 7c24950d..5c524e8d 100644 --- a/Engine_Master_UPC/PostProcessSettings.h +++ b/Engine_Master_UPC/PostProcessSettings.h @@ -20,6 +20,19 @@ struct PostProcessSettings : public ISerializable bool chromaticAberrationEnabled = false; float chromaticAberrationStrength = 1.0f; + // Heartbeat / low-health "damage screen" effect. + bool heartbeatEnabled = false; + float healthThreshold = 0.5f; // health below this triggers the effect + // Runtime inputs (normally driven by gameplay: PlayerDamageable / Bound). + float health = 1.0f; // 0..1 + float separation = 0.0f; // 0..1 + + // Death fade: desaturate to grey, then fade fully to black. Triggered by + // gameplay when every player is down (health 0, death animation playing). + bool deathFadeActive = false; // runtime trigger + float deathGreyDuration = 1.5f; // seconds to reach full grey + float deathBlackDuration = 1.5f;// seconds to fade from grey to black + void serialize(IArchive& archive) override { archive.serialize(exposure, "exposure"); @@ -33,5 +46,12 @@ struct PostProcessSettings : public ISerializable archive.serialize(chromaticAberrationEnabled, "chromaticAberrationEnabled"); archive.serialize(chromaticAberrationStrength, "chromaticAberrationStrength"); + + // Only the authored knobs persist; health/separation are runtime inputs. + archive.serialize(heartbeatEnabled, "heartbeatEnabled"); + archive.serialize(healthThreshold, "healthThreshold"); + + archive.serialize(deathGreyDuration, "deathGreyDuration"); + archive.serialize(deathBlackDuration, "deathBlackDuration"); } }; diff --git a/Engine_Master_UPC/SceneConfig.cpp b/Engine_Master_UPC/SceneConfig.cpp index 13f74172..f6336777 100644 --- a/Engine_Master_UPC/SceneConfig.cpp +++ b/Engine_Master_UPC/SceneConfig.cpp @@ -214,6 +214,19 @@ void SceneConfig::drawPostProcessSettings() ImGui::Separator(); ImGui::Checkbox("Chromatic Aberration###PPCAEnabled", &pp.chromaticAberrationEnabled); ImGui::DragFloat("CA Strength###PPCAStrength", &pp.chromaticAberrationStrength, 0.05f, 0.0f, 10.0f); + + ImGui::Separator(); + ImGui::Checkbox("Heartbeat (Damage FX)###PPHeartbeat", &pp.heartbeatEnabled); + ImGui::DragFloat("Health Threshold###PPHealthThreshold", &pp.healthThreshold, 0.01f, 0.0f, 1.0f); + ImGui::SliderFloat("Health (test)###PPHealth", &pp.health, 0.0f, 1.0f); + ImGui::SliderFloat("Separation (test)###PPSeparation", &pp.separation, 0.0f, 1.0f); + ImGui::TextDisabled("Health/Separation are normally driven by gameplay."); + + ImGui::Separator(); + ImGui::Checkbox("Death Fade (test)###PPDeath", &pp.deathFadeActive); + ImGui::DragFloat("Grey Duration (s)###PPDeathGrey", &pp.deathGreyDuration, 0.05f, 0.1f, 10.0f); + ImGui::DragFloat("Black Duration (s)###PPDeathBlack", &pp.deathBlackDuration, 0.05f, 0.1f, 10.0f); + ImGui::TextDisabled("Triggered by gameplay when all players are down."); } } From 401b3874f3949d8aa0e6dad6059bf7585e4d3a73 Mon Sep 17 00:00:00 2001 From: Anand-Ramnarain-27 <142887960+Anand-Ramnarain-27@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:51:20 +0200 Subject: [PATCH 09/14] Add depth-based outline post-process effect Introduce a depth-driven "ink" outline in the post-process pass. Key changes: - PostProcessPixelShader.hlsl: add depthTexture (t3), noise/hash helpers and applyOutline() which detects edges from the depth buffer and blends an ink colour. Hook outline into the final color path. - PostProcessPass.* and ModuleResources.cpp: make depth buffer shader-visible (shaderVisibleSRV), add depth SRV to the root signature and bind it (t3). Transition the depth resource to PIXEL_SHADER_RESOURCE for the post-process, then restore it to DEPTH_WRITE and re-bind the DSV for subsequent overlay passes. - PostProcessPass.h / PostProcessSettings.h / SceneConfig.cpp: add parameters for outline (enabled, thickness, threshold, intensity, colour, wobble, noise scale, breakup), expose them in the editor UI and propagate them into PostProcessParams. These changes enable a tunable, depth-based outline effect while preserving correct depth state for later overlay rendering. --- Engine_Master_UPC/ModuleResources.cpp | 1 + Engine_Master_UPC/PostProcessPass.cpp | 68 ++++++++----- Engine_Master_UPC/PostProcessPass.h | 35 ++++--- Engine_Master_UPC/PostProcessPixelShader.hlsl | 96 +++++++++++++++---- Engine_Master_UPC/PostProcessSettings.h | 39 ++++++-- Engine_Master_UPC/SceneConfig.cpp | 17 ++++ 6 files changed, 189 insertions(+), 67 deletions(-) diff --git a/Engine_Master_UPC/ModuleResources.cpp b/Engine_Master_UPC/ModuleResources.cpp index 3b500e7a..935eab48 100644 --- a/Engine_Master_UPC/ModuleResources.cpp +++ b/Engine_Master_UPC/ModuleResources.cpp @@ -142,6 +142,7 @@ Texture* ModuleResources::createDepthBuffer(float width, float height) desc.initialState = D3D12_RESOURCE_STATE_DEPTH_WRITE; desc.hasClearValue = true; desc.clearValue = CD3DX12_CLEAR_VALUE(DXGI_FORMAT_D32_FLOAT, 1.0f, 0); + desc.shaderVisibleSRV = true; // sampleable by the post-process outline pass return new Texture(GenerateUID(), *m_device.Get(), desc); } diff --git a/Engine_Master_UPC/PostProcessPass.cpp b/Engine_Master_UPC/PostProcessPass.cpp index 08429788..6891fba0 100644 --- a/Engine_Master_UPC/PostProcessPass.cpp +++ b/Engine_Master_UPC/PostProcessPass.cpp @@ -25,18 +25,18 @@ PostProcessPass::PostProcessPass(ComPtr device) : m_device(device) { - // Root signature: b0 (params) + t0 (HDR scene) + t1 (bloom) + t2 (LUT 3D), - // with a static bilinear-clamp sampler at s0. - CD3DX12_DESCRIPTOR_RANGE sceneRange, bloomRange, lutRange; - sceneRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0, 0); + CD3DX12_DESCRIPTOR_RANGE sceneRange, bloomRange, lutRange, depthRange; + sceneRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0, 0); bloomRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 1, 0); - lutRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 2, 0); + lutRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 2, 0); + depthRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 3, 0); - CD3DX12_ROOT_PARAMETER rootParameters[4] = {}; + CD3DX12_ROOT_PARAMETER rootParameters[5] = {}; rootParameters[0].InitAsConstants(sizeof(PostProcessParams) / sizeof(UINT32), 0, 0, D3D12_SHADER_VISIBILITY_PIXEL); rootParameters[1].InitAsDescriptorTable(1, &sceneRange, D3D12_SHADER_VISIBILITY_PIXEL); rootParameters[2].InitAsDescriptorTable(1, &bloomRange, D3D12_SHADER_VISIBILITY_PIXEL); rootParameters[3].InitAsDescriptorTable(1, &lutRange, D3D12_SHADER_VISIBILITY_PIXEL); + rootParameters[4].InitAsDescriptorTable(1, &depthRange, D3D12_SHADER_VISIBILITY_PIXEL); D3D12_STATIC_SAMPLER_DESC sampler = PostProcess::bilinearClampSampler(); @@ -48,14 +48,10 @@ PostProcessPass::PostProcessPass(ComPtr device) : m_device(device DXCall(D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error)); DXCall(m_device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&m_rootSignature))); - // Resolve into the LDR display target (COLOR_0); keep a matching depth - // format so the depth view can stay bound for the overlay passes. - m_pipelineState = PostProcess::createFullscreenPSO(m_device.Get(), m_rootSignature.Get(), L"PostProcessPixelShader.cso", DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_D32_FLOAT); + m_pipelineState = PostProcess::createFullscreenPSO(m_device.Get(), m_rootSignature.Get(), L"PostProcessPixelShader.cso", DXGI_FORMAT_R8G8B8A8_UNORM); - // Bloom is produced internally and fed into the resolve. m_bloomPass = std::make_unique(m_device); - // Neutral identity LUT used whenever colour grading is disabled. m_identityLut = CubeLut::createIdentity(*m_device.Get(), 2); // 1x1 placeholder for the bloom slot when bloom is inactive. @@ -106,6 +102,18 @@ void PostProcessPass::prepare(const RenderContext& ctx) m_params.enableCA = settings.chromaticAberrationEnabled ? 1u : 0u; m_params.caStrength = settings.chromaticAberrationStrength; + // Ink outline. + m_params.enableOutline = settings.outlineEnabled ? 1u : 0u; + m_params.outlineThickness = settings.outlineThickness; + m_params.outlineThreshold = settings.outlineThreshold; + m_params.outlineIntensity = settings.outlineIntensity; + m_params.outlineColorR = settings.outlineColorR; + m_params.outlineColorG = settings.outlineColorG; + m_params.outlineColorB = settings.outlineColorB; + m_params.outlineWobble = settings.outlineWobble; + m_params.outlineNoiseScale = settings.outlineNoiseScale; + m_params.outlineBreakup = settings.outlineBreakup; + m_runBloom = settings.bloomEnabled; m_params.enableBloom = settings.bloomEnabled ? 1u : 0u; m_params.bloomIntensity = settings.bloomIntensity; @@ -113,8 +121,8 @@ void PostProcessPass::prepare(const RenderContext& ctx) if (m_runBloom) m_bloomPass->prepare(ctx); - // Advance time-based effects once per frame (even with several viewports). - // Unscaled time keeps them animating in the editor / when the game is paused. + //time-based effects once per frame. + // Unscaled time keeps them animating in the editor const uint32_t frame = app->getModuleTime()->frameCount(); const float dt = (frame != m_lastFrame) ? std::min(app->getModuleTime()->unscaledDeltaTime(), 0.05f) : 0.0f; m_lastFrame = frame; @@ -138,8 +146,7 @@ void PostProcessPass::updateDeathFade(const PostProcessSettings& settings, float const float grey = std::max(0.01f, settings.deathGreyDuration); const float black = std::max(0.01f, settings.deathBlackDuration); - // First desaturate to full grey (and blur out of focus), then fade from - // grey to black. + // First desaturate to full grey (and blur out of focus), then fade from grey to black. m_params.deathDesat = std::min(1.0f, m_deathTime / grey); m_params.deathBlur = m_params.deathDesat; m_params.deathFade = std::min(1.0f, std::max(0.0f, (m_deathTime - grey) / black)); @@ -157,7 +164,7 @@ void PostProcessPass::updateHeartbeat(const PostProcessSettings& settings, const const bool healthActive = health < settings.healthThreshold; const bool sepActive = sepDanger > 0.05f; - // Faster heart + sharper diastole as danger rises (ported from the preview). + // Faster heart + sharper diastole as danger rises const float danger = healthActive ? hpDanger : (sepActive ? sepDanger * 0.8f : 0.0f); auto interBeatSeconds = [](float t) { return 0.12f + (1.0f - t) * 0.55f; }; auto diastoleSeconds = [](float t) { return 0.18f + (1.0f - t) * 0.80f; }; @@ -169,14 +176,13 @@ void PostProcessPass::updateHeartbeat(const PostProcessSettings& settings, const m_hbPulseType = 0; }; - // Screen sway (only when critically low health). + // Screen sway m_hbSwayAngle += dt * 0.4f; const float critT = healthActive ? std::max(0.0f, (hpDanger - 0.5f) / 0.5f) : 0.0f; const float swayAmt = critT * 4.0f; // pixels const float swayXpx = std::sin(m_hbSwayAngle) * swayAmt; const float swayYpx = std::cos(m_hbSwayAngle * 0.7f) * swayAmt * 0.5f; - // Lub-dub state machine. if (healthActive) { if (m_hbDubTimer < 0.0f && m_hbLubTimer < 0.0f) @@ -212,7 +218,6 @@ void PostProcessPass::updateHeartbeat(const PostProcessSettings& settings, const m_hbPulseAnim = std::max(0.0f, m_hbPulseAnim - dt * 3.5f); (void)danger; - // Derived per-frame outputs fed to the shader. m_params.hbHealthVignette = healthActive ? std::pow(hpDanger, 1.4f) * 0.7f : 0.0f; m_params.hbSepVignette = sepActive ? std::pow(sepDanger, 1.3f) * 0.55f : 0.0f; m_params.hbPulse = m_hbPulseAnim * (m_hbPulseType == 0 ? 1.0f : 0.55f) * std::max(hpDanger, sepDanger * 0.7f); @@ -230,12 +235,16 @@ void PostProcessPass::apply(ID3D12GraphicsCommandList4* commandList) auto sceneHDR = m_surface->getTexture(RenderSurface::COLOR_1); auto composite = m_surface->getTexture(RenderSurface::COLOR_0); - if (!sceneHDR || !composite) + auto depthTex = m_surface->getTexture(RenderSurface::DEPTH_STENCIL); + if (!sceneHDR || !composite || !depthTex) return; - // The scene passes left the HDR target (COLOR_1) as a render target; make it readable. - CD3DX12_RESOURCE_BARRIER toSRV = CD3DX12_RESOURCE_BARRIER::Transition(sceneHDR->getD3D12Resource().Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); - commandList->ResourceBarrier(1, &toSRV); + // Make the HDR scene and the depth buffer readable in the shader (the depth buffer feeds the outline edge detection). + CD3DX12_RESOURCE_BARRIER toRead[2] = { + CD3DX12_RESOURCE_BARRIER::Transition(sceneHDR->getD3D12Resource().Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE), + CD3DX12_RESOURCE_BARRIER::Transition(depthTex->getD3D12Resource().Get(), D3D12_RESOURCE_STATE_DEPTH_WRITE, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE) + }; + commandList->ResourceBarrier(2, toRead); // Bloom reads the HDR scene and produces its own blurred texture. D3D12_GPU_DESCRIPTOR_HANDLE bloomHandle = m_dummyTexture->getSRV().gpu; @@ -247,10 +256,8 @@ void PostProcessPass::apply(ID3D12GraphicsCommandList4* commandList) const std::shared_ptr& lut = m_lutTexture ? m_lutTexture : m_identityLut; - auto depthTex = m_surface->getTexture(RenderSurface::DEPTH_STENCIL); D3D12_CPU_DESCRIPTOR_HANDLE targetRTV = composite->getRTV(0).cpu; - D3D12_CPU_DESCRIPTOR_HANDLE dsv = depthTex->getDSV().cpu; - commandList->OMSetRenderTargets(1, &targetRTV, FALSE, &dsv); + commandList->OMSetRenderTargets(1, &targetRTV, FALSE, nullptr); commandList->RSSetViewports(1, &m_viewport); commandList->RSSetScissorRects(1, &m_scissorRect); @@ -264,9 +271,18 @@ void PostProcessPass::apply(ID3D12GraphicsCommandList4* commandList) commandList->SetGraphicsRootDescriptorTable(1, sceneHDR->getSRV().gpu); commandList->SetGraphicsRootDescriptorTable(2, bloomHandle); commandList->SetGraphicsRootDescriptorTable(3, lut->getSRV().gpu); + commandList->SetGraphicsRootDescriptorTable(4, depthTex->getSRV().gpu); PostProcess::drawFullscreenTriangle(commandList); + // Restore the depth buffer to a writable state and re-bind COLOR_0 + depth + // so the overlay passes (debug draw / UI / fonts) inherit a valid target. + CD3DX12_RESOURCE_BARRIER depthBack = CD3DX12_RESOURCE_BARRIER::Transition(depthTex->getD3D12Resource().Get(), D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_DEPTH_WRITE); + commandList->ResourceBarrier(1, &depthBack); + + D3D12_CPU_DESCRIPTOR_HANDLE dsv = depthTex->getDSV().cpu; + commandList->OMSetRenderTargets(1, &targetRTV, FALSE, &dsv); + // Leave COLOR_1 (HDR) in PIXEL_SHADER_RESOURCE for the next frame's scene // pass, which will transition it back to RENDER_TARGET. } diff --git a/Engine_Master_UPC/PostProcessPass.h b/Engine_Master_UPC/PostProcessPass.h index 1a8a0e32..c4d70622 100644 --- a/Engine_Master_UPC/PostProcessPass.h +++ b/Engine_Master_UPC/PostProcessPass.h @@ -26,42 +26,55 @@ class PostProcessPass : public IRenderPass private: struct PostProcessParams { - // row 0 float exposure = 0.0f; float bloomIntensity = 0.5f; float lutSize = 2.0f; float caStrength = 1.0f; - // row 1 uint32_t enableBloom = 0; uint32_t enableLUT = 0; uint32_t enableCA = 0; uint32_t enableHeartbeat = 0; - // row 2 — heartbeat outputs + // heartbeat outputs float hbHealthVignette = 0.0f; float hbSepVignette = 0.0f; float hbPulse = 0.0f; uint32_t hbPulseIsLub = 1; - // row 3 — heartbeat outputs + // heartbeat outputs float hbCrit = 0.0f; float hbDesat = 0.0f; float hbSwayX = 0.0f; float hbSwayY = 0.0f; - // row 4 — death fade outputs + //death fade outputs float deathDesat = 0.0f; // 0..1 desaturation towards grey float deathFade = 0.0f; // 0..1 fade towards black float deathBlur = 0.0f; // 0..1 out-of-focus blur amount float deathPad0 = 0.0f; + + //outline + uint32_t enableOutline = 0; + float outlineThickness = 1.5f; + float outlineThreshold = 0.02f; + float outlineIntensity = 0.6f; + + // outline + float outlineColorR = 0.05f; + float outlineColorG = 0.04f; + float outlineColorB = 0.05f; + float outlineWobble = 1.0f; + + //outline + float outlineNoiseScale = 90.0f; + float outlineBreakup = 0.5f; + float outlinePad0 = 0.0f; + float outlinePad1 = 0.0f; }; - // Advances the heartbeat state machine and fills the heartbeat fields of - // m_params from the current health/separation. dt is the per-frame delta. void updateHeartbeat(const PostProcessSettings& settings, const RenderContext& ctx, float dt); - // Advances the death-fade timer and fills the death fields of m_params. void updateDeathFade(const PostProcessSettings& settings, float dt); ComPtr m_device; @@ -81,22 +94,18 @@ class PostProcessPass : public IRenderPass // 1x1 placeholder bound to the bloom slot when bloom is inactive. std::shared_ptr m_dummyTexture; - // Per-frame state captured in prepare(). RenderSurface* m_surface = nullptr; D3D12_VIEWPORT m_viewport{}; D3D12_RECT m_scissorRect{}; bool m_runBloom = false; - // Time-based effects are advanced once per frame, guarded by this index. uint32_t m_lastFrame = 0xFFFFFFFFu; - // Heartbeat timing state. float m_hbDubTimer = -1.0f; float m_hbLubTimer = -1.0f; float m_hbPulseAnim = 0.0f; - int m_hbPulseType = 0; // 0 = lub, 1 = dub + int m_hbPulseType = 0; float m_hbSwayAngle = 0.0f; - // Death-fade timing state. float m_deathTime = 0.0f; }; diff --git a/Engine_Master_UPC/PostProcessPixelShader.hlsl b/Engine_Master_UPC/PostProcessPixelShader.hlsl index 770311a1..b0cbf9d5 100644 --- a/Engine_Master_UPC/PostProcessPixelShader.hlsl +++ b/Engine_Master_UPC/PostProcessPixelShader.hlsl @@ -4,6 +4,7 @@ Texture2D sceneTexture : register(t0); Texture2D bloomTexture : register(t1); Texture3D lutTexture : register(t2); +Texture2D depthTexture : register(t3); SamplerState bilinearClamp : register(s0); cbuffer PostProcessParams : register(b0) @@ -34,8 +35,72 @@ cbuffer PostProcessParams : register(b0) float deathFade; float deathBlur; float deathPad0; + + // Outline (ink). + uint enableOutline; + float outlineThickness; + float outlineThreshold; + float outlineIntensity; + + float outlineColorR; + float outlineColorG; + float outlineColorB; + float outlineWobble; + + float outlineNoiseScale; + float outlineBreakup; + float outlinePad0; + float outlinePad1; }; +float hash21(float2 p) +{ + p = frac(p * float2(123.34, 456.21)); + p += dot(p, p + 45.32); + return frac(p.x * p.y); +} + +float valueNoise(float2 p) +{ + float2 i = floor(p); + float2 f = frac(p); + f = f * f * (3.0 - 2.0 * f); + float a = hash21(i); + float b = hash21(i + float2(1.0, 0.0)); + float c = hash21(i + float2(0.0, 1.0)); + float d = hash21(i + float2(1.0, 1.0)); + return lerp(lerp(a, b, f.x), lerp(c, d, f.x), f.y); +} + +float3 applyOutline(float3 color, float2 uv) +{ + float2 texSize; + sceneTexture.GetDimensions(texSize.x, texSize.y); + float2 texel = 1.0 / texSize; + float2 o = texel * outlineThickness; + + float2 warp = float2(valueNoise(uv * outlineNoiseScale), + valueNoise(uv * outlineNoiseScale + 17.0)) - 0.5; + float2 suv = uv + warp * texel * outlineThickness * outlineWobble * 2.0; + + float d0 = depthTexture.Sample(bilinearClamp, suv - o).r; + float d1 = depthTexture.Sample(bilinearClamp, suv + o).r; + float d2 = depthTexture.Sample(bilinearClamp, suv + float2(o.x, -o.y)).r; + float d3 = depthTexture.Sample(bilinearClamp, suv + float2(-o.x, o.y)).r; + float dc = depthTexture.Sample(bilinearClamp, suv).r; + + float g = abs(d0 - d1) + abs(d2 - d3); + + float edge = (dc < 0.9999) ? g : 0.0; + edge = smoothstep(outlineThreshold, outlineThreshold * 3.0 + 1e-4, edge); + + float breakup = lerp(1.0, valueNoise(uv * outlineNoiseScale * 2.3), outlineBreakup); + edge *= breakup; + + float3 ink = float3(outlineColorR, outlineColorG, outlineColorB); + return lerp(color, ink, saturate(edge * outlineIntensity)); +} + float3 sampleScene(float2 uv) { if (enableCA == 0) @@ -57,8 +122,6 @@ float3 applyLUT(float3 color) return lutTexture.SampleLevel(bilinearClamp, uvw, 0).rgb; } -// Out-of-focus disc blur of the scene, used by the death fade. 17 taps in two -// rings; radius grows with the blur amount. float3 sampleSceneBlurred(float2 uv, float blur) { if (blur <= 0.001) @@ -70,7 +133,7 @@ float3 sampleSceneBlurred(float2 uv, float blur) float2( 0.707, 0.707), float2(-0.707, 0.707), float2(0.707, -0.707), float2(-0.707, -0.707) }; - float2 radius = blur * 0.02; // up to ~2% of the screen + float2 radius = blur * 0.02; float3 sum = sceneTexture.Sample(bilinearClamp, uv).rgb; [unroll] @@ -84,32 +147,25 @@ float3 sampleSceneBlurred(float2 uv, float blur) } // Layered low-health "damage screen": desaturation, red/blue vignettes, -// heartbeat pulse tint and critical edge darkening. Applied to the final -// displayed colour (after tone mapping + gamma). +// heartbeat pulse tint and critical edge darkening. float3 applyHeartbeat(float3 color, float2 uv) { - // Desaturate towards luminance. float lum = dot(color, float3(0.299, 0.587, 0.114)); color = lerp(color, lum.xxx, saturate(hbDesat)); - - // Radial distance from the screen centre (0 centre .. ~1 corner). + float r = saturate(length(uv - 0.5) * 1.4); float edge = smoothstep(0.3, 1.0, r); - - // Health vignette (red): tint + darken the edges. + float hv = edge * hbHealthVignette; color = lerp(color, float3(0.5, 0.0, 0.0), hv); color *= 1.0 - hv * 0.5; - - // Separation vignette (blue). + float sv = edge * hbSepVignette; color = lerp(color, float3(0.0, 0.1, 0.4), sv); - - // Heartbeat pulse: warm flash on the "lub", cooler on the "dub". + float3 pulseCol = hbPulseIsLub ? float3(1.0, 0.78, 0.70) : float3(0.70, 0.86, 1.0); color += pulseCol * (hbPulse * (hbPulseIsLub ? 0.25 : 0.18)); - - // Critical edge darkening. + color *= 1.0 - edge * hbCrit; return color; @@ -117,7 +173,7 @@ float3 applyHeartbeat(float3 color, float2 uv) float4 main(float2 uv : TEXCOORD) : SV_TARGET { - // Heartbeat sway shifts the scene sampling (zero when the effect is off). + // Heartbeat sway shifts the scene sampling float2 suv = uv + float2(hbSwayX, hbSwayY); // Scene sample, blurred out of focus during the death fade. @@ -138,10 +194,14 @@ float4 main(float2 uv : TEXCOORD) : SV_TARGET float3 outColor = LinearToSRGB(mapped); + // Ink outline sits on the lit scene, under the full-screen state effects. + if (enableOutline != 0) + outColor = applyOutline(outColor, uv); + if (enableHeartbeat != 0) outColor = applyHeartbeat(outColor, uv); - // Death fade: desaturate to grey, then fade fully to black (no-op when both 0). + // Death fade: desaturate to grey, then fade fully to black. float deathLum = dot(outColor, float3(0.299, 0.587, 0.114)); outColor = lerp(outColor, deathLum.xxx, saturate(deathDesat)); outColor *= 1.0 - saturate(deathFade); diff --git a/Engine_Master_UPC/PostProcessSettings.h b/Engine_Master_UPC/PostProcessSettings.h index 5c524e8d..3031bc86 100644 --- a/Engine_Master_UPC/PostProcessSettings.h +++ b/Engine_Master_UPC/PostProcessSettings.h @@ -22,16 +22,25 @@ struct PostProcessSettings : public ISerializable // Heartbeat / low-health "damage screen" effect. bool heartbeatEnabled = false; - float healthThreshold = 0.5f; // health below this triggers the effect - // Runtime inputs (normally driven by gameplay: PlayerDamageable / Bound). - float health = 1.0f; // 0..1 - float separation = 0.0f; // 0..1 + float healthThreshold = 0.5f; + float health = 1.0f; + float separation = 0.0f; - // Death fade: desaturate to grey, then fade fully to black. Triggered by - // gameplay when every player is down (health 0, death animation playing). - bool deathFadeActive = false; // runtime trigger - float deathGreyDuration = 1.5f; // seconds to reach full grey - float deathBlackDuration = 1.5f;// seconds to fade from grey to black + // Death fade: desaturate to grey, then fade fully to black. + bool deathFadeActive = false; + float deathGreyDuration = 1.5f; + float deathBlackDuration = 1.5f; + + bool outlineEnabled = false; + float outlineThickness = 1.5f; + float outlineThreshold = 0.02f; + float outlineIntensity = 0.6f; + float outlineColorR = 0.05f; + float outlineColorG = 0.04f; + float outlineColorB = 0.05f; + float outlineWobble = 1.0f; + float outlineNoiseScale = 90.0f; + float outlineBreakup = 0.5f; void serialize(IArchive& archive) override { @@ -47,11 +56,21 @@ struct PostProcessSettings : public ISerializable archive.serialize(chromaticAberrationEnabled, "chromaticAberrationEnabled"); archive.serialize(chromaticAberrationStrength, "chromaticAberrationStrength"); - // Only the authored knobs persist; health/separation are runtime inputs. archive.serialize(heartbeatEnabled, "heartbeatEnabled"); archive.serialize(healthThreshold, "healthThreshold"); archive.serialize(deathGreyDuration, "deathGreyDuration"); archive.serialize(deathBlackDuration, "deathBlackDuration"); + + archive.serialize(outlineEnabled, "outlineEnabled"); + archive.serialize(outlineThickness, "outlineThickness"); + archive.serialize(outlineThreshold, "outlineThreshold"); + archive.serialize(outlineIntensity, "outlineIntensity"); + archive.serialize(outlineColorR, "outlineColorR"); + archive.serialize(outlineColorG, "outlineColorG"); + archive.serialize(outlineColorB, "outlineColorB"); + archive.serialize(outlineWobble, "outlineWobble"); + archive.serialize(outlineNoiseScale, "outlineNoiseScale"); + archive.serialize(outlineBreakup, "outlineBreakup"); } }; diff --git a/Engine_Master_UPC/SceneConfig.cpp b/Engine_Master_UPC/SceneConfig.cpp index f6336777..3e887bdb 100644 --- a/Engine_Master_UPC/SceneConfig.cpp +++ b/Engine_Master_UPC/SceneConfig.cpp @@ -227,6 +227,23 @@ void SceneConfig::drawPostProcessSettings() ImGui::DragFloat("Grey Duration (s)###PPDeathGrey", &pp.deathGreyDuration, 0.05f, 0.1f, 10.0f); ImGui::DragFloat("Black Duration (s)###PPDeathBlack", &pp.deathBlackDuration, 0.05f, 0.1f, 10.0f); ImGui::TextDisabled("Triggered by gameplay when all players are down."); + + ImGui::Separator(); + ImGui::Checkbox("Outline (Ink)###PPOutline", &pp.outlineEnabled); + ImGui::DragFloat("Thickness (px)###PPOutThick", &pp.outlineThickness, 0.05f, 0.5f, 6.0f); + ImGui::DragFloat("Threshold###PPOutThresh", &pp.outlineThreshold, 0.001f, 0.001f, 0.5f, "%.3f"); + ImGui::DragFloat("Intensity###PPOutIntensity", &pp.outlineIntensity, 0.01f, 0.0f, 1.0f); + float ink[3] = { pp.outlineColorR, pp.outlineColorG, pp.outlineColorB }; + if (ImGui::ColorEdit3("Ink Colour###PPOutColor", ink)) + { + pp.outlineColorR = ink[0]; + pp.outlineColorG = ink[1]; + pp.outlineColorB = ink[2]; + } + ImGui::DragFloat("Wobble###PPOutWobble", &pp.outlineWobble, 0.05f, 0.0f, 5.0f); + ImGui::DragFloat("Noise Scale###PPOutNoise", &pp.outlineNoiseScale, 1.0f, 1.0f, 400.0f); + ImGui::DragFloat("Break-up###PPOutBreakup", &pp.outlineBreakup, 0.01f, 0.0f, 1.0f); + ImGui::TextDisabled("Depth-based; threshold is scene-dependent - tune to taste."); } } From d0ab03ba3ab27f169a6bf6b89f207e8b12c41a1e Mon Sep 17 00:00:00 2001 From: Anand-Ramnarain-27 <142887960+Anand-Ramnarain-27@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:58:14 +0200 Subject: [PATCH 10/14] Add PostProcessAPI for post-processing controls Introduce a new PostProcessAPI namespace in EngineAPI.h/cpp to expose runtime control of post-processing effects to code/scripts. Adds header declarations and implementations for toggling heartbeat, death fade, bloom, outline, chromatic aberration, and for setting health, separation and exposure. Includes a getSettings() helper with nullptr checks to safely access scene post-process settings. Changes affect Engine_Master_UPC/EngineAPI.h and Engine_Master_UPC/EngineAPI.cpp. --- Engine_Master_UPC/EngineAPI.cpp | 72 +++++++++++++++++++++++++++++++++ Engine_Master_UPC/EngineAPI.h | 20 +++++++++ 2 files changed, 92 insertions(+) diff --git a/Engine_Master_UPC/EngineAPI.cpp b/Engine_Master_UPC/EngineAPI.cpp index 6e0f4ffe..e66ba7bf 100644 --- a/Engine_Master_UPC/EngineAPI.cpp +++ b/Engine_Master_UPC/EngineAPI.cpp @@ -2816,4 +2816,76 @@ namespace AudioAPI component->resumeEvent(playingID); } } +} + +namespace PostProcessAPI +{ + static PostProcessSettings* getSettings() + { + if (!app || !app->getModuleScene() || !app->getModuleScene()->getScene()) + { + return nullptr; + } + return &app->getModuleScene()->getScene()->getPostProcessSettings(); + } + + void setHeartbeatEnabled(bool enabled) + { + if (PostProcessSettings* pp = getSettings()) + pp->heartbeatEnabled = enabled; + } + + bool isHeartbeatEnabled() + { + const PostProcessSettings* pp = getSettings(); + return pp ? pp->heartbeatEnabled : false; + } + + void setHealth(float health01) + { + if (PostProcessSettings* pp = getSettings()) + pp->health = health01; + } + + void setSeparation(float separation01) + { + if (PostProcessSettings* pp = getSettings()) + pp->separation = separation01; + } + + void setDeathFadeActive(bool active) + { + if (PostProcessSettings* pp = getSettings()) + pp->deathFadeActive = active; + } + + bool isDeathFadeActive() + { + const PostProcessSettings* pp = getSettings(); + return pp ? pp->deathFadeActive : false; + } + + void setExposure(float ev) + { + if (PostProcessSettings* pp = getSettings()) + pp->exposure = ev; + } + + void setBloomEnabled(bool enabled) + { + if (PostProcessSettings* pp = getSettings()) + pp->bloomEnabled = enabled; + } + + void setOutlineEnabled(bool enabled) + { + if (PostProcessSettings* pp = getSettings()) + pp->outlineEnabled = enabled; + } + + void setChromaticAberrationEnabled(bool enabled) + { + if (PostProcessSettings* pp = getSettings()) + pp->chromaticAberrationEnabled = enabled; + } } \ No newline at end of file diff --git a/Engine_Master_UPC/EngineAPI.h b/Engine_Master_UPC/EngineAPI.h index d96f9471..1db8d067 100644 --- a/Engine_Master_UPC/EngineAPI.h +++ b/Engine_Master_UPC/EngineAPI.h @@ -417,4 +417,24 @@ namespace AudioAPI ENGINE_API void resumeEvent(ComponentSoundSource* component, uint32_t playingID); } +namespace PostProcessAPI +{ + // Heartbeat / low-health "damage screen" effect. + ENGINE_API void setHeartbeatEnabled(bool enabled); + ENGINE_API bool isHeartbeatEnabled(); + ENGINE_API void setHealth(float health01); // 0..1, drives the heartbeat + ENGINE_API void setSeparation(float separation01); // 0..1, drives the heartbeat + + // Death fade (desaturate to grey, then fade to black). Set active when all + // players are down; clear it to restore the normal image. + ENGINE_API void setDeathFadeActive(bool active); + ENGINE_API bool isDeathFadeActive(); + + // General toggles a script might drive for scripted moments. + ENGINE_API void setExposure(float ev); + ENGINE_API void setBloomEnabled(bool enabled); + ENGINE_API void setOutlineEnabled(bool enabled); + ENGINE_API void setChromaticAberrationEnabled(bool enabled); +} + #include "EngineAPI.inl" From e73f31f1e43cfdfd479e7e535afd70f60ce02df0 Mon Sep 17 00:00:00 2001 From: Anand-Ramnarain-27 <142887960+Anand-Ramnarain-27@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:09:45 +0200 Subject: [PATCH 11/14] Expose and expand PostProcess settings API Add many new getters/setters and expand PostProcessAPI surface to expose additional tuning parameters. Refactor implementations in EngineAPI.cpp to compact inline methods with safe null checks and sensible default return values. New controls include bloom (threshold/intensity), LUT enable/path, chromatic aberration strength, heartbeat/health getters, health threshold, death fade durations, and extensive outline parameters (thickness, threshold, intensity, color, wobble, noise scale, breakup). Update EngineAPI.h to declare the new functions for scripting/API use. --- Engine_Master_UPC/EngineAPI.cpp | 123 +++++++++++++++++--------------- Engine_Master_UPC/EngineAPI.h | 68 +++++++++++++----- 2 files changed, 116 insertions(+), 75 deletions(-) diff --git a/Engine_Master_UPC/EngineAPI.cpp b/Engine_Master_UPC/EngineAPI.cpp index e66ba7bf..643763d3 100644 --- a/Engine_Master_UPC/EngineAPI.cpp +++ b/Engine_Master_UPC/EngineAPI.cpp @@ -2829,63 +2829,68 @@ namespace PostProcessAPI return &app->getModuleScene()->getScene()->getPostProcessSettings(); } - void setHeartbeatEnabled(bool enabled) - { - if (PostProcessSettings* pp = getSettings()) - pp->heartbeatEnabled = enabled; - } - - bool isHeartbeatEnabled() - { - const PostProcessSettings* pp = getSettings(); - return pp ? pp->heartbeatEnabled : false; - } - - void setHealth(float health01) - { - if (PostProcessSettings* pp = getSettings()) - pp->health = health01; - } - - void setSeparation(float separation01) - { - if (PostProcessSettings* pp = getSettings()) - pp->separation = separation01; - } - - void setDeathFadeActive(bool active) - { - if (PostProcessSettings* pp = getSettings()) - pp->deathFadeActive = active; - } - - bool isDeathFadeActive() - { - const PostProcessSettings* pp = getSettings(); - return pp ? pp->deathFadeActive : false; - } - - void setExposure(float ev) - { - if (PostProcessSettings* pp = getSettings()) - pp->exposure = ev; - } - - void setBloomEnabled(bool enabled) - { - if (PostProcessSettings* pp = getSettings()) - pp->bloomEnabled = enabled; - } - - void setOutlineEnabled(bool enabled) - { - if (PostProcessSettings* pp = getSettings()) - pp->outlineEnabled = enabled; - } - - void setChromaticAberrationEnabled(bool enabled) - { - if (PostProcessSettings* pp = getSettings()) - pp->chromaticAberrationEnabled = enabled; - } + void setExposure(float ev) { if (auto* pp = getSettings()) pp->exposure = ev; } + float getExposure() { auto* pp = getSettings(); return pp ? pp->exposure : 0.0f; } + + void setBloomEnabled(bool e) { if (auto* pp = getSettings()) pp->bloomEnabled = e; } + bool isBloomEnabled() { auto* pp = getSettings(); return pp ? pp->bloomEnabled : false; } + void setBloomThreshold(float t) { if (auto* pp = getSettings()) pp->bloomThreshold = t; } + float getBloomThreshold() { auto* pp = getSettings(); return pp ? pp->bloomThreshold : 0.0f; } + void setBloomIntensity(float i) { if (auto* pp = getSettings()) pp->bloomIntensity = i; } + float getBloomIntensity() { auto* pp = getSettings(); return pp ? pp->bloomIntensity : 0.0f; } + + void setLutEnabled(bool e) { if (auto* pp = getSettings()) pp->lutEnabled = e; } + bool isLutEnabled() { auto* pp = getSettings(); return pp ? pp->lutEnabled : false; } + void setLutPath(const char* path) { if (auto* pp = getSettings()) pp->lutPath = path ? path : ""; } + const char* getLutPath() { auto* pp = getSettings(); return pp ? pp->lutPath.c_str() : ""; } + + void setChromaticAberrationEnabled(bool e) { if (auto* pp = getSettings()) pp->chromaticAberrationEnabled = e; } + bool isChromaticAberrationEnabled() { auto* pp = getSettings(); return pp ? pp->chromaticAberrationEnabled : false; } + void setChromaticAberrationStrength(float s) { if (auto* pp = getSettings()) pp->chromaticAberrationStrength = s; } + float getChromaticAberrationStrength() { auto* pp = getSettings(); return pp ? pp->chromaticAberrationStrength : 0.0f; } + + void setHeartbeatEnabled(bool e) { if (auto* pp = getSettings()) pp->heartbeatEnabled = e; } + bool isHeartbeatEnabled() { auto* pp = getSettings(); return pp ? pp->heartbeatEnabled : false; } + void setHealth(float h) { if (auto* pp = getSettings()) pp->health = h; } + float getHealth() { auto* pp = getSettings(); return pp ? pp->health : 1.0f; } + void setSeparation(float s) { if (auto* pp = getSettings()) pp->separation = s; } + float getSeparation() { auto* pp = getSettings(); return pp ? pp->separation : 0.0f; } + void setHealthThreshold(float t) { if (auto* pp = getSettings()) pp->healthThreshold = t; } + float getHealthThreshold() { auto* pp = getSettings(); return pp ? pp->healthThreshold : 0.5f; } + + void setDeathFadeActive(bool a) { if (auto* pp = getSettings()) pp->deathFadeActive = a; } + bool isDeathFadeActive() { auto* pp = getSettings(); return pp ? pp->deathFadeActive : false; } + void setDeathGreyDuration(float s) { if (auto* pp = getSettings()) pp->deathGreyDuration = s; } + float getDeathGreyDuration() { auto* pp = getSettings(); return pp ? pp->deathGreyDuration : 0.0f; } + void setDeathBlackDuration(float s) { if (auto* pp = getSettings()) pp->deathBlackDuration = s; } + float getDeathBlackDuration() { auto* pp = getSettings(); return pp ? pp->deathBlackDuration : 0.0f; } + + void setOutlineEnabled(bool e) { if (auto* pp = getSettings()) pp->outlineEnabled = e; } + bool isOutlineEnabled() { auto* pp = getSettings(); return pp ? pp->outlineEnabled : false; } + void setOutlineThickness(float t) { if (auto* pp = getSettings()) pp->outlineThickness = t; } + float getOutlineThickness() { auto* pp = getSettings(); return pp ? pp->outlineThickness : 0.0f; } + void setOutlineThreshold(float t) { if (auto* pp = getSettings()) pp->outlineThreshold = t; } + float getOutlineThreshold() { auto* pp = getSettings(); return pp ? pp->outlineThreshold : 0.0f; } + void setOutlineIntensity(float i) { if (auto* pp = getSettings()) pp->outlineIntensity = i; } + float getOutlineIntensity() { auto* pp = getSettings(); return pp ? pp->outlineIntensity : 0.0f; } + void setOutlineColor(const Vector3& rgb) + { + if (auto* pp = getSettings()) + { + pp->outlineColorR = rgb.x; + pp->outlineColorG = rgb.y; + pp->outlineColorB = rgb.z; + } + } + Vector3 getOutlineColor() + { + auto* pp = getSettings(); + return pp ? Vector3(pp->outlineColorR, pp->outlineColorG, pp->outlineColorB) : Vector3::Zero; + } + void setOutlineWobble(float w) { if (auto* pp = getSettings()) pp->outlineWobble = w; } + float getOutlineWobble() { auto* pp = getSettings(); return pp ? pp->outlineWobble : 0.0f; } + void setOutlineNoiseScale(float s) { if (auto* pp = getSettings()) pp->outlineNoiseScale = s; } + float getOutlineNoiseScale() { auto* pp = getSettings(); return pp ? pp->outlineNoiseScale : 0.0f; } + void setOutlineBreakup(float b) { if (auto* pp = getSettings()) pp->outlineBreakup = b; } + float getOutlineBreakup() { auto* pp = getSettings(); return pp ? pp->outlineBreakup : 0.0f; } } \ No newline at end of file diff --git a/Engine_Master_UPC/EngineAPI.h b/Engine_Master_UPC/EngineAPI.h index 1db8d067..0dde807f 100644 --- a/Engine_Master_UPC/EngineAPI.h +++ b/Engine_Master_UPC/EngineAPI.h @@ -419,22 +419,58 @@ namespace AudioAPI namespace PostProcessAPI { - // Heartbeat / low-health "damage screen" effect. - ENGINE_API void setHeartbeatEnabled(bool enabled); - ENGINE_API bool isHeartbeatEnabled(); - ENGINE_API void setHealth(float health01); // 0..1, drives the heartbeat - ENGINE_API void setSeparation(float separation01); // 0..1, drives the heartbeat - - // Death fade (desaturate to grey, then fade to black). Set active when all - // players are down; clear it to restore the normal image. - ENGINE_API void setDeathFadeActive(bool active); - ENGINE_API bool isDeathFadeActive(); - - // General toggles a script might drive for scripted moments. - ENGINE_API void setExposure(float ev); - ENGINE_API void setBloomEnabled(bool enabled); - ENGINE_API void setOutlineEnabled(bool enabled); - ENGINE_API void setChromaticAberrationEnabled(bool enabled); + ENGINE_API void setExposure(float ev); + ENGINE_API float getExposure(); + + ENGINE_API void setBloomEnabled(bool enabled); + ENGINE_API bool isBloomEnabled(); + ENGINE_API void setBloomThreshold(float threshold); + ENGINE_API float getBloomThreshold(); + ENGINE_API void setBloomIntensity(float intensity); + ENGINE_API float getBloomIntensity(); + + ENGINE_API void setLutEnabled(bool enabled); + ENGINE_API bool isLutEnabled(); + ENGINE_API void setLutPath(const char* path); + ENGINE_API const char* getLutPath(); + + ENGINE_API void setChromaticAberrationEnabled(bool enabled); + ENGINE_API bool isChromaticAberrationEnabled(); + ENGINE_API void setChromaticAberrationStrength(float strength); + ENGINE_API float getChromaticAberrationStrength(); + + ENGINE_API void setHeartbeatEnabled(bool enabled); + ENGINE_API bool isHeartbeatEnabled(); + ENGINE_API void setHealth(float health01); + ENGINE_API float getHealth(); + ENGINE_API void setSeparation(float separation01); + ENGINE_API float getSeparation(); + ENGINE_API void setHealthThreshold(float threshold); + ENGINE_API float getHealthThreshold(); + + ENGINE_API void setDeathFadeActive(bool active); + ENGINE_API bool isDeathFadeActive(); + ENGINE_API void setDeathGreyDuration(float seconds); + ENGINE_API float getDeathGreyDuration(); + ENGINE_API void setDeathBlackDuration(float seconds); + ENGINE_API float getDeathBlackDuration(); + + ENGINE_API void setOutlineEnabled(bool enabled); + ENGINE_API bool isOutlineEnabled(); + ENGINE_API void setOutlineThickness(float pixels); + ENGINE_API float getOutlineThickness(); + ENGINE_API void setOutlineThreshold(float threshold); + ENGINE_API float getOutlineThreshold(); + ENGINE_API void setOutlineIntensity(float intensity); + ENGINE_API float getOutlineIntensity(); + ENGINE_API void setOutlineColor(const Vector3& rgb); + ENGINE_API Vector3 getOutlineColor(); + ENGINE_API void setOutlineWobble(float wobble); + ENGINE_API float getOutlineWobble(); + ENGINE_API void setOutlineNoiseScale(float scale); + ENGINE_API float getOutlineNoiseScale(); + ENGINE_API void setOutlineBreakup(float breakup); + ENGINE_API float getOutlineBreakup(); } #include "EngineAPI.inl" From 1d8cca5a993197e9c454329a88371aa1fa11fe48 Mon Sep 17 00:00:00 2001 From: Anand-Ramnarain-27 <142887960+Anand-Ramnarain-27@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:11:41 +0200 Subject: [PATCH 12/14] Bypass heavy postprocessing for background Sample scene depth and treat pixels with depth >= 0.9999 as background. Background pixels now use a direct scene sample (saturated) and skip the blurred HDR path, bloom, exposure/tone-mapping/LUT and sRGB conversion to reduce work; non-background pixels follow the existing blurred HDR + bloom + exposure + PBR neutral tone mapping + optional LUT + LinearToSRGB flow. Preserves existing outline compositing behavior. --- Engine_Master_UPC/PostProcessPixelShader.hlsl | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/Engine_Master_UPC/PostProcessPixelShader.hlsl b/Engine_Master_UPC/PostProcessPixelShader.hlsl index b0cbf9d5..c7469b78 100644 --- a/Engine_Master_UPC/PostProcessPixelShader.hlsl +++ b/Engine_Master_UPC/PostProcessPixelShader.hlsl @@ -173,26 +173,37 @@ float3 applyHeartbeat(float3 color, float2 uv) float4 main(float2 uv : TEXCOORD) : SV_TARGET { - // Heartbeat sway shifts the scene sampling + // Heartbeat sway shifts the scene sampling float2 suv = uv + float2(hbSwayX, hbSwayY); - // Scene sample, blurred out of focus during the death fade. - float3 hdr = sampleSceneBlurred(suv, deathBlur); + float sceneDepth = depthTexture.Sample(bilinearClamp, uv).r; + bool isBackground = sceneDepth >= 0.9999; - if (enableBloom != 0) + float3 outColor; + if (isBackground) { - float3 bloom = bloomTexture.Sample(bilinearClamp, suv).rgb; - hdr += bloom * bloomIntensity; + outColor = saturate(sampleScene(suv)); } + else + { + // Scene sample, blurred out of focus during the death fade. + float3 hdr = sampleSceneBlurred(suv, deathBlur); + + if (enableBloom != 0) + { + float3 bloom = bloomTexture.Sample(bilinearClamp, suv).rgb; + hdr += bloom * bloomIntensity; + } - hdr *= 1.2 * pow(2.0, exposure); + hdr *= 1.2 * pow(2.0, exposure); - float3 mapped = PBRNeutralToneMapping(hdr); + float3 mapped = PBRNeutralToneMapping(hdr); - if (enableLUT != 0) - mapped = applyLUT(mapped); + if (enableLUT != 0) + mapped = applyLUT(mapped); - float3 outColor = LinearToSRGB(mapped); + outColor = LinearToSRGB(mapped); + } // Ink outline sits on the lit scene, under the full-screen state effects. if (enableOutline != 0) From 3484e3a22d58cfd68937a540b3c0e6b63919f088 Mon Sep 17 00:00:00 2001 From: Anand-Ramnarain-27 <142887960+Anand-Ramnarain-27@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:26:00 +0200 Subject: [PATCH 13/14] Clamp bloom brightness to prevent Inf/NaN Add a configurable bloomClamp to post-process settings and use it to limit bloom contribution so extremely bright or Inf HDR pixels can't drive the fp16 bloom chain to Inf/NaN. Introduces bloomClamp (serialized and exposed in the UI), a BloomParams.maxBrightness field, and sets maxBrightness in BloomPass (clamped 0..8192). BloomThresholdPixelShader now clamps sampled color to maxBrightness before thresholding, and PostProcessPixelShader additionally clamps the final bloom sample to 65000.0 to sanitize any residual Inf values. --- Engine_Master_UPC/BloomPass.cpp | 5 ++++- Engine_Master_UPC/BloomPass.h | 2 +- Engine_Master_UPC/BloomThresholdPixelShader.hlsl | 8 ++++++-- Engine_Master_UPC/PostProcessPixelShader.hlsl | 3 ++- Engine_Master_UPC/PostProcessSettings.h | 2 ++ Engine_Master_UPC/SceneConfig.cpp | 2 ++ 6 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Engine_Master_UPC/BloomPass.cpp b/Engine_Master_UPC/BloomPass.cpp index 09415855..a243d78d 100644 --- a/Engine_Master_UPC/BloomPass.cpp +++ b/Engine_Master_UPC/BloomPass.cpp @@ -72,7 +72,10 @@ void BloomPass::prepare(const RenderContext& ctx) if (Scene* scene = app->getModuleScene()->getScene()) { - m_params.threshold = scene->getPostProcessSettings().bloomThreshold; + const PostProcessSettings& pp = scene->getPostProcessSettings(); + m_params.threshold = pp.bloomThreshold; + // Keep a hard ceiling so the fp16 bloom chain can never reach Inf. + m_params.maxBrightness = std::min(std::max(pp.bloomClamp, 0.0f), 8192.0f); } } diff --git a/Engine_Master_UPC/BloomPass.h b/Engine_Master_UPC/BloomPass.h index 7fcdb5f7..3aaeb0dd 100644 --- a/Engine_Master_UPC/BloomPass.h +++ b/Engine_Master_UPC/BloomPass.h @@ -26,7 +26,7 @@ class BloomPass struct BloomParams { float threshold = 1.0f; - float pad0 = 0.0f; + float maxBrightness = 0.1f; // clamp so very bright pixels can't overflow the chain float pad1 = 0.0f; float pad2 = 0.0f; }; diff --git a/Engine_Master_UPC/BloomThresholdPixelShader.hlsl b/Engine_Master_UPC/BloomThresholdPixelShader.hlsl index 42b8198c..e47d1a98 100644 --- a/Engine_Master_UPC/BloomThresholdPixelShader.hlsl +++ b/Engine_Master_UPC/BloomThresholdPixelShader.hlsl @@ -4,14 +4,18 @@ SamplerState bilinearClamp : register(s0); cbuffer BloomParams : register(b0) { float threshold; - float pad0; + float maxBrightness; float pad1; float pad2; }; float4 main(float2 uv : TEXCOORD) : SV_TARGET { - float3 c = inputTexture.Sample(bilinearClamp, uv).rgb; + // Clamp the input so an extremely bright (or Inf) HDR pixel can't push the + // 16-bit bloom chain up to Inf during the additive upsample (that would + // tone-map to NaN and show up as a black blob) and so very intense lights + // don't bloom into huge blown-out discs. + float3 c = min(inputTexture.Sample(bilinearClamp, uv).rgb, maxBrightness); float brightness = max(c.r, max(c.g, c.b)); float contribution = max(brightness - threshold, 0.0) / max(brightness, 1e-5); diff --git a/Engine_Master_UPC/PostProcessPixelShader.hlsl b/Engine_Master_UPC/PostProcessPixelShader.hlsl index c7469b78..dd51811e 100644 --- a/Engine_Master_UPC/PostProcessPixelShader.hlsl +++ b/Engine_Master_UPC/PostProcessPixelShader.hlsl @@ -191,7 +191,8 @@ float4 main(float2 uv : TEXCOORD) : SV_TARGET if (enableBloom != 0) { - float3 bloom = bloomTexture.Sample(bilinearClamp, suv).rgb; + // min() also sanitises any residual Inf to a finite value. + float3 bloom = min(bloomTexture.Sample(bilinearClamp, suv).rgb, 65000.0); hdr += bloom * bloomIntensity; } diff --git a/Engine_Master_UPC/PostProcessSettings.h b/Engine_Master_UPC/PostProcessSettings.h index 3031bc86..1ff584ca 100644 --- a/Engine_Master_UPC/PostProcessSettings.h +++ b/Engine_Master_UPC/PostProcessSettings.h @@ -11,6 +11,7 @@ struct PostProcessSettings : public ISerializable bool bloomEnabled = false; float bloomThreshold = 1.0f; float bloomIntensity = 0.5f; + float bloomClamp = 0.1f; // max brightness a pixel may contribute to bloom // Colour grading bool lutEnabled = false; @@ -49,6 +50,7 @@ struct PostProcessSettings : public ISerializable archive.serialize(bloomEnabled, "bloomEnabled"); archive.serialize(bloomThreshold, "bloomThreshold"); archive.serialize(bloomIntensity, "bloomIntensity"); + archive.serialize(bloomClamp, "bloomClamp"); archive.serialize(lutEnabled, "lutEnabled"); archive.serialize(lutPath, "lutPath"); diff --git a/Engine_Master_UPC/SceneConfig.cpp b/Engine_Master_UPC/SceneConfig.cpp index 3e887bdb..8381fe3d 100644 --- a/Engine_Master_UPC/SceneConfig.cpp +++ b/Engine_Master_UPC/SceneConfig.cpp @@ -199,6 +199,8 @@ void SceneConfig::drawPostProcessSettings() ImGui::Checkbox("Bloom###PPBloomEnabled", &pp.bloomEnabled); ImGui::DragFloat("Bloom Threshold###PPBloomThreshold", &pp.bloomThreshold, 0.01f, 0.0f, 10.0f); ImGui::DragFloat("Bloom Intensity###PPBloomIntensity", &pp.bloomIntensity, 0.01f, 0.0f, 5.0f); + ImGui::DragFloat("Bloom Clamp###PPBloomClamp", &pp.bloomClamp, 0.1f, 0.5f, 8192.0f); + ImGui::TextDisabled("Lower = tame blown-out discs from very bright lights."); ImGui::Separator(); ImGui::Checkbox("Colour Grading (LUT)###PPLutEnabled", &pp.lutEnabled); From 0eaf906dd4618a80bb949368bc84e17b980b8716 Mon Sep 17 00:00:00 2001 From: Anand-Ramnarain-27 <142887960+Anand-Ramnarain-27@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:45:49 +0200 Subject: [PATCH 14/14] Use Poisson-disc blur and shader cleanup Replace the old 8-direction fixed blur with a high-quality Poisson-disc (golden-angle) sampling loop (N=64) that uses aspect-corrected offsets, radius scaling, and exponential weights; samples are clamped and the result normalized by total weight to avoid bright-pixel ghosting. Also remove/trim some obsolete comments in BloomThresholdPixelShader.hlsl and tidy minor whitespace/comments in PostProcessPixelShader.hlsl. --- .../BloomThresholdPixelShader.hlsl | 4 --- Engine_Master_UPC/PostProcessPixelShader.hlsl | 36 +++++++++++-------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/Engine_Master_UPC/BloomThresholdPixelShader.hlsl b/Engine_Master_UPC/BloomThresholdPixelShader.hlsl index e47d1a98..c42f9a72 100644 --- a/Engine_Master_UPC/BloomThresholdPixelShader.hlsl +++ b/Engine_Master_UPC/BloomThresholdPixelShader.hlsl @@ -11,10 +11,6 @@ cbuffer BloomParams : register(b0) float4 main(float2 uv : TEXCOORD) : SV_TARGET { - // Clamp the input so an extremely bright (or Inf) HDR pixel can't push the - // 16-bit bloom chain up to Inf during the additive upsample (that would - // tone-map to NaN and show up as a black blob) and so very intense lights - // don't bloom into huge blown-out discs. float3 c = min(inputTexture.Sample(bilinearClamp, uv).rgb, maxBrightness); float brightness = max(c.r, max(c.g, c.b)); diff --git a/Engine_Master_UPC/PostProcessPixelShader.hlsl b/Engine_Master_UPC/PostProcessPixelShader.hlsl index dd51811e..a9b3a2f6 100644 --- a/Engine_Master_UPC/PostProcessPixelShader.hlsl +++ b/Engine_Master_UPC/PostProcessPixelShader.hlsl @@ -127,23 +127,33 @@ float3 sampleSceneBlurred(float2 uv, float blur) if (blur <= 0.001) return sampleScene(uv); - const float2 dirs[8] = - { - float2( 1.0, 0.0), float2(-1.0, 0.0), float2(0.0, 1.0), float2(0.0, -1.0), - float2( 0.707, 0.707), float2(-0.707, 0.707), float2(0.707, -0.707), float2(-0.707, -0.707) - }; + const int N = 64; + const float goldenAngle = 2.39996323; + const float maxRadius = blur * 0.006; + + float2 texSize; + sceneTexture.GetDimensions(texSize.x, texSize.y); + float aspect = texSize.y / texSize.x; - float2 radius = blur * 0.02; - float3 sum = sceneTexture.Sample(bilinearClamp, uv).rgb; + float3 sum = 0.0; + float totalWeight = 0.0; [unroll] - for (int i = 0; i < 8; ++i) + for (int i = 0; i < N; ++i) { - sum += sceneTexture.Sample(bilinearClamp, uv + dirs[i] * radius).rgb; - sum += sceneTexture.Sample(bilinearClamp, uv + dirs[i] * radius * 0.5).rgb; + float t = (float(i) + 0.5) / float(N); + float r = sqrt(t) * maxRadius; + float a = float(i) * goldenAngle; + float2 offset = float2(cos(a) * aspect, sin(a)) * r; + float w = exp(-1.5 * t); + + // Clamp so one very bright pixel can't ghost into bright copies. + float3 s = min(sceneTexture.Sample(bilinearClamp, uv + offset).rgb, 8.0); + sum += s * w; + totalWeight += w; } - return sum / 17.0; + return sum / totalWeight; } // Layered low-health "damage screen": desaturation, red/blue vignettes, @@ -191,7 +201,6 @@ float4 main(float2 uv : TEXCOORD) : SV_TARGET if (enableBloom != 0) { - // min() also sanitises any residual Inf to a finite value. float3 bloom = min(bloomTexture.Sample(bilinearClamp, suv).rgb, 65000.0); hdr += bloom * bloomIntensity; } @@ -205,8 +214,7 @@ float4 main(float2 uv : TEXCOORD) : SV_TARGET outColor = LinearToSRGB(mapped); } - - // Ink outline sits on the lit scene, under the full-screen state effects. + if (enableOutline != 0) outColor = applyOutline(outColor, uv);