// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.

#ifndef __SHADOWS_SAMPLING__
#define __SHADOWS_SAMPLING__

#include "./Flax/ShadowsCommon.hlsl"
#include "./Flax/GBufferCommon.hlsl"
#include "./Flax/LightingCommon.hlsl"

// Select shadows filter based on quality
// Supported sampling kernel sizes fo each shadowing technique:
// CSM: 2, 3, 5, 7, 9
// Cube: 2, 5, 12, 29
// Spot: 2, 5, 12, 29
#if SHADOWS_QUALITY == 0

	#define FilterSizeCSM 2
	#define FilterSizeCube 2
	#define FilterSizeSpot 2
	
#elif SHADOWS_QUALITY == 1

	#define FilterSizeCSM 3
	#define FilterSizeCube 5
	#define FilterSizeSpot 5
	
#elif SHADOWS_QUALITY == 2

	#define FilterSizeCSM 5
	#define FilterSizeCube 12
	#define FilterSizeSpot 12
	
#else // SHADOWS_QUALITY == 3

	#define FilterSizeCSM 7
	#define FilterSizeCube 12
	#define FilterSizeSpot 12
	
#endif

#if SHADOWS_QUALITY != 0
#include "./Flax/PCFKernels.hlsl"
#endif

// Samples the shadow map with a fixed-size PCF kernel optimized with GatherCmpRed.
// Uses code from "Fast Conventional Shadow Filtering" by Holger Gruen, in GPU Pro.
float SampleShadowMapFixedSizePCF(Texture2DArray shadowMap, float2 shadowMapSize, float sceneDepth, float2 shadowPos, uint cascadeIndex)
{
#if FilterSizeCSM == 2

	#if FEATURE_LEVEL >= FEATURE_LEVEL_SM5

		return shadowMap.SampleCmpLevelZero(ShadowSamplerPCF, float3(shadowPos.xy, cascadeIndex), sceneDepth);

	#else

		return sceneDepth < shadowMap.SampleLevel(SamplerLinearClamp, float3(shadowPos.xy, cascadeIndex), 0).r;

	#endif

#else

	const int FS_2 = FilterSizeCSM / 2;
	float2 tc = shadowPos.xy;
	float4 s = 0.0f;
	float2 stc = (shadowMapSize * tc.xy) + float2(0.5f, 0.5f);
	float2 tcs = floor(stc);
	float2 fc;
	int row;
	int col;
	float4 v1[FS_2 + 1];
	float2 v0[FS_2 + 1];
	float3 baseUV = float3(tc.xy, cascadeIndex);
    float2 shadowMapSizeInv = 1.0f / shadowMapSize;

	fc.xy = stc - tcs;
	tc.xy = tcs * shadowMapSizeInv;
	
	// Loop over the rows
	UNROLL
	for (row = -FS_2; row <= FS_2; row += 2)
	{
		UNROLL
		for (col = -FS_2; col <= FS_2; col += 2)
		{
			float value = CSMFilterWeights[row + FS_2][col + FS_2];

			if (col > -FS_2)
				value += CSMFilterWeights[row + FS_2][col + FS_2 - 1];

			if (col < FS_2)
				value += CSMFilterWeights[row + FS_2][col + FS_2 + 1];

			if (row > -FS_2) {
				value += CSMFilterWeights[row + FS_2 - 1][col + FS_2];

				if (col < FS_2)
					value += CSMFilterWeights[row + FS_2 - 1][col + FS_2 + 1];

				if (col > -FS_2)
					value += CSMFilterWeights[row + FS_2 - 1][col + FS_2 - 1];
			}

			if (value != 0.0f)
			{
				// Using Gather: xyzw in counter clockwise order starting with the sample to the lower left of the queried location
				#if CAN_USE_GATHER
				
					v1[(col + FS_2) / 2] = shadowMap.GatherCmp(ShadowSampler, baseUV, sceneDepth, int2(col, row));
				
				#else
				
					float4 gather;
					
					gather.x = sceneDepth < shadowMap.SampleLevel(SamplerPointClamp, float3(tc.xy + float2(0, 1) * shadowMapSizeInv, cascadeIndex), 0, int2(col, row)).r;
					gather.y = sceneDepth < shadowMap.SampleLevel(SamplerPointClamp, float3(tc.xy + float2(1, 1) * shadowMapSizeInv, cascadeIndex), 0, int2(col, row)).r;
					gather.z = sceneDepth < shadowMap.SampleLevel(SamplerPointClamp, float3(tc.xy + float2(1, 0) * shadowMapSizeInv, cascadeIndex), 0, int2(col, row)).r;
					gather.w = sceneDepth < shadowMap.SampleLevel(SamplerPointClamp, float3(tc.xy + float2(0, 0) * shadowMapSizeInv, cascadeIndex), 0, int2(col, row)).r;
					
					v1[(col + FS_2) / 2] = gather;
				
				#endif
			}
			else
				v1[(col + FS_2) / 2] = 0.0f;

			if (col == -FS_2)
			{
				s.x += (1.0f - fc.y) * (v1[0].w * (CSMFilterWeights[row + FS_2][col + FS_2]
									 - CSMFilterWeights[row + FS_2][col + FS_2] * fc.x)
									 + v1[0].z * (fc.x * (CSMFilterWeights[row + FS_2][col + FS_2]
									 - CSMFilterWeights[row + FS_2][col + FS_2 + 1.0f])
									 + CSMFilterWeights[row + FS_2][col + FS_2 + 1]));
				s.y += fc.y * (v1[0].x * (CSMFilterWeights[row + FS_2][col + FS_2]
									 - CSMFilterWeights[row + FS_2][col + FS_2] * fc.x)
									 + v1[0].y * (fc.x * (CSMFilterWeights[row + FS_2][col + FS_2]
									 - CSMFilterWeights[row + FS_2][col + FS_2 + 1])
									 +  CSMFilterWeights[row + FS_2][col + FS_2 + 1]));
				if(row > -FS_2)
				{
					s.z += (1.0f - fc.y) * (v0[0].x * (CSMFilterWeights[row + FS_2 - 1][col + FS_2]
										   - CSMFilterWeights[row + FS_2 - 1][col + FS_2] * fc.x)
										   + v0[0].y * (fc.x * (CSMFilterWeights[row + FS_2 - 1][col + FS_2]
										   - CSMFilterWeights[row + FS_2 - 1][col + FS_2 + 1])
										   + CSMFilterWeights[row + FS_2 - 1][col + FS_2 + 1]));
					s.w += fc.y * (v1[0].w * (CSMFilterWeights[row + FS_2 - 1][col + FS_2]
										- CSMFilterWeights[row + FS_2 - 1][col + FS_2] * fc.x)
										+ v1[0].z * (fc.x * (CSMFilterWeights[row + FS_2 - 1][col + FS_2]
										- CSMFilterWeights[row + FS_2 - 1][col + FS_2 + 1])
										+ CSMFilterWeights[row + FS_2 - 1][col + FS_2 + 1]));
				}
			}
			else if (col == FS_2)
			{
				s.x += (1 - fc.y) * (v1[FS_2].w * (fc.x * (CSMFilterWeights[row + FS_2][col + FS_2 - 1]
									 - CSMFilterWeights[row + FS_2][col + FS_2]) + CSMFilterWeights[row + FS_2][col + FS_2])
									 + v1[FS_2].z * fc.x * CSMFilterWeights[row + FS_2][col + FS_2]);
				s.y += fc.y * (v1[FS_2].x * (fc.x * (CSMFilterWeights[row + FS_2][col + FS_2 - 1]
									 - CSMFilterWeights[row + FS_2][col + FS_2] ) + CSMFilterWeights[row + FS_2][col + FS_2])
									 + v1[FS_2].y * fc.x * CSMFilterWeights[row + FS_2][col + FS_2]);
				if(row > -FS_2) {
					s.z += (1 - fc.y) * (v0[FS_2].x * (fc.x * (CSMFilterWeights[row + FS_2 - 1][col + FS_2 - 1]
										- CSMFilterWeights[row + FS_2 - 1][col + FS_2])
										+ CSMFilterWeights[row + FS_2 - 1][col + FS_2])
										+ v0[FS_2].y * fc.x * CSMFilterWeights[row + FS_2 - 1][col + FS_2]);
					s.w += fc.y * (v1[FS_2].w * (fc.x * (CSMFilterWeights[row + FS_2 - 1][col + FS_2 - 1]
										- CSMFilterWeights[row + FS_2 - 1][col + FS_2])
										+ CSMFilterWeights[row + FS_2 - 1][col + FS_2])
										+ v1[FS_2].z * fc.x * CSMFilterWeights[row + FS_2 - 1][col + FS_2]);
				}
			}
			else
			{
				s.x += (1 - fc.y) * (v1[(col + FS_2) / 2].w * (fc.x * (CSMFilterWeights[row + FS_2][col + FS_2 - 1]
									- CSMFilterWeights[row + FS_2][col + FS_2 + 0] ) + CSMFilterWeights[row + FS_2][col + FS_2 + 0])
									+ v1[(col + FS_2) / 2].z * (fc.x * (CSMFilterWeights[row + FS_2][col + FS_2 - 0]
									- CSMFilterWeights[row + FS_2][col + FS_2 + 1]) + CSMFilterWeights[row + FS_2][col + FS_2 + 1]));
				s.y += fc.y * (v1[(col + FS_2) / 2].x * (fc.x * (CSMFilterWeights[row + FS_2][col + FS_2-1]
									- CSMFilterWeights[row + FS_2][col + FS_2 + 0]) + CSMFilterWeights[row + FS_2][col + FS_2 + 0])
									+ v1[(col + FS_2) / 2].y * (fc.x * (CSMFilterWeights[row + FS_2][col + FS_2 - 0]
									- CSMFilterWeights[row + FS_2][col + FS_2 + 1]) + CSMFilterWeights[row + FS_2][col + FS_2 + 1]));
				if(row > -FS_2) {
					s.z += (1 - fc.y) * (v0[(col + FS_2) / 2].x * (fc.x * (CSMFilterWeights[row + FS_2 - 1][col + FS_2 - 1]
											- CSMFilterWeights[row + FS_2 - 1][col + FS_2 + 0]) + CSMFilterWeights[row + FS_2 - 1][col + FS_2 + 0])
											+ v0[(col + FS_2) / 2].y * (fc.x * (CSMFilterWeights[row + FS_2 - 1][col + FS_2 - 0]
											- CSMFilterWeights[row + FS_2 - 1][col + FS_2 + 1]) + CSMFilterWeights[row + FS_2 - 1][col + FS_2 + 1]));
					s.w += fc.y * (v1[(col + FS_2) / 2].w * (fc.x * (CSMFilterWeights[row + FS_2 - 1][col + FS_2 - 1]
											- CSMFilterWeights[row + FS_2 - 1][col + FS_2 + 0]) + CSMFilterWeights[row + FS_2 - 1][col + FS_2 + 0])
											+ v1[(col + FS_2) / 2].z * (fc.x * (CSMFilterWeights[row + FS_2 - 1][col + FS_2 - 0]
											- CSMFilterWeights[row + FS_2 - 1][col + FS_2 + 1]) + CSMFilterWeights[row + FS_2 - 1][col + FS_2 + 1]));
				}
			}

			if (row != FS_2)
				v0[(col + FS_2) / 2] = v1[(col + FS_2) / 2].xy;
		}
	}

	return dot(s, 1.0f) / CSMFilterWeightsSum;
	
#endif
}

