mirror of
https://github.com/bevyengine/bevy.git
synced 2026-07-01 08:12:51 -04:00
7517c61ecd
# Objective Resolves #21902. ## Solution This PR adopts a relatively transparent approach to reduce the GPU vertex buffer size. On CPU-side mesh can still use uncompressed Float32 data, and users are not required to insert compressed vertex formats. The vertex data is automatically processed into lower-precision/octahedral encoded data when uploading to the GPU. To enable vertex attribute compression, just set the `attribute_compression` field of Mesh, or set `mesh_attribute_compression` of GltfLoaderSettings. If enabled, normal and tangent will be octahedral encoded Unorm16x2, uv0, uv1, joint weight and color will be corresponding Unorm16 or Float16. I also provide Unorm8x4 for vertex color if hdr isn't needed. Update 2026-2-16 Removed previous approach that automatically compresses vertex buffer according to flags when uploading to GPU. Instead, I added `compressed_mesh` method to Mesh to construct compressed Mesh ahead of time. GltfLoader can also opt-in mesh compressing when loading. I also add an option to convert indices to u16, though I believe blender gltf exporter already uses u16 indices when possible. ## Testing Run `many_cubes`, `many_foxes`, `many_morph_targets` with `--vertex-compression` to test 3d. Run `bevymark` with `sprite_mesh` to test 2d, because `SpriteMesh` uses compressed quad mesh now. --------- Co-authored-by: Greeble <166992735+greeble-dev@users.noreply.github.com>
365 lines
11 KiB
Rust
365 lines
11 KiB
Rust
//! Loads animations from a skinned glTF, spawns many of them, and plays the
|
|
//! animation to stress test skinned meshes.
|
|
|
|
use std::{f32::consts::PI, time::Duration};
|
|
|
|
use argh::FromArgs;
|
|
use bevy::{
|
|
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
|
|
gltf::GltfPlugin,
|
|
light::CascadeShadowConfigBuilder,
|
|
mesh::MeshAttributeCompressionFlags,
|
|
post_process::motion_blur::MotionBlur,
|
|
prelude::*,
|
|
window::{PresentMode, WindowResolution},
|
|
winit::WinitSettings,
|
|
world_serialization::WorldInstanceReady,
|
|
};
|
|
|
|
#[derive(FromArgs, Resource)]
|
|
/// `many_foxes` stress test
|
|
struct Args {
|
|
/// whether all foxes run in sync.
|
|
#[argh(switch)]
|
|
sync: bool,
|
|
|
|
/// total number of foxes.
|
|
#[argh(option, default = "1000")]
|
|
count: usize,
|
|
|
|
/// enable motion blur.
|
|
#[argh(switch)]
|
|
motion_blur: bool,
|
|
|
|
/// whether to enable vertex compression.
|
|
#[argh(switch)]
|
|
vertex_compression: bool,
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
struct Foxes {
|
|
count: usize,
|
|
speed: f32,
|
|
moving: bool,
|
|
sync: bool,
|
|
}
|
|
|
|
fn main() {
|
|
// `from_env` panics on the web
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
let args: Args = argh::from_env();
|
|
#[cfg(target_arch = "wasm32")]
|
|
let args = Args::from_args(&[], &[]).unwrap();
|
|
|
|
App::new()
|
|
.add_plugins((
|
|
DefaultPlugins
|
|
.set(WindowPlugin {
|
|
primary_window: Some(Window {
|
|
title: "🦊🦊🦊 Many Foxes! 🦊🦊🦊".into(),
|
|
present_mode: PresentMode::AutoNoVsync,
|
|
resolution: WindowResolution::new(1920, 1080)
|
|
.with_scale_factor_override(1.0),
|
|
..default()
|
|
}),
|
|
..default()
|
|
})
|
|
.set(GltfPlugin {
|
|
mesh_attribute_compression: if args.vertex_compression {
|
|
MeshAttributeCompressionFlags::all()
|
|
.with_color(MeshAttributeCompressionFlags::COMPRESS_COLOR_UNORM8)
|
|
} else {
|
|
MeshAttributeCompressionFlags::empty()
|
|
},
|
|
..default()
|
|
}),
|
|
FrameTimeDiagnosticsPlugin::default(),
|
|
LogDiagnosticsPlugin::default(),
|
|
))
|
|
.insert_resource(StaticTransformOptimizations::Disabled)
|
|
.insert_resource(WinitSettings::continuous())
|
|
.insert_resource(Foxes {
|
|
count: args.count,
|
|
speed: 2.0,
|
|
moving: true,
|
|
sync: args.sync,
|
|
})
|
|
.insert_resource(args)
|
|
.add_systems(Startup, setup)
|
|
.add_systems(
|
|
Update,
|
|
(
|
|
keyboard_animation_control,
|
|
update_fox_rings.after(keyboard_animation_control),
|
|
),
|
|
)
|
|
.run();
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
struct Animations {
|
|
node_indices: Vec<AnimationNodeIndex>,
|
|
graph: Handle<AnimationGraph>,
|
|
}
|
|
|
|
const RING_SPACING: f32 = 2.0;
|
|
const FOX_SPACING: f32 = 2.0;
|
|
|
|
#[derive(Component, Clone, Copy)]
|
|
enum RotationDirection {
|
|
CounterClockwise,
|
|
Clockwise,
|
|
}
|
|
|
|
impl RotationDirection {
|
|
fn sign(&self) -> f32 {
|
|
match self {
|
|
RotationDirection::CounterClockwise => 1.0,
|
|
RotationDirection::Clockwise => -1.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Component)]
|
|
struct Ring {
|
|
radius: f32,
|
|
}
|
|
|
|
fn setup(
|
|
mut commands: Commands,
|
|
asset_server: Res<AssetServer>,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
mut animation_graphs: ResMut<Assets<AnimationGraph>>,
|
|
foxes: Res<Foxes>,
|
|
args: Res<Args>,
|
|
) {
|
|
warn!(include_str!("warning_string.txt"));
|
|
|
|
// Insert a resource with the current scene information
|
|
let animation_clips = [
|
|
asset_server.load(GltfAssetLabel::Animation(2).from_asset("models/animated/Fox.glb")),
|
|
asset_server.load(GltfAssetLabel::Animation(1).from_asset("models/animated/Fox.glb")),
|
|
asset_server.load(GltfAssetLabel::Animation(0).from_asset("models/animated/Fox.glb")),
|
|
];
|
|
let mut animation_graph = AnimationGraph::new();
|
|
let node_indices = animation_graph
|
|
.add_clips(animation_clips, 1.0, animation_graph.root)
|
|
.collect();
|
|
commands.insert_resource(Animations {
|
|
node_indices,
|
|
graph: animation_graphs.add(animation_graph),
|
|
});
|
|
|
|
// Foxes
|
|
// Concentric rings of foxes, running in opposite directions. The rings are spaced at 2m radius intervals.
|
|
// The foxes in each ring are spaced at least 2m apart around its circumference.'
|
|
|
|
// NOTE: This fox model faces +z
|
|
let fox_handle =
|
|
asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb"));
|
|
|
|
let ring_directions = [
|
|
(
|
|
Quat::from_rotation_y(PI),
|
|
RotationDirection::CounterClockwise,
|
|
),
|
|
(Quat::IDENTITY, RotationDirection::Clockwise),
|
|
];
|
|
|
|
let mut ring_index = 0;
|
|
let mut radius = RING_SPACING;
|
|
let mut foxes_remaining = foxes.count;
|
|
|
|
info!("Spawning {} foxes...", foxes.count);
|
|
|
|
while foxes_remaining > 0 {
|
|
let (base_rotation, ring_direction) = ring_directions[ring_index % 2];
|
|
let ring_parent = commands
|
|
.spawn((
|
|
Transform::default(),
|
|
Visibility::default(),
|
|
ring_direction,
|
|
Ring { radius },
|
|
))
|
|
.id();
|
|
|
|
let circumference = PI * 2. * radius;
|
|
let foxes_in_ring = ((circumference / FOX_SPACING) as usize).min(foxes_remaining);
|
|
let fox_spacing_angle = circumference / (foxes_in_ring as f32 * radius);
|
|
|
|
for fox_i in 0..foxes_in_ring {
|
|
let fox_angle = fox_i as f32 * fox_spacing_angle;
|
|
let (s, c) = ops::sin_cos(fox_angle);
|
|
let (x, z) = (radius * c, radius * s);
|
|
|
|
commands.entity(ring_parent).with_children(|builder| {
|
|
builder
|
|
.spawn((
|
|
WorldAssetRoot(fox_handle.clone()),
|
|
Transform::from_xyz(x, 0.0, z)
|
|
.with_scale(Vec3::splat(0.01))
|
|
.with_rotation(base_rotation * Quat::from_rotation_y(-fox_angle)),
|
|
))
|
|
.observe(setup_scene_once_loaded);
|
|
});
|
|
}
|
|
|
|
foxes_remaining -= foxes_in_ring;
|
|
radius += RING_SPACING;
|
|
ring_index += 1;
|
|
}
|
|
|
|
// Camera
|
|
let zoom = 0.8;
|
|
let translation = Vec3::new(
|
|
radius * 1.25 * zoom,
|
|
radius * 0.5 * zoom,
|
|
radius * 1.5 * zoom,
|
|
);
|
|
let mut camera = commands.spawn((
|
|
Camera3d::default(),
|
|
Transform::from_translation(translation)
|
|
.looking_at(0.2 * Vec3::new(translation.x, 0.0, translation.z), Vec3::Y),
|
|
));
|
|
|
|
if args.motion_blur {
|
|
camera.insert((
|
|
MotionBlur {
|
|
// Use an unrealistically large shutter angle so that motion blur is clearly visible.
|
|
shutter_angle: 3.0,
|
|
..Default::default()
|
|
},
|
|
// MSAA and MotionBlur are not compatible on WebGL.
|
|
#[cfg(all(feature = "webgl2", target_arch = "wasm32", not(feature = "webgpu")))]
|
|
Msaa::Off,
|
|
));
|
|
}
|
|
|
|
// Plane
|
|
commands.spawn((
|
|
Mesh3d(meshes.add(Plane3d::default().mesh().size(5000.0, 5000.0))),
|
|
MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
|
|
));
|
|
|
|
// Light
|
|
commands.spawn((
|
|
Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
|
|
DirectionalLight {
|
|
shadow_maps_enabled: true,
|
|
..default()
|
|
},
|
|
CascadeShadowConfigBuilder {
|
|
first_cascade_far_bound: 0.9 * radius,
|
|
maximum_distance: 2.8 * radius,
|
|
..default()
|
|
}
|
|
.build(),
|
|
));
|
|
|
|
println!("Animation controls:");
|
|
println!(" - spacebar: play / pause");
|
|
println!(" - arrow up / down: speed up / slow down animation playback");
|
|
println!(" - arrow left / right: seek backward / forward");
|
|
println!(" - return: change animation");
|
|
}
|
|
|
|
// Once the scene is loaded, start the animation
|
|
fn setup_scene_once_loaded(
|
|
scene_ready: On<WorldInstanceReady>,
|
|
animations: Res<Animations>,
|
|
foxes: Res<Foxes>,
|
|
mut commands: Commands,
|
|
children: Query<&Children>,
|
|
mut players: Query<&mut AnimationPlayer>,
|
|
) {
|
|
for child in children.iter_descendants(scene_ready.entity) {
|
|
if let Ok(mut player) = players.get_mut(child) {
|
|
let playing_animation = player.play(animations.node_indices[0]).repeat();
|
|
if !foxes.sync {
|
|
playing_animation.seek_to(scene_ready.entity.index_u32() as f32 / 10.0);
|
|
}
|
|
commands.entity(child).insert((
|
|
AnimationGraphHandle(animations.graph.clone()),
|
|
AnimationTransitions::default(),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn update_fox_rings(
|
|
time: Res<Time>,
|
|
foxes: Res<Foxes>,
|
|
mut rings: Query<(&Ring, &RotationDirection, &mut Transform)>,
|
|
) {
|
|
if !foxes.moving {
|
|
return;
|
|
}
|
|
|
|
let dt = time.delta_secs();
|
|
for (ring, rotation_direction, mut transform) in &mut rings {
|
|
let angular_velocity = foxes.speed / ring.radius;
|
|
transform.rotate_y(rotation_direction.sign() * angular_velocity * dt);
|
|
}
|
|
}
|
|
|
|
fn keyboard_animation_control(
|
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
|
mut animation_player: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
|
|
animations: Res<Animations>,
|
|
mut current_animation: Local<usize>,
|
|
mut foxes: ResMut<Foxes>,
|
|
) {
|
|
if keyboard_input.just_pressed(KeyCode::Space) {
|
|
foxes.moving = !foxes.moving;
|
|
}
|
|
|
|
if keyboard_input.just_pressed(KeyCode::ArrowUp) {
|
|
foxes.speed *= 1.25;
|
|
}
|
|
|
|
if keyboard_input.just_pressed(KeyCode::ArrowDown) {
|
|
foxes.speed *= 0.8;
|
|
}
|
|
|
|
if keyboard_input.just_pressed(KeyCode::Enter) {
|
|
*current_animation = (*current_animation + 1) % animations.node_indices.len();
|
|
}
|
|
|
|
for (mut player, mut transitions) in &mut animation_player {
|
|
if keyboard_input.just_pressed(KeyCode::Space) {
|
|
if player.all_paused() {
|
|
player.resume_all();
|
|
} else {
|
|
player.pause_all();
|
|
}
|
|
}
|
|
|
|
if keyboard_input.just_pressed(KeyCode::ArrowUp) {
|
|
player.adjust_speeds(1.25);
|
|
}
|
|
|
|
if keyboard_input.just_pressed(KeyCode::ArrowDown) {
|
|
player.adjust_speeds(0.8);
|
|
}
|
|
|
|
if keyboard_input.just_pressed(KeyCode::ArrowLeft) {
|
|
player.seek_all_by(-0.1);
|
|
}
|
|
|
|
if keyboard_input.just_pressed(KeyCode::ArrowRight) {
|
|
player.seek_all_by(0.1);
|
|
}
|
|
|
|
if keyboard_input.just_pressed(KeyCode::Enter) {
|
|
transitions
|
|
.play(
|
|
&mut player,
|
|
animations.node_indices[*current_animation],
|
|
Duration::from_millis(250),
|
|
)
|
|
.repeat();
|
|
}
|
|
}
|
|
}
|