mod error; use ash::vk; pub use error::{Result, SceneError}; use glam::Mat4; use shared::Vertex; use std::{collections::HashMap, path::Path, sync::Arc}; use resource_manager::{Geometry, Material, ResourceManager, SamplerDesc, SamplerHandle, Texture}; /// Represents a drawable entity in the scene, storing geometry with its transform. #[derive(Clone)] pub struct Mesh { pub name: String, pub material: Arc, pub geometry: Arc, pub transform: Mat4, } /// Stores all objects to be rendered by the renderer. pub struct Scene { pub name: String, pub meshes: Vec, } fn sampler_desc_from_gltf(g_sampler: &gltf::texture::Sampler) -> SamplerDesc { let wrap_s = g_sampler.wrap_s(); let wrap_t = g_sampler.wrap_t(); SamplerDesc { mag_filter: g_sampler .mag_filter() .map_or(vk::Filter::LINEAR, |mf| match mf { gltf::texture::MagFilter::Nearest => vk::Filter::NEAREST, gltf::texture::MagFilter::Linear => vk::Filter::LINEAR, }), min_filter: g_sampler .min_filter() .map_or(vk::Filter::LINEAR, |mf| match mf { gltf::texture::MinFilter::Nearest | gltf::texture::MinFilter::NearestMipmapNearest | gltf::texture::MinFilter::NearestMipmapLinear => vk::Filter::NEAREST, gltf::texture::MinFilter::Linear | gltf::texture::MinFilter::LinearMipmapNearest | gltf::texture::MinFilter::LinearMipmapLinear => vk::Filter::LINEAR, }), mipmap_mode: g_sampler .min_filter() .map_or(vk::SamplerMipmapMode::LINEAR, |mf| match mf { gltf::texture::MinFilter::NearestMipmapNearest | gltf::texture::MinFilter::LinearMipmapNearest => vk::SamplerMipmapMode::NEAREST, gltf::texture::MinFilter::NearestMipmapLinear | gltf::texture::MinFilter::LinearMipmapLinear => vk::SamplerMipmapMode::LINEAR, _ => vk::SamplerMipmapMode::LINEAR, // Default if no mipmapping }), address_mode_u: vk_address_mode(wrap_s), address_mode_v: vk_address_mode(wrap_t), address_mode_w: vk::SamplerAddressMode::REPEAT, // glTF doesn't define wrapR } } fn vk_address_mode(g_mode: gltf::texture::WrappingMode) -> vk::SamplerAddressMode { match g_mode { gltf::texture::WrappingMode::ClampToEdge => vk::SamplerAddressMode::CLAMP_TO_EDGE, gltf::texture::WrappingMode::MirroredRepeat => vk::SamplerAddressMode::MIRRORED_REPEAT, gltf::texture::WrappingMode::Repeat => vk::SamplerAddressMode::REPEAT, } } impl Scene { /// Takes a glTF file and returns a `Scene`. pub fn from_gltf(path: T, resource_manager: Arc) -> Result where T: AsRef, { let path_ref = path.as_ref(); let base_path = path_ref.parent().unwrap_or_else(|| Path::new("")); tracing::info!("Loading glTF from: {:?}", path_ref); tracing::info!("Base path for resources: {:?}", base_path); // Import images as well let (doc, buffers, images) = gltf::import(path_ref)?; tracing::info!( "glTF Stats: {} scenes, {} nodes, {} meshes, {} materials, {} textures, {} images", doc.scenes().len(), doc.nodes().len(), doc.meshes().len(), doc.materials().len(), doc.textures().len(), doc.images().len() ); let mut meshes = Vec::new(); // Cache Geometry: Key = (mesh_index, primitive_index) let mut geometry_cache: HashMap<(usize, usize), Arc> = HashMap::new(); // Cache Materials: Key = glTF material index (usize::MAX for default) let mut material_cache: HashMap> = HashMap::new(); // Cache default sampler handle to avoid repeated lookups let default_sampler_handle = resource_manager.get_or_create_sampler(&SamplerDesc::default())?; let scene_to_load = doc .default_scene() .unwrap_or_else(|| doc.scenes().next().expect("No scenes found in glTF")); let scene_name = scene_to_load .name() .unwrap_or("") .to_string(); tracing::info!( "Processing scene '{}' ({})", scene_name, scene_to_load.index() ); // Create a context struct to pass around common data let mut load_ctx = LoadContext { doc: &doc, buffers: &buffers, images: &images, base_path, resource_manager, geometry_cache: &mut geometry_cache, material_cache: &mut material_cache, default_sampler_handle, meshes: &mut meshes, }; for node in scene_to_load.nodes() { Self::process_node(&node, &Mat4::IDENTITY, &mut load_ctx)?; } tracing::info!("Successfully loaded {} render meshes.", meshes.len()); Ok(Self { name: scene_name, meshes, }) } /// Recursively processes a glTF node. fn process_node( node: &gltf::Node, parent_transform: &Mat4, ctx: &mut LoadContext, // Pass context mutably for caches ) -> Result<()> { let local_transform = Mat4::from_cols_array_2d(&node.transform().matrix()); let world_transform = *parent_transform * local_transform; let node_name = node.name().unwrap_or(""); if let Some(mesh) = node.mesh() { let mesh_index = mesh.index(); let mesh_name = mesh.name().unwrap_or(""); tracing::debug!( "Node '{}' ({}) has Mesh '{}' ({})", node_name, node.index(), mesh_name, mesh_index ); // Process mesh primitives for (primitive_index, primitive) in mesh.primitives().enumerate() { // Generate a name for the Mesh object let primitive_name = format!("{}_prim{}", mesh_name, primitive_index); Self::process_primitive( &primitive, mesh_index, primitive_index, &primitive_name, // Pass name world_transform, ctx, // Pass context )?; } } else { tracing::trace!("Node '{}' ({}) has no mesh.", node_name, node.index()); } // Recursively process child nodes for child_node in node.children() { Self::process_node( &child_node, &world_transform, // Pass current world transform ctx, // Pass context )?; } Ok(()) } /// Processes a single glTF primitive, creating Geometry, Material, and Mesh. fn process_primitive( primitive: &gltf::Primitive, mesh_index: usize, primitive_index: usize, mesh_name: &str, // Name for the final Mesh object world_transform: Mat4, ctx: &mut LoadContext, // Use context ) -> Result<()> { let geometry_cache_key = (mesh_index, primitive_index); // --- Get or Create Geometry --- let geometry = if let Some(cached_geo) = ctx.geometry_cache.get(&geometry_cache_key) { tracing::trace!("Using cached Geometry for key {:?}", geometry_cache_key); cached_geo.clone() } else { tracing::trace!("Creating new Geometry for key {:?}", geometry_cache_key); let reader = primitive.reader(|buffer| Some(&ctx.buffers[buffer.index()])); let Some(pos_iter) = reader.read_positions() else { tracing::warn!( "Primitive {:?} missing positions. Skipping.", geometry_cache_key ); return Ok(()); // Skip this primitive }; let positions: Vec<[f32; 3]> = pos_iter.collect(); let vertex_count = positions.len(); if vertex_count == 0 { tracing::warn!( "Primitive {:?} has no vertices. Skipping.", geometry_cache_key ); return Ok(()); } let normals: Vec<[f32; 3]> = reader .read_normals() .map(|iter| iter.collect()) .unwrap_or_else(|| { tracing::debug!( "Primitive {:?} missing normals, using default.", geometry_cache_key ); vec![[0.0, 1.0, 0.0]; vertex_count] }); // Read Texture Coordinates (Set 0) - needed for vertex struct regardless of material let tex_coords: Vec<[f32; 2]> = reader .read_tex_coords(0) // Read UV set 0 .map(|iter| iter.into_f32().collect()) .unwrap_or_else(|| { tracing::trace!( "Primitive {:?} missing tex_coords (set 0), using default.", geometry_cache_key ); vec![[0.0, 0.0]; vertex_count] }); if normals.len() != vertex_count || tex_coords.len() != vertex_count { return Err(SceneError::InconsistentData(format!( "Attribute count mismatch for Primitive {:?} (Pos: {}, Norm: {}, TexCoord0: {}).", geometry_cache_key, vertex_count, normals.len(), tex_coords.len() ))); } let vertices: Vec = positions .into_iter() .zip(normals) .zip(tex_coords) .map(|((pos, normal), tex_coord)| Vertex { pos, normal, tex_coord, }) .collect(); let indices: Vec = reader .read_indices() .map(|read_indices| read_indices.into_u32().collect()) .unwrap_or_else(|| (0..vertex_count as u32).collect()); if indices.is_empty() && vertex_count > 0 { tracing::warn!( "Primitive {:?} has vertices but no indices. Skipping.", geometry_cache_key ); return Ok(()); } let new_geo = Arc::new(Geometry::new( ctx.resource_manager.clone(), &vertices, &indices, )?); ctx.geometry_cache .insert(geometry_cache_key, new_geo.clone()); new_geo }; // --- Get or Create Material --- let g_material = primitive.material(); // Use index usize::MAX as key for default material if index() is None let material_cache_key = g_material.index().unwrap_or(usize::MAX); let material_name = g_material.name().unwrap_or(""); let material = if let Some(cached_mat) = ctx.material_cache.get(&material_cache_key) { tracing::trace!( "Using cached Material index {} ('{}')", material_cache_key, material_name ); cached_mat.clone() } else { tracing::trace!( "Creating new Material for index {} ('{}')", material_cache_key, material_name ); let pbr = g_material.pbr_metallic_roughness(); let base_color_factor = pbr.base_color_factor(); let metallic_factor = pbr.metallic_factor(); let roughness_factor = pbr.roughness_factor(); let mut loaded_base_color_texture: Option> = None; let mut loaded_base_color_sampler: Option = None; // --- Load Base Color Texture (if it exists) --- if let Some(color_info) = pbr.base_color_texture() { let tex_coord_set = color_info.tex_coord(); if tex_coord_set != 0 { tracing::warn!( "Material '{}' requests tex_coord set {}, but only set 0 is currently loaded. Texture ignored.", material_name, tex_coord_set ); // Fall through, texture won't be loaded } else { let g_texture = color_info.texture(); let g_sampler = g_texture.sampler(); let g_image = g_texture.source(); // This is the gltf::Image tracing::debug!( "Material '{}' uses Texture index {}, Sampler index {:?}, Image index {}", material_name, g_texture.index(), g_sampler.index(), g_image.index() ); // Get or create sampler let sampler_desc = sampler_desc_from_gltf(&g_sampler); let sampler_handle = ctx.resource_manager.get_or_create_sampler(&sampler_desc)?; loaded_base_color_sampler = Some(sampler_handle); // Load texture image data via ResourceManager // Pass the correct gltf::Image using its index let texture = ctx.resource_manager.load_texture( &ctx.doc .images() .nth(g_image.index()) .expect("Image index out of bounds"), // Get gltf::Image &g_image.source(), // Get gltf::image::Source ctx.base_path, ctx.buffers, vk::ImageUsageFlags::SAMPLED, // Standard usage for textures )?; loaded_base_color_texture = Some(texture); // Store Arc } } // Assign default sampler if none was loaded via texture info if loaded_base_color_sampler.is_none() { loaded_base_color_sampler = Some(ctx.default_sampler_handle); tracing::trace!("Material '{}' using default sampler.", material_name); } // Create the application Material struct let new_mat = Arc::new(Material { name: material_name.to_string(), base_color_texture: loaded_base_color_texture, base_color_sampler: loaded_base_color_sampler, base_color_factor, metallic_factor, roughness_factor, // Initialize other material properties here... }); ctx.material_cache .insert(material_cache_key, new_mat.clone()); new_mat }; // Create the final Mesh object ctx.meshes.push(Mesh { name: mesh_name.to_string(), geometry, material, // Assign the Arc transform: world_transform, }); Ok(()) } } // Context struct to avoid passing too many arguments struct LoadContext<'a> { doc: &'a gltf::Document, buffers: &'a [gltf::buffer::Data], images: &'a [gltf::image::Data], // Keep image data accessible if needed by RM base_path: &'a Path, resource_manager: Arc, geometry_cache: &'a mut HashMap<(usize, usize), Arc>, material_cache: &'a mut HashMap>, default_sampler_handle: SamplerHandle, // Store the default sampler meshes: &'a mut Vec, // Store results directly }