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.
This commit is contained in:
reo 2025-07-19 00:15:21 +03:00
parent 97195fbd05
commit a3d3f641cd
10 changed files with 115 additions and 74 deletions

4
Cargo.lock generated
View file

@ -1743,8 +1743,6 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"glam", "glam",
"glium",
"glutin",
"hecs", "hecs",
"raidillon_core", "raidillon_core",
"raidillon_ecs", "raidillon_ecs",
@ -1774,6 +1772,7 @@ dependencies = [
"hecs", "hecs",
"image", "image",
"raidillon_ecs", "raidillon_ecs",
"winit",
] ]
[[package]] [[package]]
@ -1785,6 +1784,7 @@ dependencies = [
"imgui", "imgui",
"imgui-glium-renderer", "imgui-glium-renderer",
"imgui-winit-support", "imgui-winit-support",
"raidillon_render",
"winit", "winit",
] ]

View file

@ -6,8 +6,6 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.98" anyhow = "1.0.98"
glam = "0.30.4" 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" winit = "0.30"
raidillon_render = { path = "../raidillon_render" } raidillon_render = { path = "../raidillon_render" }
raidillon_ecs = { path = "../raidillon_ecs" } raidillon_ecs = { path = "../raidillon_ecs" }

View file

@ -1,13 +1,13 @@
use anyhow::Result; use anyhow::Result;
use glam::{Quat, Vec3, EulerRot}; use glam::{Quat, Vec3, EulerRot};
use glium::backend::glutin::SimpleWindowBuilder;
use raidillon_core::Time; use raidillon_core::Time;
use raidillon_ecs::Transform; 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_ui::Gui;
use raidillon_input::{Input, FPSCameraController}; use raidillon_input::{Input, FPSCameraController};
use winit::keyboard::KeyCode; use winit::keyboard::KeyCode;
use winit::window::CursorGrabMode; use winit::window::CursorGrabMode;
use winit::event::MouseButton;
#[derive(Copy, Clone, Eq, PartialEq, Hash)] #[derive(Copy, Clone, Eq, PartialEq, Hash)]
enum Action { enum Action {
@ -18,24 +18,17 @@ enum Action {
} }
fn main() -> Result<()> { fn main() -> Result<()> {
let event_loop = glium::winit::event_loop::EventLoop::builder() let event_loop = winit::event_loop::EventLoop::builder()
.build() .build()
.expect("create event-loop"); .expect("create event-loop");
let (window, display) = SimpleWindowBuilder::new() let (window, _display): (winit::window::Window, DisplayHandle) = init_render_window(&event_loop, "raidillon", (1280, 720))?;
.with_title("raidillon")
.with_inner_size(1280, 720)
.build(&event_loop);
// Create ECS renderer which internally owns both the world and the renderer // Create ECS renderer which internally owns both the world and the renderer
let mut ecsr = { let mut ecsr = ECSRenderer::from_display_handle(&_display)?;
let world = hecs::World::new();
let renderer = GliumRenderer::new(display.clone())?;
ECSRenderer::new(renderer, world)
};
// Dear ImGui integration // Dear ImGui integration
let mut gui = Gui::new(&display, &window)?; let mut gui = Gui::new(&_display, &window)?;
let mut input = Input::<Action>::new(); let mut input = Input::<Action>::new();
input.map_key(KeyCode::KeyW, Action::MoveForward); 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 camera_controller = FPSCameraController::new(Vec3::new(0.0, 0.0, 2.0));
let mut cursor_grabbed = false; let mut right_mouse_held = false;
let mut attempted_initial_grab = false;
let mut time = Time::new(); let mut time = Time::new();
let object_ent = { let object_ent = ecsr.load_mesh_from_gltf("resources/models/tree.gltf", Transform {
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), translation: Vec3::new(0.0, -2.5, -5.0),
rotation: Quat::IDENTITY, rotation: Quat::IDENTITY,
scale: Vec3::new(0.01, 0.01, 0.01), scale: Vec3::new(0.01, 0.01, 0.01),
}) })?;
};
let ground_ent = { let ground_ent = ecsr.load_mesh_from_gltf("resources/models/plane.gltf", Transform {
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), translation: Vec3::new(0.0, -1.5, 0.0),
rotation: Quat::IDENTITY, rotation: Quat::IDENTITY,
scale: Vec3::new(1.0, 1.0, 1.0), scale: Vec3::new(1.0, 1.0, 1.0),
}) })?;
};
let camera_ent = { let camera_ent = {
@ -84,7 +70,7 @@ fn main() -> Result<()> {
event_loop event_loop
.run(move |event, el| { .run(move |event, el| {
use glium::winit::event::{Event, WindowEvent}; use winit::event::{Event, WindowEvent};
gui.handle_event(&window, &event); gui.handle_event(&window, &event);
@ -98,13 +84,33 @@ fn main() -> Result<()> {
cam.aspect = sz.width as f32 / sz.height as f32; 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 => { WindowEvent::RedrawRequested => {
let mut target = display.draw(); gui.render_world(&mut ecsr, &window, |ui, ecsr| {
ecsr.render_into(&mut target);
gui.render_with(&mut target, &window, |ui| {
if let Ok(mut tr) = ecsr.world.query_one_mut::<&mut Transform>(object_ent) { 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 // Translation controls
let mut translation = [tr.translation.x, tr.translation.y, tr.translation.z]; let mut translation = [tr.translation.x, tr.translation.y, tr.translation.z];
if ui.input_float3("Translation", &mut translation).build() { if ui.input_float3("Translation", &mut translation).build() {
@ -128,49 +134,21 @@ fn main() -> Result<()> {
} }
} }
}); });
target.finish().unwrap();
} }
_ => {} _ => {}
}, },
Event::AboutToWait => { Event::AboutToWait => {
time.tick(); 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(); let dt = time.delta_seconds();
camera_controller.update( camera_controller.update(
&input, &input,
dt, dt,
cursor_grabbed, right_mouse_held,
(Action::MoveForward, Action::MoveBackward, Action::MoveLeft, Action::MoveRight), (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) { if let Ok(mut cam) = ecsr.world.query_one_mut::<&mut Camera>(camera_ent) {
cam.eye = camera_controller.position; cam.eye = camera_controller.position;
cam.center = camera_controller.position + camera_controller.front(); cam.center = camera_controller.position + camera_controller.front();

View file

@ -12,3 +12,4 @@ glutin = { version = "0.32.3", default-features = false }
hecs = "0.10.5" hecs = "0.10.5"
image = "0.25.6" image = "0.25.6"
raidillon_ecs = { path = "../raidillon_ecs" } raidillon_ecs = { path = "../raidillon_ecs" }
winit = "0.30"

View file

@ -12,6 +12,11 @@ pub struct ECSRenderer {
} }
impl ECSRenderer { impl ECSRenderer {
pub fn from_display_handle(handle: &crate::window::DisplayHandle) -> anyhow::Result<Self> {
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 { pub fn new(renderer: GliumRenderer, world: World) -> Self {
Self { renderer, world } Self { renderer, world }
} }
@ -45,4 +50,13 @@ impl ECSRenderer {
pub fn render_into<S: glium::Surface>(&mut self, target: &mut S) { pub fn render_into<S: glium::Surface>(&mut self, target: &mut S) {
self.renderer.render_into(&self.world, target); self.renderer.render_into(&self.world, target);
} }
pub fn load_mesh_from_gltf<P: AsRef<std::path::Path> + std::fmt::Debug>(
&mut self,
path: P,
transform: Transform,
) -> anyhow::Result<Entity> {
let model = crate::gltf_loader::load_gltf(path, self.renderer.display())?;
Ok(self.spawn_mesh(model, transform))
}
} }

View file

@ -3,7 +3,9 @@ pub mod model;
pub mod gltf_loader; pub mod gltf_loader;
pub mod render; pub mod render;
pub mod ecs_renderer; pub mod ecs_renderer;
pub mod window;
pub use camera::Camera; pub use camera::Camera;
pub use render::GliumRenderer; pub use render::GliumRenderer;
pub use ecs_renderer::ECSRenderer; pub use ecs_renderer::ECSRenderer;
pub use window::{DisplayHandle, init_window as init_render_window};

View file

@ -160,4 +160,8 @@ impl GliumRenderer {
self.draw_scene(world, &mut frame); self.draw_scene(world, &mut frame);
frame.finish().unwrap(); frame.finish().unwrap();
} }
pub fn display(&self) -> &glium::Display<WindowSurface> {
&self.display
}
} }

View file

@ -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<WindowSurface>);
impl DisplayHandle {
pub fn as_inner(&self) -> &Display<WindowSurface> {
&self.0
}
}
pub fn init_window<T>(
event_loop: &EventLoop<T>,
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)))
}

View file

@ -10,3 +10,4 @@ imgui = "0.12"
imgui-winit-support = "0.13" imgui-winit-support = "0.13"
imgui-glium-renderer = "0.13" imgui-glium-renderer = "0.13"
winit = "0.30" winit = "0.30"
raidillon_render = { path = "../raidillon_render" }

View file

@ -5,8 +5,8 @@ use imgui::{Context as ImguiContext, Ui};
use imgui_winit_support::{HiDpiMode, WinitPlatform}; use imgui_winit_support::{HiDpiMode, WinitPlatform};
use imgui_glium_renderer::Renderer as ImguiGliumRenderer; use imgui_glium_renderer::Renderer as ImguiGliumRenderer;
use winit::window::Window; use winit::window::Window;
use glium::{Frame}; use glium::Frame;
use glium::glutin::surface::WindowSurface; use raidillon_render::{DisplayHandle, ECSRenderer};
/// Convenience wrapper that owns all ImGui state required for integration with /// Convenience wrapper that owns all ImGui state required for integration with
/// winit + glium. /// winit + glium.
@ -18,13 +18,13 @@ pub struct Gui {
} }
impl Gui { impl Gui {
pub fn new(display: &glium::Display<WindowSurface>, window: &Window) -> Result<Self> { pub fn new(display: &DisplayHandle, window: &Window) -> Result<Self> {
let mut imgui = ImguiContext::create(); let mut imgui = ImguiContext::create();
imgui.set_ini_filename(None); imgui.set_ini_filename(None);
let mut platform = WinitPlatform::new(&mut imgui); let mut platform = WinitPlatform::new(&mut imgui);
platform.attach_window(imgui.io_mut(), window, HiDpiMode::Default); platform.attach_window(imgui.io_mut(), window, HiDpiMode::Default);
imgui.fonts().add_font(&[imgui::FontSource::DefaultFontData { config: None }]); 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 { Ok(Self {
imgui, imgui,
@ -74,6 +74,21 @@ impl Gui {
.expect("imgui rendering failed"); .expect("imgui rendering failed");
} }
pub fn render_world<F>(&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<F>(&mut self, build: F) pub fn ui<F>(&mut self, build: F)
where where
F: FnOnce(&Ui), F: FnOnce(&Ui),