From 26dc75cd38887a01fb007d080cb773f364424756 Mon Sep 17 00:00:00 2001 From: alphafetus Date: Thu, 17 Sep 2026 19:49:17 -0400 Subject: [PATCH] refactor: change mesh load pipeline These changes allow mesh loading to conform even better to the file format, meaning that loading should be more natural and performant. --- src/assets/asset_manager.rs | 22 ++-- src/assets/loaders/gltf_loader.rs | 167 ++++++++++++++++++----------- src/assets/loaders/model_loader.rs | 39 ++++++- src/assets/loaders/obj_loader.rs | 4 +- src/common/material.rs | 2 +- src/common/mesh.rs | 2 +- 6 files changed, 155 insertions(+), 81 deletions(-) diff --git a/src/assets/asset_manager.rs b/src/assets/asset_manager.rs index 2537ee4..839bde8 100644 --- a/src/assets/asset_manager.rs +++ b/src/assets/asset_manager.rs @@ -1,5 +1,3 @@ -use crate::assets::loaders::mat_loader::MaterialLoader; -use crate::assets::loaders::mesh_loader::MeshLoader; use crate::common::material::{Material, MaterialRegistry}; use crate::common::mesh::{MeshHandle, MeshRegistry}; use crate::common::scene::Scene; @@ -11,6 +9,7 @@ use std::path::Path; use std::sync::Arc; use vulkano::buffer::Subbuffer; use vulkano::memory::allocator::MemoryAllocator; +use crate::assets::loaders::model_loader::{ModelLoadOptions, ModelLoader}; /// A manager that controls all assets used for display. It handles loading all meshes, materials, /// and textures for now. @@ -27,10 +26,6 @@ pub struct AssetManager { material_registry: MaterialRegistry, /// The asset manager's [`TextureRegistry`]. texture_registry: TextureRegistry, - - // Loaders - mesh_loader: MeshLoader, - material_loader: MaterialLoader, } impl AssetManager { @@ -43,8 +38,6 @@ impl AssetManager { mesh_registry, material_registry, texture_registry: TextureRegistry::new(), - mesh_loader: MeshLoader::new(), - material_loader: MaterialLoader::new(), } } @@ -59,8 +52,12 @@ impl AssetManager { // `MeshRegistry::append_mesh`), matching the index materials are registered under below. // Not every mesh format has a material loader yet (e.g. `.obj`), so a failure here just // means no materials get registered for this mesh rather than failing the whole load. - match self.material_loader.load_materials(&path) { - Ok(materials) => { + let path = path.as_ref(); + let mut loader = ModelLoader::new(); + let model_info = loader.load_model(path, ModelLoadOptions::default())?; + + match model_info.materials { + Some(materials) => { for (index, info) in materials.into_iter().enumerate() { let material_id = format!("{mesh_id}_mat_{index}"); self.material_registry @@ -68,10 +65,11 @@ impl AssetManager { println!("Loaded material {material_id}"); } } - Err(err) => println!("No materials loaded for mesh \"{mesh_id}\": {err}"), + None => println!("No materials loaded for mesh \"{mesh_id}\""), } - let mesh = self.mesh_loader.load_mesh(&path)?; + let mesh = model_info.mesh + .expect("Mesh was not loaded"); self.mesh_registry.register_mesh(mesh_id.clone(), mesh); Ok(()) diff --git a/src/assets/loaders/gltf_loader.rs b/src/assets/loaders/gltf_loader.rs index 423f9ab..0ae026a 100644 --- a/src/assets/loaders/gltf_loader.rs +++ b/src/assets/loaders/gltf_loader.rs @@ -1,19 +1,73 @@ use anyhow::Result; use std::path::Path; -use gltf::buffer::Data; +use gltf::buffer::Data as BufferData; +use gltf::image::Data as ImageData; use gltf::image::Source; -use gltf::Texture; +use gltf::{Document, Texture}; use nalgebra::{Matrix4, Vector4}; use crate::assets::loaders::common::{MaterialLoadInfo, MaterialUniforms, MeshLoadInfo, Submesh, TextureLoadInfo}; +use crate::assets::loaders::model_loader::{LoadModel, ModelLoadInfo, ModelLoadOptions}; pub struct GltfLoader; impl GltfLoader { - /// Traverses and loads all information from a GLTF/GLB node. - pub fn traverse_node( + /// Returns a stable identifier for a texture's image, meant to be resolved by a texture + /// loader later. External images are identified by their URI; images embedded in the file + /// itself (e.g. in a `.glb`) have no URI, so they're identified by their image index instead. + fn texture_id(texture: &Texture) -> String { + let image = texture.source(); + match image.source() { + Source::Uri { uri, .. } => uri.to_string(), + Source::View { .. } => format!("embedded_image_{}", image.index()), + } + } + + /// Loads a mesh from the given [`Document`] and vector of [`Data`], both specific to the glTF + /// file format. + fn load_mesh(&self, document: &Document, buffers: &Vec) -> Result { + let mut vertices = Vec::new(); + let mut normals = Vec::new(); + let mut uvs = Vec::new(); + let mut indices = Vec::::new(); + let mut submeshes = Vec::new(); + + let scenes = document.scenes(); + + // We need to iterate through scenes and apply the transforms from each scene to their + // corresponding vertices. + for scene in scenes { + for node in scene.nodes() { + // Read through the nodes and get all vertex information. + // Even if there are multiple scenes, we're treating the whole file as a single mesh. + Self::mesh_traverse_node( + &node, + &Matrix4::identity(), + &buffers, + &mut vertices, + &mut normals, + &mut uvs, + &mut indices, + &mut submeshes, + ); + } + } + + let info = MeshLoadInfo { + vertices, + indices, + normals, + uvs, + submeshes, + }; + + Ok(info) + } + + /// Traverses and loads all mesh information from a glTF/glB node. + pub fn mesh_traverse_node( node: &gltf::Node, parent_transform: &Matrix4, - buffers: &Vec, + buffers: &Vec, vertices: &mut Vec<[f32; 3]>, normals: &mut Vec<[f32; 3]>, uvs: &mut Vec<[f32; 3]>, @@ -73,7 +127,7 @@ impl GltfLoader { // Process all child nodes as well for child in node.children() { - Self::traverse_node( + Self::mesh_traverse_node( &child, &full_transform, &buffers, @@ -86,62 +140,12 @@ impl GltfLoader { } } - /// Returns a stable identifier for a texture's image, meant to be resolved by a texture - /// loader later. External images are identified by their URI; images embedded in the file - /// itself (e.g. in a `.glb`) have no URI, so they're identified by their image index instead. - fn texture_id(texture: &Texture) -> String { - let image = texture.source(); - match image.source() { - Source::Uri { uri, .. } => uri.to_string(), - Source::View { .. } => format!("embedded_image_{}", image.index()), - } - } - - fn load_mesh(&self, path: &Path) -> Result { - let (document, buffers, _) = gltf::import(path)?; - - let mut vertices = Vec::new(); - let mut normals = Vec::new(); - let mut uvs = Vec::new(); - let mut indices = Vec::::new(); - let mut submeshes = Vec::new(); - - let scenes = document.scenes(); - - // We need to iterate through scenes and apply the transforms from each scene to their - // corresponding vertices. - println!("Parsing {} scenes from {:?}", scenes.len(), path); - for scene in scenes { - for node in scene.nodes() { - // Read through the nodes and get all vertex information. - // Even if there are multiple scenes, we're treating the whole file as a single mesh. - Self::traverse_node( - &node, - &Matrix4::identity(), - &buffers, - &mut vertices, - &mut normals, - &mut uvs, - &mut indices, - &mut submeshes, - ); - } - } - - let info = MeshLoadInfo { - vertices, - indices, - normals, - uvs, - submeshes, - }; - - Ok(info) - } - - fn load_materials(&self, path: &Path) -> anyhow::Result> { - let (document, _buffers, _images) = gltf::import(path)?; - + /// Loads all materials from the given [`Document`] and [`Data`] buffers. These are glTF/glB + /// specific. + /// + /// Note that this does not load any textures that are used with the materials, so this must + /// be called in tandem with [`GltfLoader::load_textures`]. + fn load_materials(&self, document: &Document, buffers: &Vec) -> anyhow::Result> { // Collected in document order, so a material's position here matches the // `primitive.material().index()` that mesh loading recorded for its submeshes. Ok(document @@ -179,8 +183,43 @@ impl GltfLoader { }) .collect()) } - - fn load_texture(&self, path: &Path) -> Result { + + /// Loads all textures from the given [`Document`] and [`Data`] buffers. + /// + /// Note that loading this does not load any materials that may use the textures, so this must + /// be called in tandem with [`GltfLoader::load_materials`]. + fn load_textures(&self, document: &Document, buffers: &Vec, images: &Vec) -> Result> { todo!() } } + +impl LoadModel for GltfLoader { + fn load_model(&self, path: &Path, options: ModelLoadOptions) -> Result { + let mut info = ModelLoadInfo::default(); + + // First try to parse the file at the given path and get the information from it, then + // continue with processing. + let (document, buffers, images) = gltf::import(path)?; + + if options.load_mesh { + // Load the mesh. + let mesh_info = self.load_mesh(&document, &buffers)?; + info.mesh = Some(mesh_info); + } + + if options.load_materials { + // Load the materials. + let material_info = self.load_materials(&document, &buffers)?; + info.materials = Some(material_info); + } + + // TODO Textures aren't currently supported. + // if options.load_textures { + // // Load the textures. + // let texture_info = self.load_textures(&document, &buffers, &images)?; + // info.textures = Some(texture_info) + // } + + Ok(info) + } +} diff --git a/src/assets/loaders/model_loader.rs b/src/assets/loaders/model_loader.rs index 50fdb5b..33201cc 100644 --- a/src/assets/loaders/model_loader.rs +++ b/src/assets/loaders/model_loader.rs @@ -1,7 +1,9 @@ use anyhow::Result; use std::collections::HashMap; use std::path::Path; +use thiserror::Error; use crate::assets::loaders::common::{MaterialLoadInfo, MeshLoadInfo, TextureLoadInfo}; +use crate::assets::loaders::gltf_loader::GltfLoader; /// A trait for loading a model from a given path. pub trait LoadModel { @@ -10,7 +12,7 @@ pub trait LoadModel { } /// The options applicable to loading a model. -#[derive(Default, Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug)] pub struct ModelLoadOptions { /// Whether to load the full mesh. pub load_mesh: bool, @@ -20,8 +22,27 @@ pub struct ModelLoadOptions { pub load_textures: bool, } +impl Default for ModelLoadOptions { + fn default() -> Self { + Self { + load_mesh: true, + load_materials: true, + load_textures: true, + } + } +} + +#[derive(Error, Debug, Clone)] +pub enum ModelLoadError { + #[error("Extension was not found")] + ExtensionNotFound, + #[error("Extension {0} is not supported")] + ExtensionNotSupported(String), +} + /// Contains the information about the mesh, materials, and textures that this model has loaded. /// Each type of information will only be loaded if the caller indicates that they should be. +#[derive(Default)] pub struct ModelLoadInfo { /// The loaded mesh, if applicable. pub mesh: Option, @@ -41,9 +62,25 @@ impl ModelLoader { /// Creates a new `ModelLoader` instance. pub fn new() -> Self { let mut loaders = HashMap::new(); + loaders.insert("gltf".into(), Box::new(GltfLoader) as Box); + loaders.insert("glb".into(), Box::new(GltfLoader)); Self { loaders, } } + + /// Loads a model from the given path. The available options are indicated by the [`ModelLoadOptions`] + /// struct. + pub fn load_model>(&mut self, path: P, options: ModelLoadOptions) -> Result { + // Determine which loader type to use. + let path = path.as_ref(); + let ext = path.extension() + .ok_or(ModelLoadError::ExtensionNotFound)? + .to_str().unwrap(); + + self.loaders.get(ext) + .ok_or(ModelLoadError::ExtensionNotSupported(ext.into()))? + .load_model(path, options) + } } diff --git a/src/assets/loaders/obj_loader.rs b/src/assets/loaders/obj_loader.rs index 440110b..f558512 100644 --- a/src/assets/loaders/obj_loader.rs +++ b/src/assets/loaders/obj_loader.rs @@ -1,10 +1,10 @@ -use crate::assets::loaders::mesh_loader::{LoadMesh, MeshLoadInfo, Submesh}; use std::path::Path; use wavefront::Obj; +use crate::assets::loaders::common::{MeshLoadInfo, Submesh}; pub struct ObjLoader; -impl LoadMesh for ObjLoader { +impl ObjLoader { fn load_mesh(&self, path: &Path) -> anyhow::Result { let obj_info = Obj::from_file(path).expect("Couldn't read wavefront file"); diff --git a/src/common/material.rs b/src/common/material.rs index e0bc005..68e26d0 100644 --- a/src/common/material.rs +++ b/src/common/material.rs @@ -1,5 +1,5 @@ -use crate::assets::loaders::mat_loader::MaterialLoadInfo; use std::collections::HashMap; +use crate::assets::loaders::common::MaterialLoadInfo; pub struct MaterialRegistry { materials: HashMap diff --git a/src/common/mesh.rs b/src/common/mesh.rs index e7075cd..c3e8643 100644 --- a/src/common/mesh.rs +++ b/src/common/mesh.rs @@ -1,9 +1,9 @@ -use crate::assets::loaders::mesh_loader::MeshLoadInfo; use crate::gpu::vertex3::Vertex3; use std::collections::HashMap; use std::sync::Arc; use vulkano::buffer::{Buffer, BufferCreateInfo, BufferUsage, Subbuffer}; use vulkano::memory::allocator::{AllocationCreateInfo, MemoryAllocator, MemoryTypeFilter}; +use crate::assets::loaders::common::MeshLoadInfo; /// This struct is slightly misleading - it doesn't actually contain the mesh information, but /// the locations of the mesh's information in the overall application's vertex buffer, index buffer,