Files
UnrealEngine/Engine/Shaders/Private/VirtualShadowMaps/VirtualShadowMapProjectionDirectional.ush
2025-05-18 13:04:45 +08:00

350 lines
14 KiB
HLSL

// Copyright Epic Games, Inc. All Rights Reserved.
/*=============================================================================
VirtualShadowMapProjectionDirectional.ush:
=============================================================================*/
#pragma once
#include "../DeferredShadingCommon.ush"
#include "../SceneTexturesCommon.ush"
#include "../LightShaderParameters.ush"
#include "../Visualization.ush"
#include "VirtualShadowMapPageAccessCommon.ush"
#include "VirtualShadowMapProjectionCommon.ush"
#include "VirtualShadowMapSMRTCommon.ush"
float2 ComputeDepthSlopeDirectionalUV(
FVirtualShadowMapProjectionShaderData ProjectionData,
float3 EstimatedGeoWorldNormal,
bool bClamp = true)
{
float4 NormalPlaneUV = mul(float4(EstimatedGeoWorldNormal, 0.0f), ProjectionData.TranslatedWorldToShadowUVNormalMatrix);
float2 DepthSlopeUV = -NormalPlaneUV.xy / NormalPlaneUV.z;
// Clamp to avoid excessive degenerate slope biases causing flickering lit pixels
float2 Clamp = 0.05f;
DepthSlopeUV = bClamp ? clamp(DepthSlopeUV, -Clamp, Clamp) : DepthSlopeUV;
return DepthSlopeUV;
}
float ComputeOptimalSlopeBiasDirectional(
float2 DepthSlopeUV,
float2 OffsetUV)
{
return 2.0f * max(0.0f, dot(DepthSlopeUV, OffsetUV));
}
FVirtualShadowMapHandle GetMappedClipmap(FVirtualShadowMapHandle VirtualShadowMapHandle, float3 RayOriginTranslatedWorld, float SceneDepth)
{
// This is the (unbiased) most detailed clipmap level that we are guaranteed to have a page table entry for
const FVirtualShadowMapProjectionShaderData BaseProjectionData = GetVirtualShadowMapProjectionData(VirtualShadowMapHandle);
float ClipmapLevelFloat = CalcBiasedAbsoluteClipmapLevelForSampling(BaseProjectionData, RayOriginTranslatedWorld, SceneDepth);
int ClipmapIndex = max(0, int(floor(ClipmapLevelFloat)) - BaseProjectionData.ClipmapLevel);
if (ClipmapIndex >= BaseProjectionData.ClipmapLevelCountRemaining)
{
return FVirtualShadowMapHandle::MakeInvalid();
}
// Clipmap origin should be near shaded samples, so shadow translated world should be regular range
FVirtualShadowMapHandle ClipmapHandle = VirtualShadowMapHandle.MakeOffset(ClipmapIndex);
const FVirtualShadowMapProjectionShaderData ProjectionData = GetVirtualShadowMapProjectionData(ClipmapHandle);
float3 ViewToShadowTranslation = DFFastLocalSubtractDemote(ProjectionData.PreViewTranslation, PrimaryView.PreViewTranslation);
float3 RayOriginShadowTranslatedWorld = RayOriginTranslatedWorld + ViewToShadowTranslation;
float3 RayStartUVZ = mul(float4(RayOriginShadowTranslatedWorld, 1.0f), ProjectionData.TranslatedWorldToShadowUVMatrix).xyz;
// NOTE: We don't need the actual sample here, we're just looking for the best mapped page at the ray origin
// Trusting a lot in DCE here... may be better to refactor at some point
FVirtualShadowMapSample ShadowSample = SampleVirtualShadowMapClipmap(ClipmapHandle, RayStartUVZ.xy);
if (ShadowSample.bValid && ShadowSample.VirtualShadowMapHandle.Id > ClipmapHandle.Id)
{
// NOTE: We could bias this (as long as we clamp it to 0 still) to allow sampling higher resolution
// data in the case where we just happened to hit a pixel with lower resolution, but note that performance
// suffers when we have to use fallback levels in the SMRT loop, so this is fine for now.
// We may have to revisit this if we start getting finer grained with caching and mapped page resolutions,
// but likely if this matters here we would already be seeing visible resolution seams between pages regardless.
ClipmapIndex += (ShadowSample.VirtualShadowMapHandle.Id - ClipmapHandle.Id);
}
return VirtualShadowMapHandle.MakeOffset(ClipmapIndex);
}
struct FSMRTClipmapRayState
{
FVirtualShadowMapHandle ClipmapHandle; // Pre-offset virtual shadow map ID of specific clipmap level
float3 RayStartUVZ;
float3 RayStepUVZ;
float ExtrapolateSlope;
uint2 PhysicalTexelAddress;
uint2 VirtualTexelAddress;
FVirtualShadowMapHandle SampledClipmapHandle;
};
FSMRTClipmapRayState SMRTClipmapRayInitialize(
const FVirtualShadowMapProjectionShaderData ProjectionData,
float3 RayOriginShadowTranslatedWorld,
float3 RayDir,
float RayLength,
float RayStartOffset,
float2 DepthSlopeUV,
float2 TexelOffset,
float ExtrapolateSlope)
{
float3 RayStart = RayOriginShadowTranslatedWorld + RayDir * RayStartOffset;
float3 RayVector = RayDir * RayLength;
float3 RayStartUVZ = mul(float4(RayStart, 1.0f), ProjectionData.TranslatedWorldToShadowUVMatrix).xyz;
float3 RayStepUVZ = mul(float4(RayVector, 0.0f), ProjectionData.TranslatedWorldToShadowUVMatrix).xyz;
// Texel dither to hide aliasing
// Note that this is directly scaled in texel space of the clipmap level, so it can create visual
// discontinuities at clipmap boundaries. We could scale this by distance instead of texel size but
// in the case where texels are large enough to be obvious there are already visual discontinuities.
{
float OptimalBias = ComputeOptimalSlopeBiasDirectional(DepthSlopeUV, TexelOffset);
// Subtract off any portion of the bias that was already covered by the ray start offset (usually screen ray)
OptimalBias = max(0.0f, OptimalBias - abs(RayStartOffset * ProjectionData.ShadowViewToClipMatrix._33));
RayStartUVZ.xy += TexelOffset;
RayStartUVZ.z += OptimalBias;
}
FSMRTClipmapRayState Result = (FSMRTClipmapRayState)0;
Result.ClipmapHandle = ProjectionData.VirtualShadowMapHandle;
Result.RayStartUVZ = RayStartUVZ;
Result.RayStepUVZ = RayStepUVZ;
// Max depth slope for slope-based extrapolation when using SMRT. Scale so that it doesn't change based on ZRangeScale.
Result.ExtrapolateSlope = abs(ExtrapolateSlope * ProjectionData.ShadowViewToClipMatrix._33);
Result.VirtualTexelAddress = uint2(0xFFFFFFFF, 0xFFFFFFFF);
Result.PhysicalTexelAddress = uint2(0xFFFFFFFF, 0xFFFFFFFF);
return Result;
}
FSMRTSample SMRTFindSample(inout FSMRTClipmapRayState RayState, float SampleTime)
{
const float3 SampleUVZ = RayState.RayStartUVZ + RayState.RayStepUVZ * SampleTime;
FVirtualShadowMapSample ShadowSample = SampleVirtualShadowMapClipmap(RayState.ClipmapHandle, SampleUVZ.xy);
FSMRTSample Sample = InitSMRTSample();
Sample.bValid = ShadowSample.bValid;
Sample.ReferenceDepth = SampleUVZ.z;
Sample.ExtrapolateSlope = RayState.ExtrapolateSlope;
if (ShadowSample.bValid)
{
Sample.SampleDepth = ShadowSample.Depth;
// Debug
RayState.VirtualTexelAddress = ShadowSample.VirtualTexelAddress;
RayState.PhysicalTexelAddress = ShadowSample.PhysicalTexelAddress;
RayState.SampledClipmapHandle = ShadowSample.VirtualShadowMapHandle;
}
return Sample;
}
// Instantiate SMRTRayCast for FSMRTClipmapRayState
#define SMRT_TEMPLATE_RAY_STRUCT FSMRTClipmapRayState
#include "VirtualShadowMapSMRTTemplate.ush"
#undef SMRT_TEMPLATE_RAY_STRUCT
float3 GetRandomDirectionalLightRayDir(FLightShaderParameters Light, float2 E)
{
float3 RayDir = Light.Direction;
{
float2 DiskUV = UniformSampleDiskConcentric(E) * Light.SourceRadius;
float3 N = RayDir;
float3 dPdu = cross(N, (abs(N.x) > 1e-6f) ? float3(1, 0, 0) : float3(0, 1, 0));
float3 dPdv = cross(dPdu, N);
RayDir += dPdu * DiskUV.x + dPdv * DiskUV.y;
}
return normalize(RayDir);
}
// Normal and LightDirection should be normalized
bool IsBackfaceToDirectionalLight(float3 Normal, float3 LightDirection, float LightSourceRadius)
{
// Allow a minimum of ~5 degrees of wiggle room to account for normal issues
float MinSinAlpha = 0.1;
float SinAlpha = max(abs(LightSourceRadius), MinSinAlpha);
return dot(Normal, LightDirection) < -SinAlpha;
}
FVirtualShadowMapSampleResult TraceDirectional(
int VirtualShadowMapId,
FLightShaderParameters Light,
uint2 PixelPos,
const float SceneDepth,
float3 TranslatedWorldPosition,
float RayStartOffset,
const float Noise,
float3 WorldNormal,
const FSMRTTraceSettings Settings = GetSMRTTraceSettingsDirectional())
{
FVirtualShadowMapHandle VirtualShadowMapHandle = FVirtualShadowMapHandle::MakeFromIdDirectional(VirtualShadowMapId);
float3 ViewPosition = mul(float4(TranslatedWorldPosition, 1.0f), View.TranslatedWorldToView).xyz;
float DistanceFromViewOrigin = length(ViewPosition);
FVirtualShadowMapSampleResult Result = InitVirtualShadowMapSampleResult();
Result.bValid = true; // TODO: false if all samples of all rays miss pages?
Result.ShadowFactor = 1.0f;
// Find the best resolution mapped clipmap at the ray origin location
const FVirtualShadowMapHandle ClipmapHandle = GetMappedClipmap(VirtualShadowMapHandle, TranslatedWorldPosition, SceneDepth);
if (!ClipmapHandle.IsValid())
{
// TODO: False for valid probably? But make sure that doesn't do anything weird as we have no fallback here
Result.ShadowFactor = 1.0f; // Fully lit outside clipmap range
return Result;
}
const FVirtualShadowMapProjectionShaderData ProjectionData = GetVirtualShadowMapProjectionData(ClipmapHandle);
// This function is designed to vary in depth in roughly the same way as shadow texel density does,
// thus the dependence on Clipmap ResolutionLodBias. The goal here is to define a texel dither that
// is "smooth" and doesn't have obvious artifacts at page edges of different clipmap levels.
//
// This could become a problem later if we break too far from that global resolution bias. That said,
// these cases would have unavoidable discontinuities at page edges, so that would have to be handled
// in some different way.
//
// 0.5 is arbitrary but makes it consistent with the previous dither scale cvar
float PerLightTexelDitherScale = Settings.TexelDitherScale * ProjectionData.TexelDitherScale;
float DitherScale = 0.0f;
if (PerLightTexelDitherScale > 0.0f)
{
DitherScale =
((0.5f / float(CalcLevelDimsTexels(0))) * PerLightTexelDitherScale * DistanceFromViewOrigin) /
(exp2(ProjectionData.ClipmapLevel - ProjectionData.ResolutionLodBias));
}
float2 DepthSlopeUV = ComputeDepthSlopeDirectionalUV(ProjectionData, WorldNormal);
// Compute max ray length based on view depth
// This affects both how far we are willing to trace across the shadow map (for clipmaps this is related to view depth)
// and the maximum size a penumbra can be.
// Too high values will cause shadows to detach from their contact points (unless more samples are used).
// Too low values will greatly restrict how large penumbras can be in screen space.
float RayLength = VirtualShadowMap.SMRTRayLengthScale * DistanceFromViewOrigin;
// Clipmap origin should be near shaded samples, so shadow translated world should be regular range
float3 ViewToShadowTranslation = DFFastLocalSubtractDemote(ProjectionData.PreViewTranslation, PrimaryView.PreViewTranslation);
float3 RayOriginShadowTranslatedWorld = TranslatedWorldPosition + ViewToShadowTranslation;
uint RayMissCount = 0;
uint i = 0;
float OccluderDistanceSum = 0.0f;
float MaxOccluderDistance = -1.0f;
const uint MaxRayCount = Settings.RayCount;
for ( ; i < MaxRayCount; i++)
{
// One sample for ray, one for texel dither
float4 RandSample = VirtualShadowMapGetRandomSample(PixelPos, View.StateFrameIndex, i, MaxRayCount);
float3 RayDir = GetRandomDirectionalLightRayDir(Light, RandSample.xy);
float2 TexelOffset = (RandSample.zw - 0.5f) * DitherScale;
FSMRTClipmapRayState RayState = SMRTClipmapRayInitialize(
ProjectionData,
RayOriginShadowTranslatedWorld,
RayDir,
RayLength,
RayStartOffset,
DepthSlopeUV,
TexelOffset,
Settings.ExtrapolateMaxSlope);
FSMRTResult SMRTResult = SMRTRayCast(RayState, Settings.SamplesPerRay, Noise);
// Debug output (DCE'd if not used)
Result.ClipmapOrMipLevel = GetVirtualShadowMapProjectionData(RayState.SampledClipmapHandle).ClipmapLevel;
Result.VirtualTexelAddress = RayState.VirtualTexelAddress;
Result.PhysicalTexelAddress = RayState.PhysicalTexelAddress;
//Result.GeneralDebug = GreenToRedTurbo((RayState.SampledClipmapId - RayState.ClipmapId) / 2.0f);
if (SMRTResult.bValidHit)
{
float OccluderDistance = ComputeOccluderDistanceOrtho(
GetVirtualShadowMapProjectionData(RayState.ClipmapHandle).ShadowViewToClipMatrix,
SMRTResult.HitDepth,
RayState.RayStartUVZ.z);
OccluderDistanceSum += OccluderDistance;
MaxOccluderDistance = max(MaxOccluderDistance, OccluderDistance);
}
else
{
++RayMissCount;
}
#if COMPUTESHADER
if (Settings.AdaptiveRayCount > 0)
{
// TODO: Adapt this heuristic based on SMRTAdaptiveRayCount as well?
if( i == 0 )
{
bool bHit = SMRTResult.bValidHit;
// All lanes missed
bool bAllLanesMiss = WaveActiveAllTrue( !bHit );
if( bAllLanesMiss )
{
break;
}
}
else if (i >= Settings.AdaptiveRayCount)
{
// After 2 iterations and all have hit, assume umbra
bool bAllLanesHit = WaveActiveAllTrue( RayMissCount == 0 );
if( bAllLanesHit )
{
break;
}
}
}
#endif
}
uint RayCount = min(i + 1U, MaxRayCount); // break vs regular for loop exit
/* TODO: Experiment with max vs avg distance in various cases
float OccluderDistance = (View.GeneralPurposeTweak == 0)
? MaxOccluderDistance
: (OccluderDistanceSum / float(max(1, RayCount - RayMissCount)));
*/
float OccluderDistance = (OccluderDistanceSum / float(max(1U, RayCount - RayMissCount)));
Result.ShadowFactor = float(RayMissCount) / float(RayCount);
Result.OccluderDistance = OccluderDistance;
Result.RayCount = RayCount;
return Result;
}
// Generate a ray based on directional light source geometry (e.g, source radius)
bool GenerateRayDirectional(
FLightShaderParameters Light,
uint2 PixelPos,
float3 TranslatedWorldPosition,
float RayLengthScale,
uint RayIndex,
uint RayCount,
inout float3 OutRayStart,
inout float3 OutRayEnd)
{
float3 ViewPosition = mul(float4(TranslatedWorldPosition, 1.0f), View.TranslatedWorldToView).xyz;
float DistanceFromViewOrigin = length(ViewPosition);
// Compute max ray length based on view depth
// This affects both how far we are willing to trace across the shadow map (for clipmaps this is related to view depth)
// and the maximum size a penumbra can be.
// Too high values will cause shadows to detach from their contact points (unless more samples are used).
// Too low values will greatly restrict how large penumbras can be in screen space.
float RayLength = RayLengthScale * DistanceFromViewOrigin;
float2 E = VirtualShadowMapGetRandomSample(PixelPos, View.StateFrameIndex, RayIndex, RayCount).xy;
float3 RayDir = GetRandomDirectionalLightRayDir(Light, E);
OutRayStart = TranslatedWorldPosition;
OutRayEnd = OutRayStart + RayDir * RayLength;
return true;
}