use crate::{
ColorSpace, Component, Entity, Geometry, JsonError, TextureLayers, TextureShape, Transform, World,
};
use serde::Serialize;
use serde_json::Value;
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct IrradianceVolume {
pub coefficients: [Entity; 4],
pub basis: [[f32; 3]; 3],
pub sampling_from_region: Transform,
pub normal_offset: bool,
pub half_extents: [f32; 3],
pub fade_distance: f32,
pub diffuse_scale: f32,
pub priority: u8,
}
impl Component for IrradianceVolume {
const NAME: &'static str = "core.irradiance_volume";
const GEOMETRY: Geometry = Geometry::None;
fn references(&self, out: &mut Vec<Entity>) {
out.extend(self.coefficients);
}
fn to_json(&self) -> Result<Option<Value>, JsonError> {
crate::json::to_value(self).map(Some)
}
fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
let transform = w
.get::<Transform>(e)
.ok_or("irradiance volume has no transform")?;
if crate::Linear::of(transform).singular() || crate::Linear::of(&self.sampling_from_region).singular()
{
return Err("irradiance volume has a singular transform".into());
}
if !self.basis.iter().flatten().all(|x| x.is_finite()) {
return Err("irradiance basis is not finite".into());
}
if crate::Linear::new(self.basis).singular() {
return Err("irradiance coefficient basis is singular".into());
}
if !self
.sampling_from_region
.cols
.iter()
.flatten()
.all(|x| x.is_finite())
{
return Err("irradiance sampling transform is not finite".into());
}
if !self.half_extents.iter().all(|v| v.is_finite() && *v > 0.) {
return Err("irradiance half-extents must be finite and positive".into());
}
if !self.fade_distance.is_finite()
|| self.fade_distance < 0.
|| !self.diffuse_scale.is_finite()
|| self.diffuse_scale < 0.
{
return Err("irradiance fade or diffuse scale is invalid".into());
}
let mut dimensions = None;
for texture in self.coefficients {
let t = w
.get::<TextureLayers>(texture)
.ok_or("irradiance coefficient is not a layered texture")?;
let TextureShape::Volume { depth } = t.shape else {
return Err("irradiance coefficient is not 3D".into());
};
if t.color_space != ColorSpace::Linear {
return Err("irradiance coefficient is not linear".into());
}
if !matches!(
t.format,
crate::TexFormat::Bc7
| crate::TexFormat::Rgba8Unorm
| crate::TexFormat::Rgba16Float
| crate::TexFormat::Rgba32Float
| crate::TexFormat::Rgba16Unorm
) {
return Err("irradiance coefficient must store four RGBA channels".into());
}
let size = (t.width, t.height, depth, t.format);
if dimensions.is_some_and(|other| other != size) {
return Err("irradiance coefficient layouts differ".into());
}
dimensions = Some(size);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn coefficient_references_require_matching_linear_volumes() {
let mut w = World::new();
let coefficients = std::array::from_fn(|_| {
let e = w.spawn();
w.insert(
e,
TextureLayers {
shape: TextureShape::Volume { depth: 2 },
width: 1,
height: 1,
format: crate::TexFormat::Rgba8Unorm,
levels: vec![vec![vec![0; 4]; 2]],
color_space: ColorSpace::Linear,
role: crate::TexRole::Other,
normal: None,
},
);
e
});
let e = w.spawn();
w.insert(e, Transform::IDENTITY);
let volume = IrradianceVolume {
coefficients,
basis: [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]],
sampling_from_region: Transform::IDENTITY,
normal_offset: true,
half_extents: [1.; 3],
fade_distance: 3.,
diffuse_scale: 1.,
priority: 20,
};
assert_eq!(volume.verify(e, &w), Ok(()));
w.insert(e, volume.clone());
assert!(coefficients.into_iter().all(|t| w.closure([e]).contains(&t)));
w.get_mut::<TextureLayers>(coefficients[2])
.expect("texture")
.color_space = ColorSpace::Srgb;
assert_eq!(
volume.verify(e, &w),
Err("irradiance coefficient is not linear".into())
);
w.get_mut::<TextureLayers>(coefficients[2])
.expect("texture")
.color_space = ColorSpace::Linear;
w.get_mut::<TextureLayers>(coefficients[2])
.expect("texture")
.shape = TextureShape::Array { layers: 2 };
assert_eq!(
volume.verify(e, &w),
Err("irradiance coefficient is not 3D".into())
);
}
}