bevy/examples/stress_tests/many_cubes.rs
Alice Cecile 599e5e4e76
Migrate from LegacyColor to bevy_color::Color (#12163)
# Objective

- As part of the migration process we need to a) see the end effect of
the migration on user ergonomics b) check for serious perf regressions
c) actually migrate the code
- To accomplish this, I'm going to attempt to migrate all of the
remaining user-facing usages of `LegacyColor` in one PR, being careful
to keep a clean commit history.
- Fixes #12056.

## Solution

I've chosen to use the polymorphic `Color` type as our standard
user-facing API.

- [x] Migrate `bevy_gizmos`.
- [x] Take `impl Into<Color>` in all `bevy_gizmos` APIs
- [x] Migrate sprites
- [x] Migrate UI
- [x] Migrate `ColorMaterial`
- [x] Migrate `MaterialMesh2D`
- [x] Migrate fog
- [x] Migrate lights
- [x] Migrate StandardMaterial
- [x] Migrate wireframes
- [x] Migrate clear color
- [x] Migrate text
- [x] Migrate gltf loader
- [x] Register color types for reflection
- [x] Remove `LegacyColor`
- [x] Make sure CI passes

Incidental improvements to ease migration:

- added `Color::srgba_u8`, `Color::srgba_from_array` and friends
- added `set_alpha`, `is_fully_transparent` and `is_fully_opaque` to the
`Alpha` trait
- add and immediately deprecate (lol) `Color::rgb` and friends in favor
of more explicit and consistent `Color::srgb`
- standardized on white and black for most example text colors
- added vector field traits to `LinearRgba`: ~~`Add`, `Sub`,
`AddAssign`, `SubAssign`,~~ `Mul<f32>` and `Div<f32>`. Multiplications
and divisions do not scale alpha. `Add` and `Sub` have been cut from
this PR.
- added `LinearRgba` and `Srgba` `RED/GREEN/BLUE`
- added `LinearRgba_to_f32_array` and `LinearRgba::to_u32`

## Migration Guide

Bevy's color types have changed! Wherever you used a
`bevy::render::Color`, a `bevy::color::Color` is used instead.

These are quite similar! Both are enums storing a color in a specific
color space (or to be more precise, using a specific color model).
However, each of the different color models now has its own type.

TODO...

- `Color::rgba`, `Color::rgb`, `Color::rbga_u8`, `Color::rgb_u8`,
`Color::rgb_from_array` are now `Color::srgba`, `Color::srgb`,
`Color::srgba_u8`, `Color::srgb_u8` and `Color::srgb_from_array`.
- `Color::set_a` and `Color::a` is now `Color::set_alpha` and
`Color::alpha`. These are part of the `Alpha` trait in `bevy_color`.
- `Color::is_fully_transparent` is now part of the `Alpha` trait in
`bevy_color`
- `Color::r`, `Color::set_r`, `Color::with_r` and the equivalents for
`g`, `b` `h`, `s` and `l` have been removed due to causing silent
relatively expensive conversions. Convert your `Color` into the desired
color space, perform your operations there, and then convert it back
into a polymorphic `Color` enum.
- `Color::hex` is now `Srgba::hex`. Call `.into` or construct a
`Color::Srgba` variant manually to convert it.
- `WireframeMaterial`, `ExtractedUiNode`, `ExtractedDirectionalLight`,
`ExtractedPointLight`, `ExtractedSpotLight` and `ExtractedSprite` now
store a `LinearRgba`, rather than a polymorphic `Color`
- `Color::rgb_linear` and `Color::rgba_linear` are now
`Color::linear_rgb` and `Color::linear_rgba`
- The various CSS color constants are no longer stored directly on
`Color`. Instead, they're defined in the `Srgba` color space, and
accessed via `bevy::color::palettes::css`. Call `.into()` on them to
convert them into a `Color` for quick debugging use, and consider using
the much prettier `tailwind` palette for prototyping.
- The `LIME_GREEN` color has been renamed to `LIMEGREEN` to comply with
the standard naming.
- Vector field arithmetic operations on `Color` (add, subtract, multiply
and divide by a f32) have been removed. Instead, convert your colors
into `LinearRgba` space, and perform your operations explicitly there.
This is particularly relevant when working with emissive or HDR colors,
whose color channel values are routinely outside of the ordinary 0 to 1
range.
- `Color::as_linear_rgba_f32` has been removed. Call
`LinearRgba::to_f32_array` instead, converting if needed.
- `Color::as_linear_rgba_u32` has been removed. Call
`LinearRgba::to_u32` instead, converting if needed.
- Several other color conversion methods to transform LCH or HSL colors
into float arrays or `Vec` types have been removed. Please reimplement
these externally or open a PR to re-add them if you found them
particularly useful.
- Various methods on `Color` such as `rgb` or `hsl` to convert the color
into a specific color space have been removed. Convert into
`LinearRgba`, then to the color space of your choice.
- Various implicitly-converting color value methods on `Color` such as
`r`, `g`, `b` or `h` have been removed. Please convert it into the color
space of your choice, then check these properties.
- `Color` no longer implements `AsBindGroup`. Store a `LinearRgba`
internally instead to avoid conversion costs.

