├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── benches └── local_to_world.rs ├── examples ├── hierarchy.rs └── types_of_transforms.rs └── src ├── components ├── children.rs ├── local_to_parent.rs ├── local_to_world.rs ├── mod.rs ├── non_uniform_scale.rs ├── parent.rs ├── rotation.rs ├── scale.rs └── translation.rs ├── lib.rs ├── local_to_parent_system.rs ├── local_to_world_propagate_system.rs ├── local_to_world_system.rs ├── missing_previous_parent_system.rs ├── parent_update_system.rs └── transform_system_bundle.rs /.gitignore: -------------------------------------------------------------------------------- 1 | book/book 2 | target 3 | Cargo.lock 4 | *.log 5 | 6 | # Backup files 7 | .DS_Store 8 | thumbs.db 9 | *~ 10 | *.rs.bk 11 | *.swp 12 | 13 | # IDE / Editor files 14 | *.iml 15 | .idea 16 | .vscode 17 | 18 | 19 | #Added by cargo 20 | # 21 | #already existing elements are commented out 22 | 23 | /target 24 | **/*.rs.bk 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - nightly 5 | matrix: 6 | allow_failures: 7 | - rust: nightly 8 | fast_finish: true 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "legion_transform" 3 | version = "0.3.0" 4 | authors = ["Alec Thilenius "] 5 | edition = "2018" 6 | 7 | license = "MIT" 8 | 9 | [dependencies] 10 | legion = { git = "https://github.com/TomGillen/legion", features = ["extended-tuple-impls"], rev = "b93b636d" } 11 | log = "0.4" 12 | nalgebra = { version = "0.19.0", features = ["serde-serialize", "mint"] } 13 | rayon = "1.2" 14 | serde = { version = "1", features = ["derive"] } 15 | smallvec = "0.6" 16 | shrinkwraprs = "0.2" 17 | 18 | [dev-dependencies] 19 | env_logger = "0.7" 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alec Thilenius 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hierarchical Legion Transform 2 | 3 | [![Build Status][build_img]][build_lnk] 4 | 5 | [build_img]: https://travis-ci.org/amethyst/legion_transform.svg?branch=master 6 | [build_lnk]: https://travis-ci.org/amethyst/legion_transform 7 | 8 | A hierarchical space transform system, implemented using [Legion 9 | ECS](https://github.com/TomGillen/legion). The implementation is based heavily 10 | on the new Unity ECS Transformation layout. 11 | 12 | ## Usage 13 | 14 | ### TL;DR - Just show me the secret codes and incantations! 15 | 16 | See [examples/hierarchy.rs](examples/hierarchy.rs) 17 | 18 | ```rust 19 | #[allow(unused)] 20 | fn tldr_sample() { 21 | // Create a normal Legion World 22 | let mut world = Universe::new().create_world(); 23 | 24 | // Create a system bundle (vec of systems) for LegionTransform 25 | let transform_system_bundle = TransformSystemBundle::default().build(); 26 | 27 | let parent_entity = *world 28 | .insert( 29 | (), 30 | vec![( 31 | // Always needed for an Entity that has any space transform 32 | LocalToWorld::identity(), 33 | // The only mutable space transform a parent has is a translation. 34 | Translation::new(100.0, 0.0, 0.0), 35 | )], 36 | ) 37 | .first() 38 | .unwrap(); 39 | 40 | world.insert( 41 | (), 42 | vec![ 43 | ( 44 | // Again, always need a `LocalToWorld` component for the Entity to have a custom 45 | // space transform. 46 | LocalToWorld::identity(), 47 | // Here we define a Translation, Rotation and uniform Scale. 48 | Translation::new(1.0, 2.0, 3.0), 49 | Rotation::from_euler_angles(3.14, 0.0, 0.0), 50 | Scale(2.0), 51 | // Add a Parent and LocalToParent component to attach a child to a parent. 52 | Parent(parent_entity), 53 | LocalToParent::identity(), 54 | ); 55 | 4 56 | ], 57 | ); 58 | } 59 | ``` 60 | 61 | See [examples](/examples) for both transform and hierarchy examples. 62 | 63 | ### Transform Overview 64 | 65 | The Transform and Hierarchy parts of Legion Transform are largely separate and 66 | can thus be explained independently. We will start with space transforms, so for 67 | now completely put hierarchies out of mind (all entities have space transforms 68 | directly from their space to world space). 69 | 70 | A 3D space transform can come in many forms. The most generic of these is a 71 | matrix 4x4 which can represent any arbitrary (linear) space transform, including 72 | projections and sheers. These are rarely useful for entity transformations 73 | though, which are normally defined by things like 74 | 75 | - A **Translation** - movement along the X, Y or Z axis. 76 | - A **Rotation** - 3D rotation encoded as a Unit Quaternion to prevent [gimbal 77 | lock](https://en.wikipedia.org/wiki/Gimbal_lock). 78 | - A **Scale** - Defined as a single floating point value, but often 79 | **incorrectly defined as a Vector3** (which is a `NonUniformScale`) in other 80 | engines and 3D applications. 81 | - A **NonUniformScale** - Defined as a scale for the X, Y and Z axis 82 | independently from each other. 83 | 84 | In fact, in Legion Transform, each of the above is its own `Component` type. 85 | These components can be added in any combination to an `Entity` with the only 86 | exception being that `Scale` and `NonUniformScale` are mutually exclusive. 87 | 88 | Higher-order transformations can be built out of combinations of these 89 | components, for example: 90 | 91 | - Isometry: `Translation` + `Rotation` 92 | - Similarity: `Translation` + `Rotation` + `Scale` 93 | - Affine: `Translation` + `Rotation` + `NonUniformScale` 94 | 95 | The combination of these components will be processed (when they change) by the 96 | `LocalToWorldSystem` which will produce a correct `LocalToWorld` based on the 97 | attached transformations. This `LocalToWorld` is a homogeneous matrix4x4 98 | computed as: `(Translation * (Rotation * (Scale | NonUniformScale)))`. 99 | 100 | Breaking apart the transform into separate components means that you need only 101 | pay the runtime cost of computing the actual transform you need per-entity. 102 | Further, having `LocalToWorld` be a separate component means that any static 103 | entity (including those in static hierarchies) can be pre-baked into a 104 | `LocalToWorld` component and the rest of the transform data need not be loaded 105 | or stored in the final build of the game. 106 | 107 | In the event that the Entity is a member of a hierarchy, the `LocalToParent` 108 | matrix will house the `(Translation * (Rotation * (Scale | NonUniformScale)))` 109 | computation instead, and the `LocalToWorld` matrix will house the final local 110 | space to world space transformation (after all it's parent transformations have 111 | been computed). In other words, the `LocalToWorld` matrix is **always** the 112 | transformation from an entities local space, directly into world space, 113 | regardless of if the entity is a member of a hierarchy or not. 114 | 115 | ### Why not just NonUniformScale always? 116 | 117 | NonUniformScale is somewhat evil. It has been used (and abused) in countless 118 | game engines and 3D applications. A Transform with a non-uniform scale is known 119 | as an `Affine Transform` and it cannot be applied to things like a sphere 120 | collider in a physics engine without some serious gymnastics, loss of precision 121 | and/or detrimental performance impacts. For this reason, you should always use a 122 | uniform `Scale` component when possible. This component was named `Scale` over 123 | something like "UniformScale" to imply it's status as the default scale 124 | component and `NonUniformScale`'s status as a special case component. 125 | 126 | For more info on space transformations, see [nalgebra Points and 127 | Transformations](https://www.nalgebra.org/points_and_transformations/). 128 | 129 | ### Hierarchies 130 | 131 | Hierarchies in Legion Transform are defined in two parts. The first is the 132 | _Source Of Truth_ for the hierarchy, it is always correct and always up-to-date: 133 | the `Parent` Component. This is a component attached to children of a parent (ie 134 | a child 'has a' `Parent`). Users can update this component directly, and because 135 | it points toward the root of the hierarchy tree, it is impossible to form any 136 | other type of graph apart from a tree. 137 | 138 | Each time the Legion Transform system bundle is run, the 139 | `LocalToParentPropagateSystem` will also add/modify/remove a `Children` 140 | component on any entity that has children (ie entities that have a `Parent` 141 | component pointing to the parent entity). Because this component is only updated 142 | during the system bundle run, **it can be out of date, incorrect or missing 143 | altogether** after world mutations. 144 | 145 | It is important to note that as of today, any member of a hierarchy has it's 146 | `LocalToWorld` matrix re-computed each system bundle run, regardless of 147 | changes. This may someday change, but it is expected that the number of entities 148 | in a dynamic hierarchy for a final game should be small (static hierarchies can 149 | be pre-baked, where each entity gets a pre-baked `LocalToWorld` matrix). 150 | 151 | ## This is no good 'tall, why didn't you do it _this_ way? 152 | 153 | The first implementation used Legion `Tags` to store the Parent component for 154 | any child. This allowed for things like `O(1)` lookup of children, but caused 155 | too much fragmentation (Legion is an archetypical, chunked ECS). 156 | 157 | The second implementation was based on [this fine article by Michele 158 | Caini](https://skypjack.github.io/2019-06-25-ecs-baf-part-4/) which structures 159 | the hierarchy as an explicit parent pointer, a pointer to the first (and only 160 | first) child, and implicitly forms a linked-list of siblings. While elegant, the 161 | actual implementation was both complicated and near-impossible to multi-thread. 162 | For example, iterating through children entities required a global query to the 163 | Legion `World` for each child. I decided a small amount of memory by storing a 164 | possibly-out-of-date `SmallVec` of children was worth sacrificing on parent 165 | entities to make code both simpler and faster (theoretically, I never tested 166 | it). 167 | 168 | A lot of other options were considered as well, for example storing the entire 169 | hierarchy out-of-band from the ECS (much like Amethyst pre-Legion does). This 170 | has some pretty nasty drawbacks though. It makes streaming entities much harder, 171 | it means that hierarchies need to be special-case serialized/deserialized with 172 | initialization code being run on the newly deserialized entities. And it means 173 | that the hierarchy does not conform to the rest of the ECS. It also means that 174 | Legion, and all the various optimizations for querying / iterating large numbers 175 | of entities, was going to be mostly unused and a lot of global queries would 176 | need to be made against the `World` while syncing the `World` and out-of-band 177 | data-structure. I felt very strongly against an out-of-band implementation 178 | despite it being simpler to implement upfront. 179 | 180 | ## Todo 181 | 182 | - [ ] Hierarchy maintenance 183 | - [x] Remove changed `Parent` from `Children` list of the previous parent. 184 | - [x] Add changed `Parent` to `Children` list of the new parent. 185 | - [x] Update `PreviousParent` to the new Parent. 186 | - [x] Handle Entities with removed `Parent` components. 187 | - [x] Handle Entities with `Children` but without `LocalToWorld` (move their 188 | children to non-hierarchical). 189 | - [ ] Handle deleted Legion Entities (requires 190 | [Legion #13](https://github.com/TomGillen/legion/issues/13)) 191 | - [x] Local to world and parent transformation 192 | - [x] Handle homogeneous `Matrix4` calculation for combinations of: 193 | - [x] Translation 194 | - [x] Rotation 195 | - [x] Scale 196 | - [x] NonUniformScale 197 | - [x] Handle change detection and only recompute `LocalToWorld` when needed. 198 | - [x] Multi-threaded updates for non-hierarchical `LocalToWorld` computation. 199 | - [x] Recompute `LocalToParent` each run, always. 200 | - [ ] Transform hierarchy propagation 201 | - [x] Collect roots of the hierarchy forest 202 | - [x] Recursively re-compute `LocalToWorld` from the `Parent`'s `LocalToWorld` 203 | and the `LocalToParent` of each child. 204 | - [ ] Multi-threaded updates for hierarchical `LocalToWorld` computation. 205 | - [ ] Compute all changes and flush them to a `CommandBuffer` rather than 206 | direct mutation of components. 207 | 208 | ## Blockers 209 | 210 | - Legion has no ability to detect deleted entities or components. 211 | [GitHub Issue #13](https://github.com/TomGillen/legion/issues/13) 212 | -------------------------------------------------------------------------------- /benches/local_to_world.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate test; 4 | 5 | use legion::*; 6 | use legion_transform::{local_to_world_system, prelude::*}; 7 | use test::Bencher; 8 | 9 | #[bench] 10 | fn local_to_world_update_without_change(b: &mut Bencher) { 11 | let _ = env_logger::builder().is_test(true).try_init(); 12 | 13 | let mut resources = Resources::default(); 14 | let mut world = World::default(); 15 | let mut schedule = Schedule::builder() 16 | .add_system(local_to_world_system::build()) 17 | .build(); 18 | 19 | let ltw = LocalToWorld::identity(); 20 | let t = Translation::new(1.0, 2.0, 3.0); 21 | let r = Rotation::from_euler_angles(1.0, 2.0, 3.0); 22 | let s = Scale(2.0); 23 | let nus = NonUniformScale::new(1.0, 2.0, 3.0); 24 | 25 | // Add N of every combination of transform types. 26 | let n = 1000; 27 | world.extend(vec![(ltw, t); n]); 28 | world.extend(vec![(ltw, r); n]); 29 | world.extend(vec![(ltw, s); n]); 30 | world.extend(vec![(ltw, nus); n]); 31 | world.extend(vec![(ltw, t, r); n]); 32 | world.extend(vec![(ltw, t, s); n]); 33 | world.extend(vec![(ltw, t, nus); n]); 34 | world.extend(vec![(ltw, r, s); n]); 35 | world.extend(vec![(ltw, r, nus); n]); 36 | world.extend(vec![(ltw, t, r, s); n]); 37 | world.extend(vec![(ltw, t, r, nus); n]); 38 | 39 | // Run the system once outside the test (which should compute everything and it shouldn't be 40 | // touched again). 41 | schedule.execute(&mut world, &mut resources); 42 | 43 | // Then time the already-computed updates. 44 | b.iter(|| { 45 | schedule.execute(&mut world, &mut resources); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /examples/hierarchy.rs: -------------------------------------------------------------------------------- 1 | extern crate legion; 2 | extern crate legion_transform; 3 | 4 | use legion::*; 5 | use legion_transform::prelude::*; 6 | 7 | #[allow(unused)] 8 | fn tldr_sample() { 9 | // Create a normal Legion World 10 | let mut world = World::default(); 11 | let mut resources = Resources::default(); 12 | 13 | // Create a system bundle (vec of systems) for LegionTransform 14 | let transform_system_bundle = transform_system_bundle::build(); 15 | 16 | let parent_entity = world.push(( 17 | // Always needed for an Entity that has any space transform 18 | LocalToWorld::identity(), 19 | // The only mutable space transform a parent has is a translation. 20 | Translation::new(100.0, 0.0, 0.0), 21 | )); 22 | 23 | world.extend(vec![ 24 | ( 25 | // Again, always need a `LocalToWorld` component for the Entity to have a custom 26 | // space transform. 27 | LocalToWorld::identity(), 28 | // Here we define a Translation, Rotation and uniform Scale. 29 | Translation::new(1.0, 2.0, 3.0), 30 | Rotation::from_euler_angles(3.14, 0.0, 0.0), 31 | Scale(2.0), 32 | // Add a Parent and LocalToParent component to attach a child to a parent. 33 | Parent(parent_entity), 34 | LocalToParent::identity(), 35 | ); 36 | 4 37 | ]); 38 | } 39 | 40 | fn main() { 41 | // Create a normal Legion World 42 | let mut resources = Resources::default(); 43 | let mut world = World::default(); 44 | 45 | // Create a system bundle (vec of systems) for LegionTransform 46 | let mut transform_system_bundle = transform_system_bundle::build(); 47 | 48 | // See `./types_of_transforms.rs` for an explanation of space-transform types. 49 | let parent_entity = world.push((LocalToWorld::identity(), Translation::new(100.0, 0.0, 0.0))); 50 | 51 | let four_children: Vec<_> = world 52 | .extend(vec![ 53 | ( 54 | LocalToWorld::identity(), 55 | Translation::new(1.0, 2.0, 3.0), 56 | Rotation::from_euler_angles(3.14, 0.0, 0.0), 57 | Scale(2.0), 58 | // Add a Parent and LocalToParent component to attach a child to a parent. 59 | Parent(parent_entity), 60 | LocalToParent::identity(), 61 | ); 62 | 4 63 | ]) 64 | .iter() 65 | .cloned() 66 | .collect(); 67 | 68 | // At this point the parent does NOT have a `Children` component attached to it. The `Children` 69 | // component is updated by the transform system bundle and thus can be out of date (or 70 | // non-existent for newly added members). By this logic, the `Parent` components should be 71 | // considered the always-correct 'source of truth' for any hierarchy. 72 | for system in transform_system_bundle.iter_mut() { 73 | system.prepare(&world); 74 | system.run(&mut world, &mut resources); 75 | system 76 | .command_buffer_mut(world.id()) 77 | .unwrap() 78 | .flush(&mut world); 79 | } 80 | 81 | // At this point all parents with children have a correct `Children` component. 82 | let parents_children = world 83 | .entry_ref(parent_entity) 84 | .unwrap() 85 | .get_component::() 86 | .unwrap() 87 | .0 88 | .clone(); 89 | 90 | println!("Parent {:?}", parent_entity); 91 | for child in parents_children.iter() { 92 | println!(" -> Has child: {:?}", child); 93 | } 94 | 95 | // Each child will also have a `LocalToParent` component attached to it, which is a 96 | // space-transform from its local space to that of its parent. 97 | for child in four_children.iter() { 98 | println!("The child {:?}", child); 99 | println!( 100 | " -> Has a LocalToParent matrix: {}", 101 | world 102 | .entry_ref(*child) 103 | .unwrap() 104 | .get_component::() 105 | .unwrap() 106 | ); 107 | println!( 108 | " -> Has a LocalToWorld matrix: {}", 109 | world 110 | .entry_ref(*child) 111 | .unwrap() 112 | .get_component::() 113 | .unwrap() 114 | ); 115 | } 116 | 117 | // Re-parent the second child to be a grandchild of the first. 118 | world 119 | .entry(four_children[1]) 120 | .unwrap() 121 | .add_component(Parent(four_children[0])); 122 | 123 | // Re-running the system will cleanup and fix all `Children` components. 124 | for system in transform_system_bundle.iter_mut() { 125 | system.prepare(&world); 126 | system.run(&mut world, &mut resources); 127 | system 128 | .command_buffer_mut(world.id()) 129 | .unwrap() 130 | .flush(&mut world); 131 | } 132 | 133 | println!("After the second child was re-parented as a grandchild of the first child..."); 134 | 135 | for child in world 136 | .entry_ref(parent_entity) 137 | .unwrap() 138 | .get_component::() 139 | .unwrap() 140 | .0 141 | .iter() 142 | { 143 | println!("Parent {:?} has child: {:?}", parent_entity, child); 144 | } 145 | 146 | for grandchild in world 147 | .entry_ref(four_children[0]) 148 | .unwrap() 149 | .get_component::() 150 | .unwrap() 151 | .0 152 | .iter() 153 | { 154 | println!( 155 | "Child {:?} has grandchild: {:?}", 156 | four_children[0], grandchild 157 | ); 158 | } 159 | 160 | println!("Grandchild: {:?}", four_children[1]); 161 | println!( 162 | " -> Has a LocalToWorld matrix: {}", 163 | world 164 | .entry_ref(four_children[1]) 165 | .unwrap() 166 | .get_component::() 167 | .unwrap() 168 | ); 169 | } 170 | -------------------------------------------------------------------------------- /examples/types_of_transforms.rs: -------------------------------------------------------------------------------- 1 | extern crate legion; 2 | extern crate legion_transform; 3 | 4 | use legion::*; 5 | use legion_transform::prelude::*; 6 | 7 | fn main() { 8 | // Create a normal Legion World 9 | let mut world = World::default(); 10 | let mut resources = Resources::default(); 11 | 12 | // Create a system bundle (vec of systems) for LegionTransform 13 | let mut transform_system_bundle = transform_system_bundle::build(); 14 | 15 | // A user-defined space transform is split into 4 different components: [`Translation`, 16 | // `Rotation`, `Scale`, `NonUniformScale`]. Any combination of these components can be added to 17 | // an entity to transform it's space (exception: `Scale` and `NonUniformScale` are mutually 18 | // exclusive). 19 | 20 | // Note that all entities need an explicitly added `LocalToWorld` component to be considered for 21 | // processing during transform system passes. 22 | 23 | // Add an entity with just a Translation 24 | // See: https://www.nalgebra.org/rustdoc/nalgebra/geometry/struct.Translation.html 25 | // API on Translation, as a LegionTransform `Translation` is just a nalgebra `Translation3`. 26 | world.push((LocalToWorld::identity(), Translation::new(1.0, 2.0, 3.0))); 27 | 28 | // Add an entity with just a Rotation. 29 | // See: https://www.nalgebra.org/rustdoc/nalgebra/geometry/type.UnitQuaternion.html for the full 30 | // API on Rotation, as a LegionTransform `Rotation` is just a nalgebra `UnityQuaternion`. 31 | world.push(( 32 | LocalToWorld::identity(), 33 | Rotation::from_euler_angles(3.14, 0.0, 0.0), 34 | )); 35 | 36 | // Add an entity with just a uniform Scale (the default and strongly-preferred scale component). 37 | // This is simply a `f32` wrapper. 38 | world.push((LocalToWorld::identity(), Scale(2.0))); 39 | 40 | // Add an entity with just a NonUniformScale (This should be avoided unless you **really** need 41 | // non-uniform scaling as it breaks things like physics colliders. 42 | // See: https://docs.rs/nalgebra/0.10.1/nalgebra/struct.Vector3.html for the full API on 43 | // NonUniformScale, as a LegionTransform `NonUniformScale` is simply a nalgebra `Vector3`, 44 | // although note that it is wrapped in a tuple-struct. 45 | world.push(( 46 | LocalToWorld::identity(), 47 | NonUniformScale::new(1.0, 2.0, 1.0), 48 | )); 49 | 50 | // Add an entity with a combination of Translation and Rotation 51 | world.push(( 52 | LocalToWorld::identity(), 53 | Translation::new(1.0, 2.0, 3.0), 54 | Rotation::from_euler_angles(3.14, 0.0, 0.0), 55 | )); 56 | 57 | // Add an entity with a combination of Translation and Rotation and uniform Scale. 58 | world.push(( 59 | LocalToWorld::identity(), 60 | Translation::new(1.0, 2.0, 3.0), 61 | Rotation::from_euler_angles(3.14, 0.0, 0.0), 62 | Scale(2.0), 63 | )); 64 | 65 | // Run the system bundle (this API will likely change). 66 | for system in transform_system_bundle.iter_mut() { 67 | system.prepare(&world); 68 | system.run(&mut world, &mut resources); 69 | system 70 | .command_buffer_mut(world.id()) 71 | .unwrap() 72 | .flush(&mut world); 73 | } 74 | 75 | // At this point all `LocalToWorld` components have correct values in them. Running the system 76 | // again will result in a short-circuit as only changed components are considered for update. 77 | let mut query = <(Entity, Read)>::query(); 78 | for (entity, local_to_world) in query.iter(&mut world) { 79 | println!( 80 | "Entity {:?} and a LocalToWorld matrix: {}", 81 | entity, *local_to_world 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/components/children.rs: -------------------------------------------------------------------------------- 1 | use crate::ecs::*; 2 | use shrinkwraprs::Shrinkwrap; 3 | use smallvec::SmallVec; 4 | 5 | #[derive(Shrinkwrap, Default, Clone)] 6 | #[shrinkwrap(mutable)] 7 | pub struct Children(pub SmallVec<[Entity; 8]>); 8 | 9 | impl Children { 10 | pub fn with(entity: &[Entity]) -> Self { 11 | Self(SmallVec::from_slice(entity)) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/local_to_parent.rs: -------------------------------------------------------------------------------- 1 | use crate::math::Matrix4; 2 | use shrinkwraprs::Shrinkwrap; 3 | use std::fmt; 4 | 5 | #[derive(Shrinkwrap, Debug, PartialEq, Clone, Copy)] 6 | #[shrinkwrap(mutable)] 7 | pub struct LocalToParent(pub Matrix4); 8 | 9 | impl LocalToParent { 10 | pub fn identity() -> Self { 11 | Self(Matrix4::identity()) 12 | } 13 | } 14 | 15 | impl Default for LocalToParent { 16 | fn default() -> Self { 17 | Self::identity() 18 | } 19 | } 20 | 21 | impl fmt::Display for LocalToParent { 22 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 23 | write!(f, "{}", self.0) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/local_to_world.rs: -------------------------------------------------------------------------------- 1 | use crate::math::Matrix4; 2 | use shrinkwraprs::Shrinkwrap; 3 | use std::fmt; 4 | 5 | #[derive(Shrinkwrap, Debug, PartialEq, Clone, Copy)] 6 | #[shrinkwrap(mutable)] 7 | pub struct LocalToWorld(pub Matrix4); 8 | 9 | impl LocalToWorld { 10 | #[inline(always)] 11 | pub fn identity() -> Self { 12 | Self(Matrix4::identity()) 13 | } 14 | } 15 | 16 | impl Default for LocalToWorld { 17 | fn default() -> Self { 18 | Self::identity() 19 | } 20 | } 21 | 22 | impl fmt::Display for LocalToWorld { 23 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 24 | write!(f, "{}", self.0) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/mod.rs: -------------------------------------------------------------------------------- 1 | mod children; 2 | mod local_to_parent; 3 | mod local_to_world; 4 | mod non_uniform_scale; 5 | mod parent; 6 | mod rotation; 7 | mod scale; 8 | mod translation; 9 | 10 | pub use children::Children; 11 | pub use local_to_parent::*; 12 | pub use local_to_world::*; 13 | pub use non_uniform_scale::*; 14 | pub use parent::{Parent, PreviousParent}; 15 | pub use rotation::*; 16 | pub use scale::*; 17 | pub use translation::*; 18 | -------------------------------------------------------------------------------- /src/components/non_uniform_scale.rs: -------------------------------------------------------------------------------- 1 | use crate::math::Vector3; 2 | use shrinkwraprs::Shrinkwrap; 3 | use std::fmt; 4 | 5 | #[derive(Shrinkwrap, Debug, PartialEq, Clone, Copy)] 6 | #[shrinkwrap(mutable)] 7 | pub struct NonUniformScale(pub Vector3); 8 | 9 | impl NonUniformScale { 10 | pub fn new(x: f32, y: f32, z: f32) -> Self { 11 | Self(Vector3::new(x, y, z)) 12 | } 13 | } 14 | 15 | impl From> for NonUniformScale { 16 | fn from(scale: Vector3) -> Self { 17 | Self(scale) 18 | } 19 | } 20 | 21 | impl From<&Vector3> for NonUniformScale { 22 | fn from(scale: &Vector3) -> Self { 23 | Self(*scale) 24 | } 25 | } 26 | 27 | impl From<&mut Vector3> for NonUniformScale { 28 | fn from(scale: &mut Vector3) -> Self { 29 | Self(*scale) 30 | } 31 | } 32 | 33 | impl fmt::Display for NonUniformScale { 34 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 35 | write!( 36 | f, 37 | "NonUniformScale({}, {}, {})", 38 | self.0.x, self.0.y, self.0.z 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/parent.rs: -------------------------------------------------------------------------------- 1 | use crate::ecs::*; 2 | use shrinkwraprs::Shrinkwrap; 3 | 4 | #[derive(Shrinkwrap, Debug, Copy, Clone, Eq, PartialEq)] 5 | #[shrinkwrap(mutable)] 6 | pub struct Parent(pub Entity); 7 | 8 | #[derive(Shrinkwrap, Debug, Copy, Clone, Eq, PartialEq)] 9 | #[shrinkwrap(mutable)] 10 | pub struct PreviousParent(pub Option); 11 | -------------------------------------------------------------------------------- /src/components/rotation.rs: -------------------------------------------------------------------------------- 1 | use crate::math::UnitQuaternion; 2 | use shrinkwraprs::Shrinkwrap; 3 | 4 | #[derive(Shrinkwrap, Debug, PartialEq, Clone, Copy)] 5 | #[shrinkwrap(mutable)] 6 | pub struct Rotation(pub UnitQuaternion); 7 | impl Rotation { 8 | #[inline(always)] 9 | pub fn identity() -> Self { 10 | Self(UnitQuaternion::identity()) 11 | } 12 | 13 | #[inline(always)] 14 | pub fn from_euler_angles(roll: f32, pitch: f32, yaw: f32) -> Self { 15 | Self(UnitQuaternion::from_euler_angles(roll, pitch, yaw)) 16 | } 17 | } 18 | 19 | impl Default for Rotation { 20 | fn default() -> Self { 21 | Self::identity() 22 | } 23 | } 24 | 25 | impl From> for Rotation { 26 | fn from(rotation: UnitQuaternion) -> Self { 27 | Self(rotation) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/scale.rs: -------------------------------------------------------------------------------- 1 | use shrinkwraprs::Shrinkwrap; 2 | use std::fmt; 3 | 4 | #[derive(Shrinkwrap, Debug, PartialEq, Clone, Copy)] 5 | #[shrinkwrap(mutable)] 6 | pub struct Scale(pub f32); 7 | 8 | impl From for Scale { 9 | #[inline(always)] 10 | fn from(scale: f32) -> Self { 11 | Self(scale) 12 | } 13 | } 14 | 15 | impl Scale { 16 | #[inline(always)] 17 | pub fn identity() -> Self { 18 | Scale(1.0) 19 | } 20 | } 21 | 22 | impl Default for Scale { 23 | fn default() -> Self { 24 | Self::identity() 25 | } 26 | } 27 | 28 | impl fmt::Display for Scale { 29 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 30 | write!(f, "Scale({})", self.0) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/translation.rs: -------------------------------------------------------------------------------- 1 | use crate::math::{Translation3, Vector3}; 2 | use shrinkwraprs::Shrinkwrap; 3 | 4 | #[derive(Shrinkwrap, Debug, PartialEq, Clone, Copy)] 5 | #[shrinkwrap(mutable)] 6 | pub struct Translation(pub Translation3); 7 | 8 | impl Translation { 9 | #[inline(always)] 10 | pub fn identity() -> Self { 11 | Self(Translation3::identity()) 12 | } 13 | 14 | #[inline(always)] 15 | pub fn new(x: f32, y: f32, z: f32) -> Self { 16 | Self(Translation3::new(x, y, z)) 17 | } 18 | } 19 | 20 | impl Default for Translation { 21 | fn default() -> Self { 22 | Self::identity() 23 | } 24 | } 25 | 26 | impl From> for Translation { 27 | fn from(translation: Vector3) -> Self { 28 | Self(Translation3::from(translation)) 29 | } 30 | } 31 | 32 | impl From> for Translation { 33 | fn from(translation: Translation3) -> Self { 34 | Self(translation) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use legion as ecs; 2 | pub use nalgebra as math; 3 | 4 | pub mod components; 5 | pub mod local_to_parent_system; 6 | pub mod local_to_world_propagate_system; 7 | pub mod local_to_world_system; 8 | pub mod missing_previous_parent_system; 9 | pub mod parent_update_system; 10 | pub mod transform_system_bundle; 11 | 12 | pub mod prelude { 13 | pub use crate::components::*; 14 | pub use crate::local_to_parent_system; 15 | pub use crate::local_to_world_propagate_system; 16 | pub use crate::local_to_world_system; 17 | pub use crate::missing_previous_parent_system; 18 | pub use crate::parent_update_system; 19 | pub use crate::transform_system_bundle; 20 | } 21 | -------------------------------------------------------------------------------- /src/local_to_parent_system.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use crate::{ 3 | components::*, 4 | ecs::{systems::ParallelRunnable, *}, 5 | math::Matrix4, 6 | }; 7 | 8 | pub fn build() -> impl ParallelRunnable { 9 | SystemBuilder::<()>::new("LocalToParentUpdateSystem") 10 | // Translation 11 | .with_query(<(Write, Read)>::query().filter( 12 | !component::() 13 | & !component::() 14 | & !component::() 15 | & (maybe_changed::()), 16 | )) 17 | // Rotation 18 | .with_query(<(Write, Read)>::query().filter( 19 | !component::() 20 | & !component::() 21 | & !component::() 22 | & (maybe_changed::()), 23 | )) 24 | // Scale 25 | .with_query(<(Write, Read)>::query().filter( 26 | !component::() 27 | & !component::() 28 | & !component::() 29 | & (maybe_changed::()), 30 | )) 31 | // NonUniformScale 32 | .with_query( 33 | <(Write, Read)>::query().filter( 34 | !component::() 35 | & !component::() 36 | & !component::() 37 | & (maybe_changed::()), 38 | ), 39 | ) 40 | // Translation + Rotation 41 | .with_query( 42 | <(Write, Read, Read)>::query().filter( 43 | !component::() 44 | & !component::() 45 | & (maybe_changed::() | maybe_changed::()), 46 | ), 47 | ) 48 | // Translation + Scale 49 | .with_query( 50 | <(Write, Read, Read)>::query().filter( 51 | !component::() 52 | & !component::() 53 | & (maybe_changed::() | maybe_changed::()), 54 | ), 55 | ) 56 | // Translation + NonUniformScale 57 | .with_query( 58 | <( 59 | Write, 60 | Read, 61 | Read, 62 | )>::query() 63 | .filter( 64 | !component::() 65 | & !component::() 66 | & (maybe_changed::() | maybe_changed::()), 67 | ), 68 | ) 69 | // Rotation + Scale 70 | .with_query( 71 | <(Write, Read, Read)>::query().filter( 72 | !component::() 73 | & !component::() 74 | & (maybe_changed::() | maybe_changed::()), 75 | ), 76 | ) 77 | // Rotation + NonUniformScale 78 | .with_query( 79 | <(Write, Read, Read)>::query().filter( 80 | !component::() 81 | & !component::() 82 | & (maybe_changed::() | maybe_changed::()), 83 | ), 84 | ) 85 | // Translation + Rotation + Scale 86 | .with_query( 87 | <( 88 | Write, 89 | Read, 90 | Read, 91 | Read, 92 | )>::query() 93 | .filter( 94 | !component::() 95 | & (maybe_changed::() 96 | | maybe_changed::() 97 | | maybe_changed::()), 98 | ), 99 | ) 100 | // Translation + Rotation + NonUniformScale 101 | .with_query( 102 | <( 103 | Write, 104 | Read, 105 | Read, 106 | Read, 107 | )>::query() 108 | .filter( 109 | !component::() 110 | & (maybe_changed::() 111 | | maybe_changed::() 112 | | maybe_changed::()), 113 | ), 114 | ) 115 | // Just to issue warnings: Scale + NonUniformScale 116 | .with_query(<( 117 | Entity, 118 | Read, 119 | Read, 120 | Read, 121 | )>::query()) 122 | .build(move |_commands, world, _, queries| { 123 | let (a, b, c, d, e, f, g, h, i, j, k, l) = queries; 124 | rayon::scope(|s| { 125 | s.spawn(|_| unsafe { 126 | // Translation 127 | a.for_each_unchecked(world, |(ltw, translation)| { 128 | *ltw = LocalToParent(translation.to_homogeneous()); 129 | }); 130 | }); 131 | s.spawn(|_| unsafe { 132 | // Rotation 133 | b.for_each_unchecked(world, |(ltw, rotation)| { 134 | *ltw = LocalToParent(rotation.to_homogeneous()); 135 | }); 136 | }); 137 | s.spawn(|_| unsafe { 138 | // Scale 139 | c.for_each_unchecked(world, |(ltw, scale)| { 140 | *ltw = LocalToParent(Matrix4::new_scaling(scale.0)); 141 | }); 142 | }); 143 | s.spawn(|_| unsafe { 144 | // NonUniformScale 145 | d.for_each_unchecked(world, |(ltw, non_uniform_scale)| { 146 | *ltw = LocalToParent(Matrix4::new_nonuniform_scaling(&non_uniform_scale.0)); 147 | }); 148 | 149 | // Translation + Rotation 150 | e.for_each_unchecked(world, |(ltw, translation, rotation)| { 151 | *ltw = LocalToParent( 152 | rotation 153 | .to_homogeneous() 154 | .append_translation(&translation.vector), 155 | ); 156 | }); 157 | }); 158 | s.spawn(|_| unsafe { 159 | // Translation + Scale 160 | f.for_each_unchecked(world, |(ltw, translation, scale)| { 161 | *ltw = LocalToParent(translation.to_homogeneous().prepend_scaling(scale.0)); 162 | }); 163 | 164 | // Translation + NonUniformScale 165 | g.for_each_unchecked(world, |(ltw, translation, non_uniform_scale)| { 166 | *ltw = LocalToParent( 167 | translation 168 | .to_homogeneous() 169 | .prepend_nonuniform_scaling(&non_uniform_scale.0), 170 | ); 171 | }); 172 | }); 173 | s.spawn(|_| unsafe { 174 | // Rotation + Scale 175 | h.for_each_unchecked(world, |(ltw, rotation, scale)| { 176 | *ltw = LocalToParent(rotation.to_homogeneous().prepend_scaling(scale.0)); 177 | }); 178 | }); 179 | s.spawn(|_| unsafe { 180 | // Rotation + NonUniformScale 181 | i.for_each_unchecked(world, |(ltw, rotation, non_uniform_scale)| { 182 | *ltw = LocalToParent( 183 | rotation 184 | .to_homogeneous() 185 | .prepend_nonuniform_scaling(&non_uniform_scale.0), 186 | ); 187 | }); 188 | }); 189 | s.spawn(|_| unsafe { 190 | // Translation + Rotation + Scale 191 | j.for_each_unchecked(world, |(ltw, translation, rotation, scale)| { 192 | *ltw = LocalToParent( 193 | rotation 194 | .to_homogeneous() 195 | .append_translation(&translation.vector) 196 | .prepend_scaling(scale.0), 197 | ); 198 | }); 199 | }); 200 | s.spawn(|_| unsafe { 201 | // Translation + Rotation + NonUniformScale 202 | k.for_each_unchecked( 203 | world, 204 | |(ltw, translation, rotation, non_uniform_scale)| { 205 | *ltw = LocalToParent( 206 | rotation 207 | .to_homogeneous() 208 | .append_translation(&translation.vector) 209 | .prepend_nonuniform_scaling(&non_uniform_scale.0), 210 | ); 211 | }, 212 | ); 213 | }); 214 | }); 215 | // Just to issue warnings: Scale + NonUniformScale 216 | l.for_each_mut(world, |(entity, mut _ltw, _scale, _non_uniform_scale)| { 217 | log::warn!( 218 | "Entity {:?} has both a Scale and NonUniformScale component.", 219 | entity 220 | ); 221 | }); 222 | }) 223 | } 224 | 225 | #[cfg(test)] 226 | mod test { 227 | use super::*; 228 | 229 | #[test] 230 | fn correct_parent_transformation() { 231 | let _ = env_logger::builder().is_test(true).try_init(); 232 | 233 | let mut resources = Resources::default(); 234 | let mut world = World::default(); 235 | let mut schedule = Schedule::builder().add_system(build()).build(); 236 | 237 | let ltw = LocalToParent::identity(); 238 | let t = Translation::new(1.0, 2.0, 3.0); 239 | let r = Rotation::from_euler_angles(1.0, 2.0, 3.0); 240 | let s = Scale(2.0); 241 | let nus = NonUniformScale::new(1.0, 2.0, 3.0); 242 | 243 | // Add every combination of transform types. 244 | let translation = world.push((ltw, t)); 245 | let rotation = world.push((ltw, r)); 246 | let scale = world.push((ltw, s)); 247 | let non_uniform_scale = world.push((ltw, nus)); 248 | let translation_and_rotation = world.push((ltw, t, r)); 249 | let translation_and_scale = world.push((ltw, t, s)); 250 | let translation_and_nus = world.push((ltw, t, nus)); 251 | let rotation_scale = world.push((ltw, r, s)); 252 | let rotation_nus = world.push((ltw, r, nus)); 253 | let translation_rotation_scale = world.push((ltw, t, r, s)); 254 | let translation_rotation_nus = world.push((ltw, t, r, nus)); 255 | 256 | // Run the system 257 | schedule.execute(&mut world, &mut resources); 258 | 259 | // Verify that each was transformed correctly. 260 | assert_eq!( 261 | world 262 | .entry(translation) 263 | .unwrap() 264 | .get_component::() 265 | .unwrap() 266 | .0, 267 | t.to_homogeneous() 268 | ); 269 | assert_eq!( 270 | world 271 | .entry(rotation) 272 | .unwrap() 273 | .get_component::() 274 | .unwrap() 275 | .0, 276 | r.to_homogeneous() 277 | ); 278 | assert_eq!( 279 | world 280 | .entry(scale) 281 | .unwrap() 282 | .get_component::() 283 | .unwrap() 284 | .0, 285 | Matrix4::new_scaling(s.0), 286 | ); 287 | assert_eq!( 288 | world 289 | .entry(non_uniform_scale) 290 | .unwrap() 291 | .get_component::() 292 | .unwrap() 293 | .0, 294 | Matrix4::new_nonuniform_scaling(&nus.0), 295 | ); 296 | assert_eq!( 297 | world 298 | .entry(translation_and_rotation) 299 | .unwrap() 300 | .get_component::() 301 | .unwrap() 302 | .0, 303 | r.to_homogeneous().append_translation(&t.vector), 304 | ); 305 | assert_eq!( 306 | world 307 | .entry(translation_and_scale) 308 | .unwrap() 309 | .get_component::() 310 | .unwrap() 311 | .0, 312 | t.to_homogeneous().prepend_scaling(s.0), 313 | ); 314 | assert_eq!( 315 | world 316 | .entry(translation_and_nus) 317 | .unwrap() 318 | .get_component::() 319 | .unwrap() 320 | .0, 321 | t.to_homogeneous().prepend_nonuniform_scaling(&nus.0), 322 | ); 323 | assert_eq!( 324 | world 325 | .entry(rotation_scale) 326 | .unwrap() 327 | .get_component::() 328 | .unwrap() 329 | .0, 330 | r.to_homogeneous().prepend_scaling(s.0) 331 | ); 332 | assert_eq!( 333 | world 334 | .entry(rotation_nus) 335 | .unwrap() 336 | .get_component::() 337 | .unwrap() 338 | .0, 339 | r.to_homogeneous().prepend_nonuniform_scaling(&nus.0) 340 | ); 341 | assert_eq!( 342 | world 343 | .entry(translation_rotation_scale) 344 | .unwrap() 345 | .get_component::() 346 | .unwrap() 347 | .0, 348 | r.to_homogeneous() 349 | .append_translation(&t.vector) 350 | .prepend_scaling(s.0) 351 | ); 352 | assert_eq!( 353 | world 354 | .entry(translation_rotation_nus) 355 | .unwrap() 356 | .get_component::() 357 | .unwrap() 358 | .0, 359 | r.to_homogeneous() 360 | .append_translation(&t.vector) 361 | .prepend_nonuniform_scaling(&nus.0) 362 | ); 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /src/local_to_world_propagate_system.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use crate::{ 3 | components::*, 4 | ecs::{ 5 | systems::{CommandBuffer, ParallelRunnable}, 6 | world::SubWorld, 7 | *, 8 | }, 9 | }; 10 | 11 | pub fn build() -> impl ParallelRunnable { 12 | SystemBuilder::<()>::new("LocalToWorldPropagateSystem") 13 | // Entities with a `Children` and `LocalToWorld` but NOT a `Parent` (ie those that are 14 | // roots of a hierarchy). 15 | .with_query(<(Read, Read)>::query().filter(!component::())) 16 | .read_component::() 17 | .read_component::() 18 | .build(move |commands, world, _resource, query| { 19 | for (children, local_to_world) in query.iter(world) { 20 | for child in children.0.iter() { 21 | propagate_recursive(*local_to_world, world, *child, commands); 22 | } 23 | } 24 | }) 25 | } 26 | 27 | fn propagate_recursive( 28 | parent_local_to_world: LocalToWorld, 29 | world: &SubWorld, 30 | entity: Entity, 31 | commands: &mut CommandBuffer, 32 | ) { 33 | log::trace!("Updating LocalToWorld for {:?}", entity); 34 | let local_to_parent = { 35 | if let Some(local_to_parent) = world 36 | .entry_ref(entity) 37 | .and_then(|entry| entry.into_component::().ok()) 38 | { 39 | *local_to_parent 40 | } else { 41 | log::warn!( 42 | "Entity {:?} is a child in the hierarchy but does not have a LocalToParent", 43 | entity 44 | ); 45 | return; 46 | } 47 | }; 48 | 49 | let new_local_to_world = LocalToWorld(parent_local_to_world.0 * local_to_parent.0); 50 | commands.add_component(entity, new_local_to_world); 51 | 52 | // Collect children 53 | let children = if let Some(entry) = world.entry_ref(entity) { 54 | entry 55 | .get_component::() 56 | .map(|e| e.0.iter().cloned().collect::>()) 57 | .unwrap_or_default() 58 | } else { 59 | Vec::default() 60 | }; 61 | 62 | for child in children { 63 | propagate_recursive(new_local_to_world, world, child, commands); 64 | } 65 | } 66 | 67 | #[cfg(test)] 68 | mod test { 69 | use super::*; 70 | use crate::{ 71 | local_to_parent_system, local_to_world_propagate_system, local_to_world_system, 72 | missing_previous_parent_system, parent_update_system, 73 | }; 74 | 75 | #[test] 76 | fn did_propagate() { 77 | let _ = env_logger::builder().is_test(true).try_init(); 78 | 79 | let mut resources = Resources::default(); 80 | let mut world = World::default(); 81 | 82 | let mut schedule = Schedule::builder() 83 | .add_system(missing_previous_parent_system::build()) 84 | .flush() 85 | .add_system(parent_update_system::build()) 86 | .flush() 87 | .add_system(local_to_parent_system::build()) 88 | .flush() 89 | .add_system(local_to_world_system::build()) 90 | .flush() 91 | .add_system(local_to_world_propagate_system::build()) 92 | .build(); 93 | 94 | // Root entity 95 | let parent = world.push((Translation::new(1.0, 0.0, 0.0), LocalToWorld::identity())); 96 | 97 | let children = world.extend(vec![ 98 | ( 99 | Translation::new(0.0, 2.0, 0.0), 100 | LocalToParent::identity(), 101 | LocalToWorld::identity(), 102 | ), 103 | ( 104 | Translation::new(0.0, 0.0, 3.0), 105 | LocalToParent::identity(), 106 | LocalToWorld::identity(), 107 | ), 108 | ]); 109 | let (e1, e2) = (children[0], children[1]); 110 | 111 | // Parent `e1` and `e2` to `parent`. 112 | world.entry(e1).unwrap().add_component(Parent(parent)); 113 | world.entry(e2).unwrap().add_component(Parent(parent)); 114 | 115 | // Run systems 116 | schedule.execute(&mut world, &mut resources); 117 | 118 | assert_eq!( 119 | world 120 | .entry(e1) 121 | .unwrap() 122 | .get_component::() 123 | .unwrap() 124 | .0, 125 | Translation::new(1.0, 0.0, 0.0).to_homogeneous() 126 | * Translation::new(0.0, 2.0, 0.0).to_homogeneous() 127 | ); 128 | 129 | assert_eq!( 130 | world 131 | .entry(e2) 132 | .unwrap() 133 | .get_component::() 134 | .unwrap() 135 | .0, 136 | Translation::new(1.0, 0.0, 0.0).to_homogeneous() 137 | * Translation::new(0.0, 0.0, 3.0).to_homogeneous() 138 | ); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/local_to_world_system.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use crate::{ 3 | components::*, 4 | ecs::{systems::ParallelRunnable, *}, 5 | math::Matrix4, 6 | }; 7 | 8 | pub fn build() -> impl ParallelRunnable { 9 | SystemBuilder::<()>::new("LocalToWorldUpdateSystem") 10 | // Translation 11 | .with_query(<(Write, Read)>::query().filter( 12 | !component::() 13 | & !component::() 14 | & !component::() 15 | & !component::() 16 | & (maybe_changed::()), 17 | )) 18 | // Rotation 19 | .with_query(<(Write, Read)>::query().filter( 20 | !component::() 21 | & !component::() 22 | & !component::() 23 | & !component::() 24 | & (maybe_changed::()), 25 | )) 26 | // Scale 27 | .with_query(<(Write, Read)>::query().filter( 28 | !component::() 29 | & !component::() 30 | & !component::() 31 | & !component::() 32 | & (maybe_changed::()), 33 | )) 34 | // NonUniformScale 35 | .with_query( 36 | <(Write, Read)>::query().filter( 37 | !component::() 38 | & !component::() 39 | & !component::() 40 | & !component::() 41 | & (maybe_changed::()), 42 | ), 43 | ) 44 | // Translation + Rotation 45 | .with_query( 46 | <(Write, Read, Read)>::query().filter( 47 | !component::() 48 | & !component::() 49 | & !component::() 50 | & (maybe_changed::() | maybe_changed::()), 51 | ), 52 | ) 53 | // Translation + Scale 54 | .with_query( 55 | <(Write, Read, Read)>::query().filter( 56 | !component::() 57 | & !component::() 58 | & !component::() 59 | & (maybe_changed::() | maybe_changed::()), 60 | ), 61 | ) 62 | // Translation + NonUniformScale 63 | .with_query( 64 | <( 65 | Write, 66 | Read, 67 | Read, 68 | )>::query() 69 | .filter( 70 | !component::() 71 | & !component::() 72 | & !component::() 73 | & (maybe_changed::() | maybe_changed::()), 74 | ), 75 | ) 76 | // Rotation + Scale 77 | .with_query( 78 | <(Write, Read, Read)>::query().filter( 79 | !component::() 80 | & !component::() 81 | & !component::() 82 | & (maybe_changed::() | maybe_changed::()), 83 | ), 84 | ) 85 | // Rotation + NonUniformScale 86 | .with_query( 87 | <(Write, Read, Read)>::query().filter( 88 | !component::() 89 | & !component::() 90 | & !component::() 91 | & (maybe_changed::() | maybe_changed::()), 92 | ), 93 | ) 94 | // Translation + Rotation + Scale 95 | .with_query( 96 | <( 97 | Write, 98 | Read, 99 | Read, 100 | Read, 101 | )>::query() 102 | .filter( 103 | !component::() 104 | & !component::() 105 | & (maybe_changed::() 106 | | maybe_changed::() 107 | | maybe_changed::()), 108 | ), 109 | ) 110 | // Translation + Rotation + NonUniformScale 111 | .with_query( 112 | <( 113 | Write, 114 | Read, 115 | Read, 116 | Read, 117 | )>::query() 118 | .filter( 119 | !component::() 120 | & !component::() 121 | & (maybe_changed::() 122 | | maybe_changed::() 123 | | maybe_changed::()), 124 | ), 125 | ) 126 | // Just to issue warnings: Scale + NonUniformScale 127 | .with_query( 128 | <( 129 | Entity, 130 | Read, 131 | Read, 132 | Read, 133 | )>::query() 134 | .filter(!component::()), 135 | ) 136 | .build(move |_commands, world, _, queries| { 137 | let (a, b, c, d, e, f, g, h, i, j, k, l) = queries; 138 | rayon::scope(|s| { 139 | s.spawn(|_| unsafe { 140 | // Translation 141 | a.for_each_unchecked(world, |(ltw, translation)| { 142 | *ltw = LocalToWorld(translation.to_homogeneous()); 143 | }); 144 | }); 145 | s.spawn(|_| unsafe { 146 | // Rotation 147 | b.for_each_unchecked(world, |(ltw, rotation)| { 148 | *ltw = LocalToWorld(rotation.to_homogeneous()); 149 | }); 150 | }); 151 | s.spawn(|_| unsafe { 152 | // Scale 153 | c.for_each_unchecked(world, |(ltw, scale)| { 154 | *ltw = LocalToWorld(Matrix4::new_scaling(scale.0)); 155 | }); 156 | }); 157 | s.spawn(|_| unsafe { 158 | // NonUniformScale 159 | d.for_each_unchecked(world, |(ltw, non_uniform_scale)| { 160 | *ltw = LocalToWorld(Matrix4::new_nonuniform_scaling(&non_uniform_scale.0)); 161 | }); 162 | }); 163 | s.spawn(|_| unsafe { 164 | // Translation + Rotation 165 | e.for_each_unchecked(world, |(ltw, translation, rotation)| { 166 | *ltw = LocalToWorld( 167 | rotation 168 | .to_homogeneous() 169 | .append_translation(&translation.vector), 170 | ); 171 | }); 172 | }); 173 | s.spawn(|_| unsafe { 174 | // Translation + Scale 175 | f.for_each_unchecked(world, |(ltw, translation, scale)| { 176 | *ltw = LocalToWorld(translation.to_homogeneous().prepend_scaling(scale.0)); 177 | }); 178 | }); 179 | s.spawn(|_| unsafe { 180 | // Translation + NonUniformScale 181 | g.for_each_unchecked(world, |(ltw, translation, non_uniform_scale)| { 182 | *ltw = LocalToWorld( 183 | translation 184 | .to_homogeneous() 185 | .prepend_nonuniform_scaling(&non_uniform_scale.0), 186 | ); 187 | }); 188 | }); 189 | s.spawn(|_| unsafe { 190 | // Rotation + Scale 191 | h.for_each_unchecked(world, |(ltw, rotation, scale)| { 192 | *ltw = LocalToWorld(rotation.to_homogeneous().prepend_scaling(scale.0)); 193 | }); 194 | }); 195 | s.spawn(|_| unsafe { 196 | // Rotation + NonUniformScale 197 | i.for_each_unchecked(world, |(ltw, rotation, non_uniform_scale)| { 198 | *ltw = LocalToWorld( 199 | rotation 200 | .to_homogeneous() 201 | .prepend_nonuniform_scaling(&non_uniform_scale.0), 202 | ); 203 | }); 204 | }); 205 | s.spawn(|_| unsafe { 206 | // Translation + Rotation + Scale 207 | j.for_each_unchecked(world, |(ltw, translation, rotation, scale)| { 208 | *ltw = LocalToWorld( 209 | rotation 210 | .to_homogeneous() 211 | .append_translation(&translation.vector) 212 | .prepend_scaling(scale.0), 213 | ); 214 | }); 215 | }); 216 | s.spawn(|_| unsafe { 217 | // Translation + Rotation + NonUniformScale 218 | k.for_each_unchecked( 219 | world, 220 | |(ltw, translation, rotation, non_uniform_scale)| { 221 | *ltw = LocalToWorld( 222 | rotation 223 | .to_homogeneous() 224 | .append_translation(&translation.vector) 225 | .prepend_nonuniform_scaling(&non_uniform_scale.0), 226 | ); 227 | }, 228 | ); 229 | }); 230 | 231 | // Just to issue warnings: Scale + NonUniformScale 232 | l.iter(world) 233 | .for_each(|(entity, mut _ltw, _scale, _non_uniform_scale)| { 234 | log::warn!( 235 | "Entity {:?} has both a Scale and NonUniformScale component.", 236 | entity 237 | ); 238 | }); 239 | }); 240 | }) 241 | } 242 | 243 | #[cfg(test)] 244 | mod test { 245 | use super::*; 246 | 247 | #[test] 248 | fn correct_world_transformation() { 249 | let _ = env_logger::builder().is_test(true).try_init(); 250 | 251 | let mut resources = Resources::default(); 252 | let mut world = World::default(); 253 | let mut schedule = Schedule::builder().add_system(build()).build(); 254 | 255 | let ltw = LocalToWorld::identity(); 256 | let t = Translation::new(1.0, 2.0, 3.0); 257 | let r = Rotation::from_euler_angles(1.0, 2.0, 3.0); 258 | let s = Scale(2.0); 259 | let nus = NonUniformScale::new(1.0, 2.0, 3.0); 260 | 261 | // Add every combination of transform types. 262 | let translation = world.push((ltw, t)); 263 | let rotation = world.push((ltw, r)); 264 | let scale = world.push((ltw, s)); 265 | let non_uniform_scale = world.push((ltw, nus)); 266 | let translation_and_rotation = world.push((ltw, t, r)); 267 | let translation_and_scale = world.push((ltw, t, s)); 268 | let translation_and_nus = world.push((ltw, t, nus)); 269 | let rotation_scale = world.push((ltw, r, s)); 270 | let rotation_nus = world.push((ltw, r, nus)); 271 | let translation_rotation_scale = world.push((ltw, t, r, s)); 272 | let translation_rotation_nus = world.push((ltw, t, r, nus)); 273 | 274 | // Run the system 275 | schedule.execute(&mut world, &mut resources); 276 | 277 | // Verify that each was transformed correctly. 278 | assert_eq!( 279 | world 280 | .entry(translation) 281 | .unwrap() 282 | .get_component::() 283 | .unwrap() 284 | .0, 285 | t.to_homogeneous() 286 | ); 287 | assert_eq!( 288 | world 289 | .entry(rotation) 290 | .unwrap() 291 | .get_component::() 292 | .unwrap() 293 | .0, 294 | r.to_homogeneous() 295 | ); 296 | assert_eq!( 297 | world 298 | .entry(scale) 299 | .unwrap() 300 | .get_component::() 301 | .unwrap() 302 | .0, 303 | Matrix4::new_scaling(s.0), 304 | ); 305 | assert_eq!( 306 | world 307 | .entry(non_uniform_scale) 308 | .unwrap() 309 | .get_component::() 310 | .unwrap() 311 | .0, 312 | Matrix4::new_nonuniform_scaling(&nus.0), 313 | ); 314 | assert_eq!( 315 | world 316 | .entry(translation_and_rotation) 317 | .unwrap() 318 | .get_component::() 319 | .unwrap() 320 | .0, 321 | r.to_homogeneous().append_translation(&t.vector), 322 | ); 323 | assert_eq!( 324 | world 325 | .entry(translation_and_scale) 326 | .unwrap() 327 | .get_component::() 328 | .unwrap() 329 | .0, 330 | t.to_homogeneous().prepend_scaling(s.0), 331 | ); 332 | assert_eq!( 333 | world 334 | .entry(translation_and_nus) 335 | .unwrap() 336 | .get_component::() 337 | .unwrap() 338 | .0, 339 | t.to_homogeneous().prepend_nonuniform_scaling(&nus.0), 340 | ); 341 | assert_eq!( 342 | world 343 | .entry(rotation_scale) 344 | .unwrap() 345 | .get_component::() 346 | .unwrap() 347 | .0, 348 | r.to_homogeneous().prepend_scaling(s.0) 349 | ); 350 | assert_eq!( 351 | world 352 | .entry(rotation_nus) 353 | .unwrap() 354 | .get_component::() 355 | .unwrap() 356 | .0, 357 | r.to_homogeneous().prepend_nonuniform_scaling(&nus.0) 358 | ); 359 | assert_eq!( 360 | world 361 | .entry(translation_rotation_scale) 362 | .unwrap() 363 | .get_component::() 364 | .unwrap() 365 | .0, 366 | r.to_homogeneous() 367 | .append_translation(&t.vector) 368 | .prepend_scaling(s.0) 369 | ); 370 | assert_eq!( 371 | world 372 | .entry(translation_rotation_nus) 373 | .unwrap() 374 | .get_component::() 375 | .unwrap() 376 | .0, 377 | r.to_homogeneous() 378 | .append_translation(&t.vector) 379 | .prepend_nonuniform_scaling(&nus.0) 380 | ); 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /src/missing_previous_parent_system.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | components::*, 3 | ecs::{systems::ParallelRunnable, *}, 4 | }; 5 | 6 | pub fn build() -> impl ParallelRunnable { 7 | SystemBuilder::<()>::new("MissingPreviousParentSystem") 8 | // Entities with missing `PreviousParent` 9 | .with_query(<(Entity, Read)>::query().filter( 10 | component::() 11 | & component::() 12 | & !component::(), 13 | )) 14 | .build(move |commands, world, _resource, query| { 15 | // Add missing `PreviousParent` components 16 | for (entity, _parent) in query.iter(world) { 17 | log::trace!("Adding missing PreviousParent to {:?}", entity); 18 | commands.add_component(*entity, PreviousParent(None)); 19 | } 20 | }) 21 | } 22 | 23 | #[cfg(test)] 24 | mod test { 25 | use super::*; 26 | 27 | #[test] 28 | fn previous_parent_added() { 29 | let _ = env_logger::builder().is_test(true).try_init(); 30 | 31 | let mut resources = Resources::default(); 32 | let mut world = World::default(); 33 | 34 | let mut schedule = Schedule::builder().add_system(build()).build(); 35 | 36 | let e1 = world.push(( 37 | Translation::identity(), 38 | LocalToParent::identity(), 39 | LocalToWorld::identity(), 40 | )); 41 | 42 | let e2 = world.push(( 43 | Translation::identity(), 44 | LocalToParent::identity(), 45 | LocalToWorld::identity(), 46 | Parent(e1), 47 | )); 48 | 49 | schedule.execute(&mut world, &mut resources); 50 | 51 | assert_eq!( 52 | world 53 | .entry(e1) 54 | .unwrap() 55 | .get_component::() 56 | .is_ok(), 57 | false 58 | ); 59 | 60 | assert_eq!( 61 | world 62 | .entry(e2) 63 | .unwrap() 64 | .get_component::() 65 | .is_ok(), 66 | true 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/parent_update_system.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use crate::{ 3 | components::*, 4 | ecs::{systems::ParallelRunnable, *}, 5 | }; 6 | use smallvec::SmallVec; 7 | use std::collections::HashMap; 8 | 9 | pub fn build() -> impl ParallelRunnable { 10 | SystemBuilder::<()>::new("ParentUpdateSystem") 11 | // Entities with a removed `Parent` 12 | .with_query(<(Entity, Read)>::query().filter(!component::())) 13 | // Entities with a changed `Parent` 14 | .with_query( 15 | <(Entity, Read, Write)>::query().filter( 16 | component::() 17 | & component::() 18 | & maybe_changed::(), 19 | ), 20 | ) 21 | // Deleted Parents (ie Entities with `Children` and without a `LocalToWorld`). 22 | .with_query(<(Entity, Read)>::query().filter(!component::())) 23 | .write_component::() 24 | .build(move |commands, world, _resource, queries| { 25 | // Entities with a missing `Parent` (ie. ones that have a `PreviousParent`), remove 26 | // them from the `Children` of the `PreviousParent`. 27 | let (ref mut left, ref mut right) = world.split::>(); 28 | for (entity, previous_parent) in queries.0.iter(right) { 29 | log::trace!("Parent was removed from {:?}", entity); 30 | if let Some(previous_parent_entity) = previous_parent.0 { 31 | if let Some(previous_parent_children) = left 32 | .entry_mut(previous_parent_entity) 33 | .and_then(|entry| entry.into_component_mut::().ok()) 34 | { 35 | log::trace!(" > Removing {:?} from it's prev parent's children", entity); 36 | previous_parent_children.0.retain(|e| e != entity); 37 | } 38 | } 39 | } 40 | 41 | // Tracks all newly created `Children` Components this frame. 42 | let mut children_additions = 43 | HashMap::>::with_capacity(16); 44 | 45 | // Entities with a changed Parent (that also have a PreviousParent, even if None) 46 | for (entity, parent, previous_parent) in queries.1.iter_mut(right) { 47 | log::trace!("Parent changed for {:?}", entity); 48 | 49 | // If the `PreviousParent` is not None. 50 | if let Some(previous_parent_entity) = previous_parent.0 { 51 | // New and previous point to the same Entity, carry on, nothing to see here. 52 | if previous_parent_entity == parent.0 { 53 | log::trace!(" > But the previous parent is the same, ignoring..."); 54 | continue; 55 | } 56 | 57 | // Remove from `PreviousParent.Children`. 58 | if let Some(previous_parent_children) = left 59 | .entry_mut(previous_parent_entity) 60 | .and_then(|entry| entry.into_component_mut::().ok()) 61 | { 62 | log::trace!(" > Removing {:?} from prev parent's children", entity); 63 | previous_parent_children.0.retain(|e| e != entity); 64 | } 65 | } 66 | 67 | // Set `PreviousParent = Parent`. 68 | *previous_parent = PreviousParent(Some(parent.0)); 69 | 70 | // Add to the parent's `Children` (either the real component, or 71 | // `children_additions`). 72 | log::trace!("Adding {:?} to it's new parent {:?}", entity, parent.0); 73 | if let Some(new_parent_children) = left 74 | .entry_mut(parent.0) 75 | .and_then(|entry| entry.into_component_mut::().ok()) 76 | { 77 | // This is the parent 78 | log::trace!( 79 | " > The new parent {:?} already has a `Children`, adding to it.", 80 | parent.0 81 | ); 82 | new_parent_children.0.push(*entity); 83 | } else { 84 | // The parent doesn't have a children entity, lets add it 85 | log::trace!( 86 | "The new parent {:?} doesn't yet have `Children` component.", 87 | parent.0 88 | ); 89 | children_additions 90 | .entry(parent.0) 91 | .or_insert_with(Default::default) 92 | .push(*entity); 93 | } 94 | } 95 | 96 | // Deleted `Parents` (ie. Entities with a `Children` but no `LocalToWorld`). 97 | for (entity, children) in queries.2.iter(world) { 98 | log::trace!("The entity {:?} doesn't have a LocalToWorld", entity); 99 | if children_additions.remove(&entity).is_none() { 100 | log::trace!(" > It needs to be remove from the ECS."); 101 | for child_entity in children.0.iter() { 102 | commands.remove_component::(*child_entity); 103 | commands.remove_component::(*child_entity); 104 | commands.remove_component::(*child_entity); 105 | } 106 | commands.remove_component::(*entity); 107 | } else { 108 | log::trace!(" > It was a new addition, removing it from additions map"); 109 | } 110 | } 111 | 112 | // Flush the `children_additions` to the command buffer. It is stored separate to 113 | // collect multiple new children that point to the same parent into the same 114 | // SmallVec, and to prevent redundant add+remove operations. 115 | children_additions.iter().for_each(|(k, v)| { 116 | log::trace!( 117 | "Flushing: Entity {:?} adding `Children` component {:?}", 118 | k, 119 | v 120 | ); 121 | commands.add_component(*k, Children::with(v)); 122 | }); 123 | }) 124 | } 125 | 126 | #[cfg(test)] 127 | mod test { 128 | use super::*; 129 | use crate::missing_previous_parent_system; 130 | 131 | #[test] 132 | fn correct_children() { 133 | let _ = env_logger::builder().is_test(true).try_init(); 134 | 135 | let mut resources = Resources::default(); 136 | let mut world = World::default(); 137 | 138 | let mut schedule = Schedule::builder() 139 | .add_system(missing_previous_parent_system::build()) 140 | .flush() 141 | .add_system(build()) 142 | .build(); 143 | 144 | // Add parent entities 145 | let parent = world.push((Translation::identity(), LocalToWorld::identity())); 146 | let children = world.extend(vec![ 147 | ( 148 | Translation::identity(), 149 | LocalToParent::identity(), 150 | LocalToWorld::identity(), 151 | ), 152 | ( 153 | Translation::identity(), 154 | LocalToParent::identity(), 155 | LocalToWorld::identity(), 156 | ), 157 | ]); 158 | let (e1, e2) = (children[0], children[1]); 159 | 160 | // Parent `e1` and `e2` to `parent`. 161 | world.entry(e1).unwrap().add_component(Parent(parent)); 162 | world.entry(e2).unwrap().add_component(Parent(parent)); 163 | 164 | schedule.execute(&mut world, &mut resources); 165 | 166 | assert_eq!( 167 | world 168 | .entry(parent) 169 | .unwrap() 170 | .get_component::() 171 | .unwrap() 172 | .0 173 | .iter() 174 | .cloned() 175 | .collect::>(), 176 | vec![e1, e2] 177 | ); 178 | 179 | // Parent `e1` to `e2`. 180 | world 181 | .entry_mut(e1) 182 | .unwrap() 183 | .get_component_mut::() 184 | .unwrap() 185 | .0 = e2; 186 | 187 | // Run the systems 188 | schedule.execute(&mut world, &mut resources); 189 | 190 | assert_eq!( 191 | world 192 | .entry(parent) 193 | .unwrap() 194 | .get_component::() 195 | .unwrap() 196 | .0 197 | .iter() 198 | .cloned() 199 | .collect::>(), 200 | vec![e2] 201 | ); 202 | 203 | assert_eq!( 204 | world 205 | .entry(e2) 206 | .unwrap() 207 | .get_component::() 208 | .unwrap() 209 | .0 210 | .iter() 211 | .cloned() 212 | .collect::>(), 213 | vec![e1] 214 | ); 215 | 216 | world.remove(e1); 217 | 218 | // Run the systems 219 | schedule.execute(&mut world, &mut resources); 220 | 221 | assert_eq!( 222 | world 223 | .entry(parent) 224 | .unwrap() 225 | .get_component::() 226 | .unwrap() 227 | .0 228 | .iter() 229 | .cloned() 230 | .collect::>(), 231 | vec![e2] 232 | ); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/transform_system_bundle.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | ecs::systems::ParallelRunnable, local_to_parent_system, local_to_world_propagate_system, 3 | local_to_world_system, missing_previous_parent_system, parent_update_system, 4 | }; 5 | 6 | pub fn build() -> Vec> { 7 | let mut all_systems = Vec::>::with_capacity(5); 8 | all_systems.push(Box::new(missing_previous_parent_system::build())); 9 | all_systems.push(Box::new(parent_update_system::build())); 10 | all_systems.push(Box::new(local_to_parent_system::build())); 11 | all_systems.push(Box::new(local_to_world_system::build())); 12 | all_systems.push(Box::new(local_to_world_propagate_system::build())); 13 | 14 | all_systems 15 | } 16 | --------------------------------------------------------------------------------