// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. #ifndef __SHADOWS_SAMPLING__ #define __SHADOWS_SAMPLING__ #include "./Flax/ShadowsCommon.hlsl" #include "./Flax/GBufferCommon.hlsl" #include "./Flax/LightingCommon.hlsl" #ifdef SHADOWS_CSM_BLENDING #include "./Flax/Random.hlsl" #endif #if FEATURE_LEVEL >= FEATURE_LEVEL_SM5 #define SAMPLE_SHADOW_MAP(shadowMap, shadowUV, sceneDepth) shadowMap.SampleCmpLevelZero(ShadowSamplerLinear, shadowUV, sceneDepth) #define SAMPLE_SHADOW_MAP_OFFSET(shadowMap, shadowUV, texelOffset, sceneDepth) shadowMap.SampleCmpLevelZero(ShadowSamplerLinear, shadowUV, sceneDepth, texelOffset) #else #define SAMPLE_SHADOW_MAP(shadowMap, shadowUV, sceneDepth) (sceneDepth < shadowMap.SampleLevel(SamplerLinearClamp, shadowUV, 0).r) #define SAMPLE_SHADOW_MAP_OFFSET(shadowMap, shadowUV, texelOffset, sceneDepth) (sceneDepth < shadowMap.SampleLevel(SamplerLinearClamp, shadowUV, 0, texelOffset).r) #endif float4 GetShadowMask(ShadowSample shadow) { return float4(shadow.SurfaceShadow, shadow.TransmissionShadow, 1, 1); } // Gets the cube texture face index to use for shadow map sampling for the given view-to-light direction vector // Where: direction = normalize(worldPosition - lightPosition) uint GetCubeFaceIndex(float3 direction) { uint cubeFaceIndex; float3 absDirection = abs(direction); float maxDirection = max(absDirection.x, max(absDirection.y, absDirection.z)); if (maxDirection == absDirection.x) cubeFaceIndex = absDirection.x == direction.x ? 0 : 1; else if (maxDirection == absDirection.y) cubeFaceIndex = absDirection.y == direction.y ? 2 : 3; else cubeFaceIndex = absDirection.z == direction.z ? 4 : 5; return cubeFaceIndex; } float2 GetLightShadowAtlasUV(ShadowData shadow, ShadowTileData shadowTile, float3 samplePosition, out float4 shadowPosition) { // Project into shadow space (WorldToShadow is pre-multiplied to convert Clip Space to UV Space) shadowPosition = mul(float4(samplePosition, 1.0f), shadowTile.WorldToShadow); shadowPosition.z -= shadow.Bias; shadowPosition.xyz /= shadowPosition.w; // UV Space -> Atlas Tile UV Space float2 shadowMapUV = saturate(shadowPosition.xy); shadowMapUV = shadowMapUV * shadowTile.ShadowToAtlas.xy + shadowTile.ShadowToAtlas.zw; return shadowMapUV; } float SampleShadowMap(Texture2D shadowMap, float2 shadowMapUV, float sceneDepth) { float result = SAMPLE_SHADOW_MAP(shadowMap, shadowMapUV, sceneDepth); #if SHADOWS_QUALITY == 1 result += SAMPLE_SHADOW_MAP_OFFSET(shadowMap, shadowMapUV, int2(-1, 0), sceneDepth); result += SAMPLE_SHADOW_MAP_OFFSET(shadowMap, shadowMapUV, int2(0, -1), sceneDepth); result += SAMPLE_SHADOW_MAP_OFFSET(shadowMap, shadowMapUV, int2(0, 1), sceneDepth); result += SAMPLE_SHADOW_MAP_OFFSET(shadowMap, shadowMapUV, int2(1, 0), sceneDepth); result = result * (1.0f / 4.0); #elif SHADOWS_QUALITY == 2 || SHADOWS_QUALITY == 3 result += SAMPLE_SHADOW_MAP_OFFSET(shadowMap, shadowMapUV, int2(-1, -1), sceneDepth); result += SAMPLE_SHADOW_MAP_OFFSET(shadowMap, shadowMapUV, int2(-1, 0), sceneDepth); result += SAMPLE_SHADOW_MAP_OFFSET(shadowMap, shadowMapUV, int2(-1, 1), sceneDepth); result += SAMPLE_SHADOW_MAP_OFFSET(shadowMap, shadowMapUV, int2(0, -1), sceneDepth); result += SAMPLE_SHADOW_MAP_OFFSET(shadowMap, shadowMapUV, int2(0, 1), sceneDepth); result += SAMPLE_SHADOW_MAP_OFFSET(shadowMap, shadowMapUV, int2(1, -1), sceneDepth); result += SAMPLE_SHADOW_MAP_OFFSET(shadowMap, shadowMapUV, int2(1, 0), sceneDepth); result += SAMPLE_SHADOW_MAP_OFFSET(shadowMap, shadowMapUV, int2(1, 1), sceneDepth); result = result * (1.0f / 9.0); #endif return result; } float SampleShadowMapOptimizedPCFHelper(Texture2D shadowMap, float2 baseUV, float u, float v, float2 shadowMapSizeInv, float sceneDepth) { float2 uv = baseUV + float2(u, v) * shadowMapSizeInv; return SAMPLE_SHADOW_MAP(shadowMap, uv, sceneDepth); } // [Shadow map sampling method used in The Witness, https://github.com/TheRealMJP/Shadows] float SampleShadowMapOptimizedPCF(Texture2D shadowMap, float2 shadowMapUV, float sceneDepth) { #if SHADOWS_QUALITY != 0 float2 shadowMapSize; shadowMap.GetDimensions(shadowMapSize.x, shadowMapSize.y); float2 uv = shadowMapUV.xy * shadowMapSize; // 1 unit - 1 texel float2 shadowMapSizeInv = 1.0f / shadowMapSize; float2 baseUV; baseUV.x = floor(uv.x + 0.5); baseUV.y = floor(uv.y + 0.5); float s = (uv.x + 0.5 - baseUV.x); float t = (uv.y + 0.5 - baseUV.y); baseUV -= float2(0.5, 0.5); baseUV *= shadowMapSizeInv; float sum = 0; #endif #if SHADOWS_QUALITY == 0 return SAMPLE_SHADOW_MAP(shadowMap, shadowMapUV, sceneDepth); #elif SHADOWS_QUALITY == 1 float uw0 = (3 - 2 * s); float uw1 = (1 + 2 * s); float u0 = (2 - s) / uw0 - 1; float u1 = s / uw1 + 1; float vw0 = (3 - 2 * t); float vw1 = (1 + 2 * t); float v0 = (2 - t) / vw0 - 1; float v1 = t / vw1 + 1; sum += uw0 * vw0 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u0, v0, shadowMapSizeInv, sceneDepth); sum += uw1 * vw0 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u1, v0, shadowMapSizeInv, sceneDepth); sum += uw0 * vw1 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u0, v1, shadowMapSizeInv, sceneDepth); sum += uw1 * vw1 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u1, v1, shadowMapSizeInv, sceneDepth); return sum * 1.0f / 16; #elif SHADOWS_QUALITY == 2 float uw0 = (4 - 3 * s); float uw1 = 7; float uw2 = (1 + 3 * s); float u0 = (3 - 2 * s) / uw0 - 2; float u1 = (3 + s) / uw1; float u2 = s / uw2 + 2; float vw0 = (4 - 3 * t); float vw1 = 7; float vw2 = (1 + 3 * t); float v0 = (3 - 2 * t) / vw0 - 2; float v1 = (3 + t) / vw1; float v2 = t / vw2 + 2; sum += uw0 * vw0 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u0, v0, shadowMapSizeInv, sceneDepth); sum += uw1 * vw0 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u1, v0, shadowMapSizeInv, sceneDepth); sum += uw2 * vw0 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u2, v0, shadowMapSizeInv, sceneDepth); sum += uw0 * vw1 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u0, v1, shadowMapSizeInv, sceneDepth); sum += uw1 * vw1 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u1, v1, shadowMapSizeInv, sceneDepth); sum += uw2 * vw1 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u2, v1, shadowMapSizeInv, sceneDepth); sum += uw0 * vw2 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u0, v2, shadowMapSizeInv, sceneDepth); sum += uw1 * vw2 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u1, v2, shadowMapSizeInv, sceneDepth); sum += uw2 * vw2 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u2, v2, shadowMapSizeInv, sceneDepth); return sum * 1.0f / 144; #elif SHADOWS_QUALITY == 3 float uw0 = (5 * s - 6); float uw1 = (11 * s - 28); float uw2 = -(11 * s + 17); float uw3 = -(5 * s + 1); float u0 = (4 * s - 5) / uw0 - 3; float u1 = (4 * s - 16) / uw1 - 1; float u2 = -(7 * s + 5) / uw2 + 1; float u3 = -s / uw3 + 3; float vw0 = (5 * t - 6); float vw1 = (11 * t - 28); float vw2 = -(11 * t + 17); float vw3 = -(5 * t + 1); float v0 = (4 * t - 5) / vw0 - 3; float v1 = (4 * t - 16) / vw1 - 1; float v2 = -(7 * t + 5) / vw2 + 1; float v3 = -t / vw3 + 3; sum += uw0 * vw0 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u0, v0, shadowMapSizeInv, sceneDepth); sum += uw1 * vw0 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u1, v0, shadowMapSizeInv, sceneDepth); sum += uw2 * vw0 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u2, v0, shadowMapSizeInv, sceneDepth); sum += uw3 * vw0 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u3, v0, shadowMapSizeInv, sceneDepth); sum += uw0 * vw1 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u0, v1, shadowMapSizeInv, sceneDepth); sum += uw1 * vw1 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u1, v1, shadowMapSizeInv, sceneDepth); sum += uw2 * vw1 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u2, v1, shadowMapSizeInv, sceneDepth); sum += uw3 * vw1 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u3, v1, shadowMapSizeInv, sceneDepth); sum += uw0 * vw2 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u0, v2, shadowMapSizeInv, sceneDepth); sum += uw1 * vw2 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u1, v2, shadowMapSizeInv, sceneDepth); sum += uw2 * vw2 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u2, v2, shadowMapSizeInv, sceneDepth); sum += uw3 * vw2 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u3, v2, shadowMapSizeInv, sceneDepth); sum += uw0 * vw3 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u0, v3, shadowMapSizeInv, sceneDepth); sum += uw1 * vw3 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u1, v3, shadowMapSizeInv, sceneDepth); sum += uw2 * vw3 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u2, v3, shadowMapSizeInv, sceneDepth); sum += uw3 * vw3 * SampleShadowMapOptimizedPCFHelper(shadowMap, baseUV, u3, v3, shadowMapSizeInv, sceneDepth); return sum * (1.0f / 2704); #else return 0.0f; #endif } // Samples the shadow for the given directional light on the material surface (supports subsurface shadowing) ShadowSample SampleDirectionalLightShadow(LightData light, Buffer shadowsBuffer, Texture2D shadowMap, GBufferSample gBuffer, float dither = 0.0f) { #if !LIGHTING_NO_DIRECTIONAL // Skip if surface is in a full shadow float NoL = dot(gBuffer.Normal, light.Direction); BRANCH if (NoL <= 0 #if defined(USE_GBUFFER_CUSTOM_DATA) && !IsSubsurfaceMode(gBuffer.ShadingModel) #endif ) return (ShadowSample)0; #endif ShadowSample result; result.SurfaceShadow = 1; result.TransmissionShadow = 1; // Load shadow data if (light.ShadowsBufferAddress == 0) return result; // No shadow assigned ShadowData shadow = LoadShadowsBuffer(shadowsBuffer, light.ShadowsBufferAddress); // Create a blend factor which is one before and at the fade plane float viewDepth = gBuffer.ViewPos.z; float fade = saturate((viewDepth - shadow.CascadeSplits[shadow.TilesCount - 1] + shadow.FadeDistance) / shadow.FadeDistance); BRANCH if (fade >= 1.0) return result; // Figure out which cascade to sample from uint cascadeIndex = 0; for (uint i = 0; i < shadow.TilesCount - 1; i++) { if (viewDepth > shadow.CascadeSplits[i]) cascadeIndex = i + 1; } #ifdef SHADOWS_CSM_BLENDING const float BlendThreshold = 0.05f; float nextSplit = shadow.CascadeSplits[cascadeIndex]; float splitSize = cascadeIndex == 0 ? nextSplit : nextSplit - shadow.CascadeSplits[cascadeIndex - 1]; float splitDist = (nextSplit - viewDepth) / splitSize; if (splitDist <= BlendThreshold && cascadeIndex != shadow.TilesCount - 1) { // Blend with the next cascade but with screen-space dithering (gets cleaned out by TAA) float lerpAmount = 1 - splitDist / BlendThreshold; if (step(RandN2(gBuffer.ViewPos.xy + dither).x, lerpAmount)) cascadeIndex++; } #endif ShadowTileData shadowTile = LoadShadowsBufferTile(shadowsBuffer, light.ShadowsBufferAddress, cascadeIndex); float3 samplePosition = gBuffer.WorldPos; #if !LIGHTING_NO_DIRECTIONAL // Apply normal offset bias samplePosition += GetShadowPositionOffset(shadow.NormalOffsetScale, NoL, gBuffer.Normal); #endif // Project position into shadow atlas UV float4 shadowPosition; float2 shadowMapUV = GetLightShadowAtlasUV(shadow, shadowTile, samplePosition, shadowPosition); // Sample shadow map result.SurfaceShadow = SampleShadowMapOptimizedPCF(shadowMap, shadowMapUV, shadowPosition.z); // Increase the sharpness for higher cascades to match the filter radius const float SharpnessScale[MaxNumCascades] = { 1.0f, 1.5f, 3.0f, 3.5f }; shadow.Sharpness *= SharpnessScale[cascadeIndex]; #if defined(USE_GBUFFER_CUSTOM_DATA) // Subsurface shadowing BRANCH if (IsSubsurfaceMode(gBuffer.ShadingModel)) { float opacity = gBuffer.CustomData.a; shadowMapUV = GetLightShadowAtlasUV(shadow, shadowTile, gBuffer.WorldPos, shadowPosition); float shadowMapDepth = shadowMap.SampleLevel(SamplerLinearClamp, shadowMapUV, 0).r; result.TransmissionShadow = CalculateSubsurfaceOcclusion(opacity, shadowPosition.z, shadowMapDepth); result.TransmissionShadow = PostProcessShadow(shadow, result.TransmissionShadow); } #endif result.SurfaceShadow = PostProcessShadow(shadow, result.SurfaceShadow); // Fix shadow intensity that got reduced by cascades sharpness stabilization (see above) if (cascadeIndex != 0 && result.SurfaceShadow <= 0.1f) result.SurfaceShadow += 0.01f; return result; } // Samples the shadow for the given local light on the material surface (supports subsurface shadowing) ShadowSample SampleLocalLightShadow(LightData light, Buffer shadowsBuffer, Texture2D shadowMap, GBufferSample gBuffer, float3 L, float toLightLength, uint tileIndex) { #if !LIGHTING_NO_DIRECTIONAL // Skip if surface is in a full shadow float NoL = dot(gBuffer.Normal, L); BRANCH if (NoL <= 0 #if defined(USE_GBUFFER_CUSTOM_DATA) && !IsSubsurfaceMode(gBuffer.ShadingModel) #endif ) return (ShadowSample)0; #endif ShadowSample result; result.SurfaceShadow = 1; result.TransmissionShadow = 1; // Skip pixels outside of the light influence BRANCH if (toLightLength > light.Radius) return result; // Load shadow data if (light.ShadowsBufferAddress == 0) return result; // No shadow assigned ShadowData shadow = LoadShadowsBuffer(shadowsBuffer, light.ShadowsBufferAddress); ShadowTileData shadowTile = LoadShadowsBufferTile(shadowsBuffer, light.ShadowsBufferAddress, tileIndex); float3 samplePosition = gBuffer.WorldPos; #if !LIGHTING_NO_DIRECTIONAL // Apply normal offset bias samplePosition += GetShadowPositionOffset(shadow.NormalOffsetScale, NoL, gBuffer.Normal); #endif // Project position into shadow atlas UV float4 shadowPosition; float2 shadowMapUV = GetLightShadowAtlasUV(shadow, shadowTile, samplePosition, shadowPosition); // Sample shadow map result.SurfaceShadow = SampleShadowMapOptimizedPCF(shadowMap, shadowMapUV, shadowPosition.z); #if defined(USE_GBUFFER_CUSTOM_DATA) // Subsurface shadowing BRANCH if (IsSubsurfaceMode(gBuffer.ShadingModel)) { float opacity = gBuffer.CustomData.a; shadowMapUV = GetLightShadowAtlasUV(shadow, shadowTile, gBuffer.WorldPos, shadowPosition); float shadowMapDepth = shadowMap.SampleLevel(SamplerLinearClamp, shadowMapUV, 0).r; result.TransmissionShadow = CalculateSubsurfaceOcclusion(opacity, shadowPosition.z, shadowMapDepth); result.TransmissionShadow = PostProcessShadow(shadow, result.TransmissionShadow); } #endif result.SurfaceShadow = PostProcessShadow(shadow, result.SurfaceShadow); return result; } // Samples the shadow for the given spot light on the material surface (supports subsurface shadowing) ShadowSample SampleSpotLightShadow(LightData light, Buffer shadowsBuffer, Texture2D shadowMap, GBufferSample gBuffer) { float3 toLight = light.Position - gBuffer.WorldPos; float toLightLength = length(toLight); float3 L = toLight / toLightLength; return SampleLocalLightShadow(light, shadowsBuffer, shadowMap, gBuffer, L, toLightLength, 0); } // Samples the shadow for the given point light on the material surface (supports subsurface shadowing) ShadowSample SamplePointLightShadow(LightData light, Buffer shadowsBuffer, Texture2D shadowMap, GBufferSample gBuffer) { float3 toLight = light.Position - gBuffer.WorldPos; float toLightLength = length(toLight); float3 L = toLight / toLightLength; // Figure out which cube face we're sampling from uint cubeFaceIndex = GetCubeFaceIndex(-L); return SampleLocalLightShadow(light, shadowsBuffer, shadowMap, gBuffer, L, toLightLength, cubeFaceIndex); } GBufferSample GetDummyGBufferSample(float3 worldPosition) { GBufferSample gBuffer = (GBufferSample)0; gBuffer.ShadingModel = SHADING_MODEL_LIT; gBuffer.WorldPos = worldPosition; return gBuffer; } // Samples the shadow for the given directional light at custom location ShadowSample SampleDirectionalLightShadow(LightData light, Buffer shadowsBuffer, Texture2D shadowMap, float3 worldPosition, float viewDepth, float dither = 0.0f) { GBufferSample gBuffer = GetDummyGBufferSample(worldPosition); gBuffer.ViewPos.z = viewDepth; return SampleDirectionalLightShadow(light, shadowsBuffer, shadowMap, gBuffer, dither); } // Samples the shadow for the given spot light at custom location ShadowSample SampleSpotLightShadow(LightData light, Buffer shadowsBuffer, Texture2D shadowMap, float3 worldPosition) { GBufferSample gBuffer = GetDummyGBufferSample(worldPosition); return SampleSpotLightShadow(light, shadowsBuffer, shadowMap, gBuffer); } // Samples the shadow for the given point light at custom location ShadowSample SamplePointLightShadow(LightData light, Buffer shadowsBuffer, Texture2D shadowMap, float3 worldPosition) { GBufferSample gBuffer = GetDummyGBufferSample(worldPosition); return SamplePointLightShadow(light, shadowsBuffer, shadowMap, gBuffer); } #endif