Compare commits
10 commits
234e2ab78d
...
74a1be796f
| Author | SHA1 | Date | |
|---|---|---|---|
| 74a1be796f | |||
| 2501390225 | |||
| 926515e6b2 | |||
| ae09e61f40 | |||
| 70176bb86a | |||
| dbf9544e80 | |||
| 6c70f7bc2e | |||
| b1c164dc6a | |||
| 51e4a4727e | |||
| f71f0d8e10 |
29 changed files with 4465 additions and 465 deletions
1645
Cargo.lock
generated
1645
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
10
Cargo.toml
10
Cargo.toml
|
|
@ -6,6 +6,8 @@ members = [
|
|||
"crates/gfx_hal",
|
||||
"crates/renderer",
|
||||
"crates/resource_manager",
|
||||
"crates/scene",
|
||||
"crates/shared",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
|
|
@ -24,11 +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"] }
|
||||
parking_lot = "0.12.3"
|
||||
thiserror = "2.0.12"
|
||||
gltf = "1.4.1"
|
||||
|
||||
|
||||
# # Enable incremental by default in release mode.
|
||||
|
|
@ -57,4 +60,7 @@ thiserror = "2.0.12"
|
|||
#
|
||||
# rustflags = ["-Zshare-generics=off"]
|
||||
# codegen-units = 1
|
||||
opt-level = 1
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
|
|
|
|||
|
|
@ -5,15 +5,22 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
egui.workspace = true
|
||||
egui_tiles.workspace = true
|
||||
ash.workspace = true
|
||||
ash-window.workspace = true
|
||||
color-eyre.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
winit.workspace = true
|
||||
raw-window-handle.workspace = true
|
||||
thiserror.workspace = true
|
||||
glam.workspace = true
|
||||
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -8,21 +8,28 @@ use std::{
|
|||
|
||||
use ash::vk;
|
||||
use clap::Parser;
|
||||
use egui::{Context, Slider, ViewportId};
|
||||
use egui_winit::State;
|
||||
use gfx_hal::{
|
||||
device::Device, error::GfxHalError, instance::Instance, instance::InstanceConfig,
|
||||
physical_device::PhysicalDevice, queue::Queue, surface::Surface,
|
||||
};
|
||||
use glam::Vec3;
|
||||
use raw_window_handle::HasDisplayHandle;
|
||||
use renderer::{Renderer, RendererError};
|
||||
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,
|
||||
};
|
||||
|
||||
// --- Configuration ---
|
||||
const APP_NAME: &str = "BeginDisregard";
|
||||
const ENGINE_NAME: &str = "Engine";
|
||||
|
|
@ -42,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 {
|
||||
|
|
@ -57,12 +66,125 @@ struct Application {
|
|||
// Renderer
|
||||
renderer: Renderer,
|
||||
|
||||
egui_ctx: Context,
|
||||
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
|
||||
|
||||
frame_count: u32,
|
||||
last_fps_update_time: Instant,
|
||||
last_frame_time: Instant,
|
||||
current_fps: f64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct EditorUI {}
|
||||
|
||||
impl EditorUI {
|
||||
fn title() -> String {
|
||||
"engine".to_string()
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
egui::Grid::new("main_grid")
|
||||
.spacing([40.0, 4.0])
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
ui.label("FOV");
|
||||
// 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)]
|
||||
|
|
@ -222,30 +344,15 @@ impl Application {
|
|||
|
||||
// Get specific queues (assuming graphics and present are the same for simplicity)
|
||||
let graphics_queue = device.get_graphics_queue();
|
||||
let queue_associated_device_handle = graphics_queue.device().raw().handle();
|
||||
info!(
|
||||
"App: Queue is associated with Device handle: {:?}",
|
||||
queue_associated_device_handle
|
||||
);
|
||||
assert_eq!(
|
||||
device_handle_at_creation, queue_associated_device_handle,
|
||||
"Device handle mismatch immediately after queue creation!"
|
||||
);
|
||||
|
||||
// --- 4. Resource Manager ---
|
||||
let resource_manager = Arc::new(ResourceManager::new(instance.clone(), device.clone())?);
|
||||
info!("Resource Manager initialized.");
|
||||
|
||||
let renderer_device_handle_to_pass = device.raw().handle();
|
||||
let renderer_queue_device_handle_to_pass = graphics_queue.device().raw().handle();
|
||||
info!(
|
||||
"App: Passing Device handle to Renderer: {:?}",
|
||||
renderer_device_handle_to_pass
|
||||
);
|
||||
info!(
|
||||
"App: Passing Queue associated with Device handle: {:?}",
|
||||
renderer_queue_device_handle_to_pass
|
||||
);
|
||||
let scene = Scene::from_gltf(
|
||||
"./sponza/NewSponza_Main_glTF_003.gltf",
|
||||
resource_manager.clone(),
|
||||
)?;
|
||||
|
||||
// --- 5. Renderer ---
|
||||
let initial_size = window.inner_size();
|
||||
|
|
@ -255,11 +362,26 @@ impl Application {
|
|||
graphics_queue.clone(),
|
||||
surface.clone(),
|
||||
resource_manager.clone(),
|
||||
scene,
|
||||
initial_size.width,
|
||||
initial_size.height,
|
||||
)?;
|
||||
|
||||
let egui_ctx = Context::default();
|
||||
let egui_winit = State::new(
|
||||
egui_ctx.clone(),
|
||||
ViewportId::ROOT,
|
||||
&window,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let egui_app = EditorUI::default();
|
||||
|
||||
info!("Renderer initialized.");
|
||||
|
||||
let camera_info = CameraInfo::default(); // Get default camera settings
|
||||
|
||||
Ok(Self {
|
||||
_instance: instance,
|
||||
_physical_device: physical_device,
|
||||
|
|
@ -269,13 +391,40 @@ impl Application {
|
|||
_resource_manager: resource_manager,
|
||||
renderer,
|
||||
window,
|
||||
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(),
|
||||
last_frame_time: Instant::now(),
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: &WindowEvent, active_event_loop: &ActiveEventLoop) {
|
||||
// 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 => {
|
||||
info!("Close requested. Exiting...");
|
||||
|
|
@ -298,30 +447,153 @@ 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();
|
||||
|
||||
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 {
|
||||
platform_output,
|
||||
textures_delta,
|
||||
shapes,
|
||||
pixels_per_point,
|
||||
..
|
||||
} = self.egui_ctx.run(raw_input, |ctx| {
|
||||
self.egui_app
|
||||
.build_ui(ctx, self.current_fps, &mut self.camera_info);
|
||||
});
|
||||
|
||||
self.renderer.update_textures(textures_delta).unwrap();
|
||||
|
||||
self.egui_winit
|
||||
.handle_platform_output(&self.window, platform_output);
|
||||
|
||||
let clipped_primitives = self.egui_ctx.tessellate(shapes, pixels_per_point);
|
||||
|
||||
// --- Render Frame ---
|
||||
match self.renderer.render_frame() {
|
||||
match self.renderer.render_frame(
|
||||
pixels_per_point,
|
||||
&clipped_primitives,
|
||||
self.camera_info,
|
||||
) {
|
||||
Ok(_) => {
|
||||
self.window.request_redraw();
|
||||
}
|
||||
|
|
@ -343,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 ---
|
||||
|
|
@ -450,6 +822,7 @@ struct Args {
|
|||
|
||||
// --- Entry Point ---
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
color_eyre::install()?;
|
||||
let args = Args::parse();
|
||||
|
||||
let fmt_layer = tracing_subscriber::fmt::layer()
|
||||
|
|
|
|||
|
|
@ -9,4 +9,4 @@ ash-window.workspace = true
|
|||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
winit.workspace = true
|
||||
parking_lot.workspace = true
|
||||
gpu-allocator.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
use ash::vk;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::CStr;
|
||||
use std::sync::Weak;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use crate::error::{GfxHalError, Result};
|
||||
use crate::instance::Instance;
|
||||
|
|
@ -14,7 +15,7 @@ use crate::queue::Queue;
|
|||
///
|
||||
/// Owns the `ash::Device` and provides access to device functions and queues.
|
||||
pub struct Device {
|
||||
instance: Arc<Instance>,
|
||||
_instance: Arc<Instance>,
|
||||
physical_device: vk::PhysicalDevice,
|
||||
device: ash::Device,
|
||||
queues: Mutex<HashMap<(u32, u32), Arc<Queue>>>,
|
||||
|
|
@ -32,6 +33,7 @@ impl Device {
|
|||
/// - `queue_family_indicies` must be valid indicies obtained from the `physical_device_handle`.
|
||||
/// - `required_extensions` must be supported by the `physical_device_handle`.
|
||||
/// - All feature structs passed must be supported by the `physical_device_handle`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) unsafe fn new(
|
||||
instance: Arc<Instance>,
|
||||
physical_device_handle: vk::PhysicalDevice,
|
||||
|
|
@ -112,7 +114,7 @@ impl Device {
|
|||
// --- 4. Create the Device struct in an Arc (Stage 1) ---
|
||||
// Initialize the queues map as empty for now.
|
||||
let device_arc = Arc::new(Device {
|
||||
instance: instance.clone(),
|
||||
_instance: instance.clone(),
|
||||
physical_device: physical_device_handle,
|
||||
device: ash_device, // Move the created ash::Device here
|
||||
queues: Mutex::new(HashMap::new()), // Start with empty map
|
||||
|
|
@ -146,7 +148,7 @@ impl Device {
|
|||
// Lock the mutex and insert the created queues into the map within the Arc<Device>
|
||||
{
|
||||
// Scope for the mutex guard
|
||||
let mut queues_map_guard = device_arc.queues.lock();
|
||||
let mut queues_map_guard = device_arc.queues.lock()?;
|
||||
*queues_map_guard = queues_to_insert; // Replace the empty map with the populated one
|
||||
tracing::debug!(
|
||||
"Device Arc populated with {} queues (Stage 2).",
|
||||
|
|
@ -185,15 +187,21 @@ impl Device {
|
|||
|
||||
/// Gets a wrapped queue handle.
|
||||
/// Currently only supports queue index 0 for each family.
|
||||
pub fn get_queue(&self, family_index: u32, queue_index: u32) -> Option<Arc<Queue>> {
|
||||
pub fn get_queue(&self, family_index: u32, queue_index: u32) -> Result<Arc<Queue>> {
|
||||
if queue_index != 0 {
|
||||
tracing::warn!("get_queue currently only supports queue_index 0");
|
||||
return None;
|
||||
return Err(GfxHalError::MissingQueueFamily(
|
||||
"get_queue only supports queue_index 0".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
self.queues
|
||||
.lock()
|
||||
.lock()?
|
||||
.get(&(family_index, queue_index))
|
||||
.cloned()
|
||||
.ok_or(GfxHalError::MissingQueueFamily(
|
||||
"could not get queue family".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Gets the primary graphics queue (family index from `graphics_queue_family_index`, queue index 0).
|
||||
|
|
|
|||
|
|
@ -56,9 +56,23 @@ pub enum GfxHalError {
|
|||
#[error("Error loading the ash entry.")]
|
||||
AshEntryError(#[from] ash::LoadingError),
|
||||
|
||||
/// Poisoned Mutex
|
||||
#[error("Error from poisoned mutex: {0}")]
|
||||
MutexPoisoned(String),
|
||||
|
||||
/// Placeholder for other specific errors.
|
||||
#[error("An unexpected error occurred: {0}")]
|
||||
Other(String),
|
||||
|
||||
/// Size for Buffer is invalid.
|
||||
#[error("Buffer size is invalid.")]
|
||||
BufferSizeInvalid,
|
||||
}
|
||||
|
||||
pub type Result<T, E = GfxHalError> = std::result::Result<T, E>;
|
||||
|
||||
impl<T> From<std::sync::PoisonError<T>> for GfxHalError {
|
||||
fn from(e: std::sync::PoisonError<T>) -> Self {
|
||||
Self::MutexPoisoned(e.to_string())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,3 +6,12 @@ pub mod queue;
|
|||
pub mod surface;
|
||||
pub mod swapchain;
|
||||
pub mod sync;
|
||||
|
||||
pub use device::*;
|
||||
pub use error::*;
|
||||
pub use instance::*;
|
||||
pub use physical_device::*;
|
||||
pub use queue::*;
|
||||
pub use surface::*;
|
||||
pub use swapchain::*;
|
||||
pub use sync::*;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use ash::{vk, Device as AshDevice};
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::device::Device;
|
||||
use crate::error::Result;
|
||||
|
|
@ -66,18 +65,6 @@ impl Queue {
|
|||
submits: &[vk::SubmitInfo],
|
||||
signal_fence: Option<&Fence>,
|
||||
) -> Result<()> {
|
||||
debug_assert!(
|
||||
self.device.raw().handle() == submit_device_raw.handle(),
|
||||
"Queue::submit called with an ash::Device from a different logical VkDevice than the queue belongs to!"
|
||||
);
|
||||
// Optional: Check fence device consistency
|
||||
if let Some(fence) = signal_fence {
|
||||
debug_assert!(
|
||||
fence.device().raw().handle() == submit_device_raw.handle(),
|
||||
"Fence passed to Queue::submit belongs to a different logical device than submit_device_raw!"
|
||||
);
|
||||
}
|
||||
|
||||
let fence_handle = signal_fence.map_or(vk::Fence::null(), |f| f.handle());
|
||||
|
||||
// Keep the lock for thread-safety on the VkQueue object itself
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use crate::{
|
|||
/// Wraps a `vk::Fence`, used for CPU-GPU synchronization.
|
||||
///
|
||||
/// Owns the `vk::Fence` handle.
|
||||
#[derive(Clone)]
|
||||
pub struct Fence {
|
||||
device: Arc<Device>,
|
||||
fence: vk::Fence,
|
||||
|
|
|
|||
|
|
@ -13,10 +13,11 @@ gpu-allocator.workspace = true
|
|||
egui.workspace = true
|
||||
egui-ash-renderer.workspace = true
|
||||
winit.workspace = true
|
||||
parking_lot.workspace = true
|
||||
|
||||
gfx_hal = { path = "../gfx_hal" }
|
||||
resource_manager = { path = "../resource_manager" }
|
||||
shared = { path = "../shared" }
|
||||
scene = { path = "../scene" }
|
||||
|
||||
[build-dependencies]
|
||||
shaderc = "0.9.1"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ fn main() -> Result<()> {
|
|||
let out_dir = PathBuf::from(env::var("OUT_DIR")?).join("shaders"); // Put shaders in a subdirectory for clarity
|
||||
fs::create_dir_all(&out_dir).context("Failed to create shader output directory")?;
|
||||
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
|
||||
let compiler = Compiler::new().context("Failed to create shader compiler")?;
|
||||
let mut options = CompileOptions::new().context("Failed to create compile options")?;
|
||||
|
||||
|
|
@ -49,6 +51,7 @@ fn main() -> Result<()> {
|
|||
.filter(|e| e.file_type().is_file())
|
||||
// Only process files
|
||||
{
|
||||
println!("cargo:rerun-if-changed={:?}", entry.path());
|
||||
let in_path = entry.path();
|
||||
|
||||
// Determine shader kind from extension
|
||||
|
|
|
|||
|
|
@ -1,21 +1,32 @@
|
|||
use std::{ffi::CStr, sync::Arc};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::c_void,
|
||||
mem,
|
||||
sync::{Arc, Mutex},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use ash::vk;
|
||||
use egui::{ClippedPrimitive, TextureId, TexturesDelta};
|
||||
use egui_ash_renderer::{DynamicRendering, Options, Renderer as EguiRenderer};
|
||||
use gfx_hal::{
|
||||
device::Device, error::GfxHalError, queue::Queue, surface::Surface, swapchain::Swapchain,
|
||||
swapchain::SwapchainConfig, sync::Fence, sync::Semaphore,
|
||||
};
|
||||
use gpu_allocator::{vulkan::Allocator, MemoryLocation};
|
||||
use parking_lot::Mutex;
|
||||
use resource_manager::{ImageHandle, ResourceManager, ResourceManagerError};
|
||||
use glam::{Mat4, Vec3};
|
||||
use gpu_allocator::{
|
||||
vulkan::{Allocation, AllocationCreateDesc, Allocator},
|
||||
MemoryLocation,
|
||||
};
|
||||
use resource_manager::{
|
||||
ImageHandle, Material, ResourceManager, ResourceManagerError, SamplerHandle, Texture,
|
||||
};
|
||||
use shared::{CameraInfo, UniformBufferObject};
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, error, info, warn};
|
||||
// Assuming winit is used by the app
|
||||
|
||||
// Re-export ash for convenience if needed elsewhere
|
||||
pub use ash;
|
||||
|
||||
const MAX_FRAMES_IN_FLIGHT: usize = 2;
|
||||
const MAX_MATERIALS: usize = 150;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RendererError {
|
||||
|
|
@ -51,6 +62,17 @@ pub enum RendererError {
|
|||
ImageInfoUnavailable,
|
||||
#[error("Failed to get allocator from resource manager")]
|
||||
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 {
|
||||
fn from(_: std::sync::PoisonError<T>) -> Self {
|
||||
Self::AllocatorUnavailable
|
||||
}
|
||||
}
|
||||
|
||||
struct FrameData {
|
||||
|
|
@ -58,7 +80,14 @@ struct FrameData {
|
|||
command_buffer: vk::CommandBuffer,
|
||||
image_available_semaphore: Semaphore,
|
||||
render_finished_semaphore: Semaphore,
|
||||
textures_to_free: Option<Vec<TextureId>>,
|
||||
in_flight_fence: Fence,
|
||||
|
||||
descriptor_set: vk::DescriptorSet,
|
||||
uniform_buffer_object: UniformBufferObject,
|
||||
uniform_buffer: vk::Buffer,
|
||||
uniform_buffer_allocation: Allocation,
|
||||
uniform_buffer_mapped_ptr: *mut c_void,
|
||||
}
|
||||
|
||||
struct SwapchainSupportDetails {
|
||||
|
|
@ -79,12 +108,26 @@ pub struct Renderer {
|
|||
swapchain_format: vk::SurfaceFormatKHR,
|
||||
swapchain_extent: vk::Extent2D,
|
||||
|
||||
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,
|
||||
depth_image_view: vk::ImageView, // Store the view directly
|
||||
depth_format: vk::Format,
|
||||
|
||||
triangle_pipeline_layout: vk::PipelineLayout,
|
||||
triangle_pipeline: vk::Pipeline,
|
||||
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,
|
||||
|
|
@ -93,15 +136,19 @@ pub struct Renderer {
|
|||
window_resized: bool,
|
||||
current_width: u32,
|
||||
current_height: u32,
|
||||
|
||||
start_time: Instant,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
instance: Arc<gfx_hal::instance::Instance>, // Needed for allocator
|
||||
device: Arc<Device>,
|
||||
graphics_queue: Arc<Queue>,
|
||||
surface: Arc<Surface>,
|
||||
resource_manager: Arc<ResourceManager>,
|
||||
scene: scene::Scene,
|
||||
initial_width: u32,
|
||||
initial_height: u32,
|
||||
) -> Result<Self, RendererError> {
|
||||
|
|
@ -121,33 +168,215 @@ impl Renderer {
|
|||
let (depth_image_handle, depth_image_view) =
|
||||
Self::create_depth_resources(&device, &resource_manager, extent, depth_format)?;
|
||||
|
||||
let (triangle_pipeline_layout, triangle_pipeline) =
|
||||
Self::create_triangle_pipeline(&device, format.format, depth_format)?;
|
||||
let descriptor_set_layout = Self::create_descriptor_set_layout(&device)?;
|
||||
let material_descriptor_set_layout = Self::create_material_descriptor_set_layout(&device)?;
|
||||
|
||||
let frames_data = Self::create_frame_data(&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_layouts,
|
||||
)?;
|
||||
|
||||
let start_time = Instant::now();
|
||||
|
||||
let frames_data = Self::create_frame_data(
|
||||
&device,
|
||||
&resource_manager,
|
||||
descriptor_pool,
|
||||
&descriptor_set_layouts,
|
||||
swapchain.extent(),
|
||||
)?;
|
||||
|
||||
info!("Renderer initialized successfully.");
|
||||
|
||||
let egui_renderer = EguiRenderer::with_gpu_allocator(
|
||||
resource_manager.allocator(),
|
||||
device.raw().clone(),
|
||||
DynamicRendering {
|
||||
color_attachment_format: swapchain.format().format,
|
||||
depth_attachment_format: Some(depth_format),
|
||||
},
|
||||
Options {
|
||||
srgb_framebuffer: true,
|
||||
in_flight_frames: MAX_FRAMES_IN_FLIGHT,
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
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,
|
||||
resource_manager,
|
||||
egui_renderer,
|
||||
allocator, // Store the allocator Arc
|
||||
surface,
|
||||
swapchain: Some(swapchain),
|
||||
swapchain_image_views: image_views,
|
||||
swapchain_format: format,
|
||||
swapchain_extent: extent,
|
||||
descriptor_set_layout,
|
||||
descriptor_pool,
|
||||
|
||||
material_descriptor_set_layout,
|
||||
depth_image_handle,
|
||||
depth_image_view,
|
||||
depth_format,
|
||||
triangle_pipeline_layout,
|
||||
triangle_pipeline,
|
||||
model_pipeline_layout,
|
||||
model_pipeline,
|
||||
|
||||
material_descriptor_sets: HashMap::new(),
|
||||
|
||||
default_white_texture,
|
||||
default_sampler,
|
||||
|
||||
frames_data,
|
||||
scene,
|
||||
current_frame: 0,
|
||||
window_resized: false,
|
||||
current_width: initial_width,
|
||||
current_height: initial_height,
|
||||
|
||||
start_time,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -162,7 +391,33 @@ impl Renderer {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn render_frame(&mut self) -> Result<(), RendererError> {
|
||||
pub fn update_textures(&mut self, textures_delta: TexturesDelta) -> Result<(), RendererError> {
|
||||
tracing::trace!("Updating EGUI textures!");
|
||||
|
||||
if !textures_delta.free.is_empty() {
|
||||
self.frames_data[self.current_frame].textures_to_free =
|
||||
Some(textures_delta.free.clone());
|
||||
}
|
||||
|
||||
if !textures_delta.set.is_empty() {
|
||||
self.egui_renderer
|
||||
.set_textures(
|
||||
self.device.get_graphics_queue().handle(),
|
||||
self.frames_data[self.current_frame].command_pool,
|
||||
textures_delta.set.as_slice(),
|
||||
)
|
||||
.expect("Failed to update texture");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render_frame(
|
||||
&mut self,
|
||||
pixels_per_point: f32,
|
||||
clipped_primitives: &[ClippedPrimitive],
|
||||
camera_info: CameraInfo,
|
||||
) -> Result<(), RendererError> {
|
||||
// --- Handle Resize ---
|
||||
if self.window_resized {
|
||||
self.window_resized = false;
|
||||
|
|
@ -174,7 +429,7 @@ impl Renderer {
|
|||
|
||||
// --- Wait for Previous Frame ---
|
||||
let frame_index = self.current_frame;
|
||||
let frame_data = &self.frames_data[frame_index];
|
||||
let frame_data = &mut self.frames_data[frame_index];
|
||||
|
||||
frame_data.in_flight_fence.wait(None)?; // Wait indefinitely
|
||||
|
||||
|
|
@ -183,6 +438,7 @@ impl Renderer {
|
|||
.swapchain
|
||||
.as_ref()
|
||||
.ok_or(RendererError::SwapchainAcquisitionFailed)?;
|
||||
|
||||
let (image_index, suboptimal) = unsafe {
|
||||
// Need unsafe block for acquire_next_image
|
||||
swapchain_ref.acquire_next_image(
|
||||
|
|
@ -203,6 +459,10 @@ impl Renderer {
|
|||
// --- Reset Fence (only after successful acquisition) ---
|
||||
frame_data.in_flight_fence.reset()?;
|
||||
|
||||
if let Some(textures) = frame_data.textures_to_free.take() {
|
||||
self.egui_renderer.free_textures(&textures)?;
|
||||
}
|
||||
|
||||
// --- Record Command Buffer ---
|
||||
unsafe {
|
||||
// Need unsafe for Vulkan commands
|
||||
|
|
@ -215,6 +475,15 @@ impl Renderer {
|
|||
let cmd_begin_info = vk::CommandBufferBeginInfo::default()
|
||||
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT);
|
||||
|
||||
// -- Update uniform buffer --
|
||||
self.update_uniform_buffer(camera_info)?;
|
||||
|
||||
let frame_data = &mut self.frames_data[self.current_frame];
|
||||
let swapchain_ref = self
|
||||
.swapchain
|
||||
.as_ref()
|
||||
.ok_or(RendererError::SwapchainAcquisitionFailed)?;
|
||||
|
||||
unsafe {
|
||||
// Need unsafe for Vulkan commands
|
||||
self.device
|
||||
|
|
@ -316,18 +585,74 @@ 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,
|
||||
self.triangle_pipeline,
|
||||
self.model_pipeline,
|
||||
);
|
||||
|
||||
self.device.raw().cmd_bind_descriptor_sets(
|
||||
command_buffer,
|
||||
vk::PipelineBindPoint::GRAPHICS,
|
||||
self.model_pipeline_layout,
|
||||
0,
|
||||
&[frame_data.descriptor_set],
|
||||
&[],
|
||||
);
|
||||
// Draw 3 vertices, 1 instance, 0 first vertex, 0 first instance
|
||||
self.device.raw().cmd_draw(command_buffer, 3, 1, 0, 0);
|
||||
}
|
||||
|
||||
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,
|
||||
self.swapchain_extent,
|
||||
pixels_per_point,
|
||||
clipped_primitives,
|
||||
)?;
|
||||
tracing::trace!("Rendered EGUI");
|
||||
|
||||
// --- End Dynamic Rendering ---
|
||||
unsafe {
|
||||
// Need unsafe for Vulkan commands
|
||||
|
|
@ -382,12 +707,6 @@ impl Renderer {
|
|||
.command_buffers(&command_buffers)
|
||||
.signal_semaphores(&signal_semaphores);
|
||||
|
||||
// assert_eq!(
|
||||
// self.graphics_queue.device().raw().handle(), // Device from Queue
|
||||
// self.device.raw().handle(), // Device stored in Renderer
|
||||
// "Device handle mismatch between Renderer and Graphics Queue!"
|
||||
// );
|
||||
|
||||
unsafe {
|
||||
// Need unsafe for queue submit
|
||||
self.graphics_queue.submit(
|
||||
|
|
@ -475,7 +794,7 @@ impl Renderer {
|
|||
// --- Helper: Cleanup Swapchain Dependent Resources ---
|
||||
fn cleanup_swapchain_resources(&mut self) {
|
||||
debug!("Cleaning up swapchain resources...");
|
||||
// Destroy depth buffer view
|
||||
|
||||
unsafe {
|
||||
self.device
|
||||
.raw()
|
||||
|
|
@ -484,9 +803,8 @@ impl Renderer {
|
|||
// Destroy depth buffer image via resource manager
|
||||
if let Err(e) = self.resource_manager.destroy_image(self.depth_image_handle) {
|
||||
error!("Failed to destroy depth image: {}", e);
|
||||
// Continue cleanup even if this fails
|
||||
}
|
||||
// Drop the old swapchain object (RAII in gfx_hal::Swapchain handles vkDestroySwapchainKHR)
|
||||
|
||||
self.swapchain = None;
|
||||
debug!("Swapchain resources cleaned up.");
|
||||
}
|
||||
|
|
@ -533,38 +851,10 @@ impl Renderer {
|
|||
let swapchain =
|
||||
unsafe { Swapchain::new(device.clone(), surface.clone(), config, old_swapchain)? };
|
||||
|
||||
// Create Image Views
|
||||
let image_views = swapchain
|
||||
.image_views() // Assuming Swapchain::new creates and stores these
|
||||
.to_vec(); // Clone the slice into a Vec
|
||||
|
||||
// If Swapchain::new doesn't create views, we need to do it here:
|
||||
/*
|
||||
let images = swapchain.images()?; // Assuming this method exists
|
||||
let mut image_views = Vec::with_capacity(images.len());
|
||||
for &image in images.iter() {
|
||||
let create_info = vk::ImageViewCreateInfo::default()
|
||||
.image(image)
|
||||
.view_type(vk::ImageViewType::TYPE_2D)
|
||||
.format(surface_format.format)
|
||||
.components(vk::ComponentMapping {
|
||||
r: vk::ComponentSwizzle::IDENTITY,
|
||||
g: vk::ComponentSwizzle::IDENTITY,
|
||||
b: vk::ComponentSwizzle::IDENTITY,
|
||||
a: vk::ComponentSwizzle::IDENTITY,
|
||||
})
|
||||
.subresource_range(vk::ImageSubresourceRange {
|
||||
aspect_mask: vk::ImageAspectFlags::COLOR,
|
||||
base_mip_level: 0,
|
||||
level_count: 1,
|
||||
base_array_layer: 0,
|
||||
layer_count: 1,
|
||||
});
|
||||
let view = unsafe { device.raw().create_image_view(&create_info, None)? };
|
||||
image_views.push(view);
|
||||
}
|
||||
*/
|
||||
|
||||
Ok((swapchain, surface_format, extent, image_views))
|
||||
}
|
||||
|
||||
|
|
@ -618,33 +908,12 @@ impl Renderer {
|
|||
}
|
||||
|
||||
// --- Helper: Create Triangle Pipeline ---
|
||||
fn create_triangle_pipeline(
|
||||
fn create_model_pipeline(
|
||||
device: &Arc<Device>,
|
||||
color_format: vk::Format,
|
||||
depth_format: vk::Format,
|
||||
descriptor_set_layouts: &[vk::DescriptorSetLayout],
|
||||
) -> Result<(vk::PipelineLayout, vk::Pipeline), RendererError> {
|
||||
// --- Shaders (Hardcoded example) ---
|
||||
// Vertex Shader (GLSL) - outputs clip space position based on vertex index
|
||||
/*
|
||||
#version 450
|
||||
vec2 positions[3] = vec2[](
|
||||
vec2(0.0, -0.5),
|
||||
vec2(0.5, 0.5),
|
||||
vec2(-0.5, 0.5)
|
||||
);
|
||||
void main() {
|
||||
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
|
||||
}
|
||||
*/
|
||||
// Fragment Shader (GLSL) - outputs solid orange
|
||||
/*
|
||||
#version 450
|
||||
layout(location = 0) out vec4 outColor;
|
||||
void main() {
|
||||
outColor = vec4(1.0, 0.5, 0.0, 1.0); // Orange
|
||||
}
|
||||
*/
|
||||
|
||||
// Load compiled SPIR-V (replace with actual loading)
|
||||
let vert_shader_code = include_bytes!(concat!(env!("OUT_DIR"), "/shaders/vert.glsl.spv")); // Placeholder path
|
||||
let frag_shader_code = include_bytes!(concat!(env!("OUT_DIR"), "/shaders/frag.glsl.spv")); // Placeholder path
|
||||
|
|
@ -652,7 +921,7 @@ impl Renderer {
|
|||
let vert_module = Self::create_shader_module(device, vert_shader_code)?;
|
||||
let frag_module = Self::create_shader_module(device, frag_shader_code)?;
|
||||
|
||||
let main_function_name = CStr::from_bytes_with_nul(b"main\0").unwrap();
|
||||
let main_function_name = c"main";
|
||||
|
||||
let vert_stage_info = vk::PipelineShaderStageCreateInfo::default()
|
||||
.stage(vk::ShaderStageFlags::VERTEX)
|
||||
|
|
@ -666,8 +935,13 @@ impl Renderer {
|
|||
|
||||
let shader_stages = [vert_stage_info, frag_stage_info];
|
||||
|
||||
let binding_description = shared::Vertex::get_binding_decription();
|
||||
let attribute_descriptions = shared::Vertex::get_attribute_descriptions();
|
||||
|
||||
// --- Fixed Function State ---
|
||||
let vertex_input_info = vk::PipelineVertexInputStateCreateInfo::default(); // No vertex buffers/attributes
|
||||
let vertex_input_info = vk::PipelineVertexInputStateCreateInfo::default()
|
||||
.vertex_binding_descriptions(std::slice::from_ref(&binding_description))
|
||||
.vertex_attribute_descriptions(&attribute_descriptions);
|
||||
|
||||
let input_assembly = vk::PipelineInputAssemblyStateCreateInfo::default()
|
||||
.topology(vk::PrimitiveTopology::TRIANGLE_LIST)
|
||||
|
|
@ -709,8 +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(); // No descriptors/push constants
|
||||
let layout_info = vk::PipelineLayoutCreateInfo::default()
|
||||
.set_layouts(descriptor_set_layouts)
|
||||
.push_constant_ranges(std::slice::from_ref(&push_constant_range));
|
||||
let pipeline_layout = unsafe {
|
||||
device
|
||||
.raw()
|
||||
|
|
@ -794,7 +1075,7 @@ impl Renderer {
|
|||
// --------------------------------------------------------------------
|
||||
|
||||
// 3. Create the shader module
|
||||
let create_info = vk::ShaderModuleCreateInfo::default().code(&code_slice_ref); // Pass the &[u32] slice
|
||||
let create_info = vk::ShaderModuleCreateInfo::default().code(code_slice_ref); // Pass the &[u32] slice
|
||||
|
||||
unsafe {
|
||||
device
|
||||
|
|
@ -808,7 +1089,13 @@ impl Renderer {
|
|||
}
|
||||
|
||||
// --- Helper: Create Frame Sync Objects & Command Resources ---
|
||||
fn create_frame_data(device: &Arc<Device>) -> Result<Vec<FrameData>, RendererError> {
|
||||
fn create_frame_data(
|
||||
device: &Arc<Device>,
|
||||
resource_manager: &Arc<ResourceManager>,
|
||||
descriptor_pool: vk::DescriptorPool,
|
||||
descriptor_set_layouts: &[vk::DescriptorSetLayout],
|
||||
swapchain_extent: vk::Extent2D,
|
||||
) -> Result<Vec<FrameData>, RendererError> {
|
||||
let mut frames_data = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT);
|
||||
for _ in 0..MAX_FRAMES_IN_FLIGHT {
|
||||
let image_available_semaphore = Semaphore::new(device.clone())?;
|
||||
|
|
@ -841,12 +1128,30 @@ impl Renderer {
|
|||
.map_err(RendererError::CommandBufferAllocation)?[0]
|
||||
};
|
||||
|
||||
tracing::info!("Allocated frame_data command_buffer: {:?}", command_buffer);
|
||||
|
||||
let descriptor_set =
|
||||
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);
|
||||
|
||||
frames_data.push(FrameData {
|
||||
textures_to_free: None,
|
||||
command_pool,
|
||||
command_buffer, // Stays allocated, just reset/rerecorded
|
||||
image_available_semaphore,
|
||||
render_finished_semaphore,
|
||||
in_flight_fence,
|
||||
descriptor_set,
|
||||
uniform_buffer,
|
||||
uniform_buffer_allocation,
|
||||
uniform_buffer_mapped_ptr,
|
||||
uniform_buffer_object,
|
||||
});
|
||||
}
|
||||
Ok(frames_data)
|
||||
|
|
@ -871,22 +1176,20 @@ impl Renderer {
|
|||
}
|
||||
|
||||
fn choose_swapchain_format(available_formats: &[vk::SurfaceFormatKHR]) -> vk::SurfaceFormatKHR {
|
||||
available_formats
|
||||
*available_formats
|
||||
.iter()
|
||||
.find(|format| {
|
||||
format.format == vk::Format::B8G8R8A8_SRGB // Prefer SRGB
|
||||
&& format.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR
|
||||
})
|
||||
.unwrap_or(&available_formats[0]) // Fallback to first available
|
||||
.clone()
|
||||
.unwrap_or(&available_formats[0])
|
||||
}
|
||||
|
||||
fn choose_swapchain_present_mode(available_modes: &[vk::PresentModeKHR]) -> vk::PresentModeKHR {
|
||||
available_modes
|
||||
*available_modes
|
||||
.iter()
|
||||
.find(|&&mode| mode == vk::PresentModeKHR::MAILBOX) // Prefer Mailbox (low latency)
|
||||
.unwrap_or(&vk::PresentModeKHR::FIFO) // Guaranteed fallback
|
||||
.clone()
|
||||
.find(|&&mode| mode == vk::PresentModeKHR::FIFO) // Prefer Mailbox (low latency)
|
||||
.unwrap_or(&vk::PresentModeKHR::FIFO)
|
||||
}
|
||||
|
||||
fn choose_swapchain_extent(
|
||||
|
|
@ -937,8 +1240,197 @@ impl Renderer {
|
|||
}
|
||||
Err(RendererError::Vulkan(
|
||||
vk::Result::ERROR_FORMAT_NOT_SUPPORTED,
|
||||
)) // Or custom error
|
||||
))
|
||||
}
|
||||
|
||||
fn create_material_descriptor_set_layout(
|
||||
device: &Arc<Device>,
|
||||
) -> 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)
|
||||
.descriptor_count(1)
|
||||
.stage_flags(vk::ShaderStageFlags::VERTEX);
|
||||
|
||||
let layout_info = vk::DescriptorSetLayoutCreateInfo::default()
|
||||
.bindings(std::slice::from_ref(&ubo_layout_binding));
|
||||
|
||||
let descriptor_set_layout = unsafe {
|
||||
device
|
||||
.raw()
|
||||
.create_descriptor_set_layout(&layout_info, None)?
|
||||
};
|
||||
|
||||
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(&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_pool)
|
||||
}
|
||||
|
||||
fn create_descriptor_set(
|
||||
device: &Arc<Device>,
|
||||
descriptor_set_layouts: &[vk::DescriptorSetLayout],
|
||||
descriptor_pool: vk::DescriptorPool,
|
||||
) -> Result<vk::DescriptorSet, RendererError> {
|
||||
let alloc_info = vk::DescriptorSetAllocateInfo::default()
|
||||
.descriptor_pool(descriptor_pool)
|
||||
.set_layouts(descriptor_set_layouts);
|
||||
|
||||
let descriptor_set = unsafe { device.raw().allocate_descriptor_sets(&alloc_info)? }[0];
|
||||
|
||||
Ok(descriptor_set)
|
||||
}
|
||||
|
||||
fn create_uniform_buffer(
|
||||
device: &Arc<Device>,
|
||||
resource_manager: &Arc<ResourceManager>,
|
||||
) -> Result<(vk::Buffer, Allocation, *mut std::ffi::c_void), RendererError> {
|
||||
let buffer_size = mem::size_of::<UniformBufferObject>() as vk::DeviceSize;
|
||||
|
||||
let buffer_info = vk::BufferCreateInfo::default()
|
||||
.size(buffer_size)
|
||||
.usage(vk::BufferUsageFlags::UNIFORM_BUFFER)
|
||||
.sharing_mode(vk::SharingMode::EXCLUSIVE);
|
||||
|
||||
let allocation = resource_manager
|
||||
.allocator()
|
||||
.lock()?
|
||||
.allocate(&AllocationCreateDesc {
|
||||
name: "Uniform Buffer",
|
||||
requirements: unsafe {
|
||||
{
|
||||
let temp_buffer = device.raw().create_buffer(&buffer_info, None)?;
|
||||
let req = device.raw().get_buffer_memory_requirements(temp_buffer);
|
||||
|
||||
device.raw().destroy_buffer(temp_buffer, None);
|
||||
req
|
||||
}
|
||||
},
|
||||
location: MemoryLocation::CpuToGpu,
|
||||
linear: true,
|
||||
allocation_scheme: gpu_allocator::vulkan::AllocationScheme::GpuAllocatorManaged,
|
||||
})?;
|
||||
|
||||
let buffer = unsafe { device.raw().create_buffer(&buffer_info, None)? };
|
||||
tracing::info!("Created uniform buffer {:?}", buffer);
|
||||
|
||||
unsafe {
|
||||
device
|
||||
.raw()
|
||||
.bind_buffer_memory(buffer, allocation.memory(), allocation.offset())?;
|
||||
}
|
||||
|
||||
let mapped_ptr = allocation
|
||||
.mapped_ptr()
|
||||
.ok_or_else(|| {
|
||||
error!("Failed to get mapped pointer for CPU->GPU uniform buffer");
|
||||
ResourceManagerError::Other("Failed to map uniform buffer".to_string())
|
||||
})?
|
||||
.as_ptr();
|
||||
|
||||
Ok((buffer, allocation, mapped_ptr))
|
||||
}
|
||||
|
||||
fn update_descriptor_set(
|
||||
device: Arc<Device>,
|
||||
descriptor_set: vk::DescriptorSet,
|
||||
buffer: vk::Buffer,
|
||||
) {
|
||||
let buffer_info = vk::DescriptorBufferInfo::default()
|
||||
.buffer(buffer)
|
||||
.offset(0)
|
||||
.range(mem::size_of::<UniformBufferObject>() as vk::DeviceSize);
|
||||
|
||||
let descriptor_write = vk::WriteDescriptorSet::default()
|
||||
.dst_set(descriptor_set)
|
||||
.dst_binding(0)
|
||||
.dst_array_element(0)
|
||||
.descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
|
||||
.buffer_info(std::slice::from_ref(&buffer_info));
|
||||
|
||||
unsafe {
|
||||
device
|
||||
.raw()
|
||||
.update_descriptor_sets(std::slice::from_ref(&descriptor_write), &[]);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if frame_data.uniform_buffer_object != ubo {
|
||||
let ptr = frame_data.uniform_buffer_mapped_ptr;
|
||||
unsafe {
|
||||
let aligned_ptr = ptr as *mut UniformBufferObject;
|
||||
aligned_ptr.write(ubo);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
camera_info.camera_fov.to_radians(),
|
||||
swapchain_extent.width as f32 / swapchain_extent.height as f32,
|
||||
0.1,
|
||||
1000.0,
|
||||
);
|
||||
|
||||
proj.y_axis.y *= -1.0;
|
||||
|
||||
UniformBufferObject { view, proj }
|
||||
}
|
||||
|
||||
// --- Drop Implementation ---
|
||||
|
|
@ -962,16 +1454,39 @@ impl Drop for Renderer {
|
|||
unsafe {
|
||||
self.device
|
||||
.raw()
|
||||
.destroy_pipeline(self.triangle_pipeline, None);
|
||||
.destroy_pipeline(self.model_pipeline, None);
|
||||
self.device
|
||||
.raw()
|
||||
.destroy_pipeline_layout(self.triangle_pipeline_layout, None);
|
||||
.destroy_pipeline_layout(self.model_pipeline_layout, None);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
self.device
|
||||
.raw()
|
||||
.destroy_descriptor_pool(self.descriptor_pool, None);
|
||||
self.device
|
||||
.raw()
|
||||
.destroy_descriptor_set_layout(self.descriptor_set_layout, None);
|
||||
}
|
||||
|
||||
// Destroy frame data (fences, semaphores, command pools)
|
||||
// Fences/Semaphores are handled by gfx_hal::Drop
|
||||
// Command buffers are freed with the pool
|
||||
for frame_data in self.frames_data.drain(..) {
|
||||
unsafe {
|
||||
self.device
|
||||
.raw()
|
||||
.destroy_buffer(frame_data.uniform_buffer, None);
|
||||
|
||||
let mut allocator = self
|
||||
.allocator
|
||||
.lock()
|
||||
.expect("Allocator Mutex to not be poisoned.");
|
||||
allocator
|
||||
.free(frame_data.uniform_buffer_allocation)
|
||||
.expect("Allocator to be able to free an allocation");
|
||||
}
|
||||
|
||||
unsafe {
|
||||
self.device
|
||||
.raw()
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ edition = "2021"
|
|||
ash.workspace = true
|
||||
gpu-allocator.workspace = true
|
||||
thiserror.workspace = true
|
||||
parking_lot.workspace = true
|
||||
tracing.workspace = true
|
||||
gltf.workspace = true
|
||||
|
||||
gfx_hal = { path = "../gfx_hal" }
|
||||
image = { version = "0.25.6", features = ["rayon"] }
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
175
crates/resource_manager/src/geo.rs
Normal file
175
crates/resource_manager/src/geo.rs
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use ash::vk;
|
||||
use gpu_allocator::MemoryLocation;
|
||||
use tracing::{debug, trace};
|
||||
|
||||
use crate::{BufferHandle, ResourceManager, ResourceManagerError, Result};
|
||||
|
||||
// Helper to safely get a byte slice from structured data
|
||||
unsafe fn as_byte_slice<T: Sized>(data: &[T]) -> &[u8] {
|
||||
std::slice::from_raw_parts(data.as_ptr() as *const u8, std::mem::size_of_val(data))
|
||||
}
|
||||
|
||||
/// 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,
|
||||
pub index_buffer: BufferHandle,
|
||||
pub index_count: u32,
|
||||
}
|
||||
|
||||
impl Geometry {
|
||||
/// Creates new GPU buffers for the given vetex and index data using `ResourceManager`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `resource_manager` - An Arc reference to the ResourceManager.
|
||||
/// * `vertices` - A slice of vertex data.
|
||||
/// * `indices` - A slice of index data (u32)
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a new `ResourceManagerError` if buffer creation or data upload fails.
|
||||
pub fn new<V: Sized + Copy>(
|
||||
resource_manager: Arc<ResourceManager>,
|
||||
vertices: &[V],
|
||||
indicies: &[u32],
|
||||
) -> Result<Self> {
|
||||
trace!(
|
||||
"Creating Geometry: {} vertices, {} indicies",
|
||||
vertices.len(),
|
||||
indicies.len()
|
||||
);
|
||||
|
||||
if vertices.is_empty() || indicies.is_empty() {
|
||||
return Err(ResourceManagerError::Other(
|
||||
"Cannot create Geometry with empty vertices or indicies.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let vertex_buffer = resource_manager.create_buffer_init(
|
||||
vk::BufferUsageFlags::VERTEX_BUFFER,
|
||||
MemoryLocation::GpuOnly,
|
||||
unsafe { as_byte_slice(vertices) },
|
||||
)?;
|
||||
trace!("Vertex buffer created: handle={:?}", vertex_buffer);
|
||||
|
||||
let index_buffer = resource_manager.create_buffer_init(
|
||||
vk::BufferUsageFlags::INDEX_BUFFER,
|
||||
MemoryLocation::GpuOnly,
|
||||
unsafe { as_byte_slice(indicies) },
|
||||
)?;
|
||||
trace!("Index buffer created: handle={:?}", index_buffer);
|
||||
|
||||
let index_count = indicies.len() as u32;
|
||||
|
||||
debug!(
|
||||
"Geometry created successfully: VB={:?}, IB={:?}, Indices={}",
|
||||
vertex_buffer, index_buffer, index_count
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
resource_manager,
|
||||
vertex_buffer,
|
||||
index_buffer,
|
||||
index_count,
|
||||
// vertex_count,
|
||||
})
|
||||
}
|
||||
|
||||
/// Binds the vertex and index buffers for drawing.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `device` - Raw `ash::Device` handle.
|
||||
/// * `command_buffer` - The command buffer to record binding commands into.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `ResourceManagerError` if buffer info cannot be retrieved.
|
||||
pub fn bind(&self, device: &ash::Device, command_buffer: vk::CommandBuffer) -> Result<()> {
|
||||
trace!(
|
||||
"Binding geometry: VB={:?}, IB={:?}",
|
||||
self.vertex_buffer,
|
||||
self.index_buffer
|
||||
);
|
||||
// Get buffer info (locks resource manager map briefly)
|
||||
let vb_info = self.resource_manager.get_buffer_info(self.vertex_buffer)?;
|
||||
let ib_info = self.resource_manager.get_buffer_info(self.index_buffer)?;
|
||||
|
||||
let vk_vertex_buffers = [vb_info.buffer];
|
||||
let offsets = [0_u64]; // Use vk::DeviceSize (u64)
|
||||
|
||||
unsafe {
|
||||
device.cmd_bind_vertex_buffers(
|
||||
command_buffer,
|
||||
0, // binding = 0
|
||||
&vk_vertex_buffers,
|
||||
&offsets,
|
||||
);
|
||||
device.cmd_bind_index_buffer(
|
||||
command_buffer,
|
||||
ib_info.buffer,
|
||||
0, // offset = 0
|
||||
vk::IndexType::UINT32,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Binds the geometry buffers and issues an indexed draw command.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `device` - Raw `ash::Device` handle.
|
||||
/// * `command_buffer` - The command buffer to record commands into.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `ResourceManagerError` if binding fails.
|
||||
pub fn draw(&self, device: &ash::Device, command_buffer: vk::CommandBuffer) -> Result<()> {
|
||||
self.bind(device, command_buffer)?; // Bind first
|
||||
trace!("Drawing geometry: {} indices", self.index_count);
|
||||
unsafe {
|
||||
device.cmd_draw_indexed(
|
||||
command_buffer,
|
||||
self.index_count, // Use stored index count
|
||||
1, // instance_count
|
||||
0, // first_index
|
||||
0, // vertex_offset
|
||||
0, // first_instance
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Geometry {
|
||||
fn drop(&mut self) {
|
||||
debug!(
|
||||
"Dropping Geometry: VB={:?}, IB={:?}",
|
||||
self.vertex_buffer, self.index_buffer
|
||||
);
|
||||
// Request destruction from the resource manager.
|
||||
// Ignore errors during drop, but log them.
|
||||
if let Err(e) = self.resource_manager.destroy_buffer(self.vertex_buffer) {
|
||||
tracing::error!(
|
||||
"Failed to destroy vertex buffer {:?} during Geometry drop: {}",
|
||||
self.vertex_buffer,
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(e) = self.resource_manager.destroy_buffer(self.index_buffer) {
|
||||
tracing::error!(
|
||||
"Failed to destroy index buffer {:?} during Geometry drop: {}",
|
||||
self.index_buffer,
|
||||
e
|
||||
);
|
||||
}
|
||||
// The Arc<ResourceManager> reference count decreases automatically.
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
59
crates/resource_manager/src/texture.rs
Normal file
59
crates/resource_manager/src/texture.rs
Normal 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
14
crates/scene/Cargo.toml
Normal 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
16
crates/scene/src/error.rs
Normal 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
411
crates/scene/src/lib.rs
Normal 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
|
||||
}
|
||||
11
crates/shared/Cargo.toml
Normal file
11
crates/shared/Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "shared"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
glam.workspace = true
|
||||
ash.workspace = true
|
||||
bytemuck.workspace = true
|
||||
memoffset = "0.9.1"
|
||||
derive_builder = "0.20.2"
|
||||
70
crates/shared/src/lib.rs
Normal file
70
crates/shared/src/lib.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
use ash::vk;
|
||||
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 normal: [f32; 3],
|
||||
pub tex_coord: [f32; 2],
|
||||
}
|
||||
|
||||
impl Vertex {
|
||||
pub fn get_binding_decription() -> vk::VertexInputBindingDescription {
|
||||
vk::VertexInputBindingDescription::default()
|
||||
.binding(0)
|
||||
.stride(size_of::<Self>() as u32)
|
||||
.input_rate(vk::VertexInputRate::VERTEX)
|
||||
}
|
||||
|
||||
pub fn get_attribute_descriptions() -> [vk::VertexInputAttributeDescription; 3] {
|
||||
[
|
||||
vk::VertexInputAttributeDescription::default()
|
||||
.location(0)
|
||||
.binding(0)
|
||||
.format(vk::Format::R32G32B32_SFLOAT)
|
||||
.offset(memoffset::offset_of!(Vertex, pos) as u32),
|
||||
vk::VertexInputAttributeDescription::default()
|
||||
.location(1)
|
||||
.binding(0)
|
||||
.format(vk::Format::R32G32B32_SFLOAT)
|
||||
.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),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, Copy, PartialEq)]
|
||||
pub struct UniformBufferObject {
|
||||
pub view: Mat4,
|
||||
pub proj: Mat4,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Copy, PartialEq)]
|
||||
pub struct CameraInfo {
|
||||
pub camera_pos: Vec3,
|
||||
pub camera_target: Vec3,
|
||||
pub camera_up: Vec3,
|
||||
pub camera_fov: f32,
|
||||
}
|
||||
|
||||
impl Default for CameraInfo {
|
||||
fn default() -> Self {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
0
crates/shared/src/material.rs
Normal file
0
crates/shared/src/material.rs
Normal file
45
flake.lock
generated
45
flake.lock
generated
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,34 @@
|
|||
|
||||
#version 450
|
||||
|
||||
// Input from vertex shader
|
||||
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;
|
||||
|
||||
// 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() {
|
||||
outColor = vec4(1.0, 0.5, 0.0, 1.0); // Orange
|
||||
// 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,38 @@
|
|||
#version 450
|
||||
vec2 positions[3] = vec2[](
|
||||
vec2(0.0, -0.5),
|
||||
vec2(0.5, 0.5),
|
||||
vec2(-0.5, 0.5)
|
||||
);
|
||||
|
||||
// INPUTS from Vertex Buffer (matching Vertex struct)
|
||||
layout(location = 0) in vec3 inPosition;
|
||||
layout(location = 1) in vec3 inNormal;
|
||||
layout(location = 2) in vec2 inTexCoord; // <<< MUST be vec2
|
||||
|
||||
// UNIFORMS (Set 0)
|
||||
layout(set = 0, binding = 0) uniform UniformBufferObject {
|
||||
mat4 view;
|
||||
mat4 proj;
|
||||
} ubo;
|
||||
|
||||
// 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() {
|
||||
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
|
||||
vec4 worldPos = pushConstants.model * vec4(inPosition, 1.0);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue