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..a243d78d --- /dev/null +++ b/Engine_Master_UPC/BloomPass.cpp @@ -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 +#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 = PostProcess::bilinearClampSampler(); + + 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) +{ + // 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(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); + + 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; +} diff --git a/Engine_Master_UPC/BloomPass.h b/Engine_Master_UPC/BloomPass.h new file mode 100644 index 00000000..3aaeb0dd --- /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 maxBrightness = 0.1f; // clamp so very bright pixels can't overflow the chain + 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..c42f9a72 --- /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 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); +} 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 99fc7683..ea0ce1fc 100644 --- a/Engine_Master_UPC/Engine.vcxproj +++ b/Engine_Master_UPC/Engine.vcxproj @@ -418,6 +418,11 @@ $(SolutionDir)3rdParty\RecastNavigation\DebugUtils\Include; + + + + + @@ -809,6 +814,10 @@ $(SolutionDir)3rdParty\RecastNavigation\DebugUtils\Include; + + + + @@ -986,6 +995,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 33001e3d..771543e9 100644 --- a/Engine_Master_UPC/Engine.vcxproj.filters +++ b/Engine_Master_UPC/Engine.vcxproj.filters @@ -1732,4 +1732,54 @@ + + + {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 + + + D3D12\Rendering\PostProcessing + + + D3D12\Rendering\PostProcessing + + \ No newline at end of file diff --git a/Engine_Master_UPC/EngineAPI.cpp b/Engine_Master_UPC/EngineAPI.cpp index 6e0f4ffe..643763d3 100644 --- a/Engine_Master_UPC/EngineAPI.cpp +++ b/Engine_Master_UPC/EngineAPI.cpp @@ -2816,4 +2816,81 @@ 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 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 d96f9471..0dde807f 100644 --- a/Engine_Master_UPC/EngineAPI.h +++ b/Engine_Master_UPC/EngineAPI.h @@ -417,4 +417,60 @@ namespace AudioAPI ENGINE_API void resumeEvent(ComponentSoundSource* component, uint32_t playingID); } +namespace PostProcessAPI +{ + 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" 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/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..6ab07960 100644 --- a/Engine_Master_UPC/ModuleRender.cpp +++ b/Engine_Master_UPC/ModuleRender.cpp @@ -33,6 +33,7 @@ #include "StaticTexturesPass.h" #include "SkinningComputePass.h" #include "ShadowMapPass.h" +#include "PostProcessPass.h" #include "Quadtree.h" #include "RenderContext.h" #include "WindowSceneEditor.h" @@ -74,6 +75,11 @@ bool ModuleRender::init() m_renderPasses.push_back(std::move(skyBoxPass)); m_renderPasses.push_back(std::unique_ptr(m_meshRenderPass)); 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)); @@ -114,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); } }); } @@ -156,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()); @@ -232,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); } @@ -288,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, @@ -305,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, }; @@ -336,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); } @@ -371,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; @@ -387,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..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); } @@ -182,15 +183,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/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 76b6766d..2316ffb6 100644 --- a/Engine_Master_UPC/ParticlePixelShader.hlsl +++ b/Engine_Master_UPC/ParticlePixelShader.hlsl @@ -40,7 +40,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 9c2f31c2..0c5b9083 100644 --- a/Engine_Master_UPC/ParticlesPass.cpp +++ b/Engine_Master_UPC/ParticlesPass.cpp @@ -80,7 +80,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/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 new file mode 100644 index 00000000..6891fba0 --- /dev/null +++ b/Engine_Master_UPC/PostProcessPass.cpp @@ -0,0 +1,288 @@ +#include "Globals.h" +#include "PostProcessPass.h" + +#include "RenderContext.h" +#include "RenderSurface.h" + +#include "Application.h" +#include "ModuleDescriptors.h" +#include "ModuleScene.h" +#include "ModuleTime.h" + +#include "Scene.h" +#include "PostProcessSettings.h" +#include "PostProcessCommon.h" +#include "Texture.h" +#include "CubeLut.h" +#include "BloomPass.h" +#include "UID.h" + +#include +#include +#include +#include +#include + +PostProcessPass::PostProcessPass(ComPtr device) : m_device(device) +{ + 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); + depthRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 3, 0); + + 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(); + + 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_pipelineState = PostProcess::createFullscreenPSO(m_device.Get(), m_rootSignature.Get(), L"PostProcessPixelShader.cso", DXGI_FORMAT_R8G8B8A8_UNORM); + + m_bloomPass = std::make_unique(m_device); + + 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; + 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"); +} + +PostProcessPass::~PostProcessPass() = default; + +void PostProcessPass::prepare(const RenderContext& ctx) +{ + m_surface = &ctx.renderSurface; + m_viewport = ctx.viewport; + m_scissorRect = ctx.scissorRect; + + Scene* scene = app->getModuleScene()->getScene(); + if (!scene) + return; + + const PostProcessSettings& settings = scene->getPostProcessSettings(); + + m_params.exposure = settings.exposure; + + // Colour-grading LUT + 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; + + // 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; + + if (m_runBloom) + m_bloomPass->prepare(ctx); + + //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; + + // 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 + 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 + 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; + + 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; + + 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) +{ + if (!m_surface) + return; + + auto sceneHDR = m_surface->getTexture(RenderSurface::COLOR_1); + auto composite = m_surface->getTexture(RenderSurface::COLOR_0); + auto depthTex = m_surface->getTexture(RenderSurface::DEPTH_STENCIL); + if (!sceneHDR || !composite || !depthTex) + return; + + // 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; + 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->SetGraphicsRoot32BitConstants(0, sizeof(PostProcessParams) / sizeof(UINT32), &m_params, 0); + 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 new file mode 100644 index 00000000..c4d70622 --- /dev/null +++ b/Engine_Master_UPC/PostProcessPass.h @@ -0,0 +1,111 @@ +#pragma once +#include "IRenderPass.h" + +#include +#include +#include +#include + +using Microsoft::WRL::ComPtr; + +struct RenderContext; +struct PostProcessSettings; +class RenderSurface; +class Texture; +class BloomPass; + +class PostProcessPass : public IRenderPass +{ +public: + explicit PostProcessPass(ComPtr device); + ~PostProcessPass() override; + + void prepare(const RenderContext& ctx) override; + void apply(ID3D12GraphicsCommandList4* commandList) override; + +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 enableHeartbeat = 0; + + // heartbeat outputs + float hbHealthVignette = 0.0f; + float hbSepVignette = 0.0f; + float hbPulse = 0.0f; + uint32_t hbPulseIsLub = 1; + + // heartbeat outputs + float hbCrit = 0.0f; + float hbDesat = 0.0f; + float hbSwayX = 0.0f; + float hbSwayY = 0.0f; + + //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; + }; + + void updateHeartbeat(const PostProcessSettings& settings, const RenderContext& ctx, float dt); + + void updateDeathFade(const PostProcessSettings& settings, float dt); + + ComPtr m_device; + ComPtr m_rootSignature; + ComPtr m_pipelineState; + + 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; + + RenderSurface* m_surface = nullptr; + D3D12_VIEWPORT m_viewport{}; + D3D12_RECT m_scissorRect{}; + bool m_runBloom = false; + + uint32_t m_lastFrame = 0xFFFFFFFFu; + + float m_hbDubTimer = -1.0f; + float m_hbLubTimer = -1.0f; + float m_hbPulseAnim = 0.0f; + int m_hbPulseType = 0; + float m_hbSwayAngle = 0.0f; + + float m_deathTime = 0.0f; +}; diff --git a/Engine_Master_UPC/PostProcessPixelShader.hlsl b/Engine_Master_UPC/PostProcessPixelShader.hlsl new file mode 100644 index 00000000..a9b3a2f6 --- /dev/null +++ b/Engine_Master_UPC/PostProcessPixelShader.hlsl @@ -0,0 +1,230 @@ +#include "General.hlsli" +#include "PBRGeneral.hlsli" + +Texture2D sceneTexture : register(t0); +Texture2D bloomTexture : register(t1); +Texture3D lutTexture : register(t2); +Texture2D depthTexture : register(t3); +SamplerState bilinearClamp : register(s0); + +cbuffer PostProcessParams : register(b0) +{ + float exposure; + float bloomIntensity; + float lutSize; + float caStrength; + + uint enableBloom; + uint enableLUT; + uint enableCA; + 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; + + // 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) + 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; +} + +float3 sampleSceneBlurred(float2 uv, float blur) +{ + if (blur <= 0.001) + return sampleScene(uv); + + 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; + + float3 sum = 0.0; + float totalWeight = 0.0; + + [unroll] + for (int i = 0; i < N; ++i) + { + 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 / totalWeight; +} + +// Layered low-health "damage screen": desaturation, red/blue vignettes, +// heartbeat pulse tint and critical edge darkening. +float3 applyHeartbeat(float3 color, float2 uv) +{ + float lum = dot(color, float3(0.299, 0.587, 0.114)); + color = lerp(color, lum.xxx, saturate(hbDesat)); + + float r = saturate(length(uv - 0.5) * 1.4); + float edge = smoothstep(0.3, 1.0, r); + + float hv = edge * hbHealthVignette; + color = lerp(color, float3(0.5, 0.0, 0.0), hv); + color *= 1.0 - hv * 0.5; + + float sv = edge * hbSepVignette; + color = lerp(color, float3(0.0, 0.1, 0.4), sv); + + 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)); + + color *= 1.0 - edge * hbCrit; + + return color; +} + +float4 main(float2 uv : TEXCOORD) : SV_TARGET +{ + // Heartbeat sway shifts the scene sampling + float2 suv = uv + float2(hbSwayX, hbSwayY); + + float sceneDepth = depthTexture.Sample(bilinearClamp, uv).r; + bool isBackground = sceneDepth >= 0.9999; + + float3 outColor; + if (isBackground) + { + 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 = min(bloomTexture.Sample(bilinearClamp, suv).rgb, 65000.0); + hdr += bloom * bloomIntensity; + } + + hdr *= 1.2 * pow(2.0, exposure); + + float3 mapped = PBRNeutralToneMapping(hdr); + + if (enableLUT != 0) + mapped = applyLUT(mapped); + + outColor = LinearToSRGB(mapped); + } + + if (enableOutline != 0) + outColor = applyOutline(outColor, uv); + + if (enableHeartbeat != 0) + outColor = applyHeartbeat(outColor, uv); + + // 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); + + return float4(outColor, 1.0); +} diff --git a/Engine_Master_UPC/PostProcessSettings.h b/Engine_Master_UPC/PostProcessSettings.h new file mode 100644 index 00000000..1ff584ca --- /dev/null +++ b/Engine_Master_UPC/PostProcessSettings.h @@ -0,0 +1,78 @@ +#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; + float bloomClamp = 0.1f; // max brightness a pixel may contribute to bloom + + // Colour grading + bool lutEnabled = false; + std::string lutPath; + + // Chromatic aberration + bool chromaticAberrationEnabled = false; + float chromaticAberrationStrength = 1.0f; + + // Heartbeat / low-health "damage screen" effect. + bool heartbeatEnabled = false; + float healthThreshold = 0.5f; + float health = 1.0f; + float separation = 0.0f; + + // 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 + { + archive.serialize(exposure, "exposure"); + + archive.serialize(bloomEnabled, "bloomEnabled"); + archive.serialize(bloomThreshold, "bloomThreshold"); + archive.serialize(bloomIntensity, "bloomIntensity"); + archive.serialize(bloomClamp, "bloomClamp"); + + archive.serialize(lutEnabled, "lutEnabled"); + archive.serialize(lutPath, "lutPath"); + + archive.serialize(chromaticAberrationEnabled, "chromaticAberrationEnabled"); + archive.serialize(chromaticAberrationStrength, "chromaticAberrationStrength"); + + 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/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/Scene.cpp b/Engine_Master_UPC/Scene.cpp index 05f3ebc2..ebec6e31 100644 --- a/Engine_Master_UPC/Scene.cpp +++ b/Engine_Master_UPC/Scene.cpp @@ -798,6 +798,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 9735226b..8a096670 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" @@ -33,6 +34,7 @@ class Scene: public Asset SceneLightingSettings m_lighting; SceneDataCB m_sceneDataCB; SkyBoxSettings m_skybox; + PostProcessSettings m_postProcess; CameraComponent* m_defaultCamera; std::vector m_rootObjects; @@ -99,6 +101,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..8381fe3d 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,69 @@ 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::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); + + 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); + + 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."); + + 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."); + } +} + 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 };