---------

Co-authored-by: Alice Cecile <alice.i.cecil@gmail.com>
Co-authored-by: Afonso Lage <lage.afonso@gmail.com>
Co-authored-by: Rob Parrett <robparrett@gmail.com>
Co-authored-by: Zachary Harrold <zac@harrold.com.au>
2024-02-29 19:35:12 +00:00

318 lines
10 KiB
Rust

//! Simple benchmark to test per-entity draw overhead.
//!
//! To measure performance realistically, be sure to run this in release mode.
//! `cargo run --example many_cubes --release`
//!
//! By default, this arranges the meshes in a spherical pattern that
//! distributes the meshes evenly.
//!
//! See `cargo run --example many_cubes --release -- --help` for more options.
use std::{f64::consts::PI, str::FromStr};
use argh::FromArgs;
use bevy::{
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
math::{DVec2, DVec3},
prelude::*,
render::{
render_asset::RenderAssetUsages,
render_resource::{Extent3d, TextureDimension, TextureFormat},
},
window::{PresentMode, WindowPlugin, WindowResolution},
winit::{UpdateMode, WinitSettings},
};
use rand::{rngs::StdRng, seq::SliceRandom, Rng, SeedableRng};
#[derive(FromArgs, Resource)]
/// `many_cubes` stress test
struct Args {
/// how the cube instances should be positioned.
#[argh(option, default = "Layout::Sphere")]
layout: Layout,
/// whether to step the camera animation by a fixed amount such that each frame is the same across runs.
#[argh(switch)]
benchmark: bool,
/// 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 base color. 0 means no textures.
#[argh(option, default = "0")]
material_texture_count: usize,
}
#[derive(Default, Clone)]
enum Layout {
Cube,
#[default]
Sphere,
}
impl FromStr for Layout {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"cube" => Ok(Self::Cube),
"sphere" => Ok(Self::Sphere),
_ => Err(format!(
"Unknown layout value: '{}', valid options: 'cube', 'sphere'",
s
)),
}
}
}
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 {
present_mode: PresentMode::AutoNoVsync,
resolution: WindowResolution::new(1920.0, 1080.0)
.with_scale_factor_override(1.0),
..default()
}),
..default()
}),
FrameTimeDiagnosticsPlugin,
LogDiagnosticsPlugin::default(),
))
.insert_resource(WinitSettings {
focused_mode: UpdateMode::Continuous,
unfocused_mode: UpdateMode::Continuous,
})
.insert_resource(args)
.add_systems(Startup, setup)
.add_systems(Update, (move_camera, print_mesh_count))
.run();
}
const WIDTH: usize = 200;
const HEIGHT: usize = 200;
fn setup(
mut commands: Commands,
args: Res<Args>,
mut meshes: ResMut<Assets<Mesh>>,
material_assets: ResMut<Assets<StandardMaterial>>,
images: ResMut<Assets<Image>>,
) {
warn!(include_str!("warning_string.txt"));
let args = args.into_inner();
let images = images.into_inner();
let material_assets = material_assets.into_inner();
let mesh = meshes.add(Cuboid::default());
let material_textures = init_textures(args, images);
let materials = init_materials(args, &material_textures, material_assets);
let mut material_rng = StdRng::seed_from_u64(42);
match args.layout {
Layout::Sphere => {
// NOTE: This pattern is good for testing performance of culling as it provides roughly
// the same number of visible meshes regardless of the viewing angle.
const N_POINTS: usize = WIDTH * HEIGHT * 4;
// NOTE: f64 is used to avoid precision issues that produce visual artifacts in the distribution
let radius = WIDTH as f64 * 2.5;
let golden_ratio = 0.5f64 * (1.0f64 + 5.0f64.sqrt());
for i in 0..N_POINTS {
let spherical_polar_theta_phi =
fibonacci_spiral_on_sphere(golden_ratio, i, N_POINTS);
let unit_sphere_p = spherical_polar_to_cartesian(spherical_polar_theta_phi);
commands.spawn(PbrBundle {
mesh: mesh.clone(),
material: materials.choose(&mut material_rng).unwrap().clone(),
transform: Transform::from_translation((radius * unit_sphere_p).as_vec3()),
..default()
});
}
// camera
commands.spawn(Camera3dBundle::default());
}
_ => {
// NOTE: This pattern is good for demonstrating that frustum culling is working correctly
// as the number of visible meshes rises and falls depending on the viewing angle.
for x in 0..WIDTH {
for y in 0..HEIGHT {
// introduce spaces to break any kind of moiré pattern
if x % 10 == 0 || y % 10 == 0 {
continue;
}
// cube
commands.spawn(PbrBundle {
mesh: mesh.clone(),
material: materials.choose(&mut material_rng).unwrap().clone(),
transform: Transform::from_xyz((x as f32) * 2.5, (y as f32) * 2.5, 0.0),
..default()
});
commands.spawn(PbrBundle {
mesh: mesh.clone(),
material: materials.choose(&mut material_rng).unwrap().clone(),
transform: Transform::from_xyz(
(x as f32) * 2.5,
HEIGHT as f32 * 2.5,
(y as f32) * 2.5,
),
..default()
});
commands.spawn(PbrBundle {
mesh: mesh.clone(),
material: materials.choose(&mut material_rng).unwrap().clone(),
transform: Transform::from_xyz((x as f32) * 2.5, 0.0, (y as f32) * 2.5),
..default()
});
commands.spawn(PbrBundle {
mesh: mesh.clone(),
material: materials.choose(&mut material_rng).unwrap().clone(),
transform: Transform::from_xyz(0.0, (x as f32) * 2.5, (y as f32) * 2.5),
..default()
});
}
}
// camera
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(WIDTH as f32, HEIGHT as f32, WIDTH as f32),
..default()
});
}
}
commands.spawn(DirectionalLightBundle::default());
}
fn init_textures(args: &Args, images: &mut Assets<Image>) -> Vec<Handle<Image>> {
let mut color_rng = StdRng::seed_from_u64(42);
let color_bytes: Vec<u8> = (0..(args.material_texture_count * 4))
.map(|i| if (i % 4) == 3 { 255 } else { color_rng.gen() })
.collect();
color_bytes
.chunks(4)
.map(|pixel| {
images.add(Image::new_fill(
Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
TextureDimension::D2,
pixel,
TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::RENDER_WORLD,
))
})
.collect()
}
fn init_materials(
args: &Args,
textures: &[Handle<Image>],
assets: &mut Assets<StandardMaterial>,
) -> Vec<Handle<StandardMaterial>> {
let capacity = if args.vary_per_instance {
match args.layout {
Layout::Cube => (WIDTH - WIDTH / 10) * (HEIGHT - HEIGHT / 10),
Layout::Sphere => WIDTH * HEIGHT * 4,
}
} else {
args.material_texture_count
}
.max(1);
let mut materials = Vec::with_capacity(capacity);
materials.push(assets.add(StandardMaterial {
base_color: Color::WHITE,
base_color_texture: textures.first().cloned(),
..default()
}));
let mut color_rng = StdRng::seed_from_u64(42);
let mut texture_rng = StdRng::seed_from_u64(42);
materials.extend(
std::iter::repeat_with(|| {
assets.add(StandardMaterial {
base_color: Color::srgb_u8(color_rng.gen(), color_rng.gen(), color_rng.gen()),
base_color_texture: textures.choose(&mut texture_rng).cloned(),
..default()
})
})
.take(capacity - materials.len()),
);
materials
}
// NOTE: This epsilon value is apparently optimal for optimizing for the average
// nearest-neighbor distance. See:
// http://extremelearning.com.au/how-to-evenly-distribute-points-on-a-sphere-more-effectively-than-the-canonical-fibonacci-lattice/
// for details.
const EPSILON: f64 = 0.36;
fn fibonacci_spiral_on_sphere(golden_ratio: f64, i: usize, n: usize) -> DVec2 {
DVec2::new(
PI * 2. * (i as f64 / golden_ratio),
(1.0 - 2.0 * (i as f64 + EPSILON) / (n as f64 - 1.0 + 2.0 * EPSILON)).acos(),
)
}
fn spherical_polar_to_cartesian(p: DVec2) -> DVec3 {
let (sin_theta, cos_theta) = p.x.sin_cos();
let (sin_phi, cos_phi) = p.y.sin_cos();
DVec3::new(cos_theta * sin_phi, sin_theta * sin_phi, cos_phi)
}
// System for rotating the camera
fn move_camera(
time: Res<Time>,
args: Res<Args>,
mut camera_query: Query<&mut Transform, With<Camera>>,
) {
let mut camera_transform = camera_query.single_mut();
let delta = 0.15
* if args.benchmark {
1.0 / 60.0
} else {
time.delta_seconds()
};
camera_transform.rotate_z(delta);
camera_transform.rotate_x(delta);
}
// System for printing the number of meshes on every tick of the timer
fn print_mesh_count(
time: Res<Time>,
mut timer: Local<PrintingTimer>,
sprites: Query<(&Handle<Mesh>, &ViewVisibility)>,
) {
timer.tick(time.delta());
if timer.just_finished() {
info!(
"Meshes: {} - Visible Meshes {}",
sprites.iter().len(),
sprites.iter().filter(|(_, vis)| vis.get()).count(),
);
}
}
#[derive(Deref, DerefMut)]
struct PrintingTimer(Timer);
impl Default for PrintingTimer {
fn default() -> Self {
Self(Timer::from_seconds(1.0, TimerMode::Repeating))
}
}