Skip to main content

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