I have really enjoyed this, thank you for putting it together! However, I have noticed that sunrise and sunset are faster and offset in time from reality. So I spent some time looking into it. It runs out that the root cause is the underlying model using a simplified model that only models a single light scatter per ray. At Sunrise and Sunset multiple scatters dominate what we actually see. This is also why as soon as the sun drops below the horizon it returns black.
While the proper fix is to implement multiple scattering I have had pretty good success in faking it. Essentially, just move the sun higher in the sky during sunrise and sunset. So, replace sunPos.altitude with sunPos.altitude + offset. I have had pretty good success with this offset function AI assisted me in creating giving me results that are reasonable in matching the phases of twilight in my part of the world. (Sorry I'm not using TypeScript but it should be a pretty simple conversion to make this TypeScript.)
function getMultipleScatteringOffset(actualAltitude) {
const altDeg = actualAltitude * 180 / Math.PI;
// Multiple scattering adds indirect illumination, but we need to be careful
// not to shift sunset/sunrise too much. Real sunset colors begin when sun
// is still 6-10 degrees above the horizon.
// For high sun: minimal offset needed (single scattering is fairly accurate)
// For low sun near horizon: moderate offset to account for path length
// For sun below horizon: larger offset as multiple scattering dominates
if (altDeg > 20) {
// High sun: very minimal correction needed
return 2.0 * Math.PI / 180;
} else if (altDeg > -6) {
// Sun above horizon to civil twilight: gentle increase
// Use a smooth curve that doesn't shift sunset colors too much
const t = (20 - altDeg) / 26; // 0 at 20°, 1 at -6°
const smoothT = t * t * (3 - 2 * t); // smooth step function
return (2.0 + smoothT * 6.0) * Math.PI / 180; // 2° to 8°
} else if (altDeg > -12) {
// Civil to nautical twilight: slower increase to avoid reverse sunset
// The offset must increase slower than altitude decreases (rate < 1.0)
const t = (-6 - altDeg) / 6; // 0 at -6°, 1 at -12°
const smoothT = t * t * (3 - 2 * t);
return (8.0 + smoothT * 3.0) * Math.PI / 180; // 8° to 11° (only 3° increase over 6° range)
} else {
// Deep twilight: slow linear decay to avoid overly bright night sky
// Linear decay from 11° at -12° to 0° at -30°
const t = Math.max(0, (-12 - altDeg) / 18); // 0 at -12°, 1 at -30°
return (11.0 * (1 - t)) * Math.PI / 180;
}
}
P.S. I was also able to simulate light pollution with a very similar trick. Essentially, map light pollution levels to sun altitudes and then never let the sun's altitude drop below that. I'm currently using this mapping of Bortle scale to sun altitude
const lightPollutionOffsets = {
1: 0.0, // Pristine dark sky
2: 0.1, // Typical dark sky
3: 0.2, // Rural
4: 1.0, // Rural/suburban
5: 2.0, // Suburban
6: 3.0, // Bright suburban
7: 4.0, // Urban
8: 5.0, // City
9: 6.0 // Inner city
};
alt = lightPollutionOffsets[bortleNumber] - (3 * Math.PI / 180)); // Subtract 3° as single scattering provides light until roughly -3°.
I have really enjoyed this, thank you for putting it together! However, I have noticed that sunrise and sunset are faster and offset in time from reality. So I spent some time looking into it. It runs out that the root cause is the underlying model using a simplified model that only models a single light scatter per ray. At Sunrise and Sunset multiple scatters dominate what we actually see. This is also why as soon as the sun drops below the horizon it returns black.
While the proper fix is to implement multiple scattering I have had pretty good success in faking it. Essentially, just move the sun higher in the sky during sunrise and sunset. So, replace
sunPos.altitudewithsunPos.altitude + offset. I have had pretty good success with this offset function AI assisted me in creating giving me results that are reasonable in matching the phases of twilight in my part of the world. (Sorry I'm not using TypeScript but it should be a pretty simple conversion to make this TypeScript.)P.S. I was also able to simulate light pollution with a very similar trick. Essentially, map light pollution levels to sun altitudes and then never let the sun's altitude drop below that. I'm currently using this mapping of Bortle scale to sun altitude