o3 refactor

This commit is contained in:
reo 2025-07-22 21:12:05 +03:00
parent 341d531db3
commit 049f522bb1
19 changed files with 508 additions and 214 deletions

24
Cargo.lock generated
View file

@ -811,9 +811,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cbc675ee8d97b4d206a985137f8ad59666538f56f906474f554467a63c776d" checksum = "e1cbc675ee8d97b4d206a985137f8ad59666538f56f906474f554467a63c776d"
dependencies = [ dependencies = [
"hashbrown 0.14.5", "hashbrown 0.14.5",
"hecs-macros",
"spin", "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]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.5.2" version = "0.5.2"
@ -1728,6 +1740,14 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]] [[package]]
name = "raidillon_core" name = "raidillon_core"
version = "0.1.0" version = "0.1.0"
dependencies = [
"anyhow",
"glam",
"glium",
"hecs",
"raidillon_ecs",
"raidillon_render",
]
[[package]] [[package]]
name = "raidillon_ecs" name = "raidillon_ecs"
@ -1743,6 +1763,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"glam", "glam",
"glium",
"hecs", "hecs",
"raidillon_core", "raidillon_core",
"raidillon_ecs", "raidillon_ecs",
@ -1757,6 +1778,9 @@ name = "raidillon_input"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"glam", "glam",
"hecs",
"raidillon_core",
"raidillon_render",
"winit", "winit",
] ]

View file

@ -2,3 +2,11 @@
name = "raidillon_core" name = "raidillon_core"
version = "0.1.0" version = "0.1.0"
edition = "2021" 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"

View file

@ -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>,
model_cache: HashMap<String, ModelId>,
}
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<P: AsRef<str>>(&mut self, path: P, display: &Display<WindowSurface>) -> Result<ModelId> {
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)
}
}

View file

@ -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<A> {
InputAction(A),
CameraMove { position: glam::Vec3, front: glam::Vec3 },
WindowResize { width: u32, height: u32 },
EntitySpawned(hecs::Entity),
}
pub trait EventHandler<A>: 'static {
fn handle(&mut self, event: &GameEvent<A>);
}
pub struct EventBus<A> {
events: Vec<GameEvent<A>>,
subscribers: HashMap<TypeId, Vec<Box<dyn EventHandler<A>>>>,
}
impl<A: 'static + Clone> EventBus<A> {
pub fn new() -> Self {
Self {
events: Vec::new(),
subscribers: HashMap::new(),
}
}
pub fn subscribe<H: EventHandler<A> + 'static>(&mut self, handler: H) {
self.subscribers
.entry(TypeId::of::<H>())
.or_default()
.push(Box::new(handler));
}
pub fn emit(&mut self, event: GameEvent<A>) {
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<GameEvent<A>> {
std::mem::take(&mut self.events)
}
}

View file

@ -1,3 +1,14 @@
pub mod time; pub mod time;
pub use time::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};

View file

@ -0,0 +1,33 @@
use hecs::World;
use crate::{AssetManager, EventBus};
/// A game/engine system that updates every frame.
pub trait System<A> {
/// 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<A>, dt: f32);
}
/// Stores and updates a collection of boxed systems.
pub struct SystemRegistry<A> {
systems: Vec<Box<dyn System<A>>>,
}
impl<A> SystemRegistry<A> {
pub fn new() -> Self { Self { systems: Vec::new() } }
pub fn add_system<S: System<A> + '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<A>, dt: f32) {
for s in &mut self.systems {
s.update(world, assets, events, dt);
}
}
}

View file

@ -4,5 +4,5 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
hecs = { version = "0.10.5", features = ["macros"] }
glam = "0.30.4" glam = "0.30.4"
hecs = "0.10.5"

View file

@ -13,5 +13,7 @@ impl Transform {
} }
} }
#[derive(Clone)] #[derive(Copy, Clone)]
pub struct ModelHandle(pub usize); pub struct ModelId(pub usize);
pub type ModelHandle = ModelId;

View file

@ -13,3 +13,4 @@ raidillon_ui = { path = "../raidillon_ui" }
raidillon_core = { path = "../raidillon_core" } raidillon_core = { path = "../raidillon_core" }
hecs = "0.10.5" hecs = "0.10.5"
raidillon_input = { path = "../raidillon_input" } raidillon_input = { path = "../raidillon_input" }
glium = { version = "0.35.0", features = ["glutin_backend", "simple_window_builder"] }

View file

