1use std::io::Write;
5use std::path::Path;
6
7use anyhow::Result;
8use bytemuck::cast_slice;
9use oxihuman_mesh::MeshBuffers;
10use serde_json::json;
11
12use crate::material::PbrMaterial;
13
14const GLB_MAGIC: u32 = 0x46546C67; const GLB_VERSION: u32 = 2;
17const CHUNK_JSON: u32 = 0x4E4F534A; const CHUNK_BIN: u32 = 0x004E4942; pub struct SceneMesh {
22 pub name: String,
24 pub mesh: MeshBuffers,
25 pub material: Option<PbrMaterial>,
26 pub translation: [f32; 3],
28}
29
30impl SceneMesh {
31 pub fn new(name: impl Into<String>, mesh: MeshBuffers) -> Self {
32 Self {
33 name: name.into(),
34 mesh,
35 material: None,
36 translation: [0.0, 0.0, 0.0],
37 }
38 }
39
40 pub fn with_material(mut self, m: PbrMaterial) -> Self {
41 self.material = Some(m);
42 self
43 }
44
45 pub fn with_translation(mut self, t: [f32; 3]) -> Self {
46 self.translation = t;
47 self
48 }
49}
50
51pub struct Scene {
53 pub name: String,
54 pub meshes: Vec<SceneMesh>,
55}
56
57impl Scene {
58 pub fn new(name: impl Into<String>) -> Self {
59 Self {
60 name: name.into(),
61 meshes: Vec::new(),
62 }
63 }
64
65 pub fn add_mesh(mut self, mesh: SceneMesh) -> Self {
66 self.meshes.push(mesh);
67 self
68 }
69
70 pub fn mesh_count(&self) -> usize {
71 self.meshes.len()
72 }
73
74 pub fn export(&self, path: &Path) -> Result<()> {
76 export_scene_glb(self, path)
77 }
78}
79
80struct MeshBinLayout {
82 pos_offset: usize,
83 norm_offset: usize,
84 uv_offset: usize,
85 idx_offset: usize,
86 n_verts: usize,
87 n_idx: usize,
88 pos_bytes_len: usize,
89 norm_bytes_len: usize,
90 uv_bytes_len: usize,
91 idx_bytes_len: usize,
92}
93
94pub fn export_scene_glb(scene: &Scene, path: &Path) -> Result<()> {
98 let mut bin_data: Vec<u8> = Vec::new();
101 let mut layouts: Vec<MeshBinLayout> = Vec::new();
102
103 for sm in &scene.meshes {
104 let mesh = &sm.mesh;
105 let pos_bytes: &[u8] = cast_slice(&mesh.positions);
106 let norm_bytes: &[u8] = cast_slice(&mesh.normals);
107 let uv_bytes: &[u8] = cast_slice(&mesh.uvs);
108 let idx_bytes: &[u8] = cast_slice(&mesh.indices);
109
110 let pos_offset = bin_data.len();
111 bin_data.extend_from_slice(pos_bytes);
112 let norm_offset = bin_data.len();
113 bin_data.extend_from_slice(norm_bytes);
114 let uv_offset = bin_data.len();
115 bin_data.extend_from_slice(uv_bytes);
116 let idx_offset = bin_data.len();
117 bin_data.extend_from_slice(idx_bytes);
118
119 layouts.push(MeshBinLayout {
120 pos_offset,
121 norm_offset,
122 uv_offset,
123 idx_offset,
124 n_verts: mesh.positions.len(),
125 n_idx: mesh.indices.len(),
126 pos_bytes_len: pos_bytes.len(),
127 norm_bytes_len: norm_bytes.len(),
128 uv_bytes_len: uv_bytes.len(),
129 idx_bytes_len: idx_bytes.len(),
130 });
131 }
132
133 while !bin_data.len().is_multiple_of(4) {
135 bin_data.push(0x00);
136 }
137
138 let mut materials_json: Vec<serde_json::Value> = Vec::new();
140 let mut mesh_material_idx: Vec<Option<usize>> = Vec::new();
142
143 for sm in &scene.meshes {
144 if let Some(ref mat) = sm.material {
145 let mat_idx = materials_json.len();
146 materials_json.push(mat.to_gltf_json());
147 mesh_material_idx.push(Some(mat_idx));
148 } else {
149 mesh_material_idx.push(None);
150 }
151 }
152
153 let mut accessors: Vec<serde_json::Value> = Vec::new();
155 let mut buffer_views: Vec<serde_json::Value> = Vec::new();
156 let mut meshes_json: Vec<serde_json::Value> = Vec::new();
157 let mut nodes_json: Vec<serde_json::Value> = Vec::new();
158
159 for (mesh_idx, (sm, layout)) in scene.meshes.iter().zip(layouts.iter()).enumerate() {
160 let pos_bv_idx = buffer_views.len();
164 buffer_views.push(json!({
165 "buffer": 0,
166 "byteOffset": layout.pos_offset,
167 "byteLength": layout.pos_bytes_len
168 }));
169
170 let norm_bv_idx = buffer_views.len();
171 buffer_views.push(json!({
172 "buffer": 0,
173 "byteOffset": layout.norm_offset,
174 "byteLength": layout.norm_bytes_len
175 }));
176
177 let uv_bv_idx = buffer_views.len();
178 buffer_views.push(json!({
179 "buffer": 0,
180 "byteOffset": layout.uv_offset,
181 "byteLength": layout.uv_bytes_len
182 }));
183
184 let idx_bv_idx = buffer_views.len();
185 buffer_views.push(json!({
186 "buffer": 0,
187 "byteOffset": layout.idx_offset,
188 "byteLength": layout.idx_bytes_len
189 }));
190
191 let pos_acc_idx = accessors.len();
192 accessors.push(json!({
193 "bufferView": pos_bv_idx,
194 "componentType": 5126,
195 "count": layout.n_verts,
196 "type": "VEC3"
197 }));
198
199 let norm_acc_idx = accessors.len();
200 accessors.push(json!({
201 "bufferView": norm_bv_idx,
202 "componentType": 5126,
203 "count": layout.n_verts,
204 "type": "VEC3"
205 }));
206
207 let uv_acc_idx = accessors.len();
208 accessors.push(json!({
209 "bufferView": uv_bv_idx,
210 "componentType": 5126,
211 "count": layout.n_verts,
212 "type": "VEC2"
213 }));
214
215 let idx_acc_idx = accessors.len();
216 accessors.push(json!({
217 "bufferView": idx_bv_idx,
218 "componentType": 5125,
219 "count": layout.n_idx,
220 "type": "SCALAR"
221 }));
222
223 let attributes = json!({
225 "POSITION": pos_acc_idx,
226 "NORMAL": norm_acc_idx,
227 "TEXCOORD_0": uv_acc_idx
228 });
229
230 let primitive = if let Some(mat_idx) = mesh_material_idx[mesh_idx] {
231 json!({
232 "attributes": attributes,
233 "indices": idx_acc_idx,
234 "material": mat_idx
235 })
236 } else {
237 json!({
238 "attributes": attributes,
239 "indices": idx_acc_idx
240 })
241 };
242
243 meshes_json.push(json!({
244 "name": sm.name,
245 "primitives": [primitive]
246 }));
247
248 let t = sm.translation;
250 nodes_json.push(json!({
251 "name": sm.name,
252 "mesh": mesh_idx,
253 "translation": [t[0], t[1], t[2]]
254 }));
255 }
256
257 let node_indices: Vec<usize> = (0..scene.meshes.len()).collect();
259
260 let total_bin = bin_data.len() as u32;
262
263 let gltf = if materials_json.is_empty() {
264 json!({
265 "asset": { "version": "2.0", "generator": "oxihuman-export" },
266 "scene": 0,
267 "scenes": [{ "name": scene.name, "nodes": node_indices }],
268 "nodes": nodes_json,
269 "meshes": meshes_json,
270 "accessors": accessors,
271 "bufferViews": buffer_views,
272 "buffers": [{ "byteLength": total_bin }]
273 })
274 } else {
275 json!({
276 "asset": { "version": "2.0", "generator": "oxihuman-export" },
277 "scene": 0,
278 "scenes": [{ "name": scene.name, "nodes": node_indices }],
279 "nodes": nodes_json,
280 "meshes": meshes_json,
281 "materials": materials_json,
282 "accessors": accessors,
283 "bufferViews": buffer_views,
284 "buffers": [{ "byteLength": total_bin }]
285 })
286 };
287
288 let mut json_bytes = serde_json::to_vec(&gltf)?;
289 while !json_bytes.len().is_multiple_of(4) {
291 json_bytes.push(b' ');
292 }
293
294 let json_chunk_len = json_bytes.len() as u32;
296 let bin_chunk_len = bin_data.len() as u32;
297 let total_len = 12 + 8 + json_chunk_len + 8 + bin_chunk_len;
298
299 let mut file = std::fs::File::create(path)?;
300
301 file.write_all(&GLB_MAGIC.to_le_bytes())?;
303 file.write_all(&GLB_VERSION.to_le_bytes())?;
304 file.write_all(&total_len.to_le_bytes())?;
305
306 file.write_all(&json_chunk_len.to_le_bytes())?;
308 file.write_all(&CHUNK_JSON.to_le_bytes())?;
309 file.write_all(&json_bytes)?;
310
311 file.write_all(&bin_chunk_len.to_le_bytes())?;
313 file.write_all(&CHUNK_BIN.to_le_bytes())?;
314 file.write_all(&bin_data)?;
315
316 Ok(())
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322 use oxihuman_mesh::MeshBuffers;
323 use oxihuman_morph::engine::MeshBuffers as MB;
324
325 fn tri_mesh(y_offset: f32) -> MeshBuffers {
326 MeshBuffers::from_morph(MB {
327 positions: vec![
328 [0.0, y_offset, 0.0],
329 [1.0, y_offset, 0.0],
330 [0.0, y_offset + 1.0, 0.0],
331 ],
332 normals: vec![[0.0, 0.0, 1.0]; 3],
333 uvs: vec![[0.0, 0.0]; 3],
334 indices: vec![0, 1, 2],
335 has_suit: true,
336 })
337 }
338
339 #[test]
340 fn scene_empty_export() {
341 let path = std::path::PathBuf::from("/tmp/test_scene_empty.glb");
342 let scene = Scene::new("test");
343 assert_eq!(scene.mesh_count(), 0);
344 scene.export(&path).expect("export failed");
345 assert!(path.exists(), "GLB file should be created");
346 let bytes = std::fs::read(&path).expect("should succeed");
347 assert!(bytes.len() >= 12, "GLB must have at least 12 bytes");
348 std::fs::remove_file(&path).ok();
349 }
350
351 #[test]
352 fn scene_single_mesh_export() {
353 let path = std::path::PathBuf::from("/tmp/test_scene_single.glb");
354 let scene = Scene::new("single").add_mesh(SceneMesh::new("body", tri_mesh(0.0)));
355 export_scene_glb(&scene, &path).expect("export failed");
356 assert!(path.exists(), "file should exist");
357 let bytes = std::fs::read(&path).expect("should succeed");
358 assert!(bytes.len() >= 12, "valid GLB header required");
359 std::fs::remove_file(&path).ok();
360 }
361
362 #[test]
363 fn scene_two_meshes_export() {
364 let path = std::path::PathBuf::from("/tmp/test_scene_two.glb");
365 let scene = Scene::new("two_meshes")
366 .add_mesh(SceneMesh::new("body", tri_mesh(0.0)))
367 .add_mesh(SceneMesh::new("clothing", tri_mesh(1.0)));
368 export_scene_glb(&scene, &path).expect("export failed");
369 assert!(path.exists(), "file should exist");
370 std::fs::remove_file(&path).ok();
371 }
372
373 #[test]
374 fn scene_glb_header_valid() {
375 let path = std::path::PathBuf::from("/tmp/test_scene_header.glb");
376 let scene = Scene::new("header_test").add_mesh(SceneMesh::new("mesh0", tri_mesh(0.0)));
377 export_scene_glb(&scene, &path).expect("export failed");
378 let bytes = std::fs::read(&path).expect("should succeed");
379 assert!(bytes.len() >= 4);
380 assert_eq!(
382 &bytes[0..4],
383 &[0x67u8, 0x6Cu8, 0x54u8, 0x46u8],
384 "GLB magic must be glTF"
385 );
386 std::fs::remove_file(&path).ok();
387 }
388
389 #[test]
390 fn scene_with_material() {
391 let path = std::path::PathBuf::from("/tmp/test_scene_material.glb");
392 let scene = Scene::new("with_material")
393 .add_mesh(SceneMesh::new("body", tri_mesh(0.0)).with_material(PbrMaterial::skin()));
394 export_scene_glb(&scene, &path).expect("export failed");
395 assert!(path.exists(), "file should exist");
396 std::fs::remove_file(&path).ok();
397 }
398
399 #[test]
400 fn scene_mesh_count() {
401 let scene = Scene::new("x")
402 .add_mesh(SceneMesh::new("a", tri_mesh(0.0)))
403 .add_mesh(SceneMesh::new("b", tri_mesh(1.0)));
404 assert_eq!(scene.mesh_count(), 2);
405 }
406}