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}