// Helper function for SampleShadowMapOptimizedPCF
float SampleShadowMap(Texture2DArray shadowMap, float2 baseUv, float u, float v, float2 shadowMapSizeInv, uint cascadeIndex, float depth)
{
    float2 uv = baseUv + float2(u, v) * shadowMapSizeInv;
    return shadowMap.SampleCmpLevelZero(ShadowSamplerPCF, float3(uv, cascadeIndex), depth);
}

// The method used in The Witness
float SampleShadowMapOptimizedPCF(Texture2DArray shadowMap, float2 shadowMapSize, float sceneDepth, float2 shadowPos, uint cascadeIndex)
{
    float2 uv = shadowPos.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;

#if FilterSizeCSM == 2

	return shadowMap.SampleCmpLevelZero(ShadowSamplerPCF, float3(shadowPos.xy, cascadeIndex), sceneDepth);

#elif FilterSizeCSM == 3

	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 * SampleShadowMap(shadowMap, baseUv, u0, v0, shadowMapSizeInv, cascadeIndex, sceneDepth);
	sum += uw1 * vw0 * SampleShadowMap(shadowMap, baseUv, u1, v0, shadowMapSizeInv, cascadeIndex, sceneDepth);
	sum += uw0 * vw1 * SampleShadowMap(shadowMap, baseUv, u0, v1, shadowMapSizeInv, cascadeIndex, sceneDepth);
	sum += uw1 * vw1 * SampleShadowMap(shadowMap, baseUv, u1, v1, shadowMapSizeInv, cascadeIndex, sceneDepth);

	return sum * 1.0f / 16;

	#elif FilterSizeCSM == 5

	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 * SampleShadowMap(shadowMap, baseUv, u0, v0, shadowMapSizeInv, cascadeIndex, sceneDepth);
	sum += uw1 * vw0 * SampleShadowMap(shadowMap, baseUv, u1, v0, shadowMapSizeInv, cascadeIndex, sceneDepth);
	sum += uw2 * vw0 * SampleShadowMap(shadowMap, baseUv, u2, v0, shadowMapSizeInv, cascadeIndex, sceneDepth);

	sum += uw0 * vw1 * SampleShadowMap(shadowMap, baseUv, u0, v1, shadowMapSizeInv, cascadeIndex, sceneDepth);
	sum += uw1 * vw1 * SampleShadowMap(shadowMap, baseUv, u1, v1, shadowMapSizeInv, cascadeIndex, sceneDepth);
	sum += uw2 * vw1 * SampleShadowMap(shadowMap, baseUv, u2, v1, shadowMapSizeInv, cascadeIndex, sceneDepth);

	sum += uw0 * vw2 * SampleShadowMap(shadowMap, baseUv, u0, v2, shadowMapSizeInv, cascadeIndex, sceneDepth);
	sum += uw1 * vw2 * SampleShadowMap(shadowMap, baseUv, u1, v2, shadowMapSizeInv, cascadeIndex, sceneDepth);
	sum += uw2 * vw2 * SampleShadowMap(shadowMap, baseUv, u2, v2, shadowMapSizeInv, cascadeIndex, sceneDepth);

	return sum * 1.0f / 144;

#else // FilterSizeCSM == 7

	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 * SampleShadowMap(shadowMap, baseUv, u0, v0, shadowMapSizeInv, cascadeIndex, sceneDepth);
	sum += uw1 * vw0 * SampleShadowMap(shadowMap, baseUv, u1, v0, shadowMapSizeInv, cascadeIndex, sceneDepth);
	sum += uw2 * vw0 * SampleShadowMap(shadowMap, baseUv, u2, v0, shadowMapSizeInv, cascadeIndex, sceneDepth);
	sum += uw3 * vw0 * SampleShadowMap(shadowMap, baseUv, u3, v0, shadowMapSizeInv, cascadeIndex, sceneDepth);

	sum += uw0 * vw1 * SampleShadowMap(shadowMap, baseUv, u0, v1, shadowMapSizeInv, cascadeIndex, sceneDepth);
	sum += uw1 * vw1 * SampleShadowMap(shadowMap, baseUv, u1, v1, shadowMapSizeInv, cascadeIndex, sceneDepth);
	sum += uw2 * vw1 * SampleShadowMap(shadowMap, baseUv, u2, v1, shadowMapSizeInv, cascadeIndex, sceneDepth);
	sum += uw3 * vw1 * SampleShadowMap(shadowMap, baseUv, u3, v1, shadowMapSizeInv, cascadeIndex, sceneDepth);

	sum += uw0 * vw2 * SampleShadowMap(shadowMap, baseUv, u0, v2, shadowMapSizeInv, cascadeIndex, sceneDepth);
	sum += uw1 * vw2 * SampleShadowMap(shadowMap, baseUv, u1, v2, shadowMapSizeInv, cascadeIndex, sceneDepth);
	sum += uw2 * vw2 * SampleShadowMap(shadowMap, baseUv, u2, v2, shadowMapSizeInv, cascadeIndex, sceneDepth);
	sum += uw3 * vw2 * SampleShadowMap(shadowMap, baseUv, u3, v2, shadowMapSizeInv, cascadeIndex, sceneDepth);

	sum += uw0 * vw3 * SampleShadowMap(shadowMap, baseUv, u0, v3, shadowMapSizeInv, cascadeIndex, sceneDepth);
	sum += uw1 * vw3 * SampleShadowMap(shadowMap, baseUv, u1, v3, shadowMapSizeInv, cascadeIndex, sceneDepth);
	sum += uw2 * vw3 * SampleShadowMap(shadowMap, baseUv, u2, v3, shadowMapSizeInv, cascadeIndex, sceneDepth);
	sum += uw3 * vw3 * SampleShadowMap(shadowMap, baseUv, u3, v3, shadowMapSizeInv, cascadeIndex, sceneDepth);

	return sum * (1.0f / 2704);

#endif
}

