1use std::collections::{HashMap, HashSet};
19
20use super::{
21 animation::{AnimationClip, Interpolation, JointTrack, Keyframe},
22 mesh::{Mesh, MeshBuilder, Vertex},
23 skeleton::{Joint, Skeleton, Transform},
24 skinned_mesh::{SkinnedMesh, SkinnedMeshBuilder, SkinnedVertex},
25};
26
27#[derive(Debug)]
28pub enum ModelLoadError {
29 Io(std::io::Error),
30 Parse(String),
33 UnsupportedFeature(&'static str),
37 MissingData(String),
40}
41
42impl std::fmt::Display for ModelLoadError {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 Self::Io(e) => write!(f, "failed to read model file: {e}"),
46 Self::Parse(msg) => write!(f, "failed to parse glTF: {msg}"),
47 Self::UnsupportedFeature(feature) => write!(f, "unsupported glTF feature: {feature}"),
48 Self::MissingData(msg) => write!(f, "missing glTF data: {msg}"),
49 }
50 }
51}
52
53impl std::error::Error for ModelLoadError {
54 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
55 match self {
56 Self::Io(e) => Some(e),
57 _ => None,
58 }
59 }
60}
61
62impl From<std::io::Error> for ModelLoadError {
63 fn from(e: std::io::Error) -> Self {
64 Self::Io(e)
65 }
66}
67
68pub struct LoadedModel {
74 pub skinned_meshes: Vec<(String, SkinnedMesh)>,
75 pub static_meshes: Vec<(String, Mesh)>,
76 pub skeleton: Option<Skeleton>,
77 pub animations: Vec<AnimationClip>,
78}
79
80const DEFAULT_TANGENT: [f32; 4] = [1.0, 0.0, 0.0, 1.0];
84
85pub fn load_gltf(path: &str) -> Result<LoadedModel, ModelLoadError> {
112 let (document, buffers, _images) = gltf::import(path).map_err(|e| match e {
113 gltf::Error::Io(io_err) => ModelLoadError::Io(io_err),
114 other => ModelLoadError::Parse(other.to_string()),
115 })?;
116 let get_buffer_data = |buffer: gltf::Buffer| buffers.get(buffer.index()).map(|b| b.0.as_slice());
117
118 let skin = match document.skins().len() {
119 0 => None,
120 1 => document.skins().next(),
121 _ => return Err(ModelLoadError::UnsupportedFeature("more than one skin")),
122 };
123
124 let skeleton = match &skin {
125 Some(skin) => {
126 let joint_nodes: Vec<gltf::Node> = skin.joints().collect();
127 let node_to_joint: HashMap<usize, usize> =
128 joint_nodes.iter().enumerate().map(|(i, node)| (node.index(), i)).collect();
129
130 let inverse_bind_matrices: Vec<glam::Mat4> =
131 match skin.reader(get_buffer_data).read_inverse_bind_matrices() {
132 Some(matrices) => matrices.map(|m| glam::Mat4::from_cols_array_2d(&m)).collect(),
133 None => vec![glam::Mat4::IDENTITY; joint_nodes.len()],
135 };
136 if inverse_bind_matrices.len() != joint_nodes.len() {
137 return Err(ModelLoadError::MissingData(format!(
138 "skin has {} joints but {} inverse bind matrices",
139 joint_nodes.len(),
140 inverse_bind_matrices.len(),
141 )));
142 }
143
144 let mut parent_of_node: HashMap<usize, usize> = HashMap::new();
152 for (parent_joint_index, joint_node) in joint_nodes.iter().enumerate() {
153 for child in joint_node.children() {
154 if node_to_joint.contains_key(&child.index()) {
155 parent_of_node.insert(child.index(), parent_joint_index);
156 }
157 }
158 }
159
160 let joints = joint_nodes
161 .iter()
162 .zip(inverse_bind_matrices)
163 .map(|(node, inverse_bind_matrix)| {
164 let (translation, rotation, scale) = node.transform().decomposed();
165 Joint {
166 name: node.name().unwrap_or("joint").to_string(),
167 parent: parent_of_node.get(&node.index()).copied(),
168 inverse_bind_matrix,
169 local_bind_transform: Transform {
170 translation: glam::Vec3::from(translation),
171 rotation: glam::Quat::from_array(rotation),
172 scale: glam::Vec3::from(scale),
173 },
174 }
175 })
176 .collect();
177
178 Some(Skeleton::new(joints))
179 }
180 None => None,
181 };
182
183 let skinned_node_indices: HashSet<usize> = match &skin {
184 Some(skin) => document
185 .nodes()
186 .filter(|n| matches!(n.skin(), Some(s) if s.index() == skin.index()))
187 .map(|n| n.index())
188 .collect(),
189 None => HashSet::new(),
190 };
191
192 let mut skinned_meshes = Vec::new();
193 let mut static_meshes = Vec::new();
194 for node in document.nodes() {
195 let Some(mesh) = node.mesh() else { continue };
196 let is_skinned = skinned_node_indices.contains(&node.index());
197
198 for (i, primitive) in mesh.primitives().enumerate() {
199 let name = format!("{}_{i}", mesh.name().unwrap_or("mesh"));
200 check_not_sparse(&primitive, gltf::Semantic::Positions)?;
201 let reader = primitive.reader(get_buffer_data);
202
203 let positions: Vec<[f32; 3]> = reader
204 .read_positions()
205 .ok_or_else(|| ModelLoadError::MissingData(format!("primitive '{name}' has no POSITION attribute")))?
206 .collect();
207 let normals: Vec<[f32; 3]> = reader
208 .read_normals()
209 .ok_or_else(|| ModelLoadError::MissingData(format!("primitive '{name}' has no NORMAL attribute")))?
210 .collect();
211 let tex_coords: Vec<[f32; 2]> = reader
212 .read_tex_coords(0)
213 .map(|t| t.into_f32().collect())
214 .ok_or_else(|| ModelLoadError::MissingData(format!("primitive '{name}' has no TEXCOORD_0 attribute")))?;
215 let tangents: Vec<[f32; 4]> = match reader.read_tangents() {
216 Some(t) => t.collect(),
217 None => vec![DEFAULT_TANGENT; positions.len()],
218 };
219 let indices: Vec<u32> = reader
220 .read_indices()
221 .map(|idx| idx.into_u32().collect())
222 .ok_or(ModelLoadError::UnsupportedFeature("non-indexed primitives"))?;
223
224 if is_skinned {
225 check_not_sparse(&primitive, gltf::Semantic::Joints(0))?;
226 check_not_sparse(&primitive, gltf::Semantic::Weights(0))?;
227 let joints: Vec<[u16; 4]> = reader
228 .read_joints(0)
229 .map(|j| j.into_u16().collect())
230 .ok_or_else(|| ModelLoadError::MissingData(format!("skinned primitive '{name}' has no JOINTS_0 attribute")))?;
231 let weights: Vec<[f32; 4]> = reader
232 .read_weights(0)
233 .map(|w| w.into_f32().collect())
234 .ok_or_else(|| ModelLoadError::MissingData(format!("skinned primitive '{name}' has no WEIGHTS_0 attribute")))?;
235
236 let vertices = positions
237 .into_iter()
238 .zip(normals)
239 .zip(tex_coords)
240 .zip(tangents)
241 .zip(joints)
242 .zip(weights)
243 .map(|(((((p, n), uv), t), j), w)| {
244 SkinnedVertex::new(
245 glam::Vec3::from(p),
246 glam::Vec2::from(uv),
247 glam::Vec3::from(n),
248 glam::Vec4::from(t),
249 j,
250 w,
251 )
252 })
253 .collect();
254 skinned_meshes.push((name, SkinnedMeshBuilder::new(vertices, indices).build()));
255 } else {
256 let vertices = positions
257 .into_iter()
258 .zip(normals)
259 .zip(tex_coords)
260 .zip(tangents)
261 .map(|(((p, n), uv), t)| {
262 Vertex::new(glam::Vec3::from(p), glam::Vec2::from(uv), glam::Vec3::from(n), glam::Vec4::from(t))
263 })
264 .collect();
265 static_meshes.push((name, MeshBuilder::new(vertices, indices).build()));
266 }
267 }
268 }
269
270 let animations = match &skeleton {
271 Some(skeleton) => {
272 let mut clips = Vec::new();
273 for animation in document.animations() {
274 let mut tracks: HashMap<usize, JointTrack> = HashMap::new();
275
276 for channel in animation.channels() {
277 let node = channel.target().node();
278 let Some(joint_index) = node
279 .name()
280 .and_then(|name| (0..skeleton.joint_count()).find(|&i| skeleton.joint(i).name == name))
281 else {
282 continue;
286 };
287
288 let interpolation = match channel.sampler().interpolation() {
289 gltf::animation::Interpolation::Linear => Interpolation::Linear,
290 gltf::animation::Interpolation::Step => Interpolation::Step,
291 gltf::animation::Interpolation::CubicSpline => {
292 return Err(ModelLoadError::UnsupportedFeature("CUBICSPLINE interpolation"));
293 }
294 };
295
296 let reader = channel.reader(get_buffer_data);
297 let times: Vec<f32> = reader
298 .read_inputs()
299 .ok_or_else(|| ModelLoadError::MissingData("animation channel has no keyframe times".to_string()))?
300 .collect();
301 let outputs = reader.read_outputs().ok_or_else(|| {
302 ModelLoadError::MissingData("animation channel has no keyframe values".to_string())
303 })?;
304
305 let track = tracks.entry(joint_index).or_insert_with(|| JointTrack {
306 joint_index,
307 translation: Vec::new(),
308 translation_interpolation: Interpolation::Linear,
309 rotation: Vec::new(),
310 rotation_interpolation: Interpolation::Linear,
311 scale: Vec::new(),
312 scale_interpolation: Interpolation::Linear,
313 });
314
315 match outputs {
316 gltf::animation::util::ReadOutputs::Translations(values) => {
317 track.translation = times
318 .into_iter()
319 .zip(values)
320 .map(|(time, v)| Keyframe { time, value: glam::Vec3::from(v) })
321 .collect();
322 track.translation_interpolation = interpolation;
323 }
324 gltf::animation::util::ReadOutputs::Rotations(values) => {
325 track.rotation = times
326 .into_iter()
327 .zip(values.into_f32())
328 .map(|(time, v)| Keyframe { time, value: glam::Quat::from_array(v) })
329 .collect();
330 track.rotation_interpolation = interpolation;
331 }
332 gltf::animation::util::ReadOutputs::Scales(values) => {
333 track.scale = times
334 .into_iter()
335 .zip(values)
336 .map(|(time, v)| Keyframe { time, value: glam::Vec3::from(v) })
337 .collect();
338 track.scale_interpolation = interpolation;
339 }
340 gltf::animation::util::ReadOutputs::MorphTargetWeights(_) => {
341 return Err(ModelLoadError::UnsupportedFeature("morph targets"));
342 }
343 }
344 }
345
346 clips.push(AnimationClip::new(
347 animation.name().unwrap_or("animation").to_string(),
348 tracks.into_values().collect(),
349 ));
350 }
351 clips
352 }
353 None => {
354 let animation_count = document.animations().len();
355 if animation_count > 0 {
356 tracing::warn!(
357 "load_gltf: file has {animation_count} animation(s) but no skin — skipping, \
358 nothing to animate"
359 );
360 }
361 Vec::new()
362 }
363 };
364
365 Ok(LoadedModel { skinned_meshes, static_meshes, skeleton, animations })
366}
367
368fn check_not_sparse(primitive: &gltf::Primitive, semantic: gltf::Semantic) -> Result<(), ModelLoadError> {
369 if let Some(accessor) = primitive.get(&semantic)
370 && accessor.sparse().is_some()
371 {
372 return Err(ModelLoadError::UnsupportedFeature("sparse accessors"));
373 }
374 Ok(())
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380
381 fn fixture_path() -> String {
382 concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/gltf/two_joint_skeleton.gltf").to_string()
383 }
384
385 #[test]
386 fn loads_geometry_skeleton_and_animation_from_a_hand_authored_fixture() {
387 let model = load_gltf(&fixture_path()).expect("fixture should load cleanly");
388
389 assert_eq!(model.static_meshes.len(), 0);
390 assert_eq!(model.skinned_meshes.len(), 1);
394
395 let skeleton = model.skeleton.expect("fixture has one skin, expected a Skeleton");
396 assert_eq!(skeleton.joint_count(), 3);
397 let root = skeleton.joint_index_by_name("root").unwrap();
401 let mid = skeleton.joint_index_by_name("mid").unwrap();
402 let child = skeleton.joint_index_by_name("child").unwrap();
403 assert_eq!(skeleton.joint(root).parent, None);
404 assert_eq!(skeleton.joint(mid).parent, Some(root));
405 assert_eq!(skeleton.joint(child).parent, Some(mid));
406
407 assert_eq!(model.animations.len(), 1);
408 let clip = &model.animations[0];
409 assert_eq!(clip.name, "wave");
410 assert_eq!(clip.duration, 1.0);
411
412 let poses = clip.sample(0.5, &skeleton);
415 assert!((poses[root].translation.z - 2.5).abs() < 1e-5);
416 }
417}