Skip to main content

oxihuman_export/
texture_packer.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Texture atlas packing utilities.
5
6#[allow(dead_code)]
7pub struct TextureRect {
8    pub id: u32,
9    pub x: u32,
10    pub y: u32,
11    pub width: u32,
12    pub height: u32,
13}
14
15#[allow(dead_code)]
16pub struct PackInput {
17    pub id: u32,
18    pub width: u32,
19    pub height: u32,
20    pub pixels: Vec<u8>, // RGBA
21}
22
23#[allow(dead_code)]
24pub struct PackResult {
25    pub atlas_width: u32,
26    pub atlas_height: u32,
27    pub placements: Vec<TextureRect>,
28    pub atlas_pixels: Vec<u8>,
29}
30
31#[allow(dead_code)]
32pub struct PackConfig {
33    pub padding: u32,
34    pub power_of_two: bool,
35    pub max_size: u32,
36}
37
38#[allow(dead_code)]
39pub fn default_pack_config() -> PackConfig {
40    PackConfig {
41        padding: 1,
42        power_of_two: true,
43        max_size: 4096,
44    }
45}
46
47#[allow(dead_code)]
48pub fn next_power_of_two(n: u32) -> u32 {
49    if n == 0 {
50        return 1;
51    }
52    let mut v = n;
53    v -= 1;
54    v |= v >> 1;
55    v |= v >> 2;
56    v |= v >> 4;
57    v |= v >> 8;
58    v |= v >> 16;
59    v + 1
60}
61
62/// Shelf-first packing algorithm.
63#[allow(dead_code)]
64pub fn pack_textures(inputs: &[PackInput], cfg: &PackConfig) -> PackResult {
65    if inputs.is_empty() {
66        return PackResult {
67            atlas_width: 0,
68            atlas_height: 0,
69            placements: Vec::new(),
70            atlas_pixels: Vec::new(),
71        };
72    }
73
74    // Sort by height descending for better packing
75    let mut order: Vec<usize> = (0..inputs.len()).collect();
76    order.sort_by(|&a, &b| inputs[b].height.cmp(&inputs[a].height));
77
78    let max_w = cfg.max_size;
79    let pad = cfg.padding;
80
81    let mut placements: Vec<TextureRect> = Vec::new();
82    let mut shelf_x = 0u32;
83    let mut shelf_y = 0u32;
84    let mut shelf_h = 0u32;
85    let mut atlas_w = 0u32;
86    let mut atlas_h = 0u32;
87
88    for &idx in &order {
89        let inp = &inputs[idx];
90        let needed_w = inp.width + pad;
91        let needed_h = inp.height + pad;
92
93        if shelf_x + needed_w > max_w {
94            // New shelf
95            shelf_y += shelf_h;
96            shelf_x = 0;
97            shelf_h = 0;
98        }
99
100        let x = shelf_x;
101        let y = shelf_y;
102
103        shelf_x += needed_w;
104        if needed_h > shelf_h {
105            shelf_h = needed_h;
106        }
107
108        let right = x + inp.width;
109        let bottom = y + inp.height;
110        if right > atlas_w {
111            atlas_w = right;
112        }
113        if bottom + pad > atlas_h {
114            atlas_h = bottom + pad;
115        }
116
117        placements.push(TextureRect {
118            id: inp.id,
119            x,
120            y,
121            width: inp.width,
122            height: inp.height,
123        });
124    }
125
126    // Finalise atlas dimensions
127    atlas_h += shelf_h.saturating_sub(pad);
128
129    if cfg.power_of_two {
130        atlas_w = next_power_of_two(atlas_w).min(cfg.max_size);
131        atlas_h = next_power_of_two(atlas_h).min(cfg.max_size);
132    } else {
133        atlas_w = atlas_w.min(cfg.max_size);
134        atlas_h = atlas_h.min(cfg.max_size);
135    }
136
137    let mut atlas_pixels = vec![0u8; (atlas_w * atlas_h * 4) as usize];
138
139    for (placement, &orig_idx) in placements.iter().zip(order.iter()) {
140        let src = &inputs[orig_idx];
141        blit_texture(
142            &mut atlas_pixels,
143            atlas_w,
144            &src.pixels,
145            src.width,
146            src.height,
147            placement.x,
148            placement.y,
149        );
150    }
151
152    PackResult {
153        atlas_width: atlas_w,
154        atlas_height: atlas_h,
155        placements,
156        atlas_pixels,
157    }
158}
159
160/// Blit RGBA pixels from src into atlas at (dst_x, dst_y).
161#[allow(dead_code)]
162pub fn blit_texture(
163    atlas: &mut [u8],
164    atlas_w: u32,
165    src: &[u8],
166    src_w: u32,
167    src_h: u32,
168    dst_x: u32,
169    dst_y: u32,
170) {
171    for row in 0..src_h {
172        for col in 0..src_w {
173            let src_idx = ((row * src_w + col) * 4) as usize;
174            let dst_idx = (((dst_y + row) * atlas_w + (dst_x + col)) * 4) as usize;
175            if src_idx + 3 < src.len() && dst_idx + 3 < atlas.len() {
176                atlas[dst_idx] = src[src_idx];
177                atlas[dst_idx + 1] = src[src_idx + 1];
178                atlas[dst_idx + 2] = src[src_idx + 2];
179                atlas[dst_idx + 3] = src[src_idx + 3];
180            }
181        }
182    }
183}
184
185#[allow(dead_code)]
186pub fn pack_single(input: &PackInput, cfg: &PackConfig) -> PackResult {
187    pack_textures(std::slice::from_ref(input), cfg)
188}
189
190/// Returns (offset `[u,v]`, scale `[u,v]`) for remapping UVs from a sub-texture to atlas space.
191#[allow(dead_code)]
192pub fn uv_transform_for_rect(
193    rect: &TextureRect,
194    atlas_w: u32,
195    atlas_h: u32,
196) -> ([f32; 2], [f32; 2]) {
197    let offset = [
198        rect.x as f32 / atlas_w as f32,
199        rect.y as f32 / atlas_h as f32,
200    ];
201    let scale = [
202        rect.width as f32 / atlas_w as f32,
203        rect.height as f32 / atlas_h as f32,
204    ];
205    (offset, scale)
206}
207
208#[allow(dead_code)]
209pub fn find_placement(result: &PackResult, id: u32) -> Option<&TextureRect> {
210    result.placements.iter().find(|r| r.id == id)
211}
212
213#[allow(dead_code)]
214pub fn generate_solid_color_texture(width: u32, height: u32, color: [u8; 4]) -> PackInput {
215    let pixel_count = (width * height * 4) as usize;
216    let mut pixels = Vec::with_capacity(pixel_count);
217    for _ in 0..(width * height) {
218        pixels.extend_from_slice(&color);
219    }
220    PackInput {
221        id: 0,
222        width,
223        height,
224        pixels,
225    }
226}
227
228#[allow(dead_code)]
229pub fn pack_config_max_size(cfg: &PackConfig) -> u32 {
230    cfg.max_size
231}
232
233#[allow(dead_code)]
234pub fn atlas_pixel_count(result: &PackResult) -> usize {
235    result.atlas_pixels.len()
236}
237
238#[allow(dead_code)]
239pub fn atlas_utilization(result: &PackResult) -> f32 {
240    let total = result.atlas_width * result.atlas_height;
241    if total == 0 {
242        return 0.0;
243    }
244    let packed: u32 = result.placements.iter().map(|r| r.width * r.height).sum();
245    packed as f32 / total as f32
246}
247
248#[allow(dead_code)]
249pub fn rects_overlap(a: &TextureRect, b: &TextureRect) -> bool {
250    !(a.x + a.width <= b.x
251        || b.x + b.width <= a.x
252        || a.y + a.height <= b.y
253        || b.y + b.height <= a.y)
254}
255
256// ── Tests ─────────────────────────────────────────────────────────────────────
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn test_next_power_of_two_zero() {
264        assert_eq!(next_power_of_two(0), 1);
265    }
266
267    #[test]
268    fn test_next_power_of_two_exact() {
269        assert_eq!(next_power_of_two(8), 8);
270    }
271
272    #[test]
273    fn test_next_power_of_two_round_up() {
274        assert_eq!(next_power_of_two(5), 8);
275        assert_eq!(next_power_of_two(100), 128);
276        assert_eq!(next_power_of_two(257), 512);
277    }
278
279    #[test]
280    fn test_pack_single_texture() {
281        let cfg = default_pack_config();
282        let inp = generate_solid_color_texture(64, 64, [255, 0, 0, 255]);
283        let result = pack_single(&inp, &cfg);
284        assert_eq!(result.placements.len(), 1);
285        assert!(result.atlas_width >= 64);
286        assert!(result.atlas_height >= 64);
287    }
288
289    #[test]
290    fn test_pack_multiple_textures() {
291        let cfg = default_pack_config();
292        let inputs: Vec<PackInput> = (0..4u32)
293            .map(|id| {
294                let mut t = generate_solid_color_texture(32, 32, [id as u8 * 60, 0, 0, 255]);
295                t.id = id;
296                t
297            })
298            .collect();
299        let result = pack_textures(&inputs, &cfg);
300        assert_eq!(result.placements.len(), 4);
301    }
302
303    #[test]
304    fn test_no_overlaps_in_result() {
305        let cfg = default_pack_config();
306        let inputs: Vec<PackInput> = (0..6u32)
307            .map(|id| {
308                let mut t = generate_solid_color_texture(20, 20, [255, 255, 255, 255]);
309                t.id = id;
310                t
311            })
312            .collect();
313        let result = pack_textures(&inputs, &cfg);
314        let n = result.placements.len();
315        for i in 0..n {
316            for j in (i + 1)..n {
317                assert!(
318                    !rects_overlap(&result.placements[i], &result.placements[j]),
319                    "Rects {} and {} overlap",
320                    i,
321                    j
322                );
323            }
324        }
325    }
326
327    #[test]
328    fn test_blit_texture_correct_size() {
329        let src = vec![255u8, 0, 0, 255, 0, 255, 0, 255]; // 2 RGBA pixels
330        let mut atlas = vec![0u8; 16 * 16 * 4];
331        blit_texture(&mut atlas, 16, &src, 2, 1, 0, 0);
332        // First pixel should be red
333        assert_eq!(atlas[0], 255);
334        assert_eq!(atlas[1], 0);
335        assert_eq!(atlas[2], 0);
336        assert_eq!(atlas[3], 255);
337    }
338
339    #[test]
340    fn test_utilization_in_valid_range() {
341        let cfg = default_pack_config();
342        let inputs: Vec<PackInput> = (0..3u32)
343            .map(|id| {
344                let mut t = generate_solid_color_texture(16, 16, [100, 100, 100, 255]);
345                t.id = id;
346                t
347            })
348            .collect();
349        let result = pack_textures(&inputs, &cfg);
350        let u = atlas_utilization(&result);
351        assert!(u > 0.0);
352        assert!(u <= 1.0);
353    }
354
355    #[test]
356    fn test_solid_color_texture_pixel_count() {
357        let t = generate_solid_color_texture(4, 4, [0, 0, 255, 255]);
358        assert_eq!(t.pixels.len(), 4 * 4 * 4);
359    }
360
361    #[test]
362    fn test_solid_color_texture_values() {
363        let t = generate_solid_color_texture(2, 2, [10, 20, 30, 255]);
364        assert_eq!(t.pixels[0], 10);
365        assert_eq!(t.pixels[1], 20);
366        assert_eq!(t.pixels[2], 30);
367        assert_eq!(t.pixels[3], 255);
368    }
369
370    #[test]
371    fn test_uv_transform() {
372        let rect = TextureRect {
373            id: 0,
374            x: 0,
375            y: 0,
376            width: 64,
377            height: 64,
378        };
379        let (offset, scale) = uv_transform_for_rect(&rect, 128, 128);
380        assert_eq!(offset, [0.0, 0.0]);
381        assert!((scale[0] - 0.5).abs() < 1e-5);
382        assert!((scale[1] - 0.5).abs() < 1e-5);
383    }
384
385    #[test]
386    fn test_uv_transform_offset() {
387        let rect = TextureRect {
388            id: 1,
389            x: 64,
390            y: 0,
391            width: 64,
392            height: 64,
393        };
394        let (offset, _scale) = uv_transform_for_rect(&rect, 128, 128);
395        assert!((offset[0] - 0.5).abs() < 1e-5);
396    }
397
398    #[test]
399    fn test_find_placement() {
400        let cfg = default_pack_config();
401        let mut inp = generate_solid_color_texture(10, 10, [0, 0, 0, 255]);
402        inp.id = 42;
403        let result = pack_single(&inp, &cfg);
404        let found = find_placement(&result, 42);
405        assert!(found.is_some());
406        assert_eq!(found.expect("should succeed").id, 42);
407    }
408
409    #[test]
410    fn test_find_placement_missing() {
411        let cfg = default_pack_config();
412        let inp = generate_solid_color_texture(10, 10, [0, 0, 0, 255]);
413        let result = pack_single(&inp, &cfg);
414        assert!(find_placement(&result, 99).is_none());
415    }
416
417    #[test]
418    fn test_atlas_pixel_count() {
419        let cfg = default_pack_config();
420        let inp = generate_solid_color_texture(8, 8, [0, 0, 0, 255]);
421        let result = pack_single(&inp, &cfg);
422        // Atlas pixels = atlas_w * atlas_h * 4
423        assert_eq!(
424            atlas_pixel_count(&result),
425            (result.atlas_width * result.atlas_height * 4) as usize
426        );
427    }
428
429    #[test]
430    fn test_rects_no_overlap() {
431        let a = TextureRect {
432            id: 0,
433            x: 0,
434            y: 0,
435            width: 10,
436            height: 10,
437        };
438        let b = TextureRect {
439            id: 1,
440            x: 10,
441            y: 0,
442            width: 10,
443            height: 10,
444        };
445        assert!(!rects_overlap(&a, &b));
446    }
447
448    #[test]
449    fn test_rects_do_overlap() {
450        let a = TextureRect {
451            id: 0,
452            x: 0,
453            y: 0,
454            width: 10,
455            height: 10,
456        };
457        let b = TextureRect {
458            id: 1,
459            x: 5,
460            y: 5,
461            width: 10,
462            height: 10,
463        };
464        assert!(rects_overlap(&a, &b));
465    }
466}