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