feat: render and manage textures :3

This commit is contained in:
zack 2025-04-05 20:54:38 -04:00
parent 2501390225
commit 74a1be796f
No known key found for this signature in database
GPG key ID: EE8A2B709E2401D1
21 changed files with 2908 additions and 320 deletions

862
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,9 @@ members = [
"crates/engine",
"crates/gfx_hal",
"crates/renderer",
"crates/resource_manager", "crates/shared",
"crates/resource_manager",
"crates/scene",
"crates/shared",
]
[workspace.dependencies]
@ -24,10 +26,12 @@ egui-ash-renderer = { version = "0.8.0", features = [
"dynamic-rendering",
] }
egui = "0.31"
egui_tiles = "0.12"
bytemuck = { version = "1.21.0", features = ["derive"] }
tracing = "0.1"
tracing = { features = ["release_max_level_warn"], version = "0.1" }
tracing-subscriber = { version = "0.3", features = ["json"] }
thiserror = "2.0.12"
gltf = "1.4.1"
# # Enable incremental by default in release mode.
@ -56,4 +60,7 @@ thiserror = "2.0.12"
#
# rustflags = ["-Zshare-generics=off"]
# codegen-units = 1
opt-level = 1
[profile.dev.package."*"]
opt-level = 3

View file

@ -5,6 +5,7 @@ edition = "2021"
[dependencies]
egui.workspace = true
egui_tiles.workspace = true
ash.workspace = true
ash-window.workspace = true
color-eyre.workspace = true
@ -19,6 +20,7 @@ gfx_hal = { path = "../gfx_hal" }
renderer = { path = "../renderer" }
resource_manager = { path = "../resource_manager" }
shared = { path = "../shared" }
scene = { path = "../scene" }
clap = { version = "4.5.34", features = ["derive"] }
egui-winit = "0.31.1"

View file

