#if !defined END && !defined SEVEN && !defined NETHER
	vec3 CloudField(vec2 coord, vec2 wind) {
		vec2 p = coord * 2.2;
		float warpX = texture2D(noisetex, p * 0.095 + wind * 0.025).r;
		float warpY = texture2D(noisetex, p * 0.095 - wind * 0.018
								 + vec2(0.37, 0.71)).g;
		p += (vec2(warpX, warpY) - 0.5) * 1.15;
		float weather = texture2D(noisetex, p * 0.12 + wind * 0.045).r;
		float billow = texture2D(noisetex, p * 0.27 - wind * 0.07).r * 0.64;
		billow += texture2D(noisetex, p * 0.61 + wind * 0.11).g * 0.36;
		float detail = texture2D(noisetex, p * 1.35 - wind * 0.17).b;
		return vec3(weather, billow, detail);
	}

	float CloudDensity(vec2 coord, vec2 wind, float heightFraction,
					 float coverageBias) {
		vec3 field = CloudField(coord, wind);
		float cloudType = smoothstep(0.22, 0.82, field.x);
		float threshold = 0.54 + (CLOUD_AMOUNT - 10.0) * 0.024
						+ coverageBias * 0.012 - rainStrengthS * 0.075;
		float macroShape = field.x * 0.52 + field.y * 0.48;
		float base = smoothstep(threshold - 0.13, threshold + 0.11, macroShape);
		float topHeight = mix(0.58, 0.98, cloudType);
		float bottomProfile = smoothstep(0.015, 0.14, heightFraction);
		float topProfile = 1.0 - smoothstep(topHeight - 0.20,
										 topHeight, heightFraction);
		float profile = bottomProfile * topProfile;
		float edge = 1.0 - smoothstep(0.35, 0.88, base);
		float erosionStrength = mix(0.08, 0.25,
							 smoothstep(0.30, 1.0, heightFraction));
		float density = base * profile;
		density -= (0.56 - field.z) * edge * erosionStrength;
		density += rainStrengthS * profile * 0.16;
		return clamp(density, 0.0, 1.0);
	}

	vec4 DrawCloudExperimental(vec3 viewPos, float dither, vec3 lightCol, vec3 ambientCol, float NdotU, int sampleCount) {
		float cosS = dot(normalize(viewPos), sunVec);
		float sunHeight = dot(sunVec, upVec);
		float twilight = smoothstep(-0.14, -0.01, sunHeight)
						* (1.0 - smoothstep(0.06, 0.30, sunHeight))
						* (1.0 - rainStrengthS);
		float timeBrightnessS = sqrt1(timeBrightness);
		float colorMultiplier = CLOUD_BRIGHTNESS * (0.27 + 0.065 * timeBrightnessS);
		float opacityDensity = mix(0.58, 1.46, CLOUD_OPACITY);
		float phaseG = 0.55;
		float phaseDenom = max(1.0 + phaseG * phaseG - 2.0 * phaseG * cosS, 0.06);
		float forwardPhase = clamp((1.0 - phaseG * phaseG)
						/ pow(phaseDenom, 1.5) * 0.22, 0.0, 3.8);

		float cloudHeightFactor = max(1.11 - 0.0015 * eyeAltitude, 0.0);
		cloudHeightFactor *= cloudHeightFactor;
		float cloudHeight = CLOUD_HEIGHT * cloudHeightFactor * 0.5;

		#if !defined GBUFFERS_WATER && !defined DEFERRED
			float skytime = frametime;
		#else
			float skytime = cloudtime;
		#endif
		float cloudSpeedFactor = 0.003;
		vec2 wind = vec2(skytime * CLOUD_SPEED * cloudSpeedFactor, 0.0);
		#ifdef SEVEN
			wind *= 8;
		#endif

		vec3 cloudAccum = vec3(0.0);
		float transmittance = 1.0;
		float cloudAlpha = 0.0;
		float horizonFade = smoothstep(0.025, 0.105, NdotU);

		if (NdotU > 0.015) {
			vec3 wpos = normalize((gbufferModelViewInverse * vec4(viewPos, 1.0)).xyz);
			float rayY = max(wpos.y, 0.055);
			vec3 ambientLight = sqrt(max(ambientCol, vec3(0.00001)));
			vec3 directLight = sqrt(max(lightCol, vec3(0.00001)));
			vec3 twilightLight = mix(vec3(1.00, 0.42, 0.10),
									  vec3(1.00, 0.20, 0.055), mefade);
			directLight = mix(directLight, twilightLight,
							  twilight * (0.58 + 0.22 * max(cosS, 0.0)));
			vec3 weatherLight = normalize(max(weatherCol, vec3(0.001)))
							 * (0.16 + 0.16 * sunVisibility);

			for(int i = 0; i < sampleCount; i++) {
				if (transmittance < 0.012) break;
				float layerPosition = (float(i) + dither)
									/ max(float(sampleCount), 1.0);
				vec2 planeCoord = wpos.xz * ((cloudHeight + layerPosition * 13.5)
									/ rayY) * 0.00435;
				vec2 coord = cameraPosition.xz * 0.00010 + planeCoord;
				float angle = (float(i) + dither) * 2.39996323;
				coord += vec2(cos(angle), sin(angle)) * 0.0024;
				float coverageBias = (float(i) - float(sampleCount) * 0.42) * 0.28;
				float density = CloudDensity(coord, wind, layerPosition, coverageBias);
				float stepAlpha = 1.0 - exp(-density * CLOUD_THICKNESS
									* 1.55 * opacityDensity / max(float(sampleCount), 1.0));

				float probe = texture2D(noisetex,
						(coord + sunVec.xz * mix(0.075, 0.025, layerPosition))
						* 0.62 + wind * 0.055).r;
				float sunOcclusion = smoothstep(0.48, 0.74, probe)
								 * (0.55 + 0.95 * density);
				float sunTransmittance = exp(-sunOcclusion
								 * mix(4.2, 1.8, layerPosition));
				float verticalLight = smoothstep(0.02, 0.92, layerPosition);
				vec3 sampleColor = ambientLight * mix(0.22, 0.52, verticalLight);
				sampleColor += directLight * sunTransmittance
							 * (0.20 + 0.46 * verticalLight + 0.18 * forwardPhase);
				sampleColor += directLight * forwardPhase * sunTransmittance
							 * (1.0 - density) * 0.16;
				sampleColor = mix(sampleColor, weatherLight,
								 rainStrengthS * rainStrengthS * 0.82);

				cloudAccum += transmittance * stepAlpha * sampleColor;
				transmittance *= 1.0 - stepAlpha;
			}
			cloudAlpha = (1.0 - transmittance) * horizonFade;
		}

		vec3 cloudColor = cloudAccum / max(1.0 - transmittance, 0.0001);
		return vec4(cloudColor * colorMultiplier, cloudAlpha);
	}

	// Stable Complementary cloud geometry, retained as the production path.
	// Event Horizon only extends its lighting at twilight; the proven shape,
	// coverage and horizon projection remain unchanged.
	float CloudNoise(vec2 coord, vec2 wind) {
		// Keep every octave in the same world-space projection. Rotating and
		// recursively warping these coordinates creates radial, vein-like streaks
		// when the camera looks toward the zenith.
		float noise = texture2D(noisetex, coord * 0.125 + wind * 0.25).x * 7.0;
		noise += texture2D(noisetex, coord * 0.0625 + wind * 0.15).x * 12.0;
		noise += texture2D(noisetex, coord * 0.03125 + wind * 0.05).x * 12.0;
		noise += texture2D(noisetex, coord * 0.015625 + wind * 0.05).x * 24.0;
		return noise * 0.34;
	}

	float CloudCoverage(float noise, float coverage, float NdotU, float cosS) {
		float noiseCoverageCosS = abs(cosS);
		noiseCoverageCosS *= noiseCoverageCosS;
		noiseCoverageCosS *= noiseCoverageCosS;
		// Keep coverage present across the dome. The old 0.365 factor cleared the
		// zenith and concentrated nearly all detail in a stretched horizon ring.
		float NdotUmult = CLOUD_ZENITH_COVERAGE;
		#ifdef AURORA
			float auroraMult = max(1.0 - sunVisibility - rainStrengthS, 0.0);
			#ifdef AURORA_BIOME_CHECK
				auroraMult *= isSnowy;
			#endif
			#ifdef AURORA_FULL_MOON_CHECK
				auroraMult *= float(moonPhase == 0);
			#endif
			NdotUmult *= 1.0 + 2.5 * auroraMult;
		#endif
		float noiseCoverage = coverage * coverage + CLOUD_AMOUNT
								* (1.0 + noiseCoverageCosS * CLOUD_HORIZON_COVERAGE)
								* (1.0 + NdotU * NdotUmult * (1.0 - rainStrengthS * 3.0))
								- 2.5;
		return max(noise - noiseCoverage, 0.0);
	}

	vec4 DrawCloud(vec3 viewPos, float dither, vec3 lightCol, vec3 ambientCol,
					 float NdotU, int sampleCount) {
		float cosS = dot(normalize(viewPos), sunVec);
		#if AA > 1
			dither = fract(16.0 * frameTimeCounter + dither);
		#else
			// A fixed midpoint is stable without temporal accumulation. Leaving the
			// Bayer pattern visible under FXAA turns thin cloud edges into filaments.
			dither = 0.5;
		#endif

		float timeBrightnessS = sqrt1(timeBrightness);
		float cloud = 0.0;
		float cloudGradient = 0.0;
		float gradientMix = dither * 0.1667;
		float colorMultiplier = CLOUD_BRIGHTNESS * (0.23 + 0.07 * timeBrightnessS);
		float noiseMultiplier = CLOUD_THICKNESS * 0.125;
		float scattering = (1.20 * pow(max(cosS, 0.0), 8.0)
							 + 0.08 * pow(max(-cosS, 0.0), 4.0))
							 * (1.0 - rainStrengthS);

		float cloudHeightFactor = max(1.11 - 0.0015 * eyeAltitude, 0.0);
		cloudHeightFactor *= cloudHeightFactor;
		float cloudHeight = CLOUD_HEIGHT * cloudHeightFactor * 0.5;
		#if !defined GBUFFERS_WATER && !defined DEFERRED
			float skytime = frametime;
		#else
			float skytime = cloudtime;
		#endif
		vec2 wind = vec2(skytime * CLOUD_SPEED * 0.003, 0.0);
		vec3 cloudColor = vec3(0.0);

		if (NdotU > 0.025) {
			vec3 wpos = normalize((gbufferModelViewInverse * vec4(viewPos, 1.0)).xyz);
			// Soft hemispherical projection: behaves like the original cloud plane
			// overhead, but limits its singular perspective close to the horizon.
			// The smooth square-root guard has no hard band or radial explosion.
			float cloudProjectionY = sqrt(wpos.y * wpos.y
									+ CLOUD_HORIZON_STABILITY * CLOUD_HORIZON_STABILITY);
			for (int i = 0; i < sampleCount; i++) {
				if (cloud > 0.99) break;
				vec2 planeCoord = wpos.xz * ((cloudHeight
						+ (float(i) + dither) * 2.5 * 6.0 / float(sampleCount))
						/ cloudProjectionY) * 0.0085;
				vec2 coord = cameraPosition.xz * 0.00025 + planeCoord;
				float ang1 = (float(i) + frametime * 0.025) * 2.391;
				float ang2 = ang1 + 2.391;
				coord += mix(vec2(cos(ang1), sin(ang1)),
							 vec2(cos(ang2), sin(ang2)),
							 dither * 0.25 + 0.75) * 0.009375;
				float coverage = (float(i) - 3.0 + dither) * 0.725;
				float noise = CloudCoverage(CloudNoise(coord, wind), coverage,
										 NdotU, cosS) * noiseMultiplier;
				noise = noise / sqrt(noise * noise + 1.0);
				cloudGradient = mix(cloudGradient,
						mix(gradientMix * gradientMix, 1.0 - noise, 0.25),
						noise * (1.0 - cloud));
				cloud += max(noise - cloud * 0.95, 0.0);
				cloud = mix(cloud, 1.0, rainStrengthS * pow2(noise * noise));
				gradientMix += 0.2 * (6.0 / float(sampleCount));
			}

			float meFactorP = min((1.0 - min(moonBrightness, 0.6) / 0.6)
								* 0.115, 0.075);
			vec3 meColor = vec3(0.0);
			if (cosS > 0.0) {
				float meNdotU = 1.0 - NdotU;
				float meFactor = meFactorP * meNdotU * meNdotU * 12.0
								 * (1.0 - rainStrengthS);
				meColor = mix(lightMorning, lightEvening, mefade);
				meColor *= meColor;
				meColor *= meColor;
				meColor *= meFactor * meFactor * cosS;
			}

			float sunVisibilityM = pow(sunVisibility, 4.0 - meFactorP * 24.0);
			vec3 skyColor2 = skyColor * skyColor;
			vec3 cloudNightColor = ambientCol * 8.0;
			vec3 cloudDayColor = pow(lightCol, vec3(1.5)) * 1.5;
			vec3 cloudUpColor = mix(cloudNightColor, cloudDayColor, sunVisibilityM);
			cloudUpColor *= 1.0 + scattering;
			cloudUpColor += max(meColor, vec3(0.0));

			float sunHeight = dot(sunVec, upVec);
			float twilight = smoothstep(-0.14, -0.01, sunHeight)
							 * (1.0 - smoothstep(0.06, 0.30, sunHeight))
							 * (1.0 - rainStrengthS);
			vec3 twilightColor = mix(vec3(1.00, 0.42, 0.10),
									 vec3(1.00, 0.20, 0.055), mefade);
			cloudUpColor += twilightColor * twilight
							 * (0.10 + 0.55 * pow(max(cosS, 0.0), 6.0))
							 * (0.35 + 0.65 * cloudGradient);

			vec3 cloudDownColor = skyColor2 * 0.225 * sunVisibility * skyMult;
			vec3 weatherSky = weatherCol * weatherCol;
			weatherSky *= GetLuminance(ambientCol / weatherSky) * 1.4;
			weatherSky *= mix(SKY_RAIN_NIGHT, SKY_RAIN_DAY, sunVisibility);
			weatherSky = max(weatherSky, skyColor2 * 0.75);
			weatherSky *= rainStrengthS;
			#ifdef LIGHT_SHAFTS
				weatherSky *= 12.5 + scattering * 47.5 * (1.0 + sunVisibility);
			#else
				weatherSky *= 12.5 + scattering * 47.5;
			#endif
			cloudUpColor = mix(cloudUpColor, weatherSky,
							   rainStrengthS * rainStrengthS);
			cloudColor = mix(cloudDownColor, cloudUpColor, cloudGradient);
			cloudColor *= mix(0.86, 1.0, smoothstep(0.08, 0.78, cloudGradient));
			cloud *= pow2(pow2(1.0 - exp(-(10.0 - 8.2 * rainStrengthS) * NdotU)));
		}
		return vec4(cloudColor * colorMultiplier, cloud * CLOUD_OPACITY);
	}

	#ifdef AURORA
		float AuroraNoise(vec2 coord, vec2 wind) {
			float noise = texture2D(noisetex, coord * 0.175   + wind * 0.25).x;
				  noise+= texture2D(noisetex, coord * 0.04375 + wind * 0.15).x * 5.0;

			return noise;
		}

		vec3 DrawAurora(vec3 viewPos, float dither, int sampleCount, float NdotU) {
			#if AA > 1
				dither = fract(16.0 * frameTimeCounter + dither);
			#endif
			
			float gradientMix = dither / sampleCount;
			float visibility = (1.0 - sunVisibility) * (1.0 - rainStrengthS);
			visibility *= visibility;

			#ifdef AURORA_BIOME_CHECK
				visibility *= isSnowy;
			#endif
			#ifdef AURORA_FULL_MOON_CHECK
				visibility *= float(moonPhase == 0);
			#endif

			#if !defined GBUFFERS_WATER && !defined DEFERRED
				float skytime = frametime;
			#else
				float skytime = cloudtime;
			#endif

			vec2 wind = vec2(skytime * 0.00005);

			vec3 aurora = vec3(0.0);

			float NdotUM = min(1.08 - NdotU, 1.0);
			NdotUM *= NdotUM;
			NdotUM = 1.0 - NdotUM * NdotUM;		

			if (NdotU > 0.0 && visibility > 0.0) {
				vec3 wpos = normalize((gbufferModelViewInverse * vec4(viewPos, 1.0)).xyz);
				for(int i = 0; i < sampleCount; i++) {
					vec2 planeCoord = wpos.xz * ((8.0 * AURORA_HEIGHT + (i + dither) * 7.0 / sampleCount) / wpos.y) * 0.004;
					vec2 coord = cameraPosition.xz * 0.00001 + planeCoord;

					float noise = AuroraNoise(coord, wind);
						noise = max(1.0 - 1.5 / (1.0 - NdotU * 0.8) * abs(noise - 3.0), 0.0);

					if (noise > 0.0) {
						noise *= texture2D(noisetex, coord * 0.25 + wind * 0.25).x;
						noise *= 0.5 * texture2D(noisetex, coord + wind * 16.0).x + 0.75;
						noise = noise * noise * 3.0 / sampleCount;
						noise *= NdotUM;

						vec3 auroracolor = mix(
										auroraDCol,
										auroraUCol,
										pow(gradientMix, 0.4));

						aurora += noise * auroracolor * exp2(-6.0 * i / sampleCount);
					}
					gradientMix += 1.0 / sampleCount;
				}
			}

			aurora = aurora * visibility * 1.5;

			return aurora;
		}
	#endif
