SkySim: add initial support for a moon (#9650)

- also update filament binaries

DOCS_FORCE
This commit is contained in:
Mathias Agopian
2026-01-28 01:25:03 -08:00
committed by GitHub
parent 11bf3a4493
commit 375e3a03ec
7 changed files with 214 additions and 13 deletions

View File

@@ -24,10 +24,17 @@ class SimulatedSkybox {
this.starControl = [1.0, 1.0]; // x=Density (0-1), y=Enabled (0-1)
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.initEntity();
}
@@ -220,6 +227,31 @@ class SimulatedSkybox {
this.updateCoefficients();
}
setMoonPosition(direction) {
// normalize
const len = Math.hypot(direction[0], direction[1], direction[2]);
if (len > 0) {
this.moonDirection = [direction[0] / len, direction[1] / len, direction[2] / len];
}
this.updateCoefficients();
}
setMoonIntensity(intensity) {
this.moonIntensity = Math.max(0.0, intensity);
this.updateCoefficients();
}
setMoonRadius(degrees) {
const rad = degrees * (Math.PI / 180.0);
this.moonHalo[0] = Math.cos(rad);
this.updateCoefficients();
}
setMoonEnabled(enabled) {
this.moonHalo[3] = enabled ? 1.0 : 0.0;
this.updateCoefficients();
}
updateCoefficients() {
if (!this.materialInstance) {
console.warn("updateCoefficients called before material loaded");
@@ -307,5 +339,19 @@ class SimulatedSkybox {
this.materialInstance.setFloat2Parameter('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
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));
}
}

File diff suppressed because one or more lines are too long

View File

@@ -32,8 +32,8 @@
<script src="lil-gui.js"></script>
<!-- App -->
<script src="SimulatedSkybox.js?v=39"></script>
<script src="main.js?v=39"></script>
<script src="SimulatedSkybox.js?v=47"></script>
<script src="main.js?v=47"></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').then(() => {
this.skybox.loadMaterial('assets/simulated_skybox.filamat?v=46').then(() => {
this.initGUI();
});
@@ -99,6 +99,12 @@ class App {
const exposure = this.getExposure();
const preExposedIntensity = this.params.sunIntensity * exposure;
this.skybox.setSunIntensity(preExposedIntensity);
// Moon Exposure
if (this.mParams) {
const preExposedMoon = this.mParams.intensity * exposure;
this.skybox.setMoonIntensity(preExposedMoon);
}
}
updateCameraProjection() {
@@ -139,6 +145,42 @@ class App {
// Updated: Controls params.sunIntensity and triggers updateSunIntensity
sunFolder.add(this.params, 'sunIntensity', 0.0, 500000.0).onChange(v => this.updateSunIntensity());
const moonFolder = gui.addFolder('Moon');
this.mParams = {
enabled: false,
azimuth: 180.0,
elevation: 45.0,
radius: 0.5,
intensity: 10.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 phi = az;
const x = Math.sin(theta) * Math.cos(phi);
const y = Math.cos(theta);
const z = Math.sin(theta) * Math.sin(phi);
sky.setMoonPosition([x, y, z]);
};
// Initial Moon Sync
updateMoon();
sky.setMoonEnabled(this.mParams.enabled);
sky.setMoonRadius(this.mParams.radius);
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.close();
const sunDisk = sunFolder.addFolder('Disk');
// We need local proxy for sunRadius due to conversion
this.diskParams = {
@@ -327,6 +369,7 @@ class App {
const w = this.wParams;
const s = this.sParams;
const b = this.bParams;
const m = this.mParams;
const sk = this.skybox;
return {
@@ -335,6 +378,7 @@ 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 },
k: {
t: sk.turbidity,
r: sk.rayleigh,
@@ -356,6 +400,7 @@ class App {
const w = state.w;
const s = state.s;
const b = state.b;
const m = state.m;
const k = state.k;
if (p) {
@@ -437,6 +482,28 @@ class App {
sky.setWaterControl(this.wParams.strength, this.wParams.speed, this.wParams.derivativeTrick ? 1.0 : 0.0, this.wParams.octaves);
sky.setStarControl(this.sParams.density, this.sParams.enabled);
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;
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 phi = az;
const x = Math.sin(theta) * Math.cos(phi);
const y = Math.cos(theta);
const z = Math.sin(theta) * Math.sin(phi);
sky.setMoonPosition([x, y, z]);
sky.setMoonEnabled(this.mParams.enabled);
sky.setMoonRadius(this.mParams.radius);
sky.setMoonIntensity(this.mParams.intensity);
}
this.view.setBloomOptions({
enabled: this.bParams.enabled,
lensFlare: this.bParams.lensFlare

View File

@@ -693,11 +693,93 @@ fragment {
// @param waterControl Water Control (x=Strength, y=Speed, z=DerivativeTrick).
// @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 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) {
@@ -793,6 +875,11 @@ fragment {
highp float reflSunAccess = 1.0 - smoothstep(0.0, 0.7, reflCloudDensity * 1.5);
reflection += getSunDisk(R, L, sunHalo, sunIntensity, transRefl) * reflSunAccess;
// Add Moon Disk to reflection
if (sunHalo2.w > 0.5) {
reflection += getMoonDisk(R, L2, L, sunHalo2, sunIntensity2, transRefl) * reflSunAccess;
}
// Apply clouds to reflection
reflection = mix(reflection, reflCloudLayer, reflCloudDensity);
@@ -810,6 +897,8 @@ fragment {
void material(inout MaterialInputs material) {
prepareMaterial(material);
@@ -832,13 +921,15 @@ fragment {
transmittance);
// Sun 2 (Optional)
// 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);
if (materialParams.sunHalo2.w > 0.5) {
highp vec3 L2 = normalize(materialParams.sunDirection2);
inScatter2 = getSecondarySunScattering(V, L2,
materialParams.sunIntensity2,
materialParams.depthR,
@@ -866,19 +957,15 @@ fragment {
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) * 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) * sunAccess;
// Pass L (Sun) as the light source for the Moon Phase
finalColor += getMoonDisk(V, L2, L, materialParams.sunHalo2,
materialParams.sunIntensity2, transmittance) * sunAccess;
}
// 4. Night Sky Offset
@@ -896,6 +983,7 @@ fragment {
materialParams.ozone, materialParams.multiScatParams,
materialParams.miePhaseParams,
materialParams.sunHalo,
L2, materialParams.sunIntensity2, materialParams.sunHalo2,
materialParams.cloudControl, materialParams.cloudControl2,
materialParams.shimmerControl,
materialParams.waterControl);