From a96f3cfda596e988496c8fcb5bda66d7bd6c65dc Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Mon, 2 Dec 2019 01:31:07 -0800 Subject: [PATCH] asset loading, hierarchies, more refactoring --- Cargo.toml | 3 +- examples/simple.rs | 13 +- src/application.rs | 125 ++----- src/asset/mod.rs | 73 ++++ src/core/mod.rs | 19 +- src/core/transform.rs | 14 - src/legion_transform/.gitignore | 24 ++ src/legion_transform/.travis.yml | 8 + src/legion_transform/Cargo.toml | 20 ++ src/legion_transform/LICENSE | 21 ++ src/legion_transform/README.md | 211 +++++++++++ .../benches/local_to_world.rs | 46 +++ src/legion_transform/examples/hierarchy.rs | 155 ++++++++ .../examples/types_of_transforms.rs | 94 +++++ .../src/components/children.rs | 13 + .../src/components/local_to_parent.rs | 25 ++ .../src/components/local_to_world.rs | 26 ++ src/legion_transform/src/components/mod.rs | 17 + .../src/components/non_uniform_scale.rs | 41 +++ src/legion_transform/src/components/parent.rs | 10 + .../src/components/rotation.rs | 29 ++ src/legion_transform/src/components/scale.rs | 32 ++ .../src/components/translation.rs | 36 ++ .../src/hierarchy_maintenance_system.rs | 234 ++++++++++++ src/legion_transform/src/lib.rs | 18 + .../src/local_to_parent_system.rs | 323 +++++++++++++++++ .../src/local_to_world_propagate_system.rs | 133 +++++++ .../src/local_to_world_system.rs | 340 ++++++++++++++++++ .../src/transform_system_bundle.rs | 20 ++ src/lib.rs | 11 +- src/render/camera.rs | 4 + src/render/forward/mod.rs | 19 +- src/render/mesh.rs | 111 ++++++ src/render/mod.rs | 7 + src/render/shadow/mod.rs | 20 +- src/temp.rs | 35 +- src/vertex.rs | 90 ----- tiny-town/src/main.rs | 15 +- 38 files changed, 2195 insertions(+), 240 deletions(-) create mode 100644 src/asset/mod.rs delete mode 100644 src/core/transform.rs create mode 100644 src/legion_transform/.gitignore create mode 100644 src/legion_transform/.travis.yml create mode 100644 src/legion_transform/Cargo.toml create mode 100644 src/legion_transform/LICENSE create mode 100644 src/legion_transform/README.md create mode 100644 src/legion_transform/benches/local_to_world.rs create mode 100644 src/legion_transform/examples/hierarchy.rs create mode 100644 src/legion_transform/examples/types_of_transforms.rs create mode 100644 src/legion_transform/src/components/children.rs create mode 100644 src/legion_transform/src/components/local_to_parent.rs create mode 100644 src/legion_transform/src/components/local_to_world.rs create mode 100644 src/legion_transform/src/components/mod.rs create mode 100644 src/legion_transform/src/components/non_uniform_scale.rs create mode 100644 src/legion_transform/src/components/parent.rs create mode 100644 src/legion_transform/src/components/rotation.rs create mode 100644 src/legion_transform/src/components/scale.rs create mode 100644 src/legion_transform/src/components/translation.rs create mode 100644 src/legion_transform/src/hierarchy_maintenance_system.rs create mode 100644 src/legion_transform/src/lib.rs create mode 100644 src/legion_transform/src/local_to_parent_system.rs create mode 100644 src/legion_transform/src/local_to_world_propagate_system.rs create mode 100644 src/legion_transform/src/local_to_world_system.rs create mode 100644 src/legion_transform/src/transform_system_bundle.rs create mode 100644 src/render/mesh.rs diff --git a/Cargo.toml b/Cargo.toml index 8b9a1102f0..eb8e53b035 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,8 @@ authors = ["Carter Anderson "] edition = "2018" [dependencies] -legion = { git = "https://github.com/TomGillen/legion.git" } +legion = { git = "https://github.com/TomGillen/legion.git", rev = "8628b227bcbe57582fffb5e80e73c634ec4eebd9" } +legion_transform = { path = "src/legion_transform" } nalgebra-glm = "0.5.0" wgpu = { git = "https://github.com/gfx-rs/wgpu-rs.git", rev = "44fa1bc2fa208fa92f80944253e0da56cb7ac1fe"} winit = "0.20.0-alpha4" diff --git a/examples/simple.rs b/examples/simple.rs index 1855901c31..e099f9bcc8 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,18 +1,9 @@ -use bevy::{Application, Transform}; +use bevy::{Application}; use legion::prelude::*; fn main() { - Application::run(); // Create a world to store our entities let universe = Universe::new(); let mut world = universe.create_world(); - world.insert((), vec![(Transform::new(),)]); - - // Create a query which finds all `Position` and `Velocity` components - let mut query = Read::::query(); - - // // Iterate through all entities that match the query in the world - for _ in query.iter(&mut world) { - // println!("{} hi", trans.global); - } + Application::run(universe, world); } diff --git a/src/application.rs b/src/application.rs index dcb16f2ebc..fd74b0d9a7 100644 --- a/src/application.rs +++ b/src/application.rs @@ -10,12 +10,13 @@ use legion::prelude::*; use std::sync::Arc; use std::mem; -use crate::{temp::*, vertex::*, render::*, math, Transform}; +use crate::{temp::*, vertex::*, render::*, math, LocalToWorld, Translation, ApplicationStage}; pub struct Application { pub universe: Universe, pub world: World, + pub scheduler: SystemScheduler, pub shadow_pass: ShadowPass, pub forward_pass: ForwardPass, camera_position: math::Vec3, @@ -26,35 +27,13 @@ impl Application { pub const MAX_LIGHTS: usize = 10; fn init( + universe: Universe, + mut world: World, sc_desc: &wgpu::SwapChainDescriptor, device: &wgpu::Device, ) -> (Self, Option) { - let universe = Universe::new(); - let mut world = universe.create_world(); - let vertex_size = mem::size_of::(); - let (cube_vertex_data, cube_index_data) = create_cube(); - let cube_vertex_buf = Arc::new( - device.create_buffer_with_data(cube_vertex_data.as_bytes(), wgpu::BufferUsage::VERTEX), - ); - - let cube_index_buf = Arc::new( - device.create_buffer_with_data(cube_index_data.as_bytes(), wgpu::BufferUsage::INDEX), - ); - - let (plane_vertex_data, plane_index_data) = create_plane(7); - let plane_vertex_buf = - device.create_buffer_with_data(plane_vertex_data.as_bytes(), wgpu::BufferUsage::VERTEX); - - let plane_index_buf = - device.create_buffer_with_data(plane_index_data.as_bytes(), wgpu::BufferUsage::INDEX); - - let entity_uniform_size = mem::size_of::() as wgpu::BufferAddress; - let plane_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor { - size: entity_uniform_size, - usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, - }); let local_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { @@ -65,81 +44,33 @@ impl Application { }], }); - let mut entities = vec![{ + + let mut entities = >::query(); + for mut entity in entities.iter(&mut world) { + let entity_uniform_size = mem::size_of::() as wgpu::BufferAddress; + let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor { + size: entity_uniform_size, + usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, + }); + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { layout: &local_bind_group_layout, bindings: &[wgpu::Binding { binding: 0, resource: wgpu::BindingResource::Buffer { - buffer: &plane_uniform_buf, + buffer: &uniform_buf, range: 0 .. entity_uniform_size, }, }], }); - (CubeEnt { - rotation_speed: 0.0, - color: wgpu::Color::WHITE, - vertex_buf: Arc::new(plane_vertex_buf), - index_buf: Arc::new(plane_index_buf), - index_count: plane_index_data.len(), - bind_group, - uniform_buf: plane_uniform_buf, - }, Transform::new()) - }]; + + entity.bind_group = Some(bind_group); + entity.uniform_buf = Some(uniform_buf); + } let camera_position = math::vec3(3.0f32, -10.0, 6.0); let camera_fov = math::quarter_pi(); - struct CubeDesc { - offset: math::Vec3, - rotation: f32, - } - let cube_descs = [ - CubeDesc { - offset: math::vec3(-2.0, -2.0, 2.0), - rotation: 0.1, - }, - CubeDesc { - offset: math::vec3(2.0, -2.0, 2.0), - rotation: 0.2, - }, - CubeDesc { - offset: math::vec3(-2.0, 2.0, 2.0), - rotation: 0.3, - }, - CubeDesc { - offset: math::vec3(2.0, 2.0, 2.0), - rotation: 0.4, - }, - ]; - - for cube in &cube_descs { - let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor { - size: entity_uniform_size, - usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, - }); - entities.push((CubeEnt { - rotation_speed: cube.rotation, - color: wgpu::Color::GREEN, - vertex_buf: Arc::clone(&cube_vertex_buf), - index_buf: Arc::clone(&cube_index_buf), - index_count: cube_index_data.len(), - bind_group: device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: &local_bind_group_layout, - bindings: &[wgpu::Binding { - binding: 0, - resource: wgpu::BindingResource::Buffer { - buffer: &uniform_buf, - range: 0 .. entity_uniform_size, - }, - }], - }), - uniform_buf, - }, Transform { value: math::translation(&cube.offset)})); - } - - world.insert((), entities); - let vb_desc = wgpu::VertexBufferDescriptor { stride: vertex_size as wgpu::BufferAddress, step_mode: wgpu::InputStepMode::Vertex, @@ -227,6 +158,7 @@ impl Application { let this = Application { universe, world, + scheduler: SystemScheduler::new(), shadow_pass, forward_pass, camera_position, @@ -272,7 +204,7 @@ impl Application { device.create_command_encoder(&wgpu::CommandEncoderDescriptor { todo: 0 }); { - let mut entities = <(Read, Read)>::query(); + let mut entities = <(Read, Read)>::query(); let entities_count = entities.iter(&mut self.world).count(); let size = mem::size_of::(); let temp_buf_data = device @@ -283,12 +215,12 @@ impl Application { { slot.copy_from_slice( EntityUniforms { - model: transform.value.into(), + model: transform.0.into(), color: [ - entity.color.r as f32, - entity.color.g as f32, - entity.color.b as f32, - entity.color.a as f32, + entity.color.x as f32, + entity.color.y as f32, + entity.color.z as f32, + entity.color.w as f32, ], } .as_bytes(), @@ -301,7 +233,7 @@ impl Application { encoder.copy_buffer_to_buffer( &temp_buf, (i * size) as wgpu::BufferAddress, - &entity.uniform_buf, + entity.uniform_buf.as_ref().unwrap(), 0, size as wgpu::BufferAddress, ); @@ -315,7 +247,7 @@ impl Application { } #[allow(dead_code)] - pub fn run() { + pub fn run(universe: Universe, world: World) { env_logger::init(); let event_loop = EventLoop::new(); log::info!("Initializing the window..."); @@ -354,7 +286,7 @@ impl Application { let mut swap_chain = device.create_swap_chain(&surface, &sc_desc); log::info!("Initializing the example..."); - let (mut example, init_command_buf) = Application::init(&sc_desc, &device); + let (mut example, init_command_buf) = Application::init(universe, world, &sc_desc, &device); if let Some(command_buf) = init_command_buf { queue.submit(&[command_buf]); } @@ -402,6 +334,7 @@ impl Application { let frame = swap_chain .get_next_texture() .expect("Timeout when acquiring next swap chain texture"); + example.scheduler.execute(&mut example.world); let command_buf = example.render(&frame, &device); queue.submit(&[command_buf]); } diff --git a/src/asset/mod.rs b/src/asset/mod.rs new file mode 100644 index 0000000000..d7323dae1a --- /dev/null +++ b/src/asset/mod.rs @@ -0,0 +1,73 @@ +use std::{sync::Arc, marker::PhantomData, ops::Drop}; + +pub struct Handle +{ + pub id: Arc, + marker: PhantomData, + free_indices: Arc> +} + +impl Drop for Handle { + fn drop(&mut self) { + // TODO: Maybe this should be 1 + // TODO: Is this even necessary? + if Arc::strong_count(&self.id) == 0 { + Arc::get_mut(&mut self.free_indices).unwrap().push(*self.id); + } + } +} + +pub trait Asset { + fn load(descriptor: D) -> Self; +} + +pub struct AssetStorage where T: Asset { + assets: Vec>, + free_indices: Arc>, + marker: PhantomData, +} + +impl AssetStorage where T: Asset { + pub fn new() -> AssetStorage { + AssetStorage { + assets: Vec::new(), + free_indices: Arc::new(Vec::new()), + marker: PhantomData, + } + } + + pub fn add(&mut self, asset: T) -> Handle { + match Arc::get_mut(&mut self.free_indices).unwrap().pop() { + Some(id) => { + self.assets[id as usize] = Some(asset); + Handle { + id: Arc::new(id), + marker: PhantomData, + free_indices: self.free_indices.clone() + } + }, + None => { + self.assets.push(Some(asset)); + Handle { + id: Arc::new(self.assets.len() - 1), + marker: PhantomData, + free_indices: self.free_indices.clone() + } + } + } + } + + pub fn get(&mut self, id: usize) -> Option<&mut T> { + if id >= self.assets.len() { + None + } + else { + if let Some(ref mut asset) = self.assets[id] { + Some(asset) + } else { + None + } + } + + } +} \ No newline at end of file diff --git a/src/core/mod.rs b/src/core/mod.rs index 96b0a770ad..59e3155311 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,3 +1,18 @@ -mod transform; +use legion::schedule::Stage; -pub use transform::*; \ No newline at end of file +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum ApplicationStage { + Update, + Render, +} + +impl Stage for ApplicationStage {} + +impl std::fmt::Display for ApplicationStage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ApplicationStage::Update => write!(f, "update"), + ApplicationStage::Render => write!(f, "draw"), + } + } +} diff --git a/src/core/transform.rs b/src/core/transform.rs deleted file mode 100644 index 88d3cfd88c..0000000000 --- a/src/core/transform.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::math; - -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct Transform { - pub value: math::Mat4, -} - -impl Transform { - pub fn new() -> Transform { - Transform { - value: math::identity(), - } - } -} \ No newline at end of file diff --git a/src/legion_transform/.gitignore b/src/legion_transform/.gitignore new file mode 100644 index 0000000000..af368c3d50 --- /dev/null +++ b/src/legion_transform/.gitignore @@ -0,0 +1,24 @@ +book/book +target +Cargo.lock +*.log + +# Backup files +.DS_Store +thumbs.db +*~ +*.rs.bk +*.swp + +# IDE / Editor files +*.iml +.idea +.vscode + + +#Added by cargo +# +#already existing elements are commented out + +/target +**/*.rs.bk diff --git a/src/legion_transform/.travis.yml b/src/legion_transform/.travis.yml new file mode 100644 index 0000000000..198fc4dc1f --- /dev/null +++ b/src/legion_transform/.travis.yml @@ -0,0 +1,8 @@ +language: rust +rust: + - stable + - nightly +matrix: + allow_failures: + - rust: nightly + fast_finish: true diff --git a/src/legion_transform/Cargo.toml b/src/legion_transform/Cargo.toml new file mode 100644 index 0000000000..9ca02e66b0 --- /dev/null +++ b/src/legion_transform/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "legion_transform" +version = "0.3.0" +authors = ["Alec Thilenius "] +edition = "2018" + +license = "MIT" + +[dependencies] +legion = { git = "https://github.com/TomGillen/legion.git" } +#legion = { path = "../legion" } +log = "0.4" +nalgebra = { version = "0.19.0" } +rayon = "1.2" +serde = { version = "1", features = ["derive"] } +smallvec = "0.6" +shrinkwraprs = "0.2" + +[dev-dependencies] +env_logger = "0.7" diff --git a/src/legion_transform/LICENSE b/src/legion_transform/LICENSE new file mode 100644 index 0000000000..1ab7771823 --- /dev/null +++ b/src/legion_transform/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Alec Thilenius + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/legion_transform/README.md b/src/legion_transform/README.md new file mode 100644 index 0000000000..29ea8e0ac0 --- /dev/null +++ b/src/legion_transform/README.md @@ -0,0 +1,211 @@ +# Hierarchical Legion Transform + +[![Build Status][build_img]][build_lnk] + +[build_img]: https://travis-ci.org/AThilenius/legion_transform.svg?branch=master +[build_lnk]: https://travis-ci.org/AThilenius/legion_transform + +A hierarchical space transform system, implemented using [Legion +ECS](https://github.com/TomGillen/legion). The implementation is based heavily +on the new Unity ECS Transformation layout. + +## Usage + +### TL;DR - Just show me the secret codes and incantations! + +See [examples/hierarchy.rs](examples/hierarchy.rs) + +```rust +#[allow(unused)] +fn tldr_sample() { + // Create a normal Legion World + let mut world = Universe::default().create_world(); + + // Create a system bundle (vec of systems) for LegionTransform + let transform_system_bundle = TransformSystemBundle::default().build(); + + let parent_entity = *world + .insert( + (), + vec![( + // Always needed for an Entity that has any space transform + LocalToWorld::identity(), + // The only mutable space transform a parent has is a translation. + Translation::new(100.0, 0.0, 0.0), + )], + ) + .first() + .unwrap(); + + world.insert( + (), + vec![ + ( + // Again, always need a `LocalToWorld` component for the Entity to have a custom + // space transform. + LocalToWorld::identity(), + // Here we define a Translation, Rotation and uniform Scale. + Translation::new(1.0, 2.0, 3.0), + Rotation::from_euler_angles(3.14, 0.0, 0.0), + Scale(2.0), + // Add a Parent and LocalToParent component to attach a child to a parent. + Parent(parent_entity), + LocalToParent::identity(), + ); + 4 + ], + ); +} +``` + +See [examples](/examples) for both transform and hierarchy examples. + +### Transform Overview + +The Transform and Hierarchy parts of Legion Transform are largely separate and +can thus be explained independently. We will start with space transforms, so for +now completely put hierarchies out of mind (all entities have space transforms +directly from their space to world space). + +A 3D space transform can come in many forms. The most generic of these is a +matrix 4x4 which can represent any arbitrary (linear) space transform, including +projections and sheers. These are not rarely useful for entity transformations +though, which are normally defined by things like + +- A **Translation** - movement along the X, Y or Z axis. +- A **Rotation** - 3D rotation encoded as a Unit Quaternion to prevent [gimbal + lock](https://en.wikipedia.org/wiki/Gimbal_lock). +- A **Scale** - Defined as a single floating point values, but often + **incorrectly defined as a Vector3** (which is a `NonUniformScale`) in other + engines and 3D applications. +- A **NonUniformScale** - Defined as a scale for the X, Y and Z axis + independently from each other. + +In fact, in Legion Transform, each of the above is it's own `Component` type. +These components can be added in any combination to an `Entity` with the only +exception being that `Scale` and `NonUniformScale` are mutually exclusive. + +Higher-order transformations can be built out of combinations of these +components, for example: + +- Isometry: `Translation` + `Rotation` +- Similarity: `Translation` + `Rotation` + `Scale` +- Affine: `Translation` + `Rotation` + `NonUniformScale` + +The combination of these components will be processed (when they change) by the +`LocalToWorldSystem` which will produce a correct `LocalToWorld` based on the +attached transformations. This `LocalToWorld` is a homogeneous matrix4x4 +computed as: `(Translation * (Rotation * (Scale | NonUniformScale)))`. + +Breaking apart the transform into separate components means that you need only +pay the runtime cost of computing the actual transform you need per-entity. +Further, having `LocalToWorld` be a separate component means that any static +entity (including those in static hierarchies) can be pre-baked into a +`LocalToWorld` component and the rest of the transform data need not be loaded +or stored in the final build of the game. + +In the event that the Entity is a member of a hierarchy, the `LocalToParent` +matrix will house the `(Translation * (Rotation * (Scale | NonUniformScale)))` +computation instead, and the `LocalToWorld` matrix will house the final local +space to world space transformation (after all it's parent transformations have +been computed). In other words, the `LocalToWorld` matrix is **always** the +transformation from an entities local space, directly into world space, +regardless of if the entity is a member of a hierarchy or not. + +### Why not just NonUniformScale always? + +NonUniformScale is somewhat evil. It has been used (and abused) in countless +game engines and 3D applications. A Transform with a non-uniform scale is known +as an `Affine Transform` and it cannot be applied to things like a sphere +collider in a physics engine without some serious gymnastics, loss of precision +and/or detrimental performance impacts. For this reason, you should always use a +uniform `Scale` component when possible. This component was named `Scale` over +something like "UniformScale" to imply it's status as the default scale +component and `NonUniformScale`'s status as a special case component. + +For more info on space transformations, see [nalgebra Points and +Transformations](https://www.nalgebra.org/points_and_transformations/). + +### Hierarchies + +Hierarchies in Legion Transform are defined in two parts. The first is the +_Source Of Truth_ for the hierarchy, it is always correct and always up-to-date: +the `Parent` Component. This is a component attached to children of a parent (ie +a child 'has a' `Parent`). Users can update this component directly, and because +it points toward the root of the hierarchy tree, it is impossible to form any +other type of graph apart from a tree. + +Each time the Legion Transform system bundle is run, the +`LocalToParentPropagateSystem` will also add/modify/remove a `Children` +component on any entity that has children (ie entities that have a `Parent` +component pointing to the parent entity). Because this component is only updated +during the system bundle run, **it can be out of date, incorrect or missing +altogether** after world mutations. + +It is important to note that as of today, any member of a hierarchy has it's +`LocalToWorld` matrix re-computed each system bundle run, regardless of +changes. This may someday change, but it is expected that the number of entities +in a dynamic hierarchy for a final game should be small (static hierarchies can +be pre-baked, where each entity gets a pre-baked `LocalToWorld` matrix). + +## This is no good 'tall, why didn't you do is way? + +The first implementation used Legion `Tags` to store the Parent component for +any child. This allowed for things like `O(1)` lookup of children, but was +deemed way too much fragmentation (Legion is an archetypical, chunked ECS). + +The second implementation was based on [this fine article by Michele +Caini](https://skypjack.github.io/2019-06-25-ecs-baf-part-4/) which structures +the hierarchy as explicit parent pointer, a pointer to the first (and only +first) child, and implicitly forms a linked-list of siblings. While elegant, the +actual implementation was both complicated an near-impossible to multi-thread. +For example, iterating through children entities required a global query to the +Legion `World` for each child. I decided a small amount of memory by storing a +possibly-out-of-date `SmallVec` of children was worth sacrificing on parent +entities to make code both simpler and faster (theoretically, I never tested +it). + +A lot of other options were considered as well, for example storing the entire +hierarchy out-of-band from the ECS (much like Amethyst pre-Legion does). This +has some pretty nasty drawbacks though. It makes streaming entities much harder, +it means that hierarchies need to be special-case serialized/deserialized with +initialization code being run on the newly deserialized entities. And it means +that the hierarchy does not conform to the rest of the ECS. It also means that +Legion, and all the various optimizations for querying / iterating large numbers +of entities, was going to be mostly unused and a lot of global queries would +need to be made against the `World` while syncing the `World` and out-of-band +data-structure. I felt very strongly against an out-of-band implementation +despite it being simpler to implement upfront. + +## Todo + +- [ ] Hierarchy maintenance + - [x] Remove changed `Parent` from `Children` list of the previous parent. + - [x] Add changed `Parent` to `Children` list of the new parent. + - [x] Update `PreviousParent` to the new Parent. + - [x] Handle Entities with removed `Parent` components. + - [x] Handle Entities with `Children` but without `LocalToWorld` (move their + children to non-hierarchical). + - [ ] Handle deleted Legion Entities (requires + [Legion #13](https://github.com/TomGillen/legion/issues/13)) +- [x] Local to world and parent transformation + - [x] Handle homogeneous `Matrix4` calculation for combinations of: + - [x] Translation + - [x] Rotation + - [x] Scale + - [x] NonUniformScale + - [x] Handle change detection and only recompute `LocalToWorld` when needed. + - [x] Multi-threaded updates for non-hierarchical `LocalToWorld` computation. + - [x] Recompute `LocalToParent` each run, always. +- [ ] Transform hierarchy propagation + - [x] Collect roots of the hierarchy forest + - [x] Recursively re-compute `LocalToWorld` from the `Parent`'s `LocalToWorld` + and the `LocalToParent` of each child. + - [ ] Multi-threaded updates for hierarchical `LocalToWorld` computation. + - [ ] Compute all changes and flush them to a `CommandBuffer` rather than + direct mutation of components. + +## Blockers + +- Legion has no ability to detect deleted entities or components. + [GitHub Issue #13](https://github.com/TomGillen/legion/issues/13) diff --git a/src/legion_transform/benches/local_to_world.rs b/src/legion_transform/benches/local_to_world.rs new file mode 100644 index 0000000000..77b4402aed --- /dev/null +++ b/src/legion_transform/benches/local_to_world.rs @@ -0,0 +1,46 @@ +#![feature(test)] + +extern crate test; + +use legion::prelude::*; +use legion_transform::{local_to_world_system, prelude::*}; +use test::Bencher; + +#[bench] +fn local_to_world_update_without_change(b: &mut Bencher) { + let _ = env_logger::builder().is_test(true).try_init(); + + let mut world = Universe::new().create_world(); + let system = local_to_world_system::build(&mut world); + + let ltw = LocalToWorld::identity(); + let t = Translation::new(1.0, 2.0, 3.0); + let r = Rotation::from_euler_angles(1.0, 2.0, 3.0); + let s = Scale(2.0); + let nus = NonUniformScale::new(1.0, 2.0, 3.0); + + // Add N of every combination of transform types. + let n = 1000; + let _translation = *world.insert((), vec![(ltw, t); n]).first().unwrap(); + let _rotation = *world.insert((), vec![(ltw, r); n]).first().unwrap(); + let _scale = *world.insert((), vec![(ltw, s); n]).first().unwrap(); + let _non_uniform_scale = *world.insert((), vec![(ltw, nus); n]).first().unwrap(); + let _translation_and_rotation = *world.insert((), vec![(ltw, t, r); n]).first().unwrap(); + let _translation_and_scale = *world.insert((), vec![(ltw, t, s); n]).first().unwrap(); + let _translation_and_nus = *world.insert((), vec![(ltw, t, nus); n]).first().unwrap(); + let _rotation_scale = *world.insert((), vec![(ltw, r, s); n]).first().unwrap(); + let _rotation_nus = *world.insert((), vec![(ltw, r, nus); n]).first().unwrap(); + let _translation_rotation_scale = *world.insert((), vec![(ltw, t, r, s); n]).first().unwrap(); + let _translation_rotation_nus = *world.insert((), vec![(ltw, t, r, nus); n]).first().unwrap(); + + // Run the system once outside the test (which should compute everything and it shouldn't be + // touched again). + system.run(&mut world); + system.command_buffer_mut().write(&mut world); + + // Then time the already-computed updates. + b.iter(|| { + system.run(&mut world); + system.command_buffer_mut().write(&mut world); + }); +} diff --git a/src/legion_transform/examples/hierarchy.rs b/src/legion_transform/examples/hierarchy.rs new file mode 100644 index 0000000000..acf7ae15ca --- /dev/null +++ b/src/legion_transform/examples/hierarchy.rs @@ -0,0 +1,155 @@ +extern crate legion; +extern crate legion_transform; + +use legion::prelude::*; +use legion_transform::prelude::*; + +#[allow(unused)] +fn tldr_sample() { + // Create a normal Legion World + let mut world = Universe::default().create_world(); + + // Create a system bundle (vec of systems) for LegionTransform + let transform_system_bundle = transform_system_bundle::build(&mut world); + + let parent_entity = *world + .insert( + (), + vec![( + // Always needed for an Entity that has any space transform + LocalToWorld::identity(), + // The only mutable space transform a parent has is a translation. + Translation::new(100.0, 0.0, 0.0), + )], + ) + .first() + .unwrap(); + + world.insert( + (), + vec![ + ( + // Again, always need a `LocalToWorld` component for the Entity to have a custom + // space transform. + LocalToWorld::identity(), + // Here we define a Translation, Rotation and uniform Scale. + Translation::new(1.0, 2.0, 3.0), + Rotation::from_euler_angles(3.14, 0.0, 0.0), + Scale(2.0), + // Add a Parent and LocalToParent component to attach a child to a parent. + Parent(parent_entity), + LocalToParent::identity(), + ); + 4 + ], + ); +} + +fn main() { + // Create a normal Legion World + let mut world = Universe::default().create_world(); + + // Create a system bundle (vec of systems) for LegionTransform + let transform_system_bundle = transform_system_bundle::build(&mut world); + + // See `./types_of_transforms.rs` for an explanation of space-transform types. + let parent_entity = *world + .insert( + (), + vec![(LocalToWorld::identity(), Translation::new(100.0, 0.0, 0.0))], + ) + .first() + .unwrap(); + + let four_children: Vec<_> = world + .insert( + (), + vec![ + ( + LocalToWorld::identity(), + Translation::new(1.0, 2.0, 3.0), + Rotation::from_euler_angles(3.14, 0.0, 0.0), + Scale(2.0), + // Add a Parent and LocalToParent component to attach a child to a parent. + Parent(parent_entity), + LocalToParent::identity(), + ); + 4 + ], + ) + .iter() + .cloned() + .collect(); + + // At this point the parent does NOT have a `Children` component attached to it. The `Children` + // component is updated by the transform system bundle and thus can be out of date (or + // non-existent for newly added members). By this logic, the `Parent` components should be + // considered the always-correct 'source of truth' for any hierarchy. + for system in transform_system_bundle.iter() { + system.run(&mut world); + system.command_buffer_mut().write(&mut world); + } + + // At this point all parents with children have a correct `Children` component. + let parents_children = world + .get_component::(parent_entity) + .unwrap() + .0 + .clone(); + + println!("Parent {}", parent_entity); + for child in parents_children.iter() { + println!(" -> Has child: {}", child); + } + + // Each child will also have a `LocalToParent` component attached to it, which is a + // space-transform from its local space to that of its parent. + for child in four_children.iter() { + println!("The child {}", child); + println!( + " -> Has a LocalToParent matrix: {}", + *world.get_component::(*child).unwrap() + ); + println!( + " -> Has a LocalToWorld matrix: {}", + *world.get_component::(*child).unwrap() + ); + } + + // Re-parent the second child to be a grandchild of the first. + world.add_component(four_children[1], Parent(four_children[0])); + + // Re-running the system will cleanup and fix all `Children` components. + for system in transform_system_bundle.iter() { + system.run(&world); + system.command_buffer_mut().write(&mut world); + } + + println!("After the second child was re-parented as a grandchild of the first child..."); + + for child in world + .get_component::(parent_entity) + .unwrap() + .0 + .iter() + { + println!("Parent {} has child: {}", parent_entity, child); + } + + for grandchild in world + .get_component::(four_children[0]) + .unwrap() + .0 + .iter() + { + println!("Child {} has grandchild: {}", four_children[0], grandchild); + } + + println!("Grandchild: {}", four_children[1]); + println!( + " -> Has a LocalToWorld matrix: {}", + *world + .get_component::(four_children[1]) + .unwrap() + ); +} diff --git a/src/legion_transform/examples/types_of_transforms.rs b/src/legion_transform/examples/types_of_transforms.rs new file mode 100644 index 0000000000..c87b1c4df0 --- /dev/null +++ b/src/legion_transform/examples/types_of_transforms.rs @@ -0,0 +1,94 @@ +extern crate legion; +extern crate legion_transform; + +use legion::prelude::*; +use legion_transform::prelude::*; + +fn main() { + // Create a normal Legion World + let mut world = Universe::default().create_world(); + + // Create a system bundle (vec of systems) for LegionTransform + let transform_system_bundle = transform_system_bundle::build(&mut world); + + // A user-defined space transform is split into 4 different components: [`Translation`, + // `Rotation`, `Scale`, `NonUniformScale`]. Any combination of these components can be added to + // an entity to transform it's space (exception: `Scale` and `NonUniformScale` are mutually + // exclusive). + + // Note that all entities need an explicitly added `LocalToWorld` component to be considered for + // processing during transform system passes. + + // Add an entity with just a Translation + // See: https://www.nalgebra.org/rustdoc/nalgebra/geometry/struct.Translation.html + // API on Translation, as a LegionTransform `Translation` is just a nalgebra `Translation3`. + world.insert( + (), + vec![(LocalToWorld::identity(), Translation::new(1.0, 2.0, 3.0))], + ); + + // Add an entity with just a Rotation. + // See: https://www.nalgebra.org/rustdoc/nalgebra/geometry/type.UnitQuaternion.html for the full + // API on Rotation, as a LegionTransform `Rotation` is just a nalgebra `UnityQuaternion`. + world.insert( + (), + vec![( + LocalToWorld::identity(), + Rotation::from_euler_angles(3.14, 0.0, 0.0), + )], + ); + + // Add an entity with just a uniform Scale (the default and strongly-preferred scale component). + // This is simply a `f32` wrapper. + world.insert((), vec![(LocalToWorld::identity(), Scale(2.0))]); + + // Add an entity with just a NonUniformScale (This should be avoided unless you **really** need + // non-uniform scaling as it breaks things like physics colliders. + // See: https://docs.rs/nalgebra/0.10.1/nalgebra/struct.Vector3.html for the full API on + // NonUniformScale, as a LegionTransform `NonUniformScale` is simply a nalgebra `Vector3`, + // although note that it is wrapped in a tuple-struct. + world.insert( + (), + vec![( + LocalToWorld::identity(), + NonUniformScale::new(1.0, 2.0, 1.0), + )], + ); + + // Add an entity with a combination of Translation and Rotation + world.insert( + (), + vec![( + LocalToWorld::identity(), + Translation::new(1.0, 2.0, 3.0), + Rotation::from_euler_angles(3.14, 0.0, 0.0), + )], + ); + + // Add an entity with a combination of Translation and Rotation and uniform Scale. + world.insert( + (), + vec![( + LocalToWorld::identity(), + Translation::new(1.0, 2.0, 3.0), + Rotation::from_euler_angles(3.14, 0.0, 0.0), + Scale(2.0), + )], + ); + + // Run the system bundle (this API will likely change). + for system in transform_system_bundle.iter() { + system.run(&world); + system.command_buffer_mut().write(&mut world); + } + + // At this point all `LocalToWorld` components have correct values in them. Running the system + // again will result in a short-circuit as only changed components are considered for update. + let mut query = >::query(); + for (entity, local_to_world) in query.iter_entities(&mut world) { + println!( + "Entity {} and a LocalToWorld matrix: {}", + entity, *local_to_world + ); + } +} diff --git a/src/legion_transform/src/components/children.rs b/src/legion_transform/src/components/children.rs new file mode 100644 index 0000000000..731253a2aa --- /dev/null +++ b/src/legion_transform/src/components/children.rs @@ -0,0 +1,13 @@ +use crate::ecs::prelude::*; +use shrinkwraprs::Shrinkwrap; +use smallvec::SmallVec; + +#[derive(Shrinkwrap, Default, Clone)] +#[shrinkwrap(mutable)] +pub struct Children(pub SmallVec<[Entity; 8]>); + +impl Children { + pub fn with(entity: &[Entity]) -> Self { + Self(SmallVec::from_slice(entity)) + } +} diff --git a/src/legion_transform/src/components/local_to_parent.rs b/src/legion_transform/src/components/local_to_parent.rs new file mode 100644 index 0000000000..67b82afdc3 --- /dev/null +++ b/src/legion_transform/src/components/local_to_parent.rs @@ -0,0 +1,25 @@ +use crate::math::Matrix4; +use shrinkwraprs::Shrinkwrap; +use std::fmt; + +#[derive(Shrinkwrap, Debug, PartialEq, Clone, Copy)] +#[shrinkwrap(mutable)] +pub struct LocalToParent(pub Matrix4); + +impl LocalToParent { + pub fn identity() -> Self { + Self(Matrix4::identity()) + } +} + +impl Default for LocalToParent { + fn default() -> Self { + Self::identity() + } +} + +impl fmt::Display for LocalToParent { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/src/legion_transform/src/components/local_to_world.rs b/src/legion_transform/src/components/local_to_world.rs new file mode 100644 index 0000000000..d8075b9934 --- /dev/null +++ b/src/legion_transform/src/components/local_to_world.rs @@ -0,0 +1,26 @@ +use crate::math::Matrix4; +use shrinkwraprs::Shrinkwrap; +use std::fmt; + +#[derive(Shrinkwrap, Debug, PartialEq, Clone, Copy)] +#[shrinkwrap(mutable)] +pub struct LocalToWorld(pub Matrix4); + +impl LocalToWorld { + #[inline(always)] + pub fn identity() -> Self { + Self(Matrix4::identity()) + } +} + +impl Default for LocalToWorld { + fn default() -> Self { + Self::identity() + } +} + +impl fmt::Display for LocalToWorld { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/src/legion_transform/src/components/mod.rs b/src/legion_transform/src/components/mod.rs new file mode 100644 index 0000000000..8e93128038 --- /dev/null +++ b/src/legion_transform/src/components/mod.rs @@ -0,0 +1,17 @@ +mod children; +mod local_to_parent; +mod local_to_world; +mod non_uniform_scale; +mod parent; +mod rotation; +mod scale; +mod translation; + +pub use children::Children; +pub use local_to_parent::*; +pub use local_to_world::*; +pub use non_uniform_scale::*; +pub use parent::{Parent, PreviousParent}; +pub use rotation::*; +pub use scale::*; +pub use translation::*; diff --git a/src/legion_transform/src/components/non_uniform_scale.rs b/src/legion_transform/src/components/non_uniform_scale.rs new file mode 100644 index 0000000000..32ebbaa096 --- /dev/null +++ b/src/legion_transform/src/components/non_uniform_scale.rs @@ -0,0 +1,41 @@ +use crate::math::Vector3; +use shrinkwraprs::Shrinkwrap; +use std::fmt; + +#[derive(Shrinkwrap, Debug, PartialEq, Clone, Copy)] +#[shrinkwrap(mutable)] +pub struct NonUniformScale(pub Vector3); + +impl NonUniformScale { + pub fn new(x: f32, y: f32, z: f32) -> Self { + Self(Vector3::new(x, y, z)) + } +} + +impl From> for NonUniformScale { + fn from(scale: Vector3) -> Self { + Self(scale) + } +} + +impl From<&Vector3> for NonUniformScale { + fn from(scale: &Vector3) -> Self { + Self(*scale) + } +} + +impl From<&mut Vector3> for NonUniformScale { + fn from(scale: &mut Vector3) -> Self { + Self(*scale) + } +} + +impl fmt::Display for NonUniformScale { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "NonUniformScale({}, {}, {})", + self.0.x, self.0.y, self.0.z + ) + } +} diff --git a/src/legion_transform/src/components/parent.rs b/src/legion_transform/src/components/parent.rs new file mode 100644 index 0000000000..7720b76aba --- /dev/null +++ b/src/legion_transform/src/components/parent.rs @@ -0,0 +1,10 @@ +use crate::ecs::prelude::*; +use shrinkwraprs::Shrinkwrap; + +#[derive(Shrinkwrap, Debug, Copy, Clone, Eq, PartialEq)] +#[shrinkwrap(mutable)] +pub struct Parent(pub Entity); + +#[derive(Shrinkwrap, Debug, Copy, Clone, Eq, PartialEq)] +#[shrinkwrap(mutable)] +pub struct PreviousParent(pub Option); diff --git a/src/legion_transform/src/components/rotation.rs b/src/legion_transform/src/components/rotation.rs new file mode 100644 index 0000000000..82e4b0b272 --- /dev/null +++ b/src/legion_transform/src/components/rotation.rs @@ -0,0 +1,29 @@ +use crate::math::UnitQuaternion; +use shrinkwraprs::Shrinkwrap; + +#[derive(Shrinkwrap, Debug, PartialEq, Clone, Copy)] +#[shrinkwrap(mutable)] +pub struct Rotation(pub UnitQuaternion); +impl Rotation { + #[inline(always)] + pub fn identity() -> Self { + Self(UnitQuaternion::identity()) + } + + #[inline(always)] + pub fn from_euler_angles(roll: f32, pitch: f32, yaw: f32) -> Self { + Self(UnitQuaternion::from_euler_angles(roll, pitch, yaw)) + } +} + +impl Default for Rotation { + fn default() -> Self { + Self::identity() + } +} + +impl From> for Rotation { + fn from(rotation: UnitQuaternion) -> Self { + Self(rotation) + } +} diff --git a/src/legion_transform/src/components/scale.rs b/src/legion_transform/src/components/scale.rs new file mode 100644 index 0000000000..16d8292f20 --- /dev/null +++ b/src/legion_transform/src/components/scale.rs @@ -0,0 +1,32 @@ +use shrinkwraprs::Shrinkwrap; +use std::fmt; + +#[derive(Shrinkwrap, Debug, PartialEq, Clone, Copy)] +#[shrinkwrap(mutable)] +pub struct Scale(pub f32); + +impl From for Scale { + #[inline(always)] + fn from(scale: f32) -> Self { + Self(scale) + } +} + +impl Scale { + #[inline(always)] + pub fn identity() -> Self { + Scale(1.0) + } +} + +impl Default for Scale { + fn default() -> Self { + Self::identity() + } +} + +impl fmt::Display for Scale { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Scale({})", self.0) + } +} diff --git a/src/legion_transform/src/components/translation.rs b/src/legion_transform/src/components/translation.rs new file mode 100644 index 0000000000..8760755369 --- /dev/null +++ b/src/legion_transform/src/components/translation.rs @@ -0,0 +1,36 @@ +use crate::math::{Translation3, Vector3}; +use shrinkwraprs::Shrinkwrap; + +#[derive(Shrinkwrap, Debug, PartialEq, Clone, Copy)] +#[shrinkwrap(mutable)] +pub struct Translation(pub Translation3); + +impl Translation { + #[inline(always)] + pub fn identity() -> Self { + Self(Translation3::identity()) + } + + #[inline(always)] + pub fn new(x: f32, y: f32, z: f32) -> Self { + Self(Translation3::new(x, y, z)) + } +} + +impl Default for Translation { + fn default() -> Self { + Self::identity() + } +} + +impl From> for Translation { + fn from(translation: Vector3) -> Self { + Self(Translation3::from(translation)) + } +} + +impl From> for Translation { + fn from(translation: Translation3) -> Self { + Self(translation) + } +} diff --git a/src/legion_transform/src/hierarchy_maintenance_system.rs b/src/legion_transform/src/hierarchy_maintenance_system.rs new file mode 100644 index 0000000000..7db9162699 --- /dev/null +++ b/src/legion_transform/src/hierarchy_maintenance_system.rs @@ -0,0 +1,234 @@ +#![allow(dead_code)] +use crate::{components::*, ecs::prelude::*}; +use smallvec::SmallVec; +use std::collections::HashMap; + +pub fn build(_: &mut World) -> Vec> { + let missing_previous_parent_system = SystemBuilder::<()>::new("MissingPreviousParentSystem") + // Entities with missing `PreviousParent` + .with_query(>::query().filter( + component::() + & component::() + & !component::(), + )) + .build(move |commands, world, _resource, query| { + // Add missing `PreviousParent` components + for (entity, _parent) in query.iter_entities(world) { + log::trace!("Adding missing PreviousParent to {}", entity); + commands.add_component(entity, PreviousParent(None)); + } + }); + + let parent_update_system = SystemBuilder::<()>::new("ParentUpdateSystem") + // Entities with a removed `Parent` + .with_query(>::query().filter(!component::())) + // Entities with a changed `Parent` + .with_query(<(Read, Write)>::query().filter( + component::() & component::() & changed::(), + )) + // Deleted Parents (ie Entities with `Children` and without a `LocalToWorld`). + .with_query(>::query().filter(!component::())) + .write_component::() + .build(move |commands, world, _resource, queries| { + // Entities with a missing `Parent` (ie. ones that have a `PreviousParent`), remove + // them from the `Children` of the `PreviousParent`. + for (entity, previous_parent) in queries.0.iter_entities(world) { + log::trace!("Parent was removed from {}", entity); + if let Some(previous_parent_entity) = previous_parent.0 { + if let Some(mut previous_parent_children) = + world.get_component_mut::(previous_parent_entity) + { + log::trace!(" > Removing {} from it's prev parent's children", entity); + previous_parent_children.0.retain(|e| *e != entity); + } + } + } + + // Tracks all newly created `Children` Components this frame. + let mut children_additions = + HashMap::>::with_capacity(16); + + // Entities with a changed Parent (that also have a PreviousParent, even if None) + for (entity, (parent, mut previous_parent)) in queries.1.iter_entities(world) { + log::trace!("Parent changed for {}", entity); + + // If the `PreviousParent` is not None. + if let Some(previous_parent_entity) = previous_parent.0 { + // New and previous point to the same Entity, carry on, nothing to see here. + if previous_parent_entity == parent.0 { + log::trace!(" > But the previous parent is the same, ignoring..."); + continue; + } + + // Remove from `PreviousParent.Children`. + if let Some(mut previous_parent_children) = + world.get_component_mut::(previous_parent_entity) + { + log::trace!(" > Removing {} from prev parent's children", entity); + (*previous_parent_children).0.retain(|e| *e != entity); + } + } + + // Set `PreviousParent = Parent`. + *previous_parent = PreviousParent(Some(parent.0)); + + // Add to the parent's `Children` (either the real component, or + // `children_additions`). + log::trace!("Adding {} to it's new parent {}", entity, parent.0); + if let Some(mut new_parent_children) = world.get_component_mut::(parent.0) + { + // This is the parent + log::trace!( + " > The new parent {} already has a `Children`, adding to it.", + parent.0 + ); + (*new_parent_children).0.push(entity); + } else { + // The parent doesn't have a children entity, lets add it + log::trace!( + "The new parent {} doesn't yet have `Children` component.", + parent.0 + ); + children_additions + .entry(parent.0) + .or_insert_with(Default::default) + .push(entity); + } + } + + // Deleted `Parents` (ie. Entities with a `Children` but no `LocalToWorld`). + for (entity, children) in queries.2.iter_entities(world) { + log::trace!("The entity {} doesn't have a LocalToWorld", entity); + if children_additions.remove(&entity).is_none() { + log::trace!(" > It needs to be remove from the ECS."); + for child_entity in children.0.iter() { + commands.remove_component::(*child_entity); + commands.remove_component::(*child_entity); + commands.remove_component::(*child_entity); + } + commands.remove_component::(entity); + } else { + log::trace!(" > It was a new addition, removing it from additions map"); + } + } + + // Flush the `children_additions` to the command buffer. It is stored separate to + // collect multiple new children that point to the same parent into the same + // SmallVec, and to prevent redundant add+remove operations. + children_additions.iter().for_each(|(k, v)| { + log::trace!("Flushing: Entity {} adding `Children` component {:?}", k, v); + commands.add_component(*k, Children::with(v)); + }); + }); + + vec![missing_previous_parent_system, parent_update_system] +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn correct_children() { + let _ = env_logger::builder().is_test(true).try_init(); + + let mut world = Universe::new().create_world(); + + let systems = build(&mut world); + + // Add parent entities + let parent = *world + .insert( + (), + vec![(Translation::identity(), LocalToWorld::identity())], + ) + .first() + .unwrap(); + let children = world.insert( + (), + vec![ + ( + Translation::identity(), + LocalToParent::identity(), + LocalToWorld::identity(), + ), + ( + Translation::identity(), + LocalToParent::identity(), + LocalToWorld::identity(), + ), + ], + ); + let (e1, e2) = (children[0], children[1]); + + // Parent `e1` and `e2` to `parent`. + world.add_component(e1, Parent(parent)); + world.add_component(e2, Parent(parent)); + + for system in systems.iter() { + system.run(&mut world); + system.command_buffer_mut().write(&mut world); + } + + assert_eq!( + world + .get_component::(parent) + .unwrap() + .0 + .iter() + .cloned() + .collect::>(), + vec![e1, e2] + ); + + // Parent `e1` to `e2`. + (*world.get_component_mut::(e1).unwrap()).0 = e2; + + // Run the system on it + for system in systems.iter() { + system.run(&mut world); + system.command_buffer_mut().write(&mut world); + } + + assert_eq!( + world + .get_component::(parent) + .unwrap() + .0 + .iter() + .cloned() + .collect::>(), + vec![e2] + ); + + assert_eq!( + world + .get_component::(e2) + .unwrap() + .0 + .iter() + .cloned() + .collect::>(), + vec![e1] + ); + + world.delete(e1); + + // Run the system on it + for system in systems.iter() { + system.run(&mut world); + system.command_buffer_mut().write(&mut world); + } + + assert_eq!( + world + .get_component::(parent) + .unwrap() + .0 + .iter() + .cloned() + .collect::>(), + vec![e2] + ); + } +} diff --git a/src/legion_transform/src/lib.rs b/src/legion_transform/src/lib.rs new file mode 100644 index 0000000000..d92766e464 --- /dev/null +++ b/src/legion_transform/src/lib.rs @@ -0,0 +1,18 @@ +pub use legion as ecs; +pub use nalgebra as math; + +pub mod components; +pub mod hierarchy_maintenance_system; +pub mod local_to_parent_system; +pub mod local_to_world_propagate_system; +pub mod local_to_world_system; +pub mod transform_system_bundle; + +pub mod prelude { + pub use crate::components::*; + pub use crate::hierarchy_maintenance_system; + pub use crate::local_to_parent_system; + pub use crate::local_to_world_propagate_system; + pub use crate::local_to_world_system; + pub use crate::transform_system_bundle; +} diff --git a/src/legion_transform/src/local_to_parent_system.rs b/src/legion_transform/src/local_to_parent_system.rs new file mode 100644 index 0000000000..74261c1da6 --- /dev/null +++ b/src/legion_transform/src/local_to_parent_system.rs @@ -0,0 +1,323 @@ +#![allow(dead_code)] +use crate::{components::*, ecs::prelude::*, math::Matrix4}; + +pub fn build(_: &mut World) -> Box { + SystemBuilder::<()>::new("LocalToParentUpdateSystem") + // Translation + .with_query(<(Write, Read)>::query().filter( + !component::() + & !component::() + & !component::() + & (changed::()), + )) + // Rotation + .with_query(<(Write, Read)>::query().filter( + !component::() + & !component::() + & !component::() + & (changed::()), + )) + // Scale + .with_query(<(Write, Read)>::query().filter( + !component::() + & !component::() + & !component::() + & (changed::()), + )) + // NonUniformScale + .with_query( + <(Write, Read)>::query().filter( + !component::() + & !component::() + & !component::() + & (changed::()), + ), + ) + // Translation + Rotation + .with_query( + <(Write, Read, Read)>::query().filter( + !component::() + & !component::() + & (changed::() | changed::()), + ), + ) + // Translation + Scale + .with_query( + <(Write, Read, Read)>::query().filter( + !component::() + & !component::() + & (changed::() | changed::()), + ), + ) + // Translation + NonUniformScale + .with_query( + <( + Write, + Read, + Read, + )>::query() + .filter( + !component::() + & !component::() + & (changed::() | changed::()), + ), + ) + // Rotation + Scale + .with_query( + <(Write, Read, Read)>::query().filter( + !component::() + & !component::() + & (changed::() | changed::()), + ), + ) + // Rotation + NonUniformScale + .with_query( + <(Write, Read, Read)>::query().filter( + !component::() + & !component::() + & (changed::() | changed::()), + ), + ) + // Translation + Rotation + Scale + .with_query( + <( + Write, + Read, + Read, + Read, + )>::query() + .filter( + !component::() + & (changed::() | changed::() | changed::()), + ), + ) + // Translation + Rotation + NonUniformScale + .with_query( + <( + Write, + Read, + Read, + Read, + )>::query() + .filter( + !component::() + & (changed::() + | changed::() + | changed::()), + ), + ) + // Just to issue warnings: Scale + NonUniformScale + .with_query(<(Read, Read, Read)>::query()) + .build(move |_commands, world, _, queries| { + let (a, b, c, d, e, f, g, h, i, j, k, l) = queries; + rayon::scope(|s| { + s.spawn(|_| unsafe { + // Translation + a.for_each_unchecked(world, |(mut ltw, translation)| { + *ltw = LocalToParent(translation.to_homogeneous()); + }); + }); + s.spawn(|_| unsafe { + // Rotation + b.for_each_unchecked(world, |(mut ltw, rotation)| { + *ltw = LocalToParent(rotation.to_homogeneous()); + }); + }); + s.spawn(|_| unsafe { + // Scale + c.for_each_unchecked(world, |(mut ltw, scale)| { + *ltw = LocalToParent(Matrix4::new_scaling(scale.0)); + }); + }); + s.spawn(|_| unsafe { + // NonUniformScale + d.for_each_unchecked(world, |(mut ltw, non_uniform_scale)| { + *ltw = LocalToParent(Matrix4::new_nonuniform_scaling(&non_uniform_scale.0)); + }); + + // Translation + Rotation + e.for_each_unchecked(world, |(mut ltw, translation, rotation)| { + *ltw = LocalToParent( + rotation + .to_homogeneous() + .append_translation(&translation.vector), + ); + }); + }); + s.spawn(|_| unsafe { + // Translation + Scale + f.for_each_unchecked(world, |(mut ltw, translation, scale)| { + *ltw = LocalToParent(translation.to_homogeneous().prepend_scaling(scale.0)); + }); + + // Translation + NonUniformScale + g.for_each_unchecked(world, |(mut ltw, translation, non_uniform_scale)| { + *ltw = LocalToParent( + translation + .to_homogeneous() + .prepend_nonuniform_scaling(&non_uniform_scale.0), + ); + }); + }); + s.spawn(|_| unsafe { + // Rotation + Scale + h.for_each_unchecked(world, |(mut ltw, rotation, scale)| { + *ltw = LocalToParent(rotation.to_homogeneous().prepend_scaling(scale.0)); + }); + }); + s.spawn(|_| unsafe { + // Rotation + NonUniformScale + i.for_each_unchecked(world, |(mut ltw, rotation, non_uniform_scale)| { + *ltw = LocalToParent( + rotation + .to_homogeneous() + .prepend_nonuniform_scaling(&non_uniform_scale.0), + ); + }); + }); + s.spawn(|_| unsafe { + // Translation + Rotation + Scale + j.for_each_unchecked(world, |(mut ltw, translation, rotation, scale)| { + *ltw = LocalToParent( + rotation + .to_homogeneous() + .append_translation(&translation.vector) + .prepend_scaling(scale.0), + ); + }); + }); + s.spawn(|_| unsafe { + // Translation + Rotation + NonUniformScale + k.for_each_unchecked( + world, + |(mut ltw, translation, rotation, non_uniform_scale)| { + *ltw = LocalToParent( + rotation + .to_homogeneous() + .append_translation(&translation.vector) + .prepend_nonuniform_scaling(&non_uniform_scale.0), + ); + }, + ); + }); + }); + // Just to issue warnings: Scale + NonUniformScale + l.iter_entities(world) + .for_each(|(entity, (mut _ltw, _scale, _non_uniform_scale))| { + log::warn!( + "Entity {:?} has both a Scale and NonUniformScale component.", + entity + ); + }); + }) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn correct_parent_transformation() { + let _ = env_logger::builder().is_test(true).try_init(); + + let mut world = Universe::new().create_world(); + let system = build(&mut world); + + let ltw = LocalToParent::identity(); + let t = Translation::new(1.0, 2.0, 3.0); + let r = Rotation::from_euler_angles(1.0, 2.0, 3.0); + let s = Scale(2.0); + let nus = NonUniformScale::new(1.0, 2.0, 3.0); + + // Add every combination of transform types. + let translation = *world.insert((), vec![(ltw, t)]).first().unwrap(); + let rotation = *world.insert((), vec![(ltw, r)]).first().unwrap(); + let scale = *world.insert((), vec![(ltw, s)]).first().unwrap(); + let non_uniform_scale = *world.insert((), vec![(ltw, nus)]).first().unwrap(); + let translation_and_rotation = *world.insert((), vec![(ltw, t, r)]).first().unwrap(); + let translation_and_scale = *world.insert((), vec![(ltw, t, s)]).first().unwrap(); + let translation_and_nus = *world.insert((), vec![(ltw, t, nus)]).first().unwrap(); + let rotation_scale = *world.insert((), vec![(ltw, r, s)]).first().unwrap(); + let rotation_nus = *world.insert((), vec![(ltw, r, nus)]).first().unwrap(); + let translation_rotation_scale = *world.insert((), vec![(ltw, t, r, s)]).first().unwrap(); + let translation_rotation_nus = *world.insert((), vec![(ltw, t, r, nus)]).first().unwrap(); + + // Run the system + system.run(&mut world); + system.command_buffer_mut().write(&mut world); + + // Verify that each was transformed correctly. + assert_eq!( + world.get_component::(translation).unwrap().0, + t.to_homogeneous() + ); + assert_eq!( + world.get_component::(rotation).unwrap().0, + r.to_homogeneous() + ); + assert_eq!( + world.get_component::(scale).unwrap().0, + Matrix4::new_scaling(s.0), + ); + assert_eq!( + world + .get_component::(non_uniform_scale) + .unwrap() + .0, + Matrix4::new_nonuniform_scaling(&nus.0), + ); + assert_eq!( + world + .get_component::(translation_and_rotation) + .unwrap() + .0, + r.to_homogeneous().append_translation(&t.vector), + ); + assert_eq!( + world + .get_component::(translation_and_scale) + .unwrap() + .0, + t.to_homogeneous().prepend_scaling(s.0), + ); + assert_eq!( + world + .get_component::(translation_and_nus) + .unwrap() + .0, + t.to_homogeneous().prepend_nonuniform_scaling(&nus.0), + ); + assert_eq!( + world + .get_component::(rotation_scale) + .unwrap() + .0, + r.to_homogeneous().prepend_scaling(s.0) + ); + assert_eq!( + world + .get_component::(rotation_nus) + .unwrap() + .0, + r.to_homogeneous().prepend_nonuniform_scaling(&nus.0) + ); + assert_eq!( + world + .get_component::(translation_rotation_scale) + .unwrap() + .0, + r.to_homogeneous() + .append_translation(&t.vector) + .prepend_scaling(s.0) + ); + assert_eq!( + world + .get_component::(translation_rotation_nus) + .unwrap() + .0, + r.to_homogeneous() + .append_translation(&t.vector) + .prepend_nonuniform_scaling(&nus.0) + ); + } +} diff --git a/src/legion_transform/src/local_to_world_propagate_system.rs b/src/legion_transform/src/local_to_world_propagate_system.rs new file mode 100644 index 0000000000..767daa1f82 --- /dev/null +++ b/src/legion_transform/src/local_to_world_propagate_system.rs @@ -0,0 +1,133 @@ +#![allow(dead_code)] +use crate::{ + components::*, + ecs::{prelude::*, system::PreparedWorld}, +}; + +pub fn build(_: &mut World) -> Box { + SystemBuilder::<()>::new("LocalToWorldPropagateSystem") + // Entities with a `Children` and `LocalToWorld` but NOT a `Parent` (ie those that are + // roots of a hierarchy). + .with_query(<(Read, Read)>::query().filter(!component::())) + .read_component::() + .read_component::() + .build(move |commands, world, _resource, query| { + for (children, local_to_world) in query.iter(world) { + for child in children.0.iter() { + propagate_recursive(*local_to_world, world, *child, commands); + } + } + }) +} + +fn propagate_recursive( + parent_local_to_world: LocalToWorld, + world: &mut PreparedWorld, + entity: Entity, + commands: &mut CommandBuffer, +) { + log::trace!("Updating LocalToWorld for {}", entity); + let local_to_parent = { + if let Some(local_to_parent) = world.get_component::(entity) { + *local_to_parent + } else { + log::warn!( + "Entity {} is a child in the hierarchy but does not have a LocalToParent", + entity + ); + return; + } + }; + + let new_local_to_world = LocalToWorld(parent_local_to_world.0 * local_to_parent.0); + commands.add_component(entity, new_local_to_world); + + // Collect children + let children = world + .get_component::(entity) + .map(|e| e.0.iter().cloned().collect::>()) + .unwrap_or_default(); + + for child in children { + propagate_recursive(new_local_to_world, world, child, commands); + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + hierarchy_maintenance_system, local_to_parent_system, local_to_world_propagate_system, + local_to_world_system, + }; + + #[test] + fn did_propagate() { + let _ = env_logger::builder().is_test(true).try_init(); + + let mut world = Universe::new().create_world(); + + let hierarchy_maintenance_systems = hierarchy_maintenance_system::build(&mut world); + let local_to_parent_system = local_to_parent_system::build(&mut world); + let local_to_world_system = local_to_world_system::build(&mut world); + let local_to_world_propagate_system = local_to_world_propagate_system::build(&mut world); + + // Root entity + let parent = *world + .insert( + (), + vec![(Translation::new(1.0, 0.0, 0.0), LocalToWorld::identity())], + ) + .first() + .unwrap(); + + let children = world.insert( + (), + vec![ + ( + Translation::new(0.0, 2.0, 0.0), + LocalToParent::identity(), + LocalToWorld::identity(), + ), + ( + Translation::new(0.0, 0.0, 3.0), + LocalToParent::identity(), + LocalToWorld::identity(), + ), + ], + ); + let (e1, e2) = (children[0], children[1]); + + // Parent `e1` and `e2` to `parent`. + world.add_component(e1, Parent(parent)); + world.add_component(e2, Parent(parent)); + + // Run the needed systems on it. + for system in hierarchy_maintenance_systems.iter() { + system.run(&mut world); + system.command_buffer_mut().write(&mut world); + } + local_to_parent_system.run(&mut world); + local_to_parent_system + .command_buffer_mut() + .write(&mut world); + local_to_world_system.run(&mut world); + local_to_world_system.command_buffer_mut().write(&mut world); + local_to_world_propagate_system.run(&mut world); + local_to_world_propagate_system + .command_buffer_mut() + .write(&mut world); + + assert_eq!( + world.get_component::(e1).unwrap().0, + Translation::new(1.0, 0.0, 0.0).to_homogeneous() + * Translation::new(0.0, 2.0, 0.0).to_homogeneous() + ); + + assert_eq!( + world.get_component::(e2).unwrap().0, + Translation::new(1.0, 0.0, 0.0).to_homogeneous() + * Translation::new(0.0, 0.0, 3.0).to_homogeneous() + ); + } +} diff --git a/src/legion_transform/src/local_to_world_system.rs b/src/legion_transform/src/local_to_world_system.rs new file mode 100644 index 0000000000..4ac3598a02 --- /dev/null +++ b/src/legion_transform/src/local_to_world_system.rs @@ -0,0 +1,340 @@ +#![allow(dead_code)] +use crate::{components::*, ecs::prelude::*, math::Matrix4}; + +pub fn build(_: &mut World) -> Box { + SystemBuilder::<()>::new("LocalToWorldUpdateSystem") + // Translation + .with_query(<(Write, Read)>::query().filter( + !component::() + & !component::() + & !component::() + & !component::() + & (changed::()), + )) + // Rotation + .with_query(<(Write, Read)>::query().filter( + !component::() + & !component::() + & !component::() + & !component::() + & (changed::()), + )) + // Scale + .with_query(<(Write, Read)>::query().filter( + !component::() + & !component::() + & !component::() + & !component::() + & (changed::()), + )) + // NonUniformScale + .with_query( + <(Write, Read)>::query().filter( + !component::() + & !component::() + & !component::() + & !component::() + & (changed::()), + ), + ) + // Translation + Rotation + .with_query( + <(Write, Read, Read)>::query().filter( + !component::() + & !component::() + & !component::() + & (changed::() | changed::()), + ), + ) + // Translation + Scale + .with_query( + <(Write, Read, Read)>::query().filter( + !component::() + & !component::() + & !component::() + & (changed::() | changed::()), + ), + ) + // Translation + NonUniformScale + .with_query( + <( + Write, + Read, + Read, + )>::query() + .filter( + !component::() + & !component::() + & !component::() + & (changed::() | changed::()), + ), + ) + // Rotation + Scale + .with_query( + <(Write, Read, Read)>::query().filter( + !component::() + & !component::() + & !component::() + & (changed::() | changed::()), + ), + ) + // Rotation + NonUniformScale + .with_query( + <(Write, Read, Read)>::query().filter( + !component::() + & !component::() + & !component::() + & (changed::() | changed::()), + ), + ) + // Translation + Rotation + Scale + .with_query( + <( + Write, + Read, + Read, + Read, + )>::query() + .filter( + !component::() + & !component::() + & (changed::() | changed::() | changed::()), + ), + ) + // Translation + Rotation + NonUniformScale + .with_query( + <( + Write, + Read, + Read, + Read, + )>::query() + .filter( + !component::() + & !component::() + & (changed::() + | changed::() + | changed::()), + ), + ) + // Just to issue warnings: Scale + NonUniformScale + .with_query( + <(Read, Read, Read)>::query() + .filter(!component::()), + ) + .build(move |_commands, world, _, queries| { + let (a, b, c, d, e, f, g, h, i, j, k, l) = queries; + rayon::scope(|s| { + s.spawn(|_| unsafe { + // Translation + a.for_each_unchecked(world, |(mut ltw, translation)| { + *ltw = LocalToWorld(translation.to_homogeneous()); + }); + }); + s.spawn(|_| unsafe { + // Rotation + b.for_each_unchecked(world, |(mut ltw, rotation)| { + *ltw = LocalToWorld(rotation.to_homogeneous()); + }); + }); + s.spawn(|_| unsafe { + // Scale + c.for_each_unchecked(world, |(mut ltw, scale)| { + *ltw = LocalToWorld(Matrix4::new_scaling(scale.0)); + }); + }); + s.spawn(|_| unsafe { + // NonUniformScale + d.for_each_unchecked(world, |(mut ltw, non_uniform_scale)| { + *ltw = LocalToWorld(Matrix4::new_nonuniform_scaling(&non_uniform_scale.0)); + }); + }); + s.spawn(|_| unsafe { + // Translation + Rotation + e.for_each_unchecked(world, |(mut ltw, translation, rotation)| { + *ltw = LocalToWorld( + rotation + .to_homogeneous() + .append_translation(&translation.vector), + ); + }); + }); + s.spawn(|_| unsafe { + // Translation + Scale + f.for_each_unchecked(world, |(mut ltw, translation, scale)| { + *ltw = LocalToWorld(translation.to_homogeneous().prepend_scaling(scale.0)); + }); + }); + s.spawn(|_| unsafe { + // Translation + NonUniformScale + g.for_each_unchecked(world, |(mut ltw, translation, non_uniform_scale)| { + *ltw = LocalToWorld( + translation + .to_homogeneous() + .prepend_nonuniform_scaling(&non_uniform_scale.0), + ); + }); + }); + s.spawn(|_| unsafe { + // Rotation + Scale + h.for_each_unchecked(world, |(mut ltw, rotation, scale)| { + *ltw = LocalToWorld(rotation.to_homogeneous().prepend_scaling(scale.0)); + }); + }); + s.spawn(|_| unsafe { + // Rotation + NonUniformScale + i.for_each_unchecked(world, |(mut ltw, rotation, non_uniform_scale)| { + *ltw = LocalToWorld( + rotation + .to_homogeneous() + .prepend_nonuniform_scaling(&non_uniform_scale.0), + ); + }); + }); + s.spawn(|_| unsafe { + // Translation + Rotation + Scale + j.for_each_unchecked(world, |(mut ltw, translation, rotation, scale)| { + *ltw = LocalToWorld( + rotation + .to_homogeneous() + .append_translation(&translation.vector) + .prepend_scaling(scale.0), + ); + }); + }); + s.spawn(|_| unsafe { + // Translation + Rotation + NonUniformScale + k.for_each_unchecked( + world, + |(mut ltw, translation, rotation, non_uniform_scale)| { + *ltw = LocalToWorld( + rotation + .to_homogeneous() + .append_translation(&translation.vector) + .prepend_nonuniform_scaling(&non_uniform_scale.0), + ); + }, + ); + }); + + // Just to issue warnings: Scale + NonUniformScale + unsafe { + l.iter_entities_immutable(world).for_each( + |(entity, (mut _ltw, _scale, _non_uniform_scale))| { + log::warn!( + "Entity {:?} has both a Scale and NonUniformScale component.", + entity + ); + }, + ); + } + }); + }) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn correct_world_transformation() { + let _ = env_logger::builder().is_test(true).try_init(); + + let mut world = Universe::new().create_world(); + let system = build(&mut world); + + let ltw = LocalToWorld::identity(); + let t = Translation::new(1.0, 2.0, 3.0); + let r = Rotation::from_euler_angles(1.0, 2.0, 3.0); + let s = Scale(2.0); + let nus = NonUniformScale::new(1.0, 2.0, 3.0); + + // Add every combination of transform types. + let translation = *world.insert((), vec![(ltw, t)]).first().unwrap(); + let rotation = *world.insert((), vec![(ltw, r)]).first().unwrap(); + let scale = *world.insert((), vec![(ltw, s)]).first().unwrap(); + let non_uniform_scale = *world.insert((), vec![(ltw, nus)]).first().unwrap(); + let translation_and_rotation = *world.insert((), vec![(ltw, t, r)]).first().unwrap(); + let translation_and_scale = *world.insert((), vec![(ltw, t, s)]).first().unwrap(); + let translation_and_nus = *world.insert((), vec![(ltw, t, nus)]).first().unwrap(); + let rotation_scale = *world.insert((), vec![(ltw, r, s)]).first().unwrap(); + let rotation_nus = *world.insert((), vec![(ltw, r, nus)]).first().unwrap(); + let translation_rotation_scale = *world.insert((), vec![(ltw, t, r, s)]).first().unwrap(); + let translation_rotation_nus = *world.insert((), vec![(ltw, t, r, nus)]).first().unwrap(); + + // Run the system + system.run(&mut world); + system.command_buffer_mut().write(&mut world); + + // Verify that each was transformed correctly. + assert_eq!( + world.get_component::(translation).unwrap().0, + t.to_homogeneous() + ); + assert_eq!( + world.get_component::(rotation).unwrap().0, + r.to_homogeneous() + ); + assert_eq!( + world.get_component::(scale).unwrap().0, + Matrix4::new_scaling(s.0), + ); + assert_eq!( + world + .get_component::(non_uniform_scale) + .unwrap() + .0, + Matrix4::new_nonuniform_scaling(&nus.0), + ); + assert_eq!( + world + .get_component::(translation_and_rotation) + .unwrap() + .0, + r.to_homogeneous().append_translation(&t.vector), + ); + assert_eq!( + world + .get_component::(translation_and_scale) + .unwrap() + .0, + t.to_homogeneous().prepend_scaling(s.0), + ); + assert_eq!( + world + .get_component::(translation_and_nus) + .unwrap() + .0, + t.to_homogeneous().prepend_nonuniform_scaling(&nus.0), + ); + assert_eq!( + world + .get_component::(rotation_scale) + .unwrap() + .0, + r.to_homogeneous().prepend_scaling(s.0) + ); + assert_eq!( + world.get_component::(rotation_nus).unwrap().0, + r.to_homogeneous().prepend_nonuniform_scaling(&nus.0) + ); + assert_eq!( + world + .get_component::(translation_rotation_scale) + .unwrap() + .0, + r.to_homogeneous() + .append_translation(&t.vector) + .prepend_scaling(s.0) + ); + assert_eq!( + world + .get_component::(translation_rotation_nus) + .unwrap() + .0, + r.to_homogeneous() + .append_translation(&t.vector) + .prepend_nonuniform_scaling(&nus.0) + ); + } +} diff --git a/src/legion_transform/src/transform_system_bundle.rs b/src/legion_transform/src/transform_system_bundle.rs new file mode 100644 index 0000000000..c48657fb85 --- /dev/null +++ b/src/legion_transform/src/transform_system_bundle.rs @@ -0,0 +1,20 @@ +use crate::{ + ecs::prelude::*, hierarchy_maintenance_system, local_to_parent_system, + local_to_world_propagate_system, local_to_world_system, +}; + +pub fn build(world: &mut World) -> Vec> { + let mut all_systems = Vec::with_capacity(5); + + let mut hierarchy_maintenance_systems = hierarchy_maintenance_system::build(world); + let local_to_parent_system = local_to_parent_system::build(world); + let local_to_world_system = local_to_world_system::build(world); + let local_to_world_propagate_system = local_to_world_propagate_system::build(world); + + all_systems.append(&mut hierarchy_maintenance_systems); + all_systems.push(local_to_parent_system); + all_systems.push(local_to_world_system); + all_systems.push(local_to_world_propagate_system); + + all_systems +} diff --git a/src/lib.rs b/src/lib.rs index c7228330a7..4481cc57d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,16 @@ -mod core; +pub mod render; +pub mod asset; mod application; mod vertex; -mod temp; -mod render; +pub mod temp; +mod core; pub use application::Application; pub use crate::core::*; +pub use wgpu; pub use legion; +pub use legion_transform; +pub use legion::prelude::*; +pub use legion_transform::prelude::*; pub use nalgebra_glm as math; \ No newline at end of file diff --git a/src/render/camera.rs b/src/render/camera.rs index 1e8634da01..aeb416010f 100644 --- a/src/render/camera.rs +++ b/src/render/camera.rs @@ -1,5 +1,9 @@ use crate::math; +pub struct Camera { + projection: math::Mat4, +} + pub fn get_projection_view_matrix(eye: &math::Vec3, fov: f32, aspect_ratio: f32, near: f32, far: f32) -> math::Mat4 { let projection = math::perspective(aspect_ratio, fov, near, far); diff --git a/src/render/forward/mod.rs b/src/render/forward/mod.rs index 19ab3867e5..314bacd7ad 100644 --- a/src/render/forward/mod.rs +++ b/src/render/forward/mod.rs @@ -1,4 +1,4 @@ -use crate::{render::*, temp::*}; +use crate::{render::*, temp::*, asset::*, render::mesh::*}; use legion::prelude::*; use std::{mem, sync::Arc}; use zerocopy::{AsBytes, FromBytes}; @@ -21,7 +21,7 @@ pub struct ForwardPass { impl Pass for ForwardPass { fn render(&mut self, device: &Device, frame: &SwapChainOutput, encoder: &mut CommandEncoder, world: &mut World) { - let mut entities = >::query(); + let mut mesh_query = <(Read, Read>)>::query(); let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { attachment: &frame.view, @@ -48,11 +48,16 @@ impl Pass for ForwardPass { pass.set_pipeline(&self.pipeline); pass.set_bind_group(0, &self.bind_group, &[]); - for entity in entities.iter_immutable(world) { - pass.set_bind_group(1, &entity.bind_group, &[]); - pass.set_index_buffer(&entity.index_buf, 0); - pass.set_vertex_buffers(0, &[(&entity.vertex_buf, 0)]); - pass.draw_indexed(0 .. entity.index_count as u32, 0, 0 .. 1); + let mut mesh_storage = world.resources.get_mut::>().unwrap(); + for (entity, mesh) in mesh_query.iter_immutable(world) { + if let Some(mesh_asset) = mesh_storage.get(*mesh.id) { + mesh_asset.setup_buffers(device); + pass.set_bind_group(1, entity.bind_group.as_ref().unwrap(), &[]); + pass.set_index_buffer(mesh_asset.index_buffer.as_ref().unwrap(), 0); + pass.set_vertex_buffers(0, &[(&mesh_asset.vertex_buffer.as_ref().unwrap(), 0)]); + pass.draw_indexed(0 .. mesh_asset.indices.len() as u32, 0, 0 .. 1); + + }; } } } diff --git a/src/render/mesh.rs b/src/render/mesh.rs new file mode 100644 index 0000000000..daba7c0c52 --- /dev/null +++ b/src/render/mesh.rs @@ -0,0 +1,111 @@ +use crate::{vertex::Vertex, asset::Asset}; +use wgpu::{Buffer, Device}; +use zerocopy::{AsBytes, FromBytes}; + +pub enum MeshType { + Cube, + Plane { + size: i8 + } +} + +pub struct Mesh { + pub vertices: Vec, + pub indices: Vec, + pub vertex_buffer: Option, + pub index_buffer: Option, +} + +impl Mesh { + pub fn setup_buffers(&mut self, device: &Device) { + if let None = self.vertex_buffer { + self.vertex_buffer = Some(device.create_buffer_with_data(self.vertices.as_bytes(), wgpu::BufferUsage::VERTEX)); + } + + if let None = self.index_buffer { + self.index_buffer = Some(device.create_buffer_with_data(self.indices.as_bytes(), wgpu::BufferUsage::INDEX)); + } + } +} + +impl Asset for Mesh { + fn load(descriptor: MeshType) -> Self { + let (vertices, indices) = match descriptor { + MeshType::Cube => create_cube(), + MeshType::Plane { size } => create_plane(size), + }; + + Mesh { + vertices, + indices, + vertex_buffer: None, + index_buffer: None, + } + } +} + +pub fn vertex(pos: [i8; 3], nor: [i8; 3]) -> Vertex { + Vertex { + pos: [pos[0], pos[1], pos[2], 1], + normal: [nor[0], nor[1], nor[2], 0], + } +} + +pub fn create_cube() -> (Vec, Vec) { + let vertex_data = [ + // top (0, 0, 1) + vertex([-1, -1, 1], [0, 0, 1]), + vertex([1, -1, 1], [0, 0, 1]), + vertex([1, 1, 1], [0, 0, 1]), + vertex([-1, 1, 1], [0, 0, 1]), + // bottom (0, 0, -1) + vertex([-1, 1, -1], [0, 0, -1]), + vertex([1, 1, -1], [0, 0, -1]), + vertex([1, -1, -1], [0, 0, -1]), + vertex([-1, -1, -1], [0, 0, -1]), + // right (1, 0, 0) + vertex([1, -1, -1], [1, 0, 0]), + vertex([1, 1, -1], [1, 0, 0]), + vertex([1, 1, 1], [1, 0, 0]), + vertex([1, -1, 1], [1, 0, 0]), + // left (-1, 0, 0) + vertex([-1, -1, 1], [-1, 0, 0]), + vertex([-1, 1, 1], [-1, 0, 0]), + vertex([-1, 1, -1], [-1, 0, 0]), + vertex([-1, -1, -1], [-1, 0, 0]), + // front (0, 1, 0) + vertex([1, 1, -1], [0, 1, 0]), + vertex([-1, 1, -1], [0, 1, 0]), + vertex([-1, 1, 1], [0, 1, 0]), + vertex([1, 1, 1], [0, 1, 0]), + // back (0, -1, 0) + vertex([1, -1, 1], [0, -1, 0]), + vertex([-1, -1, 1], [0, -1, 0]), + vertex([-1, -1, -1], [0, -1, 0]), + vertex([1, -1, -1], [0, -1, 0]), + ]; + + let index_data: &[u16] = &[ + 0, 1, 2, 2, 3, 0, // top + 4, 5, 6, 6, 7, 4, // bottom + 8, 9, 10, 10, 11, 8, // right + 12, 13, 14, 14, 15, 12, // left + 16, 17, 18, 18, 19, 16, // front + 20, 21, 22, 22, 23, 20, // back + ]; + + (vertex_data.to_vec(), index_data.to_vec()) +} + +pub fn create_plane(size: i8) -> (Vec, Vec) { + let vertex_data = [ + vertex([size, -size, 0], [0, 0, 1]), + vertex([size, size, 0], [0, 0, 1]), + vertex([-size, -size, 0], [0, 0, 1]), + vertex([-size, size, 0], [0, 0, 1]), + ]; + + let index_data: &[u16] = &[0, 1, 2, 2, 1, 3]; + + (vertex_data.to_vec(), index_data.to_vec()) +} \ No newline at end of file diff --git a/src/render/mod.rs b/src/render/mod.rs index 2836b6ff50..7b1a3f439d 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -1,5 +1,6 @@ pub mod camera; pub mod shader; +pub mod mesh; mod forward; mod shadow; mod light; @@ -11,7 +12,13 @@ pub use light::*; pub use shader::*; pub use pass::*; +use wgpu::BindGroup; + pub struct UniformBuffer { pub buffer: wgpu::Buffer, pub size: u64, +} + +pub struct Rendered { + pub bind_group: Option, } \ No newline at end of file diff --git a/src/render/shadow/mod.rs b/src/render/shadow/mod.rs index 89048b45fe..1f9faf67e4 100644 --- a/src/render/shadow/mod.rs +++ b/src/render/shadow/mod.rs @@ -1,4 +1,4 @@ -use crate::{render::*, temp::*}; +use crate::{render::*, temp::*, asset::*, render::mesh::*}; use wgpu::{BindGroupLayout, CommandEncoder, Device, VertexBufferDescriptor, SwapChainOutput}; use legion::prelude::*; use zerocopy::AsBytes; @@ -23,7 +23,7 @@ pub struct ShadowUniforms { impl Pass for ShadowPass { fn render(&mut self, device: &Device, _: &SwapChainOutput, encoder: &mut CommandEncoder, world: &mut World) { let mut light_query = >::query(); - let mut entity_query = >::query(); + let mut mesh_query = <(Read, Read>)>::query(); let light_count = light_query.iter(world).count(); if self.lights_are_dirty { @@ -73,11 +73,17 @@ impl Pass for ShadowPass { pass.set_pipeline(&self.pipeline); pass.set_bind_group(0, &self.bind_group, &[]); - for entity in entity_query.iter_immutable(world) { - pass.set_bind_group(1, &entity.bind_group, &[]); - pass.set_index_buffer(&entity.index_buf, 0); - pass.set_vertex_buffers(0, &[(&entity.vertex_buf, 0)]); - pass.draw_indexed(0 .. entity.index_count as u32, 0, 0 .. 1); + let mut mesh_storage = world.resources.get_mut::>().unwrap(); + for (entity, mesh) in mesh_query.iter_immutable(world) { + if let Some(mut mesh_asset) = mesh_storage.get(*mesh.id) { + mesh_asset.setup_buffers(device); + + pass.set_bind_group(1, entity.bind_group.as_ref().unwrap(), &[]); + pass.set_index_buffer(&mesh_asset.index_buffer.as_ref().unwrap(), 0); + pass.set_vertex_buffers(0, &[(&mesh_asset.vertex_buffer.as_ref().unwrap(), 0)]); + pass.draw_indexed(0 .. mesh_asset.indices.len() as u32, 0, 0 .. 1); + + }; } } } diff --git a/src/temp.rs b/src/temp.rs index 28f842460a..ee5537005b 100644 --- a/src/temp.rs +++ b/src/temp.rs @@ -1,14 +1,11 @@ use std::{sync::Arc}; use zerocopy::{AsBytes, FromBytes}; +use crate::math; pub struct CubeEnt { - pub rotation_speed: f32, - pub color: wgpu::Color, - pub vertex_buf: Arc, - pub index_buf: Arc, - pub index_count: usize, - pub bind_group: wgpu::BindGroup, - pub uniform_buf: wgpu::Buffer, + pub color: math::Vec4, + pub bind_group: Option, + pub uniform_buf: Option, } #[repr(C)] @@ -16,4 +13,28 @@ pub struct CubeEnt { pub struct EntityUniforms { pub model: [[f32; 4]; 4], pub color: [f32; 4], +} + +#[allow(dead_code)] +pub fn create_texels(size: usize) -> Vec { + use std::iter; + + (0 .. size * size) + .flat_map(|id| { + // get high five for recognizing this ;) + let cx = 3.0 * (id % size) as f32 / (size - 1) as f32 - 2.0; + let cy = 2.0 * (id / size) as f32 / (size - 1) as f32 - 1.0; + let (mut x, mut y, mut count) = (cx, cy, 0); + while count < 0xFF && x * x + y * y < 4.0 { + let old_x = x; + x = x * x - y * y + cx; + y = 2.0 * old_x * y + cy; + count += 1; + } + iter::once(0xFF - (count * 5) as u8) + .chain(iter::once(0xFF - (count * 15) as u8)) + .chain(iter::once(0xFF - (count * 50) as u8)) + .chain(iter::once(1)) + }) + .collect() } \ No newline at end of file diff --git a/src/vertex.rs b/src/vertex.rs index bad5a92f8d..963b7bb783 100644 --- a/src/vertex.rs +++ b/src/vertex.rs @@ -5,94 +5,4 @@ use zerocopy::{AsBytes, FromBytes}; pub struct Vertex { pub pos: [i8; 4], pub normal: [i8; 4], -} - -pub fn vertex(pos: [i8; 3], nor: [i8; 3]) -> Vertex { - Vertex { - pos: [pos[0], pos[1], pos[2], 1], - normal: [nor[0], nor[1], nor[2], 0], - } -} - -pub fn create_cube() -> (Vec, Vec) { - let vertex_data = [ - // top (0, 0, 1) - vertex([-1, -1, 1], [0, 0, 1]), - vertex([1, -1, 1], [0, 0, 1]), - vertex([1, 1, 1], [0, 0, 1]), - vertex([-1, 1, 1], [0, 0, 1]), - // bottom (0, 0, -1) - vertex([-1, 1, -1], [0, 0, -1]), - vertex([1, 1, -1], [0, 0, -1]), - vertex([1, -1, -1], [0, 0, -1]), - vertex([-1, -1, -1], [0, 0, -1]), - // right (1, 0, 0) - vertex([1, -1, -1], [1, 0, 0]), - vertex([1, 1, -1], [1, 0, 0]), - vertex([1, 1, 1], [1, 0, 0]), - vertex([1, -1, 1], [1, 0, 0]), - // left (-1, 0, 0) - vertex([-1, -1, 1], [-1, 0, 0]), - vertex([-1, 1, 1], [-1, 0, 0]), - vertex([-1, 1, -1], [-1, 0, 0]), - vertex([-1, -1, -1], [-1, 0, 0]), - // front (0, 1, 0) - vertex([1, 1, -1], [0, 1, 0]), - vertex([-1, 1, -1], [0, 1, 0]), - vertex([-1, 1, 1], [0, 1, 0]), - vertex([1, 1, 1], [0, 1, 0]), - // back (0, -1, 0) - vertex([1, -1, 1], [0, -1, 0]), - vertex([-1, -1, 1], [0, -1, 0]), - vertex([-1, -1, -1], [0, -1, 0]), - vertex([1, -1, -1], [0, -1, 0]), - ]; - - let index_data: &[u16] = &[ - 0, 1, 2, 2, 3, 0, // top - 4, 5, 6, 6, 7, 4, // bottom - 8, 9, 10, 10, 11, 8, // right - 12, 13, 14, 14, 15, 12, // left - 16, 17, 18, 18, 19, 16, // front - 20, 21, 22, 22, 23, 20, // back - ]; - - (vertex_data.to_vec(), index_data.to_vec()) -} - -pub fn create_plane(size: i8) -> (Vec, Vec) { - let vertex_data = [ - vertex([size, -size, 0], [0, 0, 1]), - vertex([size, size, 0], [0, 0, 1]), - vertex([-size, -size, 0], [0, 0, 1]), - vertex([-size, size, 0], [0, 0, 1]), - ]; - - let index_data: &[u16] = &[0, 1, 2, 2, 1, 3]; - - (vertex_data.to_vec(), index_data.to_vec()) -} - -#[allow(dead_code)] -pub fn create_texels(size: usize) -> Vec { - use std::iter; - - (0 .. size * size) - .flat_map(|id| { - // get high five for recognizing this ;) - let cx = 3.0 * (id % size) as f32 / (size - 1) as f32 - 2.0; - let cy = 2.0 * (id / size) as f32 / (size - 1) as f32 - 1.0; - let (mut x, mut y, mut count) = (cx, cy, 0); - while count < 0xFF && x * x + y * y < 4.0 { - let old_x = x; - x = x * x - y * y + cx; - y = 2.0 * old_x * y + cy; - count += 1; - } - iter::once(0xFF - (count * 5) as u8) - .chain(iter::once(0xFF - (count * 15) as u8)) - .chain(iter::once(0xFF - (count * 50) as u8)) - .chain(iter::once(1)) - }) - .collect() } \ No newline at end of file diff --git a/tiny-town/src/main.rs b/tiny-town/src/main.rs index 7dfe8c89c7..799b4a61ae 100644 --- a/tiny-town/src/main.rs +++ b/tiny-town/src/main.rs @@ -1,8 +1,19 @@ use bevy::*; -use bevy::legion::prelude::*; +use bevy::{render::mesh::{Mesh, MeshType}, asset::{Asset, AssetStorage}, temp::*, math}; fn main() { + let universe = Universe::new(); + let mut world = universe.create_world(); // Create a query which finds all `Position` and `Velocity` components // let mut query = Read::::query(); - Application::run(); + let cube = Mesh::load(MeshType::Cube); + let mut mesh_storage = AssetStorage::::new(); + let handle = mesh_storage.add(cube); + world.resources.insert(mesh_storage); + + world.insert((), vec![ + (CubeEnt { color: math::Vec4::identity(), bind_group: None, uniform_buf: None }, handle, LocalToWorld::identity(), Translation::new(0.0, 0.0, 0.0)) + ]); + + Application::run(universe, world); } \ No newline at end of file