fixed several OIT issues

- all: fixed depth test (yet another reverse Z pitfall...)
- VL: fixed output color mismatch when a low-impact fragment is added
- VL: fixed next closer fragment search ignoring the depth test
This commit is contained in:
myT 2024-05-03 01:17:11 +02:00
parent c937948f8f
commit 7c217a313d
4 changed files with 141 additions and 84 deletions

View file

@ -85,7 +85,7 @@ VOut vs(VIn input)
bool IsFragmentUseless(uint blendBits, float4 color)
{
const float epsilon = 1.0 / 255.0;
const float epsilon = 1.0 / 1024.0;
if(blendBits == GLS_BLEND_ADDITIVE &&
all(color.rgb < epsilon.xxx))

View file

@ -21,6 +21,56 @@ along with Challenge Quake 3. If not, see <https://www.gnu.org/licenses/>.
// reads per-pixel fragment linked lists into arrays, sorts them and composites them
/*
OIT integration with volumetric lighting
Each texel has the final accumulated in-scattering (RGB) and transmittance (A)
from the near clip plane to the current voxel's depth.
The in-scattered light and transmittance values for any given depth range are therefore:
inScattering = farScatter - nearScatter
transmittance = farTrans / nearTrans
Definitions:
B : opaque/background surface color
C : color accumulator
D : color accumulator #2
T : transmittance from near plane to opaque surface
S : in-scattered light from near plane to opaque surface
T': transmittance from near plane to fragment
S': in-scattered light from near plane to fragment
If we use a single color accumulator C and apply transmittance T and in-scattering S post-blend,
we run the following logic for the opaque surface and every fragment:
C = Blend(C, ...)
C = CT + S
Doing this creates artifacts along the geometric edges of explosions (additive blend, black borders). But why?
Suppose we have no fragment, the final color F is therefore:
F = BT + S
What if we add a single fragment that contributes nothing (e.g. black and additive blended)?
C = BT/T' + S - S'
C = Blend(C, ...) // doesn't change C
F = (BT/T' + S - S') * T'/1 + S' - 0
F = BT + ST' - S'T' + S'
It's mismatched: we get (ST' - S'T' + S') instead of (S).
Let's try a separate color accumulator D just for the in-scattered light like so:
C = Blend(C, ...)
C = CT
D = D + S
F = C + D
No fragment case:
F = C + D == BT + S
Single useless fragment case:
C = BT/T'
D = S - S'
C = Blend(C, ...) // doesn't change C
C = CT'/1 == BT/T' * T'/1 == BT
D = D + S' - 0 == S - S' + S' - 0 == S
F = C + D == BT + S
Now it matches perfectly.
*/
#include "common.hlsli"
#include "fullscreen.hlsli"
#include "oit.h.hlsli"
@ -97,24 +147,25 @@ float4 DepthFadeFragmentColor(float4 color, OIT_Fragment fragment, float opaqueV
return dst;
}
float FragmentImpact(float4 fragColor, uint blendBits)
float3 BlendInScatteredLight(float4 srcColor, float3 dstColor, uint blendBits)
{
if(blendBits == GLS_BLEND_ADDITIVE)
// source blend must not double source contributions, so we can't call BlendSource
uint srcBlend = blendBits & GLS_SRCBLEND_BITS;
float3 srcContrib = float3(0, 0, 0);
if(srcBlend == GLS_SRCBLEND_DST_COLOR)
{
return Brightness(fragColor.rgb);
srcContrib = dstColor * (float3(1, 1, 1) - dstColor);
}
else if(srcBlend == GLS_SRCBLEND_ONE_MINUS_DST_COLOR)
{
srcContrib = dstColor * (float3(1, 1, 1) - dstColor);
}
if(blendBits == GLS_BLEND_STD_ALPHA || blendBits == GLS_BLEND_PMUL_ALPHA)
{
return fragColor.a;
}
uint dstBlend = blendBits & GLS_DSTBLEND_BITS;
float3 dstContrib = BlendDest(srcColor, float4(dstColor, 1), dstBlend).rgb;
float3 result = srcContrib + dstContrib;
if(blendBits == GLS_BLEND_FILTER || blendBits == GLS_BLEND_FILTER_V2)
{
return abs(1.0 - Brightness(fragColor.rgb));
}
return 1.0;
return result;
}
struct OIT_Resolve
@ -153,7 +204,6 @@ struct OIT_Resolve
{
sorted[fragmentCount] = fragments[fragmentIndex];
fragmentIndex = sorted[fragmentCount].next;
invisible[fragmentCount] = false;
++fragmentCount;
}
@ -172,12 +222,12 @@ struct OIT_Resolve
}
}
void Resolve(VOut input, float impactThreshold)
float4 Resolve(VOut input)
{
color = opaqueColor;
smallestImpact = 666.0;
float4 color = saturate(opaqueColor);
#if defined(VOLUMETRIC_LIGHT)
float3 inScatterAccum = float3(0, 0, 0);
// initialize volume traversal
float3 volumeSize = GetTextureSize(scatterTexture);
float opaqueFroxelDepth01 = scene.FroxelViewDepthToZ01(opaqueViewDepth, volumeSize.z);
@ -186,27 +236,18 @@ struct OIT_Resolve
float3 scatterTC = float3(input.texCoords, opaqueFroxelDepth01);
float4 scatterData = scatterTexture.SampleLevel(scatterSampler, scatterTC, 0);
{
float4 closerScatterData = float4(0, 0, 0, 1);
for(uint i = 0; i < fragmentCount; ++i)
{
// @TODO: fix this loop to account for the depth test
OIT_Fragment frag = sorted[i];
float fragDepth = frag.depth;
float froxelDepth01 = scene.FroxelViewDepthToZ01(fragDepth, volumeSize.z);
float3 scatterTC = float3(input.texCoords, froxelDepth01);
closerScatterData = scatterTexture.SampleLevel(scatterSampler, scatterTC, 0);
break;
}
float4 closerScatterData = FindCloserScatterData(0, -1.0, input, volumeSize);
float3 inScattering = scatterData.rgb - closerScatterData.rgb;
float transmittance = scatterData.a / max(closerScatterData.a, 0.000001);
color.rgb = color.rgb * transmittance + inScattering;
float transmittance = min(scatterData.a / max(closerScatterData.a, 0.000001), 1.0);
inScatterAccum = inScattering;
color.rgb = color.rgb * transmittance;
scatterData = closerScatterData;
}
#endif
// blend the results
lastFragmentIndex = -1;
float dstDepth = 1.0;
float dstDepth = -1.0;
for(uint i = 0; i < fragmentCount; ++i)
{
OIT_Fragment frag = sorted[i];
@ -221,9 +262,10 @@ struct OIT_Resolve
float4 fragColor = UnpackColor(frag.color);
float4 prevColor = color;
fragColor = DepthFadeFragmentColor(fragColor, frag, opaqueViewDepth);
color = Blend(fragColor, color, frag.stateBits);
color = Blend(fragColor, color, stateBits);
color = saturate(color);
if((stateBits & GLS_DEPTHMASK_TRUE) != 0u &&
fragDepth < dstDepth)
fragDepth != dstDepth)
{
dstDepth = fragDepth;
}
@ -235,45 +277,46 @@ struct OIT_Resolve
}
#if defined(VOLUMETRIC_LIGHT)
float fragmentImpact = FragmentImpact(fragColor, stateBits & GLS_BLEND_BITS);
invisible[i] = fragmentImpact < impactThreshold;
smallestImpact = min(smallestImpact, fragmentImpact);
float4 closerScatterData = float4(0, 0, 0, 1);
for(uint j = i + 1; j < fragmentCount; ++j)
float4 closerScatterData = FindCloserScatterData(i + 1, dstDepth, input, volumeSize);
float3 inScattering = scatterData.rgb - closerScatterData.rgb;
float transmittance = min(scatterData.a / max(closerScatterData.a, 0.000001), 1.0);
inScatterAccum = inScattering + BlendInScatteredLight(fragColor, inScatterAccum, stateBits & GLS_BLEND_BITS);
color.rgb *= transmittance;
scatterData = closerScatterData;
#endif
}
#if defined(VOLUMETRIC_LIGHT)
color.rgb += inScatterAccum;
#endif
return color;
}
#if defined(VOLUMETRIC_LIGHT)
float4 FindCloserScatterData(uint startIndex, float dstDepth, VOut input, float3 volumeSize)
{
float4 closerScatterData = float4(0, 0, 0, 1);
for(uint j = startIndex; j < fragmentCount; ++j)
{
// @TODO: fix this loop to account for the depth test
OIT_Fragment frag = sorted[j];
uint stateBits = frag.stateBits;
float fragDepth = frag.depth;
if((stateBits & (GLS_DEPTHFUNC_EQUAL | GLS_DEPTHTEST_DISABLE)) == GLS_DEPTHFUNC_EQUAL &&
fragDepth != dstDepth)
{
continue;
}
float froxelDepth01 = scene.FroxelViewDepthToZ01(fragDepth, volumeSize.z);
float3 scatterTC = float3(input.texCoords, froxelDepth01);
closerScatterData = scatterTexture.SampleLevel(scatterSampler, scatterTC, 0);
break;
}
float3 inScattering = scatterData.rgb - closerScatterData.rgb;
float transmittance = scatterData.a / max(closerScatterData.a, 0.000001);
color.rgb = color.rgb * transmittance + inScattering;
scatterData = closerScatterData;
#endif
}
}
void RemoveInvisible()
{
uint newCount = 0;
for(uint i = 0; i < fragmentCount; ++i)
{
if(invisible[i])
{
continue;
}
if(newCount != i)
{
sorted[newCount] = sorted[i];
}
newCount++;
}
fragmentCount = newCount;
return closerScatterData;
}
#endif
void WriteShaderID(VOut input)
{
@ -300,13 +343,10 @@ struct OIT_Resolve
Texture2D renderTarget;
int3 tcPx;
float4 opaqueColor;
float4 color;
OIT_Fragment sorted[OIT_MAX_FRAGMENTS_PER_PIXEL];
bool invisible[OIT_MAX_FRAGMENTS_PER_PIXEL];
uint fragmentCount;
int lastFragmentIndex;
float opaqueViewDepth;
float smallestImpact;
#if defined(VOLUMETRIC_LIGHT)
Texture3D<float4> scatterTexture;
SamplerState scatterSampler;
@ -322,27 +362,7 @@ float4 ps(VOut input) : SV_Target
}
resolve.Init(input);
#if defined(VOLUMETRIC_LIGHT)
// To fight off discontinuities between adjacent pixels,
// we interpolate between the result computed normally
// and computed by rejecting low-impact fragments
// using a t value that only depends on the low-impact fragments.
// It's far from perfect but once we get rid of most sprites
// in favor of particles, we should be fine.
const float VL_ImpactThreshold = 4.0 / 255.0;
resolve.Resolve(input, VL_ImpactThreshold);
float diff = resolve.smallestImpact;
float4 color = resolve.color;
resolve.RemoveInvisible();
resolve.Resolve(input, 0.0);
float4 color2 = resolve.color;
color = lerp(color2, color, saturate(diff / VL_ImpactThreshold));
#else
resolve.Resolve(input, 666.0);
float4 color = resolve.color;
#endif
float4 color = resolve.Resolve(input);
resolve.WriteShaderID(input);
return color;

