├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── assets ├── Fox.glb ├── README.md └── RecursiveSkeletons.glb ├── benches └── benches.rs ├── dev └── dev.rs ├── examples ├── cpu_skinning.rs ├── many_foxes.rs ├── random.rs └── showcase.rs ├── notes └── Performance.md ├── rustfmt.toml ├── src ├── debug.rs └── lib.rs └── tests └── tests.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | CARGO_INCREMENTAL: 0 10 | CARGO_PROFILE_TEST_DEBUG: 0 11 | CARGO_PROFILE_DEV_DEBUG: 0 12 | 13 | jobs: 14 | all: 15 | name: All 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: dtolnay/rust-toolchain@stable 20 | - uses: Swatinem/rust-cache@v2 21 | - name: Update Packages 22 | run: sudo apt-get update -yq 23 | - name: Install dependencies 24 | run: sudo apt-get install -yq --no-install-recommends libudev-dev libasound2-dev libxcb-composite0-dev 25 | - name: Run cargo fmt 26 | run: cargo fmt --all -- --check 27 | - name: Run cargo clippy 28 | run: cargo clippy --all-features --all-targets -- -D warnings 29 | - name: Run cargo check 30 | run: cargo check --all-features --all-targets 31 | - name: Run cargo test 32 | run: cargo test 33 | - name: Run cargo bench 34 | run: cargo bench --bench benches -- --quick 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_mod_skinned_aabb" 3 | version = "0.2.0" 4 | description = "A Bevy plugin that automatically calculates AABBs for skinned meshes" 5 | authors = ["Greeble "] 6 | repository = "https://github.com/greeble-dev/bevy_mod_skinned_aabb" 7 | license = "MIT OR Apache-2.0" 8 | edition = "2024" 9 | keywords = ["bevy"] 10 | categories = ["game-development"] 11 | include = ["/src", "/LICENSE-MIT", "/LICENSE-APACHE", "/README.md"] 12 | 13 | [dependencies] 14 | bevy_app = { version = "0.16", default-features = false } 15 | bevy_asset = { version = "0.16", default-features = false } 16 | bevy_color = { version = "0.16", default-features = false } 17 | bevy_derive = { version = "0.16", default-features = false } 18 | bevy_ecs = { version = "0.16", default-features = false } 19 | bevy_gizmos = { version = "0.16", default-features = false } 20 | bevy_log = { version = "0.16", default-features = false } 21 | bevy_math = { version = "0.16", default-features = false } 22 | bevy_mesh = { version = "0.16", default-features = false } 23 | bevy_reflect = { version = "0.16", default-features = false } 24 | bevy_render = { version = "0.16", default-features = false } 25 | bevy_transform = { version = "0.16", default-features = false } 26 | 27 | [dev-dependencies] 28 | criterion = { version = "0.5", default-features = false, features = [ 29 | "cargo_bench_support", 30 | ] } 31 | bevy = "0.16" 32 | rand = "0.8" 33 | 34 | [features] 35 | # Enable performance tracing (https://github.com/bevyengine/bevy/blob/main/docs/profiling.md). 36 | trace = [] 37 | 38 | [[bench]] 39 | name = "benches" 40 | harness = false 41 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the 13 | copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other 16 | entities that control, are controlled by, or are under common control with 17 | that entity. For the purposes of this definition, "control" means (i) the 18 | power, direct or indirect, to cause the direction or management of such 19 | entity, whether by contract or otherwise, or (ii) ownership of 20 | fifty percent (50%) or more of the outstanding shares, or (iii) beneficial 21 | ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity exercising 24 | permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation source, 28 | and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical transformation 31 | or translation of a Source form, including but not limited to compiled 32 | object code, generated documentation, and conversions to 33 | other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or Object 36 | form, made available under the License, as indicated by a copyright notice 37 | that is included in or attached to the work (an example is provided in the 38 | Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object form, 41 | that is based on (or derived from) the Work and for which the editorial 42 | revisions, annotations, elaborations, or other modifications represent, 43 | as a whole, an original work of authorship. For the purposes of this 44 | License, Derivative Works shall not include works that remain separable 45 | from, or merely link (or bind by name) to the interfaces of, the Work and 46 | Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including the original 49 | version of the Work and any modifications or additions to that Work or 50 | Derivative Works thereof, that is intentionally submitted to Licensor for 51 | inclusion in the Work by the copyright owner or by an individual or 52 | Legal Entity authorized to submit on behalf of the copyright owner. 53 | For the purposes of this definition, "submitted" means any form of 54 | electronic, verbal, or written communication sent to the Licensor or its 55 | representatives, including but not limited to communication on electronic 56 | mailing lists, source code control systems, and issue tracking systems 57 | that are managed by, or on behalf of, the Licensor for the purpose of 58 | discussing and improving the Work, but excluding communication that is 59 | conspicuously marked or otherwise designated in writing by the copyright 60 | owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity on 63 | behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. 67 | 68 | Subject to the terms and conditions of this License, each Contributor 69 | hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, 70 | royalty-free, irrevocable copyright license to reproduce, prepare 71 | Derivative Works of, publicly display, publicly perform, sublicense, 72 | and distribute the Work and such Derivative Works in 73 | Source or Object form. 74 | 75 | 3. Grant of Patent License. 76 | 77 | Subject to the terms and conditions of this License, each Contributor 78 | hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, 79 | royalty-free, irrevocable (except as stated in this section) patent 80 | license to make, have made, use, offer to sell, sell, import, and 81 | otherwise transfer the Work, where such license applies only to those 82 | patent claims licensable by such Contributor that are necessarily 83 | infringed by their Contribution(s) alone or by combination of their 84 | Contribution(s) with the Work to which such Contribution(s) was submitted. 85 | If You institute patent litigation against any entity (including a 86 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 87 | Contribution incorporated within the Work constitutes direct or 88 | contributory patent infringement, then any patent licenses granted to 89 | You under this License for that Work shall terminate as of the date such 90 | litigation is filed. 91 | 92 | 4. Redistribution. 93 | 94 | You may reproduce and distribute copies of the Work or Derivative Works 95 | thereof in any medium, with or without modifications, and in Source or 96 | Object form, provided that You meet the following conditions: 97 | 98 | 1. You must give any other recipients of the Work or Derivative Works a 99 | copy of this License; and 100 | 101 | 2. You must cause any modified files to carry prominent notices stating 102 | that You changed the files; and 103 | 104 | 3. You must retain, in the Source form of any Derivative Works that You 105 | distribute, all copyright, patent, trademark, and attribution notices from 106 | the Source form of the Work, excluding those notices that do not pertain 107 | to any part of the Derivative Works; and 108 | 109 | 4. If the Work includes a "NOTICE" text file as part of its distribution, 110 | then any Derivative Works that You distribute must include a readable copy 111 | of the attribution notices contained within such NOTICE file, excluding 112 | those notices that do not pertain to any part of the Derivative Works, 113 | in at least one of the following places: within a NOTICE text file 114 | distributed as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, within a 116 | display generated by the Derivative Works, if and wherever such 117 | third-party notices normally appear. The contents of the NOTICE file are 118 | for informational purposes only and do not modify the License. 119 | You may add Your own attribution notices within Derivative Works that You 120 | distribute, alongside or as an addendum to the NOTICE text from the Work, 121 | provided that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and may 125 | provide additional or different license terms and conditions for use, 126 | reproduction, or distribution of Your modifications, or for any such 127 | Derivative Works as a whole, provided Your use, reproduction, and 128 | distribution of the Work otherwise complies with the conditions 129 | stated in this License. 130 | 131 | 5. Submission of Contributions. 132 | 133 | Unless You explicitly state otherwise, any Contribution intentionally 134 | submitted for inclusion in the Work by You to the Licensor shall be under 135 | the terms and conditions of this License, without any additional 136 | terms or conditions. Notwithstanding the above, nothing herein shall 137 | supersede or modify the terms of any separate license agreement you may 138 | have executed with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. 141 | 142 | This License does not grant permission to use the trade names, trademarks, 143 | service marks, or product names of the Licensor, except as required for 144 | reasonable and customary use in describing the origin of the Work and 145 | reproducing the content of the NOTICE file. 146 | 147 | 7. Disclaimer of Warranty. 148 | 149 | Unless required by applicable law or agreed to in writing, Licensor 150 | provides the Work (and each Contributor provides its Contributions) 151 | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 152 | either express or implied, including, without limitation, any warranties 153 | or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS 154 | FOR A PARTICULAR PURPOSE. You are solely responsible for determining the 155 | appropriateness of using or redistributing the Work and assume any risks 156 | associated with Your exercise of permissions under this License. 157 | 158 | 8. Limitation of Liability. 159 | 160 | In no event and under no legal theory, whether in tort 161 | (including negligence), contract, or otherwise, unless required by 162 | applicable law (such as deliberate and grossly negligent acts) or agreed 163 | to in writing, shall any Contributor be liable to You for damages, 164 | including any direct, indirect, special, incidental, or consequential 165 | damages of any character arising as a result of this License or out of 166 | the use or inability to use the Work (including but not limited to damages 167 | for loss of goodwill, work stoppage, computer failure or malfunction, 168 | or any and all other commercial damages or losses), even if such 169 | Contributor has been advised of the possibility of such damages. 170 | 171 | 9. Accepting Warranty or Additional Liability. 172 | 173 | While redistributing the Work or Derivative Works thereof, You may choose 174 | to offer, and charge a fee for, acceptance of support, warranty, 175 | indemnity, or other liability obligations and/or rights consistent with 176 | this License. However, in accepting such obligations, You may act only 177 | on Your own behalf and on Your sole responsibility, not on behalf of any 178 | other Contributor, and only if You agree to indemnify, defend, and hold 179 | each Contributor harmless for any liability incurred by, or claims 180 | asserted against, such Contributor by reason of your accepting any such 181 | warranty or additional liability. 182 | 183 | END OF TERMS AND CONDITIONS 184 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 12 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 13 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 14 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 17 | OR OTHER DEALINGS IN THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bevy_mod_skinned_aabb 2 | 3 | A [Bevy](https://github.com/bevyengine/bevy) plugin that automatically calculates AABBs for skinned meshes. 4 | 5 | https://github.com/user-attachments/assets/73d236da-43a8-4b63-a19e-f3625d374077 6 | 7 | The goal of the plugin is to [fix meshes disappearing due to incorrect AABBs](https://github.com/bevyengine/bevy/issues/4971). 8 | 9 | ## Quick Start 10 | 11 | To enable skinned AABBs in a Bevy 0.16 app: 12 | 13 | ```sh 14 | cargo add bevy_mod_skinned_aabb 15 | ``` 16 | 17 | ```rust 18 | use bevy_mod_skinned_aabb::prelude::*; 19 | 20 | fn main() { 21 | App::new() 22 | .add_plugins(DefaultPlugins) 23 | // Enable skinned AABBs. 24 | .add_plugins(SkinnedAabbPlugin) 25 | .run(); 26 | } 27 | ``` 28 | 29 | The plugin will automatically detect and update any skinned meshes that are added to the world. 30 | 31 | ## Limitations 32 | 33 | - Skinned AABBs require the meshes to be flagged as `RenderAssetUsages::MAIN_WORLD`. 34 | - This is enabled by default in most cases - if you're simply loading glTF 35 | files then you don't have to do anything. 36 | - If you're making custom meshes, check `Mesh::asset_usage` or the `asset_usage` parameter of `Mesh::new`. 37 | - Skinned AABBs do not account for blend shapes and vertex shader shenanigans. 38 | - Meshes that use these features may have incorrect AABBs. 39 | - Meshes that only use skinning are safe. 40 | - Skinned AABBs are conservative but not optimal. 41 | - They're conservative in that the AABB is guaranteed to contain the mesh's vertices. 42 | - But they're not optimal, in that the AABB may be larger than necessary. 43 | - Apps that use hundreds of different skinned mesh assets may have performance issues. 44 | - Each different asset adds some overhead to spawning mesh instances. 45 | - It's fine to spawn many instances of a small number of assets. 46 | - The AABBs might be wrong for one frame immediately after spawning. 47 | 48 | ## Bevy Compatibility 49 | 50 | | bevy | bevy_mod_skinned_aabb | 51 | |---------------|-----------------------| 52 | | `0.16.0` | `0.2` | 53 | | `0.16.0-rc.4` | `0.2.0-rc.4` | 54 | | `0.16.0-rc.1` | `0.2.0-rc.1` | 55 | | `0.15` | `0.1` | 56 | | `<=0.14` | Not supported | 57 | 58 | ## Examples 59 | 60 | ```sh 61 | git clone https://github.com/greeble-dev/bevy_mod_skinned_aabb 62 | cd bevy_mod_skinned_aabb 63 | 64 | # Show a variety of glTF and custom meshes. 65 | cargo run --example showcase 66 | 67 | # Stress test 1000 skinned meshes. 68 | cargo run --example many_foxes 69 | ``` 70 | 71 | ## FAQ 72 | 73 | ### What's the performance impact? 74 | 75 | The per-frame CPU cost of a skinned mesh increases by roughly 4%. The 76 | cost of loading a skinned mesh from a glTF increases by less than 1%. 77 | 78 | ### How can I see the AABBs? 79 | 80 | To see the mesh and joint AABBs in your own app, add `SkinnedAabbDebugPlugin`: 81 | 82 | ```rust 83 | use bevy_mod_skinned_aabb::prelude::*; 84 | 85 | fn main() { 86 | App::new() 87 | .add_plugins(DefaultPlugins) 88 | .add_plugins(( 89 | SkinnedAabbPlugin, 90 | // Enable debug rendering. 91 | SkinnedAabbDebugPlugin::enable_by_default(), 92 | )) 93 | .run(); 94 | } 95 | ``` 96 | 97 | The debug rendering will be enabled by default. You can also leave it disabled 98 | by default but enable it with keyboard shortcuts: 99 | 100 | ```rust 101 | use bevy_mod_skinned_aabb::prelude::*; 102 | 103 | fn main() { 104 | App::new() 105 | .add_plugins(DefaultPlugins) 106 | .add_plugins(( 107 | SkinnedAabbPlugin, 108 | // Add the debug rendering but leave it disabled by default. 109 | SkinnedAabbDebugPlugin::disable_by_default(), 110 | )) 111 | .add_systems( 112 | Update, 113 | ( 114 | // Press J to toggle joint AABBs. 115 | toggle_draw_joint_aabbs.run_if(input_just_pressed(KeyCode::KeyJ)), 116 | // Press M to toggle mesh AABBs. 117 | toggle_draw_mesh_aabbs.run_if(input_just_pressed(KeyCode::KeyM)), 118 | ), 119 | ) 120 | .run(); 121 | } 122 | ``` 123 | -------------------------------------------------------------------------------- /assets/Fox.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greeble-dev/bevy_mod_skinned_aabb/2386a9829e27261a2adbf70f7a8dc43046b6ccfb/assets/Fox.glb -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # Fox.glb 2 | 3 | - https://github.com/KhronosGroup/glTF-Sample-Assets/tree/main/Models/Fox 4 | - © 2014, Public. [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/legalcode) 5 | - PixelMannen for Model 6 | - © 2014, tomkranis. [CC BY 4.0 International](https://creativecommons.org/licenses/by/4.0/legalcode) 7 | - tomkranis for Rigging & Animation 8 | - © 2017, @AsoboStudio and @scurest. [CC BY 4.0 International](https://creativecommons.org/licenses/by/4.0/legalcode) 9 | - @AsoboStudio and @scurest for Conversion to glTF 10 | 11 | # RecursiveSkeletons.glb 12 | 13 | - https://github.com/KhronosGroup/glTF-Sample-Assets/blob/main/Models/RecursiveSkeletons 14 | - © 2021 SharpGLTF, CC-BY 4.0 https://creativecommons.org/licenses/by/4.0/ 15 | - Model by Vicente Penades. 16 | - © 2017, Cesium. [CC BY 4.0 International](https://creativecommons.org/licenses/by/4.0/legalcode) 17 | - Cesium for Everything -------------------------------------------------------------------------------- /assets/RecursiveSkeletons.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greeble-dev/bevy_mod_skinned_aabb/2386a9829e27261a2adbf70f7a8dc43046b6ccfb/assets/RecursiveSkeletons.glb -------------------------------------------------------------------------------- /benches/benches.rs: -------------------------------------------------------------------------------- 1 | #[path = "../dev/dev.rs"] 2 | mod dev; 3 | 4 | use bevy_asset::Assets; 5 | use bevy_ecs::{prelude::*, system::RunSystemOnce}; 6 | use bevy_math::{ 7 | Affine3A, Vec3, Vec3A, 8 | bounding::{Aabb3d, BoundingVolume}, 9 | }; 10 | use bevy_mesh::{Mesh, skinning::SkinnedMeshInverseBindposes}; 11 | use bevy_mod_skinned_aabb::{ 12 | PackedAabb3d, SkinnedAabbPluginSettings, aabb_transformed_by, create_skinned_aabbs, 13 | update_skinned_aabbs, 14 | }; 15 | use bevy_transform::prelude::*; 16 | use core::time::Duration; 17 | use criterion::{Bencher, Criterion, Throughput, black_box, criterion_group, criterion_main}; 18 | use dev::{ 19 | RandomSkinnedMeshType, create_dev_world, create_random_skinned_mesh_assets, 20 | spawn_random_skinned_mesh, 21 | }; 22 | use rand::{SeedableRng, rngs::StdRng}; 23 | use std::iter::repeat_with; 24 | 25 | #[derive(Resource, Copy, Clone)] 26 | struct MeshParams { 27 | num_assets: usize, 28 | num_meshes: usize, 29 | num_joints: usize, 30 | } 31 | 32 | #[inline(never)] 33 | fn core_inner(aabbs: &[PackedAabb3d], joints: &[Affine3A]) -> Aabb3d { 34 | let count = aabbs.len(); 35 | 36 | let mut t = Aabb3d { 37 | min: Vec3A::MAX, 38 | max: Vec3A::MIN, 39 | }; 40 | 41 | for index in 0..count { 42 | t = t.merge(&aabb_transformed_by(aabbs[index], joints[index])); 43 | } 44 | 45 | t 46 | } 47 | 48 | pub fn core(c: &mut Criterion) { 49 | let mut group = c.benchmark_group("core"); 50 | 51 | const COUNT: usize = (128 * 1024) / (size_of::() + size_of::()); 52 | 53 | group.throughput(Throughput::Elements(COUNT as u64)); 54 | 55 | let aabbs = &[PackedAabb3d { 56 | min: Vec3::ZERO, 57 | max: Vec3::ZERO, 58 | }; COUNT]; 59 | 60 | let joints = &[Affine3A::IDENTITY; COUNT]; 61 | 62 | group.bench_function(format!("count = {COUNT}"), |b| { 63 | b.iter(move || black_box(core_inner(aabbs, joints))) 64 | }); 65 | } 66 | 67 | fn create_meshes( 68 | mut commands: Commands, 69 | mut mesh_assets: ResMut>, 70 | mut inverse_bindposes_assets: ResMut>, 71 | params: Res, 72 | ) { 73 | let mut rng = StdRng::seed_from_u64(732935); 74 | let base_entity = commands.spawn(Transform::IDENTITY).id(); 75 | 76 | let assets = repeat_with(|| { 77 | create_random_skinned_mesh_assets( 78 | &mut mesh_assets, 79 | &mut inverse_bindposes_assets, 80 | &mut rng, 81 | RandomSkinnedMeshType::Hard, 82 | 1, 83 | params.num_joints, 84 | ) 85 | .ok() 86 | }) 87 | .take(params.num_assets) 88 | .flatten() 89 | .collect::>(); 90 | 91 | for entity_index in 0..params.num_meshes { 92 | spawn_random_skinned_mesh( 93 | &mut commands, 94 | &mut rng, 95 | base_entity, 96 | Transform::IDENTITY, 97 | &assets[entity_index % assets.len()], 98 | ); 99 | } 100 | } 101 | 102 | fn systems_internal( 103 | b: &mut Bencher, 104 | settings: SkinnedAabbPluginSettings, 105 | mesh_params: &MeshParams, 106 | ) { 107 | let world = &mut create_dev_world(settings); 108 | 109 | world.insert_resource(*mesh_params); 110 | 111 | world.run_system_once(create_meshes).unwrap(); 112 | world.run_system_once(create_skinned_aabbs).unwrap(); 113 | 114 | b.iter(move || world.run_system_cached(update_skinned_aabbs).unwrap()); 115 | } 116 | 117 | pub fn systems(c: &mut Criterion) { 118 | let mut group = c.benchmark_group("systems"); 119 | 120 | group.warm_up_time(Duration::from_millis(100)); 121 | group.measurement_time(Duration::from_millis(1000)); 122 | 123 | struct Combo { 124 | num_joints_total: usize, 125 | num_meshes: usize, 126 | } 127 | 128 | let combos = [ 129 | Combo { 130 | num_joints_total: 1_000, 131 | num_meshes: 100, 132 | }, 133 | Combo { 134 | num_joints_total: 10_000, 135 | num_meshes: 100, 136 | }, 137 | Combo { 138 | num_joints_total: 10_000, 139 | num_meshes: 1_000, 140 | }, 141 | Combo { 142 | num_joints_total: 100_000, 143 | num_meshes: 1_000, 144 | }, 145 | Combo { 146 | num_joints_total: 100_000, 147 | num_meshes: 10_000, 148 | }, 149 | Combo { 150 | num_joints_total: 1_000_000, 151 | num_meshes: 10_000, 152 | }, 153 | ]; 154 | 155 | let num_assets = 10; 156 | 157 | for parallel in [false, true] { 158 | for &Combo { 159 | num_joints_total, 160 | num_meshes, 161 | } in &combos 162 | { 163 | group.warm_up_time(Duration::from_millis(500)); 164 | 165 | if num_joints_total < 100_000 { 166 | group.sample_size(100); 167 | group.measurement_time(Duration::from_millis(500)); 168 | } else { 169 | group.sample_size(50); 170 | group.measurement_time(Duration::from_millis(2000)); 171 | } 172 | 173 | group.throughput(Throughput::Elements(num_joints_total as u64)); 174 | 175 | if num_joints_total < num_meshes { 176 | continue; 177 | } 178 | 179 | assert!((num_joints_total % num_meshes) == 0); 180 | 181 | let num_joints = num_joints_total / num_meshes; 182 | 183 | // TODO: Correct constant? 184 | if num_joints >= 255 { 185 | continue; 186 | } 187 | 188 | let name = format!( 189 | "(parallel = {}, assets = {}, joints total = {}, joints per mesh = {}, meshes = {})", 190 | parallel, num_assets, num_joints_total, num_joints, num_meshes, 191 | ); 192 | 193 | let mesh_params = MeshParams { 194 | num_assets, 195 | num_meshes, 196 | num_joints, 197 | }; 198 | 199 | let settings = SkinnedAabbPluginSettings { parallel }; 200 | 201 | group.bench_function(name, |b| systems_internal(b, settings, &mesh_params)); 202 | } 203 | } 204 | 205 | group.finish(); 206 | } 207 | 208 | criterion_group!(benches, core, systems); 209 | criterion_main!(benches); 210 | -------------------------------------------------------------------------------- /dev/dev.rs: -------------------------------------------------------------------------------- 1 | // Utilities for tests and examples. 2 | 3 | // TODO: Rust-analyzer complains about dead code even though all public 4 | // functions are used in various tests and examples. Is there a better way to 5 | // handle this? 6 | #![allow(dead_code)] 7 | 8 | use bevy::{ 9 | pbr::{MeshMaterial3d, StandardMaterial}, 10 | tasks::{ComputeTaskPool, TaskPool}, 11 | time::{Time, Virtual}, 12 | }; 13 | use bevy_asset::{Assets, Handle, RenderAssetUsages}; 14 | use bevy_color::Color; 15 | use bevy_ecs::{ 16 | change_detection::{Res, ResMut}, 17 | component::Component, 18 | entity::Entity, 19 | hierarchy::ChildOf, 20 | system::{Commands, Query}, 21 | world::World, 22 | }; 23 | use bevy_math::{ 24 | Affine3A, Mat4, Quat, Vec3, 25 | curve::{Curve, EaseFunction, EasingCurve}, 26 | ops, 27 | }; 28 | use bevy_mesh::{ 29 | Mesh, MeshVertexAttributeId, PrimitiveTopology, VertexAttributeValues, 30 | skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}, 31 | }; 32 | use bevy_mod_skinned_aabb::{ 33 | JointIndex, MAX_INFLUENCES, SkinnedAabbAsset, SkinnedAabbPluginSettings, 34 | }; 35 | use bevy_render::{mesh::Mesh3d, primitives::Aabb, view::visibility::Visibility}; 36 | use bevy_transform::components::{GlobalTransform, Transform}; 37 | use rand::{ 38 | Rng, SeedableRng, 39 | distributions::{Distribution, Slice, Uniform}, 40 | rngs::StdRng, 41 | }; 42 | use std::{borrow::Borrow, time::Duration}; 43 | use std::{ 44 | f32::consts::TAU, 45 | hash::{DefaultHasher, Hash, Hasher}, 46 | }; 47 | use std::{iter::once, iter::repeat_with}; 48 | 49 | // Return a Vec3 with each element sampled from the given distribution. 50 | fn random_vec3, D: Distribution>(rng: &mut impl Rng, dist: D) -> Vec3 { 51 | Vec3::new( 52 | *rng.sample(&dist).borrow(), 53 | *rng.sample(&dist).borrow(), 54 | *rng.sample(&dist).borrow(), 55 | ) 56 | } 57 | 58 | // Return a Vec3 with each element uniformly sampled from the set (-1.0, 0.0, 1.0). 59 | fn random_vec3_snorm_outlier(rng: &mut impl Rng) -> Vec3 { 60 | let dist = Slice::new(&[-1.0f32, 0.0f32, 1.0f32]).unwrap(); 61 | 62 | random_vec3(rng, dist) 63 | } 64 | 65 | // Return a Vec3 with each element uniformly sampled from the range [-1.0, 1.0]. 66 | pub fn random_vec3_snorm(rng: &mut impl Rng) -> Vec3 { 67 | let dist = Uniform::new_inclusive(-1.0f32, 1.0f32); 68 | 69 | random_vec3(rng, dist) 70 | } 71 | 72 | // 50/50 chance of returning random_vec3_snorm or random_vec3_snorm_outlier. 73 | fn random_vec3_snorm_maybe_outlier(rng: &mut impl Rng) -> Vec3 { 74 | if rng.r#gen::() { 75 | random_vec3_snorm(rng) 76 | } else { 77 | random_vec3_snorm_outlier(rng) 78 | } 79 | } 80 | 81 | // Return a random quaternion that's uniformly distributed on the 3-sphere. 82 | // 83 | // Source: Ken Shoemake, "Uniform Random Rotations", Graphics Gems III, Academic Press, 1992, pp. 124–132. 84 | // 85 | // We could have used Glam's default random instead. But it's implemented as a 86 | // uniformly sampled axis and angle and so is not uniformly distributed on the 87 | // 3-sphere. Which is probably fine for our purposes, but hey. 88 | fn random_quat(rng: &mut impl Rng) -> Quat { 89 | let r0 = rng.gen_range(0.0f32..TAU); 90 | let r1 = rng.gen_range(0.0f32..TAU); 91 | let r2 = rng.gen_range(0.0f32..1.0f32); 92 | 93 | let (s0, c0) = ops::sin_cos(r0); 94 | let (s1, c1) = ops::sin_cos(r1); 95 | 96 | let t0 = (1.0 - r2).sqrt(); 97 | let t1 = r2.sqrt(); 98 | 99 | Quat::from_xyzw(t0 * s0, t0 * c0, t1 * s1, t1 * c1) 100 | } 101 | 102 | // Return a random quaternion that's identity or a 90/180 degree rotation 103 | // around a single axis. 104 | fn random_quat_outlier(rng: &mut impl Rng) -> Quat { 105 | let a90 = 1.0 / 2.0f32.sqrt(); 106 | 107 | let values = [ 108 | Quat::from_xyzw(1.0, 0.0, 0.0, 0.0), 109 | Quat::from_xyzw(0.0, 1.0, 0.0, 0.0), 110 | Quat::from_xyzw(0.0, 0.0, 1.0, 0.0), 111 | Quat::from_xyzw(0.0, 0.0, 0.0, 1.0), 112 | Quat::from_xyzw(a90, a90, 0.0, 0.0), 113 | Quat::from_xyzw(a90, 0.0, a90, 0.0), 114 | Quat::from_xyzw(a90, 0.0, 0.0, a90), 115 | Quat::from_xyzw(0.0, a90, a90, 0.0), 116 | Quat::from_xyzw(0.0, a90, 0.0, a90), 117 | Quat::from_xyzw(0.0, 0.0, a90, a90), 118 | ]; 119 | 120 | *rng.sample(Slice::new(&values).unwrap()) 121 | } 122 | 123 | // 50/50 chance of returning random_quat or random_quat_outlier. 124 | fn random_quat_maybe_outlier(rng: &mut impl Rng) -> Quat { 125 | if rng.r#gen::() { 126 | random_quat(rng) 127 | } else { 128 | random_quat_outlier(rng) 129 | } 130 | } 131 | 132 | fn random_transform(rng: &mut impl Rng) -> Transform { 133 | let translation = random_vec3_snorm(rng) * 0.5; 134 | let rotation: Quat = random_quat(rng); 135 | let scale = random_vec3_snorm(rng); 136 | 137 | Transform { 138 | translation, 139 | rotation, 140 | scale, 141 | } 142 | } 143 | 144 | fn random_transform_maybe_outlier(rng: &mut impl Rng) -> Transform { 145 | let translation = random_vec3_snorm_maybe_outlier(rng) * 0.5; 146 | let rotation: Quat = random_quat_maybe_outlier(rng); 147 | let scale = random_vec3_snorm_maybe_outlier(rng); 148 | 149 | Transform { 150 | translation, 151 | rotation, 152 | scale, 153 | } 154 | } 155 | 156 | pub enum RandomMeshError { 157 | InvalidNumJoints, 158 | } 159 | 160 | // Create a mesh with random triangles skinned to random joints with varying 161 | // weights. 162 | fn create_random_soft_skinned_mesh( 163 | rng: &mut impl Rng, 164 | num_tris: usize, 165 | num_unskinned_joints: usize, 166 | num_skinned_joints: usize, 167 | ) -> Result { 168 | let num_joints = JointIndex::try_from(num_unskinned_joints + num_skinned_joints) 169 | .or(Err(RandomMeshError::InvalidNumJoints))?; 170 | 171 | let position_dist = Uniform::new_inclusive(-0.5, 0.5); 172 | let joint_index_dist = Uniform::new(num_unskinned_joints as JointIndex, num_joints); 173 | let joint_weight_dist = Uniform::new(0.01, 1.0); 174 | let num_influences_dist = Uniform::new_inclusive(1, MAX_INFLUENCES); 175 | 176 | let num_verts = num_tris * 3; 177 | 178 | let mut positions = vec![Vec3::ZERO; num_verts]; 179 | let mut joint_indices = vec![[0u16; 4]; num_verts]; 180 | let mut joint_weights = vec![[0.0f32; 4]; num_verts]; 181 | 182 | for vert_index in 0..num_verts { 183 | let position = random_vec3(rng, position_dist); 184 | 185 | let mut vert_joint_indices = [0u16; MAX_INFLUENCES]; 186 | let mut vert_joint_weights = [0.0f32; MAX_INFLUENCES]; 187 | 188 | for influence_index in 0..rng.sample(num_influences_dist) { 189 | vert_joint_indices[influence_index] = rng.sample(joint_index_dist); 190 | vert_joint_weights[influence_index] = rng.sample(joint_weight_dist); 191 | } 192 | 193 | let normalization_scale = 1.0 / vert_joint_weights.iter().sum::(); 194 | let vert_joint_weights = vert_joint_weights.map(|w| w * normalization_scale); 195 | 196 | positions[vert_index] = position; 197 | joint_indices[vert_index] = vert_joint_indices; 198 | joint_weights[vert_index] = vert_joint_weights; 199 | } 200 | 201 | let joint_indices = VertexAttributeValues::Uint16x4(joint_indices); 202 | 203 | Ok(Mesh::new( 204 | PrimitiveTopology::TriangleList, 205 | RenderAssetUsages::default(), 206 | ) 207 | .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) 208 | .with_inserted_attribute(Mesh::ATTRIBUTE_JOINT_INDEX, joint_indices) 209 | .with_inserted_attribute(Mesh::ATTRIBUTE_JOINT_WEIGHT, joint_weights)) 210 | } 211 | 212 | // Create a mesh with a triangle hard skinned to each joint. 213 | fn create_random_hard_skinned_mesh( 214 | rng: &mut impl Rng, 215 | num_unskinned_joints: usize, 216 | num_skinned_joints: usize, 217 | ) -> Result { 218 | // Check that all the joints can fit in a JointIndex. 219 | if JointIndex::try_from(num_unskinned_joints + num_skinned_joints).is_err() { 220 | return Err(RandomMeshError::InvalidNumJoints); 221 | }; 222 | 223 | let position_dist = Uniform::new_inclusive(-0.5, 0.5); 224 | 225 | let num_tris = num_skinned_joints; 226 | let num_verts = num_tris * 3; 227 | 228 | let mut positions = vec![Vec3::ZERO; num_verts]; 229 | let mut joint_indices = vec![[0u16; 4]; num_verts]; 230 | 231 | // More tris = smaller tris. 232 | let scale = 1.0 / ((num_skinned_joints as f32) * 0.2).cbrt(); 233 | 234 | for tri_index in 0..num_skinned_joints { 235 | let joint_index = (num_unskinned_joints + tri_index) as JointIndex; 236 | 237 | let base_position = random_vec3(rng, position_dist); 238 | 239 | let tri_vert_positions = [ 240 | base_position + (scale * random_vec3(rng, position_dist)), 241 | base_position + (scale * random_vec3(rng, position_dist)), 242 | base_position + (scale * random_vec3(rng, position_dist)), 243 | ]; 244 | 245 | let vert_joint_indices = [joint_index, 0, 0, 0]; 246 | 247 | for (tri_vert_index, tri_vert_position) in tri_vert_positions.iter().enumerate() { 248 | let vert_index = (tri_index * 3) + tri_vert_index; 249 | positions[vert_index] = *tri_vert_position; 250 | joint_indices[vert_index] = vert_joint_indices; 251 | } 252 | } 253 | 254 | let joint_indices = VertexAttributeValues::Uint16x4(joint_indices); 255 | let joint_weights = vec![[1.0f32, 0.0f32, 0.0f32, 0.0f32]; num_verts]; 256 | 257 | Ok(Mesh::new( 258 | PrimitiveTopology::TriangleList, 259 | RenderAssetUsages::default(), 260 | ) 261 | .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) 262 | .with_inserted_attribute(Mesh::ATTRIBUTE_JOINT_INDEX, joint_indices) 263 | .with_inserted_attribute(Mesh::ATTRIBUTE_JOINT_WEIGHT, joint_weights)) 264 | } 265 | 266 | fn create_random_inverse_bindposes( 267 | rng: &mut impl Rng, 268 | num_joints: usize, 269 | ) -> SkinnedMeshInverseBindposes { 270 | // Leaving the root as identity makes it more visually pleasing. 271 | let iter = once(Mat4::IDENTITY).chain(repeat_with(|| random_transform(rng).compute_matrix())); 272 | 273 | SkinnedMeshInverseBindposes::from(iter.take(num_joints).collect::>()) 274 | } 275 | 276 | pub struct SkinnedMeshAssets { 277 | mesh: Handle, 278 | inverse_bindposes: Handle, 279 | num_joints: usize, 280 | } 281 | 282 | pub enum RandomSkinnedMeshType { 283 | Hard, 284 | Soft { num_tris: usize }, 285 | } 286 | 287 | pub fn create_random_skinned_mesh_assets( 288 | mesh_assets: &mut Assets, 289 | inverse_bindposes_assets: &mut Assets, 290 | rng: &mut impl Rng, 291 | mesh_type: RandomSkinnedMeshType, 292 | num_unskinned_joints: usize, 293 | num_skinned_joints: usize, 294 | ) -> Result { 295 | let num_joints = num_unskinned_joints + num_skinned_joints; 296 | 297 | let mesh = match mesh_type { 298 | RandomSkinnedMeshType::Soft { num_tris } => { 299 | create_random_soft_skinned_mesh(rng, num_tris, num_unskinned_joints, num_skinned_joints) 300 | } 301 | RandomSkinnedMeshType::Hard => { 302 | create_random_hard_skinned_mesh(rng, num_unskinned_joints, num_skinned_joints) 303 | } 304 | }?; 305 | 306 | let mesh = mesh_assets.add(mesh); 307 | 308 | let inverse_bindposes = 309 | inverse_bindposes_assets.add(create_random_inverse_bindposes(rng, num_joints)); 310 | 311 | Ok(SkinnedMeshAssets { 312 | mesh, 313 | inverse_bindposes, 314 | num_joints, 315 | }) 316 | } 317 | 318 | // Hash a single value. 319 | fn hash(v: T) -> u64 { 320 | let mut hasher = DefaultHasher::new(); 321 | v.hash(&mut hasher); 322 | hasher.finish() 323 | } 324 | 325 | // An infinite timeline of discrete noise, with each sample one unit apart. 326 | struct NoiseTimeline { 327 | seed: u64, 328 | } 329 | 330 | // A sample of a NoiseTimeline. 331 | struct NoiseSample { 332 | // The two noise values before and after the sample time. 333 | keys: [u64; 2], 334 | 335 | // The alpha of the time between the noise values. 0.0 == keys[0], 1.0 == keys[1]. 336 | alpha: f32, 337 | } 338 | 339 | impl NoiseTimeline { 340 | fn sample(&self, time: f32) -> NoiseSample { 341 | assert!(time >= 0.0); 342 | 343 | let alpha = time.fract(); 344 | let basis = self.seed.wrapping_add(time.trunc() as u64); 345 | let keys = [hash(basis), hash(basis.wrapping_add(1))]; 346 | 347 | NoiseSample { keys, alpha } 348 | } 349 | } 350 | 351 | #[derive(Component)] 352 | pub struct RandomMeshAnimation { 353 | noise: NoiseTimeline, 354 | } 355 | 356 | impl RandomMeshAnimation { 357 | fn new(seed: u64) -> Self { 358 | RandomMeshAnimation { 359 | noise: NoiseTimeline { seed }, 360 | } 361 | } 362 | } 363 | 364 | pub fn spawn_joints( 365 | commands: &mut Commands, 366 | rng: &mut impl Rng, 367 | base: Entity, 368 | num: usize, 369 | ) -> Vec { 370 | assert!(num > 0); 371 | 372 | let mut joints: Vec = Vec::with_capacity(num); 373 | 374 | let root_joint = commands 375 | .spawn((Transform::IDENTITY, RandomMeshAnimation::new(rng.r#gen()))) 376 | .insert(ChildOf(base)) 377 | .id(); 378 | 379 | joints.push(root_joint); 380 | 381 | for _ in 1..num { 382 | let joint = commands 383 | .spawn((Transform::IDENTITY, RandomMeshAnimation::new(rng.r#gen()))) 384 | .insert(ChildOf(root_joint)) 385 | .id(); 386 | 387 | joints.push(joint); 388 | } 389 | 390 | joints 391 | } 392 | 393 | pub fn spawn_random_skinned_mesh( 394 | commands: &mut Commands, 395 | rng: &mut impl Rng, 396 | base: Entity, 397 | transform: Transform, 398 | assets: &SkinnedMeshAssets, 399 | ) -> Entity { 400 | let joints = spawn_joints(commands, rng, base, assets.num_joints); 401 | 402 | commands 403 | .spawn(( 404 | transform, 405 | Mesh3d(assets.mesh.clone()), 406 | SkinnedMesh { 407 | inverse_bindposes: assets.inverse_bindposes.clone(), 408 | joints, 409 | }, 410 | Aabb::default(), 411 | )) 412 | .insert(ChildOf(base)) 413 | .id() 414 | } 415 | 416 | #[allow(clippy::too_many_arguments)] 417 | pub fn create_and_spawn_random_skinned_mesh( 418 | commands: &mut Commands, 419 | mesh_assets: &mut Assets, 420 | inverse_bindposes_assets: &mut Assets, 421 | rng: &mut impl Rng, 422 | base: Entity, 423 | transform: Transform, 424 | mesh_type: RandomSkinnedMeshType, 425 | num_skinned_joints: usize, 426 | ) -> Result { 427 | let num_unskinned_joints = 1; 428 | 429 | let assets = create_random_skinned_mesh_assets( 430 | mesh_assets, 431 | inverse_bindposes_assets, 432 | rng, 433 | mesh_type, 434 | num_unskinned_joints, 435 | num_skinned_joints, 436 | )?; 437 | 438 | Ok(spawn_random_skinned_mesh( 439 | commands, rng, base, transform, &assets, 440 | )) 441 | } 442 | 443 | pub fn spawn_random_mesh_selection( 444 | mut commands: Commands, 445 | mut mesh_assets: ResMut>, 446 | mut material_assets: ResMut>, 447 | mut inverse_bindposes_assets: ResMut>, 448 | ) { 449 | let mut rng = StdRng::seed_from_u64(732935); 450 | 451 | let material = MeshMaterial3d(material_assets.add(StandardMaterial { 452 | base_color: Color::WHITE, 453 | cull_mode: None, 454 | ..Default::default() 455 | })); 456 | 457 | struct MeshInstance { 458 | mesh_type: RandomSkinnedMeshType, 459 | num_joints: usize, 460 | translation: Vec3, 461 | } 462 | 463 | let mesh_instances = [ 464 | MeshInstance { 465 | mesh_type: RandomSkinnedMeshType::Hard, 466 | num_joints: 1, 467 | translation: Vec3::new(-3.0, 1.5, 0.0), 468 | }, 469 | MeshInstance { 470 | mesh_type: RandomSkinnedMeshType::Hard, 471 | num_joints: 20, 472 | translation: Vec3::new(0.0, 1.5, 0.0), 473 | }, 474 | MeshInstance { 475 | mesh_type: RandomSkinnedMeshType::Hard, 476 | num_joints: 200, 477 | translation: Vec3::new(3.0, 1.5, 0.0), 478 | }, 479 | MeshInstance { 480 | mesh_type: RandomSkinnedMeshType::Soft { num_tris: 100 }, 481 | num_joints: 1, 482 | translation: Vec3::new(-3.0, -1.5, 0.0), 483 | }, 484 | MeshInstance { 485 | mesh_type: RandomSkinnedMeshType::Soft { num_tris: 100 }, 486 | num_joints: 20, 487 | translation: Vec3::new(0.0, -1.5, 0.0), 488 | }, 489 | MeshInstance { 490 | mesh_type: RandomSkinnedMeshType::Soft { num_tris: 1000 }, 491 | num_joints: 200, 492 | translation: Vec3::new(3.0, -1.5, 0.0), 493 | }, 494 | ]; 495 | 496 | for mesh_instance in mesh_instances { 497 | // Create a base entity. This will be the parent of the mesh and the joints. 498 | 499 | let base_transform = Transform::from_translation(mesh_instance.translation); 500 | let base_entity = commands.spawn((base_transform, Visibility::default())).id(); 501 | 502 | // Give the mesh entity a random translation. This ensures we're not depending on the 503 | // mesh having the same transform as the root joint. 504 | 505 | let mesh_transform = Transform::from_translation(random_vec3_snorm(&mut rng)); 506 | 507 | if let Ok(entity) = create_and_spawn_random_skinned_mesh( 508 | &mut commands, 509 | &mut mesh_assets, 510 | &mut inverse_bindposes_assets, 511 | &mut rng, 512 | base_entity, 513 | mesh_transform, 514 | mesh_instance.mesh_type, 515 | mesh_instance.num_joints, 516 | ) { 517 | commands.entity(entity).insert(material.clone()); 518 | } 519 | } 520 | } 521 | 522 | pub fn update_random_mesh_animations( 523 | mut query: Query<(&mut Transform, &RandomMeshAnimation)>, 524 | time: Res>, 525 | ) { 526 | for (mut transform, animation) in &mut query { 527 | // Sample the noise timeline and generate a transform for each key. 528 | 529 | let noise = animation.noise.sample(time.elapsed_secs()); 530 | 531 | let t0 = random_transform_maybe_outlier(&mut StdRng::seed_from_u64(noise.keys[0])); 532 | let t1 = random_transform_maybe_outlier(&mut StdRng::seed_from_u64(noise.keys[1])); 533 | 534 | // Blend between the transforms with a nice ease in/out over 2/3rds of a 535 | // second, then hold for 1/3rd of a second. 536 | 537 | let ease = EasingCurve::new(0.0, 1.0, EaseFunction::CubicInOut); 538 | let alpha = ease.sample_clamped(noise.alpha * 1.5); 539 | 540 | // TODO: Feels like there should be a standard function for mixing two transforms? 541 | 542 | *transform = Transform { 543 | translation: t0.translation.lerp(t1.translation, alpha), 544 | rotation: t0.rotation.lerp(t1.rotation, alpha), 545 | scale: t0.scale.lerp(t1.scale, alpha), 546 | }; 547 | } 548 | } 549 | 550 | // Create a `World` suitable for running our benchmarks and tests. 551 | pub fn create_dev_world(settings: SkinnedAabbPluginSettings) -> World { 552 | ComputeTaskPool::get_or_init(TaskPool::default); 553 | 554 | let mut world = World::default(); 555 | 556 | world.init_resource::>(); 557 | world.init_resource::>(); 558 | world.init_resource::>(); 559 | world.init_resource::>(); 560 | 561 | world.insert_resource(settings); 562 | 563 | let mut time = Time::::default(); 564 | time.advance_by(Duration::from_secs(1)); 565 | 566 | world.insert_resource(time); 567 | 568 | world 569 | } 570 | 571 | pub enum SkinError { 572 | InvalidJointIndex, 573 | MismatchedJointAndInverseBindposesLengths, 574 | MismatchedMeshAttributeLengths, 575 | MissingJointEntity, 576 | MissingInverseBindposesAsset, 577 | MissingMeshAsset, 578 | UnexpectedPositionAttributeType, 579 | UnexpectedJointIndicesAttributeType, 580 | UnexpectedJointWeightsAttributeType, 581 | } 582 | 583 | fn skin_positions( 584 | positions: &VertexAttributeValues, 585 | joint_indices: &[[u16; 4]], 586 | joint_weights: &[[f32; 4]], 587 | entity_from_binds: &[Mat4], 588 | ) -> Result, SkinError> { 589 | let VertexAttributeValues::Float32x3(positions) = positions else { 590 | return Err(SkinError::UnexpectedPositionAttributeType); 591 | }; 592 | 593 | let mut out = vec![[0.0f32, 0.0f32, 0.0f32]; positions.len()]; 594 | 595 | if joint_indices.len() != positions.len() { 596 | return Err(SkinError::MismatchedMeshAttributeLengths); 597 | } 598 | 599 | if joint_weights.len() != positions.len() { 600 | return Err(SkinError::MismatchedMeshAttributeLengths); 601 | } 602 | 603 | for (vertex_index, position) in positions.iter().enumerate() { 604 | let vertex_joint_indices = joint_indices[vertex_index]; 605 | let vertex_joint_weights = joint_weights[vertex_index]; 606 | 607 | let mut weighted_entity_from_binds = [Mat4::ZERO; 4]; 608 | 609 | for influence_index in 0..4 { 610 | let joint_weight = vertex_joint_weights[influence_index]; 611 | let joint_index = vertex_joint_indices[influence_index] as usize; 612 | let entity_from_bind = *entity_from_binds 613 | .get(joint_index) 614 | .ok_or(SkinError::InvalidJointIndex)?; 615 | 616 | weighted_entity_from_binds[influence_index] = joint_weight * entity_from_bind; 617 | } 618 | 619 | let entity_from_bind = weighted_entity_from_binds.iter().sum::(); 620 | 621 | let skinned_position = 622 | <[f32; 3]>::from(entity_from_bind.transform_point3(Vec3::from_slice(position))); 623 | 624 | out[vertex_index] = skinned_position; 625 | } 626 | 627 | Ok(out) 628 | } 629 | 630 | fn skin_internal( 631 | mesh: &Mesh, 632 | inverse_bindposes: &[Mat4], 633 | entity_from_joints: &[Mat4], 634 | ) -> Result { 635 | if entity_from_joints.len() != inverse_bindposes.len() { 636 | return Err(SkinError::MismatchedJointAndInverseBindposesLengths); 637 | } 638 | 639 | let Some(VertexAttributeValues::Uint16x4(joint_indices)) = 640 | mesh.attribute(Mesh::ATTRIBUTE_JOINT_INDEX) 641 | else { 642 | return Err(SkinError::UnexpectedJointIndicesAttributeType); 643 | }; 644 | 645 | let Some(VertexAttributeValues::Float32x4(joint_weights)) = 646 | mesh.attribute(Mesh::ATTRIBUTE_JOINT_WEIGHT) 647 | else { 648 | return Err(SkinError::UnexpectedJointWeightsAttributeType); 649 | }; 650 | 651 | let entity_from_binds = entity_from_joints 652 | .iter() 653 | .zip(inverse_bindposes.iter()) 654 | .map(|(entity_from_joint, inverse_bindpose)| *entity_from_joint * *inverse_bindpose) 655 | .collect::>(); 656 | 657 | // TODO: Awkward? Appears needed since match patterns can't be expressions. 658 | const JOINT_INDEX_ID: MeshVertexAttributeId = Mesh::ATTRIBUTE_JOINT_INDEX.id; 659 | const JOINT_WEIGHT_ID: MeshVertexAttributeId = Mesh::ATTRIBUTE_JOINT_WEIGHT.id; 660 | const POSITION_ID: MeshVertexAttributeId = Mesh::ATTRIBUTE_POSITION.id; 661 | 662 | let mut out = Mesh::new(mesh.primitive_topology(), mesh.asset_usage); 663 | 664 | for (attribute, values) in mesh.attributes() { 665 | match attribute.id { 666 | JOINT_INDEX_ID => (), 667 | JOINT_WEIGHT_ID => (), 668 | 669 | POSITION_ID => { 670 | out.insert_attribute( 671 | *attribute, 672 | skin_positions(values, joint_indices, joint_weights, &entity_from_binds)?, 673 | ); 674 | } 675 | 676 | _ => out.insert_attribute(*attribute, values.clone()), 677 | } 678 | } 679 | 680 | Ok(out) 681 | } 682 | 683 | fn try_entity_from_joint( 684 | joints: &Query<&GlobalTransform>, 685 | entity: Entity, 686 | entity_from_world: Affine3A, 687 | ) -> Option { 688 | let world_from_joint = joints.get(entity).ok()?.affine(); 689 | 690 | Some(Mat4::from(entity_from_world * world_from_joint)) 691 | } 692 | 693 | // Given the components of a skinned mesh, return a copy of the mesh with 694 | // positions skinned to the current joint transforms. The mesh's skinning 695 | // attributes are removed. Tangents and normals are *not* skinned. 696 | pub fn skin( 697 | mesh: &Mesh3d, 698 | skinned_mesh: &SkinnedMesh, 699 | world_from_entity: &GlobalTransform, 700 | mesh_assets: &Assets, 701 | inverse_bindposes_assets: &Assets, 702 | joint_transforms: &Query<&GlobalTransform>, 703 | ) -> Result { 704 | let entity_from_world = world_from_entity.affine().inverse(); 705 | 706 | let entity_from_joints = skinned_mesh 707 | .joints 708 | .iter() 709 | .map(|&entity| try_entity_from_joint(joint_transforms, entity, entity_from_world)) 710 | .collect::>>() 711 | .ok_or(SkinError::MissingJointEntity)?; 712 | 713 | let mesh_asset = mesh_assets 714 | .get(&mesh.0) 715 | .ok_or(SkinError::MissingMeshAsset)?; 716 | 717 | let inverse_bindposes_asset = inverse_bindposes_assets 718 | .get(&skinned_mesh.inverse_bindposes) 719 | .ok_or(SkinError::MissingInverseBindposesAsset)?; 720 | 721 | skin_internal(mesh_asset, inverse_bindposes_asset, &entity_from_joints) 722 | } 723 | -------------------------------------------------------------------------------- /examples/cpu_skinning.rs: -------------------------------------------------------------------------------- 1 | #[path = "../dev/dev.rs"] 2 | mod dev; 3 | 4 | use bevy::prelude::*; 5 | use bevy_mesh::skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}; 6 | use bevy_render::{camera::ScalingMode, primitives::Aabb}; 7 | use dev::{skin, spawn_random_mesh_selection, update_random_mesh_animations}; 8 | 9 | fn main() { 10 | App::new() 11 | .insert_resource(AmbientLight { 12 | brightness: 2000., 13 | ..Default::default() 14 | }) 15 | .add_plugins(DefaultPlugins) 16 | .add_systems(Startup, setup) 17 | .add_systems(Startup, spawn_random_mesh_selection) 18 | .add_systems(Update, update_random_mesh_animations) 19 | .add_systems(Update, cpu_skinning_delete_existing) 20 | /* 21 | // TODO: Why doesn't this work? Would avoid us being a frame behind. 22 | // Probably missing some required components but not sure what... tried 23 | // GlobalTransform, Visibility::Visible, ViewVisibility. 24 | .add_systems( 25 | PostUpdate, 26 | cpu_skinning_spawn_new.after(TransformSystem::TransformPropagate), 27 | ) 28 | */ 29 | .add_systems( 30 | Update, 31 | cpu_skinning_spawn_new 32 | .after(cpu_skinning_delete_existing) 33 | .before(update_random_mesh_animations), 34 | ) 35 | .run(); 36 | } 37 | 38 | fn setup(mut commands: Commands) { 39 | commands.spawn(( 40 | Camera3d::default(), 41 | Projection::Orthographic(OrthographicProjection { 42 | scaling_mode: ScalingMode::AutoMin { 43 | min_width: 16.0 * 1.1, 44 | min_height: 9.0 * 1.1, 45 | }, 46 | ..OrthographicProjection::default_3d() 47 | }), 48 | Transform::from_xyz(4.0, 0.0, 12.0).looking_at(Vec3::new(4.0, 0.0, 0.0), Vec3::Y), 49 | )); 50 | } 51 | 52 | #[derive(Component, Default)] 53 | struct CpuSkinningMarker; 54 | 55 | fn cpu_skinning_delete_existing( 56 | mut commands: Commands, 57 | query: Query>, 58 | ) { 59 | for entity in query.iter() { 60 | commands.entity(entity).despawn(); 61 | } 62 | } 63 | 64 | fn cpu_skinning_spawn_new( 65 | mut commands: Commands, 66 | query: Query<( 67 | &Mesh3d, 68 | &SkinnedMesh, 69 | &GlobalTransform, 70 | &Aabb, 71 | &MeshMaterial3d, 72 | )>, 73 | joints: Query<&GlobalTransform>, 74 | inverse_bindposes_assets: Res>, 75 | mut mesh_assets: ResMut>, 76 | ) { 77 | for (mesh, skinned_mesh, transform, aabb, material) in query.iter() { 78 | let Ok(cpu_skinned_mesh) = skin( 79 | mesh, 80 | skinned_mesh, 81 | transform, 82 | &mesh_assets, 83 | &inverse_bindposes_assets, 84 | &joints, 85 | ) else { 86 | continue; 87 | }; 88 | 89 | let cpu_skinned_transform = Transform::from_xyz(8.0, 0.0, 0.0) * *transform; 90 | let cpu_skinned_mesh_asset = Mesh3d(mesh_assets.add(cpu_skinned_mesh)); 91 | 92 | commands.spawn(( 93 | cpu_skinned_mesh_asset, 94 | material.clone(), 95 | *aabb, 96 | Transform::from(cpu_skinned_transform), 97 | CpuSkinningMarker, 98 | )); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /examples/many_foxes.rs: -------------------------------------------------------------------------------- 1 | // Based on bevy/examples/stress_tests/many_foxes.rs. Removes a bunch of options to keep 2 | // things simple. Adds skinned aabb debug rendering. 3 | 4 | use bevy::{ 5 | diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, 6 | input::common_conditions::input_just_pressed, 7 | prelude::*, 8 | window::{PresentMode, WindowResolution}, 9 | winit::{UpdateMode, WinitSettings}, 10 | }; 11 | use bevy_mod_skinned_aabb::prelude::*; 12 | use std::f32::consts::PI; 13 | 14 | #[derive(Resource)] 15 | struct Foxes { 16 | count: usize, 17 | speed: f32, 18 | moving: bool, 19 | sync: bool, 20 | } 21 | 22 | const NUM_FOXES: usize = 1000; 23 | 24 | // Magic numbers from Fox.glb. 25 | const NUM_JOINTS: usize = 24; 26 | const NUM_SKINNED_JOINTS: usize = 22; 27 | 28 | fn main() { 29 | App::new() 30 | .add_plugins(( 31 | DefaultPlugins.set(WindowPlugin { 32 | primary_window: Some(Window { 33 | title: "🦊🦊🦊 Many Foxes! 🦊🦊🦊".into(), 34 | present_mode: PresentMode::AutoNoVsync, 35 | resolution: WindowResolution::new(1920.0, 1080.0) 36 | .with_scale_factor_override(1.0), 37 | ..default() 38 | }), 39 | ..default() 40 | }), 41 | FrameTimeDiagnosticsPlugin::default(), 42 | LogDiagnosticsPlugin::default(), 43 | )) 44 | .add_plugins(( 45 | SkinnedAabbPlugin, 46 | SkinnedAabbDebugPlugin::disable_by_default(), 47 | )) 48 | .insert_resource(WinitSettings { 49 | focused_mode: UpdateMode::Continuous, 50 | unfocused_mode: UpdateMode::Continuous, 51 | }) 52 | .insert_resource(Foxes { 53 | count: NUM_FOXES, 54 | speed: 2.0, 55 | moving: true, 56 | sync: false, 57 | }) 58 | .insert_resource(AmbientLight { 59 | brightness: 2000.0, 60 | ..Default::default() 61 | }) 62 | .add_systems(Startup, setup) 63 | .add_systems(Update, (setup_scene_once_loaded, update_fox_rings)) 64 | .add_systems( 65 | Update, 66 | ( 67 | toggle_draw_joint_aabbs.run_if(input_just_pressed(KeyCode::KeyJ)), 68 | toggle_draw_mesh_aabbs.run_if(input_just_pressed(KeyCode::KeyM)), 69 | ), 70 | ) 71 | .run(); 72 | } 73 | 74 | #[derive(Resource)] 75 | struct Animations { 76 | node_indices: Vec, 77 | graph: Handle, 78 | } 79 | 80 | const RING_SPACING: f32 = 2.0; 81 | const FOX_SPACING: f32 = 2.0; 82 | 83 | #[derive(Component, Clone, Copy)] 84 | enum RotationDirection { 85 | CounterClockwise, 86 | Clockwise, 87 | } 88 | 89 | impl RotationDirection { 90 | fn sign(&self) -> f32 { 91 | match self { 92 | RotationDirection::CounterClockwise => 1.0, 93 | RotationDirection::Clockwise => -1.0, 94 | } 95 | } 96 | } 97 | 98 | #[derive(Component)] 99 | struct Ring { 100 | radius: f32, 101 | } 102 | 103 | fn setup( 104 | mut commands: Commands, 105 | asset_server: Res, 106 | mut meshes: ResMut>, 107 | mut materials: ResMut>, 108 | mut animation_graphs: ResMut>, 109 | foxes: Res, 110 | ) { 111 | commands.spawn(( 112 | Text::new("J: Toggle Joint AABBs\nM: Toggle Mesh AABBs"), 113 | TextFont { 114 | font_size: 15.0, 115 | ..default() 116 | }, 117 | Node { 118 | position_type: PositionType::Absolute, 119 | top: Val::Px(12.0), 120 | left: Val::Px(12.0), 121 | ..default() 122 | }, 123 | )); 124 | 125 | // Insert a resource with the current scene information 126 | let animation_clips = [ 127 | asset_server.load(GltfAssetLabel::Animation(2).from_asset("Fox.glb")), 128 | asset_server.load(GltfAssetLabel::Animation(1).from_asset("Fox.glb")), 129 | asset_server.load(GltfAssetLabel::Animation(0).from_asset("Fox.glb")), 130 | ]; 131 | let mut animation_graph = AnimationGraph::new(); 132 | let node_indices = animation_graph 133 | .add_clips(animation_clips.iter().cloned(), 1.0, animation_graph.root) 134 | .collect(); 135 | commands.insert_resource(Animations { 136 | node_indices, 137 | graph: animation_graphs.add(animation_graph), 138 | }); 139 | 140 | // Foxes 141 | // Concentric rings of foxes, running in opposite directions. The rings are spaced at 2m radius intervals. 142 | // The foxes in each ring are spaced at least 2m apart around its circumference.' 143 | 144 | // NOTE: This fox model faces +z 145 | let fox_handle = asset_server.load(GltfAssetLabel::Scene(0).from_asset("Fox.glb")); 146 | 147 | let ring_directions = [ 148 | ( 149 | Quat::from_rotation_y(PI), 150 | RotationDirection::CounterClockwise, 151 | ), 152 | (Quat::IDENTITY, RotationDirection::Clockwise), 153 | ]; 154 | 155 | let mut ring_index = 0; 156 | let mut radius = RING_SPACING; 157 | let mut foxes_remaining = foxes.count; 158 | 159 | info!( 160 | "Spawning {} foxes, for a total of {} joints and {} skinned joints...", 161 | foxes.count, 162 | foxes.count * NUM_JOINTS, 163 | foxes.count * NUM_SKINNED_JOINTS 164 | ); 165 | 166 | while foxes_remaining > 0 { 167 | let (base_rotation, ring_direction) = ring_directions[ring_index % 2]; 168 | let ring_parent = commands 169 | .spawn(( 170 | Transform::default(), 171 | Visibility::default(), 172 | ring_direction, 173 | Ring { radius }, 174 | )) 175 | .id(); 176 | 177 | let circumference = PI * 2. * radius; 178 | let foxes_in_ring = ((circumference / FOX_SPACING) as usize).min(foxes_remaining); 179 | let fox_spacing_angle = circumference / (foxes_in_ring as f32 * radius); 180 | 181 | for fox_i in 0..foxes_in_ring { 182 | let fox_angle = fox_i as f32 * fox_spacing_angle; 183 | let (s, c) = ops::sin_cos(fox_angle); 184 | let (x, z) = (radius * c, radius * s); 185 | 186 | commands.entity(ring_parent).with_children(|builder| { 187 | builder.spawn(( 188 | SceneRoot(fox_handle.clone()), 189 | Transform::from_xyz(x, 0.0, z) 190 | .with_scale(Vec3::splat(0.01)) 191 | .with_rotation(base_rotation * Quat::from_rotation_y(-fox_angle)), 192 | )); 193 | }); 194 | } 195 | 196 | foxes_remaining -= foxes_in_ring; 197 | radius += RING_SPACING; 198 | ring_index += 1; 199 | } 200 | 201 | // Camera 202 | let zoom = 0.8; 203 | let translation = Vec3::new( 204 | radius * 1.25 * zoom, 205 | radius * 0.5 * zoom, 206 | radius * 1.5 * zoom, 207 | ); 208 | commands.spawn(( 209 | Camera3d::default(), 210 | Transform::from_translation(translation) 211 | .looking_at(0.2 * Vec3::new(translation.x, 0.0, translation.z), Vec3::Y), 212 | )); 213 | 214 | // Plane 215 | commands.spawn(( 216 | Mesh3d(meshes.add(Plane3d::default().mesh().size(5000.0, 5000.0))), 217 | MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), 218 | )); 219 | 220 | // Light 221 | commands.spawn(( 222 | Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)), 223 | DirectionalLight { 224 | shadows_enabled: false, 225 | ..default() 226 | }, 227 | )); 228 | } 229 | 230 | // Once the scene is loaded, start the animation 231 | fn setup_scene_once_loaded( 232 | animations: Res, 233 | foxes: Res, 234 | mut commands: Commands, 235 | mut player: Query<(Entity, &mut AnimationPlayer)>, 236 | mut done: Local, 237 | ) { 238 | if !*done && player.iter().len() == foxes.count { 239 | for (entity, mut player) in &mut player { 240 | commands 241 | .entity(entity) 242 | .insert(AnimationGraphHandle(animations.graph.clone())) 243 | .insert(AnimationTransitions::new()); 244 | 245 | let playing_animation = player.play(animations.node_indices[0]).repeat(); 246 | if !foxes.sync { 247 | playing_animation.seek_to(entity.index() as f32 / 10.0); 248 | } 249 | } 250 | *done = true; 251 | } 252 | } 253 | 254 | fn update_fox_rings( 255 | time: Res