Skip to main content

gltforge_unity_export/
build.rs

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 // GLB header
61        + 8 + json_padded_len as u32 // JSON chunk
62        + if has_bin { 8 + bin_padded_len as u32 } else { 0 }; // BIN chunk
63
64    let mut out: Vec<u8> = Vec::with_capacity(total_len as usize);
65
66    // GLB header
67    out.extend_from_slice(&0x46546C67u32.to_le_bytes()); // magic "glTF"
68    out.extend_from_slice(&2u32.to_le_bytes()); // version
69    out.extend_from_slice(&total_len.to_le_bytes()); // total file length
70
71    // JSON chunk
72    out.extend_from_slice(&(json_padded_len as u32).to_le_bytes());
73    out.extend_from_slice(&0x4E4F534Au32.to_le_bytes()); // "JSON"
74    out.extend_from_slice(json_bytes);
75    out.extend(std::iter::repeat_n(0x20u8, json_padding)); // pad with spaces
76
77    // BIN chunk
78    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()); // "BIN\0"
81        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            // Unity left-handed → glTF right-handed: negate X on position, negate X+W on rotation.
229            // canon() eliminates negative zero. Omit identity components entirely.
230            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
341// --- Coordinate conversions --------------------------------------------------
342
343/// Canonicalize negative zero to positive zero.
344fn canon(x: f32) -> f32 {
345    if x == 0.0 { 0.0 } else { x }
346}
347
348/// Canonicalize a quaternion to have non-negative W (q and -q represent the same rotation).
349fn 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
379// --- Buffer packing helpers --------------------------------------------------
380
381fn 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}