From a315cbc5431e2d3877bfbc70fb0bef3a2b183150 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Thu, 10 Sep 2026 12:24:30 -0400 Subject: [PATCH] feat: implement material loading and rendering Co-authored-by: Claude Sonnet 5 --- src/app.rs | 34 +++++++++- src/assets/asset_manager.rs | 37 +++++++--- .../loaders/material/gltf_mat_loader.rs | 68 ++++++++++++++----- src/common/material.rs | 18 ++++- src/common/mesh.rs | 39 +---------- src/gpu/mod.rs | 1 + src/shaders/triangle_frag.glsl | 9 ++- 7 files changed, 138 insertions(+), 68 deletions(-) diff --git a/src/app.rs b/src/app.rs index b05e711..03c5b0d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,6 +4,7 @@ use crate::common::instance::InstanceRegistry; use crate::common::render_batch::RenderBatch; use crate::common::scene::Scene; use crate::gpu::instance::GpuInstance; +use crate::gpu::material::MaterialPushConstants; use crate::gpu::vertex3::Vertex3; use crate::triangle; use std::error::Error; @@ -48,6 +49,10 @@ use winit::event::WindowEvent; use winit::event_loop::ActiveEventLoop; use winit::window::{Window, WindowAttributes, WindowId}; +/// Used for a submesh whose material couldn't be resolved (no material assigned, or nothing +/// registered under its material ID yet). +const DEFAULT_BASE_COLOR: [f32; 4] = [0.5, 0.5, 0.5, 1.0]; + struct VulkanContext { window: Arc, device: Arc, @@ -472,13 +477,38 @@ impl VulkanContext { .get_mesh(&batch.mesh_id) .expect("Mesh does not exist in registry"); + let mesh_id = &mesh.id; + builder .bind_vertex_buffers(1, batch.instance_buffer.clone()) .unwrap(); - // Issue one draw call per submesh so each can eventually be bound to its own - // material. All submeshes share the mesh's instance buffer. + // Issue one draw call per submesh, pushing its material's base color first so + // each submesh is shaded independently. All submeshes share the mesh's instance + // buffer. Submeshes without a resolved material fall back to a flat gray. for submesh in &mesh.submeshes { + let material_id = submesh.material_id.as_ref(); + let base_color = material_id + .and_then(|id| asset_manager.get_material(id)) + .map_or_else( + || { + 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 + } + ); + + builder + .push_constants( + pipeline.layout().clone(), + 0, + MaterialPushConstants { base_color }, + ) + .unwrap(); + builder .draw_indexed( submesh.index_count as u32, diff --git a/src/assets/asset_manager.rs b/src/assets/asset_manager.rs index b465267..1582d64 100644 --- a/src/assets/asset_manager.rs +++ b/src/assets/asset_manager.rs @@ -6,7 +6,7 @@ use vulkano::buffer::Subbuffer; use vulkano::memory::allocator::MemoryAllocator; use crate::assets::loaders::material::mat_loader::MaterialLoader; use crate::assets::loaders::mesh_loader::MeshLoader; -use crate::common::material::MaterialRegistry; +use crate::common::material::{Material, MaterialRegistry}; use crate::common::mesh::{MeshHandle, MeshRegistry}; use crate::common::scene::Scene; use crate::gpu::vertex3::Vertex3; @@ -50,20 +50,35 @@ impl AssetManager { /// /// This method will attempt to auto-generate IDs for materials and textures if they do not have /// any preconfigured, using the ID of the mesh and the type of resource being stored. - pub fn load_model>(&mut self, mesh_id: String, path: P) -> Result<()> { - // TODO: load this model's materials via `self.material_loader` and register them into - // `self.material_registry` under "{mesh_id}_mat_{index}" ids. `MeshRegistry` already - // derives each submesh's `material_id` using that exact format, so nothing else needs to - // change here once a real material loader exists. + pub fn load_model>(&mut self, mesh_id: &String, path: P) -> Result<()> { + // Submeshes reference materials as "{mesh_id}_mat_{index}" (see + // `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) => { + for (index, info) in materials.into_iter().enumerate() { + let material_id = format!("{mesh_id}_mat_{index}"); + self.material_registry + .register(material_id.clone(), info.into()); + println!("Loaded material {material_id}"); + } + } + Err(err) => println!("No materials loaded for mesh \"{mesh_id}\": {err}"), + } + let mesh = self.mesh_loader.load_mesh(&path)?; - self.mesh_registry.register_mesh(mesh_id, mesh); + self.mesh_registry.register_mesh(mesh_id.clone(), mesh); Ok(()) } /// Loads every mesh referenced by a [`Scene`] and allocates the buffers needed to render them. pub fn load_scene(&mut self, scene: &Scene) -> Result<()> { - self.mesh_registry.load_scene(scene, self.allocator.clone()); + for (id, path) in &scene.mesh_paths { + self.load_model(id, path) + .expect("Couldn't load mesh file"); + } Ok(()) } @@ -97,4 +112,10 @@ impl AssetManager { pub fn meshes(&self) -> &HashMap { &self.mesh_registry.meshes } + + /// Looks up a registered material by its ID (e.g. a [`crate::common::mesh::SubmeshHandle`]'s + /// `material_id`). + pub fn get_material(&self, material_id: &String) -> Option<&Material> { + self.material_registry.get(material_id) + } } diff --git a/src/assets/loaders/material/gltf_mat_loader.rs b/src/assets/loaders/material/gltf_mat_loader.rs index da914ee..814240b 100644 --- a/src/assets/loaders/material/gltf_mat_loader.rs +++ b/src/assets/loaders/material/gltf_mat_loader.rs @@ -1,26 +1,62 @@ -use crate::assets::loaders::material::mat_loader::{MaterialFileLoader, MaterialInfo}; +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 MaterialFileLoader for GltfMaterialLoader { - fn load_materials(&self, path: &Path) -> anyhow::Result> { - let (document, buffers, images) = gltf::import(path)?; - - for material in document.materials() { - +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()), } + } +} - for scene in document.scenes() { - for node in scene.nodes() { - - } - } +impl MaterialFileLoader for GltfMaterialLoader { + fn load_materials(&self, path: &Path) -> anyhow::Result> { + let (document, _buffers, _images) = gltf::import(path)?; - // We just want to read through the information to see what kinds of materials are available. - // I'm not sure how gltf defines materials, especially if they're applied to specific sets - // of vertices. + // 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(); - todo!() + 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()) } } diff --git a/src/common/material.rs b/src/common/material.rs index 71b696c..40649a8 100644 --- a/src/common/material.rs +++ b/src/common/material.rs @@ -1,6 +1,5 @@ +use crate::assets::loaders::material::mat_loader::MaterialInfo; use std::collections::HashMap; -use std::sync::Arc; -use vulkano::memory::allocator::MemoryAllocator; pub struct MaterialRegistry { materials: HashMap @@ -36,3 +35,18 @@ pub struct Material { /// The path of the base color's texture, if any. pub base_color_texture: Option, } + +impl From 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 { + Self { + base_color: info.uniforms.base_color, + metallic: info.uniforms.metalness_factor, + roughness: info.uniforms.roughness_factor, + normal_map: info.normal_map, + base_color_texture: info.base_color_map, + } + } +} diff --git a/src/common/mesh.rs b/src/common/mesh.rs index 9adfa35..816ff4b 100644 --- a/src/common/mesh.rs +++ b/src/common/mesh.rs @@ -1,5 +1,4 @@ -use crate::assets::loaders::mesh_loader::{MeshInfo, MeshLoader}; -use crate::common::scene::Scene; +use crate::assets::loaders::mesh_loader::MeshInfo; use crate::gpu::vertex3::Vertex3; use std::collections::HashMap; use std::sync::Arc; @@ -148,42 +147,6 @@ impl MeshRegistry { self.rebuild_buffers(); } - /// Creates a `MeshRegistry` from the given [`Scene`]. - /// - /// This method will load all the meshes indicated by the scene and allocate the required - /// buffers. This significantly simplifies the allocation process, as the buffers only need to - /// be allocated once. - pub fn load_scene(&mut self, scene: &Scene, allocator: Arc) { - // Load all the meshes into a single map. - println!("Loading meshes from scene"); - self.allocator = allocator; - - let mesh_loader = MeshLoader::new(); - let meshes = scene - .mesh_paths - .iter() - .map(|(id, path)| { - ( - id.clone(), - mesh_loader - .load_mesh(path) - .expect("Couldn't read wavefront file"), - ) - }) - .collect::>(); - - self.raw_vertices.clear(); - self.raw_indices.clear(); - let mut mesh_list = HashMap::new(); - meshes.into_iter().for_each(|(id, info)| { - let handle = Self::append_mesh(&id, info, &mut self.raw_vertices, &mut self.raw_indices); - mesh_list.insert(id, handle); - }); - - self.meshes = mesh_list; - self.rebuild_buffers(); - } - /// (Re)allocates `vertex_buffer`/`index_buffer` from the currently accumulated /// `raw_vertices`/`raw_indices`. fn rebuild_buffers(&mut self) { diff --git a/src/gpu/mod.rs b/src/gpu/mod.rs index 7b47098..018cb8b 100644 --- a/src/gpu/mod.rs +++ b/src/gpu/mod.rs @@ -1,3 +1,4 @@ pub mod camera; pub mod instance; +pub mod material; pub mod vertex3; diff --git a/src/shaders/triangle_frag.glsl b/src/shaders/triangle_frag.glsl index c348e44..a6dc3a2 100644 --- a/src/shaders/triangle_frag.glsl +++ b/src/shaders/triangle_frag.glsl @@ -12,11 +12,16 @@ layout(set = 0, binding = 0) uniform CameraUniform { vec3 position; } camera; +// Per-submesh information - MaterialPushConstants struct +layout(push_constant) uniform MaterialPushConstants { + vec4 base_color; +} material; + layout(location = 0) out vec4 f_color; void main() { - // Default mesh color - no textures for now. - vec3 objectColor = vec3(0.5, 0.5, 0.5); + // No texture sampling yet - just the material's flat base color. + vec3 objectColor = material.base_color.rgb; // Default light information - we aren't going to edit light information until we know more about it. vec3 lightPosition = vec3(-100.0, 200.0, 200.0); vec3 lightColor = vec3(1.0, 1.0, 1.0);