wip: begin refactor of model loading system

This commit is contained in:
alphafetus committed 2026-09-17 18:11:20 -04:00
1 parent a315cbc543
commit 406b0f9174
17 files changed
+247 -306

No files matched your search

+1 -4
View File
@@ -495,10 +495,7 @@ impl VulkanContext {
println!("Couldn't find material {material_id:?} for {mesh_id:?}");
DEFAULT_BASE_COLOR
},
|material| {
println!("Found material {material_id:?} for {mesh_id:?}, with base color {:?}", material.base_color);
material.base_color
}
|material| material.base_color
);
builder
+11 -7
View File
@@ -1,15 +1,16 @@
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use anyhow::Result;
use vulkano::buffer::Subbuffer;
use vulkano::memory::allocator::MemoryAllocator;
use crate::assets::loaders::material::mat_loader::MaterialLoader;
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;
use crate::common::texture::TextureRegistry;
use crate::gpu::vertex3::Vertex3;
use anyhow::Result;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use vulkano::buffer::Subbuffer;
use vulkano::memory::allocator::MemoryAllocator;
/// A manager that controls all assets used for display. It handles loading all meshes, materials,
/// and textures for now.
@@ -24,6 +25,8 @@ pub struct AssetManager {
mesh_registry: MeshRegistry,
/// The asset manager's [`MaterialRegistry`].
material_registry: MaterialRegistry,
/// The asset manager's [`TextureRegistry`].
texture_registry: TextureRegistry,
// Loaders
mesh_loader: MeshLoader,
@@ -39,6 +42,7 @@ impl AssetManager {
allocator,
mesh_registry,
material_registry,
texture_registry: TextureRegistry::new(),
mesh_loader: MeshLoader::new(),
material_loader: MaterialLoader::new(),
}
+64
View File
@@ -0,0 +1,64 @@
pub struct MeshLoadInfo {
/// The mesh's vertices.
pub vertices: Vec<[f32; 3]>,
/// The mesh's vertex normals.
pub normals: Vec<[f32; 3]>,
/// The mesh's vertex indices.
pub indices: Vec<u32>,
/// The texture coordinates. Note that these may also be used on 3D textures.
pub uvs: Vec<[f32; 3]>,
/// The contiguous ranges of `indices` that should be drawn with a single material, in the
/// order they were encountered in the source file.
pub submeshes: Vec<Submesh>,
}
/// A contiguous range within a [`MeshInfo`]'s `indices`, along with the material it should be
/// drawn with. Ranges are local to a single loaded file - a [`crate::common::mesh::MeshRegistry`]
/// rewrites these into offsets within its own shared index buffer.
#[derive(Debug, Clone)]
pub struct Submesh {
/// The index of the material this submesh uses, as defined by the source file (e.g. a glTF
/// material index). `None` if the file did not assign a material to this range.
pub material_index: Option<usize>,
/// The offset of this submesh's first index within `MeshInfo::indices`.
pub index_start: usize,
/// The number of indices that make up this submesh.
pub index_count: usize,
}
/// Contains all information required for the application to load a new material.
pub struct MaterialLoadInfo {
/// The ID of this material
pub name: String,
/// The material uniforms. These should be defined on every imported material.
pub uniforms: MaterialUniforms,
/// The ID of the base color map, if any.
pub base_color_map: Option<String>,
/// The ID of the roughness map, if any. This is ignored for now.
pub roughness_map: Option<String>,
/// The ID of the normal map, if any. This is ignored for now.
pub normal_map: Option<String>,
}
/// A struct composed of the material uniforms. These uniforms will typically scale the material
/// texture maps if there are any. Refer to [`MaterialInfo`] for more information.
pub struct MaterialUniforms {
/// The base color of the material. This scales the base color texture, if any.
/// This value must be specified in normalized RGBA format (all values between 0 and 1).
pub base_color: [f32; 4],
/// The metalness of the material. This scales the metalness texture, if any. This value should
/// be specified as a normalized float between 0 and 1, based on an 8-bit number space.
///
/// This is ignored until the shaders are updated.
pub metalness_factor: f32,
/// The roughness of the material. This scales the roughness texture, if any. This value should
/// be specified as a normalized float between 0 and 1, based on an 8-bit number space.
///
/// This is ignored until the shaders are updated.
pub roughness_factor: f32,
}
/// A struct containing all information about a loaded texture.
pub struct TextureLoadInfo {
}
@@ -1,8 +1,10 @@
use crate::assets::loaders::mesh_loader::{MeshFileLoader, MeshInfo, Submesh};
use anyhow::Result;
use gltf::buffer::Data;
use nalgebra::{Matrix4, Vector4};
use std::path::Path;
use gltf::buffer::Data;
use gltf::image::Source;
use gltf::Texture;
use nalgebra::{Matrix4, Vector4};
use crate::assets::loaders::common::{MaterialLoadInfo, MaterialUniforms, MeshLoadInfo, Submesh, TextureLoadInfo};
pub struct GltfLoader;
@@ -83,10 +85,19 @@ impl GltfLoader {
);
}
}
}
impl MeshFileLoader for GltfLoader {
fn load_mesh(&self, path: &Path) -> Result<MeshInfo> {
/// 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();
@@ -117,12 +128,59 @@ impl MeshFileLoader for GltfLoader {
}
}
Ok(MeshInfo {
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)?;
// Collected in document order, so a material's position here matches the
// `primitive.material().index()` that mesh loading recorded for its submeshes.
Ok(document
.materials()
.enumerate()
.map(|(index, material)| {
let pbr = material.pbr_metallic_roughness();
let name = material
.name()
.map(str::to_string)
.unwrap_or_else(|| format!("material_{index}"));
let base_color_map = pbr
.base_color_texture()
.map(|info| Self::texture_id(&info.texture()));
let roughness_map = pbr
.metallic_roughness_texture()
.map(|info| Self::texture_id(&info.texture()));
let normal_map = material
.normal_texture()
.map(|info| Self::texture_id(&info.texture()));
MaterialLoadInfo {
name,
uniforms: MaterialUniforms {
base_color: pbr.base_color_factor(),
metalness_factor: pbr.metallic_factor(),
roughness_factor: pbr.roughness_factor(),
},
base_color_map,
roughness_map,
normal_map,
}
})
.collect())
}
fn load_texture(&self, path: &Path) -> Result<TextureLoadInfo> {
todo!()
}
}
@@ -1,62 +0,0 @@
use crate::assets::loaders::material::mat_loader::{MaterialFileLoader, MaterialInfo, MaterialUniforms};
use gltf::image::Source;
use gltf::texture::Texture;
use std::path::Path;
pub struct GltfMaterialLoader;
impl GltfMaterialLoader {
/// 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()),
}
}
}
impl MaterialFileLoader for GltfMaterialLoader {
fn load_materials(&self, path: &Path) -> anyhow::Result<Vec<MaterialInfo>> {
let (document, _buffers, _images) = gltf::import(path)?;
// Collected in document order, so a material's position here matches the
// `primitive.material().index()` that mesh loading recorded for its submeshes.
Ok(document
.materials()
.enumerate()
.map(|(index, material)| {
let pbr = material.pbr_metallic_roughness();
let name = material
.name()
.map(str::to_string)
.unwrap_or_else(|| format!("material_{index}"));
let base_color_map = pbr
.base_color_texture()
.map(|info| Self::texture_id(&info.texture()));
let roughness_map = pbr
.metallic_roughness_texture()
.map(|info| Self::texture_id(&info.texture()));
let normal_map = material
.normal_texture()
.map(|info| Self::texture_id(&info.texture()));
MaterialInfo {
name,
uniforms: MaterialUniforms {
base_color: pbr.base_color_factor(),
metalness_factor: pbr.metallic_factor(),
roughness_factor: pbr.roughness_factor(),
},
base_color_map,
roughness_map,
normal_map,
}
})
.collect())
}
}
-81
View File
@@ -1,81 +0,0 @@
use crate::assets::loaders::material::gltf_mat_loader::GltfMaterialLoader;
use anyhow::Result;
use std::collections::HashMap;
use std::path::Path;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MaterialLoaderError {
#[error("file extension is missing")]
MissingExtension,
#[error("extension {0} is not supported")]
ExtensionNotSupported(String)
}
/// A material loader struct that handles multiple different file types.
/// List of supported and planned file formats:
/// - `.gltf`
/// - `.mtl` (planned, not implemented)
pub struct MaterialLoader {
loaders: HashMap<String, Box<dyn MaterialFileLoader>>,
}
impl MaterialLoader {
/// Creates a new `MaterialLoader` struct.
pub fn new() -> Self {
let mut loaders = HashMap::new();
loaders.insert("gltf".into(), Box::new(GltfMaterialLoader) as Box<dyn MaterialFileLoader>);
Self { loaders }
}
/// Loads all materials from the given path. Refer to the [`MaterialLoader`] for more information
/// on supported material files and which files to target.
pub fn load_materials<P: AsRef<Path>>(&mut self, path: P) -> Result<Vec<MaterialInfo>> {
let path = path.as_ref();
let file_ext = path.extension().and_then(|ext| ext.to_str())
.ok_or(MaterialLoaderError::MissingExtension)?;
self.loaders.get(file_ext)
.ok_or(MaterialLoaderError::ExtensionNotSupported(file_ext.to_string()))?
.load_materials(path)
}
}
/// Contains all information required for the application to load a new material.
pub struct MaterialInfo {
/// The ID of this material
pub name: String,
/// The material uniforms. These should be defined on every imported material.
pub uniforms: MaterialUniforms,
/// The ID of the base color map, if any.
pub base_color_map: Option<String>,
/// The ID of the roughness map, if any. This is ignored for now.
pub roughness_map: Option<String>,
/// The ID of the normal map, if any. This is ignored for now.
pub normal_map: Option<String>,
}
/// A struct composed of the material uniforms. These uniforms will typically scale the material
/// texture maps if there are any. Refer to [`MaterialInfo`] for more information.
pub struct MaterialUniforms {
/// The base color of the material. This scales the base color texture, if any.
/// This value must be specified in normalized RGBA format (all values between 0 and 1).
pub base_color: [f32; 4],
/// The metalness of the material. This scales the metalness texture, if any. This value should
/// be specified as a normalized float between 0 and 1, based on an 8-bit number space.
///
/// This is ignored until the shaders are updated.
pub metalness_factor: f32,
/// The roughness of the material. This scales the roughness texture, if any. This value should
/// be specified as a normalized float between 0 and 1, based on an 8-bit number space.
///
/// This is ignored until the shaders are updated.
pub roughness_factor: f32,
}
/// A trait that must be implemented on all material loaders. Ensures consistent material
/// import/load information.
pub trait MaterialFileLoader {
fn load_materials(&self, path: &Path) -> anyhow::Result<Vec<MaterialInfo>>;
}
-2
View File
@@ -1,2 +0,0 @@
pub mod mat_loader;
pub mod gltf_mat_loader;
-126
View File
@@ -1,126 +0,0 @@
use super::gltf_loader::*;
use crate::assets::loaders::mesh::obj_loader::ObjLoader;
use anyhow::Result;
use std::collections::HashMap;
use std::path::Path;
use thiserror::Error;
pub struct MeshInfo {
/// The mesh's vertices.
pub vertices: Vec<[f32; 3]>,
/// The mesh's vertex normals.
pub normals: Vec<[f32; 3]>,
/// The mesh's vertex indices.
pub indices: Vec<u32>,
/// The texture coordinates. Note that these may also be used on 3D textures.
pub uvs: Vec<[f32; 3]>,
/// The contiguous ranges of `indices` that should be drawn with a single material, in the
/// order they were encountered in the source file.
pub submeshes: Vec<Submesh>,
}
/// A contiguous range within a [`MeshInfo`]'s `indices`, along with the material it should be
/// drawn with. Ranges are local to a single loaded file - a [`crate::common::mesh::MeshRegistry`]
/// rewrites these into offsets within its own shared index buffer.
#[derive(Debug, Clone)]
pub struct Submesh {
/// The index of the material this submesh uses, as defined by the source file (e.g. a glTF
/// material index). `None` if the file did not assign a material to this range.
pub material_index: Option<usize>,
/// The offset of this submesh's first index within `MeshInfo::indices`.
pub index_start: usize,
/// The number of indices that make up this submesh.
pub index_count: usize,
}
pub trait MeshFileLoader {
fn load_mesh(&self, path: &Path) -> Result<MeshInfo>;
}
#[derive(Error, Debug)]
pub enum MeshLoaderError {
#[error("file extension is missing")]
MissingExtension,
#[error("extension {0} is not supported")]
ExtensionNotSupported(String),
}
/// A mesh loader struct that handles multiple different file types. The list of supported and
/// planned file formats follows:
/// - `.gltf`
/// - `.obj`
pub struct MeshLoader {
/// The list of mesh loaders.
loaders: HashMap<String, Box<dyn MeshFileLoader>>,
}
impl MeshLoader {
/// Creates a new `MeshLoader` instance.
pub fn new() -> Self {
let mut loaders = HashMap::new();
loaders.insert(
"gltf".into(),
Box::new(GltfLoader) as Box<dyn MeshFileLoader>,
);
loaders.insert(
"glb".into(),
Box::new(GltfLoader) as Box<dyn MeshFileLoader>,
);
loaders.insert("obj".into(), Box::new(ObjLoader));
Self { loaders }
}
/// Loads a mesh from the given path. Note that materials are not automatically loaded through
/// this function, and must be manually loaded. Refer to the [`MeshLoader`] for more information
/// on what file types are supported.
pub fn load_mesh<P: AsRef<Path>>(&self, path: &P) -> Result<MeshInfo> {
let ext = path
.as_ref()
.extension()
.and_then(|ext| ext.to_str())
.ok_or(MeshLoaderError::MissingExtension)?;
println!("Found extension {}, continuing with loading", ext);
self.loaders
.get(ext)
.ok_or(MeshLoaderError::ExtensionNotSupported(ext.to_string()))?
.load_mesh(path.as_ref())
}
}
#[cfg(test)]
pub mod test_load_meshes {
use super::*;
fn load_gltf(loader: &MeshLoader) -> Result<MeshInfo> {
loader.load_mesh(&Path::new("../../../meshes/laat_gltf/scene.gltf"))
}
fn load_obj(loader: &MeshLoader) -> Result<MeshInfo> {
loader.load_mesh(&Path::new("../../../meshes/laat_gunship/scene.obj"))
}
#[test]
fn compare_obj_gltf() -> Result<()> {
// Create the mesh loader
let loader = MeshLoader::new();
let obj = load_obj(&loader)?;
let gltf = load_gltf(&loader)?;
// Compare mesh information between the two MeshInfo structs.
obj.vertices.iter()
.zip(gltf.vertices.iter())
.for_each(|(&a, &b)| assert_eq!(a, b));
obj.indices.iter()
.zip(gltf.indices.iter())
.for_each(|(&a, &b)| assert_eq!(a, b));
obj.normals.iter()
.zip(gltf.normals.iter())
.for_each(|(&a, &b)| assert_eq!(a, b));
Ok(())
}
}
-3
View File
@@ -1,3 +0,0 @@
pub mod gltf_loader;
pub mod mesh_loader;
mod obj_loader;
+4 -4
View File
@@ -1,4 +1,4 @@
pub mod mesh;
pub mod material;
pub use mesh::*;
pub mod model_loader;
pub mod gltf_loader;
mod obj_loader;
pub mod common;
+49
View File
@@ -0,0 +1,49 @@
use anyhow::Result;
use std::collections::HashMap;
use std::path::Path;
use crate::assets::loaders::common::{MaterialLoadInfo, MeshLoadInfo, TextureLoadInfo};
/// A trait for loading a model from a given path.
pub trait LoadModel {
/// Loads a model from the given path, returning a [`ModelLoadInformation`] if successful.
fn load_model(&self, path: &Path, options: ModelLoadOptions) -> Result<ModelLoadInfo>;
}
/// The options applicable to loading a model.
#[derive(Default, Copy, Clone, Debug)]
pub struct ModelLoadOptions {
/// Whether to load the full mesh.
pub load_mesh: bool,
/// Whether to load all materials from the file.
pub load_materials: bool,
/// Whether to load all textures from the file.
pub load_textures: bool,
}
/// 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.
pub struct ModelLoadInfo {
/// The loaded mesh, if applicable.
pub mesh: Option<MeshLoadInfo>,
/// The list of loaded materials, if applicable.
pub materials: Option<Vec<MaterialLoadInfo>>,
/// The list of loaded textures, if applicable.
pub textures: Option<Vec<TextureLoadInfo>>,
}
/// A model loader struct that handles loading model information for various file types.
pub struct ModelLoader {
/// The list of loaders, keyed by file type extension.
loaders: HashMap<String, Box<dyn LoadModel>>
}
impl ModelLoader {
/// Creates a new `ModelLoader` instance.
pub fn new() -> Self {
let mut loaders = HashMap::new();
Self {
loaders,
}
}
}
@@ -1,11 +1,11 @@
use crate::assets::loaders::mesh_loader::{MeshFileLoader, MeshInfo, Submesh};
use crate::assets::loaders::mesh_loader::{LoadMesh, MeshLoadInfo, Submesh};
use std::path::Path;
use wavefront::Obj;
pub struct ObjLoader;
impl MeshFileLoader for ObjLoader {
fn load_mesh(&self, path: &Path) -> anyhow::Result<MeshInfo> {
impl LoadMesh for ObjLoader {
fn load_mesh(&self, path: &Path) -> anyhow::Result<MeshLoadInfo> {
let obj_info = Obj::from_file(path).expect("Couldn't read wavefront file");
let index_list = obj_info
@@ -25,7 +25,7 @@ impl MeshFileLoader for ObjLoader {
index_count: index_list.len(),
}];
Ok(MeshInfo {
Ok(MeshLoadInfo {
vertices: obj_info.positions().to_vec(),
normals: obj_info.normals().to_vec(),
indices: index_list,
+3 -3
View File
@@ -1,4 +1,4 @@
use crate::assets::loaders::material::mat_loader::MaterialInfo;
use crate::assets::loaders::mat_loader::MaterialLoadInfo;
use std::collections::HashMap;
pub struct MaterialRegistry {
@@ -36,11 +36,11 @@ pub struct Material {
pub base_color_texture: Option<String>,
}
impl From<MaterialInfo> for Material {
impl From<MaterialLoadInfo> for Material {
/// Converts loader output into a storable `Material`. `MaterialInfo::roughness_map` has no
/// equivalent here yet - glTF packs it with the metallic map, and there's no texture loading
/// in place to make use of either.
fn from(info: MaterialInfo) -> Self {
fn from(info: MaterialLoadInfo) -> Self {
Self {
base_color: info.uniforms.base_color,
metallic: info.uniforms.metalness_factor,
+3 -3
View File
@@ -1,4 +1,4 @@
use crate::assets::loaders::mesh_loader::MeshInfo;
use crate::assets::loaders::mesh_loader::MeshLoadInfo;
use crate::gpu::vertex3::Vertex3;
use std::collections::HashMap;
use std::sync::Arc;
@@ -84,7 +84,7 @@ impl MeshRegistry {
/// material has actually been registered under that id yet.
fn append_mesh(
mesh_id: &str,
info: MeshInfo,
info: MeshLoadInfo,
verts: &mut Vec<Vertex3>,
indices: &mut Vec<u32>,
) -> MeshHandle {
@@ -136,7 +136,7 @@ impl MeshRegistry {
/// This is far less efficient than batching a whole scene through [`Self::load_scene`], since
/// every call reallocates and re-uploads the full vertex/index buffers - fine for loading a
/// handful of models up front, not for anything performance-sensitive.
pub fn register_mesh(&mut self, mesh_id: String, info: MeshInfo) {
pub fn register_mesh(&mut self, mesh_id: String, info: MeshLoadInfo) {
let handle = Self::append_mesh(
&mesh_id,
info,
+1
View File
@@ -4,3 +4,4 @@ pub mod material;
pub mod mesh;
pub mod render_batch;
pub mod scene;
pub mod texture;
+33
View File
@@ -0,0 +1,33 @@
use std::collections::HashMap;
use std::sync::Arc;
use vulkano::image::view::ImageView;
use vulkano::image::sampler::Sampler;
/// A texture handle that contains the texture's [`ImageView`] and [`Sampler`].
pub struct TextureHandle {
/// The texture's [`ImageView`].
pub image_view: Arc<ImageView>,
/// The [`Sampler`] associated with the image. This may be a shared sampler,
/// as they do not contain image-specific information.
pub sampler: Arc<Sampler>
}
pub struct TextureRegistry {
textures: HashMap<String, TextureHandle>,
}
impl TextureRegistry {
pub fn new() -> Self {
TextureRegistry {
textures: HashMap::new(),
}
}
pub fn register(&mut self, id: String, texture: TextureHandle) {
self.textures.insert(id, texture);
}
pub fn get(&self, id: &str) -> Option<&TextureHandle> {
self.textures.get(id)
}
}
+9
View File
@@ -0,0 +1,9 @@
use vulkano::buffer::BufferContents;
/// Mirrors the `MaterialPushConstants` block in `triangle_frag.glsl`. Pushed once per submesh
/// draw call, since each submesh may use a different material.
#[derive(BufferContents, Debug, Clone, Copy)]
#[repr(C)]
pub struct MaterialPushConstants {
pub base_color: [f32; 4],
}