// Full-resolution shadowed volumetric integration used by VOLUMETRICS=2.
// The inexpensive VOLUMETRICS=1 path remains in deferred.fsh unchanged.
#ifndef NOISE_VOLUMETRICS_GLSL
#define NOISE_VOLUMETRICS_GLSL

#include "/lib/noise_common.glsl"
#include "/lib/noise_options.glsl"
#include "/lib/noise_shadows.glsl"

const int NOISE_VOLUMETRIC_STEPS = 32;

float noiseVolumetricPhase(float cosineTheta, float anisotropy) {
	float g2 = anisotropy * anisotropy;
	float denominator = max(1.0 + g2 - 2.0 * anisotropy * cosineTheta, 0.025);
	// Normalized relative to an isotropic phase of 1 / 4pi. This keeps tuning
	// intuitive while retaining the strong forward lobe of real aerial media.
	return (1.0 - g2) / pow(denominator, 1.5);
}

float noiseVolumetricDensity(
	vec3 worldPosition,
	float daylight,
	float rainStrength,
	float frameTimeCounter
) {
	#ifdef NETHER
	float smoke = noiseNetherSmoke(worldPosition, frameTimeCounter);
	float lavaLayer = 1.0 - smoothstep(31.0, 66.0, worldPosition.y);
	float ceilingLayer = smoothstep(92.0, 126.0, worldPosition.y);
	return (0.0030 + smoke * 0.0022) *
		(1.0 + lavaLayer * 0.62 + ceilingLayer * 0.34);
	#elif defined(END)
	float lowerVoid = 1.0 - smoothstep(36.0, 74.0, worldPosition.y);
	float ribbon = sin(worldPosition.x * 0.010 - worldPosition.z * 0.013 +
		frameTimeCounter * 0.024) * 0.5 + 0.5;
	return 0.00115 + lowerVoid * mix(0.0011, 0.0024, ribbon);
	#else
	float nightFactor = noiseNight(daylight);
	float highAltitudeFade = exp(-max(worldPosition.y - 63.0, 0.0) * 0.050);
	float heightDensity = mix(0.24, 1.0, highAltitudeFade);
	float lowMist = exp(-abs(worldPosition.y - 63.0) * 0.072);
	float broadVariation = sin(worldPosition.x * 0.031 + worldPosition.z * 0.019 +
		frameTimeCounter * 0.020) * 0.5 + 0.5;
	float baseDensity = (0.00072 + rainStrength * 0.00215 +
		nightFactor * 0.00028) * heightDensity;
	return baseDensity + lowMist * mix(0.00025, 0.00105,
		broadVariation) * (1.0 + rainStrength * 1.35);
	#endif
}

vec3 noiseVolumetricIncidentColor(
	vec3 worldPosition,
	float daylight,
	float rainStrength,
	out float lightStrength,
	out float anisotropy
) {
	#ifdef NETHER
	float lavaLayer = 1.0 - smoothstep(30.0, 70.0, worldPosition.y);
	lightStrength = mix(0.32, 0.68, lavaLayer);
	anisotropy = 0.12;
	return mix(vec3(0.17, 0.018, 0.007), vec3(0.62, 0.065, 0.009),
		lavaLayer);
	#elif defined(END)
	lightStrength = 0.46;
	anisotropy = 0.48;
	return vec3(0.24, 0.20, 0.72);
	#else
	float nightFactor = noiseNight(daylight);
	lightStrength = mix(0.14, 0.74, daylight) *
		mix(1.0, 0.64, rainStrength);
	anisotropy = mix(0.42, 0.64, daylight);
	vec3 moonScatter = vec3(0.19, 0.28, 0.52);
	vec3 sunScatter = mix(vec3(1.00, 0.76, 0.47),
		vec3(0.72, 0.76, 0.82), rainStrength);
	return mix(moonScatter, sunScatter, daylight) *
		mix(0.72, 1.0, 1.0 - nightFactor);
	#endif
}

vec3 noiseVolumetricShadow(
	sampler2D shadowMapAll,
	sampler2D shadowMapOpaque,
	sampler2D shadowColorMap,
	mat4 lightProjection,
	mat4 lightModelView,
	vec3 playerPosition,
	vec2 screenPosition,
	int sampleIndex
) {
	#if NOISE_HAS_DIRECTIONAL_SHADOWS == 0
	return vec3(1.0);
	#else
	vec4 shadowClip = lightProjection * lightModelView *
		vec4(playerPosition, 1.0);
	if (shadowClip.w <= 1e-6) return vec3(1.0);
	vec3 shadowNdc = shadowClip.xyz / shadowClip.w;
	shadowNdc.xy = noiseWarpShadowPosition(shadowNdc.xy);
	shadowNdc.z *= NOISE_SHADOW_DEPTH_SCALE;
	vec3 shadowPosition = shadowNdc * 0.5 + 0.5;
	if (shadowPosition.z <= 0.0 || shadowPosition.z >= 1.0 ||
		any(lessThanEqual(shadowPosition.xy, vec2(0.0))) ||
		any(greaterThanEqual(shadowPosition.xy, vec2(1.0)))) {
		return vec3(1.0);
	}

	ivec2 mapDimensions = textureSize(shadowMapAll, 0);
	float texelSize = 1.0 / float(max(min(mapDimensions.x, mapDimensions.y), 1));
	// Keep the stochastic PCF phase fixed in screen space. Cycling it every
	// frame looked clean while stationary but boiled as soon as reprojection
	// lost confidence during camera movement.
	float randomValue = noiseHash(floor(screenPosition) + vec2(
		float(sampleIndex) * 17.17,
		float(sampleIndex) * 9.31));
	float randomAngle = randomValue * 6.28318530718;
	vec2 stochasticOffset = vec2(cos(randomAngle), sin(randomAngle)) *
		texelSize * mix(0.25, 1.05, randomValue);
	float receiverDepth = shadowPosition.z - 0.000055;
	return noiseShadowTransmission(shadowMapAll, shadowMapOpaque,
		shadowColorMap, shadowPosition.xy + stochasticOffset, receiverDepth);
	#endif
}

