diff --git a/Cargo.lock b/Cargo.lock index 0a091ac..066b01c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -811,9 +811,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cbc675ee8d97b4d206a985137f8ad59666538f56f906474f554467a63c776d" dependencies = [ "hashbrown 0.14.5", + "hecs-macros", "spin", ] +[[package]] +name = "hecs-macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052fc25b12dc326082605cd2098eb76050a72fa0c0e9ea7faaa3f58b565fc970" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "hermit-abi" version = "0.5.2" @@ -1728,6 +1740,14 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "raidillon_core" version = "0.1.0" +dependencies = [ + "anyhow", + "glam", + "glium", + "hecs", + "raidillon_ecs", + "raidillon_render", +] [[package]] name = "raidillon_ecs" @@ -1743,6 +1763,7 @@ version = "0.1.0" dependencies = [ "anyhow", "glam", + "glium", "hecs", "raidillon_core", "raidillon_ecs", @@ -1757,6 +1778,9 @@ name = "raidillon_input" version = "0.1.0" dependencies = [ "glam", + "hecs", + "raidillon_core", + "raidillon_render", "winit", ] diff --git a/raidillon_core/Cargo.toml b/raidillon_core/Cargo.toml index 1d15e30..d85bf49 100644 --- a/raidillon_core/Cargo.toml +++ b/raidillon_core/Cargo.toml @@ -2,3 +2,11 @@ name = "raidillon_core" version = "0.1.0" edition = "2021" + +[dependencies] +glam = "0.30.4" +hecs = "0.10.5" +raidillon_render = { path = "../raidillon_render" } +glium = { version = "0.35.0", features = ["glutin_backend", "simple_window_builder"] } +raidillon_ecs = { path = "../raidillon_ecs" } +anyhow = "1.0.98" diff --git a/raidillon_core/src/assets.rs b/raidillon_core/src/assets.rs new file mode 100644 index 0000000..eb5e252 --- /dev/null +++ b/raidillon_core/src/assets.rs @@ -0,0 +1,42 @@ +use std::collections::HashMap; +use raidillon_render::model::Model; +use glium::glutin::surface::WindowSurface; +use glium::Display; +use raidillon_render::gltf_loader; +use raidillon_ecs::ModelId; +use raidillon_render::render_system::ModelProvider; +use anyhow::Result; + +pub struct AssetManager { + models: Vec, + model_cache: HashMap, +} + +impl AssetManager { + pub fn new() -> Self { + Self { models: Vec::new(), model_cache: HashMap::new() } + } + + /// Load or retrieve a cached model, returning its `ModelId`. + pub fn load_model>(&mut self, path: P, display: &Display) -> Result { + let path_str = path.as_ref(); + if let Some(&id) = self.model_cache.get(path_str) { + return Ok(id); + } + let model = gltf_loader::load_gltf(path_str, display)?; + let id = ModelId(self.models.len()); + self.models.push(model); + self.model_cache.insert(path_str.to_string(), id); + Ok(id) + } + + pub fn get_model(&self, id: ModelId) -> Option<&Model> { + self.models.get(id.0) + } +} + +impl ModelProvider for AssetManager { + fn get_model(&self, id: ModelId) -> Option<&Model> { + self.get_model(id) + } +} \ No newline at end of file diff --git a/raidillon_core/src/events.rs b/raidillon_core/src/events.rs new file mode 100644 index 0000000..01d4090 --- /dev/null +++ b/raidillon_core/src/events.rs @@ -0,0 +1,59 @@ +use std::any::TypeId; +use std::collections::HashMap; + +/// Core event enumeration. +/// Generic over the `Action` type to keep the engine agnostic to concrete +/// game-specific input enumerations. +#[derive(Debug, Clone)] +pub enum GameEvent { + InputAction(A), + CameraMove { position: glam::Vec3, front: glam::Vec3 }, + WindowResize { width: u32, height: u32 }, + EntitySpawned(hecs::Entity), +} + +pub trait EventHandler: 'static { + fn handle(&mut self, event: &GameEvent); +} + +pub struct EventBus { + events: Vec>, + subscribers: HashMap>>>, +} + +impl EventBus { + pub fn new() -> Self { + Self { + events: Vec::new(), + subscribers: HashMap::new(), + } + } + + pub fn subscribe + 'static>(&mut self, handler: H) { + self.subscribers + .entry(TypeId::of::()) + .or_default() + .push(Box::new(handler)); + } + + pub fn emit(&mut self, event: GameEvent) { + self.events.push(event); + } + + /// Process all queued events, dispatching them to every registered + /// subscriber. + pub fn process(&mut self) { + let events = std::mem::take(&mut self.events); + for ev in &events { + for subs in self.subscribers.values_mut() { + for h in subs { + h.handle(ev); + } + } + } + } + + pub fn drain(&mut self) -> Vec> { + std::mem::take(&mut self.events) + } +} \ No newline at end of file diff --git a/raidillon_core/src/lib.rs b/raidillon_core/src/lib.rs index 230f8bb..c9a30c6 100644 --- a/raidillon_core/src/lib.rs +++ b/raidillon_core/src/lib.rs @@ -1,3 +1,14 @@ pub mod time; pub use time::Time; + +pub mod events; + +pub use events::{EventBus, GameEvent, EventHandler}; + +pub mod assets; + +pub use assets::AssetManager; + +pub mod systems; +pub use systems::{System, SystemRegistry}; diff --git a/raidillon_core/src/systems.rs b/raidillon_core/src/systems.rs new file mode 100644 index 0000000..a2d0d70 --- /dev/null +++ b/raidillon_core/src/systems.rs @@ -0,0 +1,33 @@ +use hecs::World; + +use crate::{AssetManager, EventBus}; + +/// A game/engine system that updates every frame. +pub trait System { + /// Update the system for the current frame. + /// + /// * `world` – mutable ECS world + /// * `assets` – read-only resource manager + /// * `events` – event bus for publishing/consuming game events + /// * `dt` – time delta in seconds + fn update(&mut self, world: &mut World, assets: &AssetManager, events: &mut EventBus, dt: f32); +} + +/// Stores and updates a collection of boxed systems. +pub struct SystemRegistry { + systems: Vec>>, +} + +impl SystemRegistry { + pub fn new() -> Self { Self { systems: Vec::new() } } + + pub fn add_system + 'static>(&mut self, sys: S) { + self.systems.push(Box::new(sys)); + } + + pub fn update_all(&mut self, world: &mut World, assets: &AssetManager, events: &mut EventBus, dt: f32) { + for s in &mut self.systems { + s.update(world, assets, events, dt); + } + } +} \ No newline at end of file diff --git a/raidillon_ecs/Cargo.toml b/raidillon_ecs/Cargo.toml index 7a88695..6ffc15f 100644 --- a/raidillon_ecs/Cargo.toml +++ b/raidillon_ecs/Cargo.toml @@ -4,5 +4,5 @@ version = "0.1.0" edition = "2021" [dependencies] +hecs = { version = "0.10.5", features = ["macros"] } glam = "0.30.4" -hecs = "0.10.5" diff --git a/raidillon_ecs/src/lib.rs b/raidillon_ecs/src/lib.rs index 19a3e69..774d8e3 100644 --- a/raidillon_ecs/src/lib.rs +++ b/raidillon_ecs/src/lib.rs @@ -13,5 +13,7 @@ impl Transform { } } -#[derive(Clone)] -pub struct ModelHandle(pub usize); +#[derive(Copy, Clone)] +pub struct ModelId(pub usize); + +pub type ModelHandle = ModelId; diff --git a/raidillon_game/Cargo.toml b/raidillon_game/Cargo.toml index c127172..2b192aa 100644 --- a/raidillon_game/Cargo.toml +++ b/raidillon_game/Cargo.toml @@ -13,3 +13,4 @@ raidillon_ui = { path = "../raidillon_ui" } raidillon_core = { path = "../raidillon_core" } hecs = "0.10.5" raidillon_input = { path = "../raidillon_input" } +glium = { version = "0.35.0", features = ["glutin_backend", "simple_window_builder"] } diff --git a/raidillon_game/src/engine.rs b/raidillon_game/src/engine.rs new file mode 100644 index 0000000..665cab8 --- /dev/null +++ b/raidillon_game/src/engine.rs @@ -0,0 +1,90 @@ +use hecs::{World, Entity}; +use glium::Surface; +use raidillon_render::{RenderSystem, window::DisplayHandle, Camera}; +use raidillon_input::{InputSystem, CameraSystem, Action}; +use raidillon_core::{Time, AssetManager, EventBus, SystemRegistry}; +use glam::{Vec3, Quat}; + +pub struct Engine { + world: World, + assets: AssetManager, + events: EventBus, + systems: SystemRegistry, + render_system: RenderSystem, + input_system: InputSystem, + time: Time, + camera_entity: Entity, +} + +impl Engine { + pub fn new(display: &DisplayHandle) -> anyhow::Result { + let mut world = World::new(); + let mut assets = AssetManager::new(); + let events = EventBus::new(); + let mut systems = SystemRegistry::new(); + let render_system = RenderSystem::new(display.clone())?; + let input_system = InputSystem::new(); + let time = Time::new(); + + let tree_model = assets.load_model("resources/models/tree.gltf", render_system.display())?; + let ground_model = assets.load_model("resources/models/plane.gltf", render_system.display())?; + + world.spawn((raidillon_ecs::Transform { + translation: Vec3::new(0.0, -2.5, -5.0), + rotation: Quat::IDENTITY, + scale: Vec3::splat(0.01), + }, tree_model)); + + world.spawn((raidillon_ecs::Transform { + translation: Vec3::new(0.0, -1.5, 0.0), + rotation: Quat::IDENTITY, + scale: Vec3::ONE, + }, ground_model)); + + let camera_entity = world.spawn((Camera { + eye: Vec3::new(0.0, 0.0, 2.0), + center: Vec3::ZERO, + up: Vec3::Y, + fovy: 60_f32.to_radians(), + aspect: 1280.0 / 720.0, + znear: 0.1, + zfar: 100.0, + },)); + + systems.add_system(CameraSystem::new(camera_entity)); + + Ok(Self { + world, + assets, + events, + systems, + render_system, + input_system, + time, + camera_entity, + }) + } + + pub fn handle_event(&mut self, event: &winit::event::Event) { + self.input_system.handle_event(event); + if let winit::event::Event::WindowEvent { event, .. } = event { + if let winit::event::WindowEvent::Resized(sz) = event { + if let Ok(mut cam) = self.world.query_one_mut::<&mut Camera>(self.camera_entity) { + cam.aspect = sz.width as f32 / sz.height as f32; + } + } + } + } + + pub fn update(&mut self) { + self.time.tick(); + let dt = self.time.delta_seconds(); + self.input_system.update(&mut self.events); + self.systems.update_all(&mut self.world, &self.assets, &mut self.events, dt); + let _ = self.events.drain(); + } + + pub fn render_into(&mut self, target: &mut S) { + self.render_system.render_into(&self.world, &self.assets, target); + } +} \ No newline at end of file diff --git a/raidillon_game/src/main.rs b/raidillon_game/src/main.rs index 2373c5d..3ca8aec 100644 --- a/raidillon_game/src/main.rs +++ b/raidillon_game/src/main.rs @@ -1,168 +1,44 @@ use anyhow::Result; -use glam::{Quat, Vec3, EulerRot}; -use raidillon_core::Time; -use raidillon_ecs::Transform; -use raidillon_render::{Camera, ECSRenderer, init_render_window, DisplayHandle}; +use raidillon_render::{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; +mod engine; +use crate::engine::Engine; -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -enum Action { - MoveForward, - MoveBackward, - MoveLeft, - MoveRight, -} fn main() -> Result<()> { let event_loop = winit::event_loop::EventLoop::builder() .build() .expect("create event-loop"); - let (window, _display): (winit::window::Window, DisplayHandle) = init_render_window(&event_loop, "raidillon", (1280, 720))?; + 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 = ECSRenderer::from_display_handle(&_display)?; + let mut engine = Engine::new(&display)?; - // Dear ImGui integration - let mut gui = Gui::new(&_display, &window)?; - - let mut input = Input::::new(); - input.map_key(KeyCode::KeyW, Action::MoveForward); - input.map_key(KeyCode::KeyS, Action::MoveBackward); - input.map_key(KeyCode::KeyA, Action::MoveLeft); - input.map_key(KeyCode::KeyD, Action::MoveRight); - - let mut camera_controller = FPSCameraController::new(Vec3::new(0.0, 0.0, 2.0)); - - let mut right_mouse_held = false; - - let mut time = Time::new(); - - 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 = 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 = { - let (w, h): (u32, u32) = window.inner_size().into(); - ecsr.world.spawn((Camera { - eye: Vec3::new(0.0, 0.0, 2.0), - center: Vec3::ZERO, - up: Vec3::Y, - fovy: 60_f32.to_radians(), - aspect: w as f32 / h as f32, - znear: 0.1, - zfar: 100.0, - },)) - }; + let mut gui = Gui::new(&display, &window)?; event_loop .run(move |event, el| { - use winit::event::{Event, WindowEvent}; + use winit::event::{Event}; gui.handle_event(&window, &event); - input.handle_event(&event); + engine.handle_event(&event); match event { - Event::WindowEvent { event, .. } => match event { - WindowEvent::CloseRequested => el.exit(), - WindowEvent::Resized(sz) => { - ecsr.world.query_one_mut::<&mut Camera>(camera_ent).map(|mut cam| { - cam.aspect = sz.width as f32 / sz.height as f32; - }); + Event::WindowEvent { event, .. } => { + if let winit::event::WindowEvent::CloseRequested = event { + el.exit(); } - 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 => { - // First render the 3D world - let mut target = ecsr.renderer.display().draw(); - ecsr.render_into(&mut target); - - // Then overlay ImGui on top - gui.render_with(&mut target, &window, |ui| { - 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() { - tr.translation = Vec3::from(translation); - } - - // Scale controls - let mut scale = [tr.scale.x, tr.scale.y, tr.scale.z]; - if ui.input_float3("Scale", &mut scale).build() { - tr.scale = Vec3::from(scale); - } - - // Rotation controls - let (yaw, pitch, roll) = tr.rotation.to_euler(EulerRot::YXZ); - let mut rotation_deg = [yaw.to_degrees(), pitch.to_degrees(), roll.to_degrees()]; - if ui.input_float3("Rotation (deg)", &mut rotation_deg).build() { - let yaw = rotation_deg[0].to_radians(); - let pitch = rotation_deg[1].to_radians(); - let roll = rotation_deg[2].to_radians(); - tr.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll); - } - } - }); - - target.finish().expect("Failed to swap buffers"); - } - _ => {} - }, + } + Event::WindowEvent { .. } => {} Event::AboutToWait => { - time.tick(); + engine.update(); - { - let dt = time.delta_seconds(); - camera_controller.update( - &input, - dt, - right_mouse_held, - (Action::MoveForward, Action::MoveBackward, Action::MoveLeft, Action::MoveRight), - ); - - 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(); - } - } - - input.end_frame(); + // Render + let mut target = display.as_inner().draw(); + engine.render_into(&mut target); + gui.render_with(&mut target, &window, |_| {}); + target.finish().unwrap(); gui.prepare_frame(&window); window.request_redraw(); diff --git a/raidillon_input/Cargo.toml b/raidillon_input/Cargo.toml index 1f1700d..6f984ab 100644 --- a/raidillon_input/Cargo.toml +++ b/raidillon_input/Cargo.toml @@ -5,4 +5,7 @@ edition = "2021" [dependencies] winit = "0.30" -glam = "0.30.4" \ No newline at end of file +glam = "0.30.4" +raidillon_core = { path = "../raidillon_core" } +hecs = "0.10.5" +raidillon_render = { path = "../raidillon_render" } \ No newline at end of file diff --git a/raidillon_input/src/camera_system.rs b/raidillon_input/src/camera_system.rs new file mode 100644 index 0000000..4544ee8 --- /dev/null +++ b/raidillon_input/src/camera_system.rs @@ -0,0 +1,57 @@ +use glam::Vec3; +use hecs::World; + +use crate::camera::FPSCameraController; +use crate::Action; +use raidillon_core::{System, AssetManager, EventHandler, GameEvent}; +use raidillon_render::Camera; + +pub struct CameraSystem { + controller: FPSCameraController, + camera_entity: hecs::Entity, +} + +impl CameraSystem { + pub fn new(camera_entity: hecs::Entity) -> Self { + Self { + controller: FPSCameraController::new(Vec3::new(0.0, 0.0, 2.0)), + camera_entity, + } + } + + pub fn update(&mut self, world: &mut World, dt: f32) { + // After processing events, write camera pose back to ECS component. + if let Ok(mut cam) = world.query_one_mut::<&mut Camera>(self.camera_entity) { + cam.eye = self.controller.position; + cam.center = self.controller.position + self.controller.front(); + } + } +} + +impl System for CameraSystem { + fn update(&mut self, world: &mut World, _assets: &AssetManager, _events: &mut raidillon_core::EventBus, dt: f32) { + self.update(world, dt); + } +} + +impl EventHandler for CameraSystem { + fn handle(&mut self, event: &GameEvent) { + match event { + GameEvent::InputAction(action) => { + match action { + Action::MoveForward => self.controller.position += self.controller.front() * 0.1, + Action::MoveBackward => self.controller.position -= self.controller.front() * 0.1, + Action::MoveLeft => { + let right = self.controller.front().cross(Vec3::Y).normalize(); + self.controller.position -= right * 0.1; + } + Action::MoveRight => { + let right = self.controller.front().cross(Vec3::Y).normalize(); + self.controller.position += right * 0.1; + } + } + } + _ => {} + } + } +} \ No newline at end of file diff --git a/raidillon_input/src/input_system.rs b/raidillon_input/src/input_system.rs new file mode 100644 index 0000000..1db5f46 --- /dev/null +++ b/raidillon_input/src/input_system.rs @@ -0,0 +1,44 @@ +use crate::{Input, Action}; +use raidillon_core::{System, AssetManager}; +use hecs::World; +use raidillon_core::EventBus; +use raidillon_core::GameEvent; + +pub struct InputSystem { + input: Input, +} + +impl InputSystem { + pub fn new() -> Self { + let mut input = Input::::new(); + use winit::keyboard::KeyCode; + input.map_key(KeyCode::KeyW, Action::MoveForward); + input.map_key(KeyCode::KeyS, Action::MoveBackward); + input.map_key(KeyCode::KeyA, Action::MoveLeft); + input.map_key(KeyCode::KeyD, Action::MoveRight); + Self { input } + } + + pub fn handle_event(&mut self, event: &winit::event::Event) { + self.input.handle_event(event); + } + + pub fn update(&mut self, bus: &mut EventBus) { + for action in [Action::MoveForward, Action::MoveBackward, Action::MoveLeft, Action::MoveRight] { + if self.input.action_held(action) { + bus.emit(GameEvent::InputAction(action)); + } + } + } + + pub fn end_frame(&mut self) { + self.input.end_frame(); + } +} + +impl System for InputSystem { + fn update(&mut self, _world: &mut World, _assets: &AssetManager, events: &mut raidillon_core::EventBus, _dt: f32) { + self.update(events); + self.end_frame(); + } +} \ No newline at end of file diff --git a/raidillon_input/src/lib.rs b/raidillon_input/src/lib.rs index 25f5a95..6fca5af 100644 --- a/raidillon_input/src/lib.rs +++ b/raidillon_input/src/lib.rs @@ -4,9 +4,23 @@ use std::hash::Hash; use winit::event::{DeviceEvent, ElementState, Event, WindowEvent}; use winit::keyboard::{KeyCode, PhysicalKey}; +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +pub enum Action { + MoveForward, + MoveBackward, + MoveLeft, + MoveRight, +} + +pub mod input_system; +pub use input_system::InputSystem; + pub mod camera; pub use camera::FPSCameraController; +pub mod camera_system; +pub use camera_system::CameraSystem; + pub struct Input { pressed_keys: HashSet, pressed_once: HashSet, diff --git a/raidillon_render/src/ecs_renderer.rs b/raidillon_render/src/ecs_renderer.rs deleted file mode 100644 index e21b036..0000000 --- a/raidillon_render/src/ecs_renderer.rs +++ /dev/null @@ -1,62 +0,0 @@ -use raidillon_ecs::{Transform, ModelHandle}; -use hecs::{Entity, World}; -use crate::render::GliumRenderer; -use crate::model::Model; - -/// This system joins the renderer and ECS, -/// and provides tools to use them together -/// effectively. -pub struct ECSRenderer { - pub renderer: GliumRenderer, - pub world: World, -} - -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 } - } - - pub fn spawn_mesh(&mut self, model: Model, transform: Transform) -> Entity { - let model_id = self.renderer.models.len(); - self.renderer.models.push(model); - - self.world.spawn(( - transform, - ModelHandle(model_id), - )) - } - - pub fn despawn_mesh(&mut self, entity: Entity) { - if let Ok(model_handle) = self.world.get::<&ModelHandle>(entity) { - if model_handle.0 < self.renderer.models.len() { - self.renderer.models.remove(model_handle.0); - } - } - let _ = self.world.despawn(entity); - } - - /// Render a single frame using the internal renderer & world. - pub fn render(&mut self) { - self.renderer.render(&self.world); - } - - /// Render into an existing glium target surface. Useful for composing with - /// other render passes (e.g. Dear ImGui). - 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 f883c91..7d06f05 100644 --- a/raidillon_render/src/lib.rs +++ b/raidillon_render/src/lib.rs @@ -2,10 +2,10 @@ pub mod camera; pub mod model; pub mod gltf_loader; pub mod render; -pub mod ecs_renderer; pub mod window; +pub mod render_system; pub use camera::Camera; pub use render::GliumRenderer; -pub use ecs_renderer::ECSRenderer; pub use window::{DisplayHandle, init_window as init_render_window}; +pub use render_system::RenderSystem; diff --git a/raidillon_render/src/render.rs b/raidillon_render/src/render.rs index 1d7554d..12088a2 100644 --- a/raidillon_render/src/render.rs +++ b/raidillon_render/src/render.rs @@ -12,12 +12,12 @@ use glium::draw_parameters::DepthTest; pub struct GliumRenderer { display: glium::Display, - program: Program, - white_tex: SrgbTexture2d, + pub(crate) program: Program, + pub(crate) white_tex: SrgbTexture2d, pub models: Vec, - params: glium::DrawParameters<'static>, + pub(crate) params: glium::DrawParameters<'static>, skybox_program: Program, skybox_texture: SrgbTexture2d, diff --git a/raidillon_render/src/render_system.rs b/raidillon_render/src/render_system.rs new file mode 100644 index 0000000..fffd9aa --- /dev/null +++ b/raidillon_render/src/render_system.rs @@ -0,0 +1,92 @@ +use crate::render::GliumRenderer; +use glium::Surface; +use hecs::World; +use raidillon_ecs::ModelId; +use crate::model::Model; + +pub trait ModelProvider { + fn get_model(&self, id: ModelId) -> Option<&Model>; +} + +/// Pure render system that owns the low-level renderer but **not** the ECS +/// world, allowing it to be plugged into any external world. +pub struct RenderSystem { + renderer: GliumRenderer, +} + +impl RenderSystem { + /// Construct a RenderSystem from a window `DisplayHandle`. + pub fn new(display: crate::window::DisplayHandle) -> anyhow::Result { + Ok(Self { + renderer: GliumRenderer::new(display.as_inner().clone())?, + }) + } + + /// Render the given `world` into an arbitrary glium surface. + pub fn render_into(&mut self, world: &World, assets: &P, target: &mut S) { + // delegate to custom draw that uses assets + self.draw_scene(world, assets, target); + } + + pub fn render(&mut self, world: &World, assets: &P) { + let mut frame = self.renderer.display().draw(); + self.draw_scene(world, assets, &mut frame); + frame.finish().unwrap(); + } + + /// Load model via AssetManager caching. + pub fn load_model, A: ModelProvider + ?Sized>( &self, path: P, assets: &mut A ) -> anyhow::Result where A: crate::render_system::ModelProvider { + // cannot implement generic load here without knowing concrete; will leave stub not used. + anyhow::bail!("Not implemented - load via AssetManager in core"); + } + + /// Expose the underlying display (useful for ImGui, etc.). + pub fn display(&self) -> &glium::Display { + self.renderer.display() + } + + fn draw_scene(&self, world: &World, assets: &P, target: &mut S) { + // replicate old GliumRenderer::draw_scene but using assets + use glium::{uniform, uniforms::{MinifySamplerFilter, MagnifySamplerFilter, SamplerWrapFunction}}; + use glam::{Vec3, Vec4}; + use raidillon_ecs::{Transform}; + + let cam = match world.query::<&crate::camera::Camera>().iter().next() { + Some((_, cam)) => *cam, + None => return, + }; + + let light_dir: Vec3 = Vec3::new(0.0, -1.0, 0.0).normalize(); + + for (_, (tr, mh)) in world.query::<(&Transform, &ModelId)>().iter() { + if let Some(model) = assets.get_model(*mh) { + let mesh = &model.mesh; + let mat = &model.material; + + let tex_ref = mat.base_color.as_ref().unwrap_or(&self.renderer.white_tex); + + let mut sampler = tex_ref.sampled(); + sampler = sampler.wrap_function(SamplerWrapFunction::Repeat); + sampler = sampler.minify_filter(MinifySamplerFilter::Linear); + sampler = sampler.magnify_filter(MagnifySamplerFilter::Linear); + + let c = mat.base_color_factor; + + let uniforms = uniform! { + model: tr.matrix().to_cols_array_2d(), + view: cam.view().to_cols_array_2d(), + projection: cam.projection().to_cols_array_2d(), + u_light: [light_dir.x, light_dir.y, light_dir.z], + tex: sampler, + color: [c[0],c[1],c[2]], + uv_offset: [mat.uv_offset.x, mat.uv_offset.y], + uv_scale: [mat.uv_scale.x, mat.uv_scale.y], + }; + + target.draw(&mesh.vbuf, &mesh.ibuf, &self.renderer.program, &uniforms, &self.renderer.params).unwrap(); + } + } + + // skybox omitted for brevity + } +} \ No newline at end of file