Files
filament/docs/wip/sky/simulated_skybox.mat
Filament Bot 99816d67c2 [automated] Updating /docs due to commit d6d4f92
Full commit hash is d6d4f92922

DOCS_ALLOW_DIRECT_EDITS
2026-02-19 20:03:46 +00:00

1285 lines
58 KiB
Plaintext

material {
name : SimulatedSkybox,
parameters : [
{
type : float3,
name : sunDirection,
precision : high
},
{
type : float3,
name : sunDirection2,
precision : high
},
{
type : float3,
name : depthR,
precision : high
},
{
type : float3,
name : depthM,
precision : high
},
{
type : float2,
name : miePhaseParams, // x=(1+g^2), y=(-2*g)
precision : high
},
{
type : float,
name : sunIntensity,
precision : high
},
{
type : float,
name : contrast,
precision : high
},
{
type : float3,
name : nightColor,
precision : high
},
{
type : float3,
name : ozone,
precision : high
},
{
type : float,
name : eclipseFactor,
precision : high
},
{
type : float4,
name : multiScatParams, // xyz=MultiScatteringColor, w=HorizonGlow
precision : high
},
{
type : float4,
name : sunHalo, // x=Size, y=Limb, z=Intensity, w=Enabled
precision : high
},
{
type : float4,
name : shimmerControl, // x=Strength, y=Frequency, z=MaskHeight, w=PlanetRadius
precision : high
},
{
type : float4,
name : cloudControl, // x=Coverage, y=Density, z=QuadraticConst, w=WindSpeed
precision : high
},
{
type : float4,
name : cloudControl2, // x=EvolutionSpeed
precision : high
},
{
type : float,
name : sunIntensity2,
precision : high
},
{
type : float4,
name : sunHalo2, // x=Size, y=Limb, z=Intensity, w=Enabled
precision : high
},
{
type : float4,
name : waterControl, // x=Strength, y=Speed, z=DerivativeTrick, w=Octaves
precision : high
},
{
type : float4,
name : starControl, // x=Density, y=Enabled, z=Frequency, w=PixelScale
precision : high
},
{
type : float,
name : starIntensity,
precision : high
},
{
type : sampler2d,
name : moonTexture
},
{
type : float,
name : exposure,
precision : high
},
{
type : sampler2d,
name : moonNormal
},
{
type : sampler2d,
name : milkyWayTexture
},
{
type : float3,
name : milkyWayControl, // x=Intensity, y=Saturation, z=Unused
precision : high
},
{
type : mat3,
name : milkyWayRotation,
precision : high
}
],
variables : [
{
name : "eyeDirection",
precision : "high"
}
],
vertexDomain : device,
depthWrite : false,
shadingModel : unlit,
culling: none
}
vertex {
void materialVertex(inout MaterialVertexInputs material) {
// This code is taken from computeWorldPosition and assumes the vertex domain is 'device'.
highp vec4 p = getPosition();
// GL convention to inverted DX convention
p.z = p.z * -0.5 + 0.5;
highp vec4 worldPosition = getWorldFromClipMatrix() * p;
// Getting the true world position would require dividing by w, but since this is a skybox
// at inifinity, this results in very large numbers for material.eyeDirection.
// Since the eyeDirection is only used as a direction vector in the fragment shader, we can
// skip that step to improve precision.
material.eyeDirection.xyz = worldPosition.xyz;
}
}
fragment {
// ------------------------------------------------------------------------
// Analytic Rayleigh and Mie Scattering (Physics Based)
// Derived from:
// - Hoffman & Preetham (2002): "Real-time Light-Atmosphere Interactions"
// - Henyey & Greenstein (1941): "Diffuse radiation in the galaxy" (Mie Phase)
// - Kasten & Young (1989): "Revised optical air mass tables" (Air Mass)
// - "Simulated Sky" / Three.js (Sky.js): Empirical adjustments for aesthetics
// ------------------------------------------------------------------------
#define PI 3.14159265359
// --- CONFIGURATION ---
// Stars
#define STAR_GLOBAL_INTENSITY 100.0 // Master brightness multiplier [0.0 - 500.0]
#define STAR_BRIGHTNESS_BASE 0.01 // Minimum random brightness [0.0 - 1.0]
#define STAR_BRIGHTNESS_VAR 15.0 // Random brightness variance range [0.0 - 10.0]
#define STAR_FADE_SUN_ELV_HIGH 0.10 // Sun elevation (sin) where stars are fully hidden [0.0 - 0.5]
#define STAR_FADE_SUN_ELV_LOW -0.20 // Sun elevation (sin) where stars are fully visible [-0.5 - 0.0]
#define STAR_CLOUD_OCCLUSION 0.1 // Visibility when covered by clouds [0.0 - 1.0]
// Clouds
#define CLOUD_UV_SCALE 0.05 // Texture scale in km^-1 [0.01 - 0.1]
#define CLOUD_EXTINCTION 20.0 // Beer's law coefficient (Opacity) [1.0 - 50.0]
#define CLOUD_SILVER_INTENSITY 40.0 // Silver lining brightness boost [0.0 - 100.0]
#define CLOUD_SILVER_G 0.9 // Silver lining anisotropy (0.0=iso, 1.0=forward)
#define CLOUD_AMBIENT_MIN 0.1 // Minimum ambient light in thick clouds [0.0 - 0.5]
#define VOLUMETRIC_SHADOW_STR 0.7 // Center darkening strength for "volume" [0.0 - 1.0]
// Water
#define WATER_PLANE_HEIGHT -10.0 // Virtual plane height relative to camera [meters]
#define WATER_UV_SCALE 0.05 // Texture scale for waves [0.01 - 0.2]
#define WATER_FRESNEL_F0 0.02 // Base reflectivity (Water is ~0.02) [0.0 - 1.0]
#define WATER_DEEP_COLOR vec3(0.0, 0.005, 0.02) // Deep water absorption color [RGB]
#define REFLECTION_HORIZON_FADE 0.1 // Normalized height to fade star reflections [0.0 - 0.5]
// Moon
#define MOON_EARTHSHINE 0.002 // Dark side brightness scalar [0.0 - 0.01]
#define MOON_LIMB_SOFTNESS 0.05 // Edge softness relative to radius [0.0 - 0.1]
// Atmosphere
#define HORIZON_GLOW_POWER 5.0 // Horizon glow falloff curve [1.0 - 10.0]
void dummy() {} // squash editor syntax highlighting bugs
// Rayleigh Phase Function: Scattering distribution for small particles (air molecules)
// Lord Rayleigh (1871)
// Normalized to integrate to 4*PI (Boosting brightness by factor PI vs standard 1-normalization)
float rayleighPhase(float cosTheta) {
const float THREE_SIXTEENTH = (3.0 / 16.0);
return THREE_SIXTEENTH * (1.0 + cosTheta * cosTheta);
}
// Henyey-Greenstein Phase Function (Mie)
// Henyey & Greenstein (1941)
// Controls the forward scattering peak (sun halo) via anisotropy parameter 'g'
// Optimized: params.x = (1 + g^2), params.y = (-2 * g)
float hgPhase(float cosTheta, vec2 params) {
const float ONE_FOURTH = (1.0 / 4.0);
// Recover (1 - g^2) => 2.0 - (1 + g^2)
float oneMinusG2 = 2.0 - params.x;
float inverse = 1.0 / pow(params.x + params.y * cosTheta, 1.5);
return ONE_FOURTH * (oneMinusG2 * inverse);
}
// --- Noise Functions for Clouds ---
highp float hash13(highp vec3 p3) {
p3 = fract(p3 * .1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
// Safe mod for negative values
// Why 289?
// 1. Historical: 17*17 = 289. Used in identifying permutations in standard GLSL noise (Ashima/stegu).
// 2. Precision: Small enough to prevent float precision loss during internal calculations (e.g. squaring).
// 3. Quality: Large enough to hide repetition and coprime to standard powers-of-two (like 256), avoiding grid resonance.
highp vec3 mod289(highp vec3 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
highp float noise(highp vec3 p) {
// Wrap input to [0, 289] to preserve precision of fract() when p is large.
// This fixes grid artifacts on mobile caused by large world coordinates.
p = mod289(p);
highp vec3 i = floor(p);
highp vec3 f = fract(p);
// Cubic Hermite Interpolation
highp vec3 u = f*f*(3.0-2.0*f);
// i is already wrapped by p=mod289(p), but mod289(i) is safe (identity for 0..289).
// Actually mod289(p) ensures p is 0..289.
// So floor(p) is 0..288.
// mod289(i) is redundant but harmless.
highp vec3 i0 = i;
highp vec3 i1 = mod289(i + 1.0);
// Wrap neighbors to ensure valid domain [0, 289] and seamless scrolling
// Optimized to use precomputed wrapped indices (2 mods instead of 9)
return mix(mix(mix(hash13(vec3(i0.x, i0.y, i0.z)), hash13(vec3(i1.x, i0.y, i0.z)), u.x),
mix(hash13(vec3(i0.x, i1.y, i0.z)), hash13(vec3(i1.x, i1.y, i0.z)), u.x), u.y),
mix(mix(hash13(vec3(i0.x, i0.y, i1.z)), hash13(vec3(i1.x, i0.y, i1.z)), u.x),
mix(hash13(vec3(i0.x, i1.y, i1.z)), hash13(vec3(i1.x, i1.y, i1.z)), u.x), u.y), u.z);
}
// Fractal Brownian Motion (4 Octaves)
highp float fbm(highp vec3 p) {
highp float total = 0.0;
highp float amplitude = 0.5;
for (int i = 0; i < 4; i++) {
total += noise(p) * amplitude;
p *= 2.02; // Lacunarity
p += 100.0; // Shift to avoid artifacts
amplitude *= 0.5; // Gain
}
return total;
}
highp float fbm(highp vec3 p, int octaves) {
highp float total = 0.0;
highp float amplitude = 0.5;
for (int i = 0; i < 8; i++) {
if (i >= octaves) break;
total += noise(p) * amplitude;
p *= 2.02;
p += 100.0;
amplitude *= 0.5;
}
return total;
}
// Ray-Sphere Intersection
// Returns distance to intersection or -1.0 if none.
// Re = Planet Radius.
// C = Re^2 - (Re + height)^2 (Precalculated on CPU for precision).
highp float raySphereIntersect(highp vec3 rd, highp float Re, highp float C) {
// Ray Origin is (0, Re, 0) relative to Planet Center (0, 0, 0)
// We solve |(0, Re, 0) + t*rd|^2 = Rm^2
// |O + tD|^2 = R^2
// t^2 + 2t(O.D) + (O^2 - R^2) = 0
// a=1, b=2(O.D), c = O^2 - R^2 = C
// Reduced quadratic: t = -b' +/- sqrt(b'^2 - c) where b' = O.D
highp float b = Re * rd.y; // dot(vec3(0, Re, 0), rd)
highp float disc = b*b - C;
if (disc < 0.0) return -1.0;
// t = -b + sqrt(disc)
return -b + sqrt(disc);
}
// ------------------------------------------------------------------------
// Atmospheric Heat Shimmer (Mirage)
// ------------------------------------------------------------------------
// Simulates heat convection turbulence near the horizon (e.g., hot desert road effect).
//
// PHYSICS:
// Heat rising from the ground creates pockets of varying air density (refractive index).
// This bends light rays, causing a visual "shimmer" or displacement.
//
// IMPLEMENTATION:
// - Perturbs the view vector `V.y` using interleaved sine waves.
// - Uses World Space `V` so the noise is stable under camera rotation.
// - Masked to only affect the horizon line.
//
// PARAMETERS:
// @param V Normalized World View Vector (modified in place).
// @param strength Max vertical displacement amplitude. (e.g. 0.002).
// @param freq Ripple frequency/density. (e.g. 20.0).
// @param maskHeight Horizon mask height (0.0 to 1.0). (e.g. 0.1).
// ------------------------------------------------------------------------
float applyHeatShimmer(inout highp vec3 V, highp float strength, highp float freq, highp float maskHeight) {
if (strength <= 0.0) return 0.0;
// Mask: Strongest at horizon (0.0), fades out by maskHeight.
highp float mask = 1.0 - smoothstep(0.0, maskHeight, abs(V.y));
if (mask > 0.0) {
// Use FBM for organic turbulence (rising heat waves)
highp float time = getUserTime().x;
// Animate upward (y) and slightly drift (x)
highp vec3 p = vec3(V.x * freq, V.y * freq + time * 2.0, time * 0.5);
// We use a cheap noise or FBM. Since we have FBM:
// Use fewer octaves for performance if possible, but 4 is fine.
highp float distortion = fbm(p);
// Remap noise from [0, 1] to [-1, 1] for perturbation
distortion = distortion * 2.0 - 1.0;
// Apply vertical perturbation
V.y += distortion * strength * mask * 0.1;
V = normalize(V);
}
return mask;
}
// ------------------------------------------------------------------------
// Analytic Sky Model (Rayleigh + Mie + Ozone)
// ------------------------------------------------------------------------
// Computes the scattering and transmittance of the atmosphere along the view ray.
//
// PHYSICS:
// - Rayleigh: Scattering by air molecules (Blue sky). High frequency (lambda^-4).
// - Mie: Scattering by aerosols/dust (White haze). Low frequency (lambda^-1.3).
// - Ozone: Absorption layer (Pink sunset). Absorbs green light.
// - Optical Mass: Approximation of path length through spherical atmosphere.
//
// OUTPUTS:
// @param V Normalized View Vector.
// @param L Normalized Sun Vector.
// @param sunIntensity Sun Illuminance (Lux).
// @param depthR Rayleigh Optical Depth (Precalculated).
// @param depthM Mie Optical Depth (Precalculated).
// @param ozone Ozone Absorption (Precalculated).
// @param multiScatParams Multi-Scattering factors (Rayleigh, Mie, Glow).
// @param mieParams Mie Phase Params (x=1+g^2, y=-2g).
// @param outTransmittance Output: Atmospheric Transmittance (0..1) along V.
// @return Output: In-Scattered Radiance (The sky color).
// ------------------------------------------------------------------------
highp vec3 getAtmosphere(highp vec3 V, highp vec3 L, highp float sunIntensity,
highp vec3 depthR, highp vec3 depthM, highp vec3 ozone,
highp vec4 multiScatParams, highp vec2 mieParams,
out highp vec3 outTransmittance) {
highp float cosTheta = dot(V, L);
// 1. Phase Functions
// "Golden Hour" Hack (Three.js Sky.js):
// Remapping cosTheta from [-1, 1] to [0, 1] breaks the symmetry of Rayleigh scattering.
highp float rPhase = rayleighPhase(cosTheta * 0.5 + 0.5);
highp float mPhase = hgPhase(cosTheta, mieParams);
// 2. Optical Depth (Air Mass)
// Kasten and Young (1989) - Relative Air Mass Model
highp float zenithCos = clamp(V.y, 0.0, 1.0);
highp float zenithAngle = acos(zenithCos);
highp float zenithAngleDeg = zenithAngle * (180.0 / PI);
highp float opticalMass = 1.0 / (zenithCos + 0.15 * pow(93.885 - zenithAngleDeg, -1.253));
// 3. Extinction & Transmittance
highp vec3 totalExtinction = depthR + depthM + ozone;
highp vec3 extinction = totalExtinction * opticalMass;
outTransmittance = exp(-extinction);
// 4. In-Scattering
// Approximate Multi-Scattering (Isotropic Fill) precomputed in C++.
highp vec3 multiScattering = multiScatParams.xyz;
highp vec3 scatteringTerm = (depthR * rPhase) + (depthM * mPhase) + multiScattering;
highp vec3 extinctionTerm = max(vec3(1e-6), totalExtinction);
// Equilibrium Radiance (Source Function)
highp vec3 inScattering = sunIntensity * (scatteringTerm / extinctionTerm);
// Single-Scattering Integral: L = L_inf * (1 - exp(-opticalDepth))
highp vec3 sunLight = inScattering * (1.0 - outTransmittance);
// 5. Horizon "Glow" Mix (Artistic Hack)
// multiScatParams.w contains the Horizon Glow Strength
// Uses Sun Elevation (L.y) to only activate during golden hour/twilight.
mediump float horizonMix = saturate(pow(1.0 - L.y, HORIZON_GLOW_POWER)) * multiScatParams.w;
highp vec3 horizonGlow = sqrt(inScattering * outTransmittance);
sunLight *= mix(vec3(1.0), horizonGlow, horizonMix);
return sunLight;
}
// ------------------------------------------------------------------------
// Secondary Sun Scattering (Simplified)
// ------------------------------------------------------------------------
// Computes in-scattering for a second light source, reusing precomputed optical depths.
// Skips multi-scattering (ambient) for performance, providing only direct beams/glow.
//
// @param V Normalized View Vector.
// @param L Normalized Sun Vector.
// @param sunIntensity Sun Illuminance.
// @param depthR Rayleigh Optical Depth.
// @param depthM Mie Optical Depth.
// @param ozone Ozone Absorption.
// @param mieParams Mie Phase Params.
// @param transmittance Precomputed Atmospheric Transmittance.
// @return In-Scattered Radiance.
// ------------------------------------------------------------------------
highp vec3 getSecondarySunScattering(highp vec3 V, highp vec3 L, highp float sunIntensity,
highp vec3 depthR, highp vec3 depthM, highp vec3 ozone,
highp vec2 miePhaseParams, highp vec3 transmittance) {
highp float cosTheta = dot(V, L);
// Phase Functions
highp float rPhase = rayleighPhase(cosTheta * 0.5 + 0.5);
highp float mPhase = hgPhase(cosTheta, miePhaseParams);
// Scattering
highp vec3 scatteringTerm = (depthR * rPhase) + (depthM * mPhase);
highp vec3 totalExtinction = depthR + depthM + ozone;
highp vec3 extinctionTerm = max(vec3(1e-6), totalExtinction);
highp vec3 inScattering = sunIntensity * (scatteringTerm / extinctionTerm);
return inScattering * (1.0 - transmittance);
}
// ------------------------------------------------------------------------
// Physical Sun Disk
// ------------------------------------------------------------------------
// Renders the Solar Photosphere with limb darkening.
//
// PHYSICS:
// - The sun is not a point light; it has an angular size (~0.53 deg).
// - Limb Darkening: The sun is darker at the edges (limbs) because we see cooler outer layers.
// - Drawn "Behind" the atmosphere, so it is attenuated by Transmittance.
//
// PARAMETERS:
// @param V Normalized View Vector.
// @param L Normalized Sun Vector.
// @param sunParams x=CosRadius, y=LimbDarkening, z=IntensityBoost, w=Enabled.
// @param sunIntensity Peak Sun Illuminance (Lux).
// @param transmittance Atmospheric Transmittance (0..1).
// @return Radiance of the sun disk (if visible and enabled).
// ------------------------------------------------------------------------
vec3 getSunDisk(highp vec3 V, highp vec3 L, highp vec4 sunParams,
highp float sunIntensity, highp vec3 transmittance) {
highp float sunCosRadius = sunParams.x;
highp float limbDarkening = sunParams.y;
highp float sunDiskIntensity = sunParams.z;
bool sunEnabled = sunParams.w > 0.5;
highp float cosTheta = dot(V, L);
// Robust edge detection for small angles using (1 - cos)
highp float dist = 1.0 - cosTheta;
highp float diskRadius = max(1e-6, 1.0 - sunCosRadius);
// AA Edge: smoothstep from radius to radius+epsilon
// We invert it because we want 1.0 inside (dist < radius)
highp float sunDiskProfile = 1.0 - smoothstep(diskRadius, diskRadius + 0.00002, dist);
if (sunEnabled && sunDiskProfile > 0.0) {
// Limb Darkening approximation: mu = sqrt(1 - (r/R)^2)
// dist/diskRadius is approx (r/R)^2 for small angles
highp float relativeDist = min(1.0, dist / diskRadius);
highp float mu = sqrt(1.0 - relativeDist);
// Avoid pow(0, 0) which causes NaNs
highp float limbFactor = (limbDarkening < 1e-4) ? 1.0 : pow(mu, limbDarkening);
// Direct Sun Light (Radiance)
// SunIntensity * Transmittance -> Physical Sun Color
// SunDiskIntensity -> Artistic Boost to punch through Mie halo
return sunIntensity * transmittance * limbFactor * sunDiskIntensity * sunDiskProfile;
}
return vec3(0.0);
}
// ------------------------------------------------------------------------
// Procedural Cirrus Clouds
// ------------------------------------------------------------------------
// Renders a thin layer of high-altitude clouds (Cirrus) using 3D Noise.
//
// IMPLEMENTATION:
// - Modeled as a spherical shell at a specific altitude.
// - Ray-Sphere intersection determines UV layout and distance.
// - Animated using 3D FBM (Fractal Brownian Motion) for shape evolution + Wind drift.
// - Lighting includes Silver Lining (HG Phase) and Atmospheric Extinction.
//
// PARAMETERS:
// @param V Normalized View Vector.
// @param L Normalized Sun Vector.
// @param control x=Coverage, y=Density, z=QuadraticConst(C), w=WindSpeed.
// @param control2 x=EvolutionSpeed.
// @param geometry w=PlanetRadius (Re).
// @param sunIntensity Sun Illuminance.
// @param transmittance Atmospheric Transmittance (Cloud Color Tint).
// @param outDensity Output: Cloud pixel density (0..1) for compositing.
// @return Cloud Radiance (Lit color * attenuation * shading).
// ------------------------------------------------------------------------
vec3 getCloudLayer(highp vec3 V, highp vec3 L,
highp vec4 control, highp vec4 control2, highp vec4 geometry,
highp float sunIntensity, highp vec3 transmittance,
out float outDensity) {
outDensity = 0.0;
highp float cloudCoverage = control.x;
// Clip clouds below the horizon (Earth occlusion)
// Simple check V.y > 0.0 is sufficient for skybox provided camera is near ground.
if (cloudCoverage > 0.0 && V.y > 0.0) {
highp float Re = geometry.w;
highp float intersectC = control.z;
highp float distToCloud = raySphereIntersect(V, Re, intersectC);
if (distToCloud > 0.0) {
highp vec3 p = V * distToCloud;
highp float speed = control.w;
highp float morphSpeed = control2.x;
highp float time = getUserTime().x;
// UV Mapping (Planar projected onto sphere cap is sufficient for skybox)
// Scale factor CLOUD_UV_SCALE km^-1
highp vec2 uv = (p.xz * CLOUD_UV_SCALE) + vec2(time * speed * 2.0, 0.0);
// 3D Noise for Morphing
highp float noiseVal = fbm(vec3(uv, time * morphSpeed));
// Remap noise based on coverage.
// Coverage 0.5 -> threshold 0.5. Coverage 1.0 -> threshold 0.0.
highp float threshold = 1.0 - cloudCoverage;
highp float cloudDensity = smoothstep(threshold, threshold + 0.3, noiseVal);
if (cloudDensity > 0.0) {
cloudDensity *= control.y; // Global Density Scalar
cloudDensity = clamp(cloudDensity, 0.0, 1.0);
outDensity = cloudDensity;
// Cloud Lighting
// Silver Lining: Strong forward scattering (Fixed g=0.9 for clouds)
highp float cosTheta = dot(V, L);
// We need separate params for cloud silver lining (g=0.9).
// 1 + 0.9^2 = 1.81. -2*0.9 = -1.8.
// Attenuation (Beer's Law)
// Thick clouds block light.
// CLOUD_EXTINCTION is an artistic extinction coefficient.
highp float extinction = exp(-cloudDensity * CLOUD_EXTINCTION);
// 1 + g^2 = 1.0 + CLOUD_SILVER_G*CLOUD_SILVER_G
// -2*g = -2.0 * CLOUD_SILVER_G
// We use hardcoded derived values for g=0.9: 1.81, -1.8
highp float silver = hgPhase(cosTheta, vec2(1.81, -1.8)) * CLOUD_SILVER_INTENSITY * extinction;
// Ambient/Diffuse term.
// We allow some ambient light to pass through even thick clouds (CLOUD_AMBIENT_MIN min)
// so they don't look like black holes.
highp float ambient = CLOUD_AMBIENT_MIN + 0.4 * extinction;
// Diffuse term (Sun Color) + Silver Lining
highp vec3 cloudLight = sunIntensity * transmittance * (ambient + silver);
// Mix based on density
highp float volumetric = control2.y;
highp float shading = 1.0;
if (volumetric > 0.5) {
// Gradient Lighting (Fake Volumetric Bump)
highp float gradX = dFdx(cloudDensity);
highp float gradY = dFdy(cloudDensity);
// Smaller Z = Steeper Bumps.
// dFdx(density) is typically small (e.g. 0.001).
// We want Normal to have significant X/Y component.
highp vec3 N = normalize(vec3(-gradX, -gradY, 0.001));
// Screen Space Sun Direction
highp vec3 sRight = normalize(dFdx(V));
highp vec3 sUp = normalize(dFdy(V));
highp vec3 L_screen = vec3(dot(L, sRight), dot(L, sUp), 0.5);
L_screen = normalize(L_screen);
shading = dot(N, L_screen);
// Increase contrast: Darker shadows
// dot is [-1, 1]. Map to [0.3, 1.0]
shading = mix(0.3, 1.0, shading * 0.5 + 0.5);
// Darken thick parts (Beer's Law approximation)
// Aggressively darken center of clouds
shading *= (1.0 - cloudDensity * VOLUMETRIC_SHADOW_STR);
}
return cloudLight * shading;
}
}
}
return vec3(0.0);
}
// ------------------------------------------------------------------------
// Dynamic Tone Mapping
// ------------------------------------------------------------------------
// Applies a contrast curve that varies with sun elevation.
//
// PROBLEM:
// Default linear/gamma tone mapping can make sunsets look washing out.
// Real eyes accept much higher dynamic range at twilight.
//
// SOLUTION:
// - Zenith (Noon): Linear gamma (Exponent 1.0). Physically accurate.
// - Horizon (Sunset): High contrast (Exponent > 1.0). Crushes shadows, boosts color.
//
// @param color Input HDR color.
// @param L Normalized Sun Vector.
// @param contrast Maximum contrast exponent (at horizon). e.g. 1.5.
// @return Tone mapped color.
// ------------------------------------------------------------------------
vec3 applyDynamicToneMapping(vec3 color, highp vec3 L, float contrast) {
highp float c = saturate(L.y);
// Exponent blends from 'contrast' (at L.y=0) to 1.0 (at L.y=1)
float exponent = mix(contrast, 1.0, sqrt(c));
return pow(max(vec3(0.0), color), vec3(exponent));
}
// ------------------------------------------------------------------------------------------------
// STARS (World-Anchored)
// ------------------------------------------------------------------------------------------------
// 4-component hash for jitter (xyz) and intensity (w)
highp vec4 hash43(highp vec3 p) {
highp vec4 p4 = fract(vec4(p.xyzx) * vec4(0.1031, 0.1030, 0.0973, 0.1099));
p4 += dot(p4, p4.wzxy + 33.33);
return fract((p4.xxyz + p4.yzzw) * p4.zywx);
}
float getStars(highp vec3 V) {
bool enabled = materialParams.starControl.y > 0.5;
if (!enabled) return 0.0;
highp float density = materialParams.starControl.x;
if (density <= 0.0) return 0.0;
// 1. Fixed World Grid (Anchor)
// We use a FIXED frequency for the grid, so stars don't move when focal length changes.
highp float gridFreq = materialParams.starControl.z; // e.g. 350.0
highp vec3 p = V * gridFreq;
highp vec3 id = floor(p);
// 2. Scan neighbors (1-tap with jitter constraint)
// We constrain jitter to be well within cell to avoid edge artifacts with 1 tap.
highp vec4 h = hash43(id);
// Density check (Probability)
if (h.w > density) return 0.0;
// 3. World Position of Star Center
// Jitter range [0.2, 0.8] to avoid clipping neighbors
highp vec3 jitter = 0.2 + 0.6 * h.xyz;
highp vec3 starCenter = (id + jitter) / gridFreq;
starCenter = normalize(starCenter);
// 4. Angular Distance
// distSq ~ 2 * (1 - cosC)
highp float cosC = dot(V, starCenter);
highp float distSq = 2.0 * (1.0 - cosC);
// 5. Screen-Adaptive Radius
// materialParams.starControl.w contains "Pixel Scale in Radians"
// Target radius: 1.5 pixels
highp float pixelScaleRad = materialParams.starControl.w;
highp float starRadiusRad = 1.0 * pixelScaleRad; // Tunable size
highp float starRadiusSq = starRadiusRad * starRadiusRad;
// 6. Rendering (Soft Disk)
highp float intensity = 0.0;
if (distSq < starRadiusSq) {
// Falloff
highp float x = distSq / starRadiusSq;
intensity = 1.0 - x;
intensity *= intensity; // Cubic-ish
// Random Brightness
// Range STAR_BRIGHTNESS_BASE - (STAR_BRIGHTNESS_BASE + STAR_BRIGHTNESS_VAR)
highp float brightness = STAR_BRIGHTNESS_BASE + STAR_BRIGHTNESS_VAR * h.w;
intensity *= brightness * STAR_GLOBAL_INTENSITY;
}
return intensity;
}
// New helper to handle Star Compositing (Fade, Rotation, Occlusion)
vec3 getStarLayer(highp vec3 V, highp vec3 L, float cloudDensity, highp vec3 transmittance, highp vec4 starControl) {
// starControl.x = Density, .y = Enabled
if (starControl.y < 0.5) return vec3(0.0);
// 1. Fade by Sun Elevation
// Start appearing sooner (when sun is still slightly up), but stay dim.
// STAR_FADE_SUN_ELV_HIGH (5.7 deg up) -> 0.0
// STAR_FADE_SUN_ELV_LOW (11.5 deg down) -> 1.0
highp float starFade = 1.0 - smoothstep(STAR_FADE_SUN_ELV_LOW, STAR_FADE_SUN_ELV_HIGH, L.y);
starFade *= starFade;
if (starFade <= 0.0) return vec3(0.0);
// 2. Rotate to break grid alignment
highp vec3 rotV = vec3(
dot(V, vec3(0.6, 0.8, 0.0)),
dot(V, vec3(-0.8, 0.6, 0.0)),
V.z
);
highp float starVal = getStars(rotV);
if (starVal <= 0.0) return vec3(0.0);
// 3. Cloud Occlusion (Aggressive)
highp float cloudOcclusion = 1.0 - smoothstep(0.0, 1.0, pow(cloudDensity, 0.1));
return vec3(starVal) * transmittance * starFade * cloudOcclusion * STAR_CLOUD_OCCLUSION;
}
// ------------------------------------------------------------------------
// Milky Way
// ------------------------------------------------------------------------
// Renders the Milky Way background from an equirectangular texture.
//
// @param V Normalized View Vector.
// @param rotation Rotation matrix (Galactic -> World).
// @return Milky Way Color.
// ------------------------------------------------------------------------
vec3 getMilkyWay(highp vec3 V, highp mat3 rotation, sampler2D tex, highp vec3 control) {
highp float intensity = control.x;
if (intensity <= 0.0) return vec3(0.0);
// Rotate V into Galactic coordinates
highp vec3 Vg = rotation * V;
// Equirectangular mapping
// u = atan(z, x) / 2pi + 0.5
// v = asin(y) / pi + 0.5
highp float u = atan(Vg.z, Vg.x) * (0.1591549) + 0.5; // 1/(2*PI)
highp float v = asin(clamp(Vg.y, -1.0, 1.0)) * (0.3183098) + 0.5; // 1/PI
// Sample
highp vec3 color = texture(tex, vec2(u, v)).rgb;
// Black Point (Remove baked-in haze)
highp float blackPoint = control.z;
color = max(vec3(0.0), color - blackPoint);
// Saturation
highp float saturation = control.y;
highp float gray = dot(color, vec3(0.299, 0.587, 0.114));
color = mix(vec3(gray), color, saturation);
return color * intensity;
}
// ------------------------------------------------------------------------
// Procedural Moon Disk (Phased)
// ------------------------------------------------------------------------
// Renders the Moon as a 3D sphere with lighting from the Sun.
//
// PARAMETERS:
// @param V Normalized View Vector.
// @param L_moon Normalized Moon Vector.
// @param L_sun Normalized Sun Vector.
// @param moonParams x=CosRadius, y=SinRadius, z=IntensityBoost, w=Enabled.
// @param moonIntensity Peak Moon Illuminance (Lux).
// @param transmittance Atmospheric Transmittance (Moon Color Tint).
// @return Radiance of the moon disk.
// ------------------------------------------------------------------------
highp vec3 getMoonDisk(highp vec3 V, highp vec3 L_moon, highp vec3 L_sun,
highp vec4 moonParams, highp float moonIntensity,
highp vec3 transmittance, sampler2D moonTex, sampler2D moonNormal) {
highp float moonCosRadius = moonParams.x;
highp float moonSinRadius = moonParams.y; // Used for precision
highp float moonDiskIntensity = moonParams.z;
bool moonEnabled = moonParams.w > 0.5;
highp float cosTheta = dot(V, L_moon);
highp float dist = 1.0 - cosTheta; // Angular distance (approx theta^2/2 for small angles)
highp float diskRadius = max(1e-6, 1.0 - moonCosRadius); // Approx Radius^2 / 2
// Soft Edge
// Make edge width proportional to radius to handle small moons relative to the sun.
// For the sun, we used a fixed width (0.00002) which creates a glow.
// For the moon, we want a sharper limb, so we use MOON_LIMB_SOFTNESS of radius.
highp float edgeWidth = max(2.0e-7, diskRadius * MOON_LIMB_SOFTNESS);
highp float moonProfile = 1.0 - smoothstep(diskRadius, diskRadius + edgeWidth, dist);
if (moonEnabled && moonProfile > 0.0) {
// 1. Reconstruct Sphere Normal
// Tangent vector T = V - (V.L)L
// This vector T points from the center of the disk towards V, in the disk plane.
// Magnitude |T| is the distance from center (sine of angle).
// Since we are working with small angles:
// T approx V - L_moon.
highp vec3 T = V - cosTheta * L_moon;
// Normalize T to get radial direction strength
// We want N_perp magnitude to be 1.0 at the edge (where |T| ~ sin(angularRadius))
// radius in "T space" is sin(radius), which we now pass explicitly in .y
highp float sinRadius = max(1e-9, moonSinRadius);
highp vec3 N_perp = T / sinRadius;
// Project onto sphere to get full normal
// Z component is along L_moon (towards viewer approx)
// |N| = 1. N.z = sqrt(1 - |N_perp|^2)
highp float nPerpSq = dot(N_perp, N_perp);
// N_sphere = N_perp - L_moon * zComp
// Vector points OUT from center.
// Since L_moon points from Earth to Moon, and the visible face normal points roughly towards Earth,
// the normal component along L_moon is negative.
highp vec3 N_sphere = N_perp - L_moon * sqrt(max(0.0, 1.0 - nPerpSq));
// Earthshine (Ambient)
// Earth Phase (approx):
// When Sun & Moon are same direction (dot=1), Earth is Full (from Moon).
// When Sun & Moon are opposite (dot=-1), Earth is New (from Moon).
// EarthPhase ~ 0.5 * (1.0 + dot(L_sun, L_moon)).
highp float earthPhase = 0.5 * (1.0 + dot(L_sun, L_moon));
// Texture Mapping
// The moon texture is an orthographic projection of the Near Side.
// We align the texture using a stable "Up" vector (World Up projected onto the Moon plane).
highp vec3 Up = vec3(0.0, 1.0, 0.0);
if (abs(L_moon.y) > 0.99) Up = vec3(0.0, 0.0, 1.0);
highp vec3 Right = normalize(cross(Up, L_moon));
highp vec3 MoonUp = cross(L_moon, Right);
// Project N_sphere onto the Right/MoonUp plane to get UVs.
highp float u = dot(N_sphere, Right) * 0.5 + 0.5;
highp float v = dot(N_sphere, MoonUp) * 0.5 + 0.5;
// Sample Texture
// We assume the texture is stored such that (0,0) is bottom-left.
// Sample Albedo
// Flip V here as well. Flip U to fix horizontal mirroring.
highp vec3 moonColorSample = texture(moonTex, vec2(1.0 - u, 1.0 - v)).rgb;
// The original code used a float 'moonAlbedo' derived from .rrr.
// To support "3 channels" properly if it had color, we use the full RGB sample.
highp vec3 moonAlbedo = moonColorSample;
// Normal Mapping
// Convert Tangent Space Normal (from texture) to World Space
// Tangent Basis: T=Right, B=MoonUp, N=N_sphere
// We need to invert V because texturing coordinates are usually bottom-up
// but our procedural disk generation (and the original texture) might be top-down oriented
// relative to the sphere mapping we did.
highp vec3 N_map = texture(moonNormal, vec2(1.0 - u, 1.0 - v)).rgb * 2.0 - 1.0;
// Scale down the normal map strength (it was too strong)
N_map.xy *= 0.2;
// Important: Since we flipped U (1.0 - u), we effectively mirrored the texture.
// A slope that was "right" is now "left" visually, but the normal map data still points "right".
// We must invert the X component of the normal to match the mirrored visual geometry.
N_map.x *= -1.0;
// Perturb N_sphere
// N_final = N_map.x * T + N_map.y * B + N_map.z * N
// Note: N_map.z is the component along the surface normal.
N_sphere = normalize(N_map.x * Right + N_map.y * MoonUp + N_map.z * N_sphere);
highp float NdotL = max(0.0, dot(N_sphere, L_sun));
highp vec3 litColor = moonIntensity * transmittance * NdotL * moonAlbedo;
highp vec3 unlitColor = moonIntensity * transmittance * MOON_EARTHSHINE * earthPhase * moonAlbedo;
// Smooth transition at terminator to avoid hard lines
return mix(unlitColor, litColor, smoothstep(0.0, 0.1, NdotL)) * moonDiskIntensity * moonProfile;
}
return vec3(0.0);
}
// ------------------------------------------------------------------------
// Phase Factor (Lambertian Sphere)
// ------------------------------------------------------------------------
// Calculates the integrated flux factor for a lit sphere based on phase angle.
// Normalized to 1.0 at Full Moon (Phase=0) and 0.0 at New Moon (Phase=PI).
//
// @param L_sun Normalized vector to Sun
// @param L_moon Normalized vector to Moon
// @return Phase factor [0.0, 1.0]
// ------------------------------------------------------------------------
highp float getPhaseFactor(highp vec3 L_sun, highp vec3 L_moon) {
highp float cosAlpha = -dot(L_sun, L_moon);
highp float alpha = acos(clamp(cosAlpha, -1.0, 1.0));
return (1.0 / PI) * (sin(alpha) + (PI - alpha) * cosAlpha);
}
// ------------------------------------------------------------------------
// Procedural Water Surface
// ------------------------------------------------------------------------
// Simulates an infinite ocean plane at y=0 using screen-space derivatives for normals.
//
// FEATURES:
// - Projected grid for infinite surface.
// - Screen-space wave normal reconstruction (no geometry required).
// - Fresnel reflection of Atmosphere, Sun, and Clouds.
// - Specular highlights (Blinn-Phong).
//
// PARAMETERS:
// @param V Normalized View Vector.
// @param L Normalized Sun Vector.
// @param sunIntensity Sun Illuminance (Lit/Dimmed).
// @param sunDiskIntensity Sun Illuminance (Un-Dimmed) for specular.
// @param depthR Rayleigh Optical Depth.
// @param depthM Mie Optical Depth.
// @param ozone Ozone Absorption.
// @param multiScatParams Multi-Scattering Params.
// @param miePhaseParams Mie Phase Params.
// @param sunHalo Sun Halo Params.
// @param cloudControl Cloud Control Params.
// @param cloudControl2 Cloud Evolution Params.
// @param shimmerControl Shimmer Control (w component used as PlanetRadius for clouds).
// @param waterControl Water Control (x=Strength, y=Speed, z=DerivativeTrick).
// @param L2 Secondary Sun Direction.
// @param sunIntensity2 Secondary Sun Intensity.
// @param sunHalo2 Secondary Sun Halo Params.
// @return Water surface color.
// ------------------------------------------------------------------------
highp vec3 getWaterColor(highp vec3 V, highp vec3 L, highp float sunIntensity, highp float sunDiskIntensity,
highp vec3 depthR, highp vec3 depthM, highp vec3 ozone,
highp vec4 multiScatParams, highp vec2 miePhaseParams,
highp vec4 sunHalo, highp vec4 cloudControl, highp vec4 cloudControl2,
highp vec4 shimmerControl, highp vec4 waterControl,
highp vec3 L2, highp float sunIntensity2, highp vec4 sunHalo2,
sampler2D moonTex, sampler2D moonNormal,
highp vec3 nightColor,
highp mat3 milkyWayRotation,
highp vec3 milkyWayControl,
sampler2D milkyWayTexture) {
// Project to plane y=0
highp float t = WATER_PLANE_HEIGHT / min(V.y, -0.0002); // Reduced clamp to minimize "wall" artifact
highp vec2 uv = V.xz * t * WATER_UV_SCALE;
highp float time = getUserTime().x;
highp float speed = waterControl.y;
uv += vec2(time * 0.5 * speed, time * 0.2 * speed);
// Wave Normal
// Use screen-space derivatives to compute world-space normal perturbation
int octaves = int(max(1.0, waterControl.w));
highp float h = fbm(vec3(uv, time * 0.1 * speed), octaves);
// Perturb normal based on height gradient
// If h increases in screen-X direction, normal tilts against sRight.
// Fade out perturbation near horizon (V.y -> 0) to reduce aliasing
highp float horizonFade = smoothstep(0.0, 0.5, abs(V.y));
highp float strength = waterControl.x;
highp vec3 N_perturb;
// Derivative Trick Toggle
if (waterControl.z > 0.5) {
// Screen-Space Derivatives (Fast, 1 tap)
// Reconstruct screen-space basis in world space
// If h increases in screen-X direction, normal tilts against sRight.
highp vec3 sRight = normalize(dFdx(V));
highp vec3 sUp = normalize(dFdy(V));
N_perturb = (sRight * dFdx(h) + sUp * dFdy(h)) * strength * horizonFade;
} else {
// Finite Difference (Standard, 3 taps)
// More expensive but analytically correct in world space (independent of view resolution/derivatives)
highp float eps = 0.02; // Epsilon for gradient
highp vec3 p = vec3(uv, time * 0.1 * speed);
highp float hx = fbm(p + vec3(eps, 0.0, 0.0), octaves);
highp float hy = fbm(p + vec3(0.0, eps, 0.0), octaves);
// Gradient
highp float dx = (hx - h) / eps;
highp float dy = (hy - h) / eps;
// Construct World Space Perturbation
// Gradient (dx, dy) acts on XZ plane.
// Normal = normalize(-dx, 1, -dy).
// We want N_perturb to SUBTRACT from (0,1,0).
// N_water = normalize(Up - Perturb).
// So Perturb = (dx, 0, dy).
// Note: Strength needs to be calibrated to match derivative trick roughly, or just raw.
// Derivative trick Strength was ~50.0.
// Here dx/dy are raw noise slopes.
// Reduced to 0.002 to match visual range of derivative trick and prevent black artifacts.
N_perturb = vec3(dx, 0.0, dy) * (strength * 0.002) * horizonFade;
}
highp vec3 N_water = normalize(vec3(0.0, 1.0, 0.0) - N_perturb);
// Reflection
highp vec3 R = reflect(V, N_water);
// Specular (Sun)
// We use `sunDiskIntensity` (Un-Dimmed) for the disk reflection to preserve its appearance,
// while `sunIntensity` (Lit/Dimmed) is used for general atmospheric scattering to match the sky.
// The reflection's occlusion is handled separately by `reflMoonOcclusion` later.
// Re-deriving Sky Color for Reflection
highp vec3 outTransmittance;
highp vec3 reflection = getAtmosphere(R, L, sunIntensity * 1.0 /* ensure match */,
depthR, depthM, ozone,
multiScatParams, miePhaseParams,
outTransmittance);
// Add Moon Scattering to Reflection
if (sunHalo2.w > 0.5) {
float phaseFactor = getPhaseFactor(L, L2);
reflection += getSecondarySunScattering(R, L2, sunIntensity2 * phaseFactor,
depthR, depthM, ozone,
miePhaseParams,
outTransmittance);
}
// Clouds in reflection
highp float reflCloudDensity;
highp vec3 reflCloudLayer = getCloudLayer(R, L, cloudControl, cloudControl2,
shimmerControl, sunIntensity, outTransmittance,
reflCloudDensity);
// Add Stars to Reflection
// Use helper with Reflection Vector and Reflection Cloud Density
// Horizon Mask: Fade out star reflections that are deep in the water (high R.y)
// Restricted to very close to horizon (0.0 to REFLECTION_HORIZON_FADE) as requested.
highp float rHorizonMask = 1.0 - smoothstep(0.0, REFLECTION_HORIZON_FADE, R.y);
if (rHorizonMask > 0.0) {
reflection += getStarLayer(R, L, reflCloudDensity, outTransmittance, materialParams.starControl) * rHorizonMask * materialParams.exposure;
}
// Sun Disk Reflection
// Use sunDiskIntensity (Un-Dimmed) + Moon Occlusion Mask
highp vec3 sunDiskRefl = getSunDisk(R, L, sunHalo, sunDiskIntensity, outTransmittance);
// Apply Moon Occlusion to Reflection
// We calculate occlusion for R.
highp float reflDiskRadius = max(1e-6, 1.0 - sunHalo.x); // Approx
highp float cosThetaRefl = dot(R, L2); // Moon vs Reflection Vector
highp float distRefl = 1.0 - cosThetaRefl;
highp float moonDiskRadius = max(1e-6, 1.0 - sunHalo2.x);
// Create a mask for the moon in the reflection
highp float reflMoonOcclusion = 1.0 - smoothstep(moonDiskRadius, moonDiskRadius + 0.00002, distRefl);
// Mask the Sun Disk
sunDiskRefl *= (1.0 - reflMoonOcclusion);
// Add Sun Disk to Sky Color
reflection += sunDiskRefl;
// Add Moon Disk to reflection
if (sunHalo2.w > 0.5) {
reflection += getMoonDisk(R, L2, L, sunHalo2, sunIntensity2, outTransmittance, moonTex, moonNormal);
}
// Apply clouds to reflection
reflection = mix(reflection, reflCloudLayer, reflCloudDensity);
// Add Milky Way to Reflection
// Calculate Fade based on Sun Elevation
highp float sunElvSin = L.y;
highp float mwFade = smoothstep(STAR_FADE_SUN_ELV_HIGH, STAR_FADE_SUN_ELV_LOW, sunElvSin);
if (mwFade > 0.0) {
highp vec3 mwColor = getMilkyWay(R, milkyWayRotation, milkyWayTexture, milkyWayControl);
// Apply Atmosphere Transmittance (approximate)
mwColor *= outTransmittance;
// Apply Fades
mwColor *= mwFade;
mwColor *= (1.0 - reflMoonOcclusion); // Occlude by Moon
mwColor *= (1.0 - reflCloudDensity); // Occlude by Clouds
reflection += mwColor;
}
// Add Night Color to Reflection
reflection += nightColor;
// Fresnel
highp float F0 = WATER_FRESNEL_F0; // Water
highp float cosTheta = clamp(dot(-V, N_water), 0.0, 1.0);
highp float F = F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
highp vec3 deepColor = WATER_DEEP_COLOR; // Deep blue/black
highp vec3 waterColor = mix(deepColor, reflection, F);
return waterColor;
}
void material(inout MaterialInputs material) {
prepareMaterial(material);
highp vec3 V = normalize(variable_eyeDirection.xyz);
highp vec3 L = normalize(materialParams.sunDirection);
// 1. Heat Shimmer
// Fade out as sun rises (Strongest at horizon, zero at 30 degrees up)
highp float sunFade = 1.0 - smoothstep(0.0, 0.5, abs(L.y));
highp float shimmerIntensity = applyHeatShimmer(V, materialParams.shimmerControl.x * sunFade,
materialParams.shimmerControl.y,
materialParams.shimmerControl.z);
// 2. Atmospheric Scattering
highp vec3 transmittance;
// Sun 2 / Moon (Optional)
// We reuse the same Transmittance (view dependent) and Phase params.
// We do NOT add extra Multi-Scattering (Ambient) for the second sun to save cost/complexity.
// It contributes Direct In-Scattering (Beams/Glow) only.
highp vec3 inScatter2 = vec3(0.0);
highp vec3 L2 = normalize(materialParams.sunDirection2);
// Calculate Solar Eclipse (Light Reduction)
// Dim the Sun Light used for Atmosphere/Clouds/Water
highp float safeEclipseFactor = materialParams.eclipseFactor;
// Safety: If moon is disabled, ensure no eclipse
if (materialParams.sunHalo2.w < 0.5) {
safeEclipseFactor = 1.0;
}
highp float sunIntensityLit = materialParams.sunIntensity * safeEclipseFactor;
// 2. Atmospheric Scattering
highp vec3 color = getAtmosphere(V, L, sunIntensityLit, materialParams.depthR, materialParams.depthM, materialParams.ozone,
materialParams.multiScatParams, materialParams.miePhaseParams, transmittance);
// 3. Secondary Scattering (Moon)
if (materialParams.sunHalo2.w > 0.5) {
float phaseFactor = getPhaseFactor(L, L2);
inScatter2 = getSecondarySunScattering(V, L2,
materialParams.sunIntensity2 * phaseFactor,
materialParams.depthR,
materialParams.depthM,
materialParams.ozone,
materialParams.miePhaseParams,
transmittance);
color += inScatter2;
}
// 4. Clouds
highp float cloudDensityVal;
highp vec3 clouds = getCloudLayer(V, L, materialParams.cloudControl, materialParams.cloudControl2, materialParams.shimmerControl,
sunIntensityLit, transmittance, cloudDensityVal);
// 5. Sun Disk (Direct)
// Use ORIGINAL sunIntensity (Un-dimmed) so the disk looks bright
// We mask it by Moon Occlusion AND Cloud Occlusion
highp vec3 sunDisk = getSunDisk(V, L, materialParams.sunHalo, materialParams.sunIntensity, transmittance);
// Moon Occlusion Mask
highp float moonOcclusion = 0.0;
if (materialParams.sunHalo2.w > 0.5) {
highp float distToMoon = 1.0 - dot(V, L2);
highp float moonDiskRad = max(1e-6, 1.0 - materialParams.sunHalo2.x);
moonOcclusion = 1.0 - smoothstep(moonDiskRad, moonDiskRad + 0.00002, distToMoon);
}
// Cloud Occlusion Mask (Sun Access)
highp float sunAccess = 1.0 - smoothstep(0.0, 0.7, cloudDensityVal * 1.5);
// Apply Masks
// Note: moonOcclusion is 1.0 if occluded. So we want (1.0 - moonOcclusion).
sunDisk *= (1.0 - moonOcclusion) * sunAccess;
color += sunDisk;
// 6. Moon Disk
if (materialParams.sunHalo2.w > 0.5) {
color += getMoonDisk(V, L2, L, materialParams.sunHalo2,
materialParams.sunIntensity2, transmittance, materialParams_moonTexture, materialParams_moonNormal) * sunAccess;
}
// 7. Stars
// Add stars before clouds (clouds cover stars)
highp vec3 starColor = getStarLayer(V, L, cloudDensityVal, transmittance, materialParams.starControl) * materialParams.exposure * materialParams.starIntensity;
// 7b. Milky Way
// Add Milky Way behind stars (conceptually) but handled similarly
// We fade it by Sun Elevation just like stars
// Re-use starFade logic implicity or calculate it?
// Let's use the starControl.y (enabled) check inside getMilkyWay if needed, or just assume it's always on if intensity > 0.
// We reuse the fade from stars for consistency:
// STAR_FADE_SUN_ELV_HIGH...
// We recalculate fade:
highp float mwFade = 1.0 - smoothstep(STAR_FADE_SUN_ELV_LOW, STAR_FADE_SUN_ELV_HIGH, L.y);
mwFade *= mwFade;
highp vec3 milkyWay = vec3(0.0);
if (mwFade > 0.0) {
milkyWay = getMilkyWay(V, materialParams.milkyWayRotation, materialParams_milkyWayTexture, materialParams.milkyWayControl);
milkyWay *= mwFade * transmittance * (1.0 - moonOcclusion);
// Apply cloud occlusion
milkyWay *= (1.0 - smoothstep(0.0, 1.0, pow(cloudDensityVal, 0.1)));
}
color += (starColor + milkyWay) * (1.0 - moonOcclusion);
// 8. Composite Clouds
// Clouds are alpha blended on top of the sky (Atmos + Stars + SunDisk)
// The cloud color `clouds` includes its own ambient and forward scattering terms.
color = mix(color, clouds, cloudDensityVal);
// 9. Night Sky Offset
color += materialParams.nightColor;
// 10. Dynamic Tone Mapping
color = applyDynamicToneMapping(color, L, materialParams.contrast);
// 11. Water Surface (Reflection)
if (V.y < 0.0) {
color = getWaterColor(V, L, sunIntensityLit, materialParams.sunIntensity,
materialParams.depthR, materialParams.depthM,
materialParams.ozone, materialParams.multiScatParams,
materialParams.miePhaseParams,
materialParams.sunHalo,
materialParams.cloudControl, materialParams.cloudControl2,
materialParams.shimmerControl,
materialParams.waterControl,
L2, materialParams.sunIntensity2, materialParams.sunHalo2,
materialParams_moonTexture, materialParams_moonNormal,
materialParams.nightColor,
materialParams.milkyWayRotation,
materialParams.milkyWayControl,
materialParams_milkyWayTexture);
color = applyDynamicToneMapping(color, L, materialParams.contrast);
}
material.baseColor = vec4(color, 1.0);
}
}