350 lines
14 KiB
HLSL
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;
|
|
}
|