Improve Moon/Earthshine, add Touch support (#9659)

- Fix Moon normal calculation in shader (was inverted).
- Implement physically based Earthshine (dynamic based on phase).
- Add Moon scattering to Water reflection.
- Fix Star occlusion (masked by Moon).
- Improved Stars
- Add mobile touch support (Orbit control) to Camera.

DOCS_FORCE
This commit is contained in:
Mathias Agopian
2026-01-28 23:32:06 -08:00
committed by GitHub
parent a1abfa30b8
commit ef24164464
6 changed files with 648 additions and 300 deletions

View File

@@ -0,0 +1,76 @@
# Analytic Skybox Sample
This sample demonstrates a **fully procedural, single-pass skybox shader** capable of simulating a dynamic day-night cycle, atmospheric scattering, volumetric clouds, and water reflections.
It is designed for graphics engineers and technical artists who need a lightweight yet physically plausible environment background without relying on static HDRI textures.
## Features & Performance
The shader uses a "Uber-Shader" approach where all features are computed per-pixel. Features can be toggled or tuned via uniforms to balance quality vs performance.
| Feature | Cost | Control | Description |
| :--- | :---: | :--- | :--- |
| **Atmosphere** | 🟡 **Medium** | `turbidity`, `rayleigh`, `mie` | Analytic Rayleigh & Mie scattering. Physically based colors. |
| **Sun Disk** | 🟢 **Low** | `sunHalo` (Radius, Limb) | Analytic sphere intersection with limb darkening. Conservation of energy (Lux). |
| **Moon & Earthshine** | 🟢 **Low** | `sunHalo2`, `moonIntensity` | Resolved moon disk with geometric phases and dynamic Earthshine. |
| **Stars** | 🟢 **Low** | `starControl` (Density) | High-frequency procedural noise. Occluded by clouds/moon. |
| **Clouds** | 🔴 **High** | `cloudControl` (Coverage, Density) | 4-Octave 3D Fractal Brownian Motion (FBM). Dominates the cost when enabled. |
| **Heat Shimmer** | 🟡 **Medium** | `shimmerControl` | UV perturbation near the horizon to simulate mirages. |
| **Water Reflection** | 🟣 **Very High** | `waterControl` | **Renders the sky twice**. Includes procedural waves (FBM) and fresnel. |
> **Note**: Rendering water (`V.y < 0`) is significantly more expensive (~2.5x) than the sky because it requires re-evaluating the atmospheric scattering and cloud noise for the reflection vector.
## Shader Techniques
### 1. Analytic Atmospheric Scattering
Based on the **Hoffman & Preetham** model. It solves the single-scattering integral analytically for air molecules (Rayleigh) and aerosols (Mie).
- **Rayleigh**: Produces the deep blue sky and red sunset colors.
- **Mie**: Produces the white halo around the sun and general haziness.
- **Optimization**: Uses a simplified optical depth approximation ("Air Mass") to avoid expensive ray-marching.
### 2. Procedural Clouds (3D Noise)
Clouds are rendered as a spherical shell at a specific altitude.
- **Technique**: Ray-sphere intersection finds the entry point, then **3D FBM Noise** determines density.
- **Lighting**: Uses a "Silver Lining" approximation (strong forward scattering) and Beers-Lambert attenuation for dark underbellies.
- **Animation**: The noise coordinate logic helps simulate wind drift and shape evolution over time.
### 3. Infinite Water Ocean
When looking below the horizon, the shader switches to "Water Mode".
- **Geometry**: A flat plane at $y=0$.
- **Waves**: Generated using **Derivative-Based Noise** (or Finite Difference). This creates slope vectors that perturb the normal without needing actual geometry.
- **Reflection**: A ray is cast from the water surface back into the sky ($R = \text{reflect}(V, N)$). The sky function is called again with $R$ to get the reflected color.
### 4. Dynamic Tone Mapping
Applies a custom tone mapping curve that varies with Sun Elevation.
- **Noon**: Linear/Gamma (Standard).
- **Sunset**: Higher contrast curve to compress the dynamic range and enhance the rich sunset oranges/purples.
## Integration
To use this in your own Filament application:
1. **Compile the Material**:
Use `matc` to compile `simulated_skybox.mat` into a `.filamat` file.
```bash
matc -p mobile -a opengl -o assets/simulated_skybox.filamat simulated_skybox.mat
```
2. **Load in JavaScript/C++**:
Create a Skybox entity and assign the material.
```javascript
// JavaScript Example
const material = engine.createMaterial('assets/simulated_skybox.filamat');
const skybox = engine.createSkybox(material);
scene.setSkybox(skybox);
```
3. **Update Uniforms**:
The shader requires specific uniforms (Sun Direction, Time, etc.) to be updated every frame. See `SimulatedSkybox.js` for a reference implementation of the uniform buffer management.
## References
* **Hoffman & Preetham (2002)**: *"Real-time Light-Atmosphere Interactions"*
* **Henyey & Greenstein (1941)**: *"Diffuse radiation in the galaxy"* (Mie Phase Function)
* **Kasten & Young (1989)**: *"Revised optical air mass tables"*
* **Three.js / Sky.js**: Empirical adjustments for "Golden Hour" aesthetics.

View File

@@ -21,19 +21,21 @@ class SimulatedSkybox {
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.starControl = [0.001, 1.0, 350.0, 0.01]; // x=Density, y=Enabled, z=Frequency, w=PixelScale
this.focalLength = 24.0;
this.height = 1000.0;
this.planetRadius = 6360.0;
// Sun Halo
// x=cos(rad), y=limbDarkening, z=intensity, w=enabled
// Sun Halo
// x=cos(rad), y=limbDarkening, z=intensity, w=enabled
this.sunHalo = [Math.cos(0.5 * Math.PI / 180.0), 0.5, 1.0, 1.0];
// Moon Parameters (Mapped to Secondary Sun)
this.moonDirection = [-0.2, 0.8, -0.2]; // Default Moon Pos
this.moonIntensity = 5000.0; // Dimmer than sun
this.moonHalo = [Math.cos(0.5 * Math.PI / 180.0), 0.0, 1.0, 0.0]; // Disabled by default
this.moonIntensity = 1.0; // Scale Factor (1.0 = Physical Peak)
// x=cos(rad), y=sin(rad) [Precision Fix], z=intensity, w=enabled
this.moonHalo = [Math.cos(0.5 * Math.PI / 180.0), Math.sin(0.5 * Math.PI / 180.0), 1.0, 0.0]; // Disabled by default
this.initEntity();
}
@@ -222,11 +224,48 @@ class SimulatedSkybox {
}
setStarControl(density, enabled) {
this.starControl[0] = Math.max(0.0, Math.min(1.0, density));
// Compensate for grid frequency reduction (350 -> 100)
// Fewer cells = fewer stars, so we increase density threshold.
// Factor ~ (350/100)^2 = 12.25
const compensatedDensity = density * 12.0;
this.starControl[0] = Math.max(0.0, Math.min(1.0, compensatedDensity));
this.starControl[1] = enabled ? 1.0 : 0.0;
this.updateCoefficients();
}
setFocalLength(mm) {
this.focalLength = Math.max(1.0, mm);
this.updateStarFrequency();
}
setResolution(height) {
this.height = Math.max(1.0, height);
this.updateStarFrequency();
}
updateStarFrequency() {
// World-Anchored Stars
// z = Fixed Frequency (World Space Grid)
// w = Pixel Scale (Screen Space Radius)
// Fixed Frequency: Defines the "Universe" coordinate system.
// Reduced to 100.0 to allow larger stars without clipping (square artifacts).
this.starControl[2] = 100.0;
// Pixel Scale in Radians
// We use linear scaling (24/f) instead of atan(fov) to ensure star size remains
// constant in pixels across all focal lengths (Perspective Projection).
const fovFactor = 24.0 / this.focalLength;
const pixelScale = (1.0 / this.height) * fovFactor;
// Pass to shader (w component)
// Target radius: 1.3 pixels (Diameter 2.6 pixels)
// Visible but sharp.
this.starControl[3] = pixelScale * 1.3;
this.updateCoefficients();
}
setMoonPosition(direction) {
// normalize
const len = Math.hypot(direction[0], direction[1], direction[2]);
@@ -244,6 +283,7 @@ class SimulatedSkybox {
setMoonRadius(degrees) {
const rad = degrees * (Math.PI / 180.0);
this.moonHalo[0] = Math.cos(rad);
this.moonHalo[1] = Math.sin(rad);
this.updateCoefficients();
}
@@ -329,29 +369,119 @@ class SimulatedSkybox {
this.materialInstance.setFloatParameter('contrast', this.contrast);
const nightColorScaled = this.nightColor.map(v => v * this.sunIntensity);
const nightColorScaled = this.nightColor.map(v => v * this.sunIntensity); // Lux scaling
this.materialInstance.setFloat3Parameter('nightColor', new Float32Array(nightColorScaled));
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.setFloat4Parameter('starControl', new Float32Array(this.starControl));
this.materialInstance.setFloatParameter('sunIntensity', physicalSunIntensity);
// Moon Upload (Secondary Sun)
this.materialInstance.setFloat3Parameter('sunDirection2', new Float32Array(this.moonDirection));
this.materialInstance.setFloatParameter('sunIntensity2', this.moonIntensity); // No atmospheric fade needed for Moon light source itself?
// Actually, Moon light should also fade near horizon if we wanted realism, but let's keep it simple for now.
// Or apply same fade? Moon is outside atmosphere.
// Let's apply basic zenith fade to it too? Maybe not.
// Moon Halo Upload
// Calculate Moon Phase Factor (Lambertian Sphere)
// We model the moon as a Lambertian sphere to calculate its integrated brightness (illuminance)
// based on the phase angle (angle between Sun-Moon and Observer-Moon vectors).
//
// Phase Angle (alpha):
// For a distant observer (Earth), the phase angle can be approximated as the angle between
// the vector to the Sun and the vector to the Earth (from the Moon).
// cos(alpha) = -dot(L_moon, L_sun)
//
// Lambertian Phase Law:
// The integrated flux of a lit sphere varies as:
// Phi(alpha) = (1/PI) * (sin(alpha) + (PI - alpha) * cos(alpha))
// This gives 1.0 at Full Moon (alpha=0) and 0.0 at New Moon (alpha=PI).
const dotSM = this.sunDirection[0] * this.moonDirection[0] +
this.sunDirection[1] * this.moonDirection[1] +
this.sunDirection[2] * this.moonDirection[2];
// Final Intensity = Peak * Scale (No Phase Factor - Phase is handled in Shader via N.L)
const MOON_PEAK_LUX = 5000.0;
const finalMoonIntensity = MOON_PEAK_LUX * this.moonIntensity;
this.materialInstance.setFloatParameter('sunIntensity2', finalMoonIntensity);
// Moon Halo Upload (Disk Visualization)
// Multiplier = 1.0 / SolidAngle
const moonSolidAngle = 2.0 * F_PI * (1.0 - this.moonHalo[0]);
const moonRadConv = 1.0 / Math.max(1e-9, moonSolidAngle);
const moonHaloUpload = [...this.moonHalo];
moonHaloUpload[2] *= moonRadConv;
this.materialInstance.setFloat4Parameter('sunHalo2', new Float32Array(moonHaloUpload));
// Solar Eclipse (CPU Calculation)
const sunRadius = Math.acos(this.sunHalo[0]);
const moonRadius = Math.acos(this.moonHalo[0]);
// Dot product of Sun and Moon directions
const dot = this.sunDirection[0] * this.moonDirection[0] +
this.sunDirection[1] * this.moonDirection[1] +
this.sunDirection[2] * this.moonDirection[2];
const separation = Math.acos(Math.max(-1.0, Math.min(1.0, dot)));
let eclipseFactor = 1.0;
// Only calculate if moon is enabled
if (this.moonHalo[3] > 0.5) {
const overlap = this.areaIntersection(sunRadius, moonRadius, separation);
const sunArea = Math.PI * sunRadius * sunRadius;
// Ensure we don't divide by zero and clamp result
const ratio = overlap / Math.max(1e-9, sunArea);
eclipseFactor = 1.0 - Math.max(0.0, Math.min(1.0, ratio));
}
// Safety check for NaN
if (isNaN(eclipseFactor)) {
console.warn("SimulatedSkybox: eclipseFactor is NaN, resetting to 1.0");
eclipseFactor = 1.0;
}
this.materialInstance.setFloatParameter('eclipseFactor', eclipseFactor);
}
areaIntersection(r1, r2, d) {
// Circle intersection area
// r1, r2: radii
// d: distance between centers
// Case 1: Too far apart
if (d >= r1 + r2) {
return 0.0;
}
// Case 2: One inside another
if (d <= Math.abs(r1 - r2)) {
return Math.PI * Math.min(r1, r2) * Math.min(r1, r2);
}
const r1sq = r1 * r1;
const r2sq = r2 * r2;
// Law of Cosines for sector angles
// c1 = (d^2 + r1^2 - r2^2) / (2 * d * r1)
// c2 = (d^2 + r2^2 - r1^2) / (2 * d * r2)
// We clamp to [-1, 1] to avoid NaN from floating point errors
const c1 = Math.max(-1.0, Math.min(1.0, (d * d + r1sq - r2sq) / (2.0 * d * r1)));
const c2 = Math.max(-1.0, Math.min(1.0, (d * d + r2sq - r1sq) / (2.0 * d * r2)));
const part1 = r1sq * Math.acos(c1);
const part2 = r2sq * Math.acos(c2);
// Heron's formula for the triangle area * 2 (or just 0.5 * sin(angle) *r*r but we have sides)
// part3 is Area of kite? No, part3 is sum of two triangles?
// Formula: Area = r1^2 * acos(c1) + r2^2 * acos(c2) - 0.5 * sqrt...
// The sqrt term represents the area of the two triangles formed by the chord and centers.
// Robust sqrt
const val = (-d + r1 + r2) * (d + r1 - r2) * (d - r1 + r2) * (d + r1 + r2);
const part3 = 0.5 * Math.sqrt(Math.max(0.0, val));
return part1 + part2 - part3;
}
}

View File

@@ -24,6 +24,7 @@
<body>
<canvas></canvas>
<!-- Filament -->
<script src="filament.js"></script>
<script src="gl-matrix-min.js"></script>
@@ -32,8 +33,8 @@
<script src="lil-gui.js"></script>
<!-- App -->
<script src="SimulatedSkybox.js?v=47"></script>
<script src="main.js?v=47"></script>
<script src="SimulatedSkybox.js?v=51"></script>
<script src="main.js?v=51"></script>
</body>
</html>

View File

@@ -21,7 +21,7 @@ class App {
// But Filament.init assets are for internal or easy access via assets object if configured?
// Let's just let SimulatedSkybox fetch it again or use a blob if we wanted.
// Simpler: Just let SimulatedSkybox fetch it.
this.skybox.loadMaterial('assets/simulated_skybox.filamat?v=46').then(() => {
this.skybox.loadMaterial('assets/simulated_skybox.filamat?v=56').then(() => {
this.initGUI();
});
@@ -112,6 +112,7 @@ class App {
const height = this.canvas.height;
const aspect = width / height;
this.camera.setLensProjection(this.params.focalLength, aspect, 0.1, 5000.0);
if (this.skybox) this.skybox.setFocalLength(this.params.focalLength);
}
initGUI() {
@@ -134,31 +135,39 @@ class App {
sky.setSunPosition([x, y, z]);
};
// Sun UI Proxy
this.sunUI = {
azimuth: (this.params.sunPhi * 180.0 / Math.PI) % 360.0,
height: Math.cos(this.params.sunTheta)
};
if (this.sunUI.azimuth < 0) this.sunUI.azimuth += 360.0;
const sunFolder = gui.addFolder('Sun');
// Helper for "Sun Height" cosine slider like C++
this.sunHeightParam = { height: Math.cos(this.params.sunTheta) };
sunFolder.add(this.sunHeightParam, 'height', -0.2, 1.0).name('Height (Cos)').onChange(v => {
sunFolder.add(this.sunUI, 'azimuth', 0.0, 360.0, 0.1).name('Azimuth').onChange(v => {
this.params.sunPhi = v * (Math.PI / 180.0);
updateSun();
});
sunFolder.add(this.sunUI, 'height', -0.2, 1.0).name('Height (Cos)').onChange(v => {
this.params.sunTheta = Math.acos(v);
updateSun();
});
sunFolder.add(this.params, 'sunPhi', 0.0, Math.PI * 2).name('Azimuth').onChange(updateSun);
// Updated: Controls params.sunIntensity and triggers updateSunIntensity
sunFolder.add(this.params, 'sunIntensity', 0.0, 500000.0).onChange(v => this.updateSunIntensity());
sunFolder.add(this.params, 'sunIntensity', 0.0, 500000.0).name('Intensity').onChange(v => this.updateSunIntensity());
const moonFolder = gui.addFolder('Moon');
this.mParams = {
enabled: false,
enabled: true,
azimuth: 180.0,
elevation: 45.0,
height: Math.cos(45.0 * Math.PI / 180.0), // Default 45 degrees elevation -> cos(45) ~ 0.707
radius: 0.5,
intensity: 10.0
intensity: 1.0
};
const updateMoon = () => {
const az = this.mParams.azimuth * (Math.PI / 180.0);
const el = this.mParams.elevation * (Math.PI / 180.0);
const theta = Math.PI / 2.0 - el;
const theta = Math.acos(this.mParams.height);
const phi = az;
const x = Math.sin(theta) * Math.cos(phi);
@@ -175,10 +184,10 @@ class App {
sky.setMoonIntensity(this.mParams.intensity);
moonFolder.add(this.mParams, 'enabled').name('Enabled').onChange(v => sky.setMoonEnabled(v));
moonFolder.add(this.mParams, 'azimuth', 0.0, 360.0).onChange(updateMoon);
moonFolder.add(this.mParams, 'elevation', -90.0, 90.0).onChange(updateMoon);
moonFolder.add(this.mParams, 'radius', 0.1, 5.0).onChange(v => sky.setMoonRadius(v));
moonFolder.add(this.mParams, 'intensity', 0.0, 1000.0).onChange(v => this.updateSunIntensity()); // Reuse update function to apply exposure
moonFolder.add(this.mParams, 'azimuth', 0.0, 360.0, 0.1).name('Azimuth').onChange(updateMoon);
moonFolder.add(this.mParams, 'height', -0.2, 1.0).name('Height (Cos)').onChange(updateMoon);
moonFolder.add(this.mParams, 'intensity', 0.0, 10.0).name('Intensity').onChange(v => this.updateSunIntensity());
moonFolder.add(this.mParams, 'radius', 0.1, 5.0).name('Radius').onChange(v => sky.setMoonRadius(v));
moonFolder.close();
const sunDisk = sunFolder.addFolder('Disk');
@@ -204,11 +213,11 @@ class App {
atmFolder.add(sky, 'mieG', 0.0, 0.999).onChange(v => sky.setMieG(v));
const artFolder = gui.addFolder('Artistic');
// Set Horizon Glow default to 1.0
sky.setHorizonGlow(1.0);
sky.msFactors[2] = 1.0;
// Set Contrast default to 0.85
sky.setContrast(0.85);
// Set Horizon Glow default to 0.0
sky.setHorizonGlow(0.0);
sky.msFactors[2] = 0.0;
// Set Contrast default to 1.0
sky.setContrast(1.0);
artFolder.add(sky.msFactors, 0, 0.0, 2.0).name('MS Rayleigh').onChange(v => sky.setMultiScattering(v, sky.msFactors[1]));
artFolder.add(sky.msFactors, 1, 0.0, 2.0).name('MS Mie').onChange(v => sky.setMultiScattering(sky.msFactors[0], v));
@@ -269,17 +278,17 @@ class App {
const starFolder = gui.addFolder('Stars');
this.sParams = {
enabled: true,
density: 1.0
density: 0.001
};
// Initialize defaults (Density 1.0, Enabled True)
sky.setStarControl(1.0, true);
// Initialize defaults (Density 0.001, Enabled True)
sky.setStarControl(0.001, true);
const updateStars = () => {
sky.setStarControl(this.sParams.density, this.sParams.enabled);
};
starFolder.add(this.sParams, 'enabled').name('Enabled').onChange(updateStars);
starFolder.add(this.sParams, 'density', 0.0, 1.0).name('Density').onChange(updateStars);
starFolder.add(this.sParams, 'density', 0.0, 0.01, 0.0001).name('Density').onChange(updateStars);
starFolder.close();
const camFolder = gui.addFolder('Camera');
@@ -378,7 +387,8 @@ class App {
w: { dt: w.derivativeTrick, st: w.strength, s: w.speed, o: w.octaves },
s: { e: s.enabled, d: s.density },
b: { e: b.enabled, lf: b.lensFlare },
m: { e: m.enabled, az: m.azimuth, el: m.elevation, r: m.radius, i: m.intensity },
m: { e: m.enabled, az: m.azimuth, h: m.height, r: m.radius, i: m.intensity },
cm: { t: this.camState.theta, p: this.camState.phi },
k: {
t: sk.turbidity,
r: sk.rayleigh,
@@ -392,6 +402,7 @@ class App {
hl: [...sk.sunHalo]
}
};
}
applyURLState(state) {
@@ -402,6 +413,7 @@ class App {
const b = state.b;
const m = state.m;
const k = state.k;
const cm = state.cm;
if (p) {
if (p.a !== undefined) this.params.aperture = p.a;
@@ -469,9 +481,16 @@ class App {
sky.updateCoefficients();
}
// Update derived Sun Height param for UI
if (this.sunHeightParam) {
this.sunHeightParam.height = Math.cos(this.params.sunTheta);
if (cm) {
if (cm.t !== undefined) this.camState.theta = cm.t;
if (cm.p !== undefined) this.camState.phi = cm.p;
}
// Update derived Sun UI
if (this.sunUI) {
this.sunUI.height = Math.cos(this.params.sunTheta);
this.sunUI.azimuth = (this.params.sunPhi * 180.0 / Math.PI) % 360.0;
if (this.sunUI.azimuth < 0) this.sunUI.azimuth += 360.0;
}
// Apply Local Params via Setters
@@ -485,14 +504,19 @@ class App {
if (m) {
if (m.e !== undefined) this.mParams.enabled = m.e;
if (m.az !== undefined) this.mParams.azimuth = m.az;
if (m.el !== undefined) this.mParams.elevation = m.el;
// Compat: if 'el' exists (old link) convert to 'h'
if (m.h !== undefined) {
this.mParams.height = m.h;
} else if (m.el !== undefined) {
// Convert elevation degrees to height cos
this.mParams.height = Math.cos(m.el * Math.PI / 180.0);
}
if (m.r !== undefined) this.mParams.radius = m.r;
if (m.i !== undefined) this.mParams.intensity = m.i;
// Sync Moon
const az = this.mParams.azimuth * (Math.PI / 180.0);
const el = this.mParams.elevation * (Math.PI / 180.0);
const theta = Math.PI / 2.0 - el;
const theta = Math.acos(this.mParams.height);
const phi = az;
const x = Math.sin(theta) * Math.cos(phi);
const y = Math.cos(theta);
@@ -504,6 +528,11 @@ class App {
sky.setMoonIntensity(this.mParams.intensity);
}
if (cm) {
if (cm.t !== undefined) this.camState.theta = cm.t;
if (cm.p !== undefined) this.camState.phi = cm.p;
}
this.view.setBloomOptions({
enabled: this.bParams.enabled,
lensFlare: this.bParams.lensFlare
@@ -516,7 +545,10 @@ class App {
const y = Math.cos(theta);
const z = Math.sin(theta) * Math.sin(phi);
sky.setSunPosition([x, y, z]);
this.updateSunIntensity();
// Update Camera Projection (Focal Length) and Exposure (Aperture/Shutter/ISO)
this.updateCameraProjection();
this.updateCameraExposure();
}
initControls() {
@@ -546,6 +578,36 @@ class App {
this.camState.phi = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, this.camState.phi));
});
// Touch support
this.canvas.addEventListener('touchstart', e => {
if (e.touches.length > 0) {
e.preventDefault(); // Prevent scroll/long-press
this.camState.dragging = true;
this.camState.lastX = e.touches[0].clientX;
this.camState.lastY = e.touches[0].clientY;
}
}, { passive: false });
window.addEventListener('touchend', () => {
this.camState.dragging = false;
});
window.addEventListener('touchmove', e => {
if (!this.camState.dragging || e.touches.length === 0) return;
e.preventDefault(); // Prevent scrolling
const x = e.touches[0].clientX;
const y = e.touches[0].clientY;
const dx = x - this.camState.lastX;
const dy = y - this.camState.lastY;
this.camState.lastX = x;
this.camState.lastY = y;
const sensitivity = 0.005;
this.camState.theta -= dx * sensitivity;
this.camState.phi += dy * sensitivity;
this.camState.phi = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, this.camState.phi));
}, { passive: false });
}
@@ -582,5 +644,6 @@ class App {
const aspect = width / height;
// near=0.1, far=5000.0
this.camera.setLensProjection(this.params.focalLength, aspect, 0.1, 5000.0);
if (this.skybox) this.skybox.setResolution(height);
}
}

View File

@@ -44,6 +44,11 @@ material {
name : ozone,
precision : high
},
{
type : float,
name : eclipseFactor,
precision : high
},
{
type : float4,
name : multiScatParams, // xyz=MultiScatteringColor, w=HorizonGlow
@@ -85,8 +90,8 @@ material {
precision : high
},
{
type : float2,
name : starControl, // x=Density, y=Enabled
type : float4,
name : starControl, // x=Density, y=Enabled, z=Frequency, w=PixelScale
precision : high
}
],
@@ -126,6 +131,38 @@ fragment {
#define PI 3.14159265359
// --- CONFIGURATION ---
// Stars
#define STAR_GLOBAL_INTENSITY 150.0 // Master brightness multiplier [0.0 - 500.0]
#define STAR_BRIGHTNESS_BASE 0.5 // Minimum random brightness [0.0 - 1.0]
#define STAR_BRIGHTNESS_VAR 4.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)
@@ -277,8 +314,8 @@ fragment {
// @param depthR Rayleigh Optical Depth (Precalculated).
// @param depthM Mie Optical Depth (Precalculated).
// @param ozone Ozone Absorption (Precalculated).
// @param msFactors Multi-Scattering factors (Rayleigh, Mie, Glow).
// @param mieG Mie Phase Anisotropy.
// @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).
// ------------------------------------------------------------------------
@@ -323,7 +360,7 @@ fragment {
// 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, 5.0)) * multiScatParams.w;
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);
@@ -430,7 +467,6 @@ fragment {
// - Lighting includes Silver Lining (HG Phase) and Atmospheric Extinction.
//
// PARAMETERS:
// @param background Current Sky Color (to be blended with).
// @param V Normalized View Vector.
// @param L Normalized Sun Vector.
// @param control x=Coverage, y=Density, z=QuadraticConst(C), w=WindSpeed.
@@ -438,23 +474,8 @@ fragment {
// @param geometry w=PlanetRadius (Re).
// @param sunIntensity Sun Illuminance.
// @param transmittance Atmospheric Transmittance (Cloud Color Tint).
// @return Sky color composed with clouds.
// ------------------------------------------------------------------------
// ------------------------------------------------------------------------
// 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).
// @param outDensity Output: Cloud pixel density (0..1) for compositing.
// @return Cloud Radiance (Lit color * attenuation * shading).
// ------------------------------------------------------------------------
highp vec3 getCloudLayer(highp vec3 V, highp vec3 L,
highp vec4 control, highp vec4 control2, highp vec4 geometry,
@@ -478,8 +499,8 @@ fragment {
highp float time = getUserTime().x;
// UV Mapping (Planar projected onto sphere cap is sufficient for skybox)
// Scale factor 0.05 km^-1
highp vec2 uv = (p.xz * 0.05) + vec2(time * speed * 2.0, 0.0);
// 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));
@@ -502,15 +523,18 @@ fragment {
// Attenuation (Beer's Law)
// Thick clouds block light.
// 20.0 is an artistic extinction coefficient.
highp float extinction = exp(-cloudDensity * 20.0);
// CLOUD_EXTINCTION is an artistic extinction coefficient.
highp float extinction = exp(-cloudDensity * CLOUD_EXTINCTION);
highp float silver = hgPhase(cosTheta, vec2(1.81, -1.8)) * 40.0 * 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 (0.05 min)
// 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 = 0.1 + 0.4 * extinction;
highp float ambient = CLOUD_AMBIENT_MIN + 0.4 * extinction;
// Diffuse term (Sun Color) + Silver Lining
highp vec3 cloudLight = sunIntensity * transmittance * (ambient + silver);
@@ -540,8 +564,8 @@ fragment {
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 * 0.7);
// Aggressively darken center of clouds
shading *= (1.0 - cloudDensity * VOLUMETRIC_SHADOW_STR);
}
return cloudLight * shading;
@@ -577,75 +601,83 @@ 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);
// ------------------------------------------------------------------------------------------------
// 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);
}
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 getStars(highp vec3 V) {
bool enabled = materialParams.starControl.y > 0.5;
if (!enabled) return 0.0;
highp float h = hash31(p);
highp float density = materialParams.starControl.x;
if (density <= 0.0) return 0.0;
// 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
// 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 star;
return intensity;
}
// 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) {
highp vec3 getStarLayer(highp vec3 V, highp vec3 L, highp 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.
// 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);
// 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);
@@ -657,13 +689,96 @@ fragment {
V.z
);
highp float starVal = getStars(rotV, starControl.x);
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 * 0.1;
// 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;
}
// ------------------------------------------------------------------------
// 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) {
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));
highp float NdotL = max(0.0, dot(N_sphere, L_sun));
// 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));
// Intensity relative to Sun ~ 0.0001 to 0.01 depending on albedo/size.
// We use 0.002 as a base scale.
// Visibility: The dark side faces Earth?
// Yes, dot(N_sphere, -L_moon) > 0 covers the face seen from Earth.
// But simpler: Is this pixel in shadow (NdotL == 0)?
// Earthshine adds ambient to the whole sphere, but is overwhelmed by Sun.
highp float earthshine = MOON_EARTHSHINE * earthPhase * clamp(dot(N_sphere, -L_moon) + 1.0, 0.0, 1.0); // Hemisphere ambient
return ((NdotL + earthshine) * moonIntensity * moonDiskIntensity * moonProfile) * transmittance;
}
return vec3(0.0);
}
// ------------------------------------------------------------------------
@@ -680,7 +795,8 @@ fragment {
// PARAMETERS:
// @param V Normalized View Vector.
// @param L Normalized Sun Vector.
// @param sunIntensity Sun Illuminance.
// @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.
@@ -691,117 +807,31 @@ fragment {
// @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.
// ------------------------------------------------------------------------
// ------------------------------------------------------------------------
// 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=LimbDarkening, 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) {
highp float moonCosRadius = moonParams.x;
// highp float limbDarkening = moonParams.y; // Unused for moon currently, maybe for earthshine?
highp float moonDiskIntensity = moonParams.z;
bool moonEnabled = moonParams.w > 0.5;
highp float cosTheta = dot(V, L_moon);
highp float dist = 1.0 - cosTheta;
highp float diskRadius = max(1e-6, 1.0 - moonCosRadius); // Approx Radius^2 / 2
// Soft Edge
highp float moonProfile = 1.0 - smoothstep(diskRadius, diskRadius + 0.00002, 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(acos(moonCosRadius)).
highp float sinRadius = sqrt(1.0 - moonCosRadius * moonCosRadius);
highp vec3 N_perp = T / sinRadius;
// Parallel component (pointing to viewer)
// N_para = -L_moon * sqrt(1 - |N_perp|^2)
highp float perpSq = dot(N_perp, N_perp);
highp vec3 N_local = vec3(0.0);
if (perpSq < 1.0) {
highp float para = sqrt(1.0 - perpSq);
N_local = N_perp - L_moon * para;
} else {
// Edge case precision fix
N_local = N_perp;
}
highp vec3 N = normalize(N_local);
// 2. Compute Phase (Sun Lighting)
highp float NdotL = dot(N, L_sun);
// Terminator softening (0.1 radian width)
highp float phase = smoothstep(-0.05, 0.05, NdotL);
// Earthshine (Ambient fill on dark side)
// Fake it as a small constant + maybe limb effect?
highp float earthshine = 0.02; // 2% brightness on dark side
highp float lighting = max(earthshine, phase);
// Texture? (Procedural Craters - Future work)
// For now just white disk
return moonIntensity * transmittance * lighting * moonDiskIntensity * moonProfile;
}
return vec3(0.0);
}
highp vec3 getWaterColor(highp vec3 V, highp vec3 L,
highp float sunIntensity,
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 vec3 L2, highp float sunIntensity2, highp vec4 sunHalo2,
highp vec4 cloudControl, highp vec4 cloudControl2,
highp vec4 shimmerControl, highp vec4 waterControl) {
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) {
// 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 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
// 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
@@ -848,47 +878,77 @@ fragment {
// Reflection
highp vec3 R = reflect(V, N_water);
highp vec3 transRefl;
highp vec3 reflection = getAtmosphere(R, L, sunIntensity,
depthR, depthM,
ozone, multiScatParams,
miePhaseParams,
transRefl);
// 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) {
reflection += getSecondarySunScattering(R, L2, sunIntensity2,
depthR, depthM, ozone,
miePhaseParams,
outTransmittance);
}
// Clouds in reflection
highp float reflCloudDensity;
highp vec3 reflCloudLayer = getCloudLayer(R, L, materialParams.cloudControl, materialParams.cloudControl2,
materialParams.shimmerControl, materialParams.sunIntensity, transRefl,
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 0.1) as requested.
highp float rHorizonMask = 1.0 - smoothstep(0.0, 0.1, 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, transRefl, materialParams.starControl) * rHorizonMask;
reflection += getStarLayer(R, L, reflCloudDensity, outTransmittance, 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;
// 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, transRefl) * reflSunAccess;
reflection += getMoonDisk(R, L2, L, sunHalo2, sunIntensity2, outTransmittance);
}
// Apply clouds to reflection
reflection = mix(reflection, reflCloudLayer, reflCloudDensity);
// Fresnel
highp float F0 = 0.02; // Water
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 = vec3(0.0, 0.005, 0.02); // Deep blue/black
highp vec3 deepColor = WATER_DEEP_COLOR; // Deep blue/black
highp vec3 waterColor = mix(deepColor, reflection, F);
@@ -896,9 +956,6 @@ fragment {
}
void material(inout MaterialInputs material) {
prepareMaterial(material);
@@ -914,13 +971,7 @@ fragment {
// 2. Atmospheric Scattering
highp vec3 transmittance;
highp vec3 inScatter1 = getAtmosphere(V, L, materialParams.sunIntensity,
materialParams.depthR, materialParams.depthM,
materialParams.ozone, materialParams.multiScatParams,
materialParams.miePhaseParams,
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.
@@ -929,6 +980,20 @@ fragment {
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;
// 1. Atmosphere Scattering
highp vec3 color = getAtmosphere(V, L, sunIntensityLit, materialParams.depthR, materialParams.depthM, materialParams.ozone,
materialParams.multiScatParams, materialParams.miePhaseParams, transmittance);
// 2. Secondary Sun/Moon Scattering
if (materialParams.sunHalo2.w > 0.5) {
inScatter2 = getSecondarySunScattering(V, L2,
materialParams.sunIntensity2,
@@ -937,60 +1002,73 @@ fragment {
materialParams.ozone,
materialParams.miePhaseParams,
transmittance);
color += inScatter2;
}
highp vec3 finalColor = inScatter1 + inScatter2;
// 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. Clouds
highp float cloudDensityVal;
highp vec3 clouds = getCloudLayer(V, L, materialParams.cloudControl, materialParams.cloudControl2, materialParams.shimmerControl,
sunIntensityLit, transmittance, cloudDensityVal);
// 3. Sun Disks - Occluded by clouds
highp float sunAccess = 1.0 - smoothstep(0.0, 0.7, cloudDensity * 1.5);
// 4. 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);
finalColor += getSunDisk(V, L, materialParams.sunHalo,
materialParams.sunIntensity, transmittance) * sunAccess;
// Moon Occlusion Mask
highp float moonOcclusion = 0.0;
if (materialParams.sunHalo2.w > 0.5) {
// Pass L (Sun) as the light source for the Moon Phase
finalColor += getMoonDisk(V, L2, L, materialParams.sunHalo2,
materialParams.sunIntensity2, transmittance) * sunAccess;
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).
// Since we logic-ed it as: if dist < rad, occlusion = 1.0. Correct.
sunDisk *= (1.0 - moonOcclusion) * sunAccess;
color += sunDisk;
// 5. Moon Disk
if (materialParams.sunHalo2.w > 0.5) {
color += getMoonDisk(V, L2, L, materialParams.sunHalo2,
materialParams.sunIntensity2, transmittance) * sunAccess;
}
// 4. Night Sky Offset
finalColor += materialParams.nightColor;
// 6. Stars
// Add stars before clouds (clouds cover stars)
color += getStarLayer(V, L, cloudDensityVal, transmittance, materialParams.starControl) * (1.0 - moonOcclusion);
// 5. Apply Clouds
finalColor = mix(finalColor, cloudLayer, cloudDensity);
// 7. 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);
// 6. Dynamic Tone Mapping
finalColor = applyDynamicToneMapping(finalColor, L, materialParams.contrast);
// 8. Night Sky Offset
color += materialParams.nightColor;
// 9. Dynamic Tone Mapping
color = applyDynamicToneMapping(color, L, materialParams.contrast);
// 10. Water Surface (Reflection)
if (V.y < 0.0) {
finalColor = getWaterColor(V, L, materialParams.sunIntensity,
color = getWaterColor(V, L, sunIntensityLit, materialParams.sunIntensity,
materialParams.depthR, materialParams.depthM,
materialParams.ozone, materialParams.multiScatParams,
materialParams.miePhaseParams,
materialParams.sunHalo,
L2, materialParams.sunIntensity2, materialParams.sunHalo2,
materialParams.cloudControl, materialParams.cloudControl2,
materialParams.shimmerControl,
materialParams.waterControl);
finalColor = applyDynamicToneMapping(finalColor, L, materialParams.contrast);
materialParams.waterControl,
L2, materialParams.sunIntensity2, materialParams.sunHalo2);
color = applyDynamicToneMapping(color, L, materialParams.contrast);
}
material.baseColor = vec4(finalColor, 1.0);
material.baseColor = vec4(color, 1.0);
}
}