mirror of
https://github.com/bevyengine/bevy.git
synced 2026-06-30 07:45:50 -04:00
f255b8e57a
# Objective - `glam`, `hexasphere` & `rand` have released their latest versions, update Bevy to support them. ## Solution - The above have been updated to their compatible versions. `rand_distr` updated as well to match `rand` v0.10 support. - `rand_chacha` is soft deprecated and no longer used by `rand`, so its usage has been changed to `chacha20` to match `rand` dep tree. - `uuid` is in the process of updating to `getrandom` v0.4, which `rand` v0.10 supports. This PR remains in draft until a new `uuid` release hits crates.io. - `RngCore` is now `Rng`, and `Rng` is now `RngExt`, so this required updating across many files. - `choose_multiple` method is deprecated, changed to `sample`. ## Testing - Chase all compiler errors, since this should not regress any already existing behaviour. - This must pass CI without regressions. ## Additional Notes `getrandom` v0.4 doesn't add anything new for Web WASM support, so the same `wasm_js` feature is used.
553 lines
16 KiB
Rust
553 lines
16 KiB
Rust
//! This example provides a 3D benchmark.
|
|
//!
|
|
//! Usage: spawn more entities by clicking with the left mouse button.
|
|
|
|
use core::time::Duration;
|
|
use std::str::FromStr;
|
|
|
|
use argh::FromArgs;
|
|
use bevy::{
|
|
asset::RenderAssetUsages,
|
|
color::palettes::basic::*,
|
|
diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
|
|
prelude::*,
|
|
render::render_resource::{Extent3d, TextureDimension, TextureFormat},
|
|
window::{PresentMode, WindowResolution},
|
|
winit::WinitSettings,
|
|
};
|
|
use chacha20::ChaCha8Rng;
|
|
use rand::{seq::IndexedRandom, RngExt, SeedableRng};
|
|
|
|
const CUBES_PER_SECOND: u32 = 10000;
|
|
const GRAVITY: f32 = -9.8;
|
|
const MAX_VELOCITY: f32 = 10.;
|
|
const CUBE_SCALE: f32 = 1.0;
|
|
const CUBE_TEXTURE_SIZE: usize = 256;
|
|
const HALF_CUBE_SIZE: f32 = CUBE_SCALE * 0.5;
|
|
const VOLUME_WIDTH: usize = 50;
|
|
const VOLUME_SIZE: Vec3 = Vec3::splat(VOLUME_WIDTH as f32);
|
|
|
|
#[derive(Resource)]
|
|
struct BevyCounter {
|
|
pub count: usize,
|
|
pub color: Color,
|
|
}
|
|
|
|
#[derive(Component)]
|
|
struct Cube {
|
|
velocity: Vec3,
|
|
}
|
|
|
|
#[derive(FromArgs, Resource)]
|
|
/// `bevymark_3d` cube stress test
|
|
struct Args {
|
|
/// whether to step animations by a fixed amount such that each frame is the same across runs.
|
|
/// If spawning waves, all are spawned up-front to immediately start rendering at the heaviest
|
|
/// load.
|
|
#[argh(switch)]
|
|
benchmark: bool,
|
|
|
|
/// how many cubes to spawn per wave.
|
|
#[argh(option, default = "0")]
|
|
per_wave: usize,
|
|
|
|
/// the number of waves to spawn.
|
|
#[argh(option, default = "0")]
|
|
waves: usize,
|
|
|
|
/// whether to vary the material data in each instance.
|
|
#[argh(switch)]
|
|
vary_per_instance: bool,
|
|
|
|
/// the number of different textures from which to randomly select the material color. 0 means no textures.
|
|
#[argh(option, default = "1")]
|
|
material_texture_count: usize,
|
|
|
|
/// the alpha mode used to spawn the cubes
|
|
#[argh(option, default = "AlphaMode::Opaque")]
|
|
alpha_mode: AlphaMode,
|
|
}
|
|
|
|
#[derive(Default, Clone)]
|
|
enum AlphaMode {
|
|
#[default]
|
|
Opaque,
|
|
Blend,
|
|
AlphaMask,
|
|
}
|
|
|
|
impl FromStr for AlphaMode {
|
|
type Err = String;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s {
|
|
"opaque" => Ok(Self::Opaque),
|
|
"blend" => Ok(Self::Blend),
|
|
"alpha_mask" => Ok(Self::AlphaMask),
|
|
_ => Err(format!(
|
|
"Unknown alpha mode: '{s}', valid modes: 'opaque', 'blend', 'alpha_mask'"
|
|
)),
|
|
}
|
|
}
|
|
}
|
|
|
|
const FIXED_TIMESTEP: f32 = 0.2;
|
|
|
|
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: "BevyMark 3D".into(),
|
|
resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),
|
|
present_mode: PresentMode::AutoNoVsync,
|
|
..default()
|
|
}),
|
|
..default()
|
|
}),
|
|
FrameTimeDiagnosticsPlugin::default(),
|
|
LogDiagnosticsPlugin::default(),
|
|
))
|
|
.insert_resource(WinitSettings::continuous())
|
|
.insert_resource(args)
|
|
.insert_resource(BevyCounter {
|
|
count: 0,
|
|
color: Color::WHITE,
|
|
})
|
|
.add_systems(Startup, setup)
|
|
.add_systems(FixedUpdate, scheduled_spawner)
|
|
.add_systems(
|
|
Update,
|
|
(
|
|
mouse_handler,
|
|
movement_system,
|
|
collision_system,
|
|
counter_system,
|
|
),
|
|
)
|
|
.insert_resource(Time::<Fixed>::from_duration(Duration::from_secs_f32(
|
|
FIXED_TIMESTEP,
|
|
)))
|
|
.run();
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
struct CubeScheduled {
|
|
waves: usize,
|
|
per_wave: usize,
|
|
}
|
|
|
|
fn scheduled_spawner(
|
|
mut commands: Commands,
|
|
args: Res<Args>,
|
|
mut scheduled: ResMut<CubeScheduled>,
|
|
mut counter: ResMut<BevyCounter>,
|
|
cube_resources: ResMut<CubeResources>,
|
|
) {
|
|
if scheduled.waves > 0 {
|
|
let cube_resources = cube_resources.into_inner();
|
|
spawn_cubes(
|
|
&mut commands,
|
|
args.into_inner(),
|
|
&mut counter,
|
|
scheduled.per_wave,
|
|
cube_resources,
|
|
None,
|
|
scheduled.waves - 1,
|
|
);
|
|
|
|
scheduled.waves -= 1;
|
|
}
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
struct CubeResources {
|
|
_textures: Vec<Handle<Image>>,
|
|
materials: Vec<Handle<StandardMaterial>>,
|
|
cube_mesh: Handle<Mesh>,
|
|
color_rng: ChaCha8Rng,
|
|
material_rng: ChaCha8Rng,
|
|
velocity_rng: ChaCha8Rng,
|
|
transform_rng: ChaCha8Rng,
|
|
}
|
|
|
|
#[derive(Component)]
|
|
struct StatsText;
|
|
|
|
fn setup(
|
|
mut commands: Commands,
|
|
args: Res<Args>,
|
|
asset_server: Res<AssetServer>,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
material_assets: ResMut<Assets<StandardMaterial>>,
|
|
images: ResMut<Assets<Image>>,
|
|
counter: ResMut<BevyCounter>,
|
|
) {
|
|
let args = args.into_inner();
|
|
let images = images.into_inner();
|
|
|
|
let mut textures = Vec::with_capacity(args.material_texture_count.max(1));
|
|
if args.material_texture_count > 0 {
|
|
textures.push(asset_server.load("branding/icon.png"));
|
|
}
|
|
init_textures(&mut textures, args, images);
|
|
|
|
let material_assets = material_assets.into_inner();
|
|
let materials = init_materials(args, &textures, material_assets);
|
|
|
|
let mut cube_resources = CubeResources {
|
|
_textures: textures,
|
|
materials,
|
|
cube_mesh: meshes.add(Cuboid::from_size(Vec3::splat(CUBE_SCALE))),
|
|
color_rng: ChaCha8Rng::seed_from_u64(42),
|
|
material_rng: ChaCha8Rng::seed_from_u64(12),
|
|
velocity_rng: ChaCha8Rng::seed_from_u64(97),
|
|
transform_rng: ChaCha8Rng::seed_from_u64(26),
|
|
};
|
|
|
|
let font = TextFont {
|
|
font_size: FontSize::Px(40.0),
|
|
..Default::default()
|
|
};
|
|
|
|
commands.spawn((
|
|
Camera3d::default(),
|
|
Transform::from_translation(VOLUME_SIZE * 1.3).looking_at(Vec3::ZERO, Vec3::Y),
|
|
));
|
|
|
|
commands.spawn((
|
|
DirectionalLight {
|
|
illuminance: 10000.0,
|
|
shadow_maps_enabled: false,
|
|
..default()
|
|
},
|
|
Transform::from_xyz(1.0, 2.0, 3.0).looking_at(Vec3::ZERO, Vec3::Y),
|
|
));
|
|
|
|
commands.spawn((
|
|
Node {
|
|
position_type: PositionType::Absolute,
|
|
padding: UiRect::all(px(5)),
|
|
..default()
|
|
},
|
|
BackgroundColor(Color::BLACK.with_alpha(0.75)),
|
|
GlobalZIndex(i32::MAX),
|
|
children![(
|
|
Text::default(),
|
|
StatsText,
|
|
children![
|
|
(
|
|
TextSpan::new("Cube Count: "),
|
|
font.clone(),
|
|
TextColor(LIME.into()),
|
|
),
|
|
(TextSpan::new(""), font.clone(), TextColor(AQUA.into())),
|
|
(
|
|
TextSpan::new("\nFPS (raw): "),
|
|
font.clone(),
|
|
TextColor(LIME.into()),
|
|
),
|
|
(TextSpan::new(""), font.clone(), TextColor(AQUA.into())),
|
|
(
|
|
TextSpan::new("\nFPS (SMA): "),
|
|
font.clone(),
|
|
TextColor(LIME.into()),
|
|
),
|
|
(TextSpan::new(""), font.clone(), TextColor(AQUA.into())),
|
|
(
|
|
TextSpan::new("\nFPS (EMA): "),
|
|
font.clone(),
|
|
TextColor(LIME.into()),
|
|
),
|
|
(TextSpan::new(""), font.clone(), TextColor(AQUA.into()))
|
|
]
|
|
)],
|
|
));
|
|
|
|
let mut scheduled = CubeScheduled {
|
|
per_wave: args.per_wave,
|
|
waves: args.waves,
|
|
};
|
|
|
|
if args.benchmark {
|
|
let counter = counter.into_inner();
|
|
for wave in (0..scheduled.waves).rev() {
|
|
spawn_cubes(
|
|
&mut commands,
|
|
args,
|
|
counter,
|
|
scheduled.per_wave,
|
|
&mut cube_resources,
|
|
Some(wave),
|
|
wave,
|
|
);
|
|
}
|
|
scheduled.waves = 0;
|
|
}
|
|
commands.insert_resource(cube_resources);
|
|
commands.insert_resource(scheduled);
|
|
}
|
|
|
|
fn mouse_handler(
|
|
mut commands: Commands,
|
|
args: Res<Args>,
|
|
time: Res<Time>,
|
|
mouse_button_input: Res<ButtonInput<MouseButton>>,
|
|
cube_resources: ResMut<CubeResources>,
|
|
mut counter: ResMut<BevyCounter>,
|
|
mut rng: Local<Option<ChaCha8Rng>>,
|
|
mut wave: Local<usize>,
|
|
) {
|
|
if rng.is_none() {
|
|
*rng = Some(ChaCha8Rng::seed_from_u64(42));
|
|
}
|
|
let rng = rng.as_mut().unwrap();
|
|
|
|
if mouse_button_input.just_released(MouseButton::Left) {
|
|
counter.color = Color::linear_rgb(rng.random(), rng.random(), rng.random());
|
|
}
|
|
|
|
if mouse_button_input.pressed(MouseButton::Left) {
|
|
let spawn_count = (CUBES_PER_SECOND as f64 * time.delta_secs_f64()) as usize;
|
|
spawn_cubes(
|
|
&mut commands,
|
|
args.into_inner(),
|
|
&mut counter,
|
|
spawn_count,
|
|
cube_resources.into_inner(),
|
|
None,
|
|
*wave,
|
|
);
|
|
*wave += 1;
|
|
}
|
|
}
|
|
|
|
fn cube_velocity_transform(
|
|
mut translation: Vec3,
|
|
velocity_rng: &mut ChaCha8Rng,
|
|
waves: Option<usize>,
|
|
dt: f32,
|
|
) -> (Transform, Vec3) {
|
|
let mut velocity = Vec3::new(0., 0., MAX_VELOCITY * velocity_rng.random::<f32>());
|
|
|
|
if let Some(waves) = waves {
|
|
for _ in 0..(waves * (FIXED_TIMESTEP / dt).round() as usize) {
|
|
step_movement(&mut translation, &mut velocity, dt);
|
|
handle_collision(&translation, &mut velocity);
|
|
}
|
|
}
|
|
(Transform::from_translation(translation), velocity)
|
|
}
|
|
|
|
const FIXED_DELTA_TIME: f32 = 1.0 / 60.0;
|
|
|
|
fn spawn_cubes(
|
|
commands: &mut Commands,
|
|
args: &Args,
|
|
counter: &mut BevyCounter,
|
|
spawn_count: usize,
|
|
cube_resources: &mut CubeResources,
|
|
waves_to_simulate: Option<usize>,
|
|
wave: usize,
|
|
) {
|
|
let batch_material = cube_resources.materials[wave % cube_resources.materials.len()].clone();
|
|
|
|
let spawn_y = VOLUME_SIZE.y / 2.0 - HALF_CUBE_SIZE;
|
|
let spawn_z = -VOLUME_SIZE.z / 2.0 + HALF_CUBE_SIZE;
|
|
|
|
let batch = (0..spawn_count)
|
|
.map(|_| {
|
|
let spawn_pos = Vec3::new(
|
|
(cube_resources.transform_rng.random::<f32>() - 0.5) * VOLUME_SIZE.x,
|
|
spawn_y,
|
|
spawn_z,
|
|
);
|
|
|
|
let (transform, velocity) = cube_velocity_transform(
|
|
spawn_pos,
|
|
&mut cube_resources.velocity_rng,
|
|
waves_to_simulate,
|
|
FIXED_DELTA_TIME,
|
|
);
|
|
|
|
let material = if args.vary_per_instance {
|
|
cube_resources
|
|
.materials
|
|
.choose(&mut cube_resources.material_rng)
|
|
.unwrap()
|
|
.clone()
|
|
} else {
|
|
batch_material.clone()
|
|
};
|
|
|
|
(
|
|
Mesh3d(cube_resources.cube_mesh.clone()),
|
|
MeshMaterial3d(material),
|
|
transform,
|
|
Cube { velocity },
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
commands.spawn_batch(batch);
|
|
|
|
counter.count += spawn_count;
|
|
counter.color = Color::linear_rgb(
|
|
cube_resources.color_rng.random(),
|
|
cube_resources.color_rng.random(),
|
|
cube_resources.color_rng.random(),
|
|
);
|
|
}
|
|
|
|
fn step_movement(translation: &mut Vec3, velocity: &mut Vec3, dt: f32) {
|
|
translation.x += velocity.x * dt;
|
|
translation.y += velocity.y * dt;
|
|
translation.z += velocity.z * dt;
|
|
velocity.y += GRAVITY * dt;
|
|
}
|
|
|
|
fn movement_system(
|
|
args: Res<Args>,
|
|
time: Res<Time>,
|
|
mut cube_query: Query<(&mut Cube, &mut Transform)>,
|
|
) {
|
|
let dt = if args.benchmark {
|
|
FIXED_DELTA_TIME
|
|
} else {
|
|
time.delta_secs()
|
|
};
|
|
for (mut cube, mut transform) in &mut cube_query {
|
|
step_movement(&mut transform.translation, &mut cube.velocity, dt);
|
|
}
|
|
}
|
|
|
|
fn handle_collision(translation: &Vec3, velocity: &mut Vec3) {
|
|
if (velocity.x > 0. && translation.x + HALF_CUBE_SIZE > VOLUME_SIZE.x / 2.0)
|
|
|| (velocity.x <= 0. && translation.x - HALF_CUBE_SIZE < -VOLUME_SIZE.x / 2.0)
|
|
{
|
|
velocity.x = -velocity.x;
|
|
}
|
|
if (velocity.z > 0. && translation.z + HALF_CUBE_SIZE > VOLUME_SIZE.z / 2.0)
|
|
|| (velocity.z <= 0. && translation.z - HALF_CUBE_SIZE < -VOLUME_SIZE.z / 2.0)
|
|
{
|
|
velocity.z = -velocity.z;
|
|
}
|
|
|
|
let velocity_y = velocity.y;
|
|
if velocity_y < 0. && translation.y - HALF_CUBE_SIZE < -VOLUME_SIZE.y / 2.0 {
|
|
velocity.y = -velocity_y;
|
|
}
|
|
if translation.y + HALF_CUBE_SIZE > VOLUME_SIZE.y / 2.0 && velocity_y > 0.0 {
|
|
velocity.y = 0.0;
|
|
}
|
|
}
|
|
|
|
fn collision_system(mut cube_query: Query<(&mut Cube, &Transform)>) {
|
|
cube_query.par_iter_mut().for_each(|(mut cube, transform)| {
|
|
handle_collision(&transform.translation, &mut cube.velocity);
|
|
});
|
|
}
|
|
|
|
fn counter_system(
|
|
diagnostics: Res<DiagnosticsStore>,
|
|
counter: Res<BevyCounter>,
|
|
query: Single<Entity, With<StatsText>>,
|
|
mut writer: TextUiWriter,
|
|
) {
|
|
let text = *query;
|
|
|
|
if counter.is_changed() {
|
|
*writer.text(text, 2) = counter.count.to_string();
|
|
}
|
|
|
|
if let Some(fps) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) {
|
|
if let Some(raw) = fps.value() {
|
|
*writer.text(text, 4) = format!("{raw:.2}");
|
|
}
|
|
if let Some(sma) = fps.average() {
|
|
*writer.text(text, 6) = format!("{sma:.2}");
|
|
}
|
|
if let Some(ema) = fps.smoothed() {
|
|
*writer.text(text, 8) = format!("{ema:.2}");
|
|
}
|
|
};
|
|
}
|
|
|
|
fn init_textures(textures: &mut Vec<Handle<Image>>, args: &Args, images: &mut Assets<Image>) {
|
|
let mut color_rng = ChaCha8Rng::seed_from_u64(42);
|
|
while textures.len() < args.material_texture_count {
|
|
let pixel = [
|
|
color_rng.random(),
|
|
color_rng.random(),
|
|
color_rng.random(),
|
|
255,
|
|
];
|
|
textures.push(images.add(Image::new_fill(
|
|
Extent3d {
|
|
width: CUBE_TEXTURE_SIZE as u32,
|
|
height: CUBE_TEXTURE_SIZE as u32,
|
|
depth_or_array_layers: 1,
|
|
},
|
|
TextureDimension::D2,
|
|
&pixel,
|
|
TextureFormat::Rgba8UnormSrgb,
|
|
RenderAssetUsages::RENDER_WORLD,
|
|
)));
|
|
}
|
|
}
|
|
|
|
fn init_materials(
|
|
args: &Args,
|
|
textures: &[Handle<Image>],
|
|
assets: &mut Assets<StandardMaterial>,
|
|
) -> Vec<Handle<StandardMaterial>> {
|
|
let mut capacity = if args.vary_per_instance {
|
|
args.per_wave * args.waves
|
|
} else {
|
|
args.material_texture_count.max(args.waves)
|
|
};
|
|
if !args.benchmark {
|
|
capacity = capacity.max(256);
|
|
}
|
|
capacity = capacity.max(1);
|
|
|
|
let alpha_mode = match args.alpha_mode {
|
|
AlphaMode::Opaque => bevy::prelude::AlphaMode::Opaque,
|
|
AlphaMode::Blend => bevy::prelude::AlphaMode::Blend,
|
|
AlphaMode::AlphaMask => bevy::prelude::AlphaMode::Mask(0.5),
|
|
};
|
|
|
|
let mut materials = Vec::with_capacity(capacity);
|
|
materials.push(assets.add(StandardMaterial {
|
|
base_color: Color::WHITE,
|
|
base_color_texture: textures.first().cloned(),
|
|
alpha_mode,
|
|
..default()
|
|
}));
|
|
|
|
let mut color_rng = ChaCha8Rng::seed_from_u64(42);
|
|
let mut texture_rng = ChaCha8Rng::seed_from_u64(42);
|
|
materials.extend(
|
|
std::iter::repeat_with(|| {
|
|
assets.add(StandardMaterial {
|
|
base_color: Color::linear_rgb(
|
|
color_rng.random(),
|
|
color_rng.random(),
|
|
color_rng.random(),
|
|
),
|
|
base_color_texture: textures.choose(&mut texture_rng).cloned(),
|
|
alpha_mode,
|
|
..default()
|
|
})
|
|
})
|
|
.take(capacity - materials.len()),
|
|
);
|
|
|
|
materials
|
|
}
|