Skip to main content

oxihuman_viewer/
instance_batch.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan) / SPDX-License-Identifier: Apache-2.0
2#![allow(dead_code)]
3
4//! Instance batch — GPU-instanced draw batch management.
5
6/// Per-instance transform (column-major 4×4, stored as 4×`[f32;4]`).
7#[allow(dead_code)]
8#[derive(Debug, Clone, Copy)]
9pub struct InstanceTransform {
10    pub cols: [[f32; 4]; 4],
11}
12
13impl Default for InstanceTransform {
14    fn default() -> Self {
15        // Identity matrix
16        Self {
17            cols: [
18                [1.0, 0.0, 0.0, 0.0],
19                [0.0, 1.0, 0.0, 0.0],
20                [0.0, 0.0, 1.0, 0.0],
21                [0.0, 0.0, 0.0, 1.0],
22            ],
23        }
24    }
25}
26
27/// Per-instance data.
28#[allow(dead_code)]
29#[derive(Debug, Clone)]
30pub struct BatchInstance {
31    pub id: u32,
32    pub transform: InstanceTransform,
33    pub color_tint: [f32; 4],
34    pub lod_level: u8,
35    pub visible: bool,
36}
37
38/// Instance batch.
39#[allow(dead_code)]
40#[derive(Debug, Clone, Default)]
41pub struct InstanceBatch {
42    pub mesh_id: u32,
43    instances: Vec<BatchInstance>,
44}
45
46#[allow(dead_code)]
47pub fn new_instance_batch(mesh_id: u32) -> InstanceBatch {
48    InstanceBatch {
49        mesh_id,
50        instances: Vec::new(),
51    }
52}
53
54#[allow(dead_code)]
55pub fn ib_add(batch: &mut InstanceBatch, id: u32, transform: InstanceTransform) {
56    batch.instances.push(BatchInstance {
57        id,
58        transform,
59        color_tint: [1.0, 1.0, 1.0, 1.0],
60        lod_level: 0,
61        visible: true,
62    });
63}
64
65#[allow(dead_code)]
66pub fn ib_remove(batch: &mut InstanceBatch, id: u32) {
67    batch.instances.retain(|i| i.id != id);
68}
69
70#[allow(dead_code)]
71pub fn ib_set_visible(batch: &mut InstanceBatch, id: u32, vis: bool) {
72    for inst in batch.instances.iter_mut() {
73        if inst.id == id {
74            inst.visible = vis;
75        }
76    }
77}
78
79#[allow(dead_code)]
80pub fn ib_set_tint(batch: &mut InstanceBatch, id: u32, tint: [f32; 4]) {
81    for inst in batch.instances.iter_mut() {
82        if inst.id == id {
83            inst.color_tint = tint;
84        }
85    }
86}
87
88#[allow(dead_code)]
89pub fn ib_count(batch: &InstanceBatch) -> usize {
90    batch.instances.len()
91}
92
93#[allow(dead_code)]
94pub fn ib_visible_count(batch: &InstanceBatch) -> usize {
95    batch.instances.iter().filter(|i| i.visible).count()
96}
97
98#[allow(dead_code)]
99pub fn ib_clear(batch: &mut InstanceBatch) {
100    batch.instances.clear();
101}
102
103/// Extract visible transforms for upload.
104#[allow(dead_code)]
105pub fn ib_visible_transforms(batch: &InstanceBatch) -> Vec<InstanceTransform> {
106    batch
107        .instances
108        .iter()
109        .filter(|i| i.visible)
110        .map(|i| i.transform)
111        .collect()
112}
113
114/// Estimated GPU memory bytes (64 bytes per transform).
115#[allow(dead_code)]
116pub fn ib_memory_bytes(batch: &InstanceBatch) -> usize {
117    batch.instances.len() * 64
118}
119
120#[allow(dead_code)]
121pub fn ib_to_json(batch: &InstanceBatch) -> String {
122    format!(
123        "{{\"mesh_id\":{},\"count\":{},\"visible\":{}}}",
124        batch.mesh_id,
125        ib_count(batch),
126        ib_visible_count(batch)
127    )
128}
129
130/// Build a translation matrix (last column only).
131#[allow(dead_code)]
132pub fn ib_translation(tx: f32, ty: f32, tz: f32) -> InstanceTransform {
133    let mut t = InstanceTransform::default();
134    t.cols[3] = [tx, ty, tz, 1.0];
135    t
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn empty_batch() {
144        let b = new_instance_batch(1);
145        assert_eq!(ib_count(&b), 0);
146    }
147
148    #[test]
149    fn add_increments_count() {
150        let mut b = new_instance_batch(1);
151        ib_add(&mut b, 1, InstanceTransform::default());
152        assert_eq!(ib_count(&b), 1);
153    }
154
155    #[test]
156    fn remove_by_id() {
157        let mut b = new_instance_batch(1);
158        ib_add(&mut b, 1, InstanceTransform::default());
159        ib_remove(&mut b, 1);
160        assert_eq!(ib_count(&b), 0);
161    }
162
163    #[test]
164    fn set_invisible_reduces_visible_count() {
165        let mut b = new_instance_batch(1);
166        ib_add(&mut b, 1, InstanceTransform::default());
167        ib_set_visible(&mut b, 1, false);
168        assert_eq!(ib_visible_count(&b), 0);
169    }
170
171    #[test]
172    fn visible_transforms_filters() {
173        let mut b = new_instance_batch(1);
174        ib_add(&mut b, 1, InstanceTransform::default());
175        ib_add(&mut b, 2, InstanceTransform::default());
176        ib_set_visible(&mut b, 1, false);
177        assert_eq!(ib_visible_transforms(&b).len(), 1);
178    }
179
180    #[test]
181    fn clear_empties() {
182        let mut b = new_instance_batch(1);
183        ib_add(&mut b, 1, InstanceTransform::default());
184        ib_clear(&mut b);
185        assert_eq!(ib_count(&b), 0);
186    }
187
188    #[test]
189    fn memory_bytes_per_instance() {
190        let mut b = new_instance_batch(1);
191        ib_add(&mut b, 1, InstanceTransform::default());
192        assert_eq!(ib_memory_bytes(&b), 64);
193    }
194
195    #[test]
196    fn translation_sets_last_column() {
197        let t = ib_translation(1.0, 2.0, 3.0);
198        assert!((t.cols[3][0] - 1.0).abs() < 1e-6);
199        assert!((t.cols[3][1] - 2.0).abs() < 1e-6);
200    }
201
202    #[test]
203    fn json_has_mesh_id() {
204        let b = new_instance_batch(42);
205        assert!(ib_to_json(&b).contains("42"));
206    }
207
208    #[test]
209    fn tint_set() {
210        let mut b = new_instance_batch(1);
211        ib_add(&mut b, 1, InstanceTransform::default());
212        ib_set_tint(&mut b, 1, [1.0, 0.0, 0.0, 1.0]);
213        assert!((b.instances[0].color_tint[0] - 1.0).abs() < 1e-6);
214    }
215}