Skip to main content

rustial_engine/models/
loader.rs

1#[cfg(any(feature = "gltf", feature = "obj"))]
2use super::ModelMesh;
3use thiserror::Error;
4
5/// Errors from 3D model loading.
6#[derive(Debug, Clone, Error)]
7pub enum ModelLoadError {
8    /// The file format could not be parsed.
9    #[error("parse error: {0}")]
10    Parse(String),
11    /// The file contains no usable mesh geometry.
12    #[error("no mesh found: {0}")]
13    NoMesh(String),
14}
15
16// ---------------------------------------------------------------------------
17// GLTF loader
18// ---------------------------------------------------------------------------
19
20#[cfg(feature = "gltf")]
21impl ModelMesh {
22    /// Load the first mesh from a glTF binary (`.glb`) or JSON (`.gltf`)
23    /// buffer.
24    ///
25    /// Extracts positions, normals, texture coordinates, and triangle
26    /// indices from the first primitive of the first mesh in the file.
27    /// Missing normals default to `[0, 0, 1]`; missing UVs default to
28    /// `[0, 0]`.
29    ///
30    /// # Errors
31    ///
32    /// Returns [`ModelLoadError::Parse`] if the glTF data is malformed,
33    /// or [`ModelLoadError::NoMesh`] if the file contains no mesh
34    /// primitives.
35    pub fn from_gltf(bytes: &[u8]) -> Result<Self, ModelLoadError> {
36        let (document, buffers, _images) =
37            gltf::import_slice(bytes).map_err(|e| ModelLoadError::Parse(e.to_string()))?;
38
39        let mesh = document
40            .meshes()
41            .next()
42            .ok_or_else(|| ModelLoadError::NoMesh("glTF contains no meshes".into()))?;
43
44        let primitive = mesh
45            .primitives()
46            .next()
47            .ok_or_else(|| ModelLoadError::NoMesh("mesh contains no primitives".into()))?;
48
49        let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()]));
50
51        let positions: Vec<[f32; 3]> = reader
52            .read_positions()
53            .ok_or_else(|| ModelLoadError::NoMesh("primitive has no POSITION attribute".into()))?
54            .collect();
55
56        let normals: Vec<[f32; 3]> = reader
57            .read_normals()
58            .map(|iter| iter.collect())
59            .unwrap_or_else(|| vec![[0.0, 0.0, 1.0]; positions.len()]);
60
61        let uvs: Vec<[f32; 2]> = reader
62            .read_tex_coords(0)
63            .map(|tc| tc.into_f32().collect())
64            .unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);
65
66        let indices: Vec<u32> = reader
67            .read_indices()
68            .map(|idx| idx.into_u32().collect())
69            .unwrap_or_else(|| (0..positions.len() as u32).collect());
70
71        Ok(ModelMesh {
72            positions,
73            normals,
74            uvs,
75            indices,
76        })
77    }
78}
79
80// ---------------------------------------------------------------------------
81// OBJ loader
82// ---------------------------------------------------------------------------
83
84#[cfg(feature = "obj")]
85impl ModelMesh {
86    /// Load the first mesh from a Wavefront OBJ buffer.
87    ///
88    /// Parses the OBJ data (no MTL material support). Extracts
89    /// positions, normals, and texture coordinates from the first
90    /// model. Missing normals default to `[0, 0, 1]`; missing UVs
91    /// default to `[0, 0]`.
92    ///
93    /// # Errors
94    ///
95    /// Returns [`ModelLoadError::Parse`] if the OBJ data is malformed,
96    /// or [`ModelLoadError::NoMesh`] if the file contains no models.
97    pub fn from_obj(bytes: &[u8]) -> Result<Self, ModelLoadError> {
98        let mut cursor = std::io::Cursor::new(bytes);
99        let (models, _materials) = tobj::load_obj_buf(
100            &mut cursor,
101            &tobj::LoadOptions {
102                triangulate: true,
103                single_index: true,
104                ..Default::default()
105            },
106            |_path| Err(tobj::LoadError::OpenFileFailed),
107        )
108        .map_err(|e| ModelLoadError::Parse(e.to_string()))?;
109
110        let model = models
111            .first()
112            .ok_or_else(|| ModelLoadError::NoMesh("OBJ contains no models".into()))?;
113
114        let mesh = &model.mesh;
115        let vertex_count = mesh.positions.len() / 3;
116
117        let positions: Vec<[f32; 3]> = mesh
118            .positions
119            .chunks_exact(3)
120            .map(|c| [c[0], c[1], c[2]])
121            .collect();
122
123        let normals: Vec<[f32; 3]> = if mesh.normals.len() == mesh.positions.len() {
124            mesh.normals
125                .chunks_exact(3)
126                .map(|c| [c[0], c[1], c[2]])
127                .collect()
128        } else {
129            vec![[0.0, 0.0, 1.0]; vertex_count]
130        };
131
132        let uvs: Vec<[f32; 2]> = if mesh.texcoords.len() >= vertex_count * 2 {
133            mesh.texcoords
134                .chunks_exact(2)
135                .map(|c| [c[0], c[1]])
136                .collect()
137        } else {
138            vec![[0.0, 0.0]; vertex_count]
139        };
140
141        let indices: Vec<u32> = mesh.indices.clone();
142
143        Ok(ModelMesh {
144            positions,
145            normals,
146            uvs,
147            indices,
148        })
149    }
150}
151
152// ---------------------------------------------------------------------------
153// Tests
154// ---------------------------------------------------------------------------
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn model_load_error_display() {
162        let e = ModelLoadError::Parse("bad data".into());
163        assert!(e.to_string().contains("bad data"));
164
165        let e = ModelLoadError::NoMesh("empty".into());
166        assert!(e.to_string().contains("empty"));
167    }
168
169    #[cfg(feature = "gltf")]
170    #[test]
171    fn from_gltf_invalid_bytes() {
172        let result = ModelMesh::from_gltf(b"not a gltf file");
173        assert!(result.is_err());
174    }
175
176    #[cfg(feature = "obj")]
177    #[test]
178    fn from_obj_invalid_is_error() {
179        // Binary garbage should fail to parse.
180        let result = ModelMesh::from_obj(&[0xFF, 0xFE, 0x00, 0x01]);
181        // tobj may parse garbage as empty or error -- either is acceptable
182        // as long as we don't panic.
183        let _ = result;
184    }
185
186    #[cfg(feature = "obj")]
187    #[test]
188    fn from_obj_triangle() {
189        let obj = b"\
190v 0.0 0.0 0.0
191v 1.0 0.0 0.0
192v 0.0 1.0 0.0
193f 1 2 3
194";
195        let mesh = ModelMesh::from_obj(obj).expect("parse triangle");
196        assert_eq!(mesh.positions.len(), 3);
197        assert_eq!(mesh.indices.len(), 3);
198        assert_eq!(mesh.normals.len(), 3);
199        assert_eq!(mesh.uvs.len(), 3);
200    }
201}