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.
This commit is contained in:
alphafetus committed 2026-09-17 19:49:17 -04:00
1 parent 406b0f9174
commit 26dc75cd38
6 files changed
+154 -80

No files matched your search

+10 -12
View File
@@ -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(())
+102 -63
View File
@@ -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<BufferData>) -> Result<MeshLoadInfo> {
let mut vertices = Vec::new();
let mut normals = Vec::new();
let mut uvs = Vec::new();
let mut indices = Vec::<u32>::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<f32>,
buffers: &Vec<Data>,
buffers: &Vec<BufferData>,
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<MeshLoadInfo> {
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::<u32>::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<Vec<MaterialLoadInfo>> {
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<BufferData>) -> anyhow::Result<Vec<MaterialLoadInfo>> {
// Collected in document order, so a material's position here matches the
// `primitive.material().index()` that mesh loading recorded for its submeshes.
Ok(document
@@ -180,7 +184,42 @@ impl GltfLoader {
.collect())
}
fn load_texture(&self, path: &Path) -> Result<TextureLoadInfo> {
/// 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<BufferData>, images: &Vec<ImageData>) -> Result<Vec<TextureLoadInfo>> {
todo!()
}
}
impl LoadModel for GltfLoader {
fn load_model(&self, path: &Path, options: ModelLoadOptions) -> Result<ModelLoadInfo> {
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)
}
}
+38 -1
View File
@@ -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<MeshLoadInfo>,
@@ -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<dyn LoadModel>);
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<P: AsRef<Path>>(&mut self, path: P, options: ModelLoadOptions) -> Result<ModelLoadInfo> {
// 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)
}
}
+2 -2
View File
@@ -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<MeshLoadInfo> {
let obj_info = Obj::from_file(path).expect("Couldn't read wavefront file");
+1 -1
View File
@@ -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<String, Material>
+1 -1
View File
@@ -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,