use glam::Vec3;
use crate::{Color, Lighting};
use super::linear;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Sky {
pub atmosphere: bool,
pub haze: f32,
pub sun_size: f32,
pub stars: f32,
pub night_light: Color,
pub fog_density: f32,
pub fog_height: f32,
pub fog_falloff: f32,
pub fog_color: Color,
pub fog_start: f32,
pub fog_end: f32,
pub fog_sun_glow: f32,
pub fog_sun_focus: f32,
pub light_shafts: f32,
}
impl Default for Sky {
fn default() -> Self {
Self {
atmosphere: true,
haze: 1.0,
sun_size: 1.5,
stars: 1.0,
night_light: Color::hex(0x2C3A56),
fog_density: 0.0,
fog_height: 0.0,
fog_falloff: 50.0,
fog_color: Color::hex(0xB8C4D0),
fog_start: 0.0,
fog_end: 0.0,
fog_sun_glow: 0.0,
fog_sun_focus: 0.7,
light_shafts: 0.0,
}
}
}
impl Sky {
pub fn sun_direction_at(hour: f32) -> Vec3 {
let angle = (hour - 6.0) / 12.0 * std::f32::consts::PI;
let tilt = 30f32.to_radians();
let sun = Vec3::new(angle.cos(), angle.sin() * tilt.cos(), -angle.sin() * tilt.sin());
-sun
}
}
const GROUND: f32 = 6360.0;
const TOP: f32 = 6460.0;
const RAYLEIGH: Vec3 = Vec3::new(5.802e-3, 13.558e-3, 33.1e-3);
const RAYLEIGH_HEIGHT: f32 = 8.0;
const MIE_SCATTER: f32 = 3.996e-3;
const MIE_EXTINCTION: f32 = 4.44e-3;
const MIE_HEIGHT: f32 = 1.2;
const OZONE: Vec3 = Vec3::new(0.650e-3, 1.881e-3, 0.085e-3);
pub(crate) const SKY_GAIN: f32 = 4.0;
const GROUND_ALBEDO: f32 = 0.25;
fn extinction(h: f32, haze: f32) -> Vec3 {
let rayleigh = RAYLEIGH * (-h / RAYLEIGH_HEIGHT).exp();
let mie = MIE_EXTINCTION * haze * (-h / MIE_HEIGHT).exp();
let ozone = OZONE * (1.0 - (h - 25.0).abs() / 15.0).max(0.0);
rayleigh + Vec3::splat(mie) + ozone
}
fn exit_distance(origin: Vec3, dir: Vec3, radius: f32) -> f32 {
let b = origin.dot(dir);
let c = origin.length_squared() - radius * radius;
-b + (b * b - c).max(0.0).sqrt()
}
fn hits_ground(origin: Vec3, dir: Vec3) -> bool {
let b = origin.dot(dir);
let c = origin.length_squared() - GROUND * GROUND;
b < 0.0 && b * b - c >= 0.0
}
fn transmittance(origin: Vec3, sun: Vec3, haze: f32) -> Vec3 {
if hits_ground(origin, sun) {
return Vec3::ZERO;
}
const STEPS: usize = 16;
let step = exit_distance(origin, sun, TOP) / STEPS as f32;
let mut depth = Vec3::ZERO;
for i in 0..STEPS {
let p = origin + sun * (step * (i as f32 + 0.5));
depth += extinction(p.length() - GROUND, haze) * step;
}
(-depth).exp()
}
fn radiance(dir: Vec3, sun: Vec3, haze: f32) -> Vec3 {
const STEPS: usize = 12;
let origin = Vec3::new(0.0, GROUND + 0.002, 0.0);
let length = if hits_ground(origin, dir) {
let b = origin.dot(dir);
-b - (b * b - origin.length_squared() + GROUND * GROUND).max(0.0).sqrt()
} else {
exit_distance(origin, dir, TOP)
};
let step = length / STEPS as f32;
let mu = dir.dot(sun);
let rayleigh_phase = 3.0 / (16.0 * std::f32::consts::PI) * (1.0 + mu * mu);
let g = 0.8f32;
let mie_phase = 3.0 / (8.0 * std::f32::consts::PI) * ((1.0 - g * g) * (1.0 + mu * mu))
/ ((2.0 + g * g) * (1.0 + g * g - 2.0 * g * mu).powf(1.5));
let mut depth = Vec3::ZERO;
let mut light = Vec3::ZERO;
for i in 0..STEPS {
let p = origin + dir * (step * (i as f32 + 0.5));
let h = p.length() - GROUND;
let ext = extinction(h, haze);
depth += ext * step * 0.5;
let scatter = RAYLEIGH * (-h / RAYLEIGH_HEIGHT).exp() * rayleigh_phase
+ Vec3::splat(MIE_SCATTER * haze * (-h / MIE_HEIGHT).exp() * mie_phase);
light += scatter * (-depth).exp() * transmittance(p, sun, haze) * step;
depth += ext * step * 0.5;
}
light
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub(crate) struct SkyLight {
pub sun: Vec3,
pub sky: Vec3,
pub ground: Vec3,
}
impl SkyLight {
pub fn new(lighting: &Lighting, sky: &Sky) -> Self {
let to_sun = (-lighting.sun_direction).try_normalize().unwrap_or(Vec3::Y);
let [r, g, b, _] = linear(lighting.sun_color);
let sun_out = Vec3::new(r, g, b) * lighting.sun_intensity;
let ground = Vec3::new(0.0, GROUND + 0.002, 0.0);
let haze = sky.haze.max(0.0);
let sun = sun_out * transmittance(ground, to_sun, haze);
let mut total = Vec3::ZERO;
let mut weights = 0.0;
for ring in 0..4 {
let up = (ring as f32 + 0.5) / 4.0; let around = (1.0 - up * up).sqrt();
for k in 0..8 {
let a = k as f32 / 8.0 * std::f32::consts::TAU;
let dir = Vec3::new(around * a.cos(), up, around * a.sin());
total += radiance(dir, to_sun, haze) * up;
weights += up;
}
}
let [nr, ng, nb, _] = linear(sky.night_light);
let night = Vec3::new(nr, ng, nb);
let sky_light = total / weights * sun_out * SKY_GAIN + night;
let ground_light = GROUND_ALBEDO * (sun * to_sun.y.max(0.0) / std::f32::consts::PI + sky_light);
Self {
sun,
sky: sky_light,
ground: ground_light,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn light(hour: f32) -> SkyLight {
let lighting = Lighting {
sun_direction: Sky::sun_direction_at(hour),
sun_color: Color::WHITE,
..Default::default()
};
SkyLight::new(&lighting, &Sky::default())
}
#[test]
fn the_sun_reddens_as_it_sets_and_is_gone_at_night() {
let noon = light(12.0);
let evening = light(17.8);
let night = light(0.0);
assert!(noon.sun.z > 0.7 * noon.sun.x, "white-ish at noon: {}", noon.sun);
assert!(
evening.sun.x > 2.0 * evening.sun.z,
"red-orange low down: {}",
evening.sun
);
assert!(evening.sun.x < noon.sun.x);
assert_eq!(night.sun, Vec3::ZERO);
}
#[test]
fn the_sky_is_blue_by_day_and_dark_at_night() {
let noon = light(12.0);
assert!(noon.sky.z > noon.sky.x, "blue: {}", noon.sky);
let night = light(0.0);
assert!(
night.sky.length() < noon.sky.length() / 3.0,
"{} vs {}",
night.sky,
noon.sky
);
assert!(night.sky.length() > 0.0, "night light");
let lighting = Lighting {
sun_direction: Sky::sun_direction_at(0.0),
..Default::default()
};
let moonless = Sky {
night_light: Color::rgb(0.0, 0.0, 0.0),
..Default::default()
};
let dark = SkyLight::new(&lighting, &moonless).sky;
assert!(dark.length() < noon.sky.length() / 100.0, "{dark} vs {}", noon.sky);
}
#[test]
fn the_sun_crosses_the_sky_east_to_west() {
let to_sun = |h| -Sky::sun_direction_at(h);
assert!(to_sun(6.0).y.abs() < 1e-5 && to_sun(6.0).x > 0.99, "rises in the east");
assert!(
(to_sun(12.0).y - 30f32.to_radians().cos()).abs() < 1e-5,
"60° up at noon"
);
assert!(to_sun(18.0).x < -0.99, "sets in the west");
}
}