// Samples the shadow from the shadow map cascade
float SampleShadowCascade(Texture2DArray shadowMap, float2 shadowMapSize, float sceneDepth, float2 shadowPosition, uint cascadeIndex)
{
	float shadow = SampleShadowMapFixedSizePCF(shadowMap, shadowMapSize, sceneDepth, shadowPosition, cascadeIndex);
	//float shadow = SampleShadowMapOptimizedPCF(shadowMap, shadowMapSize, sceneDepth, shadowPosition, cascadeIndex);
	return shadow;
}

// Samples the shadow for the given directional light (cascaded shadow map sampling)
float SampleShadow(LightData light, LightShadowData shadow, Texture2DArray shadowMap, float3 worldPosition, float viewDepth)
{
	// Create a blend factor which is one before and at the fade plane
	float fade = saturate((viewDepth - shadow.CascadeSplits[shadow.NumCascades - 1] + shadow.FadeDistance) / shadow.FadeDistance);
	BRANCH
	if (fade >= 1.0)
	{
		return 1;
	}

	// Figure out which cascade to sample from
	uint cascadeIndex = 0;
	for (uint i = 0; i < shadow.NumCascades - 1; i++)
	{
		if (viewDepth > shadow.CascadeSplits[i])
			cascadeIndex = i + 1;
	}

    // Project into shadow space
	float4 shadowPosition = mul(float4(worldPosition, 1.0f), shadow.ShadowVP[cascadeIndex]);
	shadowPosition.xy /= shadowPosition.w;
	shadowPosition.z -= shadow.Bias;

	// Sample shadow
	float result = SampleShadowCascade(shadowMap, shadow.ShadowMapSize, shadowPosition.z, shadowPosition.xy, cascadeIndex);

	// Increase the sharpness for higher cascades to match the filter radius
	const float SharpnessScale[MaxNumCascades] = { 1.0f, 1.5f, 3.0f, 3.5f };
	float sharpness = shadow.Sharpness * SharpnessScale[cascadeIndex];

#if CSM_BLENDING
	// Sample the next cascade, and blend between the two results to smooth the transition
	const float BlendThreshold = 0.1f;
	float nextSplit = shadow.CascadeSplits[cascadeIndex];
	float splitSize = cascadeIndex == 0 ? nextSplit : nextSplit - shadow.CascadeSplits[cascadeIndex - 1];
	float splitDist = (nextSplit - viewDepth) / splitSize;
	BRANCH
	if (splitDist <= BlendThreshold && cascadeIndex != shadow.NumCascades - 1)
	{
		// Find the position of this pixel in light space of next cascade
		shadowPosition = mul(float4(worldPosition, 1.0f), shadow.ShadowVP[cascadeIndex + 1]);
		shadowPosition.xy /= shadowPosition.w;
		shadowPosition.z -= shadow.Bias;

		// Sample next cascade and blur result
		float nextSplitShadow = SampleShadowCascade(shadowMap, shadow.ShadowMapSize, shadowPosition.z, shadowPosition.xy, cascadeIndex + 1);
		float lerpAmount = smoothstep(0.0f, BlendThreshold, splitDist);
		lerpAmount = splitDist / BlendThreshold;
		result = lerp(nextSplitShadow, result, lerpAmount);

		// Blur sharpness as well
		sharpness = lerp(shadow.Sharpness * SharpnessScale[cascadeIndex + 1], sharpness, lerpAmount);
	}
#endif

	// Apply shadow fade and sharpness
	result = saturate((result - 0.5) * sharpness + 0.5);
	result = lerp(1.0f, result, (1 - fade) * shadow.Fade);
	return result;
}

