rustial_engine/models/
loader.rs1#[cfg(any(feature = "gltf", feature = "obj"))]
2use super::ModelMesh;
3use thiserror::Error;
4
5#[derive(Debug, Clone, Error)]
7pub enum ModelLoadError {
8 #[error("parse error: {0}")]
10 Parse(String),
11 #[error("no mesh found: {0}")]
13 NoMesh(String),
14}
15
16#[cfg(feature = "gltf")]
21impl ModelMesh {
22 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#[cfg(feature = "obj")]
85impl ModelMesh {
86 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#[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 let result = ModelMesh::from_obj(&[0xFF, 0xFE, 0x00, 0x01]);
181 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}