Skip to main content

gltforge_unity/
convert.rs

1use std::panic::Location;
2
3use error_location::ErrorLocation;
4use gltforge::parser::resolve_accessor;
5use gltforge::schema::{AccessorComponentType, AccessorType, Gltf, MeshPrimitiveMode};
6
7use crate::error::{ConvertError, ConvertResult};
8use crate::mesh::{UnityIndices, UnityMesh, UnitySubmesh};
9
10/// Build a [`UnityMesh`] from a glTF node, loading all of its mesh's primitives as submeshes.
11///
12/// Applies the right-handed → left-handed coordinate conversion (negate X) and reverses
13/// triangle winding order. Vertices are concatenated across primitives into a single shared
14/// buffer; indices in each submesh are absolute into that buffer. Index format (u16/u32) is
15/// selected once based on the total vertex count across all primitives.
16#[track_caller]
17pub fn build_unity_mesh(
18    gltf: &Gltf,
19    buffers: &[Vec<u8>],
20    node_idx: u32,
21) -> ConvertResult<UnityMesh> {
22    // ---- node ---------------------------------------------------------------
23
24    let nodes = gltf.nodes.as_deref().ok_or_else(|| ConvertError::NoNodes {
25        location: ErrorLocation::from(Location::caller()),
26    })?;
27
28    let node = nodes
29        .get(node_idx as usize)
30        .ok_or_else(|| ConvertError::NodeIndexOutOfRange {
31            index: node_idx as usize,
32            location: ErrorLocation::from(Location::caller()),
33        })?;
34
35    let mesh_idx = node.mesh.ok_or_else(|| ConvertError::NodeHasNoMesh {
36        index: node_idx as usize,
37        location: ErrorLocation::from(Location::caller()),
38    })?;
39
40    // ---- mesh ---------------------------------------------------------------
41
42    let meshes = gltf
43        .meshes
44        .as_deref()
45        .ok_or_else(|| ConvertError::NoMeshes {
46            location: ErrorLocation::from(Location::caller()),
47        })?;
48
49    let mesh = meshes
50        .get(mesh_idx as usize)
51        .ok_or_else(|| ConvertError::MeshIndexOutOfRange {
52            index: mesh_idx as usize,
53            location: ErrorLocation::from(Location::caller()),
54        })?;
55
56    let bvs = gltf.buffer_views.as_deref().unwrap_or(&[]);
57    let accessors = gltf.accessors.as_deref().unwrap_or(&[]);
58
59    // ---- primitives ---------------------------------------------------------
60
61    let mut all_positions: Vec<[f32; 3]> = Vec::new();
62    let mut raw_submeshes: Vec<Vec<u32>> = Vec::new();
63
64    for prim in &mesh.primitives {
65        if prim.mode != MeshPrimitiveMode::Triangles {
66            return Err(ConvertError::UnsupportedPrimitiveMode {
67                mode: prim.mode,
68                location: ErrorLocation::from(Location::caller()),
69            });
70        }
71
72        let vertex_offset = all_positions.len() as u32;
73
74        // positions
75        let pos_id =
76            *prim
77                .attributes
78                .get("POSITION")
79                .ok_or_else(|| ConvertError::NoPositionAttribute {
80                    location: ErrorLocation::from(Location::caller()),
81                })? as usize;
82
83        let pos_acc =
84            accessors
85                .get(pos_id)
86                .ok_or_else(|| ConvertError::PositionAccessorOutOfRange {
87                    location: ErrorLocation::from(Location::caller()),
88                })?;
89
90        if pos_acc.accessor_type != AccessorType::Vec3
91            || pos_acc.component_type != AccessorComponentType::Float
92        {
93            return Err(ConvertError::InvalidPositionType {
94                location: ErrorLocation::from(Location::caller()),
95            });
96        }
97
98        let pos_bytes =
99            resolve_accessor(pos_acc, bvs, buffers).map_err(|e| ConvertError::Resolve {
100                source: e,
101                location: ErrorLocation::from(Location::caller()),
102            })?;
103
104        all_positions.extend(pos_bytes.chunks_exact(12).map(|c| {
105            let x = f32::from_le_bytes([c[0], c[1], c[2], c[3]]);
106            let y = f32::from_le_bytes([c[4], c[5], c[6], c[7]]);
107            let z = f32::from_le_bytes([c[8], c[9], c[10], c[11]]);
108            [-x, y, z]
109        }));
110
111        // indices
112        let idx_id = prim.indices.ok_or_else(|| ConvertError::NoIndices {
113            location: ErrorLocation::from(Location::caller()),
114        })?;
115
116        let idx_acc = accessors.get(idx_id as usize).ok_or_else(|| {
117            ConvertError::IndexAccessorOutOfRange {
118                location: ErrorLocation::from(Location::caller()),
119            }
120        })?;
121
122        let idx_bytes =
123            resolve_accessor(idx_acc, bvs, buffers).map_err(|e| ConvertError::Resolve {
124                source: e,
125                location: ErrorLocation::from(Location::caller()),
126            })?;
127
128        let raw = decode_indices(idx_bytes, idx_acc.component_type)?;
129
130        // Reverse winding and offset into the shared vertex buffer.
131        let wound: Vec<u32> = raw
132            .chunks_exact(3)
133            .flat_map(|tri| {
134                [
135                    tri[0] + vertex_offset,
136                    tri[2] + vertex_offset,
137                    tri[1] + vertex_offset,
138                ]
139            })
140            .collect();
141
142        raw_submeshes.push(wound);
143    }
144
145    // ---- index format -------------------------------------------------------
146
147    let total_vertices = all_positions.len();
148    let use_u16 = total_vertices <= 65535;
149
150    let submeshes: Vec<UnitySubmesh> = raw_submeshes
151        .into_iter()
152        .map(|wound| UnitySubmesh {
153            indices: if use_u16 {
154                UnityIndices::U16(wound.into_iter().map(|i| i as u16).collect())
155            } else {
156                UnityIndices::U32(wound)
157            },
158        })
159        .collect();
160
161    // ---- name ---------------------------------------------------------------
162
163    let name = derive_mesh_name(
164        node.name.as_deref(),
165        mesh.name.as_deref(),
166        node_idx,
167        mesh_idx,
168        mesh.primitives.len(),
169    );
170
171    Ok(UnityMesh {
172        name,
173        positions: all_positions,
174        submeshes,
175    })
176}
177
178fn derive_mesh_name(
179    node_name: Option<&str>,
180    mesh_name: Option<&str>,
181    node_idx: u32,
182    mesh_idx: u32,
183    prim_count: usize,
184) -> String {
185    match (node_name, mesh_name) {
186        (Some(n), Some(m)) => format!("{n}_{m}"),
187        (Some(n), None) if prim_count == 1 => n.to_string(),
188        (Some(n), None) => format!("{n}_{mesh_idx}"),
189        (None, _) => format!("{node_idx}_{mesh_idx}"),
190    }
191}
192
193/// Decode raw index bytes into a flat `Vec<u32>` regardless of source format.
194#[track_caller]
195fn decode_indices(bytes: &[u8], component_type: AccessorComponentType) -> ConvertResult<Vec<u32>> {
196    match component_type {
197        AccessorComponentType::UnsignedByte => Ok(bytes.iter().map(|&b| b as u32).collect()),
198        AccessorComponentType::UnsignedShort => Ok(bytes
199            .chunks_exact(2)
200            .map(|c| u16::from_le_bytes([c[0], c[1]]) as u32)
201            .collect()),
202        AccessorComponentType::UnsignedInt => Ok(bytes
203            .chunks_exact(4)
204            .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
205            .collect()),
206        other => Err(ConvertError::UnsupportedIndexComponentType {
207            component_type: other,
208            location: ErrorLocation::from(Location::caller()),
209        }),
210    }
211}