diff --git a/.vscode/launch.json b/.vscode/launch.json index a05d1f5..26d7b96 100755 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -83,6 +83,28 @@ }, "args": [], "cwd": "${workspaceFolder}/lyra-ecs" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'lyra-scene'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=lyra-scene", + //"command::tests::deferred_commands", + //"--", + //"--exact --nocapture" + ], + "filter": { + "name": "lyra-scene", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}/lyra-scene" } ] } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 53d3f80..c7c50da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1845,7 +1845,9 @@ dependencies = [ "gltf", "image", "infer", + "instant", "lyra-ecs", + "lyra-math", "mime", "notify", "notify-debouncer-full", @@ -1855,6 +1857,14 @@ dependencies = [ "uuid", ] +[[package]] +name = "lyra-scene" +version = "0.1.0" +dependencies = [ + "lyra-ecs", + "lyra-math", +] + [[package]] name = "lyra-scripting" version = "0.1.0" @@ -3093,7 +3103,6 @@ dependencies = [ "async-std", "fps_counter", "lyra-engine", - "lyra-scripting", "tracing", ] diff --git a/Cargo.toml b/Cargo.toml index 36a4517..4091b3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ members = [ "lyra-ecs", "lyra-reflect", "lyra-scripting", - "lyra-game", "lyra-math"] + "lyra-game", "lyra-math", "lyra-scene"] [features] scripting = ["dep:lyra-scripting"] diff --git a/examples/testbed/Cargo.toml b/examples/testbed/Cargo.toml index 877d0b4..79687cc 100644 --- a/examples/testbed/Cargo.toml +++ b/examples/testbed/Cargo.toml @@ -6,8 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -lyra-engine = { path = "../../", version = "0.0.1", features = ["lua_scripting"] } -lyra-scripting = { path = "../../lyra-scripting", features = ["lua", "teal"] } +lyra-engine = { path = "../../", version = "0.0.1" } +#lyra-engine = { path = "../../", version = "0.0.1", features = ["lua_scripting"] } +#lyra-scripting = { path = "../../lyra-scripting", features = ["lua", "teal"] } #lyra-ecs = { path = "../../lyra-ecs"} anyhow = "1.0.75" async-std = "1.12.0" diff --git a/examples/testbed/assets/pos-testing/child-node-cubes.glb b/examples/testbed/assets/pos-testing/child-node-cubes.glb new file mode 100644 index 0000000..eb0be46 Binary files /dev/null and b/examples/testbed/assets/pos-testing/child-node-cubes.glb differ diff --git a/examples/testbed/assets/pos-testing/separate-nodes-two-cubes.glb b/examples/testbed/assets/pos-testing/separate-nodes-two-cubes.glb new file mode 100644 index 0000000..a10cc1a Binary files /dev/null and b/examples/testbed/assets/pos-testing/separate-nodes-two-cubes.glb differ diff --git a/examples/testbed/assets/sponza/.gitignore b/examples/testbed/assets/sponza/.gitignore new file mode 100644 index 0000000..78f4b5c --- /dev/null +++ b/examples/testbed/assets/sponza/.gitignore @@ -0,0 +1,2 @@ +Sponza* +Textures \ No newline at end of file diff --git a/examples/testbed/assets/sponza/README.md b/examples/testbed/assets/sponza/README.md new file mode 100644 index 0000000..dfab197 --- /dev/null +++ b/examples/testbed/assets/sponza/README.md @@ -0,0 +1 @@ +To keep the size of this repository down, the Sponza scene is omitted from this repo. The files were downloaded from https://github.com/toji/sponza-optimized \ No newline at end of file diff --git a/examples/testbed/src/main.rs b/examples/testbed/src/main.rs index cc34877..b33a539 100644 --- a/examples/testbed/src/main.rs +++ b/examples/testbed/src/main.rs @@ -1,7 +1,7 @@ use std::ptr::NonNull; -use lyra_engine::{math::{self, Vec3}, math::Transform, input::{KeyCode, ActionHandler, Action, ActionKind, LayoutId, ActionMapping, ActionSource, ActionMappingId, InputActionPlugin, MouseInput, MouseAxis, CommonActionLabel}, game::Game, render::{window::{CursorGrabMode, WindowOptions}, light::{PointLight, directional::DirectionalLight, SpotLight}}, change_tracker::Ct, ecs::{system::{Criteria, CriteriaSchedule, BatchedSystem, IntoSystem}, World, Component}, DeltaTime, scene::{ModelComponent, CameraComponent}, lua::{LuaScriptingPlugin, LuaScript}, Script, ScriptList}; -use lyra_engine::assets::{ResourceManager, Model}; +use lyra_engine::{assets::gltf::Gltf, ecs::{system::{BatchedSystem, Criteria, CriteriaSchedule, IntoSystem}, Component, World}, game::Game, input::{Action, ActionHandler, ActionKind, ActionMapping, ActionMappingId, ActionSource, InputActionPlugin, KeyCode, LayoutId, MouseAxis, MouseInput}, math::{self, Transform, Vec3}, render::light::{directional::DirectionalLight, PointLight, SpotLight}, scene::{CameraComponent, MeshComponent}, DeltaTime}; +use lyra_engine::assets::ResourceManager; mod free_fly_camera; use free_fly_camera::{FreeFlyCameraPlugin, FreeFlyCamera}; @@ -16,6 +16,13 @@ pub enum ActionLabel { LookRoll, } +const ACTLBL_MOVE_UP_DOWN: &str = "MoveUpDown"; +const ACTLBL_MOVE_LEFT_RIGHT: &str = "MoveLeftRight"; +const ACTLBL_MOVE_FORWARD_BACKWARD: &str = "MoveForwardBackward"; +const ACTLBL_LOOK_LEFT_RIGHT: &str = "LookLeftRight"; +const ACTLBL_LOOK_UP_DOWN: &str = "LookUpDown"; +const ACTLBL_LOOK_ROLL: &str = "LookRoll"; + struct FixedTimestep { max_tps: u32, fixed_time: f32, @@ -86,30 +93,45 @@ async fn main() { //let diffuse_texture = resman.request::("assets/happy-tree.png").unwrap(); //let antique_camera_model = resman.request::("assets/AntiqueCamera.glb").unwrap(); //let cube_model = resman.request::("assets/cube-texture-bin.glb").unwrap(); - let cube_model = resman.request::("assets/texture-sep/texture-sep.gltf").unwrap(); - let crate_model = resman.request::("assets/crate/crate.gltf").unwrap(); - //let sponza_model = resman.request::("assets/sponza/Sponza.gltf").unwrap(); + /* let cube_gltf = resman.request::("assets/texture-sep/texture-sep.gltf").unwrap(); + let crate_gltf = resman.request::("assets/crate/crate.gltf").unwrap(); + + let separate_gltf = resman.request::("assets/pos-testing/child-node-cubes.glb").unwrap(); */ + //drop(resman); + + /* let cube_mesh = &cube_gltf.data_ref() + .unwrap().meshes[0]; + let crate_mesh = &crate_gltf.data_ref() + .unwrap().meshes[0]; + + let separate_scene = &separate_gltf.data_ref() + .unwrap().scenes[0]; */ + + let sponza_model = resman.request::("assets/sponza/Sponza.gltf").unwrap(); drop(resman); + let sponza_scene = &sponza_model.data_ref() + .unwrap().scenes[0]; + + world.spawn(( + sponza_scene.clone(), + Transform::from_xyz(0.0, 0.0, 0.0), + )); + /* world.spawn(( - ModelComponent(antique_camera_model), + separate_scene.clone(), Transform::from_xyz(0.0, -5.0, -10.0), )); */ - /* world.spawn(( - ModelComponent(sponza_model), - Transform::from_xyz(0.0, 0.0, 0.0), - )); */ - - { - let cube_tran = Transform::from_xyz(-3.5, 0.0, -8.0); + /* { + let cube_tran = Transform::from_xyz(-5.9026427, -1.8953488, -10.0); //cube_tran.rotate_y(math::Angle::Degrees(180.0)); world.spawn(( cube_tran, - ModelComponent(crate_model.clone()), + crate_mesh.clone(), CubeFlag, )); - } + } */ { let mut light_tran = Transform::from_xyz(1.5, 2.5, 0.0); @@ -124,11 +146,11 @@ async fn main() { specular: 1.3, }, light_tran, - ModelComponent(cube_model.clone()), + //cube_mesh.clone(), )); } - { + /* { let mut light_tran = Transform::from_xyz(-3.5, 0.2, -4.5); light_tran.scale = Vec3::new(0.5, 0.5, 0.5); world.spawn(( @@ -146,9 +168,31 @@ async fn main() { specular: 1.0, }, Transform::from(light_tran), - ModelComponent(cube_model.clone()), + cube_mesh.clone(), )); } + + { + let mut light_tran = Transform::from_xyz(2.0, 2.5, -9.5); + light_tran.scale = Vec3::new(0.5, 0.5, 0.5); + world.spawn(( + PointLight { + color: Vec3::new(0.0, 0.0, 1.0), + + intensity: 3.3, + + constant: 1.0, + linear: 0.09, + quadratic: 0.032, + + ambient: 0.2, + diffuse: 1.0, + specular: 1.3, + }, + Transform::from(light_tran), + cube_mesh.clone(), + )); + } */ /* { let mut light_tran = Transform::from_xyz(-5.0, 2.5, -9.5); @@ -172,28 +216,6 @@ async fn main() { )); } */ - { - let mut light_tran = Transform::from_xyz(2.0, 2.5, -9.5); - light_tran.scale = Vec3::new(0.5, 0.5, 0.5); - world.spawn(( - PointLight { - color: Vec3::new(0.0, 0.0, 1.0), - - intensity: 3.3, - - constant: 1.0, - linear: 0.09, - quadratic: 0.032, - - ambient: 0.2, - diffuse: 1.0, - specular: 1.3, - }, - Transform::from(light_tran), - ModelComponent(cube_model), - )); - } - let mut camera = CameraComponent::new_3d(); camera.transform.translation += math::Vec3::new(0.0, 0.0, 5.5); world.spawn(( camera, FreeFlyCamera::default() )); @@ -211,7 +233,7 @@ async fn main() { Ok(()) }; - let fps_plugin = move |game: &mut Game| { + let _fps_plugin = move |game: &mut Game| { let world = game.world_mut(); world.add_resource(fps_counter::FPSCounter::new()); }; @@ -246,54 +268,54 @@ async fn main() { }; let action_handler_plugin = |game: &mut Game| { - /* let action_handler = ActionHandler::builder() + let action_handler = ActionHandler::builder() .add_layout(LayoutId::from(0)) - .add_action(CommonActionLabel::MoveForwardBackward, Action::new(ActionKind::Axis)) - .add_action(CommonActionLabel::MoveLeftRight, Action::new(ActionKind::Axis)) - .add_action(CommonActionLabel::MoveUpDown, Action::new(ActionKind::Axis)) - .add_action(CommonActionLabel::LookLeftRight, Action::new(ActionKind::Axis)) - .add_action(CommonActionLabel::LookUpDown, Action::new(ActionKind::Axis)) - .add_action(CommonActionLabel::LookRoll, Action::new(ActionKind::Axis)) + .add_action(ACTLBL_MOVE_FORWARD_BACKWARD, Action::new(ActionKind::Axis)) + .add_action(ACTLBL_MOVE_LEFT_RIGHT, Action::new(ActionKind::Axis)) + .add_action(ACTLBL_MOVE_UP_DOWN, Action::new(ActionKind::Axis)) + .add_action(ACTLBL_LOOK_LEFT_RIGHT, Action::new(ActionKind::Axis)) + .add_action(ACTLBL_LOOK_UP_DOWN, Action::new(ActionKind::Axis)) + .add_action(ACTLBL_LOOK_ROLL, Action::new(ActionKind::Axis)) .add_mapping(ActionMapping::builder(LayoutId::from(0), ActionMappingId::from(0)) - .bind(CommonActionLabel::MoveForwardBackward, &[ + .bind(ACTLBL_MOVE_FORWARD_BACKWARD, &[ ActionSource::Keyboard(KeyCode::W).into_binding_modifier(1.0), ActionSource::Keyboard(KeyCode::S).into_binding_modifier(-1.0) ]) - .bind(CommonActionLabel::MoveLeftRight, &[ + .bind(ACTLBL_MOVE_LEFT_RIGHT, &[ ActionSource::Keyboard(KeyCode::A).into_binding_modifier(-1.0), ActionSource::Keyboard(KeyCode::D).into_binding_modifier(1.0) ]) - .bind(CommonActionLabel::MoveUpDown, &[ + .bind(ACTLBL_MOVE_UP_DOWN, &[ ActionSource::Keyboard(KeyCode::C).into_binding_modifier(1.0), ActionSource::Keyboard(KeyCode::Z).into_binding_modifier(-1.0) ]) - .bind(CommonActionLabel::LookLeftRight, &[ + .bind(ACTLBL_LOOK_LEFT_RIGHT, &[ ActionSource::Mouse(MouseInput::Axis(MouseAxis::X)).into_binding(), ActionSource::Keyboard(KeyCode::Left).into_binding_modifier(-1.0), ActionSource::Keyboard(KeyCode::Right).into_binding_modifier(1.0), //ActionSource::Gamepad(GamepadFormat::DualAxis, GamepadInput::Axis(GamepadAxis::RThumbstickX)).into_binding(), ]) - .bind(CommonActionLabel::LookUpDown, &[ + .bind(ACTLBL_LOOK_UP_DOWN, &[ ActionSource::Mouse(MouseInput::Axis(MouseAxis::Y)).into_binding(), ActionSource::Keyboard(KeyCode::Up).into_binding_modifier(-1.0), ActionSource::Keyboard(KeyCode::Down).into_binding_modifier(1.0), //ActionSource::Gamepad(GamepadFormat::DualAxis, GamepadInput::Axis(GamepadAxis::RThumbstickY)).into_binding(), ]) - .bind(CommonActionLabel::LookRoll, &[ + .bind(ACTLBL_LOOK_ROLL, &[ ActionSource::Keyboard(KeyCode::E).into_binding_modifier(-1.0), ActionSource::Keyboard(KeyCode::Q).into_binding_modifier(1.0), ]) .finish() - ); + ).finish(); let world = game.world_mut(); - world.add_resource(action_handler); */ + world.add_resource(action_handler); game.with_plugin(InputActionPlugin); }; - let script_test_plugin = |game: &mut Game| { + /* let script_test_plugin = |game: &mut Game| { game.with_plugin(LuaScriptingPlugin); let world = game.world_mut(); @@ -306,13 +328,13 @@ async fn main() { let scripts = ScriptList::new(vec![script]); world.spawn((scripts,)); - }; + }; */ Game::initialize().await .with_plugin(lyra_engine::DefaultPlugins) .with_startup_system(setup_sys.into_system()) .with_plugin(action_handler_plugin) - .with_plugin(script_test_plugin) + //.with_plugin(script_test_plugin) //.with_plugin(fps_plugin) .with_plugin(jiggle_plugin) .with_plugin(FreeFlyCameraPlugin) diff --git a/lyra-ecs/src/archetype.rs b/lyra-ecs/src/archetype.rs index d9577db..5ea7b55 100644 --- a/lyra-ecs/src/archetype.rs +++ b/lyra-ecs/src/archetype.rs @@ -55,7 +55,6 @@ impl ComponentColumn { /// Set a component from pointer at an entity index. /// /// # Safety - /// /// This column must have space to fit the component, if it does not have room it will panic. pub unsafe fn set_at(&mut self, entity_index: usize, comp_src: NonNull, tick: Tick) { assert!(entity_index < self.capacity); @@ -76,6 +75,23 @@ impl ComponentColumn { } } + /// Inserts an entity and its component at a specific index. + pub unsafe fn insert_entity(&mut self, entity_index: usize, comp_src: NonNull, tick: Tick) { + assert!(entity_index < self.capacity); + + let mut data = self.data.borrow_mut(); + let data = data.deref_mut(); + + let size = self.info.layout().size(); + let dest = NonNull::new_unchecked(data.as_ptr().add(entity_index * size)); + ptr::copy_nonoverlapping(comp_src.as_ptr(), dest.as_ptr(), size); + + // check if a component spot is being set twice and that the entity's tick is + // already stored + self.entity_ticks.push(tick); + self.len += 1; + } + /// Get a component at an entities index. /// /// # Safety @@ -152,6 +168,8 @@ impl ComponentColumn { /// Removes a component from the column, freeing it, and returning the old index of the entity that took its place in the column. pub unsafe fn remove_component(&mut self, entity_index: usize, tick: &Tick) -> Option { let _ = tick; // may be used at some point + + debug_assert!(self.len > 0, "There are no entities in the Archetype to remove from!"); let mut data = self.data.borrow_mut(); let data = data.deref_mut(); @@ -170,8 +188,7 @@ impl ComponentColumn { mem::swap(&mut old_comp_ptr, &mut new_comp_ptr); // new_comp_ptr is now the old ptr // make sure to keep entity indexes correct in the ticks list as well - self.entity_ticks.swap(moved_index, entity_index); - self.entity_ticks.pop(); + self.entity_ticks.swap_remove(entity_index); Some(moved_index) } else { None }; @@ -283,8 +300,7 @@ impl Archetype { bundle.take(|data, type_id, _size| { let col = self.get_column_mut(type_id).unwrap(); - unsafe { col.set_at(entity_index.0 as usize, data, *tick); } - col.len += 1; + unsafe { col.insert_entity(entity_index.0 as usize, data, *tick); } }); entity_index @@ -293,21 +309,21 @@ impl Archetype { /// Removes an entity from the Archetype and frees its components. Returns the entity record /// that took its place in the component column. pub(crate) fn remove_entity(&mut self, entity: Entity, tick: &Tick) -> Option<(Entity, ArchetypeEntityId)> { - let entity_index = *self.entity_ids.get(&entity) + let entity_index = self.entity_ids.remove(&entity) .expect("The entity is not in this Archetype!"); let mut removed_entity: Option<(Entity, ArchetypeEntityId)> = None; for c in self.columns.iter_mut() { let moved_entity = unsafe { c.remove_component(entity_index.0 as usize, tick) }; - if let Some(res) = moved_entity { + if let Some(moved_idx) = moved_entity { if let Some((_, aid)) = removed_entity { // Make sure that the moved entity is the same as what was moved in other columns. - assert!(res as u64 == aid.0); + assert!(moved_idx as u64 == aid.0); } else { - // This is the first move, so find the EntityId that points to the column index. - let just_removed = self.entities[res]; - removed_entity = Some((just_removed, ArchetypeEntityId(res as u64))); + // This is the first move, so find the Entity that was moved into this index. + let just_removed = self.entities[moved_idx]; + removed_entity = Some((just_removed, ArchetypeEntityId(moved_idx as u64))); } } else { // If there wasn't a moved entity, make sure no other columns moved something. @@ -316,12 +332,15 @@ impl Archetype { } // safe from the .expect at the start of this method. - self.entity_ids.remove(&entity).unwrap(); - if self.entities.len() > 1 { - let len = self.entities.len(); - self.entities.swap(entity_index.0 as _, len - 1); + //self.entity_ids.remove(&entity).unwrap(); + + // update the archetype index of the moved entity + if let Some((moved, _old_idx)) = removed_entity { + self.entity_ids.insert(moved, entity_index); } - self.entities.pop().unwrap(); + + let removed = self.entities.swap_remove(entity_index.0 as _); + assert_eq!(removed, entity); // now change the ArchetypeEntityId to be the index that the moved entity was moved into. removed_entity.map(|(e, _a)| (e, entity_index)) @@ -390,7 +409,8 @@ impl Archetype { self.capacity = new_cap; } - debug_assert_eq!(self.entity_ids.len(), self.entities.len(), "Somehow the Archetype's entity storage got unsynced"); + debug_assert_eq!(self.entity_ids.len(), self.entities.len(), + "Somehow the Archetype's entity storage got unsynced"); let entity_index = ArchetypeEntityId(self.entity_ids.len() as u64); self.entity_ids.insert(entity, entity_index); self.entities.push(entity); @@ -402,6 +422,12 @@ impl Archetype { entity_index } + /// Ensure that the internal entity lists are synced in length + pub(crate) fn ensure_synced(&self) { + debug_assert_eq!(self.entity_ids.len(), self.entities.len(), + "Somehow the Archetype's entity storage got unsynced"); + } + /// Moves the entity from this archetype into another one. /// /// # Safety @@ -453,6 +479,36 @@ impl Archetype { pub fn has_entity(&self, e: Entity) -> bool { self.entity_ids.contains_key(&e) } + + /// Extend the Archetype by adding more columns. + /// + /// In order to extend the Archetype, the archetype needs the components for the entities + /// it already has. These are provided through the `new_columns` parameter. **If the Vec + /// does not have the same amount of bundles in it as the amount of entities in the + /// Archetype, it will panic!** + pub fn extend(&mut self, tick: &Tick, new_columns: Vec) { + debug_assert_eq!(new_columns.len(), self.len(), "The amount of provided column does not \ + match the amount of entities"); + + let column_info = new_columns.iter() + .next() + .unwrap() + .info(); + + for coli in column_info.into_iter() { + let col = unsafe { ComponentColumn::new(coli, self.capacity) }; + self.columns.push(col); + } + + for (eid, bundle) in new_columns.into_iter().enumerate() { + bundle.take(|ptr, tyid, _size| { + unsafe { + let col = self.get_column_mut(tyid).unwrap(); + col.insert_entity(eid, ptr, tick.clone()); + } + }); + } + } } #[cfg(test)] @@ -666,4 +722,24 @@ mod tests { assert_eq!(unsafe { *ptr.cast::().as_ref() }, comp); assert_eq!(col.info, info); } + + /// Tests removing an entity from the Archetype when it is the only entity in it. + #[test] + fn remove_single_entity() { + let info = (Vec2::new(0.0, 0.0),).info(); + let mut a = Archetype::from_bundle_info(super::ArchetypeId(0), info); + + let ae = Entity { + id: EntityId(0), + generation: 0 + }; + + a.add_entity( + ae, + Vec2::new(10.0, 50.0), + &Tick::default() + ); + + a.remove_entity(ae, &Tick::default()); + } } \ No newline at end of file diff --git a/lyra-ecs/src/lib.rs b/lyra-ecs/src/lib.rs index 43cc726..5f1496f 100644 --- a/lyra-ecs/src/lib.rs +++ b/lyra-ecs/src/lib.rs @@ -30,7 +30,7 @@ pub use component::*; pub mod query; //pub use query::*; -mod relation; +pub mod relation; pub use relation::Relation; mod component_info; diff --git a/lyra-ecs/src/query/borrow.rs b/lyra-ecs/src/query/borrow.rs index d5a1493..989e72c 100644 --- a/lyra-ecs/src/query/borrow.rs +++ b/lyra-ecs/src/query/borrow.rs @@ -177,10 +177,10 @@ where unsafe fn fetch<'a>(&self, _world: &'a World, archetype: &'a crate::archetype::Archetype, tick: crate::Tick) -> Self::Fetch<'a> { let col = archetype.get_column(self.type_id) .expect("You ignored 'can_visit_archetype'!"); - let col = NonNull::from(col); - + // TODO: find a way to get the component column mutable with a borrowed archetype so its tick can be updated. // the fetcher needs to tick the entities tick in the archetype + let col = NonNull::from(col); FetchBorrowMut { col, diff --git a/lyra-ecs/src/query/filter/has_component.rs b/lyra-ecs/src/query/filter/has_component.rs new file mode 100644 index 0000000..1ef455a --- /dev/null +++ b/lyra-ecs/src/query/filter/has_component.rs @@ -0,0 +1,40 @@ +use std::marker::PhantomData; + +use crate::{query::{AsQuery, Query}, Archetype, Component, DynTypeId, World}; + +#[derive(Default)] +pub struct Has { + _marker: PhantomData +} + +impl Copy for Has {} + +impl Clone for Has { + fn clone(&self) -> Self { + Self { _marker: self._marker.clone() } + } +} + +impl Query for Has { + type Item<'a> = (); + + type Fetch<'a> = (); + + fn new() -> Self { + Has { + _marker: PhantomData + } + } + + fn can_visit_archetype(&self, archetype: &Archetype) -> bool { + archetype.has_column(DynTypeId::of::()) + } + + unsafe fn fetch<'a>(&self, _world: &'a World, _: &'a Archetype, _: crate::Tick) -> Self::Fetch<'a> { + () + } +} + +impl AsQuery for Has { + type Query = Self; +} \ No newline at end of file diff --git a/lyra-ecs/src/query/filter/mod.rs b/lyra-ecs/src/query/filter/mod.rs new file mode 100644 index 0000000..149de6d --- /dev/null +++ b/lyra-ecs/src/query/filter/mod.rs @@ -0,0 +1,5 @@ +pub mod has_component; +pub use has_component::*; + +pub mod or; +pub use or::*; \ No newline at end of file diff --git a/lyra-ecs/src/query/filter/or.rs b/lyra-ecs/src/query/filter/or.rs new file mode 100644 index 0000000..c76d9d8 --- /dev/null +++ b/lyra-ecs/src/query/filter/or.rs @@ -0,0 +1,40 @@ +use crate::{query::{AsQuery, Query}, Archetype, World}; + +#[derive(Default)] +pub struct Or { + left: Q1::Query, + right: Q2::Query, +} + +impl Copy for Or {} + +impl Clone for Or { + fn clone(&self) -> Self { + Self { left: self.left.clone(), right: self.right.clone() } + } +} + +impl Query for Or { + type Item<'a> = (); + + type Fetch<'a> = (); + + fn new() -> Self { + Or { + left: Q1::Query::new(), + right: Q2::Query::new(), + } + } + + fn can_visit_archetype(&self, archetype: &Archetype) -> bool { + self.left.can_visit_archetype(archetype) || self.right.can_visit_archetype(archetype) + } + + unsafe fn fetch<'a>(&self, _world: &'a World, _: &'a Archetype, _: crate::Tick) -> Self::Fetch<'a> { + () + } +} + +impl AsQuery for Or { + type Query = Self; +} \ No newline at end of file diff --git a/lyra-ecs/src/query/mod.rs b/lyra-ecs/src/query/mod.rs index a71e5c8..8738608 100644 --- a/lyra-ecs/src/query/mod.rs +++ b/lyra-ecs/src/query/mod.rs @@ -29,6 +29,8 @@ pub use world::*; pub mod dynamic; +pub mod filter; + /// A [`Fetch`]er implementation gets data out of an archetype. pub trait Fetch<'a> { /// The type that this Fetch yields diff --git a/lyra-ecs/src/query/view.rs b/lyra-ecs/src/query/view.rs index 93c67a0..d212295 100644 --- a/lyra-ecs/src/query/view.rs +++ b/lyra-ecs/src/query/view.rs @@ -90,7 +90,7 @@ where fn next(&mut self) -> Option { loop { - if Q::ALWAYS_FETCHES && F::ALWAYS_FETCHES { + if Q::ALWAYS_FETCHES { // only fetch this query once. // fetcher gets set to Some after this `next` call. if self.fetcher.is_none() { diff --git a/lyra-ecs/src/relation/child_of.rs b/lyra-ecs/src/relation/child_of.rs new file mode 100644 index 0000000..968fc0b --- /dev/null +++ b/lyra-ecs/src/relation/child_of.rs @@ -0,0 +1,8 @@ +use super::Relation; + +// TODO: Delete child entities when the parent is deleted +pub struct ChildOf; + +impl Relation for ChildOf { + +} \ No newline at end of file diff --git a/lyra-ecs/src/relation/mod.rs b/lyra-ecs/src/relation/mod.rs index 1acb0ce..6ad396a 100644 --- a/lyra-ecs/src/relation/mod.rs +++ b/lyra-ecs/src/relation/mod.rs @@ -10,19 +10,20 @@ use crate::lyra_engine; use crate::World; mod relates_to; -#[doc(hidden)] pub use relates_to::*; mod relate_pair; -#[doc(hidden)] pub use relate_pair::*; +mod child_of; +pub use child_of::*; + #[allow(unused_variables)] pub trait Relation: 'static { /// called when a relation of this type is set on a target - fn relation_add(&self, origin: Entity, target: Entity) { } + fn relation_add(&mut self, world: &mut World, origin: Entity, target: Entity) { } /// called when a relation is removed - fn relation_remove(&self, origin: Entity, target: Entity) { } + fn relation_remove(&mut self, world: &mut World, origin: Entity, target: Entity) { } } /// A component that stores the target of a relation. @@ -31,10 +32,16 @@ pub trait Relation: 'static { /// entities that the relation targets. #[derive(Component)] pub struct RelationOriginComponent { - pub(crate) relation: R, + pub relation: R, target: Entity, } +impl RelationOriginComponent { + pub fn target(&self) -> Entity { + self.target + } +} + /// A component that stores the origin of a relation. /// /// This component is on the target of the relation and can be used to find the @@ -79,12 +86,12 @@ impl World { }; self.insert(target, comp); - let comp = RelationOriginComponent { + let mut comp = RelationOriginComponent { relation, target, }; - comp.relation.relation_add(origin, target); + comp.relation.relation_add(self, origin, target); self.insert(origin, comp); } } diff --git a/lyra-ecs/src/relation/relate_pair.rs b/lyra-ecs/src/relation/relate_pair.rs index 429554e..68889a0 100644 --- a/lyra-ecs/src/relation/relate_pair.rs +++ b/lyra-ecs/src/relation/relate_pair.rs @@ -69,7 +69,7 @@ where unsafe fn fetch<'a>(&self, _world: &'a World, archetype: &'a crate::archetype::Archetype, tick: crate::Tick) -> Self::Fetch<'a> { let _ = tick; - let col = archetype.get_column(self.type_id()) + let col = archetype.get_column(TypeId::of::>()) .expect("You ignored 'can_visit_archetype'!"); FetchRelatePair { diff --git a/lyra-ecs/src/world.rs b/lyra-ecs/src/world.rs index 6ea08a6..31abb31 100644 --- a/lyra-ecs/src/world.rs +++ b/lyra-ecs/src/world.rs @@ -133,8 +133,6 @@ impl World { where B: Bundle { - // TODO: If the archetype has a single entity, add a component column for the new - // component instead of moving the entity to a brand new archetype. // TODO: If the entity already has the components in `bundle`, update the values of the // components with the bundle. @@ -142,7 +140,8 @@ impl World { let record = self.entities.entity_record(entity).unwrap(); let current_arch = self.archetypes.get(&record.id).unwrap(); - + let current_arch_len = current_arch.len(); + let mut col_types: Vec = current_arch.columns.iter().map(|c| c.info.type_id()).collect(); let orig_col = col_types.clone(); col_types.extend(bundle.type_ids()); @@ -150,7 +149,9 @@ impl World { let mut col_infos: Vec = current_arch.columns.iter().map(|c| c.info).collect(); col_infos.extend(bundle.info()); - let col_ptrs: Vec<(NonNull, ComponentInfo)> = current_arch.columns.iter().map(|c| unsafe { (NonNull::new_unchecked(c.borrow_ptr().as_ptr()), c.info) }).collect(); + let col_ptrs: Vec<(NonNull, ComponentInfo)> = current_arch.columns.iter() + .map(|c| unsafe { (NonNull::new_unchecked(c.borrow_ptr().as_ptr()), c.info) }) + .collect(); if let Some(arch) = self.archetypes.values_mut().find(|a| a.is_archetype_for(&col_types)) { let res_index = arch.reserve_one(entity); @@ -158,19 +159,21 @@ impl World { for (col_type, (col_ptr, col_info)) in orig_col.into_iter().zip(col_ptrs.into_iter()) { unsafe { let ptr = NonNull::new_unchecked(col_ptr.as_ptr() - .add(res_index.0 as usize * col_info.layout().size())); + .add(record.index.0 as usize * col_info.layout().size())); let col = arch.get_column_mut(col_type).unwrap(); + // set_at is used since the entity was reserved col.set_at(res_index.0 as _, ptr, tick); } } bundle.take(|data, type_id, _size| { let col = arch.get_column_mut(type_id).unwrap(); + // set_at is used since the entity was reserved unsafe { col.set_at(res_index.0 as _, data, tick); } - col.len += 1; }); arch.entity_ids.insert(entity, res_index); + arch.ensure_synced(); let new_record = Record { id: arch.id(), @@ -178,9 +181,29 @@ impl World { }; self.entities.insert_entity_record(entity, new_record); } else { + if current_arch_len == 1 { + // if this entity is the only entity for this archetype, add more columns to it + let current_arch = self.archetypes.get_mut(&record.id).unwrap(); + current_arch.extend(&tick, vec![bundle]); + + return; + } + let new_arch_id = self.next_archetype_id.increment(); let mut archetype = Archetype::from_bundle_info(new_arch_id, col_infos); let entity_arch_id = archetype.add_entity(entity, bundle, &tick); + + // move the old components into the new archetype + for (column_ptr, column_info) in col_ptrs.into_iter() { + unsafe { + // ptr of component for the entity + let comp_ptr = NonNull::new_unchecked(column_ptr.as_ptr() + .add(record.index.0 as usize * column_info.layout().size())); + + let col = archetype.get_column_mut(column_info.type_id()).unwrap(); + col.insert_entity(entity_arch_id.0 as _, comp_ptr, tick); + } + } self.archetypes.insert(new_arch_id, archetype); @@ -194,7 +217,13 @@ impl World { } let current_arch = self.archetypes.get_mut(&record.id).unwrap(); - current_arch.remove_entity(entity, &tick); + if current_arch.len() > 1 { + current_arch.remove_entity(entity, &tick); + } else if current_arch.len() == 1 { + // The old archetype will only be removed if there was another archetype that would + // work for the entity's components, and the old archetype only had a single entity. + self.archetypes.remove(&record.id).unwrap(); + } } pub fn entity_archetype(&self, entity: Entity) -> Option<&Archetype> { @@ -225,6 +254,13 @@ impl World { v.into_iter() } + /// View into the world for a set of entities that satisfy the queries. + pub fn filtered_view_iter(&self) -> ViewIter { + let archetypes = self.archetypes.values().collect(); + let v = ViewState::new(self, Q::Query::new(), F::Query::new(), archetypes); + v.into_iter() + } + pub fn dynamic_view(&self) -> DynamicView { DynamicView::new(self) } @@ -330,7 +366,7 @@ impl World { #[cfg(test)] mod tests { - use crate::{tests::{Vec2, Vec3}, query::TickOf}; + use crate::{query::TickOf, tests::{Vec2, Vec3}, Entity}; use super::World; @@ -453,6 +489,46 @@ mod tests { assert!(world.view_one::<&Vec3>(e).get().is_some()) } + #[test] + fn insert_multiple_times() { + let v2s = &[Vec2::rand(), Vec2::rand(), Vec2::rand()]; + let v3s = &[Vec3::rand(), Vec3::rand(), Vec3::rand()]; + + let mut world = World::new(); + let e1 = world.spawn(v2s[0]); + let e2 = world.spawn(v2s[1]); + let e3 = world.spawn(v2s[2]); + println!("Spawned entities"); + + let ev2 = world.view_one::<&Vec2>(e2).get() + .expect("Failed to find Vec2 and Vec3 on inserted entity!"); + assert_eq!(*ev2, v2s[1]); + drop(ev2); + + let insert_and_assert = |world: &mut World, e: Entity, v2: Vec2, v3: Vec3| { + println!("inserting entity"); + world.insert(e, (v3,)); + println!("inserted entity"); + + let (ev2, ev3) = world.view_one::<(&Vec2, &Vec3)>(e).get() + .expect("Failed to find Vec2 and Vec3 on inserted entity!"); + assert_eq!(*ev2, v2); + assert_eq!(*ev3, v3); + }; + + insert_and_assert(&mut world, e2, v2s[1], v3s[1]); + println!("Entity 2 is good"); + insert_and_assert(&mut world, e3, v2s[2], v3s[2]); + println!("Entity 3 is good"); + assert_eq!(world.archetypes.len(), 2); + println!("No extra archetypes were created"); + + insert_and_assert(&mut world, e1, v2s[0], v3s[0]); + println!("Entity 1 is good"); + assert_eq!(world.archetypes.len(), 1); + println!("Empty archetype was removed"); + } + #[test] fn view_one() { let v = Vec2::rand(); @@ -468,11 +544,6 @@ mod tests { fn view_change_tracking() { let mut world = World::new(); - /* let v = Vec2::rand(); - world.spawn((v,)); - let v = Vec2::rand(); - world.spawn((v,)); */ - println!("spawning"); world.spawn((Vec2::new(10.0, 10.0),)); world.spawn((Vec2::new(5.0, 5.0),)); diff --git a/lyra-game/src/render/material.rs b/lyra-game/src/render/material.rs index dec5b12..aa452d2 100755 --- a/lyra-game/src/render/material.rs +++ b/lyra-game/src/render/material.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +use lyra_resource::{ResHandle, Texture}; + use super::texture::RenderTexture; pub struct MaterialSpecular { @@ -9,13 +11,18 @@ pub struct MaterialSpecular { pub color_texture: Option, } -impl MaterialSpecular { - pub fn from_resource(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: Arc, value: &lyra_resource::Specular) -> Self { - let tex = value.texture.as_ref().map(|t| t.data_ref()) - .map(|i| RenderTexture::from_image(device, queue, bg_layout.clone(), &i.image, None).unwrap()); +fn texture_to_render(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: &Arc, i: &Option>) -> Option { + if let Some(tex) = i { + RenderTexture::from_resource(device, queue, bg_layout.clone(), tex, None).ok() + } else { + None + } +} - let color_tex = value.color_texture.as_ref().map(|t| t.data_ref()) - .map(|i| RenderTexture::from_image(device, queue, bg_layout, &i.image, None).unwrap()); +impl MaterialSpecular { + pub fn from_resource(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: Arc, value: &lyra_resource::gltf::Specular) -> Self { + let tex = texture_to_render(device, queue, &bg_layout, &value.texture); + let color_tex = texture_to_render(device, queue, &bg_layout, &value.color_texture); Self { factor: value.factor, @@ -38,9 +45,8 @@ pub struct Material { } impl Material { - pub fn from_resource(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: Arc, value: &lyra_resource::Material) -> Self { - let diffuse_texture = value.base_color_texture.as_ref().map(|t| t.data_ref()) - .map(|i| RenderTexture::from_image(device, queue, bg_layout.clone(), &i.image, None).unwrap()); + pub fn from_resource(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: Arc, value: &lyra_resource::gltf::Material) -> Self { + let diffuse_texture = texture_to_render(device, queue, &bg_layout, &value.base_color_texture); let specular = value.specular.as_ref().map(|s| MaterialSpecular::from_resource(device, queue, bg_layout.clone(), s)); diff --git a/lyra-game/src/render/render_job.rs b/lyra-game/src/render/render_job.rs index 9c7ec1c..90809b8 100755 --- a/lyra-game/src/render/render_job.rs +++ b/lyra-game/src/render/render_job.rs @@ -1,21 +1,17 @@ use lyra_ecs::Entity; -use crate::math::Transform; - pub struct RenderJob { pub entity: Entity, pub shader_id: u64, - pub mesh_buffer_id: uuid::Uuid, - pub transform: Transform, + pub mesh_uuid: uuid::Uuid, } impl RenderJob { - pub fn new(entity: Entity, shader_id: u64, mesh_buffer_id: uuid::Uuid, transform: Transform) -> Self { + pub fn new(entity: Entity, shader_id: u64, mesh_buffer_id: uuid::Uuid) -> Self { Self { entity, shader_id, - mesh_buffer_id, - transform, + mesh_uuid: mesh_buffer_id, } } } \ No newline at end of file diff --git a/lyra-game/src/render/renderer.rs b/lyra-game/src/render/renderer.rs index 48d77d2..dda0383 100755 --- a/lyra-game/src/render/renderer.rs +++ b/lyra-game/src/render/renderer.rs @@ -5,10 +5,13 @@ use std::borrow::Cow; use glam::Vec3; use instant::Instant; use itertools::izip; -use lyra_ecs::Entity; +use lyra_ecs::query::filter::{Has, Or}; +use lyra_ecs::{Entity, Tick}; use lyra_ecs::query::{Entities, TickOf}; use lyra_ecs::World; +use lyra_resource::gltf::GltfScene; use tracing::{debug, warn}; +use uuid::Uuid; use wgpu::{BindGroupLayout, Limits}; use wgpu::util::DeviceExt; use winit::window::Window; @@ -16,7 +19,7 @@ use winit::window::Window; use crate::math::Transform; use crate::render::material::MaterialUniform; use crate::render::render_buffer::BufferWrapperBuilder; -use crate::scene::{ModelComponent, CameraComponent}; +use crate::scene::CameraComponent; use super::camera::{RenderCamera, CameraUniform}; use super::desc_buf_lay::DescVertexBufferLayout; @@ -28,7 +31,10 @@ use super::transform_buffer_storage::TransformBuffers; use super::vertex::Vertex; use super::{render_pipeline::FullRenderPipeline, render_buffer::BufferStorage, render_job::RenderJob}; -use lyra_resource::Mesh; +use lyra_resource::{gltf::Mesh, ResHandle}; + +type MeshHandle = ResHandle; +type SceneHandle = ResHandle; pub trait Renderer { fn prepare(&mut self, main_world: &mut World); @@ -265,16 +271,19 @@ impl BasicRenderer { s } - fn update_mesh_buffers(&mut self, _entity: Entity, mesh: &Mesh) { - if let Some(buffers) = self.mesh_buffers.get_mut(&mesh.uuid) { + /// Checks if the mesh buffers in the GPU need to be updated. + fn check_mesh_buffers(&mut self, _entity: Entity, meshh: &ResHandle) { + let mesh_uuid = meshh.uuid(); + + if let (Some(mesh), Some(buffers)) = (meshh.data_ref(), self.mesh_buffers.get_mut(&mesh_uuid)) { // check if the buffer sizes dont match. If they dont, completely remake the buffers let vertices = mesh.position().unwrap(); if buffers.buffer_vertex.count() != vertices.len() { - debug!("Recreating buffers for mesh {}", mesh.uuid.to_string()); - let (vert, idx) = self.create_vertex_index_buffers(mesh); + debug!("Recreating buffers for mesh {}", mesh_uuid.to_string()); + let (vert, idx) = self.create_vertex_index_buffers(&mesh); // have to re-get buffers because of borrow checker - let buffers = self.mesh_buffers.get_mut(&mesh.uuid).unwrap(); + let buffers = self.mesh_buffers.get_mut(&mesh_uuid).unwrap(); buffers.buffer_indices = idx; buffers.buffer_vertex = vert; @@ -292,8 +301,8 @@ impl BasicRenderer { if let Some(index_buffer) = buffers.buffer_indices.as_ref() { let aligned_indices = match mesh.indices.as_ref().unwrap() { // U16 indices need to be aligned to u32, for wpgu, which are 4-bytes in size. - lyra_resource::MeshIndices::U16(v) => bytemuck::pod_align_to::(v).1, - lyra_resource::MeshIndices::U32(v) => bytemuck::pod_align_to::(v).1, + lyra_resource::gltf::MeshIndices::U16(v) => bytemuck::pod_align_to::(v).1, + lyra_resource::gltf::MeshIndices::U32(v) => bytemuck::pod_align_to::(v).1, }; let index_buffer = index_buffer.1.buffer(); @@ -327,8 +336,8 @@ impl BasicRenderer { let indices = match mesh.indices.as_ref() { Some(indices) => { let (idx_type, len, contents) = match indices { - lyra_resource::MeshIndices::U16(v) => (wgpu::IndexFormat::Uint16, v.len(), bytemuck::cast_slice(v)), - lyra_resource::MeshIndices::U32(v) => (wgpu::IndexFormat::Uint32, v.len(), bytemuck::cast_slice(v)), + lyra_resource::gltf::MeshIndices::U16(v) => (wgpu::IndexFormat::Uint16, v.len(), bytemuck::cast_slice(v)), + lyra_resource::gltf::MeshIndices::U32(v) => (wgpu::IndexFormat::Uint32, v.len(), bytemuck::cast_slice(v)), }; let index_buffer = self.device.create_buffer_init( @@ -354,7 +363,11 @@ impl BasicRenderer { fn create_mesh_buffers(&mut self, mesh: &Mesh) -> MeshBufferStorage { let (vertex_buffer, buffer_indices) = self.create_vertex_index_buffers(mesh); - let material = Material::from_resource(&self.device, &self.queue, self.bgl_texture.clone(), &mesh.material()); + let material = mesh.material.as_ref() + .expect("Material resource not loaded yet") + .data_ref() + .unwrap(); + let material = Material::from_resource(&self.device, &self.queue, self.bgl_texture.clone(), &material); let uni = MaterialUniform::from(&material); self.queue.write_buffer(&self.material_buffer.inner_buf, 0, bytemuck::bytes_of(&uni)); debug!("Wrote material to buffer"); @@ -367,24 +380,57 @@ impl BasicRenderer { } /// Processes the mesh for the renderer, storing and creating buffers as needed. Returns true if a new mesh was processed. - fn process_mesh(&mut self, entity: Entity, transform: Transform, mesh: &Mesh) -> bool { + fn process_mesh(&mut self, entity: Entity, transform: Transform, mesh: &Mesh, mesh_uuid: Uuid) -> bool { if self.transform_buffers.should_expand() { self.transform_buffers.expand_buffers(&self.device); } self.transform_buffers.update_or_insert(&self.queue, &self.render_limits, - entity, || ( transform.calculate_mat4(), glam::Mat3::from_quat(transform.rotation) )); + mesh_uuid, || ( transform.calculate_mat4(), glam::Mat3::from_quat(transform.rotation) )); #[allow(clippy::map_entry)] - if !self.mesh_buffers.contains_key(&mesh.uuid) { + if !self.mesh_buffers.contains_key(&mesh_uuid) { // create the mesh's buffers let buffers = self.create_mesh_buffers(mesh); - self.mesh_buffers.insert(mesh.uuid, buffers); - self.entity_meshes.insert(entity, mesh.uuid); + self.mesh_buffers.insert(mesh_uuid, buffers); + self.entity_meshes.insert(entity, mesh_uuid); true } else { false } } + + fn interpolate_transforms(&mut self, now: Instant, last_epoch: Tick, entity: Entity, transform: &Transform, transform_epoch: Tick) -> Transform { + let cached = match self.entity_last_transforms.get_mut(&entity) { + Some(last) if transform_epoch == last_epoch => { + last.from_transform = last.to_transform; + last.to_transform = *transform; + last.last_updated_at = Some(last.cached_at); + last.cached_at = now; + + last.clone() + }, + Some(last) => last.clone(), + None => { + let cached = CachedTransform { + last_updated_at: None, + cached_at: now, + from_transform: *transform, + to_transform: *transform, + }; + self.entity_last_transforms.insert(entity, cached.clone()); + cached + } + }; + + let fixed_time = match cached.last_updated_at { + Some(last_updated_at) => cached.cached_at - last_updated_at, + None => now - cached.cached_at + }.as_secs_f32(); + let accumulator = (now - cached.cached_at).as_secs_f32(); + let alpha = accumulator / fixed_time; + + cached.from_transform.lerp(cached.to_transform, alpha) + } } impl Renderer for BasicRenderer { @@ -395,57 +441,62 @@ impl Renderer for BasicRenderer { let now_inst = Instant::now(); - for (entity, model, model_epoch, transform, transform_epoch) in main_world.view_iter::<(Entities, &ModelComponent, TickOf, &Transform, TickOf)>() { + let view = main_world.filtered_view_iter::<(Entities, &Transform, TickOf), Or, Has>>(); + for (entity, transform, transform_epoch) in view { alive_entities.insert(entity); - let cached = match self.entity_last_transforms.get_mut(&entity) { - Some(last) if transform_epoch == last_epoch => { - last.from_transform = last.to_transform; - last.to_transform = *transform; - last.last_updated_at = Some(last.cached_at); - last.cached_at = now_inst; + let mesh_view = main_world.view_one::<(&MeshHandle, TickOf)>(entity); + if let Some((mesh_han, mesh_epoch)) = mesh_view.get() { + let interop_pos = self.interpolate_transforms(now_inst, last_epoch, entity, &transform, transform_epoch); - last.clone() - }, - Some(last) => last.clone(), - None => { - let cached = CachedTransform { - last_updated_at: None, - cached_at: now_inst, - from_transform: *transform, - to_transform: *transform, - }; - self.entity_last_transforms.insert(entity, cached.clone()); - cached + if let Some(mesh) = mesh_han.data_ref() { + // if process mesh did not just create a new mesh, and the epoch + // shows that the scene has changed, verify that the mesh buffers + // dont need to be resent to the gpu. + if !self.process_mesh(entity, interop_pos, &*mesh, mesh_han.uuid()) + && mesh_epoch == last_epoch { + self.check_mesh_buffers(entity, &mesh_han); + } + + let material = mesh.material.as_ref().unwrap() + .data_ref().unwrap(); + let shader = material.shader_uuid.unwrap_or(0); + let job = RenderJob::new(entity, shader, mesh_han.uuid()); + self.render_jobs.push_back(job); } - }; - //debug!("Transform: {:?}, comp: {:?}", cached.to_transform.translation, transform.transform.translation); - - let fixed_time = match cached.last_updated_at { - Some(last_updated_at) => cached.cached_at - last_updated_at, - None => now_inst - cached.cached_at - }.as_secs_f32(); - let accumulator = (now_inst - cached.cached_at).as_secs_f32(); - let alpha = accumulator / fixed_time; + } - let transform_val = cached.from_transform.lerp(cached.to_transform, alpha); + let scene_view = main_world.view_one::<(&SceneHandle, TickOf)>(entity); + if let Some((scene_han, scene_epoch)) = scene_view.get() { + // TODO: Rendering scenes - let model = model.data_ref(); - for mesh in model.meshes.iter() { - if !self.process_mesh(entity, transform_val, mesh) && model_epoch == last_epoch { - self.update_mesh_buffers(entity, mesh); + if let Some(scene) = scene_han.data_ref() { + let interpo_pos = self.interpolate_transforms(now_inst, last_epoch, entity, &transform, transform_epoch); + let meshes = scene.collect_world_meshes(); + + for (mesh_han, mesh_pos) in meshes.into_iter() { + if let Some(mesh) = mesh_han.data_ref() { + let mesh_interpo = interpo_pos + mesh_pos; + + // if process mesh did not just create a new mesh, and the epoch + // shows that the scene has changed, verify that the mesh buffers + // dont need to be resent to the gpu. + if !self.process_mesh(entity, mesh_interpo, &*mesh, mesh_han.uuid()) + && scene_epoch == last_epoch { + self.check_mesh_buffers(entity, &mesh_han); + } + + let material = mesh.material.as_ref().unwrap() + .data_ref().unwrap(); + let shader = material.shader_uuid.unwrap_or(0); + let job = RenderJob::new(entity, shader, mesh_han.uuid()); + self.render_jobs.push_back(job); + } + } } - - let shader = mesh.material().shader_uuid.unwrap_or(0); - let job = RenderJob::new(entity, shader, mesh.uuid, transform_val); - self.render_jobs.push_back(job); } } - /* for (entity, mesh, mesh_epoch, transform) in main_world.query::<(Entities, &MeshComponent, EpochOf, &TransformComponent)>().iter() { - debug!("TODO: Process MeshComponents"); // TODO: Process MeshComponents - } */ - // collect dead entities self.transform_buffers.tick(); @@ -513,7 +564,7 @@ impl Renderer for BasicRenderer { render_pass.set_pipeline(pipeline.get_wgpu_pipeline()); // get the mesh (containing vertices) and the buffers from storage - let buffers = self.mesh_buffers.get(&job.mesh_buffer_id).unwrap(); + let buffers = self.mesh_buffers.get(&job.mesh_uuid).unwrap(); // Bind the optional texture if let Some(tex) = buffers.material.as_ref() @@ -532,7 +583,7 @@ impl Renderer for BasicRenderer { } // Get the bindgroup for job's transform and bind to it using an offset. - let transform_indices = *self.transform_buffers.entity_indices(job.entity).unwrap(); + let transform_indices = *self.transform_buffers.transform_indices(job.mesh_uuid).unwrap(); let bindgroup = self.transform_buffers.bind_group(transform_indices).unwrap(); let offset = TransformBuffers::index_offset(&self.render_limits, transform_indices) as u32; render_pass.set_bind_group(1, bindgroup, &[ offset, offset, ]); diff --git a/lyra-game/src/render/texture.rs b/lyra-game/src/render/texture.rs index aa99388..e17a0ab 100755 --- a/lyra-game/src/render/texture.rs +++ b/lyra-game/src/render/texture.rs @@ -1,29 +1,10 @@ use std::sync::Arc; use image::GenericImageView; -use lyra_resource::{ResHandle, Texture}; +use lyra_resource::{FilterMode, ResHandle, Texture, WrappingMode}; use super::render_buffer::BindGroupPair; -/* #[derive(Clone)] -pub struct Texture { - texture_id: u32, - pub img: image::DynamicImage, -} - -impl Texture { - pub fn from_bytes(bytes: &[u8]) -> anyhow::Result { - let img = image::load_from_memory(bytes)?; - - Ok(Self { - texture_id: 0, - img, - }) - } -} */ - - - #[allow(dead_code)] pub struct RenderTexture { pub inner_texture: wgpu::Texture, @@ -153,8 +134,101 @@ impl RenderTexture { }) } - pub fn update_texture(&mut self, _device: &wgpu::Device, queue: &wgpu::Queue, texture: &Arc>) { - let texture = &texture.data_ref().image; + pub fn from_resource(device: &wgpu::Device, queue: &wgpu::Queue, bg_layout: Arc, texture_res: &ResHandle, label: Option<&str>) -> anyhow::Result { + let texture_ref = texture_res.data_ref().unwrap(); + let img = texture_ref.image.data_ref().unwrap(); + + let rgba = img.to_rgba8(); + let dimensions = img.dimensions(); + + let size = wgpu::Extent3d { + width: dimensions.0, + height: dimensions.1, + depth_or_array_layers: 1, + }; + let texture = device.create_texture( + &wgpu::TextureDescriptor { + label, + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + } + ); + + queue.write_texture( + wgpu::ImageCopyTexture { + aspect: wgpu::TextureAspect::All, + texture: &texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + }, + &rgba, + wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: std::num::NonZeroU32::new(4 * dimensions.0), + rows_per_image: std::num::NonZeroU32::new(dimensions.1), + }, + size, + ); + + // convert resource sampler into wgpu sampler + let sampler_desc = match &texture_ref.sampler { + Some(sampler) => { + let magf = res_filter_to_wgpu(sampler.mag_filter.unwrap_or(FilterMode::Linear)); + let minf = res_filter_to_wgpu(sampler.min_filter.unwrap_or(FilterMode::Nearest)); + let mipf = res_filter_to_wgpu(sampler.mipmap_filter.unwrap_or(FilterMode::Nearest)); + + let wrap_u = res_wrap_to_wgpu(sampler.wrap_u); + let wrap_v = res_wrap_to_wgpu(sampler.wrap_v); + let wrap_w = res_wrap_to_wgpu(sampler.wrap_w); + + wgpu::SamplerDescriptor { + address_mode_u: wrap_u, + address_mode_v: wrap_v, + address_mode_w: wrap_w, + mag_filter: magf, + min_filter: minf, + mipmap_filter: mipf, + ..Default::default() + } + } + None => wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }, + }; + + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = device.create_sampler( + &sampler_desc + ); + + let bgp = Self::create_bind_group_pair(device, bg_layout, &view, &sampler); + + Ok(Self { + inner_texture: texture, + view, + sampler, + bindgroup_pair: Some(bgp), + }) + } + + /// Updates the texture on the gpu with the provided texture. + /// + /// # Panics + /// Panics if `texture` is not loaded + pub fn update_texture(&mut self, _device: &wgpu::Device, queue: &wgpu::Queue, texture: &ResHandle) { + let texture = &texture.data_ref().unwrap().image; + let texture = &texture.data_ref().unwrap(); let rgba = texture.to_rgba8(); let dimensions = texture.dimensions(); let size = wgpu::Extent3d { @@ -230,4 +304,23 @@ impl RenderTexture { pub fn bind_group(&self) -> &wgpu::BindGroup { &self.bindgroup_pair.as_ref().unwrap().bindgroup } +} + +/// Convert [`lyra_resource::WrappingMode`] to [`wgpu::AddressMode`] +#[inline(always)] +fn res_wrap_to_wgpu(wmode: WrappingMode) -> wgpu::AddressMode { + match wmode { + WrappingMode::ClampToEdge => wgpu::AddressMode::ClampToEdge, + WrappingMode::MirroredRepeat => wgpu::AddressMode::MirrorRepeat, + WrappingMode::Repeat => wgpu::AddressMode::Repeat, + } +} + +/// Convert [`lyra_resource::FilterMode`] to [`wgpu::FilterMode`] +#[inline(always)] +fn res_filter_to_wgpu(fmode: FilterMode) -> wgpu::FilterMode { + match fmode { + FilterMode::Nearest => wgpu::FilterMode::Nearest, + FilterMode::Linear => wgpu::FilterMode::Linear, + } } \ No newline at end of file diff --git a/lyra-game/src/render/transform_buffer_storage.rs b/lyra-game/src/render/transform_buffer_storage.rs index 36e9a1d..4223df5 100644 --- a/lyra-game/src/render/transform_buffer_storage.rs +++ b/lyra-game/src/render/transform_buffer_storage.rs @@ -1,6 +1,6 @@ use std::{collections::{VecDeque, HashMap}, num::NonZeroU64}; -use lyra_ecs::Entity; +use uuid::Uuid; use wgpu::Limits; use std::mem; @@ -19,11 +19,20 @@ pub(crate) struct TransformBufferEntry { pub bindgroup: wgpu::BindGroup, } +/// A helper struct for managing the Transform buffers for meshes. +/// +/// This struct manages a "chain" of uniform buffers that store Transform for meshes. When +/// first created it only has a single "chain-link" with a buffer that is the maximum length +/// the GPU supports. When the first buffer fills up, a new one should be created which will also +/// be the maximum length the GPU supports. When the new buffer fills up, a new one will be +/// created once again, and so on. +/// +/// `Uuid`'s are used to represent entries (usually Meshes) in the buffer. The Uuid's can be used +/// to insert, update, and retrieve the transforms. pub(crate) struct TransformBuffers { pub bindgroup_layout: wgpu::BindGroupLayout, - /// A vector storing the EntityId and - pub just_updated: HashMap, - pub not_updated: HashMap, + pub just_updated: HashMap, + pub not_updated: HashMap, pub dead_indices: VecDeque, pub next_indices: TransformBufferIndices, /// (transform count, buffer, bindgroup) @@ -78,12 +87,15 @@ impl TransformBuffers { s } - /// Update an entity's buffer with the new transform. Will panic if the entity isn't stored - pub fn update_entity(&mut self, queue: &wgpu::Queue, limits: &Limits, entity: Entity, transform: glam::Mat4, normal_matrix: glam::Mat3) -> TransformBufferIndices { - let indices = self.not_updated.remove(&entity) - .or_else(|| self.just_updated.remove(&entity)) + /// Update an transform in the buffer. + /// + /// # Panics + /// Panics if the entity isn't stored, you can check if it is before with [`TransformBuffers:contains`]. + pub fn update_transform(&mut self, queue: &wgpu::Queue, limits: &Limits, uuid: Uuid, transform: glam::Mat4, normal_matrix: glam::Mat3) -> TransformBufferIndices { + let indices = self.not_updated.remove(&uuid) + .or_else(|| self.just_updated.remove(&uuid)) .expect("Use 'insert_entity' for new entities"); - self.just_updated.insert(entity, indices); + self.just_updated.insert(uuid, indices); let normal_matrix = glam::Mat4::from_mat3(normal_matrix); @@ -94,8 +106,8 @@ impl TransformBuffers { indices } - /// Insert a new entity into the buffer, returns where it was stored. - pub fn insert_entity(&mut self, queue: &wgpu::Queue, limits: &Limits, entity: Entity, transform: glam::Mat4, normal_matrix: glam::Mat3) -> TransformBufferIndices { + /// Insert a new transform into the buffer, returns where in the buffer it was stored. + pub fn insert_transform(&mut self, queue: &wgpu::Queue, limits: &Limits, uuid: Uuid, transform: glam::Mat4, normal_matrix: glam::Mat3) -> TransformBufferIndices { let indices = match self.dead_indices.pop_front() { Some(indices) => indices, None => { @@ -113,28 +125,28 @@ impl TransformBuffers { } }; - self.just_updated.insert(entity, indices); - self.update_entity(queue, limits, entity, transform, normal_matrix) + self.just_updated.insert(uuid, indices); + self.update_transform(queue, limits, uuid, transform, normal_matrix) } - /// Update or insert an entities transform - pub fn update_or_insert(&mut self, queue: &wgpu::Queue, limits: &Limits, entity: Entity, transform_fn: TFn) -> TransformBufferIndices + /// Update or insert a transform + pub fn update_or_insert(&mut self, queue: &wgpu::Queue, limits: &Limits, uuid: Uuid, transform_fn: TFn) -> TransformBufferIndices where TFn: Fn() -> (glam::Mat4, glam::Mat3) { let (tran, norm) = transform_fn(); - if self.contains(entity) { - self.update_entity(queue, limits, entity, tran, norm) + if self.contains(uuid) { + self.update_transform(queue, limits, uuid, tran, norm) } else { - self.insert_entity(queue, limits, entity, tran, norm) + self.insert_transform(queue, limits, uuid, tran, norm) } } - /// Returns true if the entity's transform is stored (does not mean its up-to-date). - pub fn contains(&self, entity: Entity) -> bool { - self.not_updated.contains_key(&entity) || self.just_updated.contains_key(&entity) + /// Returns true if the transform related to the `uuid` is stored (does not mean its up-to-date). + pub fn contains(&self, uuid: Uuid) -> bool { + self.not_updated.contains_key(&uuid) || self.just_updated.contains_key(&uuid) } - /// Collect the dead entities, mark entities and not updated for next updates. + /// Collect the dead transforms and prepare self to check next time. pub fn tick(&mut self) { // take the dead entities, these were ones that were not updated this tick let dead: VecDeque = self.not_updated.values().copied().collect(); @@ -164,7 +176,10 @@ impl TransformBuffers { .map(|entry| &entry.bindgroup) } - /// Expand the Transform buffers by adding another uniform buffer binding + /// Expand the Transform buffers by adding another uniform buffer binding. + /// + /// This object has a chain of uniform buffers, when the buffers are expanded, a new + /// "chain-link" is created. pub fn expand_buffers(&mut self, device: &wgpu::Device) { let limits = device.limits(); let max_buffer_sizes = (limits.max_uniform_buffer_binding_size as u64) / 2; @@ -228,7 +243,8 @@ impl TransformBuffers { self.buffer_bindgroups.push(entry); } - pub fn entity_indices(&self, entity: Entity) -> Option<&TransformBufferIndices> { - self.just_updated.get(&entity).or_else(|| self.not_updated.get(&entity)) + /// Returns the indices of the Transform + pub fn transform_indices(&self, uuid: Uuid) -> Option<&TransformBufferIndices> { + self.just_updated.get(&uuid).or_else(|| self.not_updated.get(&uuid)) } } \ No newline at end of file diff --git a/lyra-game/src/scene/mesh.rs b/lyra-game/src/scene/mesh.rs index 7de1ef5..f7c7255 100755 --- a/lyra-game/src/scene/mesh.rs +++ b/lyra-game/src/scene/mesh.rs @@ -1,15 +1,37 @@ use lyra_ecs::Component; -use lyra_resource::Mesh; +use lyra_reflect::Reflect; +use lyra_resource::{gltf::Mesh, ResHandle}; -#[derive(Clone, Component)] +#[derive(Clone, Component, Reflect)] pub struct MeshComponent { - pub mesh: Mesh, + #[reflect(skip)] + pub mesh: ResHandle, +} + +impl From> for MeshComponent { + fn from(value: ResHandle) -> Self { + Self { + mesh: value + } + } +} + +impl std::ops::Deref for MeshComponent { + type Target = ResHandle; + + fn deref(&self) -> &Self::Target { + &self.mesh + } +} + +impl std::ops::DerefMut for MeshComponent { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.mesh + } } impl MeshComponent { - pub fn new(mesh: Mesh) -> Self { - Self { - mesh, - } + pub fn new(mesh: ResHandle) -> Self { + Self::from(mesh) } } \ No newline at end of file diff --git a/lyra-game/src/scene/mod.rs b/lyra-game/src/scene/mod.rs index 47898a1..090ad66 100644 --- a/lyra-game/src/scene/mod.rs +++ b/lyra-game/src/scene/mod.rs @@ -1,9 +1,6 @@ pub mod mesh; pub use mesh::*; -pub mod model; -pub use model::*; - pub mod camera; pub use camera::*; diff --git a/lyra-game/src/scene/model.rs b/lyra-game/src/scene/model.rs deleted file mode 100644 index 2c5a6b3..0000000 --- a/lyra-game/src/scene/model.rs +++ /dev/null @@ -1,41 +0,0 @@ -use lyra_ecs::Component; -use lyra_reflect::Reflect; -use lyra_resource::ResHandle; - -use crate::assets::Model; - -#[derive(Clone, Component, Reflect)] -pub struct ModelComponent(#[reflect(skip)] pub ResHandle); - -impl From> for ModelComponent { - fn from(value: ResHandle) -> Self { - ModelComponent(value) - } -} - -/* impl From for ModelComponent { - -} */ - -impl std::ops::Deref for ModelComponent { - type Target = ResHandle; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl std::ops::DerefMut for ModelComponent { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -/* impl ModelComponent { - pub fn new(model, material: Material) -> Self { - Self { - mesh, - material - } - } -} */ \ No newline at end of file diff --git a/lyra-math/src/transform.rs b/lyra-math/src/transform.rs index cf9600c..35ffd84 100755 --- a/lyra-math/src/transform.rs +++ b/lyra-math/src/transform.rs @@ -92,6 +92,13 @@ impl Transform { trans.y += y; trans.z += z; } + + /// Combines a transform with another one. + /// + /// The translations are added while the rotations and scales are multiplied. + pub fn combine(self, rhs: Transform) -> Transform { + self + rhs + } /// Performs a linear interpolation between `self` and `rhs` based on the value `alpha`. /// diff --git a/lyra-resource/Cargo.toml b/lyra-resource/Cargo.toml index 753caff..a79817c 100644 --- a/lyra-resource/Cargo.toml +++ b/lyra-resource/Cargo.toml @@ -6,7 +6,8 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -lyra-ecs = { path = "../lyra-ecs" } +lyra-ecs = { path = "../lyra-ecs", features = [ "math" ] } +lyra-math = { path = "../lyra-math" } anyhow = "1.0.75" base64 = "0.21.4" crossbeam = { version = "0.8.4", features = [ "crossbeam-channel" ] } @@ -23,3 +24,4 @@ percent-encoding = "2.3.0" thiserror = "1.0.48" tracing = "0.1.37" uuid = { version = "1.4.1", features = ["v4"] } +instant = "0.1" \ No newline at end of file diff --git a/lyra-resource/src/loader/model.rs b/lyra-resource/src/gltf/loader.rs similarity index 65% rename from lyra-resource/src/loader/model.rs rename to lyra-resource/src/gltf/loader.rs index b5ed36d..4298756 100644 --- a/lyra-resource/src/loader/model.rs +++ b/lyra-resource/src/gltf/loader.rs @@ -1,8 +1,12 @@ use std::{sync::Arc, path::PathBuf}; +use glam::{Quat, Vec3}; +use instant::Instant; +use lyra_math::Transform; use thiserror::Error; -use crate::{ResourceLoader, LoaderError, Mesh, Model, MeshVertexAttribute, VertexAttributeData, ResHandle, Material, MeshIndices, ResourceManager, util}; +use crate::{gltf::GltfScene, util, LoaderError, ResHandle, ResourceLoader, ResourceManager}; +use super::{GltfNode, Material, Mesh, MeshIndices, MeshVertexAttribute, VertexAttributeData}; use tracing::debug; @@ -60,16 +64,23 @@ impl ModelLoader { } } */ - fn process_node(buffers: &Vec>, materials: &Vec, node: gltf::Node<'_>) -> Vec { - let mut meshes = vec![]; - //node.transform() + fn process_node(ctx: &mut GltfLoadContext, materials: &Vec>, gnode: gltf::Node<'_>) -> GltfNode { + let mut node = GltfNode::default(); + + node.transform = { + let gt = gnode.transform(); + let (pos, rot, scale) = gt.decomposed(); + + Transform::new(Vec3::from(pos), Quat::from_array(rot), Vec3::from(scale)) + }; + node.name = gnode.name().map(str::to_string); + + if let Some(mesh) = gnode.mesh() { + let mut new_mesh = Mesh::default(); - if let Some(mesh) = node.mesh() { for prim in mesh.primitives() { - let reader = prim.reader(|buf| Some(buffers[buf.index()].as_slice())); + let reader = prim.reader(|buf| Some(ctx.buffers[buf.index()].as_slice())); - let mut new_mesh = Mesh::default(); - // read the positions if let Some(pos) = reader.read_positions() { if prim.mode() != gltf::mesh::Mode::Triangles { @@ -111,18 +122,20 @@ impl ModelLoader { } let mat = materials.get(prim.material().index().unwrap()).unwrap(); - new_mesh.set_material(mat.clone()); - - meshes.push(new_mesh); + new_mesh.material = Some(mat.clone()); } + + let handle = ResHandle::with_data("", new_mesh); + ctx.resource_manager.store_uuid(handle.clone()); + node.mesh = Some(handle); } - for child in node.children() { - let mut child_meshes = ModelLoader::process_node(buffers, materials, child); - meshes.append(&mut child_meshes); + for child in gnode.children() { + let cmesh = ModelLoader::process_node(ctx, materials, child); + node.children.push(cmesh); } - meshes + node } } @@ -147,18 +160,6 @@ impl ResourceLoader for ModelLoader { parent_path.pop(); let parent_path = parent_path.display().to_string(); - /* let (document, buffers, images) = gltf::import(&path)?; - let buffers: Vec> = buffers.into_iter().map(|b| b.0).collect(); - - let scene = document.scenes().next().unwrap(); - - let materials: Vec = document.materials() - .map(|mat| Material::from_gltf(resource_manager, &parent_path, mat)).collect(); - - let meshes: Vec = scene.nodes() - .map(|node| self.process_node(&buffers, &materials, node)) - .flatten().collect(); */ - let gltf = gltf::Gltf::open(path)?; let mut use_bin = false; @@ -172,8 +173,7 @@ impl ResourceLoader for ModelLoader { .map_err(ModelLoaderError::UriDecodingError), }).collect(); - // TODO: Read in multiple scenes - let scene = gltf.scenes().next().unwrap(); + let mut gltf_out = super::Gltf::default(); let mut context = GltfLoadContext { resource_manager, @@ -183,15 +183,38 @@ impl ResourceLoader for ModelLoader { buffers: &buffers, }; - let materials: Vec = gltf.materials() - .map(|mat| Material::from_gltf(&mut context, mat)).collect(); - - let meshes: Vec = scene.nodes() - .flat_map(|node| ModelLoader::process_node(&buffers, &materials, node)) + let start_inst = Instant::now(); + let materials: Vec> = gltf.materials() + .map(|mat| ResHandle::with_data("", Material::from_gltf(&mut context, mat))) .collect(); - debug!("Loaded {} meshes, and {} materials from '{}'", meshes.len(), materials.len(), path); + let mat_time = Instant::now() - start_inst; + debug!("Loaded {} materials in {}s", materials.len(), mat_time.as_secs_f32()); - Ok(Arc::new(ResHandle::with_data(path, Model::new(meshes)))) + for (idx, scene) in gltf.scenes().enumerate() { + let start_inst = Instant::now(); + let nodes: Vec = scene.nodes() + .map(|node| ModelLoader::process_node(&mut context, &materials, node)) + .collect(); + let node_time = Instant::now() - start_inst; + + debug!("Loaded {} nodes in the scene in {}s", nodes.len(), node_time.as_secs_f32()); + + for mesh in nodes.iter().map(|n| &n.mesh) { + if let Some(mesh) = mesh { + gltf_out.meshes.push(mesh.clone()); + } + } + + let scene = GltfScene { + nodes, + }; + let scene = ResHandle::with_data(&format!("{}:Scene{}", path, idx), scene); + gltf_out.scenes.push(scene); + } + + gltf_out.materials = materials; + + Ok(Arc::new(ResHandle::with_data(path, gltf_out))) } #[allow(unused_variables)] @@ -202,7 +225,7 @@ impl ResourceLoader for ModelLoader { #[cfg(test)] mod tests { - use crate::ResourceLoader; + use crate::{gltf::Gltf, ResourceLoader}; use super::*; fn test_file_path(path: &str) -> String { @@ -217,16 +240,27 @@ mod tests { let mut manager = ResourceManager::new(); let loader = ModelLoader::default(); - let model = loader.load(&mut manager, &path).unwrap(); - let model = Arc::downcast::>(model.as_arc_any()).unwrap(); - let model = model.data_ref(); - assert_eq!(model.meshes.len(), 1); // There should only be 1 mesh - let mesh = &model.meshes[0]; + let gltf = loader.load(&mut manager, &path).unwrap(); + let gltf = Arc::downcast::>(gltf.as_arc_any()).unwrap(); + let gltf = gltf.data_ref().unwrap(); + + assert_eq!(gltf.scenes.len(), 1); + let scene = &gltf.scenes[0] + .data_ref().unwrap(); + + assert_eq!(scene.nodes.len(), 1); + let mnode = &scene.nodes[0]; + + assert!(mnode.mesh.is_some()); + assert_eq!(mnode.transform, Transform::from_xyz(0.0, 0.0, 0.0)); + assert_eq!(mnode.children.len(), 0); + + let mesh = mnode.mesh.as_ref().unwrap(); + let mesh = mesh.data_ref().unwrap(); assert!(mesh.position().unwrap().len() > 0); assert!(mesh.normals().unwrap().len() > 0); assert!(mesh.tex_coords().unwrap().len() > 0); assert!(mesh.indices.clone().unwrap().len() > 0); - assert!(mesh.material().base_color_texture.is_some()); - let _mesh_mat = mesh.material(); // inner panic if material was not loaded + assert!(mesh.material.as_ref().unwrap().data_ref().unwrap().base_color_texture.is_some()); } } \ No newline at end of file diff --git a/lyra-resource/src/material.rs b/lyra-resource/src/gltf/material.rs similarity index 77% rename from lyra-resource/src/material.rs rename to lyra-resource/src/gltf/material.rs index 1c3c357..1adeed0 100644 --- a/lyra-resource/src/material.rs +++ b/lyra-resource/src/gltf/material.rs @@ -1,6 +1,9 @@ use std::{collections::hash_map::DefaultHasher, hash::{Hash, Hasher}}; -use crate::{Texture, ResHandle, util, loader::model::GltfLoadContext}; +use gltf::texture::{MagFilter, MinFilter}; + +use crate::{util, FilterMode, Image, ResHandle, Texture, TextureSampler, WrappingMode}; +use super::loader::GltfLoadContext; /// PBR metallic roughness #[derive(Clone, Debug, Default)] @@ -247,15 +250,33 @@ impl Material { fn load_texture(context: &mut GltfLoadContext, texture_info: gltf::texture::Info<'_>) -> ResHandle { // TODO: texture_info.tex_coord() let tex = texture_info.texture(); + // TODO: tex.sampler() let img = tex.source(); let src = img.source(); let buf = Material::read_source(context, src).unwrap(); let buflen = buf.len(); - let mime_type = infer::get(&buf).expect("Failed to get file type").mime_type(); + let mime_type = infer::get(&buf).expect("Failed to get image type").mime_type(); - context.resource_manager.load_bytes::(&uuid::Uuid::new_v4().to_string(), mime_type, - buf, 0, buflen).unwrap() + let tex_img = context.resource_manager.load_bytes::(&uuid::Uuid::new_v4().to_string(), mime_type, + buf, 0, buflen).unwrap(); + + let samp = tex.sampler(); + let samp = TextureSampler { + mag_filter: samp.mag_filter().map(FilterMode::from_mag_filter), + min_filter: samp.min_filter().map(FilterMode::from_min_filter), + mipmap_filter: samp.min_filter().and_then(FilterMode::mipmap_filter), + wrap_u: samp.wrap_s().into(), + wrap_v: samp.wrap_t().into(), + wrap_w: WrappingMode::ClampToEdge, + }; + + let handler = ResHandle::with_data("", Texture { + image: tex_img, + sampler: Some(samp), + }); + context.resource_manager.store_uuid(handler.clone()); + handler } /// Load the Material from a gltf::Material. @@ -295,4 +316,60 @@ impl Material { specular, } } +} + +impl From for FilterMode { + #[inline(always)] + fn from(value: gltf::texture::MagFilter) -> Self { + match value { + gltf::texture::MagFilter::Nearest => Self::Nearest, + gltf::texture::MagFilter::Linear => Self::Linear, + } + } +} + +impl FilterMode { + /// Get the MinFilter mode and the mipmap filter mode from gltf MinFilter + #[inline(always)] + pub fn from_min_filter(minf: MinFilter) -> FilterMode { + match minf { + MinFilter::Nearest => FilterMode::Nearest, + MinFilter::Linear => FilterMode::Linear, + MinFilter::NearestMipmapNearest => FilterMode::Nearest, + MinFilter::LinearMipmapNearest => FilterMode::Linear, + MinFilter::NearestMipmapLinear => FilterMode::Nearest, + MinFilter::LinearMipmapLinear => FilterMode::Linear, + } + } + + #[inline(always)] + pub fn from_mag_filter(magf: MagFilter) -> FilterMode { + match magf { + MagFilter::Nearest => FilterMode::Nearest, + MagFilter::Linear => FilterMode::Linear, + } + } + + #[inline(always)] + pub fn mipmap_filter(minf: MinFilter) -> Option { + match minf { + MinFilter::Nearest => None, + MinFilter::Linear => None, + MinFilter::NearestMipmapNearest => Some(FilterMode::Nearest), + MinFilter::LinearMipmapNearest => Some(FilterMode::Nearest), + MinFilter::NearestMipmapLinear => Some(FilterMode::Linear), + MinFilter::LinearMipmapLinear => Some(FilterMode::Linear), + } + } +} + +impl From for WrappingMode { + #[inline(always)] + fn from(value: gltf::texture::WrappingMode) -> Self { + match value { + gltf::texture::WrappingMode::ClampToEdge => Self::ClampToEdge, + gltf::texture::WrappingMode::MirroredRepeat => Self::MirroredRepeat, + gltf::texture::WrappingMode::Repeat => Self::Repeat, + } + } } \ No newline at end of file diff --git a/lyra-resource/src/model.rs b/lyra-resource/src/gltf/mesh.rs similarity index 78% rename from lyra-resource/src/model.rs rename to lyra-resource/src/gltf/mesh.rs index acc8c2e..49c559f 100644 --- a/lyra-resource/src/model.rs +++ b/lyra-resource/src/gltf/mesh.rs @@ -1,7 +1,8 @@ use std::collections::HashMap; -use crate::Material; -use crate::lyra_engine; +use crate::{lyra_engine, ResHandle}; + +use super::Material; #[repr(C)] #[derive(Clone, Debug, PartialEq)] @@ -82,23 +83,11 @@ pub enum MeshVertexAttribute { Other(String), } -#[derive(Clone, lyra_ecs::Component)] +#[derive(Clone, Default, lyra_ecs::Component)] pub struct Mesh { - pub uuid: uuid::Uuid, pub attributes: HashMap, pub indices: Option, - material: Option, -} - -impl Default for Mesh { - fn default() -> Self { - Self { - uuid: uuid::Uuid::new_v4(), - attributes: Default::default(), - indices: Default::default(), - material: Default::default() - } - } + pub material: Option>, } impl Mesh { @@ -127,26 +116,4 @@ impl Mesh { self.attributes.get(&MeshVertexAttribute::TexCoords) .map(|p| p.as_vec2()) } - - pub fn material(&self) -> Material { - self.material.clone().expect("This mesh is missing a material!") - } - - pub fn set_material(&mut self, val: Material) { - self.material = Some(val); - } -} - -#[derive(Clone, Default)] -pub struct Model { - pub meshes: Vec, - //pub material -} - -impl Model { - pub fn new(meshes: Vec) -> Self { - Self { - meshes, - } - } } \ No newline at end of file diff --git a/lyra-resource/src/gltf/mod.rs b/lyra-resource/src/gltf/mod.rs new file mode 100644 index 0000000..862acdf --- /dev/null +++ b/lyra-resource/src/gltf/mod.rs @@ -0,0 +1,37 @@ +pub mod loader; +pub use loader::*; + +pub mod material; +use lyra_math::Transform; +pub use material::*; + +pub mod mesh; +pub use mesh::*; + +pub mod scene; +pub use scene::*; + +use crate::ResHandle; + +/// A loaded Gltf file +#[derive(Clone, Default)] +pub struct Gltf { + pub scenes: Vec>, + pub materials: Vec>, + pub meshes: Vec>, +} + +impl Gltf { + /// Collects all Gltf meshes and gets their world Transform. + pub fn collect_world_meshes(&self) -> Vec<(ResHandle, Transform)> { + let mut v = vec![]; + + for scene in self.scenes.iter() { + let mut tmp = scene.data_ref() + .unwrap().collect_world_meshes(); + v.append(&mut tmp); + } + + v + } +} \ No newline at end of file diff --git a/lyra-resource/src/gltf/scene.rs b/lyra-resource/src/gltf/scene.rs new file mode 100644 index 0000000..39f1d12 --- /dev/null +++ b/lyra-resource/src/gltf/scene.rs @@ -0,0 +1,55 @@ +use lyra_math::Transform; + +use super::Mesh; +use crate::ResHandle; + +/// A Node in the Gltf file +#[derive(Clone, Default)] +pub struct GltfNode { + pub name: Option, + pub mesh: Option>, + pub transform: Transform, + pub children: Vec, +} + +/// A Scene in a Gltf file +#[derive(Clone)] +pub struct GltfScene { + pub nodes: Vec, +} + +impl GltfScene { + fn collect_node(&self, parent_node: &GltfNode, node: &GltfNode) -> Vec<(ResHandle, Transform)> { + let mut v = vec![]; + + if let Some(mesh) = &node.mesh { + v.push((mesh.clone(), parent_node.transform + node.transform)); + } + + for child in node.children.iter() { + let mut tmp = self.collect_node(node, child); + v.append(&mut tmp); + } + + v + } + + /// Collects all Gltf meshes and gets their world Transform. + pub fn collect_world_meshes(&self) -> Vec<(ResHandle, Transform)> { + let mut v = vec![]; + + // process the root nodes in the scene + for parent_node in self.nodes.iter() { + if let Some(mesh) = &parent_node.mesh { + v.push((mesh.clone(), parent_node.transform)); + } + + for child in parent_node.children.iter() { + let mut tmp = self.collect_node(parent_node, child); + v.append(&mut tmp); + } + } + + v + } +} \ No newline at end of file diff --git a/lyra-resource/src/lib.rs b/lyra-resource/src/lib.rs index 0427c2f..196e75d 100644 --- a/lyra-resource/src/lib.rs +++ b/lyra-resource/src/lib.rs @@ -10,11 +10,7 @@ pub use texture::*; pub mod loader; pub use loader::*; -pub mod model; -pub use model::*; - -pub mod material; -pub use material::*; +pub mod gltf; pub mod world_ext; pub use world_ext::*; diff --git a/lyra-resource/src/loader/image.rs b/lyra-resource/src/loader/image.rs index d7c3df7..d9d0314 100644 --- a/lyra-resource/src/loader/image.rs +++ b/lyra-resource/src/loader/image.rs @@ -1,8 +1,9 @@ use std::{fs::File, sync::Arc, io::Read}; use image::ImageError; +use tracing::{debug, trace}; -use crate::{resource_manager::ResourceStorage, texture::Texture, resource::ResHandle, ResourceManager}; +use crate::{resource::ResHandle, resource_manager::ResourceStorage, Image, ResourceManager}; use super::{LoaderError, ResourceLoader}; @@ -12,7 +13,7 @@ impl From for LoaderError { } } -/// A struct that implements the `ResourceLoader` trait used for loading textures. +/// A struct that implements the `ResourceLoader` trait used for loading images. #[derive(Default)] pub struct ImageLoader; @@ -59,25 +60,25 @@ impl ResourceLoader for ImageLoader { ImageError::IoError(e) => LoaderError::IoError(e), _ => LoaderError::DecodingError(e.into()), })?; - let texture = Texture { - image, - }; - let res = ResHandle::with_data(path, texture); + let image = Image::from(image); + let res = ResHandle::with_data(path, image); Ok(Arc::new(res)) } fn load_bytes(&self, _resource_manager: &mut ResourceManager, bytes: Vec, offset: usize, length: usize) -> Result, LoaderError> { + trace!("Loading {} bytes as an image", length); + let image = image::load_from_memory(&bytes[offset..(length-offset)]) .map_err(|e| match e { ImageError::IoError(e) => LoaderError::IoError(e), _ => LoaderError::DecodingError(e.into()), })?; - let texture = Texture { - image, - }; - let res = ResHandle::with_data(&uuid::Uuid::new_v4().to_string(), texture); + let image = Image::from(image); + let res = ResHandle::with_data(&uuid::Uuid::new_v4().to_string(), image); + debug!("Finished loading image of {} bytes", length); + Ok(Arc::new(res)) } } diff --git a/lyra-resource/src/loader/mod.rs b/lyra-resource/src/loader/mod.rs index a4e9e64..5e2f0f6 100644 --- a/lyra-resource/src/loader/mod.rs +++ b/lyra-resource/src/loader/mod.rs @@ -1,5 +1,4 @@ pub mod image; -pub mod model; use std::{io, sync::Arc, path::Path, ffi::OsStr}; diff --git a/lyra-resource/src/resource.rs b/lyra-resource/src/resource.rs index 1423de2..3b7610b 100644 --- a/lyra-resource/src/resource.rs +++ b/lyra-resource/src/resource.rs @@ -1,5 +1,7 @@ use std::{any::Any, sync::{Arc, RwLock}}; +use lyra_ecs::Component; +use crate::lyra_engine; use uuid::Uuid; use crate::ResourceStorage; @@ -38,7 +40,8 @@ pub struct Resource { /// This struct has an inner [`RwLock`] to the resource data, so most methods may be blocking. /// However, the only times it will be blocking is if another thread is reloading the resource /// and has a write lock on the data. This means that most of the time, it is not blocking. -pub struct ResHandle { +#[derive(Component)] +pub struct ResHandle { pub(crate) data: Arc>>, } @@ -103,18 +106,7 @@ impl ResHandle { } /// Get a reference to the data in the resource - /// - /// # Panics - /// Panics if the resource was not loaded yet. - pub fn data_ref<'a>(&'a self) -> ResourceDataRef<'a, T> { - let d = self.data.read().expect("Resource mutex was poisoned!"); - ResourceDataRef { - guard: d - } - } - - /// Attempt to get a borrow to the resource data. Returns `None` if the resource is not loaded. - pub fn try_data_ref<'a>(&'a self) -> Option> { + pub fn data_ref<'a>(&'a self) -> Option> { if self.is_loaded() { let d = self.data.read().expect("Resource mutex was poisoned!"); Some(ResourceDataRef { diff --git a/lyra-resource/src/resource_manager.rs b/lyra-resource/src/resource_manager.rs index dc24d33..2069b5e 100644 --- a/lyra-resource/src/resource_manager.rs +++ b/lyra-resource/src/resource_manager.rs @@ -6,7 +6,7 @@ use notify_debouncer_full::{DebouncedEvent, FileIdMap}; use thiserror::Error; use uuid::Uuid; -use crate::{loader::{image::ImageLoader, model::ModelLoader, LoaderError, ResourceLoader}, resource::ResHandle, ResourceState}; +use crate::{gltf::ModelLoader, loader::{image::ImageLoader, LoaderError, ResourceLoader}, resource::ResHandle, ResourceState}; /// A trait for type erased storage of a resource. /// Implemented for [`ResHandle`] @@ -56,6 +56,7 @@ pub struct ResourceWatcher { pub struct ResourceManager { resources: HashMap>, + uuid_resources: HashMap>, loaders: Vec>, watchers: HashMap, } @@ -70,6 +71,7 @@ impl ResourceManager { pub fn new() -> Self { Self { resources: HashMap::new(), + uuid_resources: HashMap::new(), loaders: vec![ Arc::new(ImageLoader), Arc::new(ModelLoader) ], watchers: HashMap::new(), } @@ -148,6 +150,29 @@ impl ResourceManager { } } + /// Store a resource using its uuid. + /// + /// The resource cannot be requested with [`ResourceManager::request`], it can only be + /// retrieved with [`ResourceManager::request_uuid`]. + pub fn store_uuid(&mut self, res: ResHandle) { + self.uuid_resources.insert(res.uuid(), Arc::new(res)); + } + + /// Request a resource via its uuid. + /// + /// Returns `None` if the resource was not found. The resource must of had been + /// stored with [`ResourceManager::request`] to return `Some`. + pub fn request_uuid(&mut self, uuid: &Uuid) -> Option> { + match self.uuid_resources.get(uuid) { + Some(res) => { + let res = res.clone().as_arc_any(); + let res: Arc> = res.downcast::>().expect("Failure to downcast resource"); + Some(ResHandle::::clone(&res)) + }, + None => None, + } + } + /// Store bytes in the manager. If there is already an entry with the same identifier it will be updated. /// /// Panics: If there is already an entry with the same `ident`, and the entry is not bytes, this function will panic. @@ -304,7 +329,7 @@ mod tests { let mut man = ResourceManager::new(); let res = man.request::(&get_image("squiggles.png")).unwrap(); assert_eq!(res.state(), ResourceState::Ready); - let img = res.try_data_ref(); + let img = res.data_ref(); img.unwrap(); } @@ -340,7 +365,7 @@ mod tests { let mut man = ResourceManager::new(); let res = man.request::(&get_image("squiggles.png")).unwrap(); assert_eq!(res.state(), ResourceState::Ready); - let img = res.try_data_ref(); + let img = res.data_ref(); img.unwrap(); println!("Path = {}", res.path()); @@ -360,7 +385,7 @@ mod tests { let mut man = ResourceManager::new(); let res = man.request::(&image_path).unwrap(); assert_eq!(res.state(), ResourceState::Ready); - let img = res.try_data_ref(); + let img = res.data_ref(); img.unwrap(); let recv = man.watch(&image_path, false).unwrap(); diff --git a/lyra-resource/src/texture.rs b/lyra-resource/src/texture.rs index 5874d18..179b190 100644 --- a/lyra-resource/src/texture.rs +++ b/lyra-resource/src/texture.rs @@ -1,10 +1,73 @@ +use std::ops::{Deref, DerefMut}; + +//pub use gltf::texture::{MagFilter, MinFilter, WrappingMode}; use image::DynamicImage; +use crate::ResHandle; + +/// The filter mode of the sampler. +/// +/// This is used for minification, magnification, and mipmap filters +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum FilterMode { + Nearest, + Linear, +} + +/// The wrapping mode of the Texture coordinates +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum WrappingMode { + ClampToEdge, + MirroredRepeat, + Repeat, +} + +/// The descriptor of the sampler for a Texture. +#[derive(Clone)] +pub struct TextureSampler { + pub mag_filter: Option, + pub min_filter: Option, + pub mipmap_filter: Option, + pub wrap_u: WrappingMode, + pub wrap_v: WrappingMode, + pub wrap_w: WrappingMode, +} + +#[derive(Clone)] +pub struct Image(DynamicImage); + +impl Deref for Image { + type Target = DynamicImage; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Image { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From for Image { + fn from(value: DynamicImage) -> Self { + Self(value) + } +} + #[derive(Clone)] pub struct Texture { - pub image: DynamicImage, + pub image: ResHandle, + pub sampler: Option, } impl Texture { - + /// Create a texture from an image. + pub fn from_image(image: ResHandle) -> Self { + Self { + image, + sampler: None, + } + } } \ No newline at end of file diff --git a/lyra-scene/Cargo.toml b/lyra-scene/Cargo.toml new file mode 100644 index 0000000..79a85fa --- /dev/null +++ b/lyra-scene/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "lyra-scene" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +lyra-ecs = { path = "../lyra-ecs", features = [ "math" ] } +lyra-math = { path = "../lyra-math" } diff --git a/lyra-scene/src/lib.rs b/lyra-scene/src/lib.rs new file mode 100644 index 0000000..dde3c66 --- /dev/null +++ b/lyra-scene/src/lib.rs @@ -0,0 +1,316 @@ +use std::{collections::VecDeque, ops::{Deref, DerefMut}}; + +use lyra_ecs::{query::Entities, relation::ChildOf, Bundle, Component, Entity, World}; + +// So we can use lyra_ecs::Component derive macro +pub(crate) mod lyra_engine { + pub(crate) mod ecs { + pub use lyra_ecs::*; + } +} + +mod node; +use lyra_math::{Transform, Vec3}; +pub use node::*; + +/// A flag spawned on all scene node entities +#[derive(Component)] +pub struct SceneNodeFlag; + +/// A flag spawned on only the scene root node +#[derive(Component)] +pub struct SceneNodeRoot; + +enum MutCow<'a, T> { + Mut(&'a mut T), + Owned(T), +} + +impl<'a, T> Deref for MutCow<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + match self { + MutCow::Mut(t) => t, + MutCow::Owned(t) => t, + } + } +} + +impl<'a, T> DerefMut for MutCow<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + match self { + MutCow::Mut(t) => t, + MutCow::Owned(t) => t, + } + } +} + +/// A SceneGraph is a Graph of nodes that represents the hierarchy of a scene. +/// +/// This SceneGraph is special in the sense that it is literally just an ECS world with methods +/// implemented for it that make it easier to use for a SceneGraph. +//#[derive(Default)] +pub struct SceneGraph<'a> { + pub(crate) world: MutCow<'a, World>, + root_node: SceneNode, +} + +impl<'a> SceneGraph<'a> { + /// Create a new SceneGraph with its own ECS World. + pub fn new() -> Self { + let mut world = World::new(); + + let e = world.spawn((Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)), SceneNodeRoot)); + let root = SceneNode::new(None, e); + + Self { + world: MutCow::Owned(world), + root_node: root + } + } + + /// Retrieve a SceneGraph from an ECS World. + /// + /// Returns `None` if the `root_entity` was not created from a `SceneGraph` that was later + /// inserted into another world with [`SceneGraph::into_world`]. + pub fn from_world(world: &'a mut World, root_entity: Entity) -> Option { + if world.view_one::<(&SceneNodeRoot, &Transform)>(root_entity).get().is_none() { + None + } else { + Some(Self { + world: MutCow::Mut(world), + root_node: SceneNode::new(None, root_entity), + }) + } + } + + /// Create a new SceneGraph inside an existing ECS World. + pub fn new_from_world(world: &'a mut World) -> Self { + let root_en = world.spawn((Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)), SceneNodeRoot)); + let root = SceneNode::new(None, root_en); + + Self { + world: MutCow::Mut(world), + root_node: root, + } + } + + /// Consume the SceneGraph, inserting its internal world into the provided world. + /// + /// The `into` World will contain all nodes of the SceneGraph. You could iterate through the + /// SceneGraph manually if you'd like. The root entity has a [`SceneNodeRoot`] flag component, + /// and a Transform component. The children nodes have a [`SceneNodeFlag`] component and a + /// Transform component. The transforms of the children will be local, you must find its world + /// transform by traversing up the hierarchy of the scene manually with the `ChildOf` relation. + /// + /// Returns: the Entity of the root node in the provided world. + pub fn into_world(mut self, into: &mut World) -> Entity { + // first insert the root entity into the World. + let v = self.world.view_one::<&Transform>(self.root_node.entity()); + let pos = *v.get().unwrap(); + let new_root = into.spawn((pos, SceneNodeRoot)); + + // now process the children of the root node. + Self::traverse_inserting_into(&mut self.world, self.root_node.entity(), new_root, into); + new_root + } + + /// Recursively traverse the `from` world, starting at `node_en`, inserting the + /// children entity into the world. + /// + /// Parameters: + /// * `scene_world` - The world that the SceneGraph exists in + /// * `scene_en` - The entity of the parent entity inside of the `scene_world`. + /// * `parent_en` - The entity of the parent entity inside of the `into` world. + /// * `into` - The world to insert the SceneGraph world into. + fn traverse_inserting_into(scene_world: &mut World, scene_en: Entity, parent_en: Entity, into: &mut World) { + let v = scene_world.view::<(Entities, &Transform)>() + .relates_to::(scene_en); + + // unfortunately, the borrow checker exists, and wasn't happy that `scene_world` was + // immutably borrowed with the view (v), and being mutably borrowed in the + // recursive call. + let mut child_entities = VecDeque::new(); + + for ((child_scene_en, pos), _rel) in v.iter() { + let into_en = into.spawn((*pos, SceneNodeFlag)); + into.add_relation(into_en, ChildOf, parent_en); + + child_entities.push_back((child_scene_en, into_en)); + } + + while let Some((child_scene_en, into_en)) = child_entities.pop_front() { + Self::traverse_inserting_into(scene_world, child_scene_en, into_en, into); + } + } + + /// Adds a node to the root node of the SceneGraph. + /// + /// The spawned entity will have a `ChildOf` relation targeting the root node, the + /// `SceneNodeFlag` component is also added to the entity. + pub fn add_node(&mut self, local_transform: Transform, bundle: B) -> SceneNode { + let node = self.root_node.clone(); + self.add_node_under(&node, local_transform, bundle) + } + + /// Add a node under a parent node. + /// + /// The spawned entity will have a `ChildOf` relation targeting the provided parent node, + /// the `SceneNodeFlag` component is also added to the entity. + pub fn add_node_under(&mut self, parent: &SceneNode, local_transform: Transform, bundle: B) -> SceneNode { + world_add_child_node(&mut self.world, parent, local_transform, bundle) + } + + pub fn add_empty_node_under(&mut self, parent: &SceneNode, local_transform: Transform) -> SceneNode { + let e = self.world.spawn((SceneNodeFlag, local_transform)); + self.world.add_relation(e, ChildOf, parent.entity()); + + SceneNode::new(Some(parent.entity()), e) + } + + /// Traverses down the SceneGraph, calling `callback` with each SceneNode and its world transform. + /// + /// The traversal does not include the root scene node. + pub fn traverse_down(&self, mut callback: F) + where + F: FnMut(&SceneNode, Transform), + { + self.traverse_down_from(self.root_node.clone(), &mut callback); + } + + /// Recursively Traverses down the SceneGraph from a starting node, calling `callback` with each + /// SceneNode and its world transform. + fn traverse_down_from(&self, start: SceneNode, callback: &mut F) + where + F: FnMut(&SceneNode, Transform), + { + let v = self.world.view::<(Entities, &Transform)>() + .relates_to::(start.entity()); + + for ((e, _), _rel) in v.iter() { + let node = SceneNode::new(Some(start.entity()), e); + let world_pos = node.world_transform(self); + callback(&node, world_pos); + + self.traverse_down_from(node, callback); + } + } + + pub fn root_node(&self) -> SceneNode { + self.root_node.clone() + } +} + +/// Add a node under a parent node. +/// +/// The spawned entity will have a `ChildOf` relation targeting the provided parent node, +/// the `SceneNodeFlag` component is also added to the entity. +pub(crate) fn world_add_child_node(world: &mut World, parent: &SceneNode, local_transform: Transform, bundle: B) -> SceneNode { + let e = world.spawn(bundle); + world.insert(e, (SceneNodeFlag, local_transform)); + world.add_relation(e, ChildOf, parent.entity()); + + SceneNode::new(Some(parent.entity()), e) +} + +#[cfg(test)] +pub mod tests { + use lyra_ecs::{query::Entities, relation::ChildOf, Component, World}; + use lyra_math::{Transform, Vec3}; + + use crate::{lyra_engine, SceneGraph, SceneNodeRoot}; + + #[derive(Component)] + pub struct FakeMesh; + + #[test] + fn single_node_hierarchy() { + let mut scene = SceneGraph::new(); + + let a = scene.add_node(Transform::from_translation(Vec3::new(10.0, 10.0, 10.0)), FakeMesh); + assert!(a.parent(&scene).unwrap() == scene.root_node); + } + + #[test] + fn double_node_hierarchy() { + let mut scene = SceneGraph::new(); + + let a = scene.add_node(Transform::from_translation(Vec3::new(10.0, 10.0, 10.0)), FakeMesh); + assert!(a.parent(&scene).unwrap() == scene.root_node); + + let b = a.add_node(&mut scene, Transform::from_translation(Vec3::new(50.0, 50.0, 50.0)), FakeMesh); + assert!(b.parent(&scene).unwrap() == a); + } + + #[test] + fn traverse_down() { + let v2s = vec![Vec3::new(10.0, 10.0, 10.0), Vec3::new(50.0, 50.0, 50.0)]; + + let mut scene = SceneGraph::new(); + + let a = scene.add_node(Transform::from_translation(v2s[0]), FakeMesh); + assert!(a.parent(&scene).unwrap() == scene.root_node); + let b = a.add_node(&mut scene, Transform::from_translation(v2s[1]), FakeMesh); + assert!(b.parent(&scene).unwrap() == a); + + let mut idx = 0; + scene.traverse_down(|_e, pos| { + if idx == 0 { + assert_eq!(pos, Transform::from_translation(v2s[idx])); + } else if idx == 1 { + let t = v2s.iter().sum(); + assert_eq!(pos, Transform::from_translation(t)); + } + + idx += 1; + }); + } + + /* #[test] + fn inserting_and_from_world() { + let v2s = vec![Vec3::new(10.0, 10.0, 10.0), Vec3::new(50.0, 50.0, 50.0)]; + + let mut scene = SceneGraph::new(); + + let a = scene.add_node(Transform::from_translation(v2s[0]), FakeMesh); + assert!(a.parent(&scene).unwrap() == scene.root_node); + let b = a.add_node(&mut scene, Transform::from_translation(v2s[1]), FakeMesh); + assert!(b.parent(&scene).unwrap() == a); + + let mut other_world = World::new(); + let root = scene.into_world(&mut other_world); + + // check all of the entities inside of the World + let (root_pos, _) = other_world.view_one::<(&Transform, &SceneNodeRoot)>(root).get().unwrap(); + assert_eq!(*root_pos, Transform::from_xyz(0.0, 0.0, 0.0)); + + let ((child_en, child_pos), _) = other_world.view::<(Entities, &Transform)>() + .relates_to::(root).iter().next().unwrap(); + assert_eq!(*child_pos, Transform::from_translation(v2s[0])); + + let ((_, childchild_pos), _) = other_world.view::<(Entities, &Transform)>() + .relates_to::(child_en).iter().next().unwrap(); + assert_eq!(*childchild_pos, Transform::from_translation(v2s[1])); + + drop(root_pos); + drop(child_pos); + drop(childchild_pos); + + // Now get the SceneGraph inside the World and use the nice utility tools to traverse it. + let scene = SceneGraph::from_world(&mut other_world, root) + .expect("Failed to get SceneGraph from World!"); + + let mut idx = 0; + scene.traverse_down(|_e, pos| { + if idx == 0 { + assert_eq!(pos, Transform::from_translation(v2s[idx])); + } else if idx == 1 { + let t = v2s.iter().sum(); + assert_eq!(pos, Transform::from_translation(t)); + } + + idx += 1; + }); + } */ +} \ No newline at end of file diff --git a/lyra-scene/src/node.rs b/lyra-scene/src/node.rs new file mode 100644 index 0000000..557e56d --- /dev/null +++ b/lyra-scene/src/node.rs @@ -0,0 +1,101 @@ +use lyra_ecs::{query::Entities, relation::{ChildOf, RelationOriginComponent}, Bundle, Entity}; +use lyra_math::Transform; + +use crate::SceneGraph; + +#[derive(Clone, PartialEq, Eq)] +pub struct SceneNode { + //world: WorldLock, + parent: Option, + entity: Entity +} + +impl SceneNode { + pub fn new(parent: Option, entity: Entity) -> Self { + Self { + parent, + entity, + } + } + + /// If this node has a parent, retrieves the parent node from the SceneGraph. + pub fn parent(&self, graph: &SceneGraph) -> Option { + if let Some(parent) = self.parent { + let v = graph.world.view_one::<&RelationOriginComponent>(parent); + + let p_parent = if let Some(pp) = v.get() { + Some(pp.target()) + } else { None }; + + Some(SceneNode::new(p_parent, parent)) + } else { + None + } + } + + pub fn parent_entity(&self) -> Option { + self.parent + } + + pub fn children(&self, graph: &SceneGraph) -> Vec { + let v = graph.world.view::() + .relates_to::(self.entity); + + v.into_iter().map(|(e, _)| SceneNode::new(Some(self.entity), e)).collect() + } + + pub fn entity(&self) -> Entity { + self.entity + } + + /// Add a child node to this node. + /// + /// The spawned entity backing the scene node will have a `ChildOf` relation targeting + /// the provided parent node, the `SceneNodeFlag` component is also added to the entity. + pub fn add_node(&self, graph: &mut SceneGraph, local_transform: Transform, bundle: B) -> SceneNode { + graph.add_node_under(self, local_transform, bundle) + } + + pub fn local_transform(&self, graph: &SceneGraph) -> Transform { + let v = graph.world.view_one::<&Transform>(self.entity); + + let t = v.get().expect("Somehow the SceneNode is missing its Transform!"); + t.clone() + } + + /// Retrieves the world transform of the Node. + /// + /// This traverses up the SceneGraph from this node. + pub fn world_transform(&self, graph: &SceneGraph) -> Transform { + match self.parent(graph) { + Some(parent) => { + let pt = parent.world_transform(graph); + pt + self.local_transform(graph) + }, + None => { + self.local_transform(graph) + } + } + } +} + +#[cfg(test)] +mod tests { + use lyra_math::{Transform, Vec3}; + + use crate::{tests::FakeMesh, SceneGraph}; + + #[test] + fn node_world_transform() { + let mut scene = SceneGraph::new(); + + let a = scene.add_node(Transform::from_translation(Vec3::new(10.0, 10.0, 10.0)), FakeMesh); + assert!(a.parent(&scene).unwrap() == scene.root_node); + + let b = a.add_node(&mut scene, Transform::from_translation(Vec3::new(50.0, 50.0, 50.0)), FakeMesh); + assert!(b.parent(&scene).unwrap() == a); + + let wrld_tran = b.world_transform(&scene); + assert_eq!(wrld_tran, Transform::from_translation(Vec3::new(60.0, 60.0, 60.0))); + } +} \ No newline at end of file