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

524 lines
19 KiB
HLSL

// Copyright Epic Games, Inc. All Rights Reserved.
/*=============================================================================
VirtualShadowMapProjectionSpot.ush:
=============================================================================*/
#pragma once
#include "../DeferredShadingCommon.ush"
#include "../SceneTexturesCommon.ush"
#include "../LightShaderParameters.ush"
#include "../PathTracing/Utilities/PathTracingRandomSequence.ush"
#include "VirtualShadowMapPageAccessCommon.ush"
#include "VirtualShadowMapProjectionCommon.ush"
#include "VirtualShadowMapSMRTCommon.ush"
float2 ComputeDepthSlopeLocalUV(
FVirtualShadowMapHandle VirtualShadowMapHandle,
float3 ShadowTranslatedWorldPosition,
float3 WorldNormal)
{
FVirtualShadowMapProjectionShaderData ProjectionData = GetVirtualShadowMapProjectionData(VirtualShadowMapHandle);
float4 NormalPlaneTranslatedWorld = float4(WorldNormal, -dot(WorldNormal, ShadowTranslatedWorldPosition));
float4 NormalPlaneUV = mul(NormalPlaneTranslatedWorld, ProjectionData.TranslatedWorldToShadowUVNormalMatrix);
float2 DepthSlopeUV = -NormalPlaneUV.xy / NormalPlaneUV.z;
return DepthSlopeUV;
}
float ComputeOptimalSlopeBiasLocal(
FVirtualShadowMapHandle VirtualShadowMapHandle,
float3 ShadowTranslatedWorldPosition,
float2 DepthSlopeUV,
bool bClamp = true)
{
// NOTE: Better to reload the necessary data here based on VSM ID than keep it alive in registers!
FVirtualShadowMapProjectionShaderData ProjectionData = GetVirtualShadowMapProjectionData(VirtualShadowMapHandle);
float4 ShadowUVz = mul(float4(ShadowTranslatedWorldPosition, 1.0f), ProjectionData.TranslatedWorldToShadowUVMatrix);
ShadowUVz.xyz /= ShadowUVz.w;
FShadowPageTranslationResult Page = ShadowVirtualToPhysicalUV(ProjectionData.VirtualShadowMapHandle, ShadowUVz.xy, ProjectionData.MinMipLevel);
float MipLevelDim = float(CalcLevelDimsTexels(Page.LODOffset));
float2 TexelCenter = float2(Page.VirtualTexelAddress) + 0.5f;
float2 TexelCenterOffset = TexelCenter - Page.VirtualTexelAddressFloat;
float2 TexelCenterOffsetUV = TexelCenterOffset / MipLevelDim;
float OptimalSlopeBias = 2.0f * max(0.0f, dot(DepthSlopeUV, TexelCenterOffsetUV));
return OptimalSlopeBias;
}
struct FSMRTSingleRayState
{
FVirtualShadowMapHandle VirtualShadowMapHandle;
float3 RayStartUVz;
float3 RayStepUVz;
float ExtrapolateSlope;
uint MinMipLevel;
// Debug output
int MipLevel;
uint2 VirtualTexelAddress;
uint2 PhysicalTexelAddress;
};
FSMRTSingleRayState SMRTSingleRayInitialize(
FVirtualShadowMapHandle VirtualShadowMapHandle,
// Shadow PreViewTranslation already added in
float3 RayStartShadowTranslatedWorld,
float3 RayEndShadowTranslatedWorld,
float ExtrapolateSlope,
float DepthBias,
uint MipLevel,
bool bClampUVs = true)
{
FVirtualShadowMapProjectionShaderData ProjectionData = GetVirtualShadowMapProjectionData(VirtualShadowMapHandle);
float4 RayStartUVz = mul(float4(RayStartShadowTranslatedWorld, 1), ProjectionData.TranslatedWorldToShadowUVMatrix).xyzw;
float4 RayEndUVz = mul(float4(RayEndShadowTranslatedWorld , 1), ProjectionData.TranslatedWorldToShadowUVMatrix).xyzw;
// NOTE: We assume by construction that ray ends are not behind the light near plane as the warping
// due to SMRT ray tests gets severe long before the rays would otherwise clip at that plane.
// If clipping is ever necessary, it must be done to the ray endpoints in world space so that lights that
// need to walk multiple faces/clipmap levels in lock step can do it consistently.
RayStartUVz.xyz = RayStartUVz.xyz / RayStartUVz.w;
RayEndUVz.xyz = RayEndUVz.xyz / RayEndUVz.w;
float3 RayStepUVz = RayEndUVz.xyz - RayStartUVz.xyz;
// Offsets can move it slightly off the edge of spotlight projection. Clamp to edge.
// Note we do *not* want to do this if we are tracing dual face cubemap rays!
if (bClampUVs)
{
RayStartUVz.xy = saturate(RayStartUVz.xy);
}
RayStartUVz.z += DepthBias;
FSMRTSingleRayState Result;
Result.VirtualShadowMapHandle = VirtualShadowMapHandle;
Result.RayStartUVz = RayStartUVz.xyz;
Result.RayStepUVz = RayStepUVz.xyz;
Result.ExtrapolateSlope = ExtrapolateSlope * RayStepUVz.z;
Result.MinMipLevel = max(MipLevel, ProjectionData.MinMipLevel);
Result.MipLevel = 0;
Result.VirtualTexelAddress = uint2(0xFFFFFFFF, 0xFFFFFFFF);
Result.PhysicalTexelAddress = uint2(0xFFFFFFFF, 0xFFFFFFFF);
return Result;
}
FSMRTSample SMRTFindSample(inout FSMRTSingleRayState RayState, float SampleTime)
{
float3 UVz = RayState.RayStartUVz.xyz + RayState.RayStepUVz.xyz * SampleTime;
FSMRTSample Sample = InitSMRTSample();
Sample.bValid = false;
Sample.ReferenceDepth = UVz.z;
Sample.ExtrapolateSlope = RayState.ExtrapolateSlope;
if (all(UVz.xy == saturate(UVz.xy)))
{
FVirtualShadowMapSample SmSample = SampleVirtualShadowMap(RayState.VirtualShadowMapHandle, UVz.xy, RayState.MinMipLevel);
if (SmSample.bValid)
{
Sample.bValid = true;
Sample.SampleDepth = SmSample.Depth;
// Debug output
RayState.MipLevel = SmSample.MipLevel;
RayState.PhysicalTexelAddress = SmSample.PhysicalTexelAddress;
RayState.VirtualTexelAddress = SmSample.VirtualTexelAddress;
}
}
return Sample;
}
// Instantiate SMRTRayCast for FSMRTSingleRayState
#define SMRT_TEMPLATE_RAY_STRUCT FSMRTSingleRayState
#include "VirtualShadowMapSMRTTemplate.ush"
#undef SMRT_TEMPLATE_RAY_STRUCT
struct FSMRTTwoCubeFaceRayState
{
FSMRTSingleRayState Face0;
FSMRTSingleRayState Face1;
bool bSampleInFace1;
};
FSMRTTwoCubeFaceRayState SMRTTwoCubeFaceRayInitialize(
FVirtualShadowMapHandle VirtualShadowMapHandle0,
FVirtualShadowMapHandle VirtualShadowMapHandle1,
float3 RayStartShadowTranslatedWorld,
float3 RayEndShadowTranslatedWorld,
float ExtrapolateSlope,
float DepthBias,
uint MipLevel)
{
FSMRTTwoCubeFaceRayState Result;
Result.Face0 = SMRTSingleRayInitialize(VirtualShadowMapHandle0, RayStartShadowTranslatedWorld, RayEndShadowTranslatedWorld, ExtrapolateSlope, DepthBias, MipLevel, false);
Result.Face1 = SMRTSingleRayInitialize(VirtualShadowMapHandle1, RayStartShadowTranslatedWorld, RayEndShadowTranslatedWorld, ExtrapolateSlope, DepthBias, MipLevel, false);
Result.bSampleInFace1 = true;
return Result;
}
FSMRTSample SMRTFindSample(inout FSMRTTwoCubeFaceRayState RayState, float SampleTime)
{
// NOTE: Traces from END to START, so we start in face1
FSMRTSample Sample = InitSMRTSample();
if (RayState.bSampleInFace1)
{
Sample = SMRTFindSample(RayState.Face1, SampleTime);
if (!Sample.bValid)
{
Sample = SMRTFindSample(RayState.Face0, SampleTime);
Sample.bResetExtrapolation = true;
RayState.bSampleInFace1 = false;
}
}
else
{
Sample = SMRTFindSample(RayState.Face0, SampleTime);
}
return Sample;
}
// Instantiate SMRTRayCast for FSMRTTwoCubeFaceRayState
#define SMRT_TEMPLATE_RAY_STRUCT FSMRTTwoCubeFaceRayState
#include "VirtualShadowMapSMRTTemplate.ush"
#undef SMRT_TEMPLATE_RAY_STRUCT
FSMRTResult ShadowRayCastSpotLight(
FVirtualShadowMapHandle VirtualShadowMapHandle,
float3 RayStartShadowTranslatedWorld,
float3 RayEndShadowTranslatedWorld,
float DepthBias,
uint MipLevel,
int NumSteps,
float StepOffset,
float ExtrapolateSlope,
// To get debug data out
inout FSMRTSingleRayState OutRayState)
{
OutRayState = SMRTSingleRayInitialize(VirtualShadowMapHandle, RayStartShadowTranslatedWorld, RayEndShadowTranslatedWorld, ExtrapolateSlope, DepthBias, MipLevel);
return SMRTRayCast(OutRayState, NumSteps, StepOffset);
}
FSMRTResult ShadowRayCastPointLight(
FVirtualShadowMapHandle VirtualShadowMapHandle,
float3 RayStartShadowTranslatedWorld,
float3 RayEndShadowTranslatedWorld,
float DepthBias,
uint MipLevel,
int NumSteps,
float StepOffset,
float ExtrapolateSlope,
// To get debug data out
inout FSMRTSingleRayState OutRayState)
{
FVirtualShadowMapHandle RayStartFaceHandle = VirtualShadowMapHandle.MakeOffset(VirtualShadowMapGetCubeFace(RayStartShadowTranslatedWorld));
FVirtualShadowMapHandle RayEndFaceHandle = VirtualShadowMapHandle.MakeOffset(VirtualShadowMapGetCubeFace(RayEndShadowTranslatedWorld));
FSMRTResult Result;
if (WaveActiveAnyTrue(RayStartFaceHandle.Id != RayEndFaceHandle.Id))
{
FSMRTTwoCubeFaceRayState RayState = SMRTTwoCubeFaceRayInitialize(RayStartFaceHandle, RayEndFaceHandle,
RayStartShadowTranslatedWorld, RayEndShadowTranslatedWorld, ExtrapolateSlope, DepthBias, MipLevel);
Result = SMRTRayCast(RayState, NumSteps, StepOffset);
if (RayState.bSampleInFace1)
{
OutRayState = RayState.Face1;
}
else
{
OutRayState = RayState.Face0;
}
}
else
{
// Fast path: ray stays on a single cube map face (effectively a spot light)
OutRayState = SMRTSingleRayInitialize(RayEndFaceHandle, RayStartShadowTranslatedWorld, RayEndShadowTranslatedWorld, ExtrapolateSlope, DepthBias, MipLevel);
Result = SMRTRayCast(OutRayState, NumSteps, StepOffset);
}
return Result;
}
// Normal should be normalized
bool IsBackfaceToLocalLight(
float3 ToLight,
float3 Normal,
float LightSourceRadius)
{
float DistSqr = dot(ToLight, ToLight);
float Falloff = rcp(DistSqr + 1);
float InvDist = rsqrt(DistSqr);
float SinAlphaSqr = saturate(Pow2(LightSourceRadius) * Falloff);
float SinAlpha = sqrt(SinAlphaSqr);
bool bBackface = dot(Normal, ToLight * InvDist) < -SinAlpha;
return bBackface;
}
// Shadow resolution scale that roughly matches the LOD function in VSM page marking
float ComputeShadowResolutionScale(FVirtualShadowMapProjectionShaderData ProjectionData, float SceneDepth)
{
const float2 RadiusXY = 1.0f / (View.ViewSizeAndInvSize.xy * View.ViewToClip._11_22);
const float RadiusScreen = min(RadiusXY.x, RadiusXY.y);
const float DepthScale = SceneDepth * View.ViewToClip._34 + View.ViewToClip._44;
const float ScreenPixelRadiusWorld = DepthScale * RadiusScreen;
// Clamp the minimum here since our shadows sadly don't actually have infinite resolution near the camera...
float ShadowResolutionScale = max(0.1f, ScreenPixelRadiusWorld * exp2(ProjectionData.ResolutionLodBias));
return ShadowResolutionScale;
}
float VirtualShadowMapGetClampedRayDistanceScaleLocal(float CosTheta)
{
// The further afield our rays go the poorer our approximation is as the "bend" due to our testing
// against a shadow map instead of along the ray increases. Thus we avoid going all the way to the light
// where the projection becomes extreme.
// This clamping function is a bit arbitrary but is fairly smooth and avoids the worst artifacts in practice
float SinTheta = sqrt(1.0f - CosTheta * CosTheta);
return 0.75f * saturate(1.5f / (CosTheta + VirtualShadowMap.SMRTCotMaxRayAngleFromLight * SinTheta));
}
FVirtualShadowMapSampleResult TraceLocalLight(
int VirtualShadowMapId,
FLightShaderParameters Light,
uint2 PixelPos,
const float SceneDepth,
float3 TranslatedWorldPosition,
float RayStartOffset,
const float Noise,
float3 WorldNormal,
const FSMRTTraceSettings Settings = GetSMRTTraceSettingsLocal())
{
FVirtualShadowMapHandle VirtualShadowMapHandle = FVirtualShadowMapHandle::MakeFromId(VirtualShadowMapId);
const uint LightType = Light.SpotAngles.x > -2.0f ? LIGHT_TYPE_SPOT : LIGHT_TYPE_POINT;
const float3 ToLight = Light.TranslatedWorldPosition - TranslatedWorldPosition;
const float3 ConeAxis = normalize(ToLight);
const float DistToLight = length(ToLight);
const float ConeSin = Light.SourceRadius / DistToLight;
FVirtualShadowMapSampleResult Result = InitVirtualShadowMapSampleResult();
Result.bValid = true;
Result.ShadowFactor = 0.0f;
FVirtualShadowMapHandle OffsetVirtualShadowMapHandle = VirtualShadowMapHandle;
if (LightType == LIGHT_TYPE_POINT)
{
OffsetVirtualShadowMapHandle = OffsetVirtualShadowMapHandle.MakeOffset(VirtualShadowMapGetCubeFace(-ToLight));
}
// Move from PrimaryView translated world to Shadow translated world
// NOTE: Cube map faces all share the same PreViewTranslation and projection depth scale
FVirtualShadowMapProjectionShaderData ProjectionData = GetVirtualShadowMapProjectionData(OffsetVirtualShadowMapHandle);
const float3 PrimaryToShadowTranslation = DFFastLocalSubtractDemote(ProjectionData.PreViewTranslation, PrimaryView.PreViewTranslation);
const float3 ShadowTranslatedWorld = TranslatedWorldPosition + PrimaryToShadowTranslation;
const float2 DepthSlopeUV = ComputeDepthSlopeLocalUV(OffsetVirtualShadowMapHandle, ShadowTranslatedWorld, WorldNormal);
const float ShadowResolutionScale = ComputeShadowResolutionScale(ProjectionData, SceneDepth);
// Rough scaling from world units -> post projection depth.
// We use a radially symmetric mapping here instead of the true function to minimize issues crossing cubemap faces.
const float ShadowDepthFunctionScale = abs(ProjectionData.ShadowViewToClipMatrix._43 / length2(ToLight));
const float DitherScale = (Settings.TexelDitherScale * ProjectionData.TexelDitherScale) * ShadowResolutionScale;
const float MaxDitherSlope = VirtualShadowMap.SMRTMaxSlopeBiasLocal * DitherScale;
const float MaxDepthBias = VirtualShadowMap.SMRTMaxSlopeBiasLocal * ShadowResolutionScale * ShadowDepthFunctionScale;
// Moving the ray origin means we need to attempt to account for local receiver geometry to avoid
// incorrect self-shadowing. To this end we move the point on a plane aligned with the shading normal.
// It would be much better to use geometric (faceted) normals - as with optimal bias - but we do not
// have access to those. Shading normals that stray too far from the real geometry will cause artifacts.
const float3x3 ConeTangentToWorldMatrix = GetTangentBasis(ConeAxis);
const float3 NormalPlaneTangent = mul(ConeTangentToWorldMatrix, WorldNormal);
const float2 DepthSlopeTangent = -NormalPlaneTangent.xy / NormalPlaneTangent.z;
// If source angle = 0, we don't need multiple samples on the same ray
uint SamplesPerRay = Settings.SamplesPerRay;
if (ConeSin == 0.0f)
{
SamplesPerRay = 0;
}
// When using receiver mask, we also must compute the base mip level to use
// We cannot use greedy mip selection in this case since the higher res mips may not have the rendered
// geometry that covers the requested mask at this receiver. We must use a similar-or-lower mip than
// the one that we marked.
// TODO: Any significant mip changes in GetMipLevelLocal should probably affect the above
// bias/texel dither math as well, which currently assumes a roughly proportional pixel:texel mapping
uint MipLevel = VirtualShadowMapGetMipLevelLocalFromReceiver(ProjectionData, ShadowTranslatedWorld, SceneDepth);
uint i = 0;
uint RayMissCount = 0;
float StepOffset = Noise;
float OccluderDistanceSum = 0.0f;
float MaxOccluderDistance = -1.0f;
const uint MaxRayCount = Settings.RayCount;
for( ; i < MaxRayCount; i++ )
{
float4 RandSample = VirtualShadowMapGetRandomSample(PixelPos, View.StateFrameIndex, i, MaxRayCount);
float2 LightUV = UniformSampleDiskConcentricApprox( RandSample.xy ).xy;
LightUV *= ConeSin;
float SinTheta2 = dot(LightUV, LightUV);
float CosTheta = sqrt(1.0f - SinTheta2);
float3 Dir = mul(float3(LightUV, CosTheta), ConeTangentToWorldMatrix);
float3 RayStartWithDither = ShadowTranslatedWorld;
if (DitherScale > 0.0f)
{
// Offset following the plane of the receiver
float2 RandomOffset = (RandSample.zw - 0.5f) * DitherScale;
float OffsetZ = min(MaxDitherSlope, 2.0f * max(0.0f, dot(DepthSlopeTangent, RandomOffset)));
float3 Offset = mul(float3(RandomOffset, OffsetZ), ConeTangentToWorldMatrix);
RayStartWithDither += Offset;
}
float ClampedEnd = DistToLight * VirtualShadowMapGetClampedRayDistanceScaleLocal(CosTheta);
float Start = min(RayStartOffset, ClampedEnd - 1e-6f);
float3 RayStart = RayStartWithDither + Dir * Start;
float3 RayEnd = RayStartWithDither + Dir * ClampedEnd;
float DepthBias = 0.0f;
if (MaxDepthBias > 0.0f)
{
// Clamp depth bias to avoid excessive degenerate slope biases causing flickering lit pixels
DepthBias = min(MaxDepthBias, ComputeOptimalSlopeBiasLocal(OffsetVirtualShadowMapHandle, RayStart, DepthSlopeUV));
}
FSMRTResult SMRTResult;
FSMRTSingleRayState RayState; // For debug output
if ( LightType == LIGHT_TYPE_SPOT )
{
SMRTResult = ShadowRayCastSpotLight(
VirtualShadowMapHandle,
RayStart,
RayEnd,
DepthBias,
MipLevel,
SamplesPerRay,
StepOffset,
Settings.ExtrapolateMaxSlope,
RayState);
}
else
{
SMRTResult = ShadowRayCastPointLight(
VirtualShadowMapHandle,
RayStart,
RayEnd,
DepthBias,
MipLevel,
SamplesPerRay,
StepOffset,
Settings.ExtrapolateMaxSlope,
RayState);
}
if (SMRTResult.bValidHit)
{
// TODO: Do we want to mess with this at all due to our offset?
float ReceiverDistance = length(RayStart);
float OccluderDistance = ComputeOccluderDistancePerspective(
// Re-fetch this here to avoid reg pressure
GetVirtualShadowMapProjectionData(RayState.VirtualShadowMapHandle).ShadowViewToClipMatrix,
SMRTResult.HitDepth,
saturate(RayState.RayStartUVz.z),
ReceiverDistance);
OccluderDistanceSum += OccluderDistance;
MaxOccluderDistance = max(MaxOccluderDistance, OccluderDistance);
}
else
{
++RayMissCount;
}
// Debug output (DCE'd if not used)
Result.ClipmapOrMipLevel = RayState.MipLevel;
Result.VirtualTexelAddress = RayState.VirtualTexelAddress;
Result.PhysicalTexelAddress = RayState.PhysicalTexelAddress;
if (MaxRayCount > 1 && 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 N iterations and all have hit, assume umbra
bool bAllLanesHit = WaveActiveAllTrue( RayMissCount == 0 );
if( bAllLanesHit )
{
break;
}
}
}
uint Seed = asuint( StepOffset );
StepOffset = ( EvolveSobolSeed( Seed ) >> 8 ) * 5.96046447754e-08; // * 2^-24;
}
uint RayCount = min(i + 1, MaxRayCount); // break vs regular for loop exit
float OccluderDistance = (OccluderDistanceSum / float(max(1, RayCount - RayMissCount)));
//OccluderDistance = MaxOccluderDistance;
Result.ShadowFactor = float(RayMissCount) / float(RayCount);
Result.RayCount = RayCount;
Result.OccluderDistance = OccluderDistance;
return Result;
}
// Generate ray based on light source geometry
bool GenerateRayLocalLight(
FLightShaderParameters Light,
uint2 PixelPos,
float3 TranslatedWorldPosition,
float3 WorldNormal,
uint RayIndex,
uint RayCount,
inout float3 OutRayStart,
inout float3 OutRayEnd)
{
const float3 ToLight = Light.TranslatedWorldPosition - TranslatedWorldPosition;
const float3 ConeAxis = normalize(ToLight);
const float DistToLight = length(ToLight);
const float ConeSin = Light.SourceRadius / DistToLight;
bool bBackface = IsBackfaceToLocalLight(ToLight, WorldNormal, Light.SourceRadius);
if (!bBackface)
{
float2 E = VirtualShadowMapGetRandomSample(PixelPos, View.StateFrameIndex, RayIndex, RayCount).xy;
float2 LightUV = UniformSampleDiskConcentricApprox(E);
LightUV *= ConeSin;
float SinTheta2 = dot(LightUV, LightUV);
float CosTheta = sqrt(1 - SinTheta2);
float3 Dir = TangentToWorld(float3(LightUV, CosTheta), ConeAxis);
float ClampedLength = DistToLight * VirtualShadowMapGetClampedRayDistanceScaleLocal(CosTheta);
OutRayStart = TranslatedWorldPosition;
OutRayEnd = TranslatedWorldPosition + Dir * ClampedLength;
}
return !bBackface;
}