feat: implement material loading and rendering

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
alphafetusandClaude Sonnet 5 committed 2026-09-10 12:24:30 -04:00
1 parent 4d9a0c8b43
commit a315cbc543
7 files changed
+137 -67

No files matched your search

+32 -2
View File
@@ -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<Window>,
device: Arc<Device>,
@@ -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,
+29 -8
View File
@@ -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<P: AsRef<Path>>(&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<P: AsRef<Path>>(&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<String, MeshHandle> {
&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)
}
}
+51 -15
View File
@@ -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<Vec<MaterialInfo>> {
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<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();
// 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.
let name = material
.name()
.map(str::to_string)
.unwrap_or_else(|| format!("material_{index}"));
todo!()
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())
}
}
+16 -2
View File
@@ -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<String, Material>
@@ -36,3 +35,18 @@ pub struct Material {
/// The path of the base color's texture, if any.
pub base_color_texture: Option<String>,
}
impl From<MaterialInfo> 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,
}
}
}
+1 -38
View File
@@ -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<dyn MemoryAllocator>) {
// 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::<HashMap<String, MeshInfo>>();
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) {
+1
View File
@@ -1,3 +1,4 @@
pub mod camera;
pub mod instance;
pub mod material;
pub mod vertex3;
+7 -2
View File
@@ -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);