//! glTF 2.0 static-mesh importer. //! //! Loads the mesh primitives of a glTF document into engine [`Mesh`]es, reading //! their PBR-lite [`Material`] factors and the world [`Transform`] of each //! placement (the node hierarchy is flattened into world space). Missing //! normals are generated; missing UVs default to zero. Animation, skinning, and //! textures are out of scope for Stage 4. use std::path::Path; use crate::math::{Color, Transform, Vec2, Vec3}; use crate::render::{Material, Mesh, Vertex}; /// Errors produced while importing a glTF document. #[derive(Debug, thiserror::Error)] pub enum GltfError { /// The file could not be read or parsed as glTF. #[error("failed to load glTF: {0}")] Load(#[from] gltf::Error), /// A mesh primitive was missing the required `POSITION` attribute. #[error("glTF primitive has no POSITION attribute")] MissingPositions, } /// One imported mesh placement: geometry, material, and world transform. pub struct GltfMesh { /// Optional node/mesh name from the document. pub name: Option, /// The primitive's geometry. pub mesh: Mesh, /// The primitive's PBR-lite material. pub material: Material, /// World-space placement (node hierarchy flattened). pub transform: Transform, } /// An imported glTF model: a flat list of mesh placements in world space. pub struct GltfModel { /// Every mesh primitive in the default scene, already placed in world space. pub meshes: Vec, } impl GltfModel { /// Total triangle count across all imported primitives. pub fn triangle_count(&self) -> usize { self.meshes.iter().map(|m| m.mesh.triangle_count()).sum() } } /// Imports a glTF/GLB file from `path` (external buffers are resolved relative /// to the file). pub fn load_gltf(path: impl AsRef) -> Result { let (document, buffers, _images) = gltf::import(path)?; build_model(&document, &buffers) } /// The [`AssetServer`](super::AssetServer) loader for glTF/GLB files. /// /// Registered by default (handles `.gltf` and `.glb`), so /// `assets.load::("model.gltf")` works out of the box; it simply /// wraps [`load_gltf`] and adapts its error into [`AssetError`]. pub struct GltfLoader; impl super::AssetLoader for GltfLoader { type Asset = GltfModel; fn extensions(&self) -> &'static [&'static str] { &["gltf", "glb"] } fn load(&self, path: &Path) -> Result { load_gltf(path).map_err(|err| super::AssetError::Load { path: path.to_path_buf(), message: err.to_string(), }) } } /// Imports a glTF/GLB document from an in-memory byte slice (buffers must be /// embedded; used for tests and bundled assets). pub fn load_gltf_slice(bytes: &[u8]) -> Result { let (document, buffers, _images) = gltf::import_slice(bytes)?; build_model(&document, &buffers) } /// Walks the default scene's node hierarchy, accumulating world transforms and /// emitting one [`GltfMesh`] per primitive. fn build_model( document: &gltf::Document, buffers: &[gltf::buffer::Data], ) -> Result { let mut meshes = Vec::new(); let scene = document .default_scene() .or_else(|| document.scenes().next()); if let Some(scene) = scene { for node in scene.nodes() { visit_node(&node, Transform::IDENTITY, buffers, &mut meshes)?; } } Ok(GltfModel { meshes }) } fn visit_node( node: &gltf::Node, parent: Transform, buffers: &[gltf::buffer::Data], out: &mut Vec, ) -> Result<(), GltfError> { let world = parent.mul_transform(&node_transform(node)); if let Some(mesh) = node.mesh() { for primitive in mesh.primitives() { let geometry = read_primitive(&primitive, buffers)?; out.push(GltfMesh { name: node.name().or_else(|| mesh.name()).map(str::to_owned), mesh: geometry, material: read_material(&primitive), transform: world, }); } } for child in node.children() { visit_node(&child, world, buffers, out)?; } Ok(()) } /// Converts a node's local TRS into an engine [`Transform`]. fn node_transform(node: &gltf::Node) -> Transform { let (t, r, s) = node.transform().decomposed(); Transform::from_trs( Vec3::from_array(t), glam::Quat::from_array(r), Vec3::from_array(s), ) } /// Reads one primitive's vertices and indices into a [`Mesh`]. fn read_primitive( primitive: &gltf::Primitive, buffers: &[gltf::buffer::Data], ) -> Result { let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()])); let positions: Vec<[f32; 3]> = reader .read_positions() .ok_or(GltfError::MissingPositions)? .collect(); let normals: Option> = reader.read_normals().map(|n| n.collect()); let uvs: Option> = reader.read_tex_coords(0).map(|tc| tc.into_f32().collect()); let indices: Vec = match reader.read_indices() { Some(idx) => idx.into_u32().collect(), // Non-indexed primitive: every three positions form a triangle. None => (0..positions.len() as u32).collect(), }; // Generate flat normals when the document omits them, so lighting still works. let normals = normals.unwrap_or_else(|| compute_normals(&positions, &indices)); let vertices = positions .iter() .enumerate() .map(|(i, &p)| { let n = normals.get(i).copied().unwrap_or([0.0, 1.0, 0.0]); let uv = uvs .as_ref() .and_then(|u| u.get(i)) .copied() .unwrap_or([0.0, 0.0]); Vertex::new( Vec3::from_array(p), Vec3::from_array(n), Vec2::from_array(uv), ) }) .collect(); Ok(Mesh::new(vertices, indices)) } /// Smooth per-vertex normals: accumulate each triangle's face normal at its /// vertices, then normalize. fn compute_normals(positions: &[[f32; 3]], indices: &[u32]) -> Vec<[f32; 3]> { let mut normals = vec![Vec3::ZERO; positions.len()]; for tri in indices.chunks_exact(3) { let (a, b, c) = (tri[0] as usize, tri[1] as usize, tri[2] as usize); let pa = Vec3::from_array(positions[a]); let pb = Vec3::from_array(positions[b]); let pc = Vec3::from_array(positions[c]); let face = (pb - pa).cross(pc - pa); normals[a] += face; normals[b] += face; normals[c] += face; } normals .into_iter() .map(|n| n.normalize_or_zero().to_array()) .collect() } /// Maps a primitive's PBR metallic-roughness factors onto a [`Material`]. fn read_material(primitive: &gltf::Primitive) -> Material { let pbr = primitive.material().pbr_metallic_roughness(); let [r, g, b, a] = pbr.base_color_factor(); Material { albedo: Color::rgba(r, g, b, a), metallic: pbr.metallic_factor(), roughness: pbr.roughness_factor(), } }