Skip to main content

primase_core/
water_body.rs

1//! How light passes through the water under a surface (`core.water_body`, on material entities).
2
3use serde::Serialize;
4use serde_json::Value;
5
6use crate::pbr::TextureRef;
7use crate::{Component, Entity, Geometry, JsonError, World};
8
9/// `core.water_body`: the optics of the water a material's surface covers, in the world's units (metres, seconds,
10/// linear RGB), on a material entity whose [`crate::MaterialUsage`] is water. What the surface itself reflects stays
11/// [`crate::pbr::PbrMaterial`]'s; this is what lies under it and how the surface moves.
12///
13/// The model it states, which a consumer draws as far as it can: a view ray enters the water at the surface and travels
14/// `d` metres through it to what is behind (the scene below, read from the frame or a closed volume). What is behind
15/// arrives multiplied by `exp(-extinction * d)` per channel, and the water body adds `scatter * E / pi * (1 -
16/// exp(-extinction * d))`, `E` the irradiance (sun and sky, the light a horizontal diffuse surface at the water's
17/// surface receives). The sum is transmitted through the surface, weighted by `1 - F` against the reflection's `F`,
18/// Schlick's `F = F0 + (1 - F0) * (1 - cos)^5` with `F0 = ((ior - 1) / (ior + 1))^2`. The surface's normal is
19/// [`WaterBody::waves`] summed; its opacity at the shore is `saturate(depth / shore_fade)`, `depth` the water's depth
20/// below the surface along the view axis.
21#[derive(Clone, Debug, PartialEq, Serialize)]
22pub struct WaterBody {
23    /// Light lost per metre the view ray travels through the water, per channel: a path of `d` metres transmits
24    /// `exp(-extinction * d)`. Zero is clear water.
25    pub extinction: [f32; 3],
26    /// The diffuse reflectance of the water body where it is infinitely deep, per channel within 0-1: it returns
27    /// radiance `scatter * E / pi` from below the surface for an irradiance `E` arriving at the surface. A consumer that
28    /// has only a body colour takes that colour, linear, as `scatter`; zero is water that returns nothing of its own,
29    /// only what is behind it.
30    pub scatter: [f32; 3],
31    /// The surface's index of refraction, above 1: its Fresnel reflectance at normal incidence is
32    /// `((ior - 1) / (ior + 1))^2` (0.02 for 1.33, 0.04 for 1.5), and for a source that states that reflectance `F0`
33    /// instead, `ior = (1 + sqrt(F0)) / (1 - sqrt(F0))`.
34    pub ior: f32,
35    /// The moving normal maps whose tangent-space normals, summed, make the surface's normal; empty for a still flat
36    /// surface.
37    pub waves: Vec<WaveLayer>,
38    /// The depth of water (metres, below the surface along the view axis) over which the surface fades in from fully
39    /// transparent at the shore to opaque; zero is a hard edge.
40    pub shore_fade: f32,
41}
42
43/// One moving normal map of a [`WaterBody`].
44#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
45pub struct WaveLayer {
46    /// The normal map (tangent space, as [`crate::pbr::PbrMaterial::normal_texture`]), its tiling in the reference's
47    /// transform.
48    pub normal: TextureRef,
49    /// How far its coordinates move per second, in texture units after the transform: the map is sampled at
50    /// `transformed(uv) + velocity * t`.
51    pub velocity: [f32; 2],
52    /// The factor on its tangent-space x and y before the layers are summed (a normal map's `scale`).
53    pub strength: f32,
54}
55
56impl Component for WaterBody {
57    const NAME: &'static str = "core.water_body";
58    const GEOMETRY: Geometry = Geometry::None;
59    fn to_json(&self) -> Result<Option<Value>, JsonError> {
60        crate::json::to_value(self).map(Some)
61    }
62    /// Every range its fields document.
63    fn verify(&self, _e: Entity, _w: &World) -> Result<(), String> {
64        if !self.extinction.iter().all(|x| x.is_finite() && *x >= 0.) {
65            return Err(format!("extinction {:?} is not finite and >= 0", self.extinction));
66        }
67        if !self.scatter.iter().all(|x| (0.0..=1.0).contains(x)) {
68            return Err(format!("scatter {:?} is not within 0-1", self.scatter));
69        }
70        if !(self.ior.is_finite() && self.ior > 1.) {
71            return Err(format!("ior {} is not finite and above 1", self.ior));
72        }
73        if !(self.shore_fade.is_finite() && self.shore_fade >= 0.) {
74            return Err(format!("shore fade {} is not finite and >= 0", self.shore_fade));
75        }
76        for (i, w) in self.waves.iter().enumerate() {
77            if !w.velocity.iter().chain([&w.strength]).all(|x| x.is_finite()) {
78                return Err(format!(
79                    "wave {i}: velocity {:?} or strength {} is not finite",
80                    w.velocity, w.strength
81                ));
82            }
83            if let Some(t) = w.normal.transform
84                && !t
85                    .offset
86                    .iter()
87                    .chain(&t.scale)
88                    .chain([&t.rotation])
89                    .all(|x| x.is_finite())
90            {
91                return Err(format!("wave {i}: texture transform {t:?} is not finite"));
92            }
93        }
94        Ok(())
95    }
96    fn references(&self, out: &mut Vec<Entity>) {
97        out.extend(self.waves.iter().map(|w| w.normal.texture));
98    }
99}
100
101impl WaterBody {
102    /// The Fresnel reflectance at normal incidence of [`WaterBody::ior`].
103    pub fn f0(&self) -> f32 {
104        ((self.ior - 1.) / (self.ior + 1.)).powi(2)
105    }
106
107    /// The index of refraction whose reflectance at normal incidence is `f0` (within 0-1).
108    pub fn ior_of_f0(f0: f32) -> f32 {
109        let r = f0.sqrt();
110        (1. + r) / (1. - r)
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use crate::pbr::{UvTransform, Wrap};
118    use iridia_interface::Filters;
119
120    fn lake(w: &mut World) -> WaterBody {
121        let normal = w.spawn();
122        WaterBody {
123            extinction: [0.215, 0.115, 0.09],
124            scatter: [0.; 3],
125            ior: 1.5,
126            waves: vec![WaveLayer {
127                normal: TextureRef {
128                    texture: normal,
129                    texcoord: 0,
130                    wrap: [Wrap::Repeat; 2],
131                    transform: Some(UvTransform {
132                        offset: [0.; 2],
133                        scale: [4.; 2],
134                        rotation: 0.,
135                    }),
136                    filters: Filters::default(),
137                },
138                velocity: [-0.0003, 0.0015],
139                strength: 1.,
140            }],
141            shore_fade: 1. / 3.,
142        }
143    }
144
145    #[test]
146    fn a_water_body_shows_its_fields_and_refers_to_its_wave_maps() {
147        let mut w = World::new();
148        let body = lake(&mut w);
149        let json = body.to_json().unwrap().unwrap();
150        assert_eq!(
151            json["extinction"],
152            serde_json::json!([0.215f32, 0.115f32, 0.09f32])
153        );
154        assert_eq!(json["ior"], serde_json::json!(1.5));
155        assert_eq!(
156            json["waves"][0]["velocity"],
157            serde_json::json!([-0.0003f32, 0.0015f32])
158        );
159        let mut refs = Vec::new();
160        body.references(&mut refs);
161        assert_eq!(refs, vec![body.waves[0].normal.texture]);
162        let e = w.spawn();
163        assert_eq!(body.verify(e, &w), Ok(()));
164    }
165
166    #[test]
167    fn ior_and_normal_reflectance_convert_both_ways() {
168        let mut w = World::new();
169        let body = lake(&mut w);
170        assert!((body.f0() - 0.04).abs() < 1e-6);
171        assert!((WaterBody::ior_of_f0(0.04) - 1.5).abs() < 1e-5);
172        assert!((WaterBody::ior_of_f0(0.02) - 1.3294).abs() < 1e-4);
173    }
174
175    #[test]
176    fn a_value_outside_its_range_is_refused() {
177        let mut w = World::new();
178        let e = w.spawn();
179        let base = lake(&mut w);
180        let refused = |b: WaterBody| b.verify(e, &w).is_err();
181        assert!(refused(WaterBody {
182            extinction: [-0.1, 0., 0.],
183            ..base.clone()
184        }));
185        assert!(refused(WaterBody {
186            scatter: [1.5, 0., 0.],
187            ..base.clone()
188        }));
189        assert!(refused(WaterBody {
190            ior: 1.,
191            ..base.clone()
192        }));
193        assert!(refused(WaterBody {
194            shore_fade: f32::NAN,
195            ..base.clone()
196        }));
197        let mut fast = base.clone();
198        fast.waves[0].velocity = [f32::INFINITY, 0.];
199        assert!(refused(fast));
200    }
201}