Skip to main content

oxihuman_viewer/
light_map.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Light-map atlas management (baked GI atlas).
6
7/// Texel encoding.
8#[allow(dead_code)]
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum LightMapEncoding {
11    Linear,
12    Rgbm,
13    LogLuv,
14}
15
16/// Atlas entry for one mesh.
17#[allow(dead_code)]
18#[derive(Clone, Debug)]
19pub struct LightMapEntry {
20    pub mesh_id: u32,
21    pub uv_offset: [f32; 2],
22    pub uv_scale: [f32; 2],
23    pub atlas_page: u32,
24}
25
26/// Light-map atlas.
27#[allow(dead_code)]
28#[derive(Clone, Debug)]
29pub struct LightMapAtlas {
30    pub page_size: u32,
31    pub page_count: u32,
32    pub encoding: LightMapEncoding,
33    pub entries: Vec<LightMapEntry>,
34}
35
36impl Default for LightMapAtlas {
37    fn default() -> Self {
38        Self {
39            page_size: 1024,
40            page_count: 1,
41            encoding: LightMapEncoding::Linear,
42            entries: Vec::new(),
43        }
44    }
45}
46
47#[allow(dead_code)]
48pub fn new_lightmap_atlas() -> LightMapAtlas {
49    LightMapAtlas::default()
50}
51
52#[allow(dead_code)]
53pub fn lm_add_entry(
54    atlas: &mut LightMapAtlas,
55    mesh_id: u32,
56    uv_offset: [f32; 2],
57    uv_scale: [f32; 2],
58    page: u32,
59) {
60    atlas.entries.push(LightMapEntry {
61        mesh_id,
62        uv_offset,
63        uv_scale,
64        atlas_page: page,
65    });
66}
67
68#[allow(dead_code)]
69pub fn lm_remove_entry(atlas: &mut LightMapAtlas, mesh_id: u32) {
70    atlas.entries.retain(|e| e.mesh_id != mesh_id);
71}
72
73#[allow(dead_code)]
74pub fn lm_entry_count(atlas: &LightMapAtlas) -> usize {
75    atlas.entries.len()
76}
77
78#[allow(dead_code)]
79pub fn lm_get_entry(atlas: &LightMapAtlas, mesh_id: u32) -> Option<&LightMapEntry> {
80    atlas.entries.iter().find(|e| e.mesh_id == mesh_id)
81}
82
83#[allow(dead_code)]
84pub fn lm_encoding_name(enc: LightMapEncoding) -> &'static str {
85    match enc {
86        LightMapEncoding::Linear => "linear",
87        LightMapEncoding::Rgbm => "rgbm",
88        LightMapEncoding::LogLuv => "logluv",
89    }
90}
91
92#[allow(dead_code)]
93pub fn lm_memory_bytes(atlas: &LightMapAtlas) -> u64 {
94    let bytes_per_texel: u64 = match atlas.encoding {
95        LightMapEncoding::Linear => 8,
96        LightMapEncoding::Rgbm => 4,
97        LightMapEncoding::LogLuv => 4,
98    };
99    atlas.page_count as u64 * atlas.page_size as u64 * atlas.page_size as u64 * bytes_per_texel
100}
101
102#[allow(dead_code)]
103pub fn lm_clear(atlas: &mut LightMapAtlas) {
104    atlas.entries.clear();
105}
106
107#[allow(dead_code)]
108pub fn lm_to_json(atlas: &LightMapAtlas) -> String {
109    format!(
110        "{{\"page_size\":{},\"pages\":{},\"encoding\":\"{}\",\"entries\":{}}}",
111        atlas.page_size,
112        atlas.page_count,
113        lm_encoding_name(atlas.encoding),
114        atlas.entries.len()
115    )
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn new_empty_entries() {
124        assert_eq!(lm_entry_count(&new_lightmap_atlas()), 0);
125    }
126
127    #[test]
128    fn add_entry() {
129        let mut a = new_lightmap_atlas();
130        lm_add_entry(&mut a, 1, [0.0, 0.0], [0.5, 0.5], 0);
131        assert_eq!(lm_entry_count(&a), 1);
132    }
133
134    #[test]
135    fn get_entry() {
136        let mut a = new_lightmap_atlas();
137        lm_add_entry(&mut a, 42, [0.25, 0.25], [0.25, 0.25], 0);
138        assert!(lm_get_entry(&a, 42).is_some());
139    }
140
141    #[test]
142    fn remove_entry() {
143        let mut a = new_lightmap_atlas();
144        lm_add_entry(&mut a, 1, [0.0, 0.0], [1.0, 1.0], 0);
145        lm_remove_entry(&mut a, 1);
146        assert_eq!(lm_entry_count(&a), 0);
147    }
148
149    #[test]
150    fn encoding_name_rgbm() {
151        assert_eq!(lm_encoding_name(LightMapEncoding::Rgbm), "rgbm");
152    }
153
154    #[test]
155    fn memory_bytes_positive() {
156        assert!(lm_memory_bytes(&new_lightmap_atlas()) > 0);
157    }
158
159    #[test]
160    fn clear_empty() {
161        let mut a = new_lightmap_atlas();
162        lm_add_entry(&mut a, 1, [0.0, 0.0], [1.0, 1.0], 0);
163        lm_clear(&mut a);
164        assert_eq!(lm_entry_count(&a), 0);
165    }
166
167    #[test]
168    fn json_has_page_size() {
169        assert!(lm_to_json(&new_lightmap_atlas()).contains("page_size"));
170    }
171
172    #[test]
173    fn get_missing_is_none() {
174        assert!(lm_get_entry(&new_lightmap_atlas(), 99).is_none());
175    }
176
177    #[test]
178    fn uv_scale_stored() {
179        let mut a = new_lightmap_atlas();
180        lm_add_entry(&mut a, 5, [0.1, 0.2], [0.3, 0.4], 0);
181        let e = lm_get_entry(&a, 5).expect("should succeed");
182        assert!((e.uv_scale[0] - 0.3).abs() < 1e-5);
183    }
184}