float noiseVolumetricCloudTransmission(
	vec3 worldPosition,
	vec3 sunDirectionWorld,
	float daylight,
	float rainStrength,
	float frameTimeCounter
) {
	#if defined(NETHER) || defined(END) || CLOUD_QUALITY == 0
	return 1.0;
	#else
	if (daylight <= 0.025 || sunDirectionWorld.y <= 0.025 ||
		worldPosition.y >= noiseCloudTop) return 1.0;
	float cloudProbeHeight = mix(noiseCloudBottom, noiseCloudTop, 0.46);
	float distanceToCloud = (cloudProbeHeight - worldPosition.y) /
		sunDirectionWorld.y;
	if (distanceToCloud <= 0.0 || distanceToCloud > 1800.0) return 1.0;
	vec3 cloudProbePosition = worldPosition +
		sunDirectionWorld * distanceToCloud;
	vec2 cloudWind = frameTimeCounter * vec2(0.00550, 0.00180);
	// This is a long-distance shadow probe; high-frequency shape erosion would
	// alias in screen-space volumetrics without changing the useful coverage.
	float cloudDensity = noiseCloudDensityLod(cloudProbePosition, cloudWind,
		rainStrength, 0.0);
	return exp(-cloudDensity * mix(1.65, 2.65, rainStrength));
	#endif
}

vec3 noiseIntegrateVolumetricLight(
	vec3 viewRayEnd,
	vec3 cameraWorldPosition,
	mat4 viewToPlayer,
	sampler2D shadowMapAll,
	sampler2D shadowMapOpaque,
	sampler2D shadowColorMap,
	mat4 lightProjection,
	mat4 lightModelView,
	vec3 viewSunPosition,
	vec3 viewShadowLightPosition,
	float maximumViewDistance,
	float daylight,
	float rainStrength,
	float frameTimeCounter,
	vec2 screenPosition
) {
	float rayLength = min(max(length(viewRayEnd) - 0.12, 0.0),
		min(maximumViewDistance, 256.0));
	if (rayLength <= 0.05) return vec3(0.0);

	vec3 viewDirection = noiseSafeNormalize(viewRayEnd, vec3(0.0, 0.0, -1.0));
	vec3 playerDirection = noiseSafeNormalize(mat3(viewToPlayer) * viewDirection,
		vec3(0.0, 0.0, -1.0));
	vec3 viewLightDirection = noiseSafeNormalize(
		mix(viewShadowLightPosition, viewSunPosition, daylight),
		vec3(0.0, 1.0, 0.0));
	vec3 sunDirectionWorld = noiseSafeNormalize(mat3(viewToPlayer) *
		viewSunPosition, vec3(0.0, 1.0, 0.0));
	float cosineTheta = clamp(dot(-viewDirection, viewLightDirection), -1.0, 1.0);
	float stepLength = rayLength / float(NOISE_VOLUMETRIC_STEPS);
	// A deterministic interleaved offset prevents visible step bands without
	// injecting new full-frame brightness noise during movement.
	float rayJitter = noiseHash(floor(screenPosition) * 0.754877666 +
		vec2(0.137, 0.719));
	vec3 accumulatedScattering = vec3(0.0);
	float transmittance = 1.0;
	float cachedCloudTransmission = 1.0;
	int cloudTransmissionSample = -2;

	for (int sampleIndex = 0; sampleIndex < NOISE_VOLUMETRIC_STEPS;
		++sampleIndex) {
		float sampleDistance = (float(sampleIndex) + rayJitter) * stepLength;
		vec3 playerPosition = playerDirection * sampleDistance;
		vec3 worldPosition = playerPosition + cameraWorldPosition;
		float density = noiseVolumetricDensity(worldPosition, daylight,
			rainStrength, frameTimeCounter);
		if (density <= 1e-6) continue;

		float lightStrength;
		float anisotropy;
		vec3 incidentColor = noiseVolumetricIncidentColor(worldPosition,
			daylight, rainStrength, lightStrength, anisotropy);
		float phase = noiseVolumetricPhase(cosineTheta, anisotropy);
		vec3 shadowTransmission = noiseVolumetricShadow(shadowMapAll,
			shadowMapOpaque, shadowColorMap, lightProjection, lightModelView,
			playerPosition, screenPosition, sampleIndex);
		// Cloud shadow coverage varies much more slowly than the 32-step aerial
		// integration. Refresh it every second occupied step; after an empty gap,
		// refresh immediately so a newly entered fog bank never inherits stale data.
		if (sampleIndex - cloudTransmissionSample >= 2) {
			cachedCloudTransmission = noiseVolumetricCloudTransmission(worldPosition,
				sunDirectionWorld, daylight, rainStrength, frameTimeCounter);
			cloudTransmissionSample = sampleIndex;
		}

		float stepTransmittance = exp(-density * stepLength);
		float scatteredFraction = 1.0 - stepTransmittance;
		vec3 sampleLighting = incidentColor * shadowTransmission *
			cachedCloudTransmission * lightStrength * (0.10 + phase * 0.90);
		accumulatedScattering += transmittance * scatteredFraction *
			sampleLighting;
		transmittance *= stepTransmittance;
		if (transmittance < 0.018) break;
	}

	return min(max(accumulatedScattering, vec3(0.0)), vec3(4.0));
}

#endif
