Skip to content
Closed
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
40 changes: 34 additions & 6 deletions package/Shaders/Utility.hlsl
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,27 @@ struct PS_OUTPUT
float4 Color : SV_Target0;
};

// FrameCount-independent shadow filtering functions
// Eliminates temporal jitter by using position-based stable hashing
uint StableShadowHash(uint x, uint y, uint z, uint w)
{
// Jenkins-like mix function for stable shadow rotation
uint h = x * 0x27d4eb2dU;
h ^= y + 0x7feb352dU + (h << 6) + (h >> 2);
h ^= z + 0x9e3779b9U + (h << 6) + (h >> 2);
h ^= w + 0x85ebca6bU + (h << 6) + (h >> 2);
return h;
}

float StableShadowAngle(float3 worldPos, uint lightIndex, uint cascadeIndex)
{
// Quantize world position to ~6cm precision (conservative to eliminate jitter)
int3 quantizedPos = int3(floor(worldPos * 16.0));
uint hash = StableShadowHash(asuint(quantizedPos.x), asuint(quantizedPos.y), lightIndex, cascadeIndex);
// Map hash to [0, TAU) for rotation angle
return Math::TAU * ((hash & 0x00FFFFFFu) / 16777216.0f);
}
Comment on lines +306 to +313
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

The z-coordinate of quantizedPos is not used in the hash.

quantizedPos.z is computed but never passed to StableShadowHash. The hash only uses x, y, lightIndex, and cascadeIndex. This means objects at different vertical positions but same x/y will share the same rotation angle, which may cause visible banding on vertical surfaces or in scenes with significant vertical depth variation.

If this is intentional (e.g., to maintain consistency along vertical axes), consider adding a brief comment. Otherwise:

🔎 Suggested fix to include z in the hash
 float StableShadowAngle(float3 worldPos, uint lightIndex, uint cascadeIndex)
 {
 	// Quantize world position to ~6cm precision (conservative to eliminate jitter)
 	int3 quantizedPos = int3(floor(worldPos * 16.0));
-	uint hash = StableShadowHash(asuint(quantizedPos.x), asuint(quantizedPos.y), lightIndex, cascadeIndex);
+	uint hash = StableShadowHash(
+		asuint(quantizedPos.x) ^ asuint(quantizedPos.z),
+		asuint(quantizedPos.y),
+		lightIndex,
+		cascadeIndex);
 	// Map hash to [0, TAU) for rotation angle
 	return Math::TAU * ((hash & 0x00FFFFFFu) / 16777216.0f);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
float StableShadowAngle(float3 worldPos, uint lightIndex, uint cascadeIndex)
{
// Quantize world position to ~6cm precision (conservative to eliminate jitter)
int3 quantizedPos = int3(floor(worldPos * 16.0));
uint hash = StableShadowHash(asuint(quantizedPos.x), asuint(quantizedPos.y), lightIndex, cascadeIndex);
// Map hash to [0, TAU) for rotation angle
return Math::TAU * ((hash & 0x00FFFFFFu) / 16777216.0f);
}
float StableShadowAngle(float3 worldPos, uint lightIndex, uint cascadeIndex)
{
// Quantize world position to ~6cm precision (conservative to eliminate jitter)
int3 quantizedPos = int3(floor(worldPos * 16.0));
uint hash = StableShadowHash(
asuint(quantizedPos.x) ^ asuint(quantizedPos.z),
asuint(quantizedPos.y),
lightIndex,
cascadeIndex);
// Map hash to [0, TAU) for rotation angle
return Math::TAU * ((hash & 0x00FFFFFFu) / 16777216.0f);
}
🤖 Prompt for AI Agents
In package/Shaders/Utility.hlsl around lines 306 to 313, quantizedPos.z is
computed but not used in the StableShadowHash call, causing identical rotation
for different vertical positions; either include the z component in the hash
call or document that omitting z is intentional. To fix, change the
StableShadowHash invocation to pass asuint(quantizedPos.z) as an additional
parameter (and update the StableShadowHash signature/uses accordingly), or add a
clear comment explaining the deliberate omission and rationale if vertical
invariance is required.


#ifdef PSHADER

SamplerState SampBaseSampler : register(s0);
Expand Down Expand Up @@ -375,7 +396,8 @@ float GetPoissonDiskFilteredShadowVisibility(uint3 seed, Texture2DArray<float4>
float alphaTestOffset = -AlphaTestRef.y;
float sampleRadius = ShadowSampleParam.z * 2048.0;
float seedNormalized = seed.x * (1.0 / 4294967295.0); // Use only x component for efficiency
uint frameOffset = SharedData::FrameCount * sampleCount;
uint stableOffset = (uint)(positionMS.x * 1000.0) + (uint)(positionMS.y * 100.0) + (uint)(positionMS.z * 10.0);
uint frameOffset = stableOffset * sampleCount; // Use stable to minimize temporal jitter

// Pre-compute constants
const float3 positionOffsetUp = float3(0, 0, 1);
Expand Down Expand Up @@ -616,10 +638,19 @@ PS_OUTPUT main(PS_INPUT input)
float fadeFactor = input.Alpha.x;
# endif

float noise = Random::InterleavedGradientNoise(input.PositionCS.xy, SharedData::FrameCount);
uint cascadeIndex = 0;
# if defined(RENDER_SHADOWMASK)
if (2.5 < EndSplitDistances.w && EndSplitDistances.y < shadowMapDepth) {
cascadeIndex = 2;
} else if (EndSplitDistances.x < shadowMapDepth) {
cascadeIndex = 1;
}
# endif

float noise = Random::InterleavedGradientNoise(input.PositionCS.xy, 0);
precise float stableAngle = StableShadowAngle(positionMS.xyz, 0, cascadeIndex);
float2 rotation;
sincos(Math::TAU * noise, rotation.y, rotation.x);
sincos(stableAngle, rotation.y, rotation.x); // Use stable to minimize temporal jitter
float2x2 rotationMatrix = float2x2(rotation.x, rotation.y, -rotation.y, rotation.x);

noise = noise * 2.0 - 1.0;
Expand All @@ -630,15 +661,12 @@ PS_OUTPUT main(PS_INPUT input)
if (EndSplitDistances.z >= shadowMapDepth) {
float4x3 lightProjectionMatrix = ShadowMapProj[eyeIndex][0];
float shadowMapThreshold = AlphaTestRef.y;
float cascadeIndex = 0;
if (2.5 < EndSplitDistances.w && EndSplitDistances.y < shadowMapDepth) {
lightProjectionMatrix = ShadowMapProj[eyeIndex][2];
shadowMapThreshold = AlphaTestRef.z;
cascadeIndex = 2;
} else if (EndSplitDistances.x < shadowMapDepth) {
lightProjectionMatrix = ShadowMapProj[eyeIndex][1];
shadowMapThreshold = AlphaTestRef.z;
cascadeIndex = 1;
}

float shadowVisibility = 0;
Expand Down