@ -17,14 +17,16 @@ use gfx_hal::{
use glam::Vec3;
use raw_window_handle::HasDisplayHandle;
use renderer::{Renderer, RendererError};
use resource_manager::{Geometry, ResourceManager, ResourceManagerError};
use shared::{CameraInfo, Vertex};
use resource_manager::{ResourceManager, ResourceManagerError};
use scene::Scene;
use shared::CameraInfo;
use tracing::{debug, error, info, warn};
use tracing_subscriber::{filter, layer::SubscriberExt, util::SubscriberInitExt, Layer};
use winit::{
application::ApplicationHandler,
event::WindowEvent,
event::{ElementState, KeyEvent, MouseButton, WindowEvent},
event_loop::{ActiveEventLoop, EventLoop},
keyboard::{KeyCode, PhysicalKey},
window::Window,
};
@ -47,6 +49,8 @@ enum AppError {
NoSuitableDevice,
#[error("Failed to create CString: {0}")]
NulError(#[from] std::ffi::NulError),
#[error("Scene Error: {0}")]
SceneError(#[from] scene::SceneError),
}
struct Application {
@ -66,6 +70,23 @@ struct Application {
egui_winit: State,
egui_app: EditorUI,
// --- Camera State ---
camera_info: CameraInfo,
camera_speed: f32,
camera_sensitivity: f32,
// --- Input State ---
is_forward_pressed: bool,
is_backward_pressed: bool,
is_left_pressed: bool,
is_right_pressed: bool,
is_up_pressed: bool, // Optional: For flying up
is_down_pressed: bool, // Optional: For flying down
is_rmb_pressed: bool, // Right mouse button
last_mouse_pos: Option<(f64, f64)>,
mouse_delta: (f64, f64),
capture_mouse: bool, // Flag to indicate if mouse should control camera
// Windowing
window: Arc<Window>, // Use Arc for potential multi-threading later
@ -76,17 +97,15 @@ struct Application {
}
#[derive(Default)]
struct EditorUI {
camera_info: CameraInfo,
}
struct EditorUI {}
impl EditorUI {
fn title() -> String {
"engine".to_string()
}
fn build_ui(&mut self, ctx: &egui::Context, current_fps: f64) {
egui::Window::new(Self::title()).show(ctx, |ui| {
fn build_ui(&mut self, ctx: &egui::Context, current_fps: f64, camera_info: &mut CameraInfo) {
egui::SidePanel::new(egui::panel::Side::Left, Self::title()).show(ctx, |ui| {
ui.label(format!("FPS - {:.2}", current_fps));
ui.separator();
@ -96,12 +115,78 @@ impl EditorUI {
.striped(true)
.show(ui, |ui| {
ui.label("FOV");
ui.add(Slider::new(&mut self.camera_info.camera_fov, 10.0..=120.0));
// Modify the passed-in camera_info
ui.add(Slider::new(&mut camera_info.camera_fov, 10.0..=120.0));
ui.end_row(); // Good practice in grids
// You could add more camera controls here if needed
// e.g., sliders for position, target (though direct manipulation is better)
ui.label("Camera Pos");
ui.label(format!(
"({:.1}, {:.1}, {:.1})",
camera_info.camera_pos.x,
camera_info.camera_pos.y,
camera_info.camera_pos.z
));
ui.end_row();
ui.label("Camera Target");
ui.label(format!(
"({:.1}, {:.1}, {:.1})",
camera_info.camera_target.x,
camera_info.camera_target.y,
camera_info.camera_target.z
));
ui.end_row();
});
ui.separator();
ui.label("Controls:");
ui.label("RMB + Drag: Look");
ui.label("WASD: Move");
ui.label("Space: Up");
ui.label("Shift: Down");
ui.label("Hold RMB to activate controls.");
});
// let mut tree = create_tree();
//
// egui::panel::SidePanel::new(egui::panel::Side::Left, Id::new("main_panel")).show(
// ctx,
// |ui| {
// let mut behavior = TreeBehavior {};
// tree.ui(&mut behavior, ui);
// },
// );
}
}
fn create_tree() -> egui_tiles::Tree<EditorUI> {
let mut next_view_nr = 0;
let mut gen_pane = || {
let pane = EditorUI {};
next_view_nr += 1;
pane
};
let mut tiles = egui_tiles::Tiles::default();
let mut tabs = vec![];
tabs.push({
let children = (0..7).map(|_| tiles.insert_pane(gen_pane())).collect();
tiles.insert_horizontal_tile(children)
});
tabs.push({
let cells = (0..11).map(|_| tiles.insert_pane(gen_pane())).collect();
tiles.insert_grid_tile(cells)
});
tabs.push(tiles.insert_pane(gen_pane()));
let root = tiles.insert_tab_tile(tabs);
egui_tiles::Tree::new("my_tree", root, tiles)
}
#[derive(Default)]
struct ApplicationWrapper {
app: Option<Application>,
@ -264,56 +349,10 @@ impl Application {
let resource_manager = Arc::new(ResourceManager::new(instance.clone(), device.clone())?);
info!("Resource Manager initialized.");
let vertices = vec![
// Define 8 vertices for a cube with positions and colors
// Make sure winding order is correct (e.g., counter-clockwise for front faces)
Vertex {
pos: Vec3::new(-0.5, -0.5, 0.5).into(),
color: Vec3::new(1.0, 0.0, 0.0).into(),
}, // Front bottom left 0
Vertex {
pos: Vec3::new(0.5, -0.5, 0.5).into(),
color: Vec3::new(0.0, 1.0, 0.0).into(),
}, // Front bottom right 1
Vertex {
pos: Vec3::new(0.5, 0.5, 0.5).into(),
color: Vec3::new(0.0, 0.0, 1.0).into(),
}, // Front top right 2
Vertex {
pos: Vec3::new(-0.5, 0.5, 0.5).into(),
color: Vec3::new(1.0, 1.0, 0.0).into(),
}, // Front top left 3
// ... add back face vertices (4-7) ...
Vertex {
pos: Vec3::new(-0.5, -0.5, -0.5).into(),
color: Vec3::new(1.0, 0.0, 1.0).into(),
}, // Back bottom left 4
Vertex {
pos: Vec3::new(0.5, -0.5, -0.5).into(),
color: Vec3::new(0.0, 1.0, 1.0).into(),
}, // Back bottom right 5
Vertex {
pos: Vec3::new(0.5, 0.5, -0.5).into(),
color: Vec3::new(0.5, 0.5, 0.5).into(),
}, // Back top right 6
Vertex {
pos: Vec3::new(-0.5, 0.5, -0.5).into(),
color: Vec3::new(1.0, 1.0, 1.0).into(),
}, // Back top left 7
];
let indices = vec![
// Define 12 triangles (36 indices) for the cube faces
// Front face
0, 1, 2, 2, 3, 0, // Right face
1, 5, 6, 6, 2, 1, // Back face
5, 4, 7, 7, 6, 5, // Left face
4, 0, 3, 3, 7, 4, // Top face
3, 2, 6, 6, 7, 3, // Bottom face
4, 5, 1, 1, 0, 4,
];
let cube_geometry = Geometry::new(resource_manager.clone(), &vertices, &indices)?;
let scene = Scene::from_gltf(
"./sponza/NewSponza_Main_glTF_003.gltf",
resource_manager.clone(),
)?;
// --- 5. Renderer ---
let initial_size = window.inner_size();
@ -323,7 +362,7 @@ impl Application {
graphics_queue.clone(),
surface.clone(),
resource_manager.clone(),
vec![cube_geometry],
scene,
initial_size.width,
initial_size.height,
)?;
@ -341,6 +380,8 @@ impl Application {
info!("Renderer initialized.");
let camera_info = CameraInfo::default(); // Get default camera settings
Ok(Self {
_instance: instance,
_physical_device: physical_device,
@ -353,6 +394,23 @@ impl Application {
egui_winit,
egui_ctx,
egui_app,
// --- Camera ---
camera_info, // Store the camera state here
camera_speed: 5.0, // Adjust as needed
camera_sensitivity: 0.002, // Adjust as needed
// --- Input ---
is_forward_pressed: false,
is_backward_pressed: false,
is_left_pressed: false,
is_right_pressed: false,
is_up_pressed: false,
is_down_pressed: false,
is_rmb_pressed: false,
last_mouse_pos: None,
mouse_delta: (0.0, 0.0),
capture_mouse: false, // Start with mouse free
frame_count: 0,
current_fps: 0.,
last_fps_update_time: Instant::now(),
@ -361,7 +419,11 @@ impl Application {
}
fn handle_event(&mut self, event: &WindowEvent, active_event_loop: &ActiveEventLoop) {
let _ = self.egui_winit.on_window_event(&self.window, event);
// Let egui process the event first
let egui_consumed_event = self.egui_winit.on_window_event(&self.window, event);
// Only process input for camera if egui didn't consume it AND we are capturing
let process_camera_input = !egui_consumed_event.consumed && self.capture_mouse;
match event {
WindowEvent::CloseRequested => {
@ -385,29 +447,127 @@ impl Application {
.resize(new_inner_size.width, new_inner_size.height);
}
// Handle other inputs if not consumed by egui
WindowEvent::KeyboardInput { .. }
| WindowEvent::CursorMoved { .. }
| WindowEvent::MouseInput { .. } => {}
WindowEvent::MouseInput { state, button, .. } => {
if *button == MouseButton::Right {
let is_pressed = *state == ElementState::Pressed;
self.is_rmb_pressed = is_pressed;
// Decide whether to capture/release mouse based on RMB
// Only capture if pressed *outside* an egui interactive area
if is_pressed && !self.egui_ctx.is_pointer_over_area() {
self.capture_mouse = true;
self.window
.set_cursor_grab(winit::window::CursorGrabMode::Confined)
.or_else(|_| {
self.window
.set_cursor_grab(winit::window::CursorGrabMode::Locked)
})
.unwrap_or(());
self.window.set_cursor_visible(false);
self.last_mouse_pos = None; // Reset last pos on capture start
} else if !is_pressed {
self.capture_mouse = false;
self.window
.set_cursor_grab(winit::window::CursorGrabMode::None)
.unwrap_or(());
self.window.set_cursor_visible(true);
self.mouse_delta = (0.0, 0.0); // Stop camera movement
}
}
// Let egui handle its mouse clicks regardless of capture state
// (handled by on_window_event)
}
WindowEvent::CursorMoved { position, .. } => {
let current_pos = (position.x, position.y);
if self.capture_mouse {
// Only calculate delta if capturing
if let Some(last_pos) = self.last_mouse_pos {
self.mouse_delta.0 += current_pos.0 - last_pos.0;
self.mouse_delta.1 += current_pos.1 - last_pos.1;
}
// Store position relative to window center might be more robust
// with set_cursor_position, but this works with grab/confine too.
self.last_mouse_pos = Some(current_pos);
} else {
// Still update egui's pointer position even if not capturing
// (handled by on_window_event)
self.last_mouse_pos = None; // Reset if not capturing
}
}
// Use PhysicalKey for layout-independent keys
WindowEvent::KeyboardInput {
event:
KeyEvent {
physical_key,
state,
..
},
..
} => {
// Let egui handle keyboard input first if it wants it
if egui_consumed_event.consumed {
return;
}
let is_pressed = *state == ElementState::Pressed;
match physical_key {
PhysicalKey::Code(KeyCode::KeyW) | PhysicalKey::Code(KeyCode::ArrowUp) => {
self.is_forward_pressed = is_pressed;
}
PhysicalKey::Code(KeyCode::KeyS) | PhysicalKey::Code(KeyCode::ArrowDown) => {
self.is_backward_pressed = is_pressed;
}
PhysicalKey::Code(KeyCode::KeyA) | PhysicalKey::Code(KeyCode::ArrowLeft) => {
self.is_left_pressed = is_pressed;
}
PhysicalKey::Code(KeyCode::KeyD) | PhysicalKey::Code(KeyCode::ArrowRight) => {
self.is_right_pressed = is_pressed;
}
PhysicalKey::Code(KeyCode::Space) => {
self.is_up_pressed = is_pressed;
}
PhysicalKey::Code(KeyCode::ShiftLeft)
| PhysicalKey::Code(KeyCode::ShiftRight) => {
self.is_down_pressed = is_pressed;
}
// Optional: Escape to release mouse capture
PhysicalKey::Code(KeyCode::Escape) if is_pressed && self.capture_mouse => {
self.capture_mouse = false;
self.is_rmb_pressed = false; // Ensure RMB state is also reset
self.window
.set_cursor_grab(winit::window::CursorGrabMode::None)
.unwrap_or(());
self.window.set_cursor_visible(true);
self.mouse_delta = (0.0, 0.0);
}
_ => {}
}
}
WindowEvent::RedrawRequested => {
let now = Instant::now();
let _delta_time = now.duration_since(self.last_frame_time);
let delta_time = now.duration_since(self.last_frame_time).as_secs_f32();
self.last_frame_time = now;
let elapsed_sice_last_update = now.duration_since(self.last_fps_update_time);
// --- FPS Calculation ---
let elapsed_since_last_update = now.duration_since(self.last_fps_update_time);
self.frame_count += 1;
if elapsed_sice_last_update >= Duration::from_secs(1) {
let fps = self.frame_count as f64 / elapsed_sice_last_update.as_secs_f64();
self.current_fps = fps;
let new_title = format!("{} - {} - {:.0} FPS", ENGINE_NAME, APP_NAME, fps);
if elapsed_since_last_update >= Duration::from_secs(1) {
self.current_fps =
self.frame_count as f64 / elapsed_since_last_update.as_secs_f64();
let new_title = format!(
"{} - {} - {:.0} FPS",
ENGINE_NAME, APP_NAME, self.current_fps
);
self.window.set_title(&new_title);
self.frame_count = 0;
self.last_fps_update_time = now;
}
self.update_camera(delta_time); // Call the new update function
let raw_input = self.egui_winit.take_egui_input(&self.window);
let egui::FullOutput {
@ -417,7 +577,8 @@ impl Application {
pixels_per_point,
..
} = self.egui_ctx.run(raw_input, |ctx| {
self.egui_app.build_ui(ctx, self.current_fps);
self.egui_app
.build_ui(ctx, self.current_fps, &mut self.camera_info);
});
self.renderer.update_textures(textures_delta).unwrap();
@ -431,7 +592,7 @@ impl Application {
match self.renderer.render_frame(
pixels_per_point,
&clipped_primitives,
self.egui_app.camera_info,
self.camera_info,
) {
Ok(_) => {
self.window.request_redraw();
@ -454,6 +615,106 @@ impl Application {
_ => {}
}
}
// --- New Camera Update Function ---
fn update_camera(&mut self, dt: f32) {
if !self.capture_mouse
&& self.mouse_delta == (0.0, 0.0)
&& !self.is_forward_pressed
&& !self.is_backward_pressed
&& !self.is_left_pressed
&& !self.is_right_pressed
&& !self.is_up_pressed
&& !self.is_down_pressed
{
return; // No input, no update needed
}
let mut cam_pos = self.camera_info.camera_pos;
let mut cam_target = self.camera_info.camera_target;
let cam_up = self.camera_info.camera_up; // Usually Vec3::Y
// --- Mouse Look (Rotation) ---
if self.capture_mouse && self.mouse_delta != (0.0, 0.0) {
let (delta_x, delta_y) = self.mouse_delta;
self.mouse_delta = (0.0, 0.0); // Consume the delta
let sensitivity = self.camera_sensitivity;
let yaw_delta = delta_x as f32 * sensitivity;
let pitch_delta = delta_y as f32 * sensitivity;
let forward_dir = (cam_target - cam_pos).normalize();
let right_dir = forward_dir.cross(cam_up).normalize();
// Recalculate up to prevent roll if needed, though cross product handles it here
let current_up = right_dir.cross(forward_dir).normalize();
// --- Pitch (Up/Down) ---
// Calculate new forward direction based on pitch rotation around right axis
let pitch_quat = glam::Quat::from_axis_angle(right_dir, -pitch_delta); // Negative for standard mouse look
let mut new_forward = pitch_quat * forward_dir;
// Clamp pitch to avoid flipping over (e.g., +/- 89 degrees)
let max_pitch_angle = 89.0f32.to_radians();
let current_pitch = new_forward.angle_between(cam_up) - 90.0f32.to_radians();
if current_pitch.abs() > max_pitch_angle {
// Revert pitch if it exceeds limits
new_forward = forward_dir; // Keep previous forward if clamp needed
}
// --- Yaw (Left/Right) ---
// Rotate the (potentially pitch-adjusted) forward direction and right vector around the global up axis (Y)
let yaw_quat = glam::Quat::from_axis_angle(Vec3::Y, -yaw_delta); // Negative for standard mouse look
new_forward = yaw_quat * new_forward;
// Update target based on the new forward direction
cam_target = cam_pos + new_forward;
// Update the camera's internal up vector based on yaw rotation as well
// This prevents weird tilting when looking straight up/down if up wasn't Vec3::Y
// self.camera_info.camera_up = yaw_quat * current_up; // Optional: only if up can change
}
// --- Keyboard Movement ---
let forward_dir = (cam_target - cam_pos).normalize();
// Use Vec3::Y for world-relative right/up movement, or calculate from forward/up
let right_dir = forward_dir.cross(Vec3::Y).normalize();
// let up_dir = right_dir.cross(forward_dir).normalize(); // Camera's local up
let world_up_dir = Vec3::Y; // Use world up for space/shift
let effective_speed = self.camera_speed * dt;
let mut move_delta = Vec3::ZERO;
if self.is_forward_pressed {
move_delta += forward_dir;
}
if self.is_backward_pressed {
move_delta -= forward_dir;
}
if self.is_left_pressed {
move_delta -= right_dir;
}
if self.is_right_pressed {
move_delta += right_dir;
}
if self.is_up_pressed {
move_delta += world_up_dir; // Move along world Y
}
if self.is_down_pressed {
move_delta -= world_up_dir; // Move along world Y
}
// Normalize move_delta if non-zero to ensure consistent speed diagonally
if move_delta != Vec3::ZERO {
let move_vec = move_delta.normalize() * effective_speed;
cam_pos += move_vec;
cam_target += move_vec; // Move target along with position
}
// --- Apply Changes ---
self.camera_info.camera_pos = cam_pos;
self.camera_info.camera_target = cam_target;
// self.camera_info.camera_up remains Vec3::Y usually
}
}
// --- Helper Functions ---

View file

@ -17,6 +17,7 @@ winit.workspace = true
gfx_hal = { path = "../gfx_hal" }
resource_manager = { path = "../resource_manager" }
shared = { path = "../shared" }
scene = { path = "../scene" }
[build-dependencies]
shaderc = "0.9.1"

View file

@ -1,4 +1,5 @@
use std::{
collections::HashMap,
ffi::c_void,
mem,
sync::{Arc, Mutex},
@ -17,12 +18,15 @@ use gpu_allocator::{
vulkan::{Allocation, AllocationCreateDesc, Allocator},
MemoryLocation,
};
use resource_manager::{Geometry, ImageHandle, ResourceManager, ResourceManagerError};
use resource_manager::{
ImageHandle, Material, ResourceManager, ResourceManagerError, SamplerHandle, Texture,
};
use shared::{CameraInfo, UniformBufferObject};
use thiserror::Error;
use tracing::{debug, error, info, warn};
const MAX_FRAMES_IN_FLIGHT: usize = 2;
const MAX_MATERIALS: usize = 150;
#[derive(Debug, Error)]
pub enum RendererError {
@ -60,6 +64,9 @@ pub enum RendererError {
AllocatorUnavailable, // Added based on egui requirement
#[error("Allocator Error: {0}")]
AllocatorError(#[from] gpu_allocator::AllocationError),
#[error("Other Error: {0}")]
Other(String),
}
impl<T> From<std::sync::PoisonError<T>> for RendererError {
@ -101,11 +108,13 @@ pub struct Renderer {
swapchain_format: vk::SurfaceFormatKHR,
swapchain_extent: vk::Extent2D,
scene: Vec<Geometry>,
scene: scene::Scene,
descriptor_set_layout: vk::DescriptorSetLayout,
descriptor_pool: vk::DescriptorPool,
material_descriptor_set_layout: vk::DescriptorSetLayout,
egui_renderer: EguiRenderer,
depth_image_handle: ImageHandle,
@ -115,6 +124,11 @@ pub struct Renderer {
model_pipeline_layout: vk::PipelineLayout,
model_pipeline: vk::Pipeline,
material_descriptor_sets: HashMap<usize, vk::DescriptorSet>,
default_white_texture: Option<Arc<Texture>>,
default_sampler: SamplerHandle,
frames_data: Vec<FrameData>,
current_frame: usize,
@ -134,7 +148,7 @@ impl Renderer {
graphics_queue: Arc<Queue>,
surface: Arc<Surface>,
resource_manager: Arc<ResourceManager>,
scene: Vec<Geometry>,
scene: scene::Scene,
initial_width: u32,
initial_height: u32,
) -> Result<Self, RendererError> {
@ -154,14 +168,18 @@ impl Renderer {
let (depth_image_handle, depth_image_view) =
Self::create_depth_resources(&device, &resource_manager, extent, depth_format)?;
let (descriptor_set_layout, descriptor_pool) =
Self::create_descriptor_sets_resources(&device)?;
let descriptor_set_layout = Self::create_descriptor_set_layout(&device)?;
let material_descriptor_set_layout = Self::create_material_descriptor_set_layout(&device)?;
let descriptor_set_layouts = [descriptor_set_layout, material_descriptor_set_layout];
let descriptor_pool = Self::create_descriptor_pool(&device)?;
let (model_pipeline_layout, model_pipeline) = Self::create_model_pipeline(
&device,
format.format,
depth_format,
descriptor_set_layout,
&descriptor_set_layouts,
)?;
let start_time = Instant::now();
@ -170,9 +188,8 @@ impl Renderer {
&device,
&resource_manager,
descriptor_pool,
descriptor_set_layout,
&descriptor_set_layouts,
swapchain.extent(),
start_time,
)?;
info!("Renderer initialized successfully.");
@ -191,6 +208,13 @@ impl Renderer {
},
)?;
let default_sampler = resource_manager.get_or_create_sampler(&Default::default())?;
let default_white_texture = Some(Self::create_default_texture(
device.clone(),
resource_manager.clone(),
));
Ok(Self {
device,
graphics_queue,
@ -204,11 +228,19 @@ impl Renderer {
swapchain_extent: extent,
descriptor_set_layout,
descriptor_pool,
material_descriptor_set_layout,
depth_image_handle,
depth_image_view,
depth_format,
model_pipeline_layout,
model_pipeline,
material_descriptor_sets: HashMap::new(),
default_white_texture,
default_sampler,
frames_data,
scene,
current_frame: 0,
@ -220,6 +252,134 @@ impl Renderer {
})
}
/// Gets or creates/updates a descriptor set for a given material.
fn get_or_create_material_set(
&mut self,
material: &Arc<Material>, // Use Arc<Material> directly if hashable, or use a unique ID
) -> Result<vk::DescriptorSet, RendererError> {
// Return generic error
// Use a unique identifier for the material instance if Arc<Material> isn't directly hashable
// or if pointer comparison isn't reliable across runs/reloads.
// For simplicity here, we use the Arc's pointer address as a key.
// WARNING: This is only safe if the Arc<Material> instances are stable!
// A better key might be derived from material.name or a generated ID.
let material_key = Arc::as_ptr(material) as usize;
if let Some(set) = self.material_descriptor_sets.get(&material_key) {
return Ok(*set);
}
// --- Allocate Descriptor Set ---
let layouts = [self.material_descriptor_set_layout];
let alloc_info = vk::DescriptorSetAllocateInfo::default()
.descriptor_pool(self.descriptor_pool)
.set_layouts(&layouts);
let descriptor_set = unsafe { self.device.raw().allocate_descriptor_sets(&alloc_info)? }[0];
// --- Update Descriptor Set ---
let (image_handle, view_handle, sampler_handle) = match &material.base_color_texture {
Some(texture) => {
// Get the default view handle associated with the image
let img_info = self.resource_manager.get_image_info(texture.handle)?;
let view_h = img_info.default_view_handle.ok_or(RendererError::Other(
"Image missing default view handle".to_string(),
))?;
// Use the sampler specified by the material, or the default
let sampler_h = material.base_color_sampler.unwrap_or(self.default_sampler);
(texture.handle, view_h, sampler_h)
}
None => {
// Use default white texture
let default_tex =
self.default_white_texture
.as_ref()
.ok_or(RendererError::Other(
"Default texture not created".to_string(),
))?;
let img_info = self.resource_manager.get_image_info(default_tex.handle)?;
let view_h = img_info.default_view_handle.ok_or(RendererError::Other(
"Default image missing default view handle".to_string(),
))?;
(default_tex.handle, view_h, self.default_sampler)
}
};
// Get the actual Vulkan handles
let image_view_info = self.resource_manager.get_image_view_info(view_handle)?;
let sampler_info = self.resource_manager.get_sampler_info(sampler_handle)?;
let image_descriptor_info = vk::DescriptorImageInfo::default()
.image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL) // Expected layout for sampling
.image_view(image_view_info.view) // The vk::ImageView
.sampler(sampler_info.sampler); // The vk::Sampler
let writes = [
// Write for binding 0 (baseColorSampler)
vk::WriteDescriptorSet::default()
.dst_set(descriptor_set)
.dst_binding(0)
.dst_array_element(0)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.image_info(std::slice::from_ref(&image_descriptor_info)),
// Add writes for other bindings (normal map, etc.) here
];
unsafe {
self.device.raw().update_descriptor_sets(&writes, &[]); // Update the set
}
// Store in cache
self.material_descriptor_sets
.insert(material_key, descriptor_set);
Ok(descriptor_set)
}
fn create_default_texture(
device: Arc<Device>, // Need device Arc for RM
resource_manager: Arc<ResourceManager>,
) -> Arc<Texture> {
let width = 1;
let height = 1;
let data = [255u8, 255, 255, 255]; // White RGBA
let format = vk::Format::R8G8B8A8_UNORM; // Or SRGB if preferred
let create_info = vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.format(format)
.extent(vk::Extent3D {
width,
height,
depth: 1,
})
.mip_levels(1)
.array_layers(1)
.samples(vk::SampleCountFlags::TYPE_1)
.tiling(vk::ImageTiling::OPTIMAL)
.usage(vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST)
.initial_layout(vk::ImageLayout::UNDEFINED);
let handle = resource_manager
.create_image_init(
&create_info,
gpu_allocator::MemoryLocation::GpuOnly,
vk::ImageAspectFlags::COLOR,
&data,
)
.expect("Failed to create default white texture");
Arc::new(Texture {
handle,
format: vk::Format::R8G8B8A8_UNORM,
extent: vk::Extent3D {
width: 1,
height: 1,
depth: 1,
},
})
}
pub fn resize(&mut self, width: u32, height: u32) {
if width > 0 && height > 0 {
self.window_resized = true;
@ -425,9 +585,7 @@ impl Renderer {
.cmd_set_scissor(command_buffer, 0, &[scissor]);
}
// --- Draw Triangle ---
unsafe {
// Need unsafe for Vulkan commands
self.device.raw().cmd_bind_pipeline(
command_buffer,
vk::PipelineBindPoint::GRAPHICS,
@ -444,10 +602,48 @@ impl Renderer {
);
}
for g in &self.scene {
g.draw(self.device.raw(), command_buffer)?;
let meshes = self.scene.meshes.clone();
for mesh in meshes {
let material_set = self.get_or_create_material_set(&mesh.material)?;
unsafe {
self.device.raw().cmd_bind_descriptor_sets(
command_buffer,
vk::PipelineBindPoint::GRAPHICS,
self.model_pipeline_layout,
1,
&[material_set],
&[],
);
}
let model_matrix_bytes = unsafe {
std::slice::from_raw_parts(
mesh.transform.as_ref().as_ptr() as *const u8,
std::mem::size_of::<Mat4>(),
)
};
unsafe {
self.device.raw().cmd_push_constants(
command_buffer,
self.model_pipeline_layout,
vk::ShaderStageFlags::VERTEX,
0,
model_matrix_bytes,
);
}
mesh.geometry.draw(self.device.raw(), command_buffer)?;
}
let frame_data = &mut self.frames_data[self.current_frame];
let swapchain_ref = self
.swapchain
.as_ref()
.ok_or(RendererError::SwapchainAcquisitionFailed)?;
tracing::trace!("Rendering EGUI");
self.egui_renderer.cmd_draw(
command_buffer,
@ -716,7 +912,7 @@ impl Renderer {
device: &Arc<Device>,
color_format: vk::Format,
depth_format: vk::Format,
descriptor_set_layout: vk::DescriptorSetLayout,
descriptor_set_layouts: &[vk::DescriptorSetLayout],
) -> Result<(vk::PipelineLayout, vk::Pipeline), RendererError> {
// Load compiled SPIR-V (replace with actual loading)
let vert_shader_code = include_bytes!(concat!(env!("OUT_DIR"), "/shaders/vert.glsl.spv")); // Placeholder path
@ -787,9 +983,15 @@ impl Renderer {
let dynamic_state =
vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states);
let push_constant_range = vk::PushConstantRange::default()
.stage_flags(vk::ShaderStageFlags::VERTEX)
.offset(0)
.size(mem::size_of::<Mat4>() as u32);
// --- Pipeline Layout ---
let layout_info = vk::PipelineLayoutCreateInfo::default()
.set_layouts(std::slice::from_ref(&descriptor_set_layout)); // No descriptors/push constants
.set_layouts(descriptor_set_layouts)
.push_constant_ranges(std::slice::from_ref(&push_constant_range));
let pipeline_layout = unsafe {
device
.raw()
@ -891,9 +1093,8 @@ impl Renderer {
device: &Arc<Device>,
resource_manager: &Arc<ResourceManager>,
descriptor_pool: vk::DescriptorPool,
descriptor_set_layout: vk::DescriptorSetLayout,
descriptor_set_layouts: &[vk::DescriptorSetLayout],
swapchain_extent: vk::Extent2D,
instant: Instant,
) -> Result<Vec<FrameData>, RendererError> {
let mut frames_data = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT);
for _ in 0..MAX_FRAMES_IN_FLIGHT {
@ -930,15 +1131,14 @@ impl Renderer {
tracing::info!("Allocated frame_data command_buffer: {:?}", command_buffer);
let descriptor_set =
Self::create_descriptor_set(device, descriptor_set_layout, descriptor_pool)?;
Self::create_descriptor_set(device, descriptor_set_layouts, descriptor_pool)?;
let (uniform_buffer, uniform_buffer_allocation, uniform_buffer_mapped_ptr) =
Self::create_uniform_buffer(device, resource_manager)?;
Self::update_descriptor_set(device.clone(), descriptor_set, uniform_buffer);
let uniform_buffer_object =
calculate_ubo(CameraInfo::default(), swapchain_extent, instant);
let uniform_buffer_object = calculate_ubo(CameraInfo::default(), swapchain_extent);
frames_data.push(FrameData {
textures_to_free: None,
@ -1043,9 +1243,37 @@ impl Renderer {
))
}
fn create_descriptor_sets_resources(
fn create_material_descriptor_set_layout(
device: &Arc<Device>,
) -> Result<(vk::DescriptorSetLayout, vk::DescriptorPool), RendererError> {
) -> Result<vk::DescriptorSetLayout, RendererError> {
let bindings = [
// Binding 0: Combined Image Sampler (baseColorSampler)
vk::DescriptorSetLayoutBinding::default()
.binding(0)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.descriptor_count(1)
.stage_flags(vk::ShaderStageFlags::FRAGMENT), // Used in fragment shader
// Add more bindings here if needed (e.g., for normal map, metallic/roughness map)
// Binding 1: Uniform Buffer (Optional: for material factors)
// vk::DescriptorSetLayoutBinding::default()
// .binding(1)
// .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
// .descriptor_count(1)
// .stage_flags(vk::ShaderStageFlags::FRAGMENT),
];
let layout_info = vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings);
Ok(unsafe {
device
.raw()
.create_descriptor_set_layout(&layout_info, None)?
})
}
fn create_descriptor_set_layout(
device: &Arc<Device>,
) -> Result<vk::DescriptorSetLayout, RendererError> {
let ubo_layout_binding = vk::DescriptorSetLayoutBinding::default()
.binding(0)
.descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
@ -1061,29 +1289,38 @@ impl Renderer {
.create_descriptor_set_layout(&layout_info, None)?
};
let pool_size = vk::DescriptorPoolSize {
Ok(descriptor_set_layout)
}
fn create_descriptor_pool(device: &Arc<Device>) -> Result<vk::DescriptorPool, RendererError> {
let pool_sizes = [
vk::DescriptorPoolSize {
ty: vk::DescriptorType::UNIFORM_BUFFER,
descriptor_count: MAX_FRAMES_IN_FLIGHT as u32,
};
},
vk::DescriptorPoolSize {
ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
descriptor_count: MAX_MATERIALS as u32,
},
];
let pool_info = vk::DescriptorPoolCreateInfo::default()
.pool_sizes(std::slice::from_ref(&pool_size))
.max_sets(MAX_FRAMES_IN_FLIGHT as u32);
.pool_sizes(&pool_sizes)
.max_sets(MAX_FRAMES_IN_FLIGHT as u32 + MAX_MATERIALS as u32);
let descriptor_pool = unsafe { device.raw().create_descriptor_pool(&pool_info, None)? };
Ok((descriptor_set_layout, descriptor_pool))
Ok(descriptor_pool)
}
fn create_descriptor_set(
device: &Arc<Device>,
descriptor_set_layout: vk::DescriptorSetLayout,
descriptor_set_layouts: &[vk::DescriptorSetLayout],
descriptor_pool: vk::DescriptorPool,
) -> Result<vk::DescriptorSet, RendererError> {
let layouts = vec![descriptor_set_layout; 1];
let alloc_info = vk::DescriptorSetAllocateInfo::default()
.descriptor_pool(descriptor_pool)
.set_layouts(&layouts);
.set_layouts(descriptor_set_layouts);
let descriptor_set = unsafe { device.raw().allocate_descriptor_sets(&alloc_info)? }[0];
@ -1167,7 +1404,7 @@ impl Renderer {
fn update_uniform_buffer(&mut self, camera_info: CameraInfo) -> Result<(), RendererError> {
let frame_data = &mut self.frames_data[self.current_frame];
let ubo = calculate_ubo(camera_info, self.swapchain_extent, self.start_time);
let ubo = calculate_ubo(camera_info, self.swapchain_extent);
if frame_data.uniform_buffer_object != ubo {
let ptr = frame_data.uniform_buffer_mapped_ptr;
@ -1181,15 +1418,7 @@ impl Renderer {
}
}
fn calculate_ubo(
camera_info: CameraInfo,
swapchain_extent: vk::Extent2D,
start: Instant,
) -> UniformBufferObject {
let time = start.elapsed();
let model = Mat4::from_rotation_y(time.as_secs_f32());
fn calculate_ubo(camera_info: CameraInfo, swapchain_extent: vk::Extent2D) -> UniformBufferObject {
let view = Mat4::look_at_rh(camera_info.camera_pos, camera_info.camera_target, Vec3::Y);
let mut proj = Mat4::perspective_rh(
@ -1201,7 +1430,7 @@ fn calculate_ubo(
proj.y_axis.y *= -1.0;
UniformBufferObject { model, view, proj }
UniformBufferObject { view, proj }
}
// --- Drop Implementation ---

View file

@ -8,5 +8,7 @@ ash.workspace = true
gpu-allocator.workspace = true
thiserror.workspace = true
tracing.workspace = true
gltf.workspace = true
gfx_hal = { path = "../gfx_hal" }
image = { version = "0.25.6", features = ["rayon"] }

View file

@ -34,6 +34,12 @@ pub enum ResourceManagerError {
#[error("Error occurred in GfxHal: {0}")]
GfxHalError(#[from] gfx_hal::error::GfxHalError),
#[error("I/O Error occurred: {0}")]
Io(#[from] std::io::Error),
#[error("Image Error occurred: {0}")]
ImageError(#[from] image::ImageError),
#[error("An unexpected error occurred: {0}")]
Other(String),
}

View file

@ -13,6 +13,7 @@ unsafe fn as_byte_slice<T: Sized>(data: &[T]) -> &[u8] {
/// Represents geometry data (verticies and indicies) stored in GPU buffers managed by
/// ResourceManager. Handles automatic cleanup via a `Drop` implementation.
#[derive(Clone)]
pub struct Geometry {
resource_manager: Arc<ResourceManager>,
pub vertex_buffer: BufferHandle,

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,59 @@
use std::sync::Arc;
use ash::vk;
use crate::{ImageHandle, SamplerHandle};
#[derive(Debug, Clone)]
pub struct Texture {
pub handle: ImageHandle,
pub format: vk::Format,
pub extent: vk::Extent3D,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SamplerDesc {
pub mag_filter: vk::Filter,
pub min_filter: vk::Filter,
pub mipmap_mode: vk::SamplerMipmapMode,
pub address_mode_u: vk::SamplerAddressMode,
pub address_mode_v: vk::SamplerAddressMode,
pub address_mode_w: vk::SamplerAddressMode,
}
impl Default for SamplerDesc {
fn default() -> Self {
Self {
mag_filter: vk::Filter::LINEAR,
min_filter: vk::Filter::LINEAR,
mipmap_mode: vk::SamplerMipmapMode::LINEAR,
address_mode_u: vk::SamplerAddressMode::REPEAT,
address_mode_v: vk::SamplerAddressMode::REPEAT,
address_mode_w: vk::SamplerAddressMode::REPEAT,
}
}
}
#[derive(Debug, Clone)]
pub struct Material {
pub name: String,
pub base_color_texture: Option<Arc<Texture>>,
pub base_color_sampler: Option<SamplerHandle>,
pub base_color_factor: [f32; 4],
pub metallic_factor: f32,
pub roughness_factor: f32,
// TODO: Add other PBR properties:
// pub metallic_roughness_texture: Option<Arc<Texture>>,
// pub metallic_roughness_sampler: Option<SamplerHandle>,
// pub normal_texture: Option<Arc<Texture>>,
// pub normal_sampler: Option<SamplerHandle>,
// pub occlusion_texture: Option<Arc<Texture>>,
// pub occlusion_sampler: Option<SamplerHandle>,
// pub emissive_texture: Option<Arc<Texture>>,
// pub emissive_sampler: Option<SamplerHandle>,
// pub emissive_factor: [f32; 3],
// pub alpha_mode: gltf::material::AlphaMode,
// pub alpha_cutoff: f32,
// pub double_sided: bool,
}

14
crates/scene/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "scene"
version = "0.1.0"
edition = "2021"
[dependencies]
ash.workspace = true
thiserror.workspace = true
tracing.workspace = true
glam.workspace = true
gltf.workspace = true
shared = { path = "../shared" }
resource_manager = { path = "../resource_manager" }

16
crates/scene/src/error.rs Normal file
View file

@ -0,0 +1,16 @@
use thiserror::Error;
/// Any errors that can be returned from this crate.
#[derive(Error, Debug)]
pub enum SceneError {
#[error("Error from ResourceManager: {0}")]
ResourceManagerError(#[from] resource_manager::ResourceManagerError),
#[error("Error from GLTF: {0}")]
GltfError(#[from] gltf::Error),
#[error("InconsistentData: {0}")]
InconsistentData(String),
}
pub type Result<T> = std::result::Result<T, SceneError>;

411
crates/scene/src/lib.rs Normal file
View file

@ -0,0 +1,411 @@
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<Material>,
pub geometry: Arc<Geometry>,
pub transform: Mat4,
}
/// Stores all objects to be rendered by the renderer.
pub struct Scene {
pub name: String,
pub meshes: Vec<Mesh>,
}
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<T>(path: T, resource_manager: Arc<ResourceManager>) -> Result<Self>
where
T: AsRef<Path>,
{
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<Geometry>> = HashMap::new();
// Cache Materials: Key = glTF material index (usize::MAX for default)
let mut material_cache: HashMap<usize, Arc<Material>> = 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("<Default Scene>")
.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("<Unnamed Node>");
if let Some(mesh) = node.mesh() {
let mesh_index = mesh.index();
let mesh_name = mesh.name().unwrap_or("<Unnamed Mesh>");
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<Vertex> = positions
.into_iter()
.zip(normals)
.zip(tex_coords)
.map(|((pos, normal), tex_coord)| Vertex {
pos,
normal,
tex_coord,
})
.collect();
let indices: Vec<u32> = 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("<Default Material>");
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<Arc<Texture>> = None;
let mut loaded_base_color_sampler: Option<SamplerHandle> = 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<Texture>
}
}
// 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<Material>
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<ResourceManager>,
geometry_cache: &'a mut HashMap<(usize, usize), Arc<Geometry>>,
material_cache: &'a mut HashMap<usize, Arc<Material>>,
default_sampler_handle: SamplerHandle, // Store the default sampler
meshes: &'a mut Vec<Mesh>, // Store results directly
}

View file

@ -4,11 +4,14 @@ use glam::{Mat4, Vec3};
use core::f32;
use std::mem::size_of;
mod material;
#[repr(C)]
#[derive(Clone, Debug, Copy)]
pub struct Vertex {
pub pos: [f32; 3],
pub color: [f32; 3],
pub normal: [f32; 3],
pub tex_coord: [f32; 2],
}
impl Vertex {
@ -19,7 +22,7 @@ impl Vertex {
.input_rate(vk::VertexInputRate::VERTEX)
}
pub fn get_attribute_descriptions() -> [vk::VertexInputAttributeDescription; 2] {
pub fn get_attribute_descriptions() -> [vk::VertexInputAttributeDescription; 3] {
[
vk::VertexInputAttributeDescription::default()
.location(0)
@ -30,7 +33,12 @@ impl Vertex {
.location(1)
.binding(0)
.format(vk::Format::R32G32B32_SFLOAT)
.offset(memoffset::offset_of!(Vertex, color) as u32),
.offset(memoffset::offset_of!(Vertex, normal) as u32),
vk::VertexInputAttributeDescription::default()
.location(2)
.binding(0)
.format(vk::Format::R32G32_SFLOAT)
.offset(memoffset::offset_of!(Vertex, tex_coord) as u32),
]
}
}
@ -38,7 +46,6 @@ impl Vertex {
#[repr(C)]
#[derive(Clone, Debug, Copy, PartialEq)]
pub struct UniformBufferObject {
pub model: Mat4,
pub view: Mat4,
pub proj: Mat4,
}
@ -47,6 +54,7 @@ pub struct UniformBufferObject {
pub struct CameraInfo {
pub camera_pos: Vec3,
pub camera_target: Vec3,
pub camera_up: Vec3,
pub camera_fov: f32,
}
@ -55,6 +63,7 @@ impl Default for CameraInfo {
Self {
camera_pos: Vec3::new(10.0, 10.0, 10.0),
camera_target: Vec3::new(0.0, 0.0, 0.0),
camera_up: Vec3::Y,
camera_fov: 45.0,
}
}

View file

45
flake.lock generated
View file

@ -20,11 +20,11 @@
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1733312601,
"narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=",
"lastModified": 1743550720,
"narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9",
"rev": "c621e8422220273271f52058f618c94e405bb0f5",
"type": "github"
},
"original": {
@ -51,23 +51,26 @@
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1733096140,
"narHash": "sha256-1qRH7uAUsyQI7R1Uwl4T+XvdNv778H0Nb5njNrqvylY=",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz"
"lastModified": 1743296961,
"narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa",
"type": "github"
},
"original": {
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz"
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1728538411,
"narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=",
"lastModified": 1736320768,
"narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221",
"rev": "4bc9c909d9ac828a039f288cf872d16d38185db8",
"type": "github"
},
"original": {
@ -79,11 +82,11 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1733097829,
"narHash": "sha256-9hbb1rqGelllb4kVUCZ307G2k3/UhmA8PPGBoyuWaSw=",
"lastModified": 1735554305,
"narHash": "sha256-zExSA1i/b+1NMRhGGLtNfFGXgLtgo+dcuzHzaWA6w3Q=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "2c15aa59df0017ca140d9ba302412298ab4bf22a",
"rev": "0e82ab234249d8eee3e8c91437802b32c74bb3fd",
"type": "github"
},
"original": {
@ -108,11 +111,11 @@
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1735266518,
"narHash": "sha256-2XkWYGgT+911gOLjgBj+8W8ZJk6P0qHJNz8RfKgT/5o=",
"lastModified": 1743820323,
"narHash": "sha256-UXxJogXhPhBFaX4uxmMudcD/x3sEGFtoSc4busTcftY=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "e0b3654b716098b47f3643c65fbb75ef49c033e1",
"rev": "b4734ce867252f92cdc7d25f8cc3b7cef153e703",
"type": "github"
},
"original": {
@ -141,11 +144,11 @@
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1735135567,
"narHash": "sha256-8T3K5amndEavxnludPyfj3Z1IkcFdRpR23q+T0BVeZE=",
"lastModified": 1743748085,
"narHash": "sha256-uhjnlaVTWo5iD3LXics1rp9gaKgDRQj6660+gbUU3cE=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "9e09d30a644c57257715902efbb3adc56c79cf28",
"rev": "815e4121d6a5d504c0f96e5be2dd7f871e4fd99d",
"type": "github"
},
"original": {

View file

@ -35,11 +35,12 @@
commonArgs,
...
}: {
_module.args = {
_module.args = rec {
pkgs = import nixpkgs {
inherit system;
overlays = [inputs.rust-overlay.overlays.default];
};
craneLib = (inputs.crane.mkLib pkgs).overrideToolchain (
pkgs: pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml
);

View file

@ -1,6 +1,11 @@
[toolchain]
channel = "nightly-2024-11-22"
components = ["rust-src", "rustc-dev", "llvm-tools"]
components = [
"rust-src",
"rustc-dev",
"llvm-tools",
"rustc-codegen-cranelift-preview",
]
# commit_hash = b19329a37cedf2027517ae22c87cf201f93d776e
# Whenever changing the nightly channel, update the commit hash above, and make

View file

@ -1,16 +1,34 @@
#version 450
// Input from vertex shader
layout(location = 0) in vec3 fragColor;
// layout(location = 1) in vec2 fragTexCoord; // If using textures
layout(location = 0) in vec3 fragNormal; // Receive normal
layout(location = 1) in vec2 fragTexCoord; // Receive texture coordinates
// Output color
layout(location = 0) out vec4 outColor;
// layout(binding = 1) uniform sampler2D texSampler; // If using textures
// Descriptor set for material properties (Set 1)
layout(set = 1, binding = 0) uniform sampler2D baseColorSampler;
// Optional: Pass material factors via another UBO or Push Constants if needed
// layout(set = 1, binding = 1) uniform MaterialFactors {
// vec4 baseColorFactor;
// } materialFactors;
void main() {
// Use interpolated color
outColor = vec4(fragColor, 1.0);
// outColor = texture(texSampler, fragTexCoord); // If using textures
// Sample the texture
vec4 texColor = texture(baseColorSampler, fragTexCoord);
// Use the texture color
// You might multiply by baseColorFactor here if you pass it
// outColor = texColor * materialFactors.baseColorFactor;
outColor = texColor;
// Basic fallback if texture alpha is zero (or use baseColorFactor)
if (outColor.a == 0.0) {
outColor = vec4(0.8, 0.8, 0.8, 1.0); // Default grey
}
// You could add basic lighting using fragNormal here later
}

View file

@ -1,26 +1,38 @@
#version 450
// Matches Vertex struct attribute descriptions
// INPUTS from Vertex Buffer (matching Vertex struct)
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inColor;
// layout(location = 2) in vec2 inTexCoord; // If you add texture coords
layout(location = 1) in vec3 inNormal;
layout(location = 2) in vec2 inTexCoord; // <<< MUST be vec2
// Matches UniformBufferObject struct and descriptor set layout binding
layout(binding = 0) uniform UniformBufferObject {
mat4 model;
// UNIFORMS (Set 0)
layout(set = 0, binding = 0) uniform UniformBufferObject {
mat4 view;
mat4 proj;
} ubo;
// Output to fragment shader
layout(location = 0) out vec3 fragColor;
// layout(location = 1) out vec2 fragTexCoord; // If you add texture coords
// PUSH CONSTANTS
layout(push_constant) uniform PushConstants {
mat4 model;
} pushConstants;
// OUTPUTS to Fragment Shader
layout(location = 0) out vec3 fragNormal; // Location 0 for Normal
layout(location = 1) out vec2 fragTexCoord; // Location 1 for TexCoord
void main() {
// Transform position: Model -> World -> View -> Clip space
gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0);
vec4 worldPos = pushConstants.model * vec4(inPosition, 1.0);
// Pass color (and other attributes) through
fragColor = inColor;
// fragTexCoord = inTexCoord;
// Calculate final position
gl_Position = ubo.proj * ubo.view * worldPos;
// --- Pass attributes to Fragment Shader ---
// Pass world-space normal (adjust calculation if needed)
// Ensure fragNormal is assigned a vec3
fragNormal = normalize(mat3(transpose(inverse(pushConstants.model))) * inNormal);
// Pass texture coordinates (ensure inTexCoord is vec2)
// Ensure fragTexCoord is assigned a vec2
fragTexCoord = inTexCoord;
}