Skip to main content

concinnity_asset/
volumetric_fog.rs

1// Environmental volumetric fog schema.
2
3/// Environmental volumetric fog: a single lit medium that wraps the scene,
4/// thicker near the ground and thinning with height, with extra glow around the
5/// sun.
6///
7/// Only one `VolumetricFog` is honoured: the first declared instance wins;
8/// later instances are silently dropped. With none declared, there is no fog.
9///
10/// ```rust
11/// # use concinnity_asset::VolumetricFog;
12/// VolumetricFog {
13///     density: 0.08,
14///     color: [0.75, 0.82, 0.95],
15///     height_falloff: 0.18,
16///     max_distance: 160.0,
17///     phase_g: 0.5,
18///     ..Default::default()
19/// };
20/// ```
21#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
22#[serde(default)]
23pub struct VolumetricFog {
24    /// Master toggle. `false` disables the fog even when this asset is present.
25    pub enabled: bool,
26    /// Linear-space RGB tint of the fog: the colour the camera sees in the far
27    /// distance.
28    pub color: [f32; 3],
29    /// Base thickness of the fog at `height_reference` (per world unit). Higher
30    /// is thicker. Floored at 0.
31    pub density: f32,
32    /// How quickly the fog thins with height above `height_reference`. 0 keeps
33    /// it uniform; larger values pin it to the ground.
34    pub height_falloff: f32,
35    /// World-space Y at which the fog reaches full `density`. It thickens below
36    /// this height and thins above it.
37    pub height_reference: f32,
38    /// Maximum distance the fog covers from the camera, in world units. Past
39    /// this, distant geometry stays clear.
40    pub max_distance: f32,
41    /// Sun-glow anisotropy in `(-1, 1)`. Positive values concentrate brightness
42    /// around the sun (haloes), negative values scatter away from it, 0 is
43    /// uniform.
44    pub phase_g: f32,
45    /// Constant ambient brightness so the fog keeps some colour in shaded areas.
46    pub ambient: f32,
47}
48
49impl Default for VolumetricFog {
50    fn default() -> Self {
51        Self {
52            enabled: true,
53            color: [0.7, 0.78, 0.85],
54            density: 0.05,
55            height_falloff: 0.2,
56            height_reference: 0.0,
57            max_distance: 200.0,
58            phase_g: 0.4,
59            ambient: 0.15,
60        }
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn declaring_fog_turns_it_on_at_a_thin_forward_scattering_density() {
70        // The asset exists to add fog, so `enabled` starts true: `false` is the
71        // way to keep a declared fog around while switching it off.
72        let f = VolumetricFog::default();
73        assert!(f.enabled);
74        assert_eq!(f.density, 0.05);
75        assert_eq!(f.height_falloff, 0.2);
76        assert_eq!(f.height_reference, 0.0);
77        assert_eq!(f.max_distance, 200.0);
78        // Positive g scatters forward, so the sun haloes rather than backlights.
79        assert!(f.phase_g > 0.0);
80        assert_eq!(f.ambient, 0.15);
81    }
82
83    #[test]
84    fn an_authored_fog_parses_and_round_trips_through_postcard() {
85        let f: VolumetricFog = serde_json::from_str(
86            r#"{"enabled":false,"color":[0.5,0.5,0.6],"density":0.2,"height_falloff":0.05,
87                "height_reference":12,"max_distance":80,"phase_g":-0.3,"ambient":0.4}"#,
88        )
89        .unwrap();
90        assert!(!f.enabled);
91        assert!(f.phase_g < 0.0);
92
93        let bytes = postcard::to_allocvec(&f).unwrap();
94        let back: VolumetricFog = postcard::from_bytes(&bytes).unwrap();
95        assert_eq!(back.color, [0.5, 0.5, 0.6]);
96        assert_eq!(back.density, 0.2);
97        assert_eq!(back.height_falloff, 0.05);
98        assert_eq!(back.height_reference, 12.0);
99        assert_eq!(back.max_distance, 80.0);
100        assert_eq!(back.ambient, 0.4);
101    }
102}