Skip to main content

oxihuman_export/
normal_map_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Normal map generation and export (tangent-space and object-space).
5
6#[allow(dead_code)]
7#[derive(Clone, PartialEq, Debug)]
8pub enum NormalMapSpace {
9    TangentSpace,
10    ObjectSpace,
11    WorldSpace,
12}
13
14#[allow(dead_code)]
15pub struct NormalMapConfig {
16    pub width: u32,
17    pub height: u32,
18    pub space: NormalMapSpace,
19    pub flip_green: bool,
20    pub samples: u32,
21}
22
23#[allow(dead_code)]
24pub struct NormalMapBuffer {
25    pub pixels: Vec<[u8; 3]>,
26    pub width: u32,
27    pub height: u32,
28}
29
30#[allow(dead_code)]
31pub fn default_normal_map_config(width: u32, height: u32) -> NormalMapConfig {
32    NormalMapConfig {
33        width,
34        height,
35        space: NormalMapSpace::TangentSpace,
36        flip_green: false,
37        samples: 1,
38    }
39}
40
41#[allow(dead_code)]
42pub fn new_normal_map_buffer(width: u32, height: u32) -> NormalMapBuffer {
43    let count = (width * height) as usize;
44    NormalMapBuffer {
45        pixels: vec![[128, 128, 255]; count],
46        width,
47        height,
48    }
49}
50
51#[allow(dead_code)]
52pub fn normal_to_rgb(n: [f32; 3]) -> [u8; 3] {
53    let clamp = |v: f32| v.clamp(-1.0, 1.0);
54    let to_u8 = |v: f32| ((clamp(v) * 0.5 + 0.5) * 255.0).round() as u8;
55    [to_u8(n[0]), to_u8(n[1]), to_u8(n[2])]
56}
57
58#[allow(dead_code)]
59pub fn rgb_to_normal(rgb: [u8; 3]) -> [f32; 3] {
60    let to_f = |v: u8| (v as f32 / 255.0) * 2.0 - 1.0;
61    let nx = to_f(rgb[0]);
62    let ny = to_f(rgb[1]);
63    let nz = to_f(rgb[2]);
64    let len = (nx * nx + ny * ny + nz * nz).sqrt().max(1e-8);
65    [nx / len, ny / len, nz / len]
66}
67
68#[allow(dead_code)]
69pub fn flat_normal_map(buffer: &mut NormalMapBuffer, normal: [f32; 3]) {
70    let rgb = normal_to_rgb(normal);
71    for pixel in buffer.pixels.iter_mut() {
72        *pixel = rgb;
73    }
74}
75
76#[allow(dead_code)]
77pub fn encode_normal_map_ppm(buffer: &NormalMapBuffer) -> Vec<u8> {
78    let header = format!("P6\n{} {}\n255\n", buffer.width, buffer.height);
79    let mut out = header.into_bytes();
80    for pixel in &buffer.pixels {
81        out.push(pixel[0]);
82        out.push(pixel[1]);
83        out.push(pixel[2]);
84    }
85    out
86}
87
88#[allow(dead_code)]
89pub fn compute_object_space_normals(positions: &[[f32; 3]], indices: &[u32]) -> Vec<[f32; 3]> {
90    let mut normals = vec![[0.0f32; 3]; positions.len()];
91    let tri_count = indices.len() / 3;
92    for tri in 0..tri_count {
93        let i0 = indices[tri * 3] as usize;
94        let i1 = indices[tri * 3 + 1] as usize;
95        let i2 = indices[tri * 3 + 2] as usize;
96        if i0 >= positions.len() || i1 >= positions.len() || i2 >= positions.len() {
97            continue;
98        }
99        let p0 = positions[i0];
100        let p1 = positions[i1];
101        let p2 = positions[i2];
102        let e1 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
103        let e2 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
104        let cross = [
105            e1[1] * e2[2] - e1[2] * e2[1],
106            e1[2] * e2[0] - e1[0] * e2[2],
107            e1[0] * e2[1] - e1[1] * e2[0],
108        ];
109        for idx in [i0, i1, i2] {
110            normals[idx][0] += cross[0];
111            normals[idx][1] += cross[1];
112            normals[idx][2] += cross[2];
113        }
114    }
115    for n in normals.iter_mut() {
116        let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt().max(1e-8);
117        n[0] /= len;
118        n[1] /= len;
119        n[2] /= len;
120    }
121    normals
122}
123
124#[allow(dead_code)]
125pub fn normal_map_from_vertex_normals(
126    normals: &[[f32; 3]],
127    uvs: &[[f32; 2]],
128    width: u32,
129    height: u32,
130) -> NormalMapBuffer {
131    let mut buffer = new_normal_map_buffer(width, height);
132    let count = normals.len().min(uvs.len());
133    for i in 0..count {
134        let u = uvs[i][0].clamp(0.0, 1.0);
135        let v = uvs[i][1].clamp(0.0, 1.0);
136        let px = (u * (width as f32 - 1.0)).round() as u32;
137        let py = (v * (height as f32 - 1.0)).round() as u32;
138        set_normal_pixel(&mut buffer, px, py, normals[i]);
139    }
140    buffer
141}
142
143#[allow(dead_code)]
144pub fn blend_normal_maps(a: &NormalMapBuffer, b: &NormalMapBuffer, t: f32) -> NormalMapBuffer {
145    let width = a.width;
146    let height = a.height;
147    let count = (width * height) as usize;
148    let mut pixels = Vec::with_capacity(count);
149    let t = t.clamp(0.0, 1.0);
150    for i in 0..count {
151        let na = rgb_to_normal(a.pixels[i]);
152        let nb = if i < b.pixels.len() {
153            rgb_to_normal(b.pixels[i])
154        } else {
155            [0.0, 0.0, 1.0]
156        };
157        let blended = [
158            na[0] * (1.0 - t) + nb[0] * t,
159            na[1] * (1.0 - t) + nb[1] * t,
160            na[2] * (1.0 - t) + nb[2] * t,
161        ];
162        let len = (blended[0] * blended[0] + blended[1] * blended[1] + blended[2] * blended[2])
163            .sqrt()
164            .max(1e-8);
165        let norm = [blended[0] / len, blended[1] / len, blended[2] / len];
166        pixels.push(normal_to_rgb(norm));
167    }
168    NormalMapBuffer {
169        pixels,
170        width,
171        height,
172    }
173}
174
175#[allow(dead_code)]
176pub fn normal_map_pixel_count(buffer: &NormalMapBuffer) -> usize {
177    buffer.pixels.len()
178}
179
180#[allow(dead_code)]
181pub fn normal_map_size_bytes(buffer: &NormalMapBuffer) -> usize {
182    buffer.pixels.len() * 3
183}
184
185#[allow(dead_code)]
186pub fn set_normal_pixel(buffer: &mut NormalMapBuffer, x: u32, y: u32, normal: [f32; 3]) {
187    if x < buffer.width && y < buffer.height {
188        let idx = (y * buffer.width + x) as usize;
189        buffer.pixels[idx] = normal_to_rgb(normal);
190    }
191}
192
193#[allow(dead_code)]
194pub fn get_normal_pixel(buffer: &NormalMapBuffer, x: u32, y: u32) -> [f32; 3] {
195    if x < buffer.width && y < buffer.height {
196        let idx = (y * buffer.width + x) as usize;
197        rgb_to_normal(buffer.pixels[idx])
198    } else {
199        [0.0, 0.0, 1.0]
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn test_default_config() {
209        let cfg = default_normal_map_config(512, 256);
210        assert_eq!(cfg.width, 512);
211        assert_eq!(cfg.height, 256);
212        assert_eq!(cfg.space, NormalMapSpace::TangentSpace);
213        assert!(!cfg.flip_green);
214        assert_eq!(cfg.samples, 1);
215    }
216
217    #[test]
218    fn test_new_buffer() {
219        let buf = new_normal_map_buffer(4, 4);
220        assert_eq!(buf.width, 4);
221        assert_eq!(buf.height, 4);
222        assert_eq!(buf.pixels.len(), 16);
223    }
224
225    #[test]
226    fn test_normal_to_rgb_round_trip() {
227        let n = [0.0f32, 0.0, 1.0];
228        let rgb = normal_to_rgb(n);
229        let back = rgb_to_normal(rgb);
230        assert!((back[2] - 1.0).abs() < 0.02);
231    }
232
233    #[test]
234    fn test_normal_to_rgb_values() {
235        let rgb = normal_to_rgb([0.0, 0.0, 1.0]);
236        assert_eq!(rgb[0], 128);
237        assert_eq!(rgb[1], 128);
238        assert_eq!(rgb[2], 255);
239    }
240
241    #[test]
242    fn test_rgb_to_normal_normalized() {
243        let n = rgb_to_normal([255, 128, 128]);
244        let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
245        assert!((len - 1.0).abs() < 0.05);
246    }
247
248    #[test]
249    fn test_flat_normal_map() {
250        let mut buf = new_normal_map_buffer(4, 4);
251        flat_normal_map(&mut buf, [0.0, 0.0, 1.0]);
252        let rgb = normal_to_rgb([0.0, 0.0, 1.0]);
253        for pixel in &buf.pixels {
254            assert_eq!(pixel, &rgb);
255        }
256    }
257
258    #[test]
259    fn test_encode_ppm_starts_with_p6() {
260        let buf = new_normal_map_buffer(2, 2);
261        let ppm = encode_normal_map_ppm(&buf);
262        assert!(ppm.starts_with(b"P6"));
263    }
264
265    #[test]
266    fn test_encode_ppm_size() {
267        let buf = new_normal_map_buffer(3, 3);
268        let ppm = encode_normal_map_ppm(&buf);
269        let header = b"P6\n3 3\n255\n";
270        assert!(ppm.len() >= header.len() + 3 * 3 * 3);
271    }
272
273    #[test]
274    fn test_normal_map_pixel_count() {
275        let buf = new_normal_map_buffer(8, 8);
276        assert_eq!(normal_map_pixel_count(&buf), 64);
277    }
278
279    #[test]
280    fn test_normal_map_size_bytes() {
281        let buf = new_normal_map_buffer(8, 8);
282        assert_eq!(normal_map_size_bytes(&buf), 192);
283    }
284
285    #[test]
286    fn test_set_get_pixel() {
287        let mut buf = new_normal_map_buffer(4, 4);
288        let n = [1.0f32, 0.0, 0.0];
289        set_normal_pixel(&mut buf, 2, 1, n);
290        let got = get_normal_pixel(&buf, 2, 1);
291        assert!((got[0] - 1.0).abs() < 0.05);
292    }
293
294    #[test]
295    fn test_blend_maps() {
296        let a = new_normal_map_buffer(2, 2);
297        let mut b = new_normal_map_buffer(2, 2);
298        flat_normal_map(&mut b, [1.0, 0.0, 0.0]);
299        let blended = blend_normal_maps(&a, &b, 0.5);
300        assert_eq!(blended.pixels.len(), 4);
301    }
302
303    #[test]
304    fn test_object_space_normals_triangle() {
305        let positions = [[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
306        let indices = [0u32, 1, 2];
307        let normals = compute_object_space_normals(&positions, &indices);
308        assert_eq!(normals.len(), 3);
309        for n in &normals {
310            let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
311            assert!((len - 1.0).abs() < 0.01);
312        }
313    }
314
315    #[test]
316    fn test_normal_map_from_vertex_normals() {
317        let normals = vec![[0.0f32, 0.0, 1.0]; 4];
318        let uvs = vec![[0.0f32, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
319        let buf = normal_map_from_vertex_normals(&normals, &uvs, 8, 8);
320        assert_eq!(buf.width, 8);
321        assert_eq!(buf.height, 8);
322    }
323
324    #[test]
325    fn test_get_pixel_out_of_bounds() {
326        let buf = new_normal_map_buffer(4, 4);
327        let n = get_normal_pixel(&buf, 10, 10);
328        assert_eq!(n, [0.0, 0.0, 1.0]);
329    }
330}