const vec2 poissonDisk[16] = vec2[]( 
   vec2( -0.94201624, -0.39906216 ), 
   vec2( 0.94558609, -0.76890725 ), 
   vec2( -0.094184101, -0.92938870 ), 
   vec2( 0.34495938, 0.29387760 ), 
   vec2( -0.91588581, 0.45771432 ), 
   vec2( -0.81544232, -0.87912464 ), 
   vec2( -0.38277543, 0.27676845 ), 
   vec2( 0.97484398, 0.75648379 ), 
   vec2( 0.44323325, -0.97511554 ), 
   vec2( 0.53742981, -0.47373420 ), 
   vec2( -0.26496911, -0.41893023 ), 
   vec2( 0.79197514, 0.19090188 ), 
   vec2( -0.24188840, 0.99706507 ), 
   vec2( -0.81409955, 0.91437590 ), 
   vec2( 0.19984126, 0.78641367 ), 
   vec2( 0.14383161, -0.14100790 ) 
);

#ifndef SHADOW_SAMPLES
	#define SHADOW_SAMPLES ubo.settings.lighting.shadowSamples
#endif
#if VXGI
	float shadowFactorVXGI( const Light, float def );
#endif
#if RT
	float shadowFactorRT( const Light, float def );
#endif

#if CUBEMAPS
float omniShadowMap( const Light light, float def ) {
	return 1.0;
}
#else
float omniShadowMap( const Light light, float def ) {
	float factor = 1.0;

	const mat4 views[6] = {
		mat4( 0, 0, 1, 0, 0, 1, 0, 0,-1, 0, 0, 0, 0, 0, 0, 1 ),
		mat4( 0, 0,-1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1 ),
		mat4( 1, 0, 0, 0, 0, 0, 1, 0, 0,-1, 0, 0, 0, 0, 0, 1 ),
		mat4( 1, 0, 0, 0, 0, 0,-1, 0, 0, 1, 0, 0, 0, 0, 0, 1 ),
		mat4( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ),
		mat4(-1, 0, 0, 0, 0, 1, 0, 0, 0, 0,-1, 0, 0, 0, 0, 1 ),
	};
	
	const vec3 D = normalize(surface.position.world - light.position);
	const vec3 N = abs(D);
	uint A = N.y > N.x ? 1 : 0;
	A = N.z > N[A] ? 2 : A;
	uint index = A * 2;
	if ( D[A] < 0.0 ) ++index;
	
	vec4 positionClip = light.projection * views[index] * vec4(surface.position.world - light.position, 1.0);
	positionClip.xy /= positionClip.w;

	if ( positionClip.x < -1 || positionClip.x >= 1 ) return 1.0f;
	if ( positionClip.y < -1 || positionClip.y >= 1 ) return 1.0f;
	if ( positionClip.z <= 0 || positionClip.z >= 1 ) return 1.0f;

	const float bias = light.depthBias;
	const float eyeDepth = abs(positionClip.z / positionClip.w);

	const vec3 sampleOffsetDirections[20] = {
		vec3( 1,  1,  1), vec3( 1, -1,  1), vec3(-1, -1,  1), vec3(-1,  1,  1), 
		vec3( 1,  1, -1), vec3( 1, -1, -1), vec3(-1, -1, -1), vec3(-1,  1, -1),
		vec3( 1,  1,  0), vec3( 1, -1,  0), vec3(-1, -1,  0), vec3(-1,  1,  0),
		vec3( 1,  0,  1), vec3(-1,  0,  1), vec3( 1,  0, -1), vec3(-1,  0, -1),
		vec3( 0,  1,  1), vec3( 0, -1,  1), vec3( 0, -1, -1), vec3( 0,  1, -1)
	};
	
	float sampled = 0;
	const int samples = int(SHADOW_SAMPLES);
	// cubemap point light
	if ( light.typeMap == 1 ) {
		if ( samples < 1 ) {
			sampled = texture(samplerCubemaps[nonuniformEXT(light.indexMap)], D).r ;
		} else {
			for ( int i = 0; i < samples; ++i ) {
				const int idx = int( float(samples) * interleavedGradientNoise(surface.fragCoord.xy + i)) % samples;
				vec2 poisson = poissonDisk[idx] / 700.0;
				vec3 P = vec3( poisson.xy, (poisson.x + poisson.y) * 0.5 );
				sampled = texture(samplerCubemaps[nonuniformEXT(light.indexMap)], D + P ).r ;
				if ( eyeDepth < sampled - bias ) factor -= 1.0 / samples;
			}
			return factor;
		}
	// separated point lights
	} else if ( light.typeMap == 2 ) {
		const vec2 uv = positionClip.xy * 0.5 + 0.5;
		if ( samples < 1 ) {
			sampled = texture(samplerTextures[nonuniformEXT(light.indexMap + index)], uv).r ;
		} else {
			for ( int i = 0; i < samples; ++i ) {
				const int idx = int( float(samples) * interleavedGradientNoise(surface.fragCoord.xy + i)) % samples;
				sampled = texture(samplerTextures[nonuniformEXT(light.indexMap + index)], uv + poissonDisk[idx] / 700.0 ).r ;
				if ( eyeDepth < sampled - bias ) factor -= 1.0 / samples;
			}
			return factor;
		}
	}
	return eyeDepth < sampled - bias ? 0.0 : factor;
}
#endif
float shadowFactor( const Light light, float def ) {
#if RT
	return shadowFactorRT( light, def );
#endif
	if ( light.typeMap != 0 ) return omniShadowMap( light, def );
	
	if ( !validTextureIndex(light.indexMap) )
	#if VXGI
		return shadowFactorVXGI( light, def );
	#else
		return 1.0;
	#endif

	vec4 positionClip = light.projection * light.view * vec4(surface.position.world, 1.0);
	positionClip.xyz /= positionClip.w;

	if ( positionClip.x < -1 || positionClip.x >= 1 ) return def;
	if ( positionClip.y < -1 || positionClip.y >= 1 ) return def;
	if ( positionClip.z <= 0 || positionClip.z >= 1 ) return def;

	float factor = 1.0;

	// spot light
	if ( abs(light.type) == 2 || abs(light.type) == 3 ) {
		const float dist = length( positionClip.xy );
		if ( dist > 0.5 ) return def;
		
		// spot light with attenuation
		if ( abs(light.type) == 3 ) {
			factor = 1.0 - (pow(dist * 2,2.0));
		}
	}
	
	const vec2 uv = positionClip.xy * 0.5 + 0.5;
	const float bias = light.depthBias;
	const float eyeDepth = positionClip.z;
	const int samples = int(SHADOW_SAMPLES);
	const float invResolution = 1.0 / textureSize( samplerTextures[nonuniformEXT(light.indexMap)], 0 ).x;
	if ( samples < 1 ) return eyeDepth < texture(samplerTextures[nonuniformEXT(light.indexMap)], uv).r - bias ? 0.0 : factor;
	for ( int i = 0; i < samples; ++i ) {
		const int index = int( float(samples) * interleavedGradientNoise(surface.fragCoord.xy + i)) % samples;
		const float lightDepth = texture(samplerTextures[nonuniformEXT(light.indexMap)], uv + poissonDisk[index] * invResolution ).r;
		if ( eyeDepth < lightDepth - bias ) factor -= 1.0 / samples;
	}
	return factor;
}