many_foxes/
many_foxes.rs

1//! Loads animations from a skinned glTF, spawns many of them, and plays the
2//! animation to stress test skinned meshes.
3
4use std::{f32::consts::PI, time::Duration};
5
6use argh::FromArgs;
7use bevy::{
8    diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
9    light::CascadeShadowConfigBuilder,
10    prelude::*,
11    scene::SceneInstanceReady,
12    window::{PresentMode, WindowResolution},
13    winit::WinitSettings,
14};
15
16#[derive(FromArgs, Resource)]
17/// `many_foxes` stress test
18struct Args {
19    /// whether all foxes run in sync.
20    #[argh(switch)]
21    sync: bool,
22
23    /// total number of foxes.
24    #[argh(option, default = "1000")]
25    count: usize,
26}
27
28#[derive(Resource)]
29struct Foxes {
30    count: usize,
31    speed: f32,
32    moving: bool,
33    sync: bool,
34}
35
36fn main() {
37    // `from_env` panics on the web
38    #[cfg(not(target_arch = "wasm32"))]
39    let args: Args = argh::from_env();
40    #[cfg(target_arch = "wasm32")]
41    let args = Args::from_args(&[], &[]).unwrap();
42
43    App::new()
44        .add_plugins((
45            DefaultPlugins.set(WindowPlugin {
46                primary_window: Some(Window {
47                    title: "🦊🦊🦊 Many Foxes! 🦊🦊🦊".into(),
48                    present_mode: PresentMode::AutoNoVsync,
49                    resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),
50                    ..default()
51                }),
52                ..default()
53            }),
54            FrameTimeDiagnosticsPlugin::default(),
55            LogDiagnosticsPlugin::default(),
56        ))
57        .insert_resource(WinitSettings::continuous())
58        .insert_resource(Foxes {
59            count: args.count,
60            speed: 2.0,
61            moving: true,
62            sync: args.sync,
63        })
64        .add_systems(Startup, setup)
65        .add_systems(
66            Update,
67            (
68                keyboard_animation_control,
69                update_fox_rings.after(keyboard_animation_control),
70            ),
71        )
72        .run();
73}
74
75#[derive(Resource)]
76struct Animations {
77    node_indices: Vec<AnimationNodeIndex>,
78    graph: Handle<AnimationGraph>,
79}
80
81const RING_SPACING: f32 = 2.0;
82const FOX_SPACING: f32 = 2.0;
83
84#[derive(Component, Clone, Copy)]
85enum RotationDirection {
86    CounterClockwise,
87    Clockwise,
88}
89
90impl RotationDirection {
91    fn sign(&self) -> f32 {
92        match self {
93            RotationDirection::CounterClockwise => 1.0,
94            RotationDirection::Clockwise => -1.0,
95        }
96    }
97}
98
99#[derive(Component)]
100struct Ring {
101    radius: f32,
102}
103
104fn setup(
105    mut commands: Commands,
106    asset_server: Res<AssetServer>,
107    mut meshes: ResMut<Assets<Mesh>>,
108    mut materials: ResMut<Assets<StandardMaterial>>,
109    mut animation_graphs: ResMut<Assets<AnimationGraph>>,
110    foxes: Res<Foxes>,
111) {
112    warn!(include_str!("warning_string.txt"));
113
114    // Insert a resource with the current scene information
115    let animation_clips = [
116        asset_server.load(GltfAssetLabel::Animation(2).from_asset("models/animated/Fox.glb")),
117        asset_server.load(GltfAssetLabel::Animation(1).from_asset("models/animated/Fox.glb")),
118        asset_server.load(GltfAssetLabel::Animation(0).from_asset("models/animated/Fox.glb")),
119    ];
120    let mut animation_graph = AnimationGraph::new();
121    let node_indices = animation_graph
122        .add_clips(animation_clips.iter().cloned(), 1.0, animation_graph.root)
123        .collect();
124    commands.insert_resource(Animations {
125        node_indices,
126        graph: animation_graphs.add(animation_graph),
127    });
128
129    // Foxes
130    // Concentric rings of foxes, running in opposite directions. The rings are spaced at 2m radius intervals.
131    // The foxes in each ring are spaced at least 2m apart around its circumference.'
132
133    // NOTE: This fox model faces +z
134    let fox_handle =
135        asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb"));
136
137    let ring_directions = [
138        (
139            Quat::from_rotation_y(PI),
140            RotationDirection::CounterClockwise,
141        ),
142        (Quat::IDENTITY, RotationDirection::Clockwise),
143    ];
144
145    let mut ring_index = 0;
146    let mut radius = RING_SPACING;
147    let mut foxes_remaining = foxes.count;
148
149    info!("Spawning {} foxes...", foxes.count);
150
151    while foxes_remaining > 0 {
152        let (base_rotation, ring_direction) = ring_directions[ring_index % 2];
153        let ring_parent = commands
154            .spawn((
155                Transform::default(),
156                Visibility::default(),
157                ring_direction,
158                Ring { radius },
159            ))
160            .id();
161
162        let circumference = PI * 2. * radius;
163        let foxes_in_ring = ((circumference / FOX_SPACING) as usize).min(foxes_remaining);
164        let fox_spacing_angle = circumference / (foxes_in_ring as f32 * radius);
165
166        for fox_i in 0..foxes_in_ring {
167            let fox_angle = fox_i as f32 * fox_spacing_angle;
168            let (s, c) = ops::sin_cos(fox_angle);
169            let (x, z) = (radius * c, radius * s);
170
171            commands.entity(ring_parent).with_children(|builder| {
172                builder
173                    .spawn((
174                        SceneRoot(fox_handle.clone()),
175                        Transform::from_xyz(x, 0.0, z)
176                            .with_scale(Vec3::splat(0.01))
177                            .with_rotation(base_rotation * Quat::from_rotation_y(-fox_angle)),
178                    ))
179                    .observe(setup_scene_once_loaded);
180            });
181        }
182
183        foxes_remaining -= foxes_in_ring;
184        radius += RING_SPACING;
185        ring_index += 1;
186    }
187
188    // Camera
189    let zoom = 0.8;
190    let translation = Vec3::new(
191        radius * 1.25 * zoom,
192        radius * 0.5 * zoom,
193        radius * 1.5 * zoom,
194    );
195    commands.spawn((
196        Camera3d::default(),
197        Transform::from_translation(translation)
198            .looking_at(0.2 * Vec3::new(translation.x, 0.0, translation.z), Vec3::Y),
199    ));
200
201    // Plane
202    commands.spawn((
203        Mesh3d(meshes.add(Plane3d::default().mesh().size(5000.0, 5000.0))),
204        MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
205    ));
206
207    // Light
208    commands.spawn((
209        Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
210        DirectionalLight {
211            shadows_enabled: true,
212            ..default()
213        },
214        CascadeShadowConfigBuilder {
215            first_cascade_far_bound: 0.9 * radius,
216            maximum_distance: 2.8 * radius,
217            ..default()
218        }
219        .build(),
220    ));
221
222    println!("Animation controls:");
223    println!("  - spacebar: play / pause");
224    println!("  - arrow up / down: speed up / slow down animation playback");
225    println!("  - arrow left / right: seek backward / forward");
226    println!("  - return: change animation");
227}
228
229// Once the scene is loaded, start the animation
230fn setup_scene_once_loaded(
231    scene_ready: On<SceneInstanceReady>,
232    animations: Res<Animations>,
233    foxes: Res<Foxes>,
234    mut commands: Commands,
235    children: Query<&Children>,
236    mut players: Query<&mut AnimationPlayer>,
237) {
238    for child in children.iter_descendants(scene_ready.entity) {
239        if let Ok(mut player) = players.get_mut(child) {
240            let playing_animation = player.play(animations.node_indices[0]).repeat();
241            if !foxes.sync {
242                playing_animation.seek_to(scene_ready.entity.index() as f32 / 10.0);
243            }
244            commands
245                .entity(child)
246                .insert(AnimationGraphHandle(animations.graph.clone()));
247        }
248    }
249}
250
251fn update_fox_rings(
252    time: Res<Time>,
253    foxes: Res<Foxes>,
254    mut rings: Query<(&Ring, &RotationDirection, &mut Transform)>,
255) {
256    if !foxes.moving {
257        return;
258    }
259
260    let dt = time.delta_secs();
261    for (ring, rotation_direction, mut transform) in &mut rings {
262        let angular_velocity = foxes.speed / ring.radius;
263        transform.rotate_y(rotation_direction.sign() * angular_velocity * dt);
264    }
265}
266
267fn keyboard_animation_control(
268    keyboard_input: Res<ButtonInput<KeyCode>>,
269    mut animation_player: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
270    animations: Res<Animations>,
271    mut current_animation: Local<usize>,
272    mut foxes: ResMut<Foxes>,
273) {
274    if keyboard_input.just_pressed(KeyCode::Space) {
275        foxes.moving = !foxes.moving;
276    }
277
278    if keyboard_input.just_pressed(KeyCode::ArrowUp) {
279        foxes.speed *= 1.25;
280    }
281
282    if keyboard_input.just_pressed(KeyCode::ArrowDown) {
283        foxes.speed *= 0.8;
284    }
285
286    if keyboard_input.just_pressed(KeyCode::Enter) {
287        *current_animation = (*current_animation + 1) % animations.node_indices.len();
288    }
289
290    for (mut player, mut transitions) in &mut animation_player {
291        if keyboard_input.just_pressed(KeyCode::Space) {
292            if player.all_paused() {
293                player.resume_all();
294            } else {
295                player.pause_all();
296            }
297        }
298
299        if keyboard_input.just_pressed(KeyCode::ArrowUp) {
300            player.adjust_speeds(1.25);
301        }
302
303        if keyboard_input.just_pressed(KeyCode::ArrowDown) {
304            player.adjust_speeds(0.8);
305        }
306
307        if keyboard_input.just_pressed(KeyCode::ArrowLeft) {
308            player.seek_all_by(-0.1);
309        }
310
311        if keyboard_input.just_pressed(KeyCode::ArrowRight) {
312            player.seek_all_by(0.1);
313        }
314
315        if keyboard_input.just_pressed(KeyCode::Enter) {
316            transitions
317                .play(
318                    &mut player,
319                    animations.node_indices[*current_animation],
320                    Duration::from_millis(250),
321                )
322                .repeat();
323        }
324    }
325}