// Samples the shadow for the given directional light (cascaded shadow map sampling) for the material surface (supports subsurface shadowing)
float SampleShadow(LightData light, LightShadowData shadow, Texture2DArray shadowMap, GBufferSample gBuffer, out float subsurfaceShadow)
{
	subsurfaceShadow = 1;

	// 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.NumCascades - 1] + shadow.FadeDistance) / shadow.FadeDistance);
	BRANCH
	if (fade >= 1.0)
	{
		return 1;
	}

	// Figure out which cascade to sample from
	uint cascadeIndex = 0;
	for (uint i = 0; i < shadow.NumCascades - 1; i++)
	{
		if (viewDepth > shadow.CascadeSplits[i])
			cascadeIndex = i + 1;
	}

#if defined(USE_GBUFFER_CUSTOM_DATA)
	// Subsurface shadowing
	BRANCH
	if (IsSubsurfaceMode(gBuffer.ShadingModel))
	{
		// Get subsurface material info
		float opacity = gBuffer.CustomData.a;

		// Project into shadow space
		float4 shadowPosition = mul(float4(gBuffer.WorldPos, 1.0f), shadow.ShadowVP[cascadeIndex]);
		shadowPosition.xy /= shadowPosition.w;
		shadowPosition.z -= shadow.Bias;

		// Sample shadow map (single hardware sample with hardware filtering)
		float shadowMapDepth = shadowMap.SampleLevel(SamplerLinearClamp, float3(shadowPosition.xy, cascadeIndex), 0).r;
		subsurfaceShadow = CalculateSubsurfaceOcclusion(opacity, shadowPosition.z, shadowMapDepth);

		// Apply shadow fade
		subsurfaceShadow = lerp(1.0f, subsurfaceShadow, (1 - fade) * shadow.Fade);
	}
#endif

	// Skip if surface is in a full shadow
	float NoL = dot(gBuffer.Normal, light.Direction);
	BRANCH
	if (NoL <= 0)
	{
		return 0;
	}

	// Apply normal offset bias
	float3 samplePosWS = gBuffer.WorldPos;
	samplePosWS += GetShadowPositionOffset(shadow.NormalOffsetScale, NoL, gBuffer.Normal);

	// Sample shadow
	return SampleShadow(light, shadow, shadowMap, samplePosWS, viewDepth);
}

