SimSky: Refine Stars, Water, and Heat Shimmer simulation (#9628)

- **Stars**:
  - Implemented procedural stars using hash-based noise.
  - Added UI controls for Star Density and Enable/Disable.
  - Tuned star brightness (reduced intensity) and refined twilight fade timing (visible during nautical twilight).
  - Improved compositing with aggressive cloud occlusion and non-linear fade.
  - Added star reflections to water, strictly masked to the horizon line.

- **Heat Shimmer**:
  - Fixed horizon artifacts by decoupling shimmer from atmospheric density (Mie scattering).
  - Implemented FBM-based view distortion for heat waves.
  - Added sun elevation fade (shimmer fades out as sun rises > 30°).

- **Water**:
  - Implemented Finite Difference normal calculation as a high-quality fallback when "Derivative Trick" is disabled.
  - Added "Octaves" parameter to control wave detail.
  - Refined reflection logic to handle stars and sun disk properly.

- **System**:
  - Updated [simulated_skybox.mat](cci:7://file:///Users/mathias/sources/git/filament/docs_src/src_raw/wip/sky/simulated_skybox.mat:0:0-0:0) with new material parameters (`starControl`, `waterControl`).
  - Refactored JS bindings in [SimulatedSkybox.js](cci:7://file:///Users/mathias/sources/git/filament/docs_src/src_raw/wip/sky/SimulatedSkybox.js:0:0-0:0) and organized `main.js` UI into logical folders.

DOCS_FORCE
This commit is contained in:
Mathias Agopian
2026-01-23 01:06:07 -08:00
committed by GitHub
parent 7fd9e728ae
commit e21d4a5326
6 changed files with 473 additions and 105 deletions

View File

@@ -20,6 +20,8 @@ class SimulatedSkybox {
this.shimmerControl = [0.0, 20.0, 0.1];
this.cloudControl = [0.0, 0.1, 8000.0, 0.0];
this.cloudControl2 = [0.0, 0.0, 0.0, 0.0];
this.waterControl = [50.0, 1.0, 1.0, 4.0]; // x=Strength, y=Speed, z=DerivativeTrick, w=Octaves
this.starControl = [1.0, 1.0]; // x=Density (0-1), y=Enabled (0-1)
this.planetRadius = 6360.0;
// Sun Halo
@@ -204,12 +206,26 @@ class SimulatedSkybox {
this.updateCoefficients();
}
setWaterControl(strength, speed, derivativeTrick, octaves) {
this.waterControl[0] = Math.max(0.0, strength);
this.waterControl[1] = Math.max(0.0, speed);
this.waterControl[2] = derivativeTrick;
this.waterControl[3] = Math.max(1.0, Math.min(8.0, octaves));
this.updateCoefficients();
}
setStarControl(density, enabled) {
this.starControl[0] = Math.max(0.0, Math.min(1.0, density));
this.starControl[1] = enabled ? 1.0 : 0.0;
this.updateCoefficients();
}
updateCoefficients() {
if (!this.materialInstance) {
console.warn("updateCoefficients called before material loaded");
return;
}
console.log("Updating coefficients...");
// 1. Rayleigh Coefficients
const F_PI = Math.PI;
@@ -287,6 +303,8 @@ class SimulatedSkybox {
this.materialInstance.setFloat4Parameter('shimmerControl', new Float32Array(shimmerUniform));
this.materialInstance.setFloat4Parameter('cloudControl', new Float32Array(cloudUniform));
this.materialInstance.setFloat4Parameter('cloudControl2', new Float32Array(this.cloudControl2));
this.materialInstance.setFloat4Parameter('waterControl', new Float32Array(this.waterControl));
this.materialInstance.setFloat2Parameter('starControl', new Float32Array(this.starControl));
this.materialInstance.setFloatParameter('sunIntensity', physicalSunIntensity);
}

View File

@@ -1,8 +1,10 @@
# Result: /Users/mathias/sources/git/filament/out/release/filament/bin/matc
MATC="../../../out/release/filament/bin/matc"
# Result: /Users/mathias/sources/git/filament/out/cmake-release/tools/matc/matc
MATC="../../../../out/cmake-release/tools/matc/matc"
# Navigate to script directory to ensure relative paths work
cd "$(dirname "$0")"
set -e
$MATC -a opengl -p mobile -o assets/simulated_skybox.filamat simulated_skybox.mat
echo "Material recompiled to assets/simulated_skybox.filamat"

View File

@@ -32,8 +32,8 @@
<script src="lil-gui.js"></script>
<!-- App -->
<script src="SimulatedSkybox.js"></script>
<script src="main.js"></script>
<script src="SimulatedSkybox.js?v=26"></script>
<script src="main.js?v=26"></script>
</body>
</html>

View File

@@ -41,6 +41,12 @@ class App {
this.view.setColorGrading(this.colorGrading);
this.view.setPostProcessingEnabled(true); // Essential for tone mapping
// Bloom
this.view.setBloomOptions({
enabled: false,
lenseFlare: false
});
// Clear color is not really visible behind skybox, but black is standard
this.renderer.setClearOptions({ clearColor: [0.0, 0.0, 0.0, 1.0], clear: true });
@@ -198,12 +204,65 @@ class App {
cloudFolder.add(cParams, 'speed', 0.0, 200.0).onChange(v => sky.setCloudControl(cParams.coverage, cParams.density, cParams.height, v));
cloudFolder.add(cParams, 'evolution', 0.0, 2.0).onChange(v => sky.setCloudShapeEvolution(v));
const waterFolder = gui.addFolder('Water');
const wParams = {
derivativeTrick: true,
strength: 50.0,
speed: 1.0,
octaves: 4.0
};
// Initialize defaults
sky.setWaterControl(50.0, 1.0, 1.0, 4.0); // 1.0 = Derivative Trick On, 4 octaves
const updateWater = () => {
sky.setWaterControl(wParams.strength, wParams.speed, wParams.derivativeTrick ? 1.0 : 0.0, wParams.octaves);
};
waterFolder.add(wParams, 'derivativeTrick').name('Derivative Trick').onChange(updateWater);
waterFolder.add(wParams, 'strength', 10.0, 100.0).onChange(updateWater);
waterFolder.add(wParams, 'speed', 0.0, 5.0).onChange(updateWater);
waterFolder.add(wParams, 'octaves', 1, 8, 1).name('Octaves').onChange(updateWater);
waterFolder.close();
const starFolder = gui.addFolder('Stars');
const sParams = {
enabled: true,
density: 1.0
};
// Initialize defaults (Density 1.0, Enabled True)
sky.setStarControl(1.0, true);
const updateStars = () => {
sky.setStarControl(sParams.density, sParams.enabled);
};
starFolder.add(sParams, 'enabled').name('Enabled').onChange(updateStars);
starFolder.add(sParams, 'density', 0.0, 1.0).name('Density').onChange(updateStars);
starFolder.close();
const camFolder = gui.addFolder('Camera');
camFolder.add(this.params, 'focalLength', 8.0, 300.0).name('Focal Length').onChange(() => this.updateCameraProjection());
camFolder.add(this.params, 'aperture', 1.4, 32.0).onChange(() => this.updateCameraExposure());
camFolder.add(this.params, 'shutterSpeed', 1.0, 1000.0).onChange(() => this.updateCameraExposure());
camFolder.add(this.params, 'iso', 50.0, 3200.0).onChange(() => this.updateCameraExposure());
const bloomFolder = camFolder.addFolder('Bloom');
const bParams = {
enabled: false,
lensFlare: false
};
const updateBloom = () => {
this.view.setBloomOptions({
enabled: bParams.enabled,
lensFlare: bParams.lensFlare
});
};
bloomFolder.add(bParams, 'enabled').onChange(updateBloom);
bloomFolder.add(bParams, 'lensFlare').onChange(updateBloom);
bloomFolder.close();
// Collapse folders by default
sunDisk.close();
atmFolder.close();

View File

@@ -78,6 +78,16 @@ material {
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 : float2,
name : starControl, // x=Density, y=Enabled
precision : high
}
],
variables : [
@@ -169,6 +179,19 @@ fragment {
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.
@@ -210,23 +233,30 @@ fragment {
// @param freq Ripple frequency/density. (e.g. 20.0).
// @param maskHeight Horizon mask height (0.0 to 1.0). (e.g. 0.1).
// ------------------------------------------------------------------------
void applyHeatShimmer(inout highp vec3 V, highp float strength, highp float freq, highp float maskHeight) {
if (strength <= 0.0) return;
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) {
// Noise: Interleaved sine waves using World Space coordinates.
// Multiplying by freq controls the ripple density.
// Use FBM for organic turbulence (rising heat waves)
highp float time = getUserTime().x;
highp float noise = sin(V.x * freq + time * 5.0)
+ sin(V.z * freq * 1.3 - time * 3.7);
// 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 += noise * strength * mask * 0.01;
V.y += distortion * strength * mask * 0.1;
V = normalize(V);
}
return mask;
}
// ------------------------------------------------------------------------
@@ -300,6 +330,40 @@ fragment {
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
// ------------------------------------------------------------------------
@@ -376,10 +440,28 @@ fragment {
// @param transmittance Atmospheric Transmittance (Cloud Color Tint).
// @return Sky color composed with clouds.
// ------------------------------------------------------------------------
highp vec3 applyClouds(highp vec3 background, highp vec3 V, highp vec3 L,
highp vec4 control, highp vec4 control2, highp vec4 geometry,
highp float sunIntensity, highp vec3 transmittance) {
// ------------------------------------------------------------------------
// Procedural Cirrus Clouds
// ------------------------------------------------------------------------
// Renders a thin layer of high-altitude clouds (Cirrus) using 3D Noise.
//
// PARAMETERS:
// @param V .
// @param L .
// @param control .
// @param control2 .
// @param geometry .
// @param sunIntensity .
// @param transmittance.
// @param outDensity Output: Cloud Density (0..1).
// @return Cloud Lit Color (pre-multiplied by density? No, just lit color).
// ------------------------------------------------------------------------
highp vec3 getCloudLayer(highp vec3 V, highp vec3 L,
highp vec4 control, highp vec4 control2, highp vec4 geometry,
highp float sunIntensity, highp vec3 transmittance,
out highp float outDensity) {
outDensity = 0.0;
highp float cloudCoverage = control.x;
// Clip clouds below the horizon (Earth occlusion)
@@ -410,15 +492,28 @@ fragment {
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.
highp float silver = hgPhase(cosTheta, vec2(1.81, -1.8)) * 20.0;
// Attenuation (Beer's Law)
// Thick clouds block light.
// 20.0 is an artistic extinction coefficient.
highp float extinction = exp(-cloudDensity * 20.0);
highp float silver = hgPhase(cosTheta, vec2(1.81, -1.8)) * 40.0 * extinction;
// Ambient/Diffuse term.
// We allow some ambient light to pass through even thick clouds (0.05 min)
// so they don't look like black holes.
highp float ambient = 0.1 + 0.4 * extinction;
// Diffuse term (Sun Color) + Silver Lining
highp vec3 cloudLight = sunIntensity * transmittance * (0.2 + silver);
highp vec3 cloudLight = sunIntensity * transmittance * (ambient + silver);
// Mix based on density
highp float volumetric = control2.y;
@@ -449,13 +544,14 @@ fragment {
shading *= (1.0 - cloudDensity * 0.7);
}
return mix(background, cloudLight * shading, cloudDensity);
return cloudLight * shading;
}
}
}
return background;
return vec3(0.0);
}
// ------------------------------------------------------------------------
// Dynamic Tone Mapping
// ------------------------------------------------------------------------
@@ -481,6 +577,239 @@ fragment {
return pow(max(vec3(0.0), color), vec3(exponent));
}
// ------------------------------------------------------------------------
// 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.
// @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).
// @return Water surface color.
// ------------------------------------------------------------------------
// 3D Noise for Stars
highp float hash31(highp vec3 p) {
p = fract(p * 0.1031);
p += dot(p, p.yzx + 33.33);
return fract((p.x + p.y) * p.z);
}
highp float getStars(highp vec3 V, highp float density) {
// Simple procedural stars
// We use view vector direction to tile the sky
// Higher frequency = smaller stars
highp float frequency = 300.0;
highp vec3 p = floor(V * frequency);
highp float h = hash31(p);
// Threshold for stars (very sparse)
// param density: 0.0 (none) to 1.0 (max)
// Default threshold was 0.995 (0.5% stars)
// We map density 0.0 -> 1.0 threshold (no stars)
// density 1.0 -> 0.990 threshold (1.0% stars)
highp float threshold = 1.0 - (0.001 + density * 0.009);
highp float star = 0.0;
if (h > threshold) {
// Random brightness
highp float brightness = (h - threshold) / (1.0 - threshold);
star = brightness * 15.0; // Reduced from 50.0 to 15.0
}
return star;
}
// New helper to handle Star Compositing (Fade, Rotation, Occlusion)
highp vec3 getStarLayer(highp vec3 V, highp vec3 L, highp float cloudDensity, highp vec3 transmittance, highp vec2 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.
// 0.10 (5.7 deg up) -> 0.0
// -0.20 (11.5 deg down) -> 1.0
highp float starFade = 1.0 - smoothstep(-0.20, 0.10, 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, starControl.x);
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 * 0.1;
}
// ------------------------------------------------------------------------
// 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.
// @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).
// @return Water surface color.
// ------------------------------------------------------------------------
highp vec3 getWaterColor(highp vec3 V, highp vec3 L,
highp float sunIntensity,
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) {
// Project to plane y=0
highp float t = -10.0 / min(V.y, -0.0002); // Reduced clamp to minimize "wall" artifact
highp vec2 uv = V.xz * t * 0.05;
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
// 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);
// Reconstruct screen-space basis in world space
// highp vec3 sRight = normalize(dFdx(V)); // Moved inside block
// highp vec3 sUp = normalize(dFdy(V)); // Moved inside block
// 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)
float eps = 0.02; // Epsilon for gradient
vec3 p = vec3(uv, time * 0.1 * speed);
float hx = fbm(p + vec3(eps, 0.0, 0.0), octaves);
float hy = fbm(p + vec3(0.0, eps, 0.0), octaves);
// Gradient
float dx = (hx - h) / eps;
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);
highp vec3 transRefl;
highp vec3 reflection = getAtmosphere(R, L, sunIntensity,
depthR, depthM,
ozone, multiScatParams,
miePhaseParams,
transRefl);
// Clouds in reflection
highp float reflCloudDensity;
highp vec3 reflCloudLayer = getCloudLayer(R, L, materialParams.cloudControl, materialParams.cloudControl2,
materialParams.shimmerControl, materialParams.sunIntensity, transRefl,
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 0.1) as requested.
highp float rHorizonMask = 1.0 - smoothstep(0.0, 0.1, R.y);
if (rHorizonMask > 0.0) {
reflection += getStarLayer(R, L, reflCloudDensity, transRefl, materialParams.starControl) * rHorizonMask;
}
// Add Sun Disk to reflection (Occluded)
highp float reflSunAccess = 1.0 - smoothstep(0.0, 0.7, reflCloudDensity * 1.5);
reflection += getSunDisk(R, L, sunHalo, sunIntensity, transRefl) * reflSunAccess;
// Apply clouds to reflection
reflection = mix(reflection, reflCloudLayer, reflCloudDensity);
// Fresnel
highp float F0 = 0.02; // 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 = vec3(0.0, 0.005, 0.02); // Deep blue/black
highp vec3 waterColor = mix(deepColor, reflection, F);
return waterColor;
}
void material(inout MaterialInputs material) {
prepareMaterial(material);
@@ -488,20 +817,21 @@ fragment {
highp vec3 L = normalize(materialParams.sunDirection);
// 1. Heat Shimmer
applyHeatShimmer(V, materialParams.shimmerControl.x,
// 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 1
highp vec3 inScatter1 = getAtmosphere(V, L, materialParams.sunIntensity,
materialParams.depthR, materialParams.depthM,
materialParams.ozone, materialParams.multiScatParams,
materialParams.miePhaseParams,
transmittance);
// Sun 2 (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.
@@ -509,108 +839,67 @@ fragment {
highp vec3 inScatter2 = vec3(0.0);
if (materialParams.sunHalo2.w > 0.5) {
highp vec3 L2 = normalize(materialParams.sunDirection2);
// Re-calculate Phase for Sun 2
highp float cosTheta2 = dot(V, L2);
highp float rPhase2 = rayleighPhase(cosTheta2 * 0.5 + 0.5);
highp float mPhase2 = hgPhase(cosTheta2, materialParams.miePhaseParams);
// Re-calculate In-Scattering
// Note: We use the SAME optical depth/transmittance (view dependent), just different Phase & Light Intensity.
// multiScatParams.xyz (Ambient) is explicitly excluded for Sun 2.
highp vec3 scatteringTerm2 = (materialParams.depthR * rPhase2) + (materialParams.depthM * mPhase2);
// Reuse Extinction from first pass
highp vec3 totalExtinction = materialParams.depthR + materialParams.depthM + materialParams.ozone;
highp vec3 extinctionTerm = max(vec3(1e-6), totalExtinction); // Should be same as pass 1
highp vec3 inScattering2 = materialParams.sunIntensity2 * (scatteringTerm2 / extinctionTerm);
inScatter2 = inScattering2 * (1.0 - transmittance);
inScatter2 = getSecondarySunScattering(V, L2,
materialParams.sunIntensity2,
materialParams.depthR,
materialParams.depthM,
materialParams.ozone,
materialParams.miePhaseParams,
transmittance);
}
highp vec3 finalColor = inScatter1 + inScatter2;
// 3. Sun Disks
// 5. Procedural Clouds
highp float cloudDensity;
highp vec3 cloudLayer = getCloudLayer(V, L,
materialParams.cloudControl,
materialParams.cloudControl2,
materialParams.shimmerControl, // reusing w=PlanetRadius
materialParams.sunIntensity,
transmittance,
cloudDensity);
// Add Stars
// Stars are at infinity.
// Use helper function.
finalColor += getStarLayer(V, L, cloudDensity, transmittance, materialParams.starControl);
// 3. Sun Disks - Occluded by clouds
// Sun Access is (1.0 - cloudDensity) but arguably non-linear for sharp disk
highp float sunAccess = 1.0 - smoothstep(0.0, 0.7, cloudDensity * 1.5);
finalColor += getSunDisk(V, L, materialParams.sunHalo,
materialParams.sunIntensity, transmittance);
materialParams.sunIntensity, transmittance) * sunAccess;
if (materialParams.sunHalo2.w > 0.5) {
highp vec3 L2 = normalize(materialParams.sunDirection2);
// Note: Ideally we should compute cloud density for L2 direction if clouds are 3D...
// But here we use V direction clouds (view-based).
// Since clouds are in front of everything, this is correct for view-based occlusion.
finalColor += getSunDisk(V, L2, materialParams.sunHalo2,
materialParams.sunIntensity2, transmittance);
materialParams.sunIntensity2, transmittance) * sunAccess;
}
// 4. Night Sky Offset
finalColor += materialParams.nightColor;
// 5. Procedural Clouds
// We only light clouds with the Primary Sun for simplicity/cost.
finalColor = applyClouds(finalColor, V, L,
materialParams.cloudControl,
materialParams.cloudControl2,
materialParams.shimmerControl, // reusing w=PlanetRadius
materialParams.sunIntensity,
transmittance);
// 5. Apply Clouds
finalColor = mix(finalColor, cloudLayer, cloudDensity);
// 6. Dynamic Tone Mapping
finalColor = applyDynamicToneMapping(finalColor, L, materialParams.contrast);
if (V.y < 0.0) {
// Water Simulation
// Project to plane y=0
highp float t = -10.0 / min(V.y, -0.0002); // Reduced clamp to minimize "wall" artifact
highp vec2 uv = V.xz * t * 0.05;
highp float time = getUserTime().x;
uv += vec2(time * 0.5, time * 0.2);
// Wave Normal
// Use screen-space derivatives to compute world-space normal perturbation
highp float h = fbm(vec3(uv, time * 0.1));
// Reconstruct screen-space basis in world space
highp vec3 sRight = normalize(dFdx(V));
highp vec3 sUp = normalize(dFdy(V));
// 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 vec3 N_perturb = (sRight * dFdx(h) + sUp * dFdy(h)) * 50.0 * horizonFade;
highp vec3 N_water = normalize(vec3(0.0, 1.0, 0.0) - N_perturb);
// Reflection
highp vec3 R = reflect(V, N_water);
// Ensure R points up
R.y = max(R.y, 0.01);
highp vec3 transRefl;
highp vec3 reflection = getAtmosphere(R, L, materialParams.sunIntensity,
materialParams.depthR, materialParams.depthM,
materialParams.ozone, materialParams.multiScatParams,
materialParams.miePhaseParams,
transRefl);
// Add Sun Disk to reflection
reflection += getSunDisk(R, L, materialParams.sunHalo, materialParams.sunIntensity, transRefl);
// Clouds in reflection
reflection = applyClouds(reflection, R, L, materialParams.cloudControl, materialParams.cloudControl2,
materialParams.shimmerControl, materialParams.sunIntensity, transRefl);
// Fresnel
highp float F0 = 0.02; // 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 = vec3(0.0, 0.005, 0.02); // Deep blue/black
finalColor = mix(deepColor, reflection, F);
// Specular Highlight (Sun)
highp vec3 H = normalize(L - V);
highp float NdotH = max(0.0, dot(N_water, H));
highp float spec = pow(NdotH, 500.0);
finalColor += materialParams.sunIntensity * spec * 2.0 * transRefl; // Tinted by atmosphere
finalColor = getWaterColor(V, L, materialParams.sunIntensity,
materialParams.depthR, materialParams.depthM,
materialParams.ozone, materialParams.multiScatParams,
materialParams.miePhaseParams,
materialParams.sunHalo,
materialParams.cloudControl, materialParams.cloudControl2,
materialParams.shimmerControl,
materialParams.waterControl);
finalColor = applyDynamicToneMapping(finalColor, L, materialParams.contrast);
}
material.baseColor = vec4(finalColor, 1.0);