diff --git a/.gitignore b/.gitignore index cdde3c7eec3c4..b93530654e4e0 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,8 @@ assets/**/*.meta crates/bevy_asset/imported_assets imported_assets .web-asset-cache +examples/large_scenes/bistro/assets/* +examples/large_scenes/caldera_hotel/assets/* # Bevy Examples example_showcase_config.ron @@ -33,3 +35,6 @@ assets/scenes/load_scene_example-new.scn.ron # Generated by "examples/window/screenshot.rs" **/screenshot-*.png + +# Generated by "examples/large_scenes" +compressed_texture_cache diff --git a/Cargo.toml b/Cargo.toml index c5632881e1dcc..54f99d37b5a77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,11 @@ members = [ "examples/no_std/*", # Examples of compiling Bevy with automatic reflect type registration for platforms without `inventory` support. "examples/reflection/auto_register_static", + # Examples of large bevy scenes. + "examples/large_scenes/bistro", + "examples/large_scenes/caldera_hotel", + # Mipmap generator for testing large bevy scenes with mips and texture compression. + "examples/large_scenes/mipmap_generator", # Benchmarks "benches", # Internal tools that are not published. diff --git a/examples/large_scenes/bistro/Cargo.toml b/examples/large_scenes/bistro/Cargo.toml new file mode 100644 index 0000000000000..3e306029ed9bd --- /dev/null +++ b/examples/large_scenes/bistro/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "bistro" +version = "0.1.0" +edition = "2024" +publish = false +license = "MIT OR Apache-2.0" + +[dependencies] +bevy = { path = "../../../", features = [ + "bevy_camera_controller", + "free_camera", +] } +mipmap_generator = { path = "../mipmap_generator" } + +argh = "0.1.12" + +[features] + +default = ["debug_text"] +debug_text = ["bevy/bevy_ui"] +pbr_transmission_textures = ["bevy/pbr_transmission_textures"] +pbr_multi_layer_material_textures = ["bevy/pbr_multi_layer_material_textures"] +pbr_anisotropy_texture = ["bevy/pbr_anisotropy_texture"] +pbr_specular_textures = ["bevy/pbr_specular_textures"] diff --git a/examples/large_scenes/bistro/README.md b/examples/large_scenes/bistro/README.md new file mode 100644 index 0000000000000..d2a512ad0ac82 --- /dev/null +++ b/examples/large_scenes/bistro/README.md @@ -0,0 +1,58 @@ +# Bistro Example + +Download Amazon Lumberyard version of bistro [from developer.nvidia.com]() (or see link below for processed glTF files with instancing) + +Reexport BistroExterior.fbx and BistroInterior_Wine.fbx as GLTF files (in .gltf + .bin + textures format). Move the files into the respective bistro_exterior and bistro_interior_wine folders. + +- Press 1, 2 or 3 for various camera positions. +- Press B for benchmark. +- Press to animate camera along path. + +Run with texture compression while caching compressed images to disk for faster startup times: +`cargo run -p bistro --release --features mipmap_generator/compress -- --cache` + +```console +Options: +--no-gltf-lights disable glTF lights +--minimal disable bloom, AO, AA, shadows +--compress compress textures (if they are not already, requires + compress feature) +--low-quality-compression + if low_quality_compression is set, only 0.5 byte/px formats + will be used (BC1, BC4) unless the alpha channel is in use, + then BC3 will be used. When low quality is set, compression + is generally faster than CompressionSpeed::UltraFast and + CompressionSpeed is ignored. +--cache compressed texture cache (requires compress feature) +--count quantity of bistros +--spin spin the bistros and camera +--hide-frame-time don't show frame time +--deferred use deferred shading +--no-frustum-culling + disable all frustum culling. Stresses queuing and batching + as all mesh material entities in the scene are always drawn. +--no-automatic-batching + disable automatic batching. Skips batching resulting in + heavy stress on render pass draw command encoding. +--no-view-occlusion-culling + disable gpu occlusion culling for the camera +--no-shadow-occlusion-culling + disable gpu occlusion culling for the directional light +--no-indirect-drawing + disable indirect drawing. +--no-cpu-culling disable CPU culling. +--help, help display usage information +``` + +[Alternate processed files with instancing (glTF files on discord):](https://discord.com/channels/691052431525675048/1237853896471220314/1237859248067575910) + +- Fixed most of the metallic from fbx issue by using a script that makes everything dielectric unless it has metal in the name of the material +- Made the plants alpha clip instead of blend +- Setup the glassware/liquid materials correctly +- Mesh origins are at individual bounding box center instead of world origin +- Removed duplicate vertices (There were lots of odd cases, often making one instance not match another that would otherwise exactly match) +- Made the scene use instances (unique mesh count 3880 -> 1188) +- Removed 2 cases where duplicated meshes were overlapping +- Setup some of the interior/exterior lights with actual sources +- Setup some basic fake GI +- Use included scene HDRI for IBL diff --git a/examples/large_scenes/bistro/src/main.rs b/examples/large_scenes/bistro/src/main.rs new file mode 100644 index 0000000000000..304e8a47d74bb --- /dev/null +++ b/examples/large_scenes/bistro/src/main.rs @@ -0,0 +1,638 @@ +// Press B for benchmark. +// Preferably after frame time is reading consistently, rust-analyzer has calmed down, and with locked gpu clocks. + +use std::{ + f32::consts::PI, + ops::{Add, Mul, Sub}, + path::PathBuf, + time::Instant, +}; + +use argh::FromArgs; +use bevy::{ + anti_alias::taa::TemporalAntiAliasing, + camera::visibility::{NoCpuCulling, NoFrustumCulling}, + camera_controller::free_camera::{FreeCamera, FreeCameraPlugin}, + core_pipeline::prepass::{DeferredPrepass, DepthPrepass}, + diagnostic::DiagnosticsStore, + light::TransmittedShadowReceiver, + pbr::{DefaultOpaqueRendererMethod, ScreenSpaceAmbientOcclusion}, + post_process::bloom::Bloom, + render::{ + batching::NoAutomaticBatching, experimental::occlusion_culling::OcclusionCulling, + render_resource::Face, view::NoIndirectDrawing, + }, + scene::SceneInstanceReady, +}; +use bevy::{ + camera::ScreenSpaceTransmissionQuality, light::CascadeShadowConfigBuilder, render::view::Hdr, +}; +use bevy::{ + diagnostic::FrameTimeDiagnosticsPlugin, + prelude::*, + window::{PresentMode, WindowResolution}, + winit::{UpdateMode, WinitSettings}, +}; +use mipmap_generator::{ + generate_mipmaps, MipmapGeneratorDebugTextPlugin, MipmapGeneratorPlugin, + MipmapGeneratorSettings, +}; + +use crate::light_consts::lux; + +#[derive(FromArgs, Resource, Clone)] +/// Config +pub struct Args { + /// disable glTF lights + #[argh(switch)] + no_gltf_lights: bool, + + /// disable bloom, AO, AA, shadows + #[argh(switch)] + minimal: bool, + + /// compress textures (if they are not already, requires compress feature) + #[argh(switch)] + compress: bool, + + /// if low_quality_compression is set, only 0.5 byte/px formats will be used (BC1, BC4) unless the alpha channel is in use, then BC3 will be used. + /// When low quality is set, compression is generally faster than CompressionSpeed::UltraFast and CompressionSpeed is ignored. + #[argh(switch)] + low_quality_compression: bool, + + /// compressed texture cache (requires compress feature) + #[argh(switch)] + cache: bool, + + /// quantity of bistros + #[argh(option, default = "1")] + count: u32, + + /// spin the bistros and camera + #[argh(switch)] + spin: bool, + + /// don't show frame time + #[argh(switch)] + hide_frame_time: bool, + + /// use deferred shading + #[argh(switch)] + deferred: bool, + + /// disable all frustum culling. Stresses queuing and batching as all mesh material entities in the scene are always drawn. + #[argh(switch)] + no_frustum_culling: bool, + + /// disable automatic batching. Skips batching resulting in heavy stress on render pass draw command encoding. + #[argh(switch)] + no_automatic_batching: bool, + + /// disable gpu occlusion culling for the camera + #[argh(switch)] + no_view_occlusion_culling: bool, + + /// disable gpu occlusion culling for the directional light + #[argh(switch)] + no_shadow_occlusion_culling: bool, + + /// disable indirect drawing. + #[argh(switch)] + no_indirect_drawing: bool, + + /// disable CPU culling. + #[argh(switch)] + no_cpu_culling: bool, +} + +pub fn main() { + let args: Args = argh::from_env(); + + let mut app = App::new(); + + app.init_resource::() + .init_resource::() + .insert_resource(GlobalAmbientLight::NONE) + .insert_resource(args.clone()) + .insert_resource(ClearColor(Color::srgb(1.75, 1.9, 1.99))) + .insert_resource(WinitSettings { + focused_mode: UpdateMode::Continuous, + unfocused_mode: UpdateMode::Continuous, + }) + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + present_mode: PresentMode::Immediate, + resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0), + ..default() + }), + ..default() + })) + // Generating mipmaps takes a minute + // Mipmap generation be skipped if ktx2 is used + .insert_resource(MipmapGeneratorSettings { + anisotropic_filtering: 16, + compression: args.compress.then(Default::default), + compressed_image_data_cache_path: if args.cache { + Some(PathBuf::from("compressed_texture_cache")) + } else { + None + }, + low_quality: args.low_quality_compression, + ..default() + }) + .add_plugins(( + FrameTimeDiagnosticsPlugin { + max_history_length: 1000, + ..default() + }, + MipmapGeneratorPlugin, + MipmapGeneratorDebugTextPlugin, + FreeCameraPlugin, + )) + .add_systems(Startup, setup) + .add_systems( + Update, + ( + generate_mipmaps::, + input, + run_animation, + spin, + frame_time_system, + benchmark, + ) + .chain(), + ); + + if args.no_frustum_culling { + app.add_systems(Update, add_no_frustum_culling); + } + + if args.deferred { + app.insert_resource(DefaultOpaqueRendererMethod::deferred()); + } + + app.run(); +} + +#[derive(Component)] +pub struct Spin; + +#[derive(Component)] +struct FrameTimeText; + +pub fn setup(mut commands: Commands, asset_server: Res, args: Res) { + println!("Loading models, generating mipmaps"); + + let bistro_exterior = asset_server.load("bistro_exterior/BistroExterior.gltf#Scene0"); + commands + .spawn((SceneRoot(bistro_exterior.clone()), Spin)) + .observe(proc_scene); + + let bistro_interior = asset_server.load("bistro_interior_wine/BistroInterior_Wine.gltf#Scene0"); + commands + .spawn((SceneRoot(bistro_interior.clone()), Spin)) + .observe(proc_scene); + + let mut count = 0; + if args.count > 1 { + let quantity = args.count - 1; + + let side = (quantity as f32).sqrt().ceil() as i32 / 2; + + 'outer: for x in -side..=side { + for z in -side..=side { + if count >= quantity { + break 'outer; + } + if x == 0 && z == 0 { + continue; + } + commands + .spawn(( + SceneRoot(bistro_exterior.clone()), + Transform::from_xyz(x as f32 * 150.0, 0.0, z as f32 * 150.0), + Spin, + )) + .observe(proc_scene); + commands + .spawn(( + SceneRoot(bistro_interior.clone()), + Transform::from_xyz(x as f32 * 150.0, 0.3, z as f32 * 150.0 - 0.2), + Spin, + )) + .observe(proc_scene); + count += 1; + } + } + } + + if !args.no_gltf_lights { + // In Repo glTF + commands.spawn(( + SceneRoot(asset_server.load("BistroExteriorFakeGI.gltf#Scene0")), + Spin, + )); + } + + // Sun + commands + .spawn(( + Transform::from_rotation(Quat::from_euler(EulerRot::XYZ, PI * -0.35, PI * -0.13, 0.0)), + DirectionalLight { + color: Color::srgb(1.0, 0.87, 0.78), + illuminance: lux::FULL_DAYLIGHT, + shadows_enabled: !args.minimal, + shadow_depth_bias: 0.1, + shadow_normal_bias: 0.2, + ..default() + }, + CascadeShadowConfigBuilder { + num_cascades: 3, + minimum_distance: 0.05, + maximum_distance: 100.0, + first_cascade_far_bound: 10.0, + overlap_proportion: 0.2, + } + .build(), + )) + .insert_if(OcclusionCulling, || !args.no_shadow_occlusion_culling); + + // Camera + let mut cam = commands.spawn(( + Msaa::Off, + Camera3d { + screen_space_specular_transmission_steps: 0, + screen_space_specular_transmission_quality: ScreenSpaceTransmissionQuality::Low, + ..default() + }, + Hdr, + Transform::from_xyz(-10.5, 1.7, -1.0).looking_at(Vec3::new(0.0, 3.5, 0.0), Vec3::Y), + Projection::Perspective(PerspectiveProjection { + fov: std::f32::consts::PI / 3.0, + near: 0.1, + far: 1000.0, + aspect_ratio: 1.0, + ..Default::default() + }), + EnvironmentMapLight { + diffuse_map: asset_server.load("environment_maps/san_giuseppe_bridge_4k_diffuse.ktx2"), + specular_map: asset_server + .load("environment_maps/san_giuseppe_bridge_4k_specular.ktx2"), + intensity: 600.0, + ..default() + }, + FreeCamera::default(), + Spin, + )); + cam.insert_if(DepthPrepass, || args.deferred) + .insert_if(DeferredPrepass, || args.deferred) + .insert_if(OcclusionCulling, || !args.no_view_occlusion_culling) + .insert_if(NoFrustumCulling, || args.no_frustum_culling) + .insert_if(NoAutomaticBatching, || args.no_automatic_batching) + .insert_if(NoIndirectDrawing, || args.no_indirect_drawing) + .insert_if(NoCpuCulling, || args.no_cpu_culling); + if !args.minimal { + cam.insert(( + Bloom { + intensity: 0.02, + ..default() + }, + TemporalAntiAliasing::default(), + )) + .insert(ScreenSpaceAmbientOcclusion::default()); + } + + if !args.hide_frame_time { + commands + .spawn(( + Node { + left: Val::Px(1.5), + top: Val::Px(1.5), + ..default() + }, + GlobalZIndex(-1), + )) + .with_children(|parent| { + parent.spawn((Text::new(""), TextColor(Color::BLACK), FrameTimeText)); + }); + commands.spawn(Node::default()).with_children(|parent| { + parent.spawn((Text::new(""), TextColor(Color::WHITE), FrameTimeText)); + }); + } +} + +pub fn all_children( + children: &Children, + children_query: &Query<&Children>, + closure: &mut F, +) { + for child in children { + if let Ok(children) = children_query.get(*child) { + all_children(children, children_query, closure); + } + closure(*child); + } +} + +#[allow(clippy::type_complexity, clippy::too_many_arguments)] +pub fn proc_scene( + scene_ready: On, + mut commands: Commands, + children: Query<&Children>, + has_std_mat: Query<&MeshMaterial3d>, + mut materials: ResMut>, + lights: Query, With, With)>>, + cameras: Query>, + args: Res, +) { + for entity in children.iter_descendants(scene_ready.entity) { + // Sponza needs flipped normals + if let Ok(mat_h) = has_std_mat.get(entity) + && let Some(mat) = materials.get_mut(mat_h) + { + mat.flip_normal_map_y = true; + match mat.alpha_mode { + AlphaMode::Mask(_) => { + mat.diffuse_transmission = 0.6; + mat.double_sided = true; + mat.cull_mode = None; + mat.thickness = 0.2; + commands.entity(entity).insert(TransmittedShadowReceiver); + } + AlphaMode::Opaque => { + mat.double_sided = false; + mat.cull_mode = Some(Face::Back); + } + _ => (), + } + } + + if args.no_gltf_lights { + // Has a bunch of lights by default + if lights.get(entity).is_ok() { + commands.entity(entity).despawn(); + } + } + + // Has a bunch of cameras by default + if cameras.get(entity).is_ok() { + commands.entity(entity).despawn(); + } + } +} + +#[derive(Resource, Deref, DerefMut)] +struct CameraPositions([Transform; 3]); + +impl Default for CameraPositions { + fn default() -> Self { + Self([ + Transform { + translation: Vec3::new(-10.5, 1.7, -1.0), + rotation: Quat::from_array([-0.05678932, 0.7372272, -0.062454797, -0.670351]), + scale: Vec3::ONE, + }, + Transform { + translation: Vec3::new(56.23809, 2.9985719, 28.96291), + rotation: Quat::from_array([0.0020175162, 0.35272083, -0.0007605003, 0.93572617]), + scale: Vec3::ONE, + }, + Transform { + translation: Vec3::new(5.7861176, 3.3475509, -8.821455), + rotation: Quat::from_array([-0.0049382094, -0.98193514, -0.025878597, 0.18737496]), + scale: Vec3::ONE, + }, + ]) + } +} + +const ANIM_SPEED: f32 = 0.2; +const ANIM_HYSTERESIS: f32 = 0.1; // EMA/LPF + +const ANIM_CAM: [Transform; 3] = [ + Transform { + translation: Vec3::new(-6.414026, 8.179898, -23.550516), + rotation: Quat::from_array([-0.016413536, -0.88136566, -0.030704278, 0.4711502]), + scale: Vec3::ONE, + }, + Transform { + translation: Vec3::new(-14.752817, 6.279289, 5.691277), + rotation: Quat::from_array([-0.031593435, -0.516736, -0.019086324, 0.8553488]), + scale: Vec3::ONE, + }, + Transform { + translation: Vec3::new(5.1539426, 8.142523, 16.436222), + rotation: Quat::from_array([-0.07907656, -0.07581916, -0.006031934, 0.99396276]), + scale: Vec3::ONE, + }, +]; + +fn input( + input: Res>, + mut camera: Query<&mut Transform, With>, + positions: Res, +) { + let Ok(mut transform) = camera.single_mut() else { + return; + }; + if input.just_pressed(KeyCode::KeyI) { + info!("{:?}", transform); + } + if input.just_pressed(KeyCode::Digit1) { + *transform = positions[0] + } + if input.just_pressed(KeyCode::Digit2) { + *transform = positions[1] + } + if input.just_pressed(KeyCode::Digit3) { + *transform = positions[2] + } +} + +fn lerp(a: T, b: T, t: f32) -> T +where + T: Copy + Add + Sub + Mul, +{ + a + (b - a) * t +} + +fn follow_path(points: &[Transform], progress: f32) -> Transform { + let total_segments = (points.len() - 1) as f32; + let progress = progress.clamp(0.0, 1.0); + let mut segment_progress = progress * total_segments; + let segment_index = segment_progress.floor() as usize; + segment_progress -= segment_index as f32; + let a = points[segment_index]; + let b = points[(segment_index + 1).min(points.len() - 1)]; + Transform { + translation: lerp(a.translation, b.translation, segment_progress), + rotation: lerp(a.rotation, b.rotation, segment_progress), + scale: lerp(a.scale, b.scale, segment_progress), + } +} + +fn run_animation( + time: Res