/*
===========================================================================
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);
}
}