// Samples the shadow for the given spot light (PCF shadow map sampling)
float SampleShadow(LightData light, LightShadowData shadow, Texture2D shadowMap, float3 worldPosition)
{
	float3 toLight = light.Position - worldPosition;
	float toLightLength = length(toLight);
	float3 L = toLight / toLightLength;
	float dirCheck = dot(-light.Direction, L);

	// Skip pixels outside of the light influence
	BRANCH
	if (toLightLength > light.Radius || dirCheck < 0)
	{
		return 1;
	}

	// Negate direction and use normalized value
	toLight = -L;

	// Project into shadow space
	float4 shadowPosition = mul(float4(worldPosition, 1.0f), shadow.ShadowVP[0]);
	shadowPosition.z -= shadow.Bias;
	shadowPosition.xyz /= shadowPosition.w;

	float2 shadowMapUVs = shadowPosition.xy * float2(0.5f, -0.5f) + float2(0.5f, 0.5f);

#if FilterSizeSpot == 2 

	// Use single hardware sample with filtering
	float result = shadowMap.SampleCmpLevelZero(ShadowSamplerPCF, shadowMapUVs, shadowPosition.z);

#else

	float3 sideVector = normalize(cross(toLight, float3(0, 0, 1)));
	float3 upVector = cross(sideVector, toLight);

    float shadowMapSizeInv = 1.0f / shadow.ShadowMapSize.x;
	sideVector *= shadowMapSizeInv;
	upVector *= shadowMapSizeInv;

	// Use PCF filter
	float result = 0;
	UNROLL
	for(int i = 0; i < FilterSizeCube; i++)
	{
		float2 samplePos = shadowMapUVs + sideVector * PCFDiscSamples[i].x + upVector * PCFDiscSamples[i].y;
		result += shadowMap.SampleCmpLevelZero(ShadowSamplerPCF, samplePos, shadowPosition.z);
	}
	result *= (1.0f / FilterSizeCube);

#endif

	// Apply shadow fade and sharpness
	result = saturate((result - 0.5) * shadow.Sharpness + 0.5);
	result = lerp(1.0f, result, shadow.Fade);

	return result;
}

