oxihuman_viewer/
light_probe_debug.rs1#![allow(dead_code)]
7
8use std::f32::consts::PI;
9
10#[allow(dead_code)]
12#[derive(Debug, Clone)]
13pub struct ShProbe {
14 pub coeffs: [[f32; 3]; 9],
16}
17
18#[allow(dead_code)]
19impl Default for ShProbe {
20 fn default() -> Self {
21 Self {
22 coeffs: [[0.0; 3]; 9],
23 }
24 }
25}
26
27#[allow(dead_code)]
29#[derive(Debug, Clone)]
30pub struct LightProbeDebugConfig {
31 pub sphere_subdivisions: u32,
33 pub exposure: f32,
35 pub show_axes: bool,
37}
38
39#[allow(dead_code)]
40impl Default for LightProbeDebugConfig {
41 fn default() -> Self {
42 Self {
43 sphere_subdivisions: 32,
44 exposure: 1.0,
45 show_axes: true,
46 }
47 }
48}
49
50#[allow(dead_code)]
52pub fn new_light_probe_debug_config() -> LightProbeDebugConfig {
53 LightProbeDebugConfig::default()
54}
55
56#[allow(dead_code)]
58pub fn new_sh_probe() -> ShProbe {
59 ShProbe::default()
60}
61
62#[allow(dead_code)]
64pub fn sh_l0_scale() -> f32 {
65 1.0 / (2.0 * (PI).sqrt())
66}
67
68#[allow(dead_code)]
70#[allow(clippy::needless_range_loop)]
71pub fn evaluate_sh_irradiance(probe: &ShProbe, theta: f32, phi: f32) -> [f32; 3] {
72 let x = theta.sin() * phi.cos();
73 let y = theta.cos();
74 let z = theta.sin() * phi.sin();
75 let basis = [
76 0.282_095f32,
77 0.488_603 * y,
78 0.488_603 * z,
79 0.488_603 * x,
80 1.092_548 * x * y,
81 1.092_548 * y * z,
82 0.315_392 * (3.0 * z * z - 1.0),
83 1.092_548 * x * z,
84 0.546_274 * (x * x - y * y),
85 ];
86 let mut out = [0.0f32; 3];
87 for c in 0..9 {
88 out[0] += probe.coeffs[c][0] * basis[c];
89 out[1] += probe.coeffs[c][1] * basis[c];
90 out[2] += probe.coeffs[c][2] * basis[c];
91 }
92 out
93}
94
95#[allow(dead_code)]
97pub fn lpd_set_exposure(cfg: &mut LightProbeDebugConfig, value: f32) {
98 cfg.exposure = value.max(0.0);
99}
100
101#[allow(dead_code)]
103pub fn sh_probe_reset(probe: &mut ShProbe) {
104 *probe = ShProbe::default();
105}
106
107#[allow(dead_code)]
109pub fn sh_probe_energy(probe: &ShProbe) -> f32 {
110 probe
111 .coeffs
112 .iter()
113 .flat_map(|c| c.iter())
114 .map(|v| v * v)
115 .sum::<f32>()
116 .sqrt()
117}
118
119#[allow(dead_code)]
121pub fn light_probe_debug_to_json(cfg: &LightProbeDebugConfig) -> String {
122 format!(
123 r#"{{"sphere_subdivisions":{},"exposure":{:.4},"show_axes":{}}}"#,
124 cfg.sphere_subdivisions, cfg.exposure, cfg.show_axes
125 )
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn test_default_config() {
134 let c = LightProbeDebugConfig::default();
135 assert_eq!(c.sphere_subdivisions, 32);
136 assert!((c.exposure - 1.0).abs() < 1e-6);
137 }
138
139 #[test]
140 fn test_sh_l0_scale() {
141 let s = sh_l0_scale();
142 assert!(s > 0.0 && s < 1.0);
143 }
144
145 #[test]
146 fn test_evaluate_sh_zero_probe() {
147 let probe = ShProbe::default();
148 let irr = evaluate_sh_irradiance(&probe, 0.0, 0.0);
149 assert!(irr[0].abs() < 1e-5);
150 }
151
152 #[test]
153 fn test_sh_probe_energy_zero() {
154 let probe = ShProbe::default();
155 assert!(sh_probe_energy(&probe) < 1e-6);
156 }
157
158 #[test]
159 fn test_sh_probe_energy_nonzero() {
160 let mut probe = ShProbe::default();
161 probe.coeffs[0][0] = 1.0;
162 assert!(sh_probe_energy(&probe) > 0.0);
163 }
164
165 #[test]
166 fn test_set_exposure_clamped() {
167 let mut c = LightProbeDebugConfig::default();
168 lpd_set_exposure(&mut c, -2.0);
169 assert!(c.exposure < 1e-6);
170 }
171
172 #[test]
173 fn test_sh_probe_reset() {
174 let mut p = ShProbe {
175 coeffs: [[1.0; 3]; 9],
176 };
177 sh_probe_reset(&mut p);
178 assert!(sh_probe_energy(&p) < 1e-6);
179 }
180
181 #[test]
182 fn test_to_json() {
183 let j = light_probe_debug_to_json(&LightProbeDebugConfig::default());
184 assert!(j.contains("sphere_subdivisions"));
185 }
186
187 #[test]
188 fn test_pi_consts_used() {
189 let s = sh_l0_scale();
190 let expected = 1.0 / (2.0 * PI.sqrt());
191 assert!((s - expected).abs() < 1e-5);
192 }
193}