#endif

#ifdef SEVEN
	float GetNoise(vec2 pos) {
		return fract(sin(dot(pos, vec2(12.9898, 4.1414))) * 43758.54953);
	}

	vec3 DrawStars(inout vec3 color, vec3 viewPos, float NdotU) {
		vec3 wpos = vec3(gbufferModelViewInverse * vec4(viewPos, 1.0));
		vec3 planeCoord = 0.75 * wpos / (wpos.y + length(wpos.xz));
		vec2 wind = 0.75 * vec2(frametime, 0.0);
		#ifdef SEVEN
			wind = vec2(0.0);
		#endif
		vec2 coord = planeCoord.xz * 0.5 + wind * 0.00125;
		coord = floor(coord*1024.0) / 1024.0;
		
		float multiplier = 5.0 * (1.0 - rainStrengthS) * (1 - (sunVisibility*0.9 + pow(timeBrightness, 0.05)*0.1)) * pow(NdotU, 2.0);
		
		#ifdef SEVEN
			multiplier = sqrt2(NdotU) * 5.0 * (1.0 - rainStrengthS);	
		#endif
		
		float star = 1.0;
		if (NdotU > 0.0) {
			star *= GetNoise(coord.xy);
			star *= GetNoise(coord.xy+0.1);
			star *= GetNoise(coord.xy+0.23);
		}
		star = max(star - 0.825, 0.0) * multiplier;
		
		vec3 stars = star * lightNight * lightNight * 160;

		return vec3(stars);
	}
#endif

#if defined END && defined ENDER_NEBULA
	float GetNebulaStarNoise(vec2 pos) {
		return fract(sin(dot(pos, vec2(12.9898, 4.1414))) * 43758.54953);
	}

	vec2 EndOctahedralMap(vec3 direction) {
		vec3 n = normalize(direction);
		n /= max(abs(n.x) + abs(n.y) + abs(n.z), 0.0001);
		vec2 uv = n.xz;
		if (n.y < 0.0) {
			vec2 signUv = step(vec2(0.0), uv) * 2.0 - 1.0;
			uv = (1.0 - abs(uv.yx)) * signUv;
		}
		return uv;
	}

	vec3 DrawEndStarLayer(vec3 direction, float scale, float threshold,
							 float radius, float animationTime) {
		vec2 p = (EndOctahedralMap(direction) * 0.5 + 0.5) * scale;
		vec2 cell = floor(p);
		vec2 local = fract(p) - 0.5;
		float seed = GetNebulaStarNoise(cell + vec2(scale * 0.017, 0.37));
		if (seed < threshold) return vec3(0.0);

		vec2 offset = vec2(GetNebulaStarNoise(cell + vec2(2.7, 7.1)),
						   GetNebulaStarNoise(cell + vec2(13.3, 19.7))) - 0.5;
		offset *= 0.58;
		float distanceToStar = length(local - offset);
		float footprint = clamp(max(length(dFdx(p)), length(dFdy(p))) * 0.12,
								0.012, 0.16);
		float core = 1.0 - smoothstep(radius, radius + footprint, distanceToStar);
		float glow = exp(-distanceToStar * distanceToStar * 42.0) * 0.055;
		float energy = pow((seed - threshold) / max(1.0 - threshold, 0.0001), 1.4);

		float temperature = GetNebulaStarNoise(cell + vec2(23.9, 31.1));
		vec3 warm = vec3(1.02, 0.92, 0.83);
		vec3 cool = vec3(0.88, 0.94, 1.05);
		vec3 starColor = mix(warm, cool, temperature);
		starColor = mix(starColor, vec3(0.98), 0.80);
		float twinkle = 0.95 + 0.05 * sin(animationTime * (0.35 + temperature)
									 + seed * 29.0);
		return starColor * (core * 1.18 + glow) * energy * twinkle;
	}

	float EndHash31(vec3 p) {
		p = fract(p * 0.1031);
		p += dot(p, p.yzx + 33.33);
		return fract((p.x + p.y) * p.z);
	}

	float EndNebulaNoise3D(vec3 p) {
		vec3 cell = floor(p) + vec3(19.0, 43.0, 11.0);
		vec3 local = fract(p);
		local = local * local * local
				* (local * (local * 6.0 - 15.0) + 10.0);
		float n000 = EndHash31(cell + vec3(0.0, 0.0, 0.0));
		float n100 = EndHash31(cell + vec3(1.0, 0.0, 0.0));
		float n010 = EndHash31(cell + vec3(0.0, 1.0, 0.0));
		float n110 = EndHash31(cell + vec3(1.0, 1.0, 0.0));
		float n001 = EndHash31(cell + vec3(0.0, 0.0, 1.0));
		float n101 = EndHash31(cell + vec3(1.0, 0.0, 1.0));
		float n011 = EndHash31(cell + vec3(0.0, 1.0, 1.0));
		float n111 = EndHash31(cell + vec3(1.0, 1.0, 1.0));
		float lower = mix(mix(n000, n100, local.x),
						  mix(n010, n110, local.x), local.y);
		float upper = mix(mix(n001, n101, local.x),
						  mix(n011, n111, local.x), local.y);
		return mix(lower, upper, local.z);
	}

	float EndAuroraCurtain(float latitude, float azimuth, float center,
						   float width, float phase, float warp, float animationTime) {
		// Each curtain follows a slowly travelling, closed wave on the sky sphere.
		// Integer azimuth frequencies keep both sides of the spherical seam aligned.
		float wave = sin(azimuth * 2.0 + animationTime * 0.31 + phase) * 0.055;
		wave += sin(azimuth * 5.0 - animationTime * 0.17 + phase * 1.7) * 0.022;
		wave += warp * 0.070;

		float ribbonDistance = latitude - center - wave;
		float edgeAA = clamp(fwidth(ribbonDistance) * 1.35,
							 0.00075, width * 0.35);
		float edgeWidth = width + edgeAA;
		float brightEdge = exp(-ribbonDistance * ribbonDistance
							 / max(edgeWidth * edgeWidth, 0.0001));

		// A one-sided, fading veil turns the bright arc into a real auroral curtain.
		float veilDistance = max(ribbonDistance, 0.0);
		float veil = smoothstep(-width * 1.45 - edgeAA,
							 edgeAA, ribbonDistance);
		veil *= exp(-veilDistance / max(width * 4.6, 0.001));

		// Fine vertical folds move independently inside the three broad ribbons.
		float foldA = 0.5 + 0.5 * sin(azimuth * 37.0
							 + animationTime * 0.93 + phase * 5.3 + warp * 17.0);
		float foldB = 0.5 + 0.5 * sin(azimuth * 61.0
							 - animationTime * 0.57 - phase * 3.1 + warp * 9.0);
		foldA = pow(foldA, 7.0);
		foldB = pow(foldB, 11.0);
		float folds = clamp(0.28 + foldA * 0.58 + foldB * 0.42, 0.0, 1.0);
		float arcVariation = 0.64 + 0.36 * (0.5 + 0.5
							 * sin(azimuth * 3.0 - animationTime * 0.11 + phase));

		return (brightEdge * (0.52 + 0.48 * folds)
				+ veil * (0.055 + 0.16 * folds)) * arcVariation;
	}

	vec3 DrawEndAurora(vec3 direction, float skytime) {
		vec3 rayDirection = normalize(direction);
		vec3 auroraAxis = normalize(vec3(-0.26, 0.91, 0.32));
		vec3 axisRight = normalize(cross(vec3(0.0, 1.0, 0.0), auroraAxis));
		vec3 axisForward = normalize(cross(auroraAxis, axisRight));

		// Mirror the curtains across the auroral equator so a violet structure is
		// present in every viewing direction instead of living on one hidden side.
		float latitude = abs(dot(rayDirection, auroraAxis));
		float azimuth = atan(dot(rayDirection, axisForward),
							 dot(rayDirection, axisRight));
		float animationTime = skytime * 0.018 * NEBULA_SPEED;

		// Seamless continuous 3D noise bends the curtains without filling the dark sky.
		vec3 driftA = vec3(animationTime * 0.031, -animationTime * 0.019,
						   animationTime * 0.024);
		vec3 driftB = vec3(-animationTime * 0.017, animationTime * 0.013,
						   animationTime * 0.029);
		float warpA = EndNebulaNoise3D(rayDirection * 1.55 + driftA) - 0.5;
		float warpB = EndNebulaNoise3D(rayDirection * 3.70 + driftB
										 + vec3(2.7, 5.1, 1.3)) - 0.5;
		float warp = warpA * 0.72 + warpB * 0.28;

		float curtainA = EndAuroraCurtain(latitude, azimuth, 0.15, 0.060,
										 0.4, warp, animationTime);
		float curtainB = EndAuroraCurtain(latitude, azimuth, 0.39, 0.054,
										 2.3, -warp * 0.82, animationTime * 0.91);
		float curtainC = EndAuroraCurtain(latitude, azimuth, 0.65, 0.045,
										 4.7, warp * 0.64, animationTime * 1.08);

		// The outer pole fade prevents a luminous cap: only the curtains emit light.
		float poleFade = 1.0 - smoothstep(0.88, 0.985, latitude);
		float shimmer = 0.93 + 0.07 * sin(animationTime * 0.73
									 + azimuth * 7.0 + warp * 6.0);
		vec3 violetLow = vec3(0.0280, 0.0050, 0.0650);
		vec3 violetMain = vec3(0.0550, 0.0090, 0.1050);
		vec3 violetHigh = vec3(0.0200, 0.0180, 0.0580);

		vec3 aurora = violetLow * curtainA
					 + violetMain * curtainB
					 + violetHigh * curtainC;
		return aurora * poleFade * shimmer;
	}

	vec3 DrawEnderNebula(vec3 viewPos, float dither, vec3 lightCol, inout vec3 nebulaStars) {
		float NdotU = dot(normalize(viewPos), upVec);

		bool dragonBattle = gl_Fog.start / far < 0.5;

		#if !defined GBUFFERS_WATER && !defined DEFERRED
			float skytime = frametime;
		#else
			float skytime = cloudtime;
		#endif

		vec3 wpos = normalize((gbufferModelViewInverse * vec4(viewPos * 1000.0, 1.0)).xyz);
		float flow = skytime * 0.000012 * NEBULA_SPEED;
		float nebulaScale = 0.82 + NEBULA_SIZE * 21.0;
		vec3 animatedDirection = wpos * nebulaScale
							 + vec3(flow, -flow * 0.37, flow * 0.61);
		float noiseA = EndNebulaNoise3D(animatedDirection * 0.78);
		float noiseB = EndNebulaNoise3D(animatedDirection * 2.15
									 + vec3(2.3, 4.7, 1.1));
		float noiseC = EndNebulaNoise3D(animatedDirection * 5.80
									 + vec3(6.1, 0.9, 3.8));
		float structure = noiseA * 0.54 + noiseB * 0.31 + noiseC * 0.15;

		vec3 nebulaAxis = normalize(vec3(0.43, 0.79, -0.37));
		float latitude = abs(dot(wpos, nebulaAxis));
		float distribution = clamp(NEBULA_DISTRIBUTION / 4.0, 0.0, 1.0);
		float bandWidth = mix(0.52, 0.20, distribution);
		float broadBand = exp(-pow(latitude / bandWidth,
									1.35 + max(NEBULA_COMPRESSION, 0.0)));
		float coreBand = exp(-pow(latitude / max(bandWidth * 0.36, 0.035), 1.45));
		float smoothing = mix(0.045, 0.14, NEBULA_SMOOTHING);
		float wisps = smoothstep(0.38 - smoothing, 0.64 + smoothing, structure);
		float darkLane = smoothstep(0.54, 0.76, noiseB) * coreBand;

		vec3 violetWisp = vec3(0.0115, 0.0062, 0.0210)
						 * NEBULA_PURPLE_BRIGHTNESS;
		vec3 blueWisp = vec3(0.0048, 0.0130, 0.0195)
						 * NEBULA_PURPLE_BRIGHTNESS;
		vec3 warmDust = vec3(0.0130, 0.0038, 0.0015)
						 * NEBULA_ORANGE_BRIGHTNESS;
		vec3 nebula = mix(violetWisp, blueWisp, noiseA)
					 * wisps * broadBand * (0.34 + 0.66 * noiseB);
		nebula += warmDust * pow(noiseC, 5.0) * coreBand * 0.24;
		nebula *= 1.0 - darkLane * 0.88;
		nebula += vec3(0.00042, 0.00068, 0.00125) * broadBand * 0.58;
		vec3 endAurora = DrawEndAurora(wpos, skytime);

		vec3 endStarsA = DrawEndStarLayer(wpos, 190.0, 0.9835, 0.090,
										frameTimeCounter * 0.28);
		vec3 endStarsB = DrawEndStarLayer(wpos, 430.0, 0.9930, 0.072,
										frameTimeCounter * 0.41);
		float skyDistribution = 0.82 + 0.18 * (1.0 - broadBand);
		vec3 endAtmosphereTint = mix(vec3(1.0),
				normalize(max(lightCol, vec3(0.001))), 0.045);
		nebulaStars = (endStarsA * 0.86 + endStarsB * 0.64)
					* endAtmosphereTint * 1.48 * skyDistribution
					* NEBULA_STAR_BRIGHTNESS;

		if (dragonBattle) {
			nebulaStars *= vec3(1.03, 0.99, 0.94) * 0.82;
			nebula *= vec3(1.12, 0.92, 0.86) * 0.82;
			endAurora *= 0.82;
		}

		return nebula + endAurora;
	}
