Skip to main content

kvas_engine/
sky.rs

1//! Procedural atmosphere: time of day drives the sun/moon direction, light colour and ambient.
2//! The renderer draws the sky itself from the same settings, so light and sky always agree.
3
4use glam::Vec3;
5use serde::{Deserialize, Serialize};
6
7/// Serializable procedural sky, celestial lighting and day/night animation controls.
8#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
9#[serde(default)]
10pub struct SkySettings {
11    /// Draw the procedural sky (and light the scene from it) instead of a flat colour.
12    pub procedural: bool,
13    /// Hour of day, 0..24 (12 = noon).
14    pub time: f32,
15    /// Game hours that pass per real second while playing (0 = frozen, 1 = a day in 24 s).
16    pub speed: f32,
17    /// 0 = clear, 1 = overcast.
18    pub clouds: f32,
19    /// Cloud pattern scrolling speed used by the sky shader.
20    pub cloud_speed: f32,
21    /// Star brightness at night, 0..1.
22    pub stars: f32,
23    /// Air haze, 0 (crystal clear) .. 1 (hazy, milky horizon).
24    pub haze: f32,
25    /// Moon phase: 0 new, 0.5 full, 1 new again.
26    pub moon_phase: f32,
27    /// Northern-lights strength at night, 0..1.
28    pub aurora: f32,
29    /// Milky way brightness at night, 0..1.
30    pub milky_way: f32,
31}
32
33impl Default for SkySettings {
34    fn default() -> Self {
35        Self {
36            procedural: false,
37            time: 14.0,
38            speed: 0.0,
39            clouds: 0.45,
40            cloud_speed: 0.02,
41            stars: 0.8,
42            haze: 0.3,
43            moon_phase: 0.5,
44            aurora: 0.0,
45            milky_way: 0.7,
46        }
47    }
48}
49
50/// Unit vector pointing from the world towards the sun. Rises in +X at 6:00, peaks at noon, sets in -X at 18:00.
51pub fn sun_direction(hour: f32) -> Vec3 {
52    let a = (hour - 6.0) / 24.0 * std::f32::consts::TAU;
53    Vec3::new(a.cos(), a.sin(), 0.35).normalize()
54}
55
56fn smoothstep(a: f32, b: f32, x: f32) -> f32 {
57    let t = ((x - a) / (b - a)).clamp(0.0, 1.0);
58    t * t * (3.0 - 2.0 * t)
59}
60
61fn mix3(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
62    [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]
63}
64
65/// Fraction of the moon's disc that is lit: 0 at new moon, 1 at full moon.
66pub fn moon_illumination(phase: f32) -> f32 {
67    0.5 - 0.5 * (std::f32::consts::TAU * phase).cos()
68}
69
70/// The strongest light in the sky right now: `(direction towards it, linear colour, intensity)`.
71/// The sun by day (orange near the horizon), a dim blue moon at night.
72pub fn main_light(sky: &SkySettings) -> (Vec3, [f32; 3], f32) {
73    let sun = sun_direction(sky.time);
74    let e = sun.y;
75    let day = smoothstep(-0.06, 0.18, e);
76    let warm = mix3([1.0, 0.5, 0.25], [1.0, 0.96, 0.9], smoothstep(0.02, 0.45, e));
77    let cloud_dim = 1.0 - 0.55 * sky.clouds.clamp(0.0, 1.0);
78    if day > 0.001 {
79        (sun, warm, 1.35 * day * cloud_dim)
80    } else {
81        (-sun, [0.55, 0.65, 1.0], 0.03 + 0.15 * moon_illumination(sky.moon_phase))
82    }
83}
84
85/// Hemisphere ambient light colour for the time of day.
86pub fn ambient(sky: &SkySettings) -> [f32; 3] {
87    let e = sun_direction(sky.time).y;
88    let day = smoothstep(-0.1, 0.25, e);
89    let dusk = (1.0 - (e * 4.0).abs().min(1.0)) * 0.5;
90    let base = mix3([0.025, 0.035, 0.08], [0.3, 0.34, 0.42], day);
91    mix3(base, [0.4, 0.28, 0.25], dusk * day.max(0.3))
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn sun_follows_the_day() {
100        assert!(sun_direction(12.0).y > 0.9);
101        assert!(sun_direction(0.0).y < -0.9);
102        assert!(sun_direction(6.0).y.abs() < 0.05);
103        assert!(sun_direction(6.0).x > 0.9 && sun_direction(18.0).x < -0.9);
104    }
105
106    #[test]
107    fn moon_phase_dims_night_light() {
108        let full = main_light(&SkySettings { time: 0.0, moon_phase: 0.5, ..Default::default() }).2;
109        let new = main_light(&SkySettings { time: 0.0, moon_phase: 0.0, ..Default::default() }).2;
110        assert!(full > new * 3.0 && moon_illumination(0.5) > 0.99 && moon_illumination(0.0) < 0.01);
111    }
112
113    #[test]
114    fn light_is_bright_at_noon_and_dim_at_night() {
115        let noon = main_light(&SkySettings { time: 12.0, ..Default::default() });
116        let night = main_light(&SkySettings { time: 0.0, ..Default::default() });
117        assert!(noon.2 > 1.0 && night.2 < 0.3);
118        assert!(noon.0.y > 0.0 && night.0.y > 0.0, "the moon also shines from above");
119        assert!(
120            ambient(&SkySettings { time: 12.0, ..Default::default() })[0]
121                > ambient(&SkySettings { time: 0.0, ..Default::default() })[0] * 4.0
122        );
123    }
124}