Skip to main content

oxihuman_export/
texture.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Procedural texture generation: RGBA pixel buffers for skin tones,
5//! checker patterns, gradient maps, UV visualization, and normal maps.
6
7#[allow(dead_code)]
8/// An RGBA pixel buffer.
9pub struct PixelBuffer {
10    pub width: u32,
11    pub height: u32,
12    /// Row-major RGBA bytes [r, g, b, a, r, g, b, a, ...]
13    pub pixels: Vec<u8>,
14}
15
16impl PixelBuffer {
17    /// Create a new zeroed pixel buffer.
18    pub fn new(width: u32, height: u32) -> Self {
19        Self {
20            width,
21            height,
22            pixels: vec![0u8; (width * height * 4) as usize],
23        }
24    }
25
26    /// Set pixel at (x, y) to RGBA.
27    pub fn set_pixel(&mut self, x: u32, y: u32, r: u8, g: u8, b: u8, a: u8) {
28        let idx = ((y * self.width + x) * 4) as usize;
29        self.pixels[idx] = r;
30        self.pixels[idx + 1] = g;
31        self.pixels[idx + 2] = b;
32        self.pixels[idx + 3] = a;
33    }
34
35    /// Get pixel at (x, y).
36    pub fn get_pixel(&self, x: u32, y: u32) -> [u8; 4] {
37        let idx = ((y * self.width + x) * 4) as usize;
38        [
39            self.pixels[idx],
40            self.pixels[idx + 1],
41            self.pixels[idx + 2],
42            self.pixels[idx + 3],
43        ]
44    }
45
46    /// Fill entire buffer with a flat color.
47    pub fn fill(&mut self, r: u8, g: u8, b: u8, a: u8) {
48        for chunk in self.pixels.chunks_exact_mut(4) {
49            chunk[0] = r;
50            chunk[1] = g;
51            chunk[2] = b;
52            chunk[3] = a;
53        }
54    }
55
56    /// Byte count.
57    pub fn byte_len(&self) -> usize {
58        self.pixels.len()
59    }
60
61    /// Export as a TGA file (simple uncompressed 32-bit RGBA TGA — no external crates needed).
62    ///
63    /// TGA format: 18-byte header + pixel data (top-to-bottom with flip flag).
64    pub fn to_tga_bytes(&self) -> Vec<u8> {
65        let width = self.width as u16;
66        let height = self.height as u16;
67        let pixel_count = (self.width * self.height) as usize;
68
69        // 18-byte TGA header
70        let mut out = Vec::with_capacity(18 + pixel_count * 4);
71
72        out.push(0u8); // [0]  no image ID
73        out.push(0u8); // [1]  no colormap
74        out.push(2u8); // [2]  uncompressed true-color
75        out.extend_from_slice(&[0u8, 0u8]); // [3..4] colormap origin
76        out.extend_from_slice(&[0u8, 0u8]); // [5..6] colormap length
77        out.push(0u8); // [7]  colormap depth
78        out.extend_from_slice(&[0u8, 0u8]); // [8..9]  x-origin
79        out.extend_from_slice(&[0u8, 0u8]); // [10..11] y-origin
80        out.extend_from_slice(&width.to_le_bytes()); // [12..13] width
81        out.extend_from_slice(&height.to_le_bytes()); // [14..15] height
82        out.push(32u8); // [16] bits per pixel (32 = RGBA)
83        out.push(0x28u8); // [17] image descriptor: top-left origin, 8 alpha bits
84
85        // Pixel data: row by row (top to bottom), each pixel as BGRA
86        for chunk in self.pixels.chunks_exact(4) {
87            let r = chunk[0];
88            let g = chunk[1];
89            let b = chunk[2];
90            let a = chunk[3];
91            out.push(b);
92            out.push(g);
93            out.push(r);
94            out.push(a);
95        }
96
97        out
98    }
99
100    /// Save to a .tga file.
101    pub fn save_tga(&self, path: &std::path::Path) -> anyhow::Result<()> {
102        std::fs::write(path, self.to_tga_bytes()).map_err(Into::into)
103    }
104}
105
106/// Generate a solid-color skin texture.
107pub fn generate_skin_texture(width: u32, height: u32, r: u8, g: u8, b: u8) -> PixelBuffer {
108    let mut buf = PixelBuffer::new(width, height);
109    buf.fill(r, g, b, 255);
110    buf
111}
112
113/// Generate a checker pattern (for UV debugging).
114pub fn generate_checker_texture(width: u32, height: u32, cell_size: u32) -> PixelBuffer {
115    let mut buf = PixelBuffer::new(width, height);
116    let cell_size = cell_size.max(1);
117    for y in 0..height {
118        for x in 0..width {
119            let cx = x / cell_size;
120            let cy = y / cell_size;
121            if (cx + cy).is_multiple_of(2) {
122                buf.set_pixel(x, y, 255, 255, 255, 255);
123            } else {
124                buf.set_pixel(x, y, 0, 0, 0, 255);
125            }
126        }
127    }
128    buf
129}
130
131/// Generate a vertical gradient from `top` to `bottom` color.
132pub fn generate_gradient_texture(
133    width: u32,
134    height: u32,
135    top: [u8; 3],
136    bottom: [u8; 3],
137) -> PixelBuffer {
138    let mut buf = PixelBuffer::new(width, height);
139    for y in 0..height {
140        let t = if height <= 1 {
141            0.0f32
142        } else {
143            y as f32 / (height - 1) as f32
144        };
145        let r = (top[0] as f32 * (1.0 - t) + bottom[0] as f32 * t).round() as u8;
146        let g = (top[1] as f32 * (1.0 - t) + bottom[1] as f32 * t).round() as u8;
147        let b = (top[2] as f32 * (1.0 - t) + bottom[2] as f32 * t).round() as u8;
148        for x in 0..width {
149            buf.set_pixel(x, y, r, g, b, 255);
150        }
151    }
152    buf
153}
154
155/// Generate a UV coordinate visualization texture:
156/// pixel (x,y) gets R = x/width * 255, G = y/height * 255, B = 0, A = 255.
157pub fn generate_uv_texture(width: u32, height: u32) -> PixelBuffer {
158    let mut buf = PixelBuffer::new(width, height);
159    for y in 0..height {
160        for x in 0..width {
161            let r = if width <= 1 {
162                0u8
163            } else {
164                (x as f32 / (width - 1) as f32 * 255.0).round() as u8
165            };
166            let g = if height <= 1 {
167                0u8
168            } else {
169                (y as f32 / (height - 1) as f32 * 255.0).round() as u8
170            };
171            buf.set_pixel(x, y, r, g, 0, 255);
172        }
173    }
174    buf
175}
176
177/// Generate a normal map texture (flat normal pointing toward viewer: RGB = [128, 128, 255]).
178pub fn generate_flat_normal_map(width: u32, height: u32) -> PixelBuffer {
179    let mut buf = PixelBuffer::new(width, height);
180    buf.fill(128, 128, 255, 255);
181    buf
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn pixel_buffer_size() {
190        let buf = PixelBuffer::new(4, 4);
191        assert_eq!(buf.byte_len(), 64);
192    }
193
194    #[test]
195    fn set_get_pixel() {
196        let mut buf = PixelBuffer::new(4, 4);
197        buf.set_pixel(1, 1, 255, 0, 128, 255);
198        assert_eq!(buf.get_pixel(1, 1), [255, 0, 128, 255]);
199    }
200
201    #[test]
202    fn fill_sets_all() {
203        let mut buf = PixelBuffer::new(4, 4);
204        buf.fill(255, 0, 0, 255);
205        assert_eq!(buf.get_pixel(0, 0), [255, 0, 0, 255]);
206        assert_eq!(buf.get_pixel(3, 3), [255, 0, 0, 255]);
207    }
208
209    #[test]
210    fn checker_alternates() {
211        let cell_size = 4u32;
212        let buf = generate_checker_texture(16, 16, cell_size);
213        let p0 = buf.get_pixel(0, 0);
214        let p1 = buf.get_pixel(cell_size, 0);
215        assert_ne!(p0, p1);
216    }
217
218    #[test]
219    fn gradient_top_equals_top_color() {
220        let top = [255u8, 0, 0];
221        let bottom = [0u8, 0, 255];
222        let buf = generate_gradient_texture(8, 8, top, bottom);
223        let p = buf.get_pixel(0, 0);
224        assert_eq!([p[0], p[1], p[2]], top);
225    }
226
227    #[test]
228    fn gradient_bottom_equals_bottom_color() {
229        let top = [255u8, 0, 0];
230        let bottom = [0u8, 0, 255];
231        let buf = generate_gradient_texture(8, 8, top, bottom);
232        let p = buf.get_pixel(0, 7);
233        assert_eq!([p[0], p[1], p[2]], bottom);
234    }
235
236    #[test]
237    fn uv_texture_top_left_is_zero() {
238        let buf = generate_uv_texture(8, 8);
239        let p = buf.get_pixel(0, 0);
240        assert_eq!(p[0], 0); // R = 0
241        assert_eq!(p[1], 0); // G = 0
242    }
243
244    #[test]
245    fn flat_normal_map_is_blue() {
246        let buf = generate_flat_normal_map(4, 4);
247        let p = buf.get_pixel(0, 0);
248        assert_eq!(p[2], 255); // B = 255
249    }
250
251    #[test]
252    fn tga_header_magic() {
253        let buf = generate_skin_texture(4, 4, 200, 150, 130);
254        let bytes = buf.to_tga_bytes();
255        assert_eq!(bytes[2], 2); // uncompressed true-color type
256    }
257
258    #[test]
259    fn save_tga_creates_file() {
260        let buf = generate_uv_texture(16, 16);
261        let path = std::path::Path::new("/tmp/test_texture.tga");
262        buf.save_tga(path).expect("save_tga failed");
263        assert!(path.exists());
264    }
265}