#endif

#if defined NETHER && defined NETHER_SMOKE
	float SmokeNoise(vec2 coord, vec2 wind) {
		float noise = texture2D(noisetex, coord * 0.175   + wind * 0.25).x;
				noise+= texture2D(noisetex, coord * 0.04375 + wind * 0.15).x * 5.0;

		return noise;
	}

	vec3 DrawNetherSmoke(vec3 viewPos, float dither, vec3 lightCol) {
		float NdotU = dot(normalize(viewPos), upVec);

		#if AA > 1
			dither = fract(16.0 * frameTimeCounter + dither);
		#endif

		int sampleCount = 20;
		
		float gradientMix = dither / sampleCount;

		#if !defined GBUFFERS_WATER && !defined DEFERRED
			float skytime = frametime;
		#else
			float skytime = cloudtime;
		#endif

		vec2 wind = vec2(skytime * 0.00005);

		vec3 smoke = vec3(0.0);

		float NdotUM = abs(NdotU);
		NdotUM = 1.0 - NdotUM;

		vec3 wpos = normalize((gbufferModelViewInverse * vec4(viewPos, 1.0)).xyz);
		for(int i = 0; i < sampleCount; i++) {
			vec2 planeCoord = wpos.xz * (1.0 + (i + dither) * 6.0 / sampleCount) * 0.03;
			vec2 coord = planeCoord + cameraPosition.xz * 0.0017;

			float noise = SmokeNoise(coord, wind);
					noise = max(0.75 - 1.0 / abs(noise - 6.0), 0.0) * 3.0;

			if (noise > 0.0) {
				noise *= texture2D(noisetex, abs(coord * 0.25) + wind * 8.0).x;
				float heightNoise = wpos.y;
				float fireNoise = texture2D(noisetex, abs(coord * 0.2) + (heightNoise + cameraPosition.y * 0.01) * 0.01 + wind * -4.0).x;
				noise = noise * noise * 3.0 / sampleCount;
				noise *= NdotUM;

				vec3 smokeColor = pow(lightCol, vec3(0.6, 0.5, 0.6)) * 12.0 * pow(fireNoise, 5.0);
				smokeColor *= gradientMix;

				smoke += noise * smokeColor * exp2(-6.0 * i / sampleCount);
			}
			gradientMix += 1.0 / sampleCount;
		}

		return smoke * 2.0;
	}
#endif
