1use crate::{
2 error::{ExportError, ExportResult},
3 export_context::ExportContext,
4};
5
6use gltforge::schema::{
7 Accessor, AccessorComponentType, AccessorType, Asset, Buffer, BufferView, BufferViewTarget,
8 Gltf, GltfId, Mesh, MeshPrimitive, MeshPrimitiveMode, Node, Scene,
9};
10
11use bytemuck::cast_slice;
12use error_location::ErrorLocation;
13use std::{collections::HashMap, panic::Location, path::Path};
14
15pub(crate) fn write(ctx: ExportContext, gltf_path: &Path) -> ExportResult<()> {
16 let stem = gltf_path
17 .file_stem()
18 .and_then(|s| s.to_str())
19 .unwrap_or("output");
20 let bin_name = format!("{stem}.bin");
21 let bin_path = gltf_path.with_file_name(&bin_name);
22
23 let (gltf, binary) = build_gltf(&ctx, Some(&bin_name));
24
25 std::fs::write(&bin_path, &binary).map_err(|e| ExportError::Io {
26 path: bin_path.to_string_lossy().into_owned(),
27 source: e,
28 location: ErrorLocation::from(Location::caller()),
29 })?;
30
31 let json = serde_json::to_string_pretty(&gltf).map_err(|e| ExportError::Json {
32 source: e,
33 location: ErrorLocation::from(Location::caller()),
34 })?;
35
36 std::fs::write(gltf_path, json).map_err(|e| ExportError::Io {
37 path: gltf_path.to_string_lossy().into_owned(),
38 source: e,
39 location: ErrorLocation::from(Location::caller()),
40 })?;
41
42 Ok(())
43}
44
45pub(crate) fn write_glb(ctx: ExportContext, glb_path: &Path) -> ExportResult<()> {
46 let (gltf, binary) = build_gltf(&ctx, None);
47
48 let json = serde_json::to_string(&gltf).map_err(|e| ExportError::Json {
49 source: e,
50 location: ErrorLocation::from(Location::caller()),
51 })?;
52 let json_bytes = json.as_bytes();
53 let json_padded_len = (json_bytes.len() + 3) & !3;
54 let json_padding = json_padded_len - json_bytes.len();
55
56 let has_bin = !binary.is_empty();
57 let bin_padded_len = if has_bin { (binary.len() + 3) & !3 } else { 0 };
58 let bin_padding = bin_padded_len - binary.len();
59
60 let total_len: u32 = 12 + 8 + json_padded_len as u32 + if has_bin { 8 + bin_padded_len as u32 } else { 0 }; let mut out: Vec<u8> = Vec::with_capacity(total_len as usize);
65
66 out.extend_from_slice(&0x46546C67u32.to_le_bytes()); out.extend_from_slice(&2u32.to_le_bytes()); out.extend_from_slice(&total_len.to_le_bytes()); out.extend_from_slice(&(json_padded_len as u32).to_le_bytes());
73 out.extend_from_slice(&0x4E4F534Au32.to_le_bytes()); out.extend_from_slice(json_bytes);
75 out.extend(std::iter::repeat_n(0x20u8, json_padding)); if has_bin {
79 out.extend_from_slice(&(bin_padded_len as u32).to_le_bytes());
80 out.extend_from_slice(&0x004E4942u32.to_le_bytes()); out.extend_from_slice(&binary);
82 out.extend(std::iter::repeat_n(0x00u8, bin_padding));
83 }
84
85 std::fs::write(glb_path, &out).map_err(|e| ExportError::Io {
86 path: glb_path.to_string_lossy().into_owned(),
87 source: e,
88 location: ErrorLocation::from(Location::caller()),
89 })
90}
91
92fn build_gltf(ctx: &ExportContext, bin_uri: Option<&str>) -> (Gltf, Vec<u8>) {
93 let mut binary: Vec<u8> = Vec::new();
94 let mut buffer_views: Vec<BufferView> = Vec::new();
95 let mut accessors: Vec<Accessor> = Vec::new();
96
97 struct MeshAccessors {
98 position: GltfId,
99 normal: Option<GltfId>,
100 uvs: Vec<GltfId>,
101 submesh_indices: Vec<GltfId>,
102 }
103
104 let mut mesh_accessor_map: Vec<MeshAccessors> = Vec::new();
105
106 for mesh_data in &ctx.meshes {
107 let use_u16 = mesh_data.positions.len() <= 65535;
108
109 let gltf_positions = to_gltf_positions(&mesh_data.positions);
110 let position = push_vec3(
111 &mut binary,
112 &mut buffer_views,
113 &mut accessors,
114 &gltf_positions,
115 BufferViewTarget::ArrayBuffer,
116 true,
117 );
118
119 let normal = if !mesh_data.normals.is_empty() {
120 let gltf_normals = to_gltf_normals(&mesh_data.normals);
121 Some(push_vec3(
122 &mut binary,
123 &mut buffer_views,
124 &mut accessors,
125 &gltf_normals,
126 BufferViewTarget::ArrayBuffer,
127 false,
128 ))
129 } else {
130 None
131 };
132
133 let uvs: Vec<GltfId> = mesh_data
134 .uvs
135 .iter()
136 .map(|ch| {
137 let gltf_uvs = to_gltf_uvs(ch);
138 push_vec2(
139 &mut binary,
140 &mut buffer_views,
141 &mut accessors,
142 &gltf_uvs,
143 BufferViewTarget::ArrayBuffer,
144 )
145 })
146 .collect();
147
148 let submesh_indices: Vec<GltfId> = mesh_data
149 .submeshes
150 .iter()
151 .map(|sm| {
152 let gltf_indices = reverse_winding(&sm.indices);
153 if use_u16 {
154 let indices_u16: Vec<u16> = gltf_indices.iter().map(|&i| i as u16).collect();
155 push_indices_u16(&mut binary, &mut buffer_views, &mut accessors, &indices_u16)
156 } else {
157 push_indices_u32(
158 &mut binary,
159 &mut buffer_views,
160 &mut accessors,
161 &gltf_indices,
162 )
163 }
164 })
165 .collect();
166
167 mesh_accessor_map.push(MeshAccessors {
168 position,
169 normal,
170 uvs,
171 submesh_indices,
172 });
173 }
174
175 let gltf_meshes: Vec<Mesh> = ctx
176 .meshes
177 .iter()
178 .zip(&mesh_accessor_map)
179 .map(|(mesh_data, accs)| {
180 let primitives = mesh_data
181 .submeshes
182 .iter()
183 .enumerate()
184 .map(|(i, _)| {
185 let mut attributes = HashMap::new();
186 attributes.insert("POSITION".to_string(), accs.position);
187 if let Some(n) = accs.normal {
188 attributes.insert("NORMAL".to_string(), n);
189 }
190 for (ch, &uv_acc) in accs.uvs.iter().enumerate() {
191 attributes.insert(format!("TEXCOORD_{ch}"), uv_acc);
192 }
193 MeshPrimitive {
194 attributes,
195 indices: Some(accs.submesh_indices[i]),
196 material: None,
197 mode: MeshPrimitiveMode::default(),
198 targets: None,
199 extensions: None,
200 extras: None,
201 }
202 })
203 .collect();
204
205 Mesh {
206 primitives,
207 weights: None,
208 name: mesh_data.name.clone(),
209 extensions: None,
210 extras: None,
211 }
212 })
213 .collect();
214
215 let gltf_nodes: Vec<Node> = ctx
216 .nodes
217 .iter()
218 .enumerate()
219 .map(|(idx, n)| {
220 let children: Vec<u32> = ctx
221 .nodes
222 .iter()
223 .enumerate()
224 .filter(|(_, cn)| cn.parent == Some(idx as u32))
225 .map(|(ci, _)| ci as u32)
226 .collect();
227
228 let translation = n.translation.and_then(|[x, y, z]| {
231 let t = [canon(-x), canon(y), canon(z)];
232 if t == [0.0, 0.0, 0.0] { None } else { Some(t) }
233 });
234 let rotation = n.rotation.and_then(|[x, y, z, w]| {
235 let r = canon_quat([canon(-x), canon(y), canon(z), canon(-w)]);
236 if r == [0.0, 0.0, 0.0, 1.0] {
237 None
238 } else {
239 Some(r)
240 }
241 });
242
243 Node {
244 children: if children.is_empty() {
245 None
246 } else {
247 Some(children)
248 },
249 mesh: n.mesh_index,
250 skin: None,
251 camera: None,
252 matrix: None,
253 translation,
254 rotation,
255 scale: n.scale.filter(|&s| s != [1.0, 1.0, 1.0]),
256 weights: None,
257 name: n.name.clone(),
258 extensions: None,
259 extras: None,
260 }
261 })
262 .collect();
263
264 let root_nodes: Vec<u32> = ctx
265 .nodes
266 .iter()
267 .enumerate()
268 .filter(|(_, n)| n.parent.is_none())
269 .map(|(i, _)| i as u32)
270 .collect();
271
272 let bin_len = binary.len() as u32;
273
274 let gltf = Gltf {
275 asset: Asset {
276 version: "2.0".to_string(),
277 generator: Some("gltforge".to_string()),
278 copyright: None,
279 min_version: None,
280 extensions: None,
281 extras: None,
282 },
283 scene: Some(0),
284 scenes: Some(vec![Scene {
285 nodes: if root_nodes.is_empty() {
286 None
287 } else {
288 Some(root_nodes)
289 },
290 name: None,
291 extensions: None,
292 extras: None,
293 }]),
294 nodes: if gltf_nodes.is_empty() {
295 None
296 } else {
297 Some(gltf_nodes)
298 },
299 meshes: if gltf_meshes.is_empty() {
300 None
301 } else {
302 Some(gltf_meshes)
303 },
304 accessors: if accessors.is_empty() {
305 None
306 } else {
307 Some(accessors)
308 },
309 buffer_views: if buffer_views.is_empty() {
310 None
311 } else {
312 Some(buffer_views)
313 },
314 buffers: if binary.is_empty() {
315 None
316 } else {
317 Some(vec![Buffer {
318 byte_length: bin_len,
319 uri: bin_uri.map(|s| s.to_string()),
320 name: None,
321 extensions: None,
322 extras: None,
323 }])
324 },
325 animations: None,
326 cameras: None,
327 images: None,
328 materials: None,
329 samplers: None,
330 skins: None,
331 textures: None,
332 extensions_used: None,
333 extensions_required: None,
334 extensions: None,
335 extras: None,
336 };
337
338 (gltf, binary)
339}
340
341fn canon(x: f32) -> f32 {
345 if x == 0.0 { 0.0 } else { x }
346}
347
348fn canon_quat([x, y, z, w]: [f32; 4]) -> [f32; 4] {
350 if w < 0.0 {
351 [-x, -y, -z, -w]
352 } else {
353 [x, y, z, w]
354 }
355}
356
357fn to_gltf_positions(positions: &[[f32; 3]]) -> Vec<[f32; 3]> {
358 positions.iter().map(|&[x, y, z]| [-x, y, z]).collect()
359}
360
361fn to_gltf_normals(normals: &[[f32; 3]]) -> Vec<[f32; 3]> {
362 normals.iter().map(|&[x, y, z]| [-x, y, z]).collect()
363}
364
365fn to_gltf_uvs(uvs: &[[f32; 2]]) -> Vec<[f32; 2]> {
366 uvs.iter().map(|&[u, v]| [u, 1.0 - v]).collect()
367}
368
369fn reverse_winding(indices: &[u32]) -> Vec<u32> {
370 let mut out = Vec::with_capacity(indices.len());
371 for tri in indices.chunks_exact(3) {
372 out.push(tri[0]);
373 out.push(tri[2]);
374 out.push(tri[1]);
375 }
376 out
377}
378
379fn align_to(buf: &mut Vec<u8>, alignment: usize) {
382 let rem = buf.len() % alignment;
383 if rem != 0 {
384 buf.extend(std::iter::repeat_n(0u8, alignment - rem));
385 }
386}
387
388fn push_vec3(
389 binary: &mut Vec<u8>,
390 buffer_views: &mut Vec<BufferView>,
391 accessors: &mut Vec<Accessor>,
392 data: &[[f32; 3]],
393 target: BufferViewTarget,
394 compute_min_max: bool,
395) -> GltfId {
396 align_to(binary, 4);
397 let byte_offset = binary.len() as u32;
398 let bytes: &[u8] = cast_slice(data);
399 binary.extend_from_slice(bytes);
400
401 let bv_idx = buffer_views.len() as u32;
402 buffer_views.push(BufferView {
403 buffer: 0,
404 byte_offset,
405 byte_length: bytes.len() as u32,
406 byte_stride: None,
407 target: Some(target),
408 name: None,
409 extensions: None,
410 extras: None,
411 });
412
413 let (min, max) = if compute_min_max && !data.is_empty() {
414 let mut mn = data[0];
415 let mut mx = data[0];
416 for &[x, y, z] in data.iter().skip(1) {
417 mn[0] = mn[0].min(x);
418 mn[1] = mn[1].min(y);
419 mn[2] = mn[2].min(z);
420 mx[0] = mx[0].max(x);
421 mx[1] = mx[1].max(y);
422 mx[2] = mx[2].max(z);
423 }
424 (
425 Some(vec![mn[0] as f64, mn[1] as f64, mn[2] as f64]),
426 Some(vec![mx[0] as f64, mx[1] as f64, mx[2] as f64]),
427 )
428 } else {
429 (None, None)
430 };
431
432 let acc_idx = accessors.len() as u32;
433 accessors.push(Accessor {
434 buffer_view: Some(bv_idx),
435 byte_offset: None,
436 component_type: AccessorComponentType::Float,
437 count: data.len() as u32,
438 accessor_type: AccessorType::Vec3,
439 normalized: None,
440 min,
441 max,
442 sparse: None,
443 name: None,
444 extensions: None,
445 extras: None,
446 });
447 acc_idx
448}
449
450fn push_vec2(
451 binary: &mut Vec<u8>,
452 buffer_views: &mut Vec<BufferView>,
453 accessors: &mut Vec<Accessor>,
454 data: &[[f32; 2]],
455 target: BufferViewTarget,
456) -> GltfId {
457 align_to(binary, 4);
458 let byte_offset = binary.len() as u32;
459 let bytes: &[u8] = cast_slice(data);
460 binary.extend_from_slice(bytes);
461
462 let bv_idx = buffer_views.len() as u32;
463 buffer_views.push(BufferView {
464 buffer: 0,
465 byte_offset,
466 byte_length: bytes.len() as u32,
467 byte_stride: None,
468 target: Some(target),
469 name: None,
470 extensions: None,
471 extras: None,
472 });
473
474 let acc_idx = accessors.len() as u32;
475 accessors.push(Accessor {
476 buffer_view: Some(bv_idx),
477 byte_offset: None,
478 component_type: AccessorComponentType::Float,
479 count: data.len() as u32,
480 accessor_type: AccessorType::Vec2,
481 normalized: None,
482 min: None,
483 max: None,
484 sparse: None,
485 name: None,
486 extensions: None,
487 extras: None,
488 });
489 acc_idx
490}
491
492fn push_indices_u16(
493 binary: &mut Vec<u8>,
494 buffer_views: &mut Vec<BufferView>,
495 accessors: &mut Vec<Accessor>,
496 indices: &[u16],
497) -> GltfId {
498 align_to(binary, 2);
499 let byte_offset = binary.len() as u32;
500 let bytes: &[u8] = cast_slice(indices);
501 binary.extend_from_slice(bytes);
502
503 let bv_idx = buffer_views.len() as u32;
504 buffer_views.push(BufferView {
505 buffer: 0,
506 byte_offset,
507 byte_length: bytes.len() as u32,
508 byte_stride: None,
509 target: Some(BufferViewTarget::ElementArrayBuffer),
510 name: None,
511 extensions: None,
512 extras: None,
513 });
514
515 let acc_idx = accessors.len() as u32;
516 accessors.push(Accessor {
517 buffer_view: Some(bv_idx),
518 byte_offset: None,
519 component_type: AccessorComponentType::UnsignedShort,
520 count: indices.len() as u32,
521 accessor_type: AccessorType::Scalar,
522 normalized: None,
523 min: None,
524 max: None,
525 sparse: None,
526 name: None,
527 extensions: None,
528 extras: None,
529 });
530 acc_idx
531}
532
533fn push_indices_u32(
534 binary: &mut Vec<u8>,
535 buffer_views: &mut Vec<BufferView>,
536 accessors: &mut Vec<Accessor>,
537 indices: &[u32],
538) -> GltfId {
539 align_to(binary, 4);
540 let byte_offset = binary.len() as u32;
541 let bytes: &[u8] = cast_slice(indices);
542 binary.extend_from_slice(bytes);
543
544 let bv_idx = buffer_views.len() as u32;
545 buffer_views.push(BufferView {
546 buffer: 0,
547 byte_offset,
548 byte_length: bytes.len() as u32,
549 byte_stride: None,
550 target: Some(BufferViewTarget::ElementArrayBuffer),
551 name: None,
552 extensions: None,
553 extras: None,
554 });
555
556 let acc_idx = accessors.len() as u32;
557 accessors.push(Accessor {
558 buffer_view: Some(bv_idx),
559 byte_offset: None,
560 component_type: AccessorComponentType::UnsignedInt,
561 count: indices.len() as u32,
562 accessor_type: AccessorType::Scalar,
563 normalized: None,
564 min: None,
565 max: None,
566 sparse: None,
567 name: None,
568 extensions: None,
569 extras: None,
570 });
571 acc_idx
572}