Skip to content

Fix artifacts in volumetric lighting near opaque geometry #493

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jun 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions com.unity.render-pipelines.high-definition/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Fixed an exception happening when using RTSSS without using RTShadows.
- Fix inconsistencies with transparent motion vectors and opaque by allowing camera only transparent motion vectors.
- Fix reflection probe frame settings override
- Fixed certain shadow bias artifacts present in volumetric lighting (case 1231885).

### Changed
- Improve MIP selection for decals on Transparents
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,13 +279,13 @@ void EvaluateAtmosphericScattering(PositionInputs posInput, float3 V, out float3
{
float4 value = SampleVBuffer(TEXTURE3D_ARGS(_VBufferLighting, s_linear_clamp_sampler),
posInput.positionNDC,
fogFragDist,
fogFragDist,
_VBufferViewportSize,
_VBufferLightingViewportScale.xyz,
_VBufferLightingViewportLimit.xyz,
_VBufferDistanceEncodingParams,
_VBufferDistanceDecodingParams,
true, false);
true, true, false);

// TODO: add some slowly animated noise (dither?) to the reconstructed value.
// TODO: re-enable tone mapping after implementing pre-exposure.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@

#define SHADOW_OPTIMIZE_REGISTER_USAGE 1

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

#ifndef SHADOW_USE_DEPTH_BIAS // Enable clip space z biasing
#define SHADOW_USE_DEPTH_BIAS 1 // Externally define as 0 to disable
#endif

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

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

// Note: Here we assume that all the shadow map cube faces have been added contiguously in the buffer to retreive the shadow information
HDShadowData sd = shadowContext.shadowDatas[shadowDataIndex];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
// if (clampToBorder), samples outside of the buffer return 0 (border color).
// Otherwise, the sampler simply clamps the texture coordinate to the edge of the texture.
// Warning: clamping to border may not work as expected with the quadratic filter due to its extent.
//
// if (biasLookup), we apply a constant bias to the look-up to avoid light leaks through geometry.
float4 SampleVBuffer(TEXTURE3D_PARAM(VBuffer, clampSampler),
float2 positionNDC,
float linearDistance,
Expand All @@ -20,13 +22,22 @@ float4 SampleVBuffer(TEXTURE3D_PARAM(VBuffer, clampSampler),
float3 VBufferViewportLimit,
float4 VBufferDistanceEncodingParams,
float4 VBufferDistanceDecodingParams,
bool biasLookup,
bool quadraticFilterXY,
bool clampToBorder)
{
// These are the viewport coordinates.
float2 uv = positionNDC;
float w = EncodeLogarithmicDepthGeneralized(linearDistance, VBufferDistanceEncodingParams);

if (biasLookup)
{
// The value is higher than 0.5 (we use half of the length the diagonal of a unit cube).
// to account for varying angles of incidence.
// TODO: XR?
w -= (sqrt(3)/2) * _VBufferRcpSliceCount;
}

bool coordIsInsideFrustum;

if (clampToBorder)
Expand Down Expand Up @@ -79,6 +90,7 @@ float4 SampleVBuffer(TEXTURE3D_PARAM(VBuffer, clampSampler),

// The sampler clamps to the edge (so UVWs < 0 are OK).
// TODO: perform per-sample (4, in this case) bilateral filtering, rather than per-pixel. This should reduce leaking.
// Currently we don't do it, since it is expensive and doesn't appear to be helpful/necessary in practice.
result = (weights[0].x * weights[0].y) * SAMPLE_TEXTURE3D_LOD(VBuffer, clampSampler, min(float3(texUv0, texW), VBufferViewportLimit), 0)
+ (weights[1].x * weights[0].y) * SAMPLE_TEXTURE3D_LOD(VBuffer, clampSampler, min(float3(texUv1, texW), VBufferViewportLimit), 0)
+ (weights[0].x * weights[1].y) * SAMPLE_TEXTURE3D_LOD(VBuffer, clampSampler, min(float3(texUv2, texW), VBufferViewportLimit), 0)
Expand All @@ -105,6 +117,7 @@ float4 SampleVBuffer(TEXTURE3D_PARAM(VBuffer, clampSampler),
float3 VBufferViewportLimit,
float4 VBufferDistanceEncodingParams,
float4 VBufferDistanceDecodingParams,
bool biasLookup,
bool quadraticFilterXY,
bool clampToBorder)
{
Expand All @@ -119,6 +132,7 @@ float4 SampleVBuffer(TEXTURE3D_PARAM(VBuffer, clampSampler),
VBufferViewportLimit,
VBufferDistanceEncodingParams,
VBufferDistanceDecodingParams,
biasLookup,
quadraticFilterXY,
clampToBorder);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@
#define VBUFFER_VOXEL_SIZE 8
#endif

#define GROUP_SIZE_1D 8
#define SUPPORT_LOCAL_LIGHTS 1 // Local lights contribute to fog lighting
#define SHADOW_USE_DEPTH_BIAS 0
#define PREFER_HALF 0
#define PREFER_HALF 0
#define GROUP_SIZE_1D 8
#define SUPPORT_LOCAL_LIGHTS 1 // Local lights contribute to fog lighting
#define SHADOW_USE_DEPTH_BIAS 0 // Too expensive, not particularly effective
#define SHADOW_LOW // Too expensive
#define SHADOW_AUTO_FLIP_NORMAL 0 // No normal information, so no need to flip
#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.
#define USE_DEPTH_BUFFER 1 // Accounts for opaque geometry along the camera ray

//--------------------------------------------------------------------------------------------------
// Included headers
Expand All @@ -56,8 +60,6 @@

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

// Use low quality shadow when doing volumetric
#define SHADOW_LOW
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/LightLoop/LightLoopDef.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/LightEvaluation.hlsl"
Expand All @@ -83,6 +85,7 @@ struct JitteredRay
float3 jitterDirWS;
float3 xDirDerivWS;
float3 yDirDerivWS;
float geomDist;
};

struct VoxelLighting
Expand All @@ -91,6 +94,11 @@ struct VoxelLighting
float3 radianceNoPhase;
};

bool IsInRange(float x, float2 range)
{
return clamp(x, range.x, range.y) == x;
}

float ComputeHistoryWeight()
{
// Compute the exponential moving average over 'n' frames:
Expand Down Expand Up @@ -149,9 +157,13 @@ VoxelLighting EvaluateVoxelLightingDirectional(LightLoopContext context, uint fe
// Is it worth sampling the shadow map?
if ((light.volumetricLightDimmer > 0) && (light.volumetricShadowDimmer > 0))
{
// Pass the light direction as the normal to avoid issues with shadow biasing code.
// TODO: does it make sense?
float3 shadowN = L;
#if SHADOW_VIEW_BIAS
// Our shadows only support normal bias. Volumetrics has no access to the surface normal.
// We fake view bias by invoking the normal bias code with the view direction.
float3 shadowN = -ray.jitterDirWS;
#else
float3 shadowN = 0; // No bias
#endif // SHADOW_VIEW_BIAS

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

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

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

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

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

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

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

float e1 = slice * de + de; // (slice + 1) / sliceCount
float t1 = DecodeLogarithmicDepthGeneralized(e1, _VBufferDistanceDecodingParams);
float dt = t1 - t0;
float tNext = t1;

#if USE_DEPTH_BUFFER
bool containsOpaqueGeometry = IsInRange(ray.geomDist, float2(t0, t1));

if (containsOpaqueGeometry)
{
// Only integrate up to the opaque surface (make the voxel shorter, but not completely flat).
// Note that we can NOT completely stop integrating when the ray reaches geometry, since
// otherwise we get flickering at geometric discontinuities if reprojection is enabled.
// In this case, a temporally stable light leak is better than flickering.
t1 = max(t0 * 1.0001, ray.geomDist);
}
#endif

float dt = t1 - t0; // Is geometry-aware

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

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

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

#else // ENABLE_REPROJECTION
#else // NO REPROJECTION

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

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

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

t0 = t1;
t0 = tNext;
}
}

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

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

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

float3 F = GetViewForwardDir();
float3 U = GetViewUpDir();
float3 R = cross(F, U);

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

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

#ifdef ENABLE_REPROJECTION
ray.jitterDirWS = normalize(ray.centerDirWS + _VBufferSampleOffset.x * ray.xDirDerivWS
+ _VBufferSampleOffset.y * ray.yDirDerivWS);
float2 sampleOffset = _VBufferSampleOffset.xy;
#else
ray.jitterDirWS = ray.centerDirWS;
float2 sampleOffset = 0;
#endif

ray.jitterDirWS = normalize(ray.centerDirWS + sampleOffset.x * ray.xDirDerivWS
+ sampleOffset.y * ray.yDirDerivWS);

// We would like to determine the screen pixel (at the full resolution) which
// the jittered ray corresponds to. The exact solution can be obtained by intersecting
// the ray with the screen plane, e.i. (ViewSpace(jitterDirWS).z = 1). That's a little expensive.
// So, as an approximation, we ignore the curvature of the frustum.
uint2 pixelCoord = (uint2)((voxelCoord + 0.5 + sampleOffset) * _VBufferVoxelSize);

#ifdef VL_PRESET_OPTIMAL
// The entire thread group is within the same light tile.
uint2 tileCoord = groupOffset * VBUFFER_VOXEL_SIZE / TILE_SIZE_BIG_TILE;
#else
// No compile-time optimizations, no scalarization.
uint2 tileCoord = pixelCoord / TILE_SIZE_BIG_TILE;
#endif
uint tileIndex = tileCoord.x + _NumTileBigTileX * tileCoord.y;

// Do not jitter 'voxelCoord' else. It's expected to correspond to the center of the voxel.
PositionInputs posInput = GetPositionInput(voxelCoord, _VBufferViewportSize.zw, tileCoord);

ray.geomDist = FLT_INF;

#if USE_DEPTH_BUFFER
float deviceDepth = LoadCameraDepth(pixelCoord);

if (deviceDepth > 0) // Skip the skybox
{
// Convert it to distance along the ray. Doesn't work with tilt shift, etc.
float linearDepth = LinearEyeDepth(deviceDepth, _ZBufferParams);
ray.geomDist = linearDepth * rcp(dot(ray.jitterDirWS, F));
}
#endif

// TODO
LightLoopContext context;
context.shadowContext = InitShadowContext();
Expand Down