transform_hierarchy/transform_hierarchy.rs
1//! Hierarchy and transform propagation stress test.
2//!
3//! Running this example:
4//!
5//! ```
6//! cargo r --release --example transform_hierarchy <configuration name>
7//! ```
8//!
9//! | Configuration | Description |
10//! | -------------------- | ----------------------------------------------------------------- |
11//! | `large_tree` | A fairly wide and deep tree. |
12//! | `wide_tree` | A shallow but very wide tree. |
13//! | `deep_tree` | A deep but not very wide tree. |
14//! | `chain` | A chain. 2500 levels deep. |
15//! | `update_leaves` | Same as `large_tree`, but only leaves are updated. |
16//! | `update_shallow` | Same as `large_tree`, but only the first few levels are updated. |
17//! | `humanoids_active` | 4000 active humanoid rigs. |
18//! | `humanoids_inactive` | 4000 humanoid rigs. Only 10 are active. |
19//! | `humanoids_mixed` | 2000 active and 2000 inactive humanoid rigs. |
20
21use bevy::{
22 diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
23 prelude::*,
24 window::ExitCondition,
25};
26use rand::Rng;
27
28/// pre-defined test configurations with name
29const CONFIGS: [(&str, Cfg); 9] = [
30 (
31 "large_tree",
32 Cfg {
33 test_case: TestCase::NonUniformTree {
34 depth: 18,
35 branch_width: 8,
36 },
37 update_filter: UpdateFilter {
38 probability: 0.5,
39 min_depth: 0,
40 max_depth: u32::MAX,
41 },
42 },
43 ),
44 (
45 "wide_tree",
46 Cfg {
47 test_case: TestCase::Tree {
48 depth: 3,
49 branch_width: 500,
50 },
51 update_filter: UpdateFilter {
52 probability: 0.5,
53 min_depth: 0,
54 max_depth: u32::MAX,
55 },
56 },
57 ),
58 (
59 "deep_tree",
60 Cfg {
61 test_case: TestCase::NonUniformTree {
62 depth: 25,
63 branch_width: 2,
64 },
65 update_filter: UpdateFilter {
66 probability: 0.5,
67 min_depth: 0,
68 max_depth: u32::MAX,
69 },
70 },
71 ),
72 (
73 "chain",
74 Cfg {
75 test_case: TestCase::Tree {
76 depth: 2500,
77 branch_width: 1,
78 },
79 update_filter: UpdateFilter {
80 probability: 0.5,
81 min_depth: 0,
82 max_depth: u32::MAX,
83 },
84 },
85 ),
86 (
87 "update_leaves",
88 Cfg {
89 test_case: TestCase::Tree {
90 depth: 18,
91 branch_width: 2,
92 },
93 update_filter: UpdateFilter {
94 probability: 0.5,
95 min_depth: 17,
96 max_depth: u32::MAX,
97 },
98 },
99 ),
100 (
101 "update_shallow",
102 Cfg {
103 test_case: TestCase::Tree {
104 depth: 18,
105 branch_width: 2,
106 },
107 update_filter: UpdateFilter {
108 probability: 0.5,
109 min_depth: 0,
110 max_depth: 8,
111 },
112 },
113 ),
114 (
115 "humanoids_active",
116 Cfg {
117 test_case: TestCase::Humanoids {
118 active: 4000,
119 inactive: 0,
120 },
121 update_filter: UpdateFilter {
122 probability: 1.0,
123 min_depth: 0,
124 max_depth: u32::MAX,
125 },
126 },
127 ),
128 (
129 "humanoids_inactive",
130 Cfg {
131 test_case: TestCase::Humanoids {
132 active: 10,
133 inactive: 3990,
134 },
135 update_filter: UpdateFilter {
136 probability: 1.0,
137 min_depth: 0,
138 max_depth: u32::MAX,
139 },
140 },
141 ),
142 (
143 "humanoids_mixed",
144 Cfg {
145 test_case: TestCase::Humanoids {
146 active: 2000,
147 inactive: 2000,
148 },
149 update_filter: UpdateFilter {
150 probability: 1.0,
151 min_depth: 0,
152 max_depth: u32::MAX,
153 },
154 },
155 ),
156];
157
158fn print_available_configs() {
159 println!("available configurations:");
160 for (name, _) in CONFIGS {
161 println!(" {name}");
162 }
163}
164
165fn main() {
166 // parse cli argument and find the selected test configuration
167 let cfg: Cfg = match std::env::args().nth(1) {
168 Some(arg) => match CONFIGS.iter().find(|(name, _)| *name == arg) {
169 Some((name, cfg)) => {
170 println!("test configuration: {name}");
171 cfg.clone()
172 }
173 None => {
174 println!("test configuration \"{arg}\" not found.\n");
175 print_available_configs();
176 return;
177 }
178 },
179 None => {
180 println!("missing argument: <test configuration>\n");
181 print_available_configs();
182 return;
183 }
184 };
185
186 println!("\n{cfg:#?}");
187
188 App::new()
189 .insert_resource(cfg)
190 .add_plugins((
191 DefaultPlugins.set(WindowPlugin {
192 primary_window: None,
193 exit_condition: ExitCondition::DontExit,
194 ..default()
195 }),
196 FrameTimeDiagnosticsPlugin::default(),
197 LogDiagnosticsPlugin::default(),
198 ))
199 .add_systems(Startup, setup)
200 // Updating transforms *must* be done before `PostUpdate`
201 // or the hierarchy will momentarily be in an invalid state.
202 .add_systems(Update, update)
203 .run();
204}
205
206/// test configuration
207#[derive(Resource, Debug, Clone)]
208struct Cfg {
209 /// which test case should be inserted
210 test_case: TestCase,
211 /// which entities should be updated
212 update_filter: UpdateFilter,
213}
214
215#[derive(Debug, Clone)]
216enum TestCase {
217 /// a uniform tree, exponentially growing with depth
218 Tree {
219 /// total depth
220 depth: u32,
221 /// number of children per node
222 branch_width: u32,
223 },
224 /// a non uniform tree (one side is deeper than the other)
225 /// creates significantly less nodes than `TestCase::Tree` with the same parameters
226 NonUniformTree {
227 /// the maximum depth
228 depth: u32,
229 /// max number of children per node
230 branch_width: u32,
231 },
232 /// one or multiple humanoid rigs
233 Humanoids {
234 /// number of active instances (uses the specified [`UpdateFilter`])
235 active: u32,
236 /// number of inactive instances (always inactive)
237 inactive: u32,
238 },
239}
240
241/// a filter to restrict which nodes are updated
242#[derive(Debug, Clone)]
243struct UpdateFilter {
244 /// starting depth (inclusive)
245 min_depth: u32,
246 /// end depth (inclusive)
247 max_depth: u32,
248 /// probability of a node to get updated (evaluated at insertion time, not during update)
249 /// 0 (never) .. 1 (always)
250 probability: f32,
251}
252
253/// update component with some per-component value
254#[derive(Component)]
255struct UpdateValue(f32);
256
257/// update positions system
258fn update(time: Res<Time>, mut query: Query<(&mut Transform, &mut UpdateValue)>) {
259 for (mut t, mut u) in &mut query {
260 u.0 += time.delta_secs() * 0.1;
261 set_translation(&mut t.translation, u.0);
262 }
263}
264
265/// set translation based on the angle `a`
266fn set_translation(translation: &mut Vec3, a: f32) {
267 translation.x = ops::cos(a) * 32.0;
268 translation.y = ops::sin(a) * 32.0;
269}
270
271fn setup(mut commands: Commands, cfg: Res<Cfg>) {
272 warn!(include_str!("warning_string.txt"));
273
274 commands.spawn((Camera2d, Transform::from_xyz(0.0, 0.0, 100.0)));
275
276 let result = match cfg.test_case {
277 TestCase::Tree {
278 depth,
279 branch_width,
280 } => {
281 let tree = gen_tree(depth, branch_width);
282 spawn_tree(&tree, &mut commands, &cfg.update_filter, default())
283 }
284 TestCase::NonUniformTree {
285 depth,
286 branch_width,
287 } => {
288 let tree = gen_non_uniform_tree(depth, branch_width);
289 spawn_tree(&tree, &mut commands, &cfg.update_filter, default())
290 }
291 TestCase::Humanoids { active, inactive } => {
292 let mut result = InsertResult::default();
293 let mut rng = rand::thread_rng();
294
295 for _ in 0..active {
296 result.combine(spawn_tree(
297 &HUMANOID_RIG,
298 &mut commands,
299 &cfg.update_filter,
300 Transform::from_xyz(
301 rng.r#gen::<f32>() * 500.0 - 250.0,
302 rng.r#gen::<f32>() * 500.0 - 250.0,
303 0.0,
304 ),
305 ));
306 }
307
308 for _ in 0..inactive {
309 result.combine(spawn_tree(
310 &HUMANOID_RIG,
311 &mut commands,
312 &UpdateFilter {
313 // force inactive by setting the probability < 0
314 probability: -1.0,
315 ..cfg.update_filter
316 },
317 Transform::from_xyz(
318 rng.r#gen::<f32>() * 500.0 - 250.0,
319 rng.r#gen::<f32>() * 500.0 - 250.0,
320 0.0,
321 ),
322 ));
323 }
324
325 result
326 }
327 };
328
329 println!("\n{result:#?}");
330}
331
332/// overview of the inserted hierarchy
333#[derive(Default, Debug)]
334struct InsertResult {
335 /// total number of nodes inserted
336 inserted_nodes: usize,
337 /// number of nodes that get updated each frame
338 active_nodes: usize,
339 /// maximum depth of the hierarchy tree
340 maximum_depth: usize,
341}
342
343impl InsertResult {
344 fn combine(&mut self, rhs: Self) -> &mut Self {
345 self.inserted_nodes += rhs.inserted_nodes;
346 self.active_nodes += rhs.active_nodes;
347 self.maximum_depth = self.maximum_depth.max(rhs.maximum_depth);
348 self
349 }
350}
351
352/// spawns a tree defined by a parent map (excluding root)
353/// the parent map must be ordered (parent must exist before child)
354fn spawn_tree(
355 parent_map: &[usize],
356 commands: &mut Commands,
357 update_filter: &UpdateFilter,
358 root_transform: Transform,
359) -> InsertResult {
360 // total count (# of nodes + root)
361 let count = parent_map.len() + 1;
362
363 #[derive(Default, Clone, Copy)]
364 struct NodeInfo {
365 child_count: u32,
366 depth: u32,
367 }
368
369 // node index -> entity lookup list
370 let mut ents: Vec<Entity> = Vec::with_capacity(count);
371 let mut node_info: Vec<NodeInfo> = vec![default(); count];
372 for (i, &parent_idx) in parent_map.iter().enumerate() {
373 // assert spawn order (parent must be processed before child)
374 assert!(parent_idx <= i, "invalid spawn order");
375 node_info[parent_idx].child_count += 1;
376 }
377
378 // insert root
379 ents.push(commands.spawn(root_transform).id());
380
381 let mut result = InsertResult::default();
382 let mut rng = rand::thread_rng();
383 // used to count through the number of children (used only for visual layout)
384 let mut child_idx: Vec<u16> = vec![0; count];
385
386 // insert children
387 for (current_idx, &parent_idx) in parent_map.iter().enumerate() {
388 let current_idx = current_idx + 1;
389
390 // separation factor to visually separate children (0..1)
391 let sep = child_idx[parent_idx] as f32 / node_info[parent_idx].child_count as f32;
392 child_idx[parent_idx] += 1;
393
394 // calculate and set depth
395 // this works because it's guaranteed that we have already iterated over the parent
396 let depth = node_info[parent_idx].depth + 1;
397 let info = &mut node_info[current_idx];
398 info.depth = depth;
399
400 // update max depth of tree
401 result.maximum_depth = result.maximum_depth.max(depth.try_into().unwrap());
402
403 // insert child
404 let child_entity = {
405 let mut cmd = commands.spawn_empty();
406
407 // check whether or not to update this node
408 let update = (rng.r#gen::<f32>() <= update_filter.probability)
409 && (depth >= update_filter.min_depth && depth <= update_filter.max_depth);
410
411 if update {
412 cmd.insert(UpdateValue(sep));
413 result.active_nodes += 1;
414 }
415
416 let transform = {
417 let mut translation = Vec3::ZERO;
418 // use the same placement fn as the `update` system
419 // this way the entities won't be all at (0, 0, 0) when they don't have an `Update` component
420 set_translation(&mut translation, sep);
421 Transform::from_translation(translation)
422 };
423
424 // only insert the components necessary for the transform propagation
425 cmd.insert(transform);
426
427 cmd.id()
428 };
429
430 commands.entity(ents[parent_idx]).add_child(child_entity);
431
432 ents.push(child_entity);
433 }
434
435 result.inserted_nodes = ents.len();
436 result
437}
438
439/// generate a tree `depth` levels deep, where each node has `branch_width` children
440fn gen_tree(depth: u32, branch_width: u32) -> Vec<usize> {
441 // calculate the total count of branches
442 let mut count: usize = 0;
443 for i in 0..(depth - 1) {
444 count += TryInto::<usize>::try_into(branch_width.pow(i)).unwrap();
445 }
446
447 // the tree is built using this pattern:
448 // 0, 0, 0, ... 1, 1, 1, ... 2, 2, 2, ... (count - 1)
449 (0..count)
450 .flat_map(|i| std::iter::repeat_n(i, branch_width.try_into().unwrap()))
451 .collect()
452}
453
454/// recursive part of [`gen_non_uniform_tree`]
455fn add_children_non_uniform(
456 tree: &mut Vec<usize>,
457 parent: usize,
458 mut curr_depth: u32,
459 max_branch_width: u32,
460) {
461 for _ in 0..max_branch_width {
462 tree.push(parent);
463
464 curr_depth = curr_depth.checked_sub(1).unwrap();
465 if curr_depth == 0 {
466 return;
467 }
468 add_children_non_uniform(tree, tree.len(), curr_depth, max_branch_width);
469 }
470}
471
472/// generate a tree that has more nodes on one side that the other
473/// the deepest hierarchy path is `max_depth` and the widest branches have `max_branch_width` children
474fn gen_non_uniform_tree(max_depth: u32, max_branch_width: u32) -> Vec<usize> {
475 let mut tree = Vec::new();
476 add_children_non_uniform(&mut tree, 0, max_depth, max_branch_width);
477 tree
478}
479
480/// parent map for a decently complex humanoid rig (based on mixamo rig)
481const HUMANOID_RIG: [usize; 67] = [
482 // (0: root)
483 0, // 1: hips
484 1, // 2: spine
485 2, // 3: spine 1
486 3, // 4: spine 2
487 4, // 5: neck
488 5, // 6: head
489 6, // 7: head top
490 6, // 8: left eye
491 6, // 9: right eye
492 4, // 10: left shoulder
493 10, // 11: left arm
494 11, // 12: left forearm
495 12, // 13: left hand
496 13, // 14: left hand thumb 1
497 14, // 15: left hand thumb 2
498 15, // 16: left hand thumb 3
499 16, // 17: left hand thumb 4
500 13, // 18: left hand index 1
501 18, // 19: left hand index 2
502 19, // 20: left hand index 3
503 20, // 21: left hand index 4
504 13, // 22: left hand middle 1
505 22, // 23: left hand middle 2
506 23, // 24: left hand middle 3
507 24, // 25: left hand middle 4
508 13, // 26: left hand ring 1
509 26, // 27: left hand ring 2
510 27, // 28: left hand ring 3
511 28, // 29: left hand ring 4
512 13, // 30: left hand pinky 1
513 30, // 31: left hand pinky 2
514 31, // 32: left hand pinky 3
515 32, // 33: left hand pinky 4
516 4, // 34: right shoulder
517 34, // 35: right arm
518 35, // 36: right forearm
519 36, // 37: right hand
520 37, // 38: right hand thumb 1
521 38, // 39: right hand thumb 2
522 39, // 40: right hand thumb 3
523 40, // 41: right hand thumb 4
524 37, // 42: right hand index 1
525 42, // 43: right hand index 2
526 43, // 44: right hand index 3
527 44, // 45: right hand index 4
528 37, // 46: right hand middle 1
529 46, // 47: right hand middle 2
530 47, // 48: right hand middle 3
531 48, // 49: right hand middle 4
532 37, // 50: right hand ring 1
533 50, // 51: right hand ring 2
534 51, // 52: right hand ring 3
535 52, // 53: right hand ring 4
536 37, // 54: right hand pinky 1
537 54, // 55: right hand pinky 2
538 55, // 56: right hand pinky 3
539 56, // 57: right hand pinky 4
540 1, // 58: left upper leg
541 58, // 59: left leg
542 59, // 60: left foot
543 60, // 61: left toe base
544 61, // 62: left toe end
545 1, // 63: right upper leg
546 63, // 64: right leg
547 64, // 65: right foot
548 65, // 66: right toe base
549 66, // 67: right toe end
550];