// Samples the shadow for the given spot light (PCF shadow map sampling) for the material surface (supports subsurface shadowing)
float SampleShadow(LightData light, LightShadowData shadow, Texture2D shadowMap, GBufferSample gBuffer, out float subsurfaceShadow)
{
	subsurfaceShadow = 1;
	float3 toLight = light.Position - gBuffer.WorldPos;
	float toLightLength = length(toLight);
	float3 L = toLight / toLightLength;
	float dirCheck = dot(-light.Direction, L);

	// Skip pixels outside of the light influence
	BRANCH
	if (toLightLength > light.Radius || dirCheck < 0)
	{
		return 1;
	}

#if defined(USE_GBUFFER_CUSTOM_DATA)
	// Subsurface shadowing
	BRANCH
	if (IsSubsurfaceMode(gBuffer.ShadingModel))
	{
		// Get subsurface material info
		float opacity = gBuffer.CustomData.a;

		// Project into shadow space
		float4 shadowPosition = mul(float4(gBuffer.WorldPos, 1.0f), shadow.ShadowVP[0]);
		shadowPosition.z -= shadow.Bias;
		shadowPosition.xyz /= shadowPosition.w;

		// Sample shadow map (use single hardware sample with filtering)
		float shadowMapDepth = shadowMap.SampleLevel(SamplerLinearClamp, shadowPosition.xy * float2(0.5f, -0.5f) + float2(0.5f, 0.5f), 0).r;
		subsurfaceShadow = CalculateSubsurfaceOcclusion(opacity, shadowPosition.z, shadowMapDepth);

		// Apply shadow fade
		subsurfaceShadow = lerp(1.0f, subsurfaceShadow, shadow.Fade);
	}
#endif

	// Skip if surface is in a full shadow
	float NoL = dot(gBuffer.Normal, L);
	BRANCH
	if (NoL <= 0)
	{
		return 0;
	}

	// Apply normal offset bias
	float3 samplePosWS = gBuffer.WorldPos;
	samplePosWS += GetShadowPositionOffset(shadow.NormalOffsetScale, NoL, gBuffer.Normal);

	// Sample shadow
	return SampleShadow(light, shadow, shadowMap, samplePosWS);
}

