oxihuman_viewer/
env_diffuse.rs1#![allow(dead_code)]
4
5use std::f32::consts::{FRAC_1_PI, PI};
8
9pub const SH_COEFF_COUNT: usize = 9;
11
12#[derive(Debug, Clone, PartialEq)]
14#[allow(dead_code)]
15pub struct EnvDiffuseProbe {
16 pub sh_r: [f32; SH_COEFF_COUNT],
18 pub sh_g: [f32; SH_COEFF_COUNT],
19 pub sh_b: [f32; SH_COEFF_COUNT],
20 pub intensity: f32,
21 pub enabled: bool,
22}
23
24impl Default for EnvDiffuseProbe {
25 fn default() -> Self {
26 Self {
27 sh_r: [0.0; SH_COEFF_COUNT],
28 sh_g: [0.0; SH_COEFF_COUNT],
29 sh_b: [0.0; SH_COEFF_COUNT],
30 intensity: 1.0,
31 enabled: true,
32 }
33 }
34}
35
36#[derive(Debug, Clone, PartialEq)]
38#[allow(dead_code)]
39pub struct EnvDiffuseConfig {
40 pub max_probes: usize,
41 pub blend_radius: f32,
42}
43
44impl Default for EnvDiffuseConfig {
45 fn default() -> Self {
46 Self {
47 max_probes: 8,
48 blend_radius: 10.0,
49 }
50 }
51}
52
53#[derive(Debug, Clone, Default)]
55#[allow(dead_code)]
56pub struct EnvDiffuse {
57 pub config: EnvDiffuseConfig,
58 pub probes: Vec<EnvDiffuseProbe>,
59}
60
61#[allow(dead_code)]
63pub fn new_env_diffuse(cfg: EnvDiffuseConfig) -> EnvDiffuse {
64 EnvDiffuse {
65 config: cfg,
66 probes: Vec::new(),
67 }
68}
69
70#[allow(dead_code)]
72pub fn add_probe(e: &mut EnvDiffuse, probe: EnvDiffuseProbe) -> Option<usize> {
73 if e.probes.len() >= e.config.max_probes {
74 return None;
75 }
76 let idx = e.probes.len();
77 e.probes.push(probe);
78 Some(idx)
79}
80
81#[allow(dead_code)]
83pub fn probe_count_env(e: &EnvDiffuse) -> usize {
84 e.probes.len()
85}
86
87#[allow(dead_code)]
89pub fn sample_sh_irradiance(probe: &EnvDiffuseProbe, normal: [f32; 3]) -> [f32; 3] {
90 let (nx, ny, nz) = (normal[0], normal[1], normal[2]);
91 let y0 = 0.282_094_8; let y1 = 0.488_602_5 * ny; let y2 = 0.488_602_5 * nz; let y3 = 0.488_602_5 * nx; let sh = [y0, y1, y2, y3, 0.0, 0.0, 0.0, 0.0, 0.0];
97 let mut r = 0.0f32;
98 let mut g = 0.0f32;
99 let mut b = 0.0f32;
100 #[allow(clippy::needless_range_loop)]
101 for i in 0..SH_COEFF_COUNT {
102 r += probe.sh_r[i] * sh[i];
103 g += probe.sh_g[i] * sh[i];
104 b += probe.sh_b[i] * sh[i];
105 }
106 [
107 r * probe.intensity,
108 g * probe.intensity,
109 b * probe.intensity,
110 ]
111}
112
113#[allow(dead_code)]
115pub fn ambient_color(probe: &EnvDiffuseProbe) -> [f32; 3] {
116 let scale = FRAC_1_PI * probe.intensity;
117 [
118 probe.sh_r[0] * scale,
119 probe.sh_g[0] * scale,
120 probe.sh_b[0] * scale,
121 ]
122}
123
124#[allow(dead_code)]
126pub fn hemisphere_pdf() -> f32 {
127 1.0 / (2.0 * PI)
128}
129
130#[allow(dead_code)]
132pub fn blend_probes_env(a: &EnvDiffuseProbe, b: &EnvDiffuseProbe, t: f32) -> EnvDiffuseProbe {
133 let t = t.clamp(0.0, 1.0);
134 let mut result = EnvDiffuseProbe::default();
135 #[allow(clippy::needless_range_loop)]
136 for i in 0..SH_COEFF_COUNT {
137 result.sh_r[i] = a.sh_r[i] + (b.sh_r[i] - a.sh_r[i]) * t;
138 result.sh_g[i] = a.sh_g[i] + (b.sh_g[i] - a.sh_g[i]) * t;
139 result.sh_b[i] = a.sh_b[i] + (b.sh_b[i] - a.sh_b[i]) * t;
140 }
141 result.intensity = a.intensity + (b.intensity - a.intensity) * t;
142 result
143}
144
145#[allow(dead_code)]
147pub fn env_diffuse_to_json(e: &EnvDiffuse) -> String {
148 format!(
149 r#"{{"probe_count":{},"max_probes":{}}}"#,
150 e.probes.len(),
151 e.config.max_probes
152 )
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn new_system_empty() {
161 let e = new_env_diffuse(EnvDiffuseConfig::default());
162 assert_eq!(probe_count_env(&e), 0);
163 }
164
165 #[test]
166 fn add_probe_ok() {
167 let mut e = new_env_diffuse(EnvDiffuseConfig::default());
168 let r = add_probe(&mut e, EnvDiffuseProbe::default());
169 assert!(r.is_some());
170 }
171
172 #[test]
173 fn add_probe_capacity_limit() {
174 let mut e = new_env_diffuse(EnvDiffuseConfig {
175 max_probes: 1,
176 ..Default::default()
177 });
178 add_probe(&mut e, EnvDiffuseProbe::default());
179 let r = add_probe(&mut e, EnvDiffuseProbe::default());
180 assert!(r.is_none());
181 }
182
183 #[test]
184 fn sample_sh_neutral_probe() {
185 let probe = EnvDiffuseProbe::default();
186 let color = sample_sh_irradiance(&probe, [0.0, 1.0, 0.0]);
187 assert_eq!(color, [0.0, 0.0, 0.0]);
188 }
189
190 #[test]
191 fn ambient_color_uses_frac1pi() {
192 let mut probe = EnvDiffuseProbe::default();
193 probe.sh_r[0] = PI;
194 let c = ambient_color(&probe);
195 assert!((c[0] - 1.0).abs() < 1e-4);
196 }
197
198 #[test]
199 fn hemisphere_pdf_correct() {
200 let pdf = hemisphere_pdf();
201 assert!((pdf - 1.0 / (2.0 * PI)).abs() < 1e-6);
202 }
203
204 #[test]
205 fn blend_probes_at_zero() {
206 let a = EnvDiffuseProbe {
207 intensity: 0.5,
208 ..Default::default()
209 };
210 let b = EnvDiffuseProbe {
211 intensity: 1.0,
212 ..Default::default()
213 };
214 let m = blend_probes_env(&a, &b, 0.0);
215 assert!((m.intensity - 0.5).abs() < 1e-6);
216 }
217
218 #[test]
219 fn blend_probes_at_one() {
220 let a = EnvDiffuseProbe {
221 intensity: 0.0,
222 ..Default::default()
223 };
224 let b = EnvDiffuseProbe {
225 intensity: 1.0,
226 ..Default::default()
227 };
228 let m = blend_probes_env(&a, &b, 1.0);
229 assert!((m.intensity - 1.0).abs() < 1e-6);
230 }
231
232 #[test]
233 fn json_contains_probe_count() {
234 let e = new_env_diffuse(EnvDiffuseConfig::default());
235 assert!(env_diffuse_to_json(&e).contains("probe_count"));
236 }
237
238 #[test]
239 fn probes_slice_not_empty() {
240 let mut e = new_env_diffuse(EnvDiffuseConfig::default());
241 add_probe(&mut e, EnvDiffuseProbe::default());
242 assert!(!e.probes.is_empty());
243 }
244}