Skip to content

Commit

Permalink
many_cubes: Add no automatic batching and generation of different mes…
Browse files Browse the repository at this point in the history
…hes (#12363)

# Objective

- Enable stressing of more of the material mesh entity draw code paths

## Solution

- Support generation of a number of different mesh assets from the
built-in primitives, and select randomly from them. This breaks batches
based on different meshes.
- Support disabling automatic batching. This skips the batching cost at
the expense of stressing render pass draw command encoding.
- Support enabling directional light cascaded shadow mapping - this is
commonly a big source of slow down in normal scenes.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: François Mockers <francois.mockers@vleue.com>
  • Loading branch information
3 people committed Apr 1, 2024
1 parent 37522fd commit d0a5dda
Showing 1 changed file with 167 additions and 18 deletions.
185 changes: 167 additions & 18 deletions examples/stress_tests/many_cubes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ use argh::FromArgs;
use bevy::{
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
math::{DVec2, DVec3},
pbr::NotShadowCaster,
prelude::*,
render::{
batching::NoAutomaticBatching,
render_asset::RenderAssetUsages,
render_resource::{Extent3d, TextureDimension, TextureFormat},
view::NoFrustumCulling,
Expand All @@ -39,15 +41,27 @@ struct Args {

/// whether to vary the material data in each instance.
#[argh(switch)]
vary_per_instance: bool,
vary_material_data_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,

/// whether to disable frustum culling, for stress testing purposes
/// the number of different meshes from which to randomly select. Clamped to at least 1.
#[argh(option, default = "1")]
mesh_count: usize,

/// whether to disable frustum culling. Stresses queuing and batching as all mesh material entities in the scene are always drawn.
#[argh(switch)]
no_frustum_culling: bool,

/// whether to disable automatic batching. Skips batching resulting in heavy stress on render pass draw command encoding.
#[argh(switch)]
no_automatic_batching: bool,

/// whether to enable directional light cascaded shadow mapping.
#[argh(switch)]
shadows: bool,
}

#[derive(Default, Clone)]
Expand Down Expand Up @@ -109,7 +123,7 @@ const HEIGHT: usize = 200;
fn setup(
mut commands: Commands,
args: Res<Args>,
mut meshes: ResMut<Assets<Mesh>>,
mesh_assets: ResMut<Assets<Mesh>>,
material_assets: ResMut<Assets<StandardMaterial>>,
images: ResMut<Assets<Image>>,
) {
Expand All @@ -118,8 +132,9 @@ fn setup(
let args = args.into_inner();
let images = images.into_inner();
let material_assets = material_assets.into_inner();
let mesh_assets = mesh_assets.into_inner();

let mesh = meshes.add(Cuboid::default());
let meshes = init_meshes(args, mesh_assets);

let material_textures = init_textures(args, images);
let materials = init_materials(args, &material_textures, material_assets);
Expand All @@ -139,23 +154,40 @@ fn setup(
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);
let (mesh, transform) = meshes.choose(&mut material_rng).unwrap();
let mut cube = commands.spawn(PbrBundle {
mesh: mesh.clone(),
material: materials.choose(&mut material_rng).unwrap().clone(),
transform: Transform::from_translation((radius * unit_sphere_p).as_vec3()),
transform: Transform::from_translation((radius * unit_sphere_p).as_vec3())
.looking_at(Vec3::ZERO, Vec3::Y)
.mul_transform(*transform),
..default()
});
if args.no_frustum_culling {
cube.insert(NoFrustumCulling);
}
if args.no_automatic_batching {
cube.insert(NoAutomaticBatching);
}
}

// camera
commands.spawn(Camera3dBundle::default());
// Inside-out box around the meshes onto which shadows are cast (though you cannot see them...)
commands.spawn((
PbrBundle {
mesh: mesh_assets.add(Cuboid::from_size(Vec3::splat(radius as f32 * 2.2))),
material: material_assets.add(StandardMaterial::from(Color::WHITE)),
transform: Transform::from_scale(-Vec3::ONE),
..default()
},
NotShadowCaster,
));
}
_ => {
// 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.
let scale = 2.5;
for x in 0..WIDTH {
for y in 0..HEIGHT {
// introduce spaces to break any kind of moiré pattern
Expand All @@ -164,44 +196,62 @@ fn setup(
}
// cube
commands.spawn(PbrBundle {
mesh: mesh.clone(),
mesh: meshes.choose(&mut material_rng).unwrap().0.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),
transform: Transform::from_xyz((x as f32) * scale, (y as f32) * scale, 0.0),
..default()
});
commands.spawn(PbrBundle {
mesh: mesh.clone(),
mesh: meshes.choose(&mut material_rng).unwrap().0.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,
(x as f32) * scale,
HEIGHT as f32 * scale,
(y as f32) * scale,
),
..default()
});
commands.spawn(PbrBundle {
mesh: mesh.clone(),
mesh: meshes.choose(&mut material_rng).unwrap().0.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),
transform: Transform::from_xyz((x as f32) * scale, 0.0, (y as f32) * scale),
..default()
});
commands.spawn(PbrBundle {
mesh: mesh.clone(),
mesh: meshes.choose(&mut material_rng).unwrap().0.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),
transform: Transform::from_xyz(0.0, (x as f32) * scale, (y as f32) * scale),
..default()
});
}
}
// camera
let center = 0.5 * scale * Vec3::new(WIDTH as f32, HEIGHT as f32, WIDTH as f32);
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(WIDTH as f32, HEIGHT as f32, WIDTH as f32),
transform: Transform::from_translation(center),
..default()
});
// Inside-out box around the meshes onto which shadows are cast (though you cannot see them...)
commands.spawn((
PbrBundle {
mesh: mesh_assets.add(Cuboid::from_size(2.0 * 1.1 * center)),
material: material_assets.add(StandardMaterial::from(Color::WHITE)),
transform: Transform::from_scale(-Vec3::ONE).with_translation(center),
..default()
},
NotShadowCaster,
));
}
}

commands.spawn(DirectionalLightBundle::default());
commands.spawn(DirectionalLightBundle {
directional_light: DirectionalLight {
shadows_enabled: args.shadows,
..default()
},
transform: Transform::IDENTITY.looking_at(Vec3::new(0.0, -1.0, -1.0), Vec3::Y),
..default()
});
}

fn init_textures(args: &Args, images: &mut Assets<Image>) -> Vec<Handle<Image>> {
Expand Down Expand Up @@ -234,7 +284,7 @@ fn init_materials(
textures: &[Handle<Image>],
assets: &mut Assets<StandardMaterial>,
) -> Vec<Handle<StandardMaterial>> {
let capacity = if args.vary_per_instance {
let capacity = if args.vary_material_data_per_instance {
match args.layout {
Layout::Cube => (WIDTH - WIDTH / 10) * (HEIGHT - HEIGHT / 10),
Layout::Sphere => WIDTH * HEIGHT * 4,
Expand Down Expand Up @@ -269,6 +319,105 @@ fn init_materials(
materials
}

fn init_meshes(args: &Args, assets: &mut Assets<Mesh>) -> Vec<(Handle<Mesh>, Transform)> {
let capacity = args.mesh_count.max(1);

// We're seeding the PRNG here to make this example deterministic for testing purposes.
// This isn't strictly required in practical use unless you need your app to be deterministic.
let mut radius_rng = ChaCha8Rng::seed_from_u64(42);
let mut variant = 0;
std::iter::repeat_with(|| {
let radius = radius_rng.gen_range(0.25f32..=0.75f32);
let (handle, transform) = match variant % 15 {
0 => (
assets.add(Cuboid {
half_size: Vec3::splat(radius),
}),
Transform::IDENTITY,
),
1 => (
assets.add(Capsule3d {
radius,
half_length: radius,
}),
Transform::IDENTITY,
),
2 => (
assets.add(Circle { radius }),
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
),
3 => {
let mut vertices = [Vec2::ZERO; 3];
let dtheta = std::f32::consts::TAU / 3.0;
for (i, vertex) in vertices.iter_mut().enumerate() {
let (s, c) = (i as f32 * dtheta).sin_cos();
*vertex = Vec2::new(c, s) * radius;
}
(
assets.add(Triangle2d { vertices }),
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
)
}
4 => (
assets.add(Rectangle {
half_size: Vec2::splat(radius),
}),
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
),
v if (5..=8).contains(&v) => (
assets.add(RegularPolygon {
circumcircle: Circle { radius },
sides: v,
}),
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
),
9 => (
assets.add(Cylinder {
radius,
half_height: radius,
}),
Transform::IDENTITY,
),
10 => (
assets.add(Ellipse {
half_size: Vec2::new(radius, 0.5 * radius),
}),
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
),
11 => (
assets.add(
Plane3d {
normal: Dir3::NEG_Z,
}
.mesh()
.size(radius, radius),
),
Transform::IDENTITY,
),
12 => (assets.add(Sphere { radius }), Transform::IDENTITY),
13 => (
assets.add(Torus {
minor_radius: 0.5 * radius,
major_radius: radius,
}),
Transform::IDENTITY.looking_at(Vec3::Y, Vec3::Y),
),
14 => (
assets.add(Capsule2d {
radius,
half_length: radius,
}),
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
),
_ => unreachable!(),
};
variant += 1;
(handle, transform)
})
.take(capacity)
.collect()
}

// 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/
Expand Down

0 comments on commit d0a5dda

Please sign in to comment.