/* =========================================================================== Copyright (C) 2023-2024 Gian 'myT' Schellenbaum This file is part of Challenge Quake 3 (CNQ3). Challenge Quake 3 is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Challenge Quake 3 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Challenge Quake 3. If not, see . =========================================================================== */ // gather depth of field: near-field and far-field blur #include "common.hlsli" #include "gatherdof.hlsli" cbuffer RootConstants : register(b0) { uint colorTextureIndex; uint nearColorTextureIndex; uint nearMaxCocTextureIndex; // tile uint nearCocTextureIndex; // blurry uint nearOutputTextureIndex; uint farColorTextureIndex; uint farCocTextureIndex; // sharp uint farOutputTextureIndex; uint samplerIndex; // linear/clamp float brightnessScale; float bladeCount; float bokehAngleRad; }; // the input is in [0,1]^2, the output polygon is centered at the origin float2 MapUnitSquareToPolygon(float2 square, float apertureBladeCount, float apertureAngleRad) { // needed to avoid inf/nan propagation through theta for samples // that are exactly in the middle of the quad on either axis // (i.e. square.x|y == 0.5 gets remapped to 0.0) const float epsilon = 0.000001; // morph into a square in [-1,1]^2 square = square * 2.0 - 1.0; // morph the square into a disk // "A Low Distortion Map Between Disk and Square" by Peter Shirley and Kenneth Chiu float radius, angle; float2 square2 = square * square; if(square2.x > square2.y) { // left and right quadrants radius = square.x; angle = (square.y * PI_D4) / (square.x + epsilon); } else { // top and bottom quadrants radius = square.y; angle = PI_D2 - (square.x * PI_D4) / (square.y + epsilon); } if(radius < 0.0) { radius = -radius; angle += PI; } // morph the disk into a polygon // "Graphics Gems from CryENGINE 3" by Tiago Sousa float edgeCount = apertureBladeCount; if(edgeCount >= 3.0) { float num = cos(PI / edgeCount); float den0 = PI_M2 / edgeCount; float den1 = (angle * edgeCount + PI) / PI_M2; float den = angle - (den0 * floor(den1)); radius *= num / cos(den); angle += apertureAngleRad; } float2 disk; sincos(angle, disk.y, disk.x); disk *= radius; return disk; } float4 BlurFarField(Texture2D inTexture, SamplerState samplerState, float coc, float2 tc01, float2 pixelSize) { const int TAP_COUNT_BLUR = 16; float2 tcScale = 16.0 * coc * pixelSize; float4 result = inTexture.SampleLevel(samplerState, tc01, 0); for(int y = 0; y < TAP_COUNT_BLUR; ++y) { for(int x = 0; x < TAP_COUNT_BLUR; ++x) { float2 tcQuad = float2(x, y) / float(TAP_COUNT_BLUR - 1); float2 tcOffset = MapUnitSquareToPolygon(tcQuad, bladeCount, bokehAngleRad) * tcScale; float4 sampleValue = inTexture.SampleLevel(samplerState, tc01 + tcOffset, 0); result += sampleValue; } } result /= result.a; return result; } float4 BlurNearField(Texture2D inTexture, SamplerState samplerState, float tileMaxCoc, float2 tc01, float2 pixelSize) { const int TAP_COUNT_BLUR = 15; // must be odd so we generate 1 sample at 0.5, 0.5 in the quad float2 tcScale = 16.0 * tileMaxCoc * pixelSize; float insideCount = 0.0; float totalCount = 1.0 + float(TAP_COUNT_BLUR * TAP_COUNT_BLUR); float4 result = float4(0, 0, 0, 0); float weightSum = 0.0; for(int y = 0; y < TAP_COUNT_BLUR; ++y) { for(int x = 0; x < TAP_COUNT_BLUR; ++x) { float2 tcQuad = float2(x, y) / float(TAP_COUNT_BLUR - 1); float2 tcOffset = MapUnitSquareToPolygon(tcQuad, bladeCount, bokehAngleRad) * tcScale; float4 sampleValue = inTexture.SampleLevel(samplerState, tc01 + tcOffset, 0); float inside = sampleValue.a > 0.0 ? 1.0 : 0.0; float brightnessWeight = 1.0 + brightnessScale * Brightness(sampleValue.rgb); float colorWeight = (sampleValue.a / tileMaxCoc) * brightnessWeight; insideCount += inside; weightSum += inside * colorWeight; result += inside * float4(colorWeight.xxx, 1) * sampleValue; } } if(insideCount >= 1.0) { result.rgb /= weightSum; result.a /= insideCount; result.a *= EaseInOutCubic(saturate(2.0 * (insideCount / totalCount))); } else { result = float4(1, 1, 0, 0); } return result; } [numthreads(8, 8, 1)] void cs(uint3 dtid : SV_DispatchThreadID) { uint2 tc = dtid.xy; RWTexture2D nearOutputTexture = ResourceDescriptorHeap[nearOutputTextureIndex]; RWTexture2D farOutputTexture = ResourceDescriptorHeap[farOutputTextureIndex]; uint width, height; farOutputTexture.GetDimensions(width, height); if(any(dtid.xy >= uint2(width, height))) { return; } SamplerState samplerState = SamplerDescriptorHeap[samplerIndex]; Texture2D colorTexture = ResourceDescriptorHeap[colorTextureIndex]; Texture2D nearColorTexture = ResourceDescriptorHeap[nearColorTextureIndex]; Texture2D farColorTexture = ResourceDescriptorHeap[farColorTextureIndex]; Texture2D nearCocTexture = ResourceDescriptorHeap[nearCocTextureIndex]; Texture2D nearMaxCocTexture = ResourceDescriptorHeap[nearMaxCocTextureIndex]; Texture2D farCocTexture = ResourceDescriptorHeap[farCocTextureIndex]; RWTexture2D nearOutTexture = ResourceDescriptorHeap[nearOutputTextureIndex]; RWTexture2D farOutTexture = ResourceDescriptorHeap[farOutputTextureIndex]; float2 tc01 = (float2(dtid.xy) + float2(0.5, 0.5)) / float2(width, height); float2 pixelSize = float2(1, 1) / float2(width, height); float nearCoc = nearCocTexture.SampleLevel(samplerState, tc01, 0); float nearMaxCoc = nearMaxCocTexture.SampleLevel(samplerState, tc01, 0); float farCoc = farCocTexture.SampleLevel(samplerState, tc01, 0); float4 color = colorTexture.SampleLevel(samplerState, tc01, 0); if(nearMaxCoc > 0.0) { nearOutTexture[tc] = BlurNearField(nearColorTexture, samplerState, nearMaxCoc, tc01, pixelSize); } else { // A must be 0 to disable the near field from being blended nearOutTexture[tc] = float4(color.rgb, 0); } if(farCoc > 0.0) { farOutTexture[tc] = BlurFarField(farColorTexture, samplerState, farCoc, tc01, pixelSize); } else { // RGB must be 0 to not mess up the fill pass of neighbor pixels that are inside the near field // A must be 0 to disable the far field from being blended farOutTexture[tc] = float4(0, 0, 0, 0); } }