View file

@ -1269,6 +1269,8 @@ void R_CompleteShaderName_f( int startArg, int compArg );
const char* R_GetShaderPath( const shader_t* shader );
qbool R_EditShader( shader_t* sh, const shader_t* original, const char* shaderText );
void R_SetShaderData( shader_t* sh, const shader_t* original );
const char* R_GetSourceBlendName( unsigned int stateBits );
const char* R_GetDestBlendName( unsigned int stateBits );
/*
====================================================================

View file

@ -3273,3 +3273,38 @@ const char* R_GetShaderPath( const shader_t* sh )
return va( "scripts/%s", fileName );
}
const char* R_GetSourceBlendName( unsigned int stateBits )
{
switch ( stateBits & GLS_SRCBLEND_BITS ) {
case GLS_SRCBLEND_ZERO: return "0";
case GLS_SRCBLEND_ONE: return "1";
case GLS_SRCBLEND_DST_COLOR: return "dst.rgb";
case GLS_SRCBLEND_ONE_MINUS_DST_COLOR: return "(1 - dst.rgb)";
case GLS_SRCBLEND_SRC_ALPHA: return "src.a";
case GLS_SRCBLEND_ONE_MINUS_SRC_ALPHA: return "(1 - src.a)";
case GLS_SRCBLEND_DST_ALPHA: return "dst.a";
case GLS_SRCBLEND_ONE_MINUS_DST_ALPHA: return "(1 - dst.a)";
case GLS_SRCBLEND_ALPHA_SATURATE: return "min(src.a, 1 - dst.a)";
case 0: return "1";
default: Q_assert( !"Invalid source blend bits" ); return "";
}
}
const char* R_GetDestBlendName( unsigned int stateBits )
{
switch ( stateBits & GLS_DSTBLEND_BITS ) {
case GLS_DSTBLEND_ZERO: return "0";
case GLS_DSTBLEND_ONE: return "1";
case GLS_DSTBLEND_SRC_COLOR: return "src.rgb";
case GLS_DSTBLEND_ONE_MINUS_SRC_COLOR: return "(1 - src.rgb)";
case GLS_DSTBLEND_SRC_ALPHA: return "src.a";
case GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA: return "(1 - src.a)";
case GLS_DSTBLEND_DST_ALPHA: return "dst.a";
case GLS_DSTBLEND_ONE_MINUS_DST_ALPHA: return "(1 - dst.a)";
case 0: return "0";
default: Q_assert( !"Invalid dest blend bits" ); return "";
}
}