@ -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<Action>,
systems: SystemRegistry<Action>,
render_system: RenderSystem,
input_system: InputSystem,
time: Time,
camera_entity: Entity,
}
impl Engine {
pub fn new(display: &DisplayHandle) -> anyhow::Result<Self> {
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<T>(&mut self, event: &winit::event::Event<T>) {
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<S: Surface>(&mut self, target: &mut S) {
self.render_system.render_into(&self.world, &self.assets, target);
}
}

View file

@ -1,168 +1,44 @@
use anyhow::Result; use anyhow::Result;
use glam::{Quat, Vec3, EulerRot}; use raidillon_render::{init_render_window, DisplayHandle};
use raidillon_core::Time;
use raidillon_ecs::Transform;
use raidillon_render::{Camera, ECSRenderer, init_render_window, DisplayHandle};
use raidillon_ui::Gui; use raidillon_ui::Gui;
use raidillon_input::{Input, FPSCameraController}; mod engine;
use winit::keyboard::KeyCode; use crate::engine::Engine;
use winit::window::CursorGrabMode;
use winit::event::MouseButton;
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
enum Action {
MoveForward,
MoveBackward,
MoveLeft,
MoveRight,
}
fn main() -> Result<()> { fn main() -> Result<()> {
let event_loop = 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): (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 engine = Engine::new(&display)?;
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::<Action>::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,
},))
};
event_loop event_loop
.run(move |event, el| { .run(move |event, el| {
use winit::event::{Event, WindowEvent}; use winit::event::{Event};
gui.handle_event(&window, &event); gui.handle_event(&window, &event);
input.handle_event(&event); engine.handle_event(&event);
match event { match event {
Event::WindowEvent { event, .. } => match event { Event::WindowEvent { event, .. } => {
WindowEvent::CloseRequested => el.exit(), if let winit::event::WindowEvent::CloseRequested = event {
WindowEvent::Resized(sz) => { el.exit();
ecsr.world.query_one_mut::<&mut Camera>(camera_ent).map(|mut cam| {
cam.aspect = sz.width as f32 / sz.height as f32;
});
} }
WindowEvent::MouseInput { state, button, .. } => { }
if button == MouseButton::Right { Event::WindowEvent { .. } => {}
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::AboutToWait => { Event::AboutToWait => {
time.tick(); engine.update();
{ // Render
let dt = time.delta_seconds(); let mut target = display.as_inner().draw();
camera_controller.update( engine.render_into(&mut target);
&input, gui.render_with(&mut target, &window, |_| {});
dt, target.finish().unwrap();
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();
gui.prepare_frame(&window); gui.prepare_frame(&window);
window.request_redraw(); window.request_redraw();

View file

@ -5,4 +5,7 @@ edition = "2021"
[dependencies] [dependencies]
winit = "0.30" winit = "0.30"
glam = "0.30.4" glam = "0.30.4"
raidillon_core = { path = "../raidillon_core" }
hecs = "0.10.5"
raidillon_render = { path = "../raidillon_render" }

View file

@ -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<crate::Action> for CameraSystem {
fn update(&mut self, world: &mut World, _assets: &AssetManager, _events: &mut raidillon_core::EventBus<crate::Action>, dt: f32) {
self.update(world, dt);
}
}
impl EventHandler<Action> for CameraSystem {
fn handle(&mut self, event: &GameEvent<Action>) {
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;
}
}
}
_ => {}
}
}
}

View file

@ -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<Action>,
}
impl InputSystem {
pub fn new() -> Self {
let mut input = Input::<Action>::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<T>(&mut self, event: &winit::event::Event<T>) {
self.input.handle_event(event);
}
pub fn update(&mut self, bus: &mut EventBus<Action>) {
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<crate::Action> for InputSystem {
fn update(&mut self, _world: &mut World, _assets: &AssetManager, events: &mut raidillon_core::EventBus<crate::Action>, _dt: f32) {
self.update(events);
self.end_frame();
}
}

View file

@ -4,9 +4,23 @@ use std::hash::Hash;
use winit::event::{DeviceEvent, ElementState, Event, WindowEvent}; use winit::event::{DeviceEvent, ElementState, Event, WindowEvent};
use winit::keyboard::{KeyCode, PhysicalKey}; 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 mod camera;
pub use camera::FPSCameraController; pub use camera::FPSCameraController;
pub mod camera_system;
pub use camera_system::CameraSystem;
pub struct Input<A: Copy + Eq + Hash> { pub struct Input<A: Copy + Eq + Hash> {
pressed_keys: HashSet<KeyCode>, pressed_keys: HashSet<KeyCode>,
pressed_once: HashSet<KeyCode>, pressed_once: HashSet<KeyCode>,

View file

@ -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<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 {
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<S: glium::Surface>(&mut self, target: &mut S) {
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

@ -2,10 +2,10 @@ pub mod camera;
pub mod model; pub mod model;
pub mod gltf_loader; pub mod gltf_loader;
pub mod render; pub mod render;
pub mod ecs_renderer;
pub mod window; pub mod window;
pub mod render_system;
pub use camera::Camera; pub use camera::Camera;
pub use render::GliumRenderer; pub use render::GliumRenderer;
pub use ecs_renderer::ECSRenderer;
pub use window::{DisplayHandle, init_window as init_render_window}; pub use window::{DisplayHandle, init_window as init_render_window};
pub use render_system::RenderSystem;

View file

@ -12,12 +12,12 @@ use glium::draw_parameters::DepthTest;
pub struct GliumRenderer { pub struct GliumRenderer {
display: glium::Display<WindowSurface>, display: glium::Display<WindowSurface>,
program: Program, pub(crate) program: Program,
white_tex: SrgbTexture2d, pub(crate) white_tex: SrgbTexture2d,
pub models: Vec<Model>, pub models: Vec<Model>,
params: glium::DrawParameters<'static>, pub(crate) params: glium::DrawParameters<'static>,
skybox_program: Program, skybox_program: Program,
skybox_texture: SrgbTexture2d, skybox_texture: SrgbTexture2d,

View file

@ -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<Self> {
Ok(Self {
renderer: GliumRenderer::new(display.as_inner().clone())?,
})
}
/// Render the given `world` into an arbitrary glium surface.
pub fn render_into<S: Surface, P: ModelProvider>(&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<S: Surface, P: ModelProvider>(&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<P: AsRef<str>, A: ModelProvider + ?Sized>( &self, path: P, assets: &mut A ) -> anyhow::Result<ModelId> 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<glium::glutin::surface::WindowSurface> {
self.renderer.display()
}
fn draw_scene<S: Surface, P: ModelProvider>(&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
}
}