Skip to main content

oxihuman_export/
light_probe_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5use std::f32::consts::PI;
6
7/// Export light probe / irradiance volume data.
8#[allow(dead_code)]
9pub enum ProbeType {
10    Irradiance,
11    Reflection,
12    Combined,
13}
14
15#[allow(dead_code)]
16pub struct LightProbe {
17    pub name: String,
18    pub position: [f32; 3],
19    pub probe_type: ProbeType,
20    pub radius: f32,
21    pub intensity: f32,
22    pub sh_coefficients: Vec<[f32; 3]>, // 9 SH bands RGB
23}
24
25#[allow(dead_code)]
26pub struct LightProbeExport {
27    pub probes: Vec<LightProbe>,
28    pub grid_size: [u32; 3],
29}
30
31#[allow(dead_code)]
32pub fn new_light_probe_export() -> LightProbeExport {
33    LightProbeExport {
34        probes: vec![],
35        grid_size: [1, 1, 1],
36    }
37}
38
39#[allow(dead_code)]
40pub fn add_probe(export: &mut LightProbeExport, probe: LightProbe) {
41    export.probes.push(probe);
42}
43
44#[allow(dead_code)]
45pub fn probe_count(export: &LightProbeExport) -> usize {
46    export.probes.len()
47}
48
49#[allow(dead_code)]
50pub fn default_irradiance_probe(name: &str, position: [f32; 3]) -> LightProbe {
51    LightProbe {
52        name: name.to_string(),
53        position,
54        probe_type: ProbeType::Irradiance,
55        radius: 5.0,
56        intensity: 1.0,
57        sh_coefficients: vec![[0.0; 3]; 9],
58    }
59}
60
61/// Evaluate SH irradiance for a given direction (L0 + L1 approximation).
62#[allow(dead_code)]
63pub fn eval_sh_l1(sh: &[[f32; 3]], dir: [f32; 3]) -> [f32; 3] {
64    if sh.is_empty() {
65        return [0.0; 3];
66    }
67    let l0 = sh[0];
68    let mut result = [l0[0] * 0.282095, l0[1] * 0.282095, l0[2] * 0.282095];
69    if sh.len() >= 4 {
70        let coeff = 0.488603;
71        for c in 0..3 {
72            result[c] += sh[1][c] * coeff * dir[1];
73            result[c] += sh[2][c] * coeff * dir[2];
74            result[c] += sh[3][c] * coeff * dir[0];
75        }
76    }
77    result
78}
79
80#[allow(dead_code)]
81pub fn probe_to_json(probe: &LightProbe) -> String {
82    let ptype = match probe.probe_type {
83        ProbeType::Irradiance => "irradiance",
84        ProbeType::Reflection => "reflection",
85        ProbeType::Combined => "combined",
86    };
87    format!(
88        "{{\"name\":\"{}\",\"type\":\"{}\",\"radius\":{},\"intensity\":{}}}",
89        probe.name, ptype, probe.radius, probe.intensity
90    )
91}
92
93#[allow(dead_code)]
94pub fn light_probe_export_to_json(export: &LightProbeExport) -> String {
95    format!("{{\"probe_count\":{}}}", export.probes.len())
96}
97
98#[allow(dead_code)]
99pub fn validate_probe(probe: &LightProbe) -> bool {
100    !probe.name.is_empty()
101        && probe.radius > 0.0
102        && probe.intensity >= 0.0
103        && (probe.sh_coefficients.is_empty() || probe.sh_coefficients.len() == 9)
104}
105
106#[allow(dead_code)]
107pub fn find_probe<'a>(export: &'a LightProbeExport, name: &str) -> Option<&'a LightProbe> {
108    export.probes.iter().find(|p| p.name == name)
109}
110
111/// Build a probe grid along XZ plane.
112#[allow(dead_code)]
113pub fn probe_grid(origin: [f32; 3], grid: [u32; 3], spacing: f32) -> LightProbeExport {
114    let mut e = new_light_probe_export();
115    e.grid_size = grid;
116    for xi in 0..grid[0] {
117        for zi in 0..grid[2] {
118            let pos = [
119                origin[0] + xi as f32 * spacing,
120                origin[1],
121                origin[2] + zi as f32 * spacing,
122            ];
123            let name = format!("probe_{xi}_{zi}");
124            e.probes.push(default_irradiance_probe(&name, pos));
125        }
126    }
127    e
128}
129
130/// Compute total probe coverage area (pi * r^2 * count).
131#[allow(dead_code)]
132pub fn total_coverage_area(export: &LightProbeExport) -> f32 {
133    export.probes.iter().map(|p| PI * p.radius * p.radius).sum()
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_add_probe() {
142        let mut e = new_light_probe_export();
143        add_probe(&mut e, default_irradiance_probe("p1", [0.0; 3]));
144        assert_eq!(probe_count(&e), 1);
145    }
146
147    #[test]
148    fn test_validate_probe() {
149        let p = default_irradiance_probe("test", [0.0; 3]);
150        assert!(validate_probe(&p));
151    }
152
153    #[test]
154    fn test_find_probe_found() {
155        let mut e = new_light_probe_export();
156        add_probe(&mut e, default_irradiance_probe("main", [0.0; 3]));
157        assert!(find_probe(&e, "main").is_some());
158    }
159
160    #[test]
161    fn test_find_probe_missing() {
162        let e = new_light_probe_export();
163        assert!(find_probe(&e, "none").is_none());
164    }
165
166    #[test]
167    fn test_probe_grid_count() {
168        let e = probe_grid([0.0; 3], [3, 1, 3], 2.0);
169        assert_eq!(probe_count(&e), 9);
170    }
171
172    #[test]
173    fn test_total_coverage_positive() {
174        let mut e = new_light_probe_export();
175        add_probe(&mut e, default_irradiance_probe("p1", [0.0; 3]));
176        assert!(total_coverage_area(&e) > 0.0);
177    }
178
179    #[test]
180    fn test_eval_sh_l1_constant() {
181        let sh = vec![[1.0f32, 1.0, 1.0]; 9];
182        let c = eval_sh_l1(&sh, [0.0, 1.0, 0.0]);
183        assert!(c[0] > 0.0);
184    }
185
186    #[test]
187    fn test_eval_sh_empty() {
188        let c = eval_sh_l1(&[], [0.0, 1.0, 0.0]);
189        assert_eq!(c, [0.0; 3]);
190    }
191
192    #[test]
193    fn test_to_json() {
194        let p = default_irradiance_probe("test", [0.0; 3]);
195        let j = probe_to_json(&p);
196        assert!(j.contains("irradiance"));
197    }
198
199    #[test]
200    fn test_export_to_json() {
201        let mut e = new_light_probe_export();
202        add_probe(&mut e, default_irradiance_probe("p", [0.0; 3]));
203        let j = light_probe_export_to_json(&e);
204        assert!(j.contains("probe_count"));
205    }
206}