1use crate::{
3 ColorSpace, Component, Entity, Geometry, IrradianceVolume, JsonError, Linear, TextureLayers,
4 TextureShape, Transform, World,
5};
6use serde::Serialize;
7use serde_json::Value;
8
9#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
12#[serde(rename_all = "snake_case")]
13pub enum ProbeMipPolicy {
14 Fixed(f32),
16 LinearRoughness { minimum: f32, maximum: f32 },
18}
19
20#[derive(Clone, Debug, PartialEq, Serialize)]
23pub struct ProbeState {
24 pub cube: Entity,
25 pub diffuse_volume: Option<Entity>,
26 pub intensity: f32,
28 pub mip_policy: ProbeMipPolicy,
29}
30
31#[derive(Clone, Debug, PartialEq, Serialize)]
36pub struct EnvironmentProbeBlend {
37 pub state_a: ProbeState,
38 pub state_b: ProbeState,
39 pub factor: f32,
41}
42
43#[derive(Clone, Debug, PartialEq, Serialize)]
51pub struct ReflectionProbeRegion {
52 pub blend: EnvironmentProbeBlend,
53 pub half_extents: [f32; 3],
54 pub fade_distance: f32,
55 pub priority: u8,
56 pub reflection_origin: [f32; 3],
58 pub parallax_from_region: Option<Transform>,
61}
62
63impl Component for ReflectionProbeRegion {
64 const NAME: &'static str = "core.reflection_probe_region";
65 const GEOMETRY: Geometry = Geometry::None;
66
67 fn references(&self, out: &mut Vec<Entity>) {
68 for state in [&self.blend.state_a, &self.blend.state_b] {
69 out.push(state.cube);
70 out.extend(state.diffuse_volume);
71 }
72 }
73
74 fn to_json(&self) -> Result<Option<Value>, JsonError> {
75 crate::json::to_value(self).map(Some)
76 }
77
78 fn verify(&self, entity: Entity, world: &World) -> Result<(), String> {
79 let transform = world
80 .get::<Transform>(entity)
81 .ok_or("reflection probe region has no transform")?;
82 for transform in std::iter::once(transform).chain(self.parallax_from_region.as_ref()) {
83 if !transform.cols.iter().flatten().all(|v| v.is_finite()) || Linear::of(transform).singular() {
84 return Err("reflection probe region has a nonfinite or singular transform".into());
85 }
86 }
87 if !self.half_extents.iter().all(|v| v.is_finite() && *v > 0.)
88 || !self.fade_distance.is_finite()
89 || self.fade_distance < 0.
90 || !self.reflection_origin.iter().all(|v| v.is_finite())
91 {
92 return Err("reflection probe region has invalid bounds, fade or origin".into());
93 }
94 if !self.blend.factor.is_finite() || !(0. ..=1.).contains(&self.blend.factor) {
95 return Err("environment probe blend factor must be finite and in [0,1]".into());
96 }
97 for state in [&self.blend.state_a, &self.blend.state_b] {
98 if !state.intensity.is_finite() || state.intensity < 0. {
99 return Err("reflection probe intensity must be finite and nonnegative".into());
100 }
101 let cube = world
102 .get::<TextureLayers>(state.cube)
103 .ok_or("reflection probe cube is not a layered texture")?;
104 if cube.shape != (TextureShape::Cube { cubes: 1 })
105 || cube.width != cube.height
106 || cube.color_space != ColorSpace::Linear
107 || cube.levels.is_empty()
108 {
109 return Err("reflection probe requires one square linear cube with stored mips".into());
110 }
111 let (minimum, maximum) = match state.mip_policy {
112 ProbeMipPolicy::Fixed(lod) => (lod, lod),
113 ProbeMipPolicy::LinearRoughness { minimum, maximum } => (minimum, maximum),
114 };
115 if !minimum.is_finite()
116 || !maximum.is_finite()
117 || minimum < 0.
118 || maximum < minimum
119 || maximum > (cube.levels.len() - 1) as f32
120 {
121 return Err("reflection probe LOD policy exceeds its stored mip chain".into());
122 }
123 if let Some(diffuse) = state.diffuse_volume
124 && world.get::<IrradianceVolume>(diffuse).is_none()
125 {
126 return Err("reflection probe diffuse state is not an irradiance volume".into());
127 }
128 }
129 Ok(())
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 fn region(world: &mut World) -> (Entity, ReflectionProbeRegion) {
138 let cube = world.spawn();
139 world.insert(
140 cube,
141 TextureLayers {
142 shape: TextureShape::Cube { cubes: 1 },
143 width: 1,
144 height: 1,
145 format: crate::TexFormat::Rgba16Float,
146 levels: vec![vec![vec![0; 8]; 6]],
147 color_space: ColorSpace::Linear,
148 role: crate::TexRole::Other,
149 normal: None,
150 },
151 );
152 let state = ProbeState {
153 cube,
154 diffuse_volume: None,
155 intensity: 1.,
156 mip_policy: ProbeMipPolicy::LinearRoughness {
157 minimum: 0.,
158 maximum: 0.,
159 },
160 };
161 let entity = world.spawn();
162 world.insert(entity, Transform::IDENTITY);
163 (
164 entity,
165 ReflectionProbeRegion {
166 blend: EnvironmentProbeBlend {
167 state_a: state.clone(),
168 state_b: state,
169 factor: 0.5,
170 },
171 half_extents: [1.; 3],
172 fade_distance: 0.,
173 priority: 0,
174 reflection_origin: [0.; 3],
175 parallax_from_region: None,
176 },
177 )
178 }
179
180 #[test]
181 fn probe_states_retain_both_cube_and_diffuse_dependencies() {
182 let mut world = World::new();
183 let (entity, mut probe) = region(&mut world);
184 let diffuse = world.spawn();
185 probe.blend.state_b.diffuse_volume = Some(diffuse);
187 assert!(probe.verify(entity, &world).is_err());
188 let mut references = Vec::new();
189 probe.references(&mut references);
190 assert_eq!(
191 references,
192 vec![probe.blend.state_a.cube, probe.blend.state_b.cube, diffuse]
193 );
194 probe.blend.state_b.diffuse_volume = None;
195 world.insert(entity, probe);
196 assert!(world.verify_components().is_empty());
197 assert!(world.closure([entity]).contains(&references[0]));
198 }
199
200 #[test]
201 fn probes_refuse_missing_mips_invalid_blends_and_array_stand_ins() {
202 let mut world = World::new();
203 let (entity, mut probe) = region(&mut world);
204 probe.blend.state_a.mip_policy = ProbeMipPolicy::Fixed(1.);
205 assert!(probe.verify(entity, &world).is_err());
206 probe.blend.state_a.mip_policy = ProbeMipPolicy::Fixed(0.);
207 probe.blend.factor = f32::NAN;
208 assert!(probe.verify(entity, &world).is_err());
209 probe.blend.factor = 1.;
210 assert!(probe.verify(entity, &world).is_ok());
211 world
212 .get_mut::<TextureLayers>(probe.blend.state_b.cube)
213 .expect("cube")
214 .shape = TextureShape::Array { layers: 6 };
215 assert!(probe.verify(entity, &world).is_err());
216 }
217}