From a3d3f641cd8d9db2a256a60b0624a87b7db835db Mon Sep 17 00:00:00 2001 From: reo Date: Sat, 19 Jul 2025 00:15:21 +0300 Subject: [PATCH] Improve abstraction of the engine - Improved camera controls - Introduce new convenience function ecsr.load_mesh_from_gltf - Abstract out the glium stuff to allow for more backends in the future. We're still tied to winit though as it can be used with any of the major graphics libraries in the Rust ecosystem. --- Cargo.lock | 4 +- raidillon_game/Cargo.toml | 2 - raidillon_game/src/main.rs | 110 +++++++++++---------------- raidillon_render/Cargo.toml | 1 + raidillon_render/src/ecs_renderer.rs | 14 ++++ raidillon_render/src/lib.rs | 2 + raidillon_render/src/render.rs | 4 + raidillon_render/src/window.rs | 28 +++++++ raidillon_ui/Cargo.toml | 1 + raidillon_ui/src/ui.rs | 23 +++++- 10 files changed, 115 insertions(+), 74 deletions(-) create mode 100644 raidillon_render/src/window.rs diff --git a/Cargo.lock b/Cargo.lock index bc40061..0a091ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1743,8 +1743,6 @@ version = "0.1.0" dependencies = [ "anyhow", "glam", - "glium", - "glutin", "hecs", "raidillon_core", "raidillon_ecs", @@ -1774,6 +1772,7 @@ dependencies = [ "hecs", "image", "raidillon_ecs", + "winit", ] [[package]] @@ -1785,6 +1784,7 @@ dependencies = [ "imgui", "imgui-glium-renderer", "imgui-winit-support", + "raidillon_render", "winit", ] diff --git a/raidillon_game/Cargo.toml b/raidillon_game/Cargo.toml index a5aad2f..c127172 100644 --- a/raidillon_game/Cargo.toml +++ b/raidillon_game/Cargo.toml @@ -6,8 +6,6 @@ edition = "2021" [dependencies] anyhow = "1.0.98" glam = "0.30.4" -glium = { version = "0.35.0", features = ["glutin_backend", "simple_window_builder"] } -glutin = { version = "0.32.3", default-features = false } winit = "0.30" raidillon_render = { path = "../raidillon_render" } raidillon_ecs = { path = "../raidillon_ecs" } diff --git a/raidillon_game/src/main.rs b/raidillon_game/src/main.rs index 17d665c..825f1c2 100644 --- a/raidillon_game/src/main.rs +++ b/raidillon_game/src/main.rs @@ -1,13 +1,13 @@ use anyhow::Result; use glam::{Quat, Vec3, EulerRot}; -use glium::backend::glutin::SimpleWindowBuilder; use raidillon_core::Time; use raidillon_ecs::Transform; -use raidillon_render::{Camera, GliumRenderer, gltf_loader, ECSRenderer}; +use raidillon_render::{Camera, ECSRenderer, init_render_window, DisplayHandle}; use raidillon_ui::Gui; use raidillon_input::{Input, FPSCameraController}; use winit::keyboard::KeyCode; use winit::window::CursorGrabMode; +use winit::event::MouseButton; #[derive(Copy, Clone, Eq, PartialEq, Hash)] enum Action { @@ -18,24 +18,17 @@ enum Action { } fn main() -> Result<()> { - let event_loop = glium::winit::event_loop::EventLoop::builder() + let event_loop = winit::event_loop::EventLoop::builder() .build() .expect("create event-loop"); - let (window, display) = SimpleWindowBuilder::new() - .with_title("raidillon") - .with_inner_size(1280, 720) - .build(&event_loop); + let (window, _display): (winit::window::Window, DisplayHandle) = init_render_window(&event_loop, "raidillon", (1280, 720))?; // Create ECS renderer which internally owns both the world and the renderer - let mut ecsr = { - let world = hecs::World::new(); - let renderer = GliumRenderer::new(display.clone())?; - ECSRenderer::new(renderer, world) - }; + let mut ecsr = ECSRenderer::from_display_handle(&_display)?; // Dear ImGui integration - let mut gui = Gui::new(&display, &window)?; + let mut gui = Gui::new(&_display, &window)?; let mut input = Input::::new(); input.map_key(KeyCode::KeyW, Action::MoveForward); @@ -45,28 +38,21 @@ fn main() -> Result<()> { let mut camera_controller = FPSCameraController::new(Vec3::new(0.0, 0.0, 2.0)); - let mut cursor_grabbed = false; - let mut attempted_initial_grab = false; + let mut right_mouse_held = false; let mut time = Time::new(); - let object_ent = { - let model_3d = gltf_loader::load_gltf("resources/models/tree.gltf", &display)?; - ecsr.spawn_mesh(model_3d, Transform { - translation: Vec3::new(0.0, -2.5, -5.0), - rotation: Quat::IDENTITY, - scale: Vec3::new(0.01, 0.01, 0.01), - }) - }; + let object_ent = ecsr.load_mesh_from_gltf("resources/models/tree.gltf", Transform { + translation: Vec3::new(0.0, -2.5, -5.0), + rotation: Quat::IDENTITY, + scale: Vec3::new(0.01, 0.01, 0.01), + })?; - let ground_ent = { - let model_3d = gltf_loader::load_gltf("resources/models/plane.gltf", &display)?; - ecsr.spawn_mesh(model_3d, Transform { - translation: Vec3::new(0.0, -1.5, 0.0), - rotation: Quat::IDENTITY, - scale: Vec3::new(1.0, 1.0, 1.0), - }) - }; + let ground_ent = ecsr.load_mesh_from_gltf("resources/models/plane.gltf", Transform { + translation: Vec3::new(0.0, -1.5, 0.0), + rotation: Quat::IDENTITY, + scale: Vec3::new(1.0, 1.0, 1.0), + })?; let camera_ent = { @@ -84,7 +70,7 @@ fn main() -> Result<()> { event_loop .run(move |event, el| { - use glium::winit::event::{Event, WindowEvent}; + use winit::event::{Event, WindowEvent}; gui.handle_event(&window, &event); @@ -98,13 +84,33 @@ fn main() -> Result<()> { cam.aspect = sz.width as f32 / sz.height as f32; }); } + WindowEvent::MouseInput { state, button, .. } => { + if button == MouseButton::Right { + match state { + winit::event::ElementState::Pressed => { + if window + .set_cursor_grab(CursorGrabMode::Confined) + .or_else(|_| window.set_cursor_grab(CursorGrabMode::Locked)) + .is_ok() + { + window.set_cursor_visible(false); + right_mouse_held = true; + } + } + winit::event::ElementState::Released => { + let _ = window.set_cursor_grab(CursorGrabMode::None); + window.set_cursor_visible(true); + right_mouse_held = false; + } + } + } + } WindowEvent::RedrawRequested => { - let mut target = display.draw(); - - ecsr.render_into(&mut target); - - gui.render_with(&mut target, &window, |ui| { + gui.render_world(&mut ecsr, &window, |ui, ecsr| { if let Ok(mut tr) = ecsr.world.query_one_mut::<&mut Transform>(object_ent) { + ui.text("Hold right click to control the camera"); + ui.text("WASD to move"); + // Translation controls let mut translation = [tr.translation.x, tr.translation.y, tr.translation.z]; if ui.input_float3("Translation", &mut translation).build() { @@ -128,49 +134,21 @@ fn main() -> Result<()> { } } }); - target.finish().unwrap(); } _ => {} }, Event::AboutToWait => { time.tick(); - if !attempted_initial_grab { - attempted_initial_grab = true; - if window - .set_cursor_grab(CursorGrabMode::Confined) - .or_else(|_| window.set_cursor_grab(CursorGrabMode::Locked)) - .is_ok() - { - window.set_cursor_visible(false); - cursor_grabbed = true; - } - } - { let dt = time.delta_seconds(); camera_controller.update( &input, dt, - cursor_grabbed, + right_mouse_held, (Action::MoveForward, Action::MoveBackward, Action::MoveLeft, Action::MoveRight), ); - if input.key_pressed(KeyCode::Escape) { - if cursor_grabbed { - let _ = window.set_cursor_grab(CursorGrabMode::None); - window.set_cursor_visible(true); - cursor_grabbed = false; - } else if window - .set_cursor_grab(CursorGrabMode::Confined) - .or_else(|_| window.set_cursor_grab(CursorGrabMode::Locked)) - .is_ok() - { - window.set_cursor_visible(false); - cursor_grabbed = true; - } - } - if let Ok(mut cam) = ecsr.world.query_one_mut::<&mut Camera>(camera_ent) { cam.eye = camera_controller.position; cam.center = camera_controller.position + camera_controller.front(); diff --git a/raidillon_render/Cargo.toml b/raidillon_render/Cargo.toml index 1f5247c..ae6c94a 100644 --- a/raidillon_render/Cargo.toml +++ b/raidillon_render/Cargo.toml @@ -12,3 +12,4 @@ glutin = { version = "0.32.3", default-features = false } hecs = "0.10.5" image = "0.25.6" raidillon_ecs = { path = "../raidillon_ecs" } +winit = "0.30" diff --git a/raidillon_render/src/ecs_renderer.rs b/raidillon_render/src/ecs_renderer.rs index edac676..e21b036 100644 --- a/raidillon_render/src/ecs_renderer.rs +++ b/raidillon_render/src/ecs_renderer.rs @@ -12,6 +12,11 @@ pub struct ECSRenderer { } impl ECSRenderer { + pub fn from_display_handle(handle: &crate::window::DisplayHandle) -> anyhow::Result { + let world = World::new(); + let renderer = crate::render::GliumRenderer::new(handle.as_inner().clone())?; + Ok(Self { renderer, world }) + } pub fn new(renderer: GliumRenderer, world: World) -> Self { Self { renderer, world } } @@ -45,4 +50,13 @@ impl ECSRenderer { pub fn render_into(&mut self, target: &mut S) { self.renderer.render_into(&self.world, target); } + + pub fn load_mesh_from_gltf + std::fmt::Debug>( + &mut self, + path: P, + transform: Transform, + ) -> anyhow::Result { + let model = crate::gltf_loader::load_gltf(path, self.renderer.display())?; + Ok(self.spawn_mesh(model, transform)) + } } diff --git a/raidillon_render/src/lib.rs b/raidillon_render/src/lib.rs index 40e3033..f883c91 100644 --- a/raidillon_render/src/lib.rs +++ b/raidillon_render/src/lib.rs @@ -3,7 +3,9 @@ pub mod model; pub mod gltf_loader; pub mod render; pub mod ecs_renderer; +pub mod window; pub use camera::Camera; pub use render::GliumRenderer; pub use ecs_renderer::ECSRenderer; +pub use window::{DisplayHandle, init_window as init_render_window}; diff --git a/raidillon_render/src/render.rs b/raidillon_render/src/render.rs index 38f4175..1d7554d 100644 --- a/raidillon_render/src/render.rs +++ b/raidillon_render/src/render.rs @@ -160,4 +160,8 @@ impl GliumRenderer { self.draw_scene(world, &mut frame); frame.finish().unwrap(); } + + pub fn display(&self) -> &glium::Display { + &self.display + } } diff --git a/raidillon_render/src/window.rs b/raidillon_render/src/window.rs new file mode 100644 index 0000000..db0897c --- /dev/null +++ b/raidillon_render/src/window.rs @@ -0,0 +1,28 @@ +use glium::backend::glutin::SimpleWindowBuilder; +use glium::glutin::surface::WindowSurface; +use glium::Display; +use anyhow::Result; +use winit::event_loop::EventLoop; +use winit::window::Window; + +#[derive(Clone)] +pub struct DisplayHandle(Display); + +impl DisplayHandle { + pub fn as_inner(&self) -> &Display { + &self.0 + } +} + +pub fn init_window( + event_loop: &EventLoop, + title: &str, + size: (u32, u32), +) -> Result<(Window, DisplayHandle)> { + let (window, display) = SimpleWindowBuilder::new() + .with_title(title) + .with_inner_size(size.0, size.1) + .build(event_loop); + + Ok((window, DisplayHandle(display))) +} diff --git a/raidillon_ui/Cargo.toml b/raidillon_ui/Cargo.toml index d26d063..2f4b606 100644 --- a/raidillon_ui/Cargo.toml +++ b/raidillon_ui/Cargo.toml @@ -10,3 +10,4 @@ imgui = "0.12" imgui-winit-support = "0.13" imgui-glium-renderer = "0.13" winit = "0.30" +raidillon_render = { path = "../raidillon_render" } diff --git a/raidillon_ui/src/ui.rs b/raidillon_ui/src/ui.rs index 8ac7b0d..224ee1a 100644 --- a/raidillon_ui/src/ui.rs +++ b/raidillon_ui/src/ui.rs @@ -5,8 +5,8 @@ use imgui::{Context as ImguiContext, Ui}; use imgui_winit_support::{HiDpiMode, WinitPlatform}; use imgui_glium_renderer::Renderer as ImguiGliumRenderer; use winit::window::Window; -use glium::{Frame}; -use glium::glutin::surface::WindowSurface; +use glium::Frame; +use raidillon_render::{DisplayHandle, ECSRenderer}; /// Convenience wrapper that owns all ImGui state required for integration with /// winit + glium. @@ -18,13 +18,13 @@ pub struct Gui { } impl Gui { - pub fn new(display: &glium::Display, window: &Window) -> Result { + pub fn new(display: &DisplayHandle, window: &Window) -> Result { let mut imgui = ImguiContext::create(); imgui.set_ini_filename(None); let mut platform = WinitPlatform::new(&mut imgui); platform.attach_window(imgui.io_mut(), window, HiDpiMode::Default); imgui.fonts().add_font(&[imgui::FontSource::DefaultFontData { config: None }]); - let renderer = ImguiGliumRenderer::new(&mut imgui, display)?; + let renderer = ImguiGliumRenderer::new(&mut imgui, display.as_inner())?; Ok(Self { imgui, @@ -74,6 +74,21 @@ impl Gui { .expect("imgui rendering failed"); } + pub fn render_world(&mut self, ecsr: &mut ECSRenderer, window: &Window, build_ui: F) + where + F: FnOnce(&Ui, &mut ECSRenderer), + { + let mut target = ecsr.renderer.display().draw(); + + ecsr.render_into(&mut target); + + self.render_with(&mut target, window, |ui| { + build_ui(ui, ecsr); + }); + + target.finish().expect("Failed to swap buffers"); + } + pub fn ui(&mut self, build: F) where F: FnOnce(&Ui),