// Samples the shadow for the given point light (PCF shadow map sampling)
float SampleShadow(LightData light, LightShadowData shadow, TextureCube<float> shadowMap, float3 worldPosition)
{
	float3 toLight = light.Position - worldPosition;
	float toLightLength = length(toLight);
	float3 L = toLight / toLightLength;

	// Skip pixels outside of the light influence
	BRANCH
	if (toLightLength > light.Radius)
	{
		return 1;
	}

	// Negate direction and use normalized value
	toLight = -L;

	// Figure out which cube face we're sampling from
	int cubeFaceIndex = GetCubeFaceIndex(toLight);

	// Project into shadow space
	float4 shadowPosition = mul(float4(worldPosition, 1.0f), shadow.ShadowVP[cubeFaceIndex]);
	shadowPosition.z -= shadow.Bias;
	shadowPosition.xyz /= shadowPosition.w;

#if FilterSizeCube == 2 

	// Use single hardware sample with filtering
	float result = shadowMap.SampleCmpLevelZero(ShadowSamplerPCF, toLight, shadowPosition.z);

#else

	float3 sideVector = normalize(cross(toLight, float3(0, 0, 1)));
	float3 upVector = cross(sideVector, toLight);

    float shadowMapSizeInv = 1.0f / shadow.ShadowMapSize.x;
	sideVector *= shadowMapSizeInv;
	upVector *= shadowMapSizeInv;

	// Use PCF filter
	float result = 0;
	UNROLL
	for (int i = 0; i < FilterSizeCube; i++)
	{
		float3 cubeSamplePos = toLight + sideVector * PCFDiscSamples[i].x + upVector * PCFDiscSamples[i].y;
		result += shadowMap.SampleCmpLevelZero(ShadowSamplerPCF, cubeSamplePos, shadowPosition.z);
	}
	result *= (1.0f / FilterSizeCube);

#endif

	// Apply shadow fade and sharpness
	result = saturate((result - 0.5) * shadow.Sharpness + 0.5);
	result = lerp(1.0f, result, shadow.Fade);

	return result;
}

// Samples the shadow for the given point light (PCF shadow map sampling) for the material surface (supports subsurface shadowing)
float SampleShadow(LightData light, LightShadowData shadow, TextureCube<float> shadowMap, GBufferSample gBuffer, out float subsurfaceShadow)
{
	subsurfaceShadow = 1;
	float3 toLight = light.Position - gBuffer.WorldPos;
	float toLightLength = length(toLight);
	float3 L = toLight / toLightLength;

	// Skip pixels outside of the light influence
	BRANCH
	if (toLightLength > light.Radius)
	{
		return 1;
	}

	// Negate direction and use normalized value
	toLight = -L;

	// Figure out which cube face we're sampling from
	int cubeFaceIndex = GetCubeFaceIndex(toLight);

#if defined(USE_GBUFFER_CUSTOM_DATA)
	// Subsurface shadowing
	BRANCH
	if (IsSubsurfaceMode(gBuffer.ShadingModel))
	{
		// Get subsurface material info
		float opacity = gBuffer.CustomData.a;

		// Project into shadow space
		float4 shadowPosition = mul(float4(gBuffer.WorldPos, 1.0f), shadow.ShadowVP[cubeFaceIndex]);
		shadowPosition.z -= shadow.Bias;
		shadowPosition.xyz /= shadowPosition.w;

		// Sample shadow map (use single hardware sample with filtering)
		float shadowMapDepth = shadowMap.SampleLevel(SamplerLinearClamp, toLight, 0).r;
		subsurfaceShadow = CalculateSubsurfaceOcclusion(opacity, shadowPosition.z, shadowMapDepth);

		// Apply shadow fade
		subsurfaceShadow = lerp(1.0f, subsurfaceShadow, shadow.Fade);
	}
#endif

    // Skip if surface is in a full shadow
	float NoL = dot(gBuffer.Normal, L);
	BRANCH
	if (NoL <= 0)
	{
		return 0;
	}

	// Apply normal offset bias
	float3 samplePosWS = gBuffer.WorldPos;
	samplePosWS += GetShadowPositionOffset(shadow.NormalOffsetScale, NoL, gBuffer.Normal);

	// Sample shadow
	return SampleShadow(light, shadow, shadowMap, samplePosWS);
}

#endif
