// 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) { // Single hardware sample with filtering 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 // TODO: implement Percentage-Closer Soft Shadows (PCSS) for Ultra quality 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; } // 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 = SampleShadowMap(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); 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 = SampleShadowMap(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