Skip to content

Commit 5e24e94

Browse files
Fix artifacts in volumetric lighting near opaque geometry (#493)
* Add view bias to volumetrics * Implement geometry-distance-aware volume rendering * Comment * Fix the flickering issue * Clean * Fix * Changelog * Implement lookup bias for volumetrics * Update CHANGELOG.md * update test filter * fix refernce screenshots Co-authored-by: sebastienlagarde <[email protected]>
1 parent 7f0f732 commit 5e24e94

File tree

10 files changed

+517
-345
lines changed

10 files changed

+517
-345
lines changed

TestProjects/HDRP_Tests/Assets/GraphicTests/Scenes/1x_Materials/1204_Lit_Fog.unity

Lines changed: 105 additions & 98 deletions
Large diffs are not rendered by default.

TestProjects/HDRP_Tests/Assets/GraphicTests/Scenes/5x_SkyAndFog/5001_Fog_FogFallback.unity

Lines changed: 282 additions & 199 deletions
Large diffs are not rendered by default.
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 2 additions & 2 deletions
Loading

com.unity.render-pipelines.high-definition/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
661661
- Fixed an exception happening when using RTSSS without using RTShadows.
662662
- Fix inconsistencies with transparent motion vectors and opaque by allowing camera only transparent motion vectors.
663663
- Fix reflection probe frame settings override
664+
- Fixed certain shadow bias artifacts present in volumetric lighting (case 1231885).
664665

665666
### Changed
666667
- Improve MIP selection for decals on Transparents

com.unity.render-pipelines.high-definition/Runtime/Lighting/AtmosphericScattering/AtmosphericScattering.hlsl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,13 +279,13 @@ void EvaluateAtmosphericScattering(PositionInputs posInput, float3 V, out float3
279279
{
280280
float4 value = SampleVBuffer(TEXTURE3D_ARGS(_VBufferLighting, s_linear_clamp_sampler),
281281
posInput.positionNDC,
282-
fogFragDist,
282+
fogFragDist,
283283
_VBufferViewportSize,
284284
_VBufferLightingViewportScale.xyz,
285285
_VBufferLightingViewportLimit.xyz,
286286
_VBufferDistanceEncodingParams,
287287
_VBufferDistanceDecodingParams,
288-
true, false);
288+
true, true, false);
289289

290290
// TODO: add some slowly animated noise (dither?) to the reconstructed value.
291291
// TODO: re-enable tone mapping after implementing pre-exposure.

com.unity.render-pipelines.high-definition/Runtime/Lighting/LightLoop/HDShadow.hlsl

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33

44
#define SHADOW_OPTIMIZE_REGISTER_USAGE 1
55

6-
#ifndef SHADOW_USE_DEPTH_BIAS
7-
#define SHADOW_USE_DEPTH_BIAS 1 // Enable clip space z biasing
6+
#ifndef SHADOW_AUTO_FLIP_NORMAL // If (NdotL < 0), we flip the normal to correctly bias lit back-faces (used for transmission)
7+
#define SHADOW_AUTO_FLIP_NORMAL 1 // Externally define as 0 to disable
8+
#endif
9+
10+
#ifndef SHADOW_USE_DEPTH_BIAS // Enable clip space z biasing
11+
#define SHADOW_USE_DEPTH_BIAS 1 // Externally define as 0 to disable
812
#endif
913

1014
#if SHADOW_OPTIMIZE_REGISTER_USAGE == 1
@@ -16,8 +20,9 @@
1620
// normalWS is the vertex normal if available or shading normal use to bias the shadow position
1721
float GetDirectionalShadowAttenuation(HDShadowContext shadowContext, float2 positionSS, float3 positionWS, float3 normalWS, int shadowDataIndex, float3 L)
1822
{
19-
// If NdotL < 0, we flip the normal in case it is used for the transmission to correctly bias shadow position
23+
#if SHADOW_AUTO_FLIP_NORMAL
2024
normalWS *= FastSign(dot(normalWS, L));
25+
#endif
2126
#if defined(SHADOW_LOW) || defined(SHADOW_MEDIUM)
2227
return EvalShadow_CascadedDepth_Dither(shadowContext, _ShadowmapCascadeAtlas, s_linear_clamp_compare_sampler, positionSS, positionWS, normalWS, shadowDataIndex, L);
2328
#else
@@ -31,8 +36,9 @@ float GetPunctualShadowAttenuation(HDShadowContext shadowContext, float2 positio
3136
shadowDataIndex = WaveReadLaneFirst(shadowDataIndex);
3237
#endif
3338

34-
// If NdotL < 0, we flip the normal in case it is used for the transmission to correctly bias shadow position
39+
#if SHADOW_AUTO_FLIP_NORMAL
3540
normalWS *= FastSign(dot(normalWS, L));
41+
#endif
3642

3743
// Note: Here we assume that all the shadow map cube faces have been added contiguously in the buffer to retreive the shadow information
3844
HDShadowData sd = shadowContext.shadowDatas[shadowDataIndex];

com.unity.render-pipelines.high-definition/Runtime/Lighting/VolumetricLighting/VBuffer.hlsl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
// if (clampToBorder), samples outside of the buffer return 0 (border color).
1313
// Otherwise, the sampler simply clamps the texture coordinate to the edge of the texture.
1414
// Warning: clamping to border may not work as expected with the quadratic filter due to its extent.
15+
//
16+
// if (biasLookup), we apply a constant bias to the look-up to avoid light leaks through geometry.
1517
float4 SampleVBuffer(TEXTURE3D_PARAM(VBuffer, clampSampler),
1618
float2 positionNDC,
1719
float linearDistance,
@@ -20,13 +22,22 @@ float4 SampleVBuffer(TEXTURE3D_PARAM(VBuffer, clampSampler),
2022
float3 VBufferViewportLimit,
2123
float4 VBufferDistanceEncodingParams,
2224
float4 VBufferDistanceDecodingParams,
25+
bool biasLookup,
2326
bool quadraticFilterXY,
2427
bool clampToBorder)
2528
{
2629
// These are the viewport coordinates.
2730
float2 uv = positionNDC;
2831
float w = EncodeLogarithmicDepthGeneralized(linearDistance, VBufferDistanceEncodingParams);
2932

33+
if (biasLookup)
34+
{
35+
// The value is higher than 0.5 (we use half of the length the diagonal of a unit cube).
36+
// to account for varying angles of incidence.
37+
// TODO: XR?
38+
w -= (sqrt(3)/2) * _VBufferRcpSliceCount;
39+
}
40+
3041
bool coordIsInsideFrustum;
3142

3243
if (clampToBorder)
@@ -79,6 +90,7 @@ float4 SampleVBuffer(TEXTURE3D_PARAM(VBuffer, clampSampler),
7990

8091
// The sampler clamps to the edge (so UVWs < 0 are OK).
8192
// TODO: perform per-sample (4, in this case) bilateral filtering, rather than per-pixel. This should reduce leaking.
93+
// Currently we don't do it, since it is expensive and doesn't appear to be helpful/necessary in practice.
8294
result = (weights[0].x * weights[0].y) * SAMPLE_TEXTURE3D_LOD(VBuffer, clampSampler, min(float3(texUv0, texW), VBufferViewportLimit), 0)
8395
+ (weights[1].x * weights[0].y) * SAMPLE_TEXTURE3D_LOD(VBuffer, clampSampler, min(float3(texUv1, texW), VBufferViewportLimit), 0)
8496
+ (weights[0].x * weights[1].y) * SAMPLE_TEXTURE3D_LOD(VBuffer, clampSampler, min(float3(texUv2, texW), VBufferViewportLimit), 0)
@@ -105,6 +117,7 @@ float4 SampleVBuffer(TEXTURE3D_PARAM(VBuffer, clampSampler),
105117
float3 VBufferViewportLimit,
106118
float4 VBufferDistanceEncodingParams,
107119
float4 VBufferDistanceDecodingParams,
120+
bool biasLookup,
108121
bool quadraticFilterXY,
109122
bool clampToBorder)
110123
{
@@ -119,6 +132,7 @@ float4 SampleVBuffer(TEXTURE3D_PARAM(VBuffer, clampSampler),
119132
VBufferViewportLimit,
120133
VBufferDistanceEncodingParams,
121134
VBufferDistanceDecodingParams,
135+
biasLookup,
122136
quadraticFilterXY,
123137
clampToBorder);
124138
}

com.unity.render-pipelines.high-definition/Runtime/Lighting/VolumetricLighting/VolumetricLighting.compute

Lines changed: 97 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,14 @@
2828
#define VBUFFER_VOXEL_SIZE 8
2929
#endif
3030

31-
#define GROUP_SIZE_1D 8
32-
#define SUPPORT_LOCAL_LIGHTS 1 // Local lights contribute to fog lighting
33-
#define SHADOW_USE_DEPTH_BIAS 0
34-
#define PREFER_HALF 0
31+
#define PREFER_HALF 0
32+
#define GROUP_SIZE_1D 8
33+
#define SUPPORT_LOCAL_LIGHTS 1 // Local lights contribute to fog lighting
34+
#define SHADOW_USE_DEPTH_BIAS 0 // Too expensive, not particularly effective
35+
#define SHADOW_LOW // Too expensive
36+
#define SHADOW_AUTO_FLIP_NORMAL 0 // No normal information, so no need to flip
37+
#define SHADOW_VIEW_BIAS 1 // Prevents light leaking through thin geometry. Not as good as normal bias at grazing angles, but cheaper and independent from the geometry.
38+
#define USE_DEPTH_BUFFER 1 // Accounts for opaque geometry along the camera ray
3539

3640
//--------------------------------------------------------------------------------------------------
3741
// Included headers
@@ -56,8 +60,6 @@
5660

5761
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Sky/PhysicallyBasedSky/PhysicallyBasedSkyCommon.hlsl"
5862

59-
// Use low quality shadow when doing volumetric
60-
#define SHADOW_LOW
6163
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/Lighting.hlsl"
6264
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/LightLoop/LightLoopDef.hlsl"
6365
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/LightEvaluation.hlsl"
@@ -83,6 +85,7 @@ struct JitteredRay
8385
float3 jitterDirWS;
8486
float3 xDirDerivWS;
8587
float3 yDirDerivWS;
88+
float geomDist;
8689
};
8790

8891
struct VoxelLighting
@@ -91,6 +94,11 @@ struct VoxelLighting
9194
float3 radianceNoPhase;
9295
};
9396

97+
bool IsInRange(float x, float2 range)
98+
{
99+
return clamp(x, range.x, range.y) == x;
100+
}
101+
94102
float ComputeHistoryWeight()
95103
{
96104
// Compute the exponential moving average over 'n' frames:
@@ -149,9 +157,13 @@ VoxelLighting EvaluateVoxelLightingDirectional(LightLoopContext context, uint fe
149157
// Is it worth sampling the shadow map?
150158
if ((light.volumetricLightDimmer > 0) && (light.volumetricShadowDimmer > 0))
151159
{
152-
// Pass the light direction as the normal to avoid issues with shadow biasing code.
153-
// TODO: does it make sense?
154-
float3 shadowN = L;
160+
#if SHADOW_VIEW_BIAS
161+
// Our shadows only support normal bias. Volumetrics has no access to the surface normal.
162+
// We fake view bias by invoking the normal bias code with the view direction.
163+
float3 shadowN = -ray.jitterDirWS;
164+
#else
165+
float3 shadowN = 0; // No bias
166+
#endif // SHADOW_VIEW_BIAS
155167

156168
context.shadowValue = GetDirectionalShadowAttenuation(context.shadowContext,
157169
posInput.positionSS, posInput.positionWS, shadowN,
@@ -182,9 +194,13 @@ VoxelLighting EvaluateVoxelLightingDirectional(LightLoopContext context, uint fe
182194
lightColor.a *= light.volumetricLightDimmer;
183195
lightColor.rgb *= lightColor.a; // Composite
184196

185-
// Pass the light direction as the normal to avoid issues with shadow biasing code.
186-
// TODO: does it make sense?
187-
float3 shadowN = L;
197+
#if SHADOW_VIEW_BIAS
198+
// Our shadows only support normal bias. Volumetrics has no access to the surface normal.
199+
// We fake view bias by invoking the normal bias code with the view direction.
200+
float3 shadowN = -ray.jitterDirWS;
201+
#else
202+
float3 shadowN = 0; // No bias
203+
#endif // SHADOW_VIEW_BIAS
188204

189205
// This code works for both surface reflection and thin object transmission.
190206
DirectionalShadowType shadow = EvaluateShadow_Directional(context, posInput, light, unused, shadowN);
@@ -298,9 +314,13 @@ VoxelLighting EvaluateVoxelLightingLocal(LightLoopContext context, uint featureF
298314
lightColor.a *= light.volumetricLightDimmer;
299315
lightColor.rgb *= lightColor.a; // Composite
300316

301-
// Pass the light direction as the normal to avoid issues with shadow biasing code.
302-
// TODO: does it make sense?
303-
float3 shadowN = L;
317+
#if SHADOW_VIEW_BIAS
318+
// Our shadows only support normal bias. Volumetrics has no access to the surface normal.
319+
// We fake view bias by invoking the normal bias code with the view direction.
320+
float3 shadowN = -ray.jitterDirWS;
321+
#else
322+
float3 shadowN = 0; // No bias
323+
#endif // SHADOW_VIEW_BIAS
304324

305325
float shadow = EvaluateShadow_Punctual(context, posInput, light, unused, shadowN, L, distances);
306326
lightColor.rgb *= ComputeShadowColor(shadow, light.shadowTint, light.penumbraTint);
@@ -379,9 +399,13 @@ VoxelLighting EvaluateVoxelLightingLocal(LightLoopContext context, uint featureF
379399
lightColor.a *= light.volumetricLightDimmer;
380400
lightColor.rgb *= lightColor.a; // Composite
381401

382-
// Pass the light direction as the normal to avoid issues with shadow biasing code.
383-
// TODO: does it make sense?
384-
float3 shadowN = L;
402+
#if SHADOW_VIEW_BIAS
403+
// Our shadows only support normal bias. Volumetrics has no access to the surface normal.
404+
// We fake view bias by invoking the normal bias code with the view direction.
405+
float3 shadowN = -ray.jitterDirWS;
406+
#else
407+
float3 shadowN = 0; // No bias
408+
#endif // SHADOW_VIEW_BIAS
385409

386410
float shadow = EvaluateShadow_Punctual(context, posInput, light, unused, shadowN, L, distances);
387411
lightColor.rgb *= ComputeShadowColor(shadow, light.shadowTint, light.penumbraTint);
@@ -474,7 +498,22 @@ void FillVolumetricLightingBuffer(LightLoopContext context, uint featureFlags,
474498

475499
float e1 = slice * de + de; // (slice + 1) / sliceCount
476500
float t1 = DecodeLogarithmicDepthGeneralized(e1, _VBufferDistanceDecodingParams);
477-
float dt = t1 - t0;
501+
float tNext = t1;
502+
503+
#if USE_DEPTH_BUFFER
504+
bool containsOpaqueGeometry = IsInRange(ray.geomDist, float2(t0, t1));
505+
506+
if (containsOpaqueGeometry)
507+
{
508+
// Only integrate up to the opaque surface (make the voxel shorter, but not completely flat).
509+
// Note that we can NOT completely stop integrating when the ray reaches geometry, since
510+
// otherwise we get flickering at geometric discontinuities if reprojection is enabled.
511+
// In this case, a temporally stable light leak is better than flickering.
512+
t1 = max(t0 * 1.0001, ray.geomDist);
513+
}
514+
#endif
515+
516+
float dt = t1 - t0; // Is geometry-aware
478517

479518
// Accurately compute the center of the voxel in the log space. It's important to perform
480519
// the inversion exactly, since the accumulated value of the integral is stored at the center.
@@ -553,7 +592,7 @@ void FillVolumetricLightingBuffer(LightLoopContext context, uint featureFlags,
553592
_VBufferHistoryViewportLimit.xyz,
554593
_VBufferPrevDistanceEncodingParams,
555594
_VBufferPrevDistanceDecodingParams,
556-
false, true) * float4(GetInversePreviousExposureMultiplier().xxx, 1);
595+
false, false, true) * float4(GetInversePreviousExposureMultiplier().xxx, 1);
557596

558597
bool reprojSuccess = (_VBufferHistoryIsValid != 0) && (reprojValue.a != 0);
559598

@@ -592,7 +631,7 @@ void FillVolumetricLightingBuffer(LightLoopContext context, uint featureFlags,
592631
blendValue.rgb *= phaseCurrFrame;
593632
#endif // ENABLE_ANISOTROPY
594633

595-
#else // ENABLE_REPROJECTION
634+
#else // NO REPROJECTION
596635

597636
#ifdef ENABLE_ANISOTROPY
598637
float4 blendValue = float4(aggregateLighting.radianceComplete, extinction * dt);
@@ -615,6 +654,7 @@ void FillVolumetricLightingBuffer(LightLoopContext context, uint featureFlags,
615654
// Integral{a, b}{Transmittance(0, t) * L_s(t) dt} = Transmittance(0, a) * Integral{a, b}{Transmittance(0, t - a) * L_s(t) dt}.
616655
float3 probeRadiance = probeInScatteredRadiance * TransmittanceIntegralHomogeneousMedium(extinction, dt);
617656

657+
// Accumulate radiance along the ray.
618658
totalRadiance += transmittance * scattering * (phase * blendValue.rgb + probeRadiance);
619659

620660
// Compute the optical depth up to the center of the interval.
@@ -630,7 +670,7 @@ void FillVolumetricLightingBuffer(LightLoopContext context, uint featureFlags,
630670
// Compute the optical depth up to the end of the interval.
631671
opticalDepth += 0.5 * blendValue.a;
632672

633-
t0 = t1;
673+
t0 = tNext;
634674
}
635675
}
636676

@@ -643,17 +683,6 @@ void VolumetricLighting(uint3 dispatchThreadId : SV_DispatchThreadID,
643683

644684
uint2 groupOffset = groupId * GROUP_SIZE_1D;
645685
uint2 voxelCoord = groupOffset + groupThreadId;
646-
#ifdef VL_PRESET_OPTIMAL
647-
// The entire thread group is within the same light tile.
648-
uint2 tileCoord = groupOffset * VBUFFER_VOXEL_SIZE / TILE_SIZE_BIG_TILE;
649-
#else
650-
// No compile-time optimizations, no scalarization.
651-
// If _VBufferVoxelSize is not a power of 2 or > TILE_SIZE_BIG_TILE, a voxel may straddle
652-
// a tile boundary. This means different voxel subsamples may belong to different tiles.
653-
// We accept this error, and simply use the coordinates of the center of the voxel.
654-
uint2 tileCoord = (uint2)((voxelCoord + 0.5) * _VBufferVoxelSize / TILE_SIZE_BIG_TILE);
655-
#endif
656-
uint tileIndex = tileCoord.x + _NumTileBigTileX * tileCoord.y;
657686

658687
// Reminder: our voxels are sphere-capped right frustums (truncated right pyramids).
659688
// The curvature of the front and back faces is quite gentle, so we can use
@@ -665,6 +694,7 @@ void VolumetricLighting(uint3 dispatchThreadId : SV_DispatchThreadID,
665694

666695
float3 F = GetViewForwardDir();
667696
float3 U = GetViewUpDir();
697+
float3 R = cross(F, U);
668698

669699
float2 centerCoord = voxelCoord + float2(0.5, 0.5);
670700

@@ -685,14 +715,45 @@ void VolumetricLighting(uint3 dispatchThreadId : SV_DispatchThreadID,
685715
ray.yDirDerivWS = cross(ray.xDirDerivWS, ray.centerDirWS); // Will have the length of 'unitDistFaceSize' by construction
686716

687717
#ifdef ENABLE_REPROJECTION
688-
ray.jitterDirWS = normalize(ray.centerDirWS + _VBufferSampleOffset.x * ray.xDirDerivWS
689-
+ _VBufferSampleOffset.y * ray.yDirDerivWS);
718+
float2 sampleOffset = _VBufferSampleOffset.xy;
690719
#else
691-
ray.jitterDirWS = ray.centerDirWS;
720+
float2 sampleOffset = 0;
692721
#endif
693722

723+
ray.jitterDirWS = normalize(ray.centerDirWS + sampleOffset.x * ray.xDirDerivWS
724+
+ sampleOffset.y * ray.yDirDerivWS);
725+
726+
// We would like to determine the screen pixel (at the full resolution) which
727+
// the jittered ray corresponds to. The exact solution can be obtained by intersecting
728+
// the ray with the screen plane, e.i. (ViewSpace(jitterDirWS).z = 1). That's a little expensive.
729+
// So, as an approximation, we ignore the curvature of the frustum.
730+
uint2 pixelCoord = (uint2)((voxelCoord + 0.5 + sampleOffset) * _VBufferVoxelSize);
731+
732+
#ifdef VL_PRESET_OPTIMAL
733+
// The entire thread group is within the same light tile.
734+
uint2 tileCoord = groupOffset * VBUFFER_VOXEL_SIZE / TILE_SIZE_BIG_TILE;
735+
#else
736+
// No compile-time optimizations, no scalarization.
737+
uint2 tileCoord = pixelCoord / TILE_SIZE_BIG_TILE;
738+
#endif
739+
uint tileIndex = tileCoord.x + _NumTileBigTileX * tileCoord.y;
740+
741+
// Do not jitter 'voxelCoord' else. It's expected to correspond to the center of the voxel.
694742
PositionInputs posInput = GetPositionInput(voxelCoord, _VBufferViewportSize.zw, tileCoord);
695743

744+
ray.geomDist = FLT_INF;
745+
746+
#if USE_DEPTH_BUFFER
747+
float deviceDepth = LoadCameraDepth(pixelCoord);
748+
749+
if (deviceDepth > 0) // Skip the skybox
750+
{
751+
// Convert it to distance along the ray. Doesn't work with tilt shift, etc.
752+
float linearDepth = LinearEyeDepth(deviceDepth, _ZBufferParams);
753+
ray.geomDist = linearDepth * rcp(dot(ray.jitterDirWS, F));
754+
}
755+
#endif
756+
696757
// TODO
697758
LightLoopContext context;
698759
context.shadowContext = InitShadowContext();

0 commit comments

Comments
 (0)