Skip to main content

primase_core/
irradiance_volume.rs

1//! Spatial first-order spherical-harmonic irradiance, on box-region entities.
2use crate::{
3    ColorSpace, Component, Entity, Geometry, JsonError, TextureLayers, TextureShape, Transform, World,
4};
5use serde::Serialize;
6use serde_json::Value;
7
8/// Four 3D coefficient textures, in X/Y/Z/constant order. For each sampled RGBA value,
9/// decode RGB as `(rgb - 0.5) * 2 * exp2((a - 0.5) * 20)`, then dot the four
10/// terms with `(dot(n,basis[0]), dot(n,basis[1]), dot(n,basis[2]), 1)` and take max(0).
11/// The entity's transform maps the unit box [-1,1] to its blend region in world metres.
12/// Regions blend in increasing priority; their interior excludes lower-priority regions,
13/// while their fade shell blends over them. Equal-priority ordering is source-defined.
14#[derive(Clone, Debug, PartialEq, Serialize)]
15pub struct IrradianceVolume {
16    pub coefficients: [Entity; 4],
17    /// Unit directions in world space for the three directional coefficient terms.
18    pub basis: [[f32; 3]; 3],
19    /// Sampling box relative to the blend box. Identity when both domains coincide.
20    pub sampling_from_region: Transform,
21    /// Offset the sample towards the surface normal by one normalized sampling-space texel.
22    pub normal_offset: bool,
23    /// Authored metric half-extents, retained separately from the rounded matrix basis.
24    pub half_extents: [f32; 3],
25    pub fade_distance: f32,
26    pub diffuse_scale: f32,
27    pub priority: u8,
28}
29impl Component for IrradianceVolume {
30    const NAME: &'static str = "core.irradiance_volume";
31    const GEOMETRY: Geometry = Geometry::None;
32    fn references(&self, out: &mut Vec<Entity>) {
33        out.extend(self.coefficients);
34    }
35    fn to_json(&self) -> Result<Option<Value>, JsonError> {
36        crate::json::to_value(self).map(Some)
37    }
38    fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
39        let transform = w
40            .get::<Transform>(e)
41            .ok_or("irradiance volume has no transform")?;
42        if crate::Linear::of(transform).singular() || crate::Linear::of(&self.sampling_from_region).singular()
43        {
44            return Err("irradiance volume has a singular transform".into());
45        }
46        if !self.basis.iter().flatten().all(|x| x.is_finite()) {
47            return Err("irradiance basis is not finite".into());
48        }
49        if crate::Linear::new(self.basis).singular() {
50            return Err("irradiance coefficient basis is singular".into());
51        }
52        if !self
53            .sampling_from_region
54            .cols
55            .iter()
56            .flatten()
57            .all(|x| x.is_finite())
58        {
59            return Err("irradiance sampling transform is not finite".into());
60        }
61        if !self.half_extents.iter().all(|v| v.is_finite() && *v > 0.) {
62            return Err("irradiance half-extents must be finite and positive".into());
63        }
64        if !self.fade_distance.is_finite()
65            || self.fade_distance < 0.
66            || !self.diffuse_scale.is_finite()
67            || self.diffuse_scale < 0.
68        {
69            return Err("irradiance fade or diffuse scale is invalid".into());
70        }
71        let mut dimensions = None;
72        for texture in self.coefficients {
73            let t = w
74                .get::<TextureLayers>(texture)
75                .ok_or("irradiance coefficient is not a layered texture")?;
76            let TextureShape::Volume { depth } = t.shape else {
77                return Err("irradiance coefficient is not 3D".into());
78            };
79            if t.color_space != ColorSpace::Linear {
80                return Err("irradiance coefficient is not linear".into());
81            }
82            if !matches!(
83                t.format,
84                crate::TexFormat::Bc7
85                    | crate::TexFormat::Rgba8Unorm
86                    | crate::TexFormat::Rgba16Float
87                    | crate::TexFormat::Rgba32Float
88                    | crate::TexFormat::Rgba16Unorm
89            ) {
90                return Err("irradiance coefficient must store four RGBA channels".into());
91            }
92            let size = (t.width, t.height, depth, t.format);
93            if dimensions.is_some_and(|other| other != size) {
94                return Err("irradiance coefficient layouts differ".into());
95            }
96            dimensions = Some(size);
97        }
98        Ok(())
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    #[test]
106    fn coefficient_references_require_matching_linear_volumes() {
107        let mut w = World::new();
108        let coefficients = std::array::from_fn(|_| {
109            let e = w.spawn();
110            w.insert(
111                e,
112                TextureLayers {
113                    shape: TextureShape::Volume { depth: 2 },
114                    width: 1,
115                    height: 1,
116                    format: crate::TexFormat::Rgba8Unorm,
117                    levels: vec![vec![vec![0; 4]; 2]],
118                    color_space: ColorSpace::Linear,
119                    role: crate::TexRole::Other,
120                    normal: None,
121                },
122            );
123            e
124        });
125        let e = w.spawn();
126        w.insert(e, Transform::IDENTITY);
127        let volume = IrradianceVolume {
128            coefficients,
129            basis: [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]],
130            sampling_from_region: Transform::IDENTITY,
131            normal_offset: true,
132            half_extents: [1.; 3],
133            fade_distance: 3.,
134            diffuse_scale: 1.,
135            priority: 20,
136        };
137        assert_eq!(volume.verify(e, &w), Ok(()));
138        w.insert(e, volume.clone());
139        assert!(coefficients.into_iter().all(|t| w.closure([e]).contains(&t)));
140        w.get_mut::<TextureLayers>(coefficients[2])
141            .expect("texture")
142            .color_space = ColorSpace::Srgb;
143        assert_eq!(
144            volume.verify(e, &w),
145            Err("irradiance coefficient is not linear".into())
146        );
147        w.get_mut::<TextureLayers>(coefficients[2])
148            .expect("texture")
149            .color_space = ColorSpace::Linear;
150        w.get_mut::<TextureLayers>(coefficients[2])
151            .expect("texture")
152            .shape = TextureShape::Array { layers: 2 };
153        assert_eq!(
154            volume.verify(e, &w),
155            Err("irradiance coefficient is not 3D".into())
156        );
157    }
158}