Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d2b80bf
Add HDR post-process (bloom, LUT, tonemap)
Anand-Ramnarain-27 Jun 19, 2026
11822e6
Merge branch 'main' into feature/postprocessing
Anand-Ramnarain-27 Jun 19, 2026
3557623
Add HDR render target and post-process resolve
Anand-Ramnarain-27 Jun 19, 2026
0d3d18e
Revert "Add HDR render target and post-process resolve"
Anand-Ramnarain-27 Jun 19, 2026
5b5e3b8
Reapply "Add HDR render target and post-process resolve"
Anand-Ramnarain-27 Jun 19, 2026
1225b0b
Merge branch 'main' into feature/postprocessing
Anand-Ramnarain-27 Jun 19, 2026
a31f65f
Bind depth-stencil DSV in PostProcessPass
Anand-Ramnarain-27 Jun 19, 2026
3bfa4b9
Merge branch 'main' into feature/postprocessing
Anand-Ramnarain-27 Jun 22, 2026
4e537d6
Add HDR render target; defer tone mapping
Anand-Ramnarain-27 Jun 22, 2026
c4dd97b
Add PostProcessCommon and refactor postprocessing
Anand-Ramnarain-27 Jun 23, 2026
0ec4a6e
Add heartbeat and death-fade postprocess effects
Anand-Ramnarain-27 Jun 23, 2026
401b387
Add depth-based outline post-process effect
Anand-Ramnarain-27 Jun 23, 2026
d0ab03b
Add PostProcessAPI for post-processing controls
Anand-Ramnarain-27 Jun 23, 2026
e73f31f
Expose and expand PostProcess settings API
Anand-Ramnarain-27 Jun 23, 2026
5a04c7b
Merge branch 'main' into feature/postprocessing
Anand-Ramnarain-27 Jun 23, 2026
1d8cca5
Bypass heavy postprocessing for background
Anand-Ramnarain-27 Jun 24, 2026
3484e3a
Clamp bloom brightness to prevent Inf/NaN
Anand-Ramnarain-27 Jun 25, 2026
0eaf906
Use Poisson-disc blur and shader cleanup
Anand-Ramnarain-27 Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Engine_Master_UPC/BloomDownsamplePixelShader.hlsl
Original file line number Diff line number Diff line change
@@ -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;
}
132 changes: 132 additions & 0 deletions Engine_Master_UPC/BloomPass.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#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 "PostProcessCommon.h"
#include "Texture.h"
#include "UID.h"

#include <d3dx12.h>
#include <d3dcompiler.h>
#include <PlatformHelpers.h>
#include <algorithm>

BloomPass::BloomPass(ComPtr<ID3D12Device4> 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 = PostProcess::bilinearClampSampler();

CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc;
rootSignatureDesc.Init(_countof(rootParameters), rootParameters, 1, &sampler, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

ComPtr<ID3DBlob> signature;
ComPtr<ID3DBlob> 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<Texture>(GenerateUID(), *m_device.Get(), desc);
m_chain[i]->setName(L"BloomChain");
}
}

ComPtr<ID3D12PipelineState> BloomPass::buildPSO(const wchar_t* pixelShaderCso, bool additiveBlend)
{
// 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)
{
(void)ctx;

if (Scene* scene = app->getModuleScene()->getScene())
{
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);
}
}

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<float>(m_width[level]), static_cast<float>(m_height[level]), 0.0f, 1.0f };
D3D12_RECT scissor = { 0, 0, static_cast<LONG>(m_width[level]), static_cast<LONG>(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);

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);
}

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;
}
48 changes: 48 additions & 0 deletions Engine_Master_UPC/BloomPass.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#pragma once
#include <d3d12.h>
#include <wrl/client.h>
#include <memory>

using Microsoft::WRL::ComPtr;

struct RenderContext;
class Texture;

class BloomPass
{
public:
explicit BloomPass(ComPtr<ID3D12Device4> 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 maxBrightness = 0.1f; // clamp so very bright pixels can't overflow the chain
float pad1 = 0.0f;
float pad2 = 0.0f;
};

ComPtr<ID3D12PipelineState> buildPSO(const wchar_t* pixelShaderCso, bool additiveBlend);
void renderLevel(ID3D12GraphicsCommandList4* commandList, ID3D12PipelineState* pso, D3D12_GPU_DESCRIPTOR_HANDLE inputSrv, int level);

ComPtr<ID3D12Device4> m_device;
ComPtr<ID3D12RootSignature> m_rootSignature;
ComPtr<ID3D12PipelineState> m_thresholdPSO;
ComPtr<ID3D12PipelineState> m_downsamplePSO;
ComPtr<ID3D12PipelineState> m_upsamplePSO;

std::shared_ptr<Texture> m_chain[LEVELS];
uint32_t m_width[LEVELS]{};
uint32_t m_height[LEVELS]{};

BloomParams m_params;
};
20 changes: 20 additions & 0 deletions Engine_Master_UPC/BloomThresholdPixelShader.hlsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Texture2D inputTexture : register(t0);
SamplerState bilinearClamp : register(s0);

cbuffer BloomParams : register(b0)
{
float threshold;
float maxBrightness;
float pad1;
float pad2;
};

float4 main(float2 uv : TEXCOORD) : SV_TARGET
{
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);

return float4(c * contribution, 1.0);
}
20 changes: 20 additions & 0 deletions Engine_Master_UPC/BloomUpsamplePixelShader.hlsl
Original file line number Diff line number Diff line change
@@ -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;
}
110 changes: 110 additions & 0 deletions Engine_Master_UPC/CubeLut.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#include "Globals.h"
#include "CubeLut.h"

#include "Texture.h"
#include "Application.h"
#include "ModuleResources.h"
#include "UID.h"

#include <cstdio>
#include <vector>

namespace
{
// Creates the 3D texture and uploads the RGBA float data into it.
std::shared_ptr<Texture> buildLutTexture(ID3D12Device4& device, int size, const std::vector<float>& rgba)
{
TextureDesc desc{};
desc.format = DXGI_FORMAT_R32G32B32A32_FLOAT;
desc.width = static_cast<uint32_t>(size);
desc.height = static_cast<uint32_t>(size);
desc.depth = static_cast<uint16_t>(size);
desc.mipLevels = 1;
desc.views = TextureView::SRV;
desc.initialState = D3D12_RESOURCE_STATE_COPY_DEST;
desc.shaderVisibleSRV = true;

auto tex = std::make_shared<Texture>(GenerateUID(), device, desc);
tex->setName(L"ColorGradingLUT");

D3D12_SUBRESOURCE_DATA sub{};
sub.pData = rgba.data();
sub.RowPitch = static_cast<LONG_PTR>(size) * sizeof(float) * 4;
sub.SlicePitch = sub.RowPitch * size;

app->getModuleResources()->uploadTextureAndTransition(tex->getD3D12Resource().Get(), { sub });
return tex;
}
}

std::shared_ptr<Texture> CubeLut::createIdentity(ID3D12Device4& device, int size)
{
if (size < 2) size = 2;

std::vector<float> rgba(static_cast<size_t>(size) * size * size * 4);
size_t idx = 0;
const float inv = 1.0f / static_cast<float>(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<Texture> 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<float> 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_t>(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);
}
13 changes: 13 additions & 0 deletions Engine_Master_UPC/CubeLut.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#pragma once
#include <d3d12.h>
#include <memory>
#include <string>

class Texture;

namespace CubeLut
{
std::shared_ptr<Texture> load(ID3D12Device4& device, const std::string& path);

std::shared_ptr<Texture> createIdentity(ID3D12Device4& device, int size);
}
Loading