From d0a5ddacd9bee2aee304c85fe7e2291da54c356a Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Tue, 2 Apr 2024 00:05:52 +0200 Subject: [PATCH] many_cubes: Add no automatic batching and generation of different meshes (#12363) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 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 Co-authored-by: François Mockers --- examples/stress_tests/many_cubes.rs | 185 +++++++++++++++++++++++++--- 1 file changed, 167 insertions(+), 18 deletions(-) diff --git a/examples/stress_tests/many_cubes.rs b/examples/stress_tests/many_cubes.rs index d0079d4eff..510a83612a 100644 --- a/examples/stress_tests/many_cubes.rs +++ b/examples/stress_tests/many_cubes.rs @@ -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, @@ -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)] @@ -109,7 +123,7 @@ const HEIGHT: usize = 200; fn setup( mut commands: Commands, args: Res, - mut meshes: ResMut>, + mesh_assets: ResMut>, material_assets: ResMut>, images: ResMut>, ) { @@ -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); @@ -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 @@ -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) -> Vec> { @@ -234,7 +284,7 @@ fn init_materials( textures: &[Handle], assets: &mut Assets, ) -> Vec> { - 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, @@ -269,6 +319,105 @@ fn init_materials( materials } +fn init_meshes(args: &Args, assets: &mut Assets) -> Vec<(Handle, 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/