diff --git a/code/renderer/shaders/crp/transp_draw.hlsl b/code/renderer/shaders/crp/transp_draw.hlsl index 1ca2dcf..2398ca7 100644 --- a/code/renderer/shaders/crp/transp_draw.hlsl +++ b/code/renderer/shaders/crp/transp_draw.hlsl @@ -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)) diff --git a/code/renderer/shaders/crp/transp_resolve.hlsl b/code/renderer/shaders/crp/transp_resolve.hlsl index 8ed7a2a..3fc1aca 100644 --- a/code/renderer/shaders/crp/transp_resolve.hlsl +++ b/code/renderer/shaders/crp/transp_resolve.hlsl @@ -21,6 +21,56 @@ along with Challenge Quake 3. If not, see . // 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) - { - // @TODO: fix this loop to account for the depth test - OIT_Fragment frag = sorted[j]; - 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(i + 1, dstDepth, 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 + BlendInScatteredLight(fragColor, inScatterAccum, stateBits & GLS_BLEND_BITS); + color.rgb *= transmittance; scatterData = closerScatterData; #endif } + +#if defined(VOLUMETRIC_LIGHT) + color.rgb += inScatterAccum; +#endif + + return color; } - void RemoveInvisible() +#if defined(VOLUMETRIC_LIGHT) + float4 FindCloserScatterData(uint startIndex, float dstDepth, VOut input, float3 volumeSize) { - uint newCount = 0; - for(uint i = 0; i < fragmentCount; ++i) + float4 closerScatterData = float4(0, 0, 0, 1); + for(uint j = startIndex; j < fragmentCount; ++j) { - if(invisible[i]) + 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; } - if(newCount != i) - { - sorted[newCount] = sorted[i]; - } - newCount++; + + float froxelDepth01 = scene.FroxelViewDepthToZ01(fragDepth, volumeSize.z); + float3 scatterTC = float3(input.texCoords, froxelDepth01); + closerScatterData = scatterTexture.SampleLevel(scatterSampler, scatterTC, 0); + break; } - 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 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; diff --git a/code/renderer/tr_local.h b/code/renderer/tr_local.h index eaaea71..869509f 100644 --- a/code/renderer/tr_local.h +++ b/code/renderer/tr_local.h @@ -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 ); /* ==================================================================== diff --git a/code/renderer/tr_shader.cpp b/code/renderer/tr_shader.cpp index 62dab03..e1dca70 100644 --- a/code/renderer/tr_shader.cpp +++ b/code/renderer/tr_shader.cpp @@ -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 ""; + } +}