1use anyhow::Result;
5use bytemuck::cast_slice;
6use oxihuman_mesh::MeshBuffers;
7use serde_json::json;
8use std::f32::consts::PI;
9use std::io::Write;
10use std::path::Path;
11
12const GLB_MAGIC: u32 = 0x46546C67; const GLB_VERSION: u32 = 2;
15const CHUNK_JSON: u32 = 0x4E4F534A; const CHUNK_BIN: u32 = 0x004E4942; #[derive(Debug, Clone)]
20pub struct InstanceTransform {
21 pub translation: [f32; 3],
22 pub rotation: [f32; 4], pub scale: [f32; 3],
24}
25
26impl InstanceTransform {
27 pub fn identity() -> Self {
28 Self {
29 translation: [0.0, 0.0, 0.0],
30 rotation: [0.0, 0.0, 0.0, 1.0],
31 scale: [1.0, 1.0, 1.0],
32 }
33 }
34
35 pub fn at(translation: [f32; 3]) -> Self {
36 Self {
37 translation,
38 ..Self::identity()
39 }
40 }
41
42 pub fn scaled(mut self, s: f32) -> Self {
43 self.scale = [s, s, s];
44 self
45 }
46}
47
48pub fn export_instanced_glb(
52 mesh: &MeshBuffers,
53 instances: &[InstanceTransform],
54 path: &Path,
55) -> Result<()> {
56 let n_verts = mesh.positions.len();
59 let n_idx = mesh.indices.len();
60
61 let pos_bytes: &[u8] = cast_slice(&mesh.positions);
62 let norm_bytes: &[u8] = cast_slice(&mesh.normals);
63 let uv_bytes: &[u8] = cast_slice(&mesh.uvs);
64 let idx_bytes: &[u8] = cast_slice(&mesh.indices);
65
66 let pos_offset = 0usize;
67 let norm_offset = pos_offset + pos_bytes.len();
68 let uv_offset = norm_offset + norm_bytes.len();
69 let idx_offset = uv_offset + uv_bytes.len();
70 let bin_len = idx_offset + idx_bytes.len();
71
72 let mut bin_data: Vec<u8> = Vec::with_capacity(bin_len + 3);
73 bin_data.extend_from_slice(pos_bytes);
74 bin_data.extend_from_slice(norm_bytes);
75 bin_data.extend_from_slice(uv_bytes);
76 bin_data.extend_from_slice(idx_bytes);
77 while !bin_data.len().is_multiple_of(4) {
79 bin_data.push(0x00);
80 }
81
82 let total_bin = bin_data.len() as u32;
84
85 let accessors = vec![
86 json!({ "bufferView": 0, "componentType": 5126, "count": n_verts, "type": "VEC3" }),
87 json!({ "bufferView": 1, "componentType": 5126, "count": n_verts, "type": "VEC3" }),
88 json!({ "bufferView": 2, "componentType": 5126, "count": n_verts, "type": "VEC2" }),
89 json!({ "bufferView": 3, "componentType": 5125, "count": n_idx, "type": "SCALAR" }),
90 ];
91
92 let buffer_views = vec![
93 json!({ "buffer": 0, "byteOffset": pos_offset, "byteLength": pos_bytes.len() }),
94 json!({ "buffer": 0, "byteOffset": norm_offset, "byteLength": norm_bytes.len() }),
95 json!({ "buffer": 0, "byteOffset": uv_offset, "byteLength": uv_bytes.len() }),
96 json!({ "buffer": 0, "byteOffset": idx_offset, "byteLength": idx_bytes.len() }),
97 ];
98
99 let nodes: Vec<serde_json::Value> = instances
101 .iter()
102 .map(|inst| {
103 json!({
104 "mesh": 0,
105 "translation": inst.translation,
106 "rotation": inst.rotation,
107 "scale": inst.scale
108 })
109 })
110 .collect();
111
112 let node_indices: Vec<usize> = (0..instances.len()).collect();
114
115 let gltf = json!({
116 "asset": { "version": "2.0", "generator": "OxiHuman 0.1.0" },
117 "scene": 0,
118 "scenes": [{ "nodes": node_indices }],
119 "nodes": nodes,
120 "meshes": [{
121 "name": "instanced_mesh",
122 "primitives": [{
123 "attributes": {
124 "POSITION": 0,
125 "NORMAL": 1,
126 "TEXCOORD_0": 2
127 },
128 "indices": 3,
129 "mode": 4
130 }]
131 }],
132 "accessors": accessors,
133 "bufferViews": buffer_views,
134 "buffers": [{ "byteLength": total_bin }]
135 });
136
137 let mut json_bytes = serde_json::to_vec(&gltf)?;
138 while json_bytes.len() % 4 != 0 {
140 json_bytes.push(b' ');
141 }
142
143 let json_chunk_len = json_bytes.len() as u32;
145 let bin_chunk_len = bin_data.len() as u32;
146 let total_len = 12 + 8 + json_chunk_len + 8 + bin_chunk_len;
147
148 let mut file = std::fs::File::create(path)?;
149
150 file.write_all(&GLB_MAGIC.to_le_bytes())?;
152 file.write_all(&GLB_VERSION.to_le_bytes())?;
153 file.write_all(&total_len.to_le_bytes())?;
154
155 file.write_all(&json_chunk_len.to_le_bytes())?;
157 file.write_all(&CHUNK_JSON.to_le_bytes())?;
158 file.write_all(&json_bytes)?;
159
160 file.write_all(&bin_chunk_len.to_le_bytes())?;
162 file.write_all(&CHUNK_BIN.to_le_bytes())?;
163 file.write_all(&bin_data)?;
164
165 Ok(())
166}
167
168pub fn row_instances(count: usize, spacing: f32) -> Vec<InstanceTransform> {
170 (0..count)
171 .map(|i| InstanceTransform::at([i as f32 * spacing, 0.0, 0.0]))
172 .collect()
173}
174
175pub fn grid_instances(rows: usize, cols: usize, spacing: f32) -> Vec<InstanceTransform> {
177 let mut out = Vec::with_capacity(rows * cols);
178 for r in 0..rows {
179 for c in 0..cols {
180 out.push(InstanceTransform::at([
181 c as f32 * spacing,
182 0.0,
183 r as f32 * spacing,
184 ]));
185 }
186 }
187 out
188}
189
190pub fn circle_instances(count: usize, radius: f32) -> Vec<InstanceTransform> {
192 (0..count)
193 .map(|i| {
194 let angle = 2.0 * PI * i as f32 / count as f32;
195 InstanceTransform::at([radius * angle.cos(), 0.0, radius * angle.sin()])
196 })
197 .collect()
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use std::io::Read;
204 use std::path::PathBuf;
205
206 fn tri_mesh() -> MeshBuffers {
207 MeshBuffers {
208 positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
209 normals: vec![[0.0, 0.0, 1.0]; 3],
210 uvs: vec![[0.0, 0.0]; 3],
211 tangents: vec![],
212 colors: None,
213 indices: vec![0, 1, 2],
214 has_suit: true,
215 }
216 }
217
218 fn tmp_path(name: &str) -> PathBuf {
219 PathBuf::from(format!("/tmp/{}", name))
220 }
221
222 #[test]
223 fn row_instances_count() {
224 assert_eq!(row_instances(5, 2.0).len(), 5);
225 }
226
227 #[test]
228 fn grid_instances_count() {
229 assert_eq!(grid_instances(3, 4, 1.0).len(), 12);
230 }
231
232 #[test]
233 fn circle_instances_count() {
234 assert_eq!(circle_instances(8, 2.0).len(), 8);
235 }
236
237 #[test]
238 fn export_single_instance() {
239 let mesh = tri_mesh();
240 let path = tmp_path("test_instancing_single.glb");
241 export_instanced_glb(&mesh, &[InstanceTransform::identity()], &path).expect("should succeed");
242 assert!(path.exists());
243 }
244
245 #[test]
246 fn export_five_instances() {
247 let mesh = tri_mesh();
248 let path = tmp_path("test_instancing_five.glb");
249 let instances = row_instances(5, 1.5);
250 export_instanced_glb(&mesh, &instances, &path).expect("should succeed");
251 assert!(path.exists());
252 let metadata = std::fs::metadata(&path).expect("should succeed");
254 assert!(metadata.len() > 12);
255 }
256
257 #[test]
258 fn glb_header_valid() {
259 let mesh = tri_mesh();
260 let path = tmp_path("test_instancing_header.glb");
261 export_instanced_glb(&mesh, &[InstanceTransform::identity()], &path).expect("should succeed");
262 let mut f = std::fs::File::open(&path).expect("should succeed");
263 let mut buf = [0u8; 4];
264 f.read_exact(&mut buf).expect("should succeed");
265 assert_eq!(buf, [0x67, 0x6C, 0x54, 0x46]);
267 }
268
269 #[test]
270 fn identity_transform_fields() {
271 let t = InstanceTransform::identity();
272 assert_eq!(t.rotation[3], 1.0);
273 }
274}