Path: blob/main/examples/large_scenes/bistro/src/main.rs
9328 views
// Press B for benchmark.1// Preferably after frame time is reading consistently, rust-analyzer has calmed down, and with locked gpu clocks.23use std::{4f32::consts::PI,5ops::{Add, Mul, Sub},6path::PathBuf,7time::Instant,8};910use argh::FromArgs;11use bevy::pbr::ContactShadows;12use bevy::{13anti_alias::taa::TemporalAntiAliasing,14camera::visibility::{NoCpuCulling, NoFrustumCulling},15camera_controller::free_camera::{FreeCamera, FreeCameraPlugin},16core_pipeline::prepass::{DeferredPrepass, DepthPrepass},17diagnostic::DiagnosticsStore,18light::TransmittedShadowReceiver,19pbr::{20DefaultOpaqueRendererMethod, ScreenSpaceAmbientOcclusion, ScreenSpaceTransmission,21ScreenSpaceTransmissionQuality,22},23post_process::bloom::Bloom,24render::{25batching::NoAutomaticBatching, occlusion_culling::OcclusionCulling, render_resource::Face,26view::NoIndirectDrawing,27},28scene::SceneInstanceReady,29};30use bevy::{31camera::Hdr,32diagnostic::FrameTimeDiagnosticsPlugin,33light::CascadeShadowConfigBuilder,34prelude::*,35window::{PresentMode, WindowResolution},36winit::WinitSettings,37};38use mipmap_generator::{39generate_mipmaps, MipmapGeneratorDebugTextPlugin, MipmapGeneratorPlugin,40MipmapGeneratorSettings,41};4243use crate::light_consts::lux;4445#[derive(FromArgs, Resource, Clone)]46/// Config47pub struct Args {48/// disable glTF lights49#[argh(switch)]50no_gltf_lights: bool,5152/// disable bloom, AO, AA, shadows53#[argh(switch)]54minimal: bool,5556/// compress textures (if they are not already, requires compress feature)57#[argh(switch)]58compress: bool,5960/// 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.61/// When low quality is set, compression is generally faster than CompressionSpeed::UltraFast and CompressionSpeed is ignored.62#[argh(switch)]63low_quality_compression: bool,6465/// compressed texture cache (requires compress feature)66#[argh(switch)]67cache: bool,6869/// quantity of bistros70#[argh(option, default = "1")]71count: u32,7273/// spin the bistros and camera74#[argh(switch)]75spin: bool,7677/// don't show frame time78#[argh(switch)]79hide_frame_time: bool,8081/// use deferred shading82#[argh(switch)]83deferred: bool,8485/// disable all frustum culling. Stresses queuing and batching as all mesh material entities in the scene are always drawn.86#[argh(switch)]87no_frustum_culling: bool,8889/// disable automatic batching. Skips batching resulting in heavy stress on render pass draw command encoding.90#[argh(switch)]91no_automatic_batching: bool,9293/// disable gpu occlusion culling for the camera94#[argh(switch)]95no_view_occlusion_culling: bool,9697/// disable gpu occlusion culling for the directional light98#[argh(switch)]99no_shadow_occlusion_culling: bool,100101/// disable indirect drawing.102#[argh(switch)]103no_indirect_drawing: bool,104105/// disable CPU culling.106#[argh(switch)]107no_cpu_culling: bool,108109/// disable mip map generation.110#[argh(switch)]111no_mip_generation: bool,112}113114pub fn main() {115let args: Args = argh::from_env();116117let mut app = App::new();118119app.init_resource::<CameraPositions>()120.init_resource::<FrameLowHigh>()121.insert_resource(GlobalAmbientLight::NONE)122.insert_resource(args.clone())123.insert_resource(ClearColor(Color::srgb(1.75, 1.9, 1.99)))124.insert_resource(WinitSettings::continuous())125.add_plugins(DefaultPlugins.set(WindowPlugin {126primary_window: Some(Window {127present_mode: PresentMode::Immediate,128resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),129..default()130}),131..default()132}))133.add_plugins((134FrameTimeDiagnosticsPlugin {135max_history_length: 1000,136..default()137},138FreeCameraPlugin,139))140.add_systems(Startup, setup)141.add_systems(142Update,143(input, run_animation, spin, frame_time_system, benchmark).chain(),144);145146if !args.no_mip_generation {147app.add_plugins((MipmapGeneratorPlugin, MipmapGeneratorDebugTextPlugin))148// Generating mipmaps takes a minute149// Mipmap generation be skipped if ktx2 is used150.insert_resource(MipmapGeneratorSettings {151anisotropic_filtering: 16,152compression: args.compress.then(Default::default),153compressed_image_data_cache_path: if args.cache {154Some(PathBuf::from("compressed_texture_cache"))155} else {156None157},158low_quality: args.low_quality_compression,159..default()160})161.add_systems(Update, generate_mipmaps::<StandardMaterial>);162}163164if args.no_frustum_culling {165app.add_systems(Update, add_no_frustum_culling);166}167168if args.deferred {169app.insert_resource(DefaultOpaqueRendererMethod::deferred());170}171172app.run();173}174175#[derive(Component)]176pub struct Spin;177178#[derive(Component)]179struct FrameTimeText;180181pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<Args>) {182println!("Loading models, generating mipmaps");183184let bistro_exterior = asset_server.load("bistro_exterior/BistroExterior.gltf#Scene0");185commands186.spawn((SceneRoot(bistro_exterior.clone()), Spin))187.observe(proc_scene);188189let bistro_interior = asset_server.load("bistro_interior_wine/BistroInterior_Wine.gltf#Scene0");190commands191.spawn((SceneRoot(bistro_interior.clone()), Spin))192.observe(proc_scene);193194let mut count = 0;195if args.count > 1 {196let quantity = args.count - 1;197198let side = (quantity as f32).sqrt().ceil() as i32 / 2;199200'outer: for x in -side..=side {201for z in -side..=side {202if count >= quantity {203break 'outer;204}205if x == 0 && z == 0 {206continue;207}208commands209.spawn((210SceneRoot(bistro_exterior.clone()),211Transform::from_xyz(x as f32 * 150.0, 0.0, z as f32 * 150.0),212Spin,213))214.observe(proc_scene);215commands216.spawn((217SceneRoot(bistro_interior.clone()),218Transform::from_xyz(x as f32 * 150.0, 0.3, z as f32 * 150.0 - 0.2),219Spin,220))221.observe(proc_scene);222count += 1;223}224}225}226227if !args.no_gltf_lights {228// In Repo glTF229commands.spawn((230SceneRoot(asset_server.load("BistroExteriorFakeGI.gltf#Scene0")),231Spin,232));233}234235// Sun236commands237.spawn((238Transform::from_rotation(Quat::from_euler(EulerRot::XYZ, PI * -0.35, PI * -0.13, 0.0)),239DirectionalLight {240color: Color::srgb(1.0, 0.87, 0.78),241illuminance: lux::FULL_DAYLIGHT,242shadow_maps_enabled: !args.minimal,243contact_shadows_enabled: !args.minimal,244shadow_depth_bias: 0.1,245shadow_normal_bias: 0.2,246..default()247},248CascadeShadowConfigBuilder {249num_cascades: 3,250minimum_distance: 0.05,251maximum_distance: 100.0,252first_cascade_far_bound: 10.0,253overlap_proportion: 0.2,254}255.build(),256))257.insert_if(OcclusionCulling, || !args.no_shadow_occlusion_culling);258259// Camera260let mut cam = commands.spawn((261Msaa::Off,262Camera3d::default(),263ScreenSpaceTransmission {264steps: 0,265quality: ScreenSpaceTransmissionQuality::Low,266},267Hdr,268Transform::from_xyz(-10.5, 1.7, -1.0).looking_at(Vec3::new(0.0, 3.5, 0.0), Vec3::Y),269Projection::Perspective(PerspectiveProjection {270fov: std::f32::consts::PI / 3.0,271near: 0.1,272far: 1000.0,273aspect_ratio: 1.0,274..Default::default()275}),276EnvironmentMapLight {277diffuse_map: asset_server.load("environment_maps/san_giuseppe_bridge_4k_diffuse.ktx2"),278specular_map: asset_server279.load("environment_maps/san_giuseppe_bridge_4k_specular.ktx2"),280intensity: 600.0,281..default()282},283ContactShadows::default(),284FreeCamera::default(),285Spin,286));287cam.insert_if(DepthPrepass, || args.deferred)288.insert_if(DeferredPrepass, || args.deferred)289.insert_if(OcclusionCulling, || !args.no_view_occlusion_culling)290.insert_if(NoFrustumCulling, || args.no_frustum_culling)291.insert_if(NoAutomaticBatching, || args.no_automatic_batching)292.insert_if(NoIndirectDrawing, || args.no_indirect_drawing)293.insert_if(NoCpuCulling, || args.no_cpu_culling);294if !args.minimal {295cam.insert((296Bloom {297intensity: 0.02,298..default()299},300TemporalAntiAliasing::default(),301))302.insert(ScreenSpaceAmbientOcclusion::default());303}304305if !args.hide_frame_time {306commands307.spawn((308Node {309left: px(1.5),310top: px(1.5),311..default()312},313GlobalZIndex(-1),314))315.with_children(|parent| {316parent.spawn((Text::new(""), TextColor(Color::BLACK), FrameTimeText));317});318commands.spawn(Node::default()).with_children(|parent| {319parent.spawn((Text::new(""), TextColor(Color::WHITE), FrameTimeText));320});321}322}323324pub fn all_children<F: FnMut(Entity)>(325children: &Children,326children_query: &Query<&Children>,327closure: &mut F,328) {329for child in children {330if let Ok(children) = children_query.get(*child) {331all_children(children, children_query, closure);332}333closure(*child);334}335}336337#[allow(clippy::type_complexity, clippy::too_many_arguments)]338pub fn proc_scene(339scene_ready: On<SceneInstanceReady>,340mut commands: Commands,341children: Query<&Children>,342has_std_mat: Query<&MeshMaterial3d<StandardMaterial>>,343mut materials: ResMut<Assets<StandardMaterial>>,344lights: Query<Entity, Or<(With<PointLight>, With<DirectionalLight>, With<SpotLight>)>>,345cameras: Query<Entity, With<Camera>>,346args: Res<Args>,347) {348for entity in children.iter_descendants(scene_ready.entity) {349// Sponza needs flipped normals350if let Ok(mat_h) = has_std_mat.get(entity)351&& let Some(mat) = materials.get_mut(mat_h)352{353mat.flip_normal_map_y = true;354match mat.alpha_mode {355AlphaMode::Mask(_) => {356mat.diffuse_transmission = 0.6;357mat.double_sided = true;358mat.cull_mode = None;359mat.thickness = 0.2;360commands.entity(entity).insert(TransmittedShadowReceiver);361}362AlphaMode::Opaque => {363mat.double_sided = false;364mat.cull_mode = Some(Face::Back);365}366_ => (),367}368}369370if args.no_gltf_lights {371// Has a bunch of lights by default372if lights.get(entity).is_ok() {373commands.entity(entity).despawn();374}375}376377// Has a bunch of cameras by default378if cameras.get(entity).is_ok() {379commands.entity(entity).despawn();380}381}382}383384#[derive(Resource, Deref, DerefMut)]385struct CameraPositions([Transform; 3]);386387impl Default for CameraPositions {388fn default() -> Self {389Self([390Transform {391translation: Vec3::new(-10.5, 1.7, -1.0),392rotation: Quat::from_array([-0.05678932, 0.7372272, -0.062454797, -0.670351]),393scale: Vec3::ONE,394},395Transform {396translation: Vec3::new(56.23809, 2.9985719, 28.96291),397rotation: Quat::from_array([0.0020175162, 0.35272083, -0.0007605003, 0.93572617]),398scale: Vec3::ONE,399},400Transform {401translation: Vec3::new(5.7861176, 3.3475509, -8.821455),402rotation: Quat::from_array([-0.0049382094, -0.98193514, -0.025878597, 0.18737496]),403scale: Vec3::ONE,404},405])406}407}408409const ANIM_SPEED: f32 = 0.2;410const ANIM_HYSTERESIS: f32 = 0.1; // EMA/LPF411412const ANIM_CAM: [Transform; 3] = [413Transform {414translation: Vec3::new(-6.414026, 8.179898, -23.550516),415rotation: Quat::from_array([-0.016413536, -0.88136566, -0.030704278, 0.4711502]),416scale: Vec3::ONE,417},418Transform {419translation: Vec3::new(-14.752817, 6.279289, 5.691277),420rotation: Quat::from_array([-0.031593435, -0.516736, -0.019086324, 0.8553488]),421scale: Vec3::ONE,422},423Transform {424translation: Vec3::new(5.1539426, 8.142523, 16.436222),425rotation: Quat::from_array([-0.07907656, -0.07581916, -0.006031934, 0.99396276]),426scale: Vec3::ONE,427},428];429430fn input(431input: Res<ButtonInput<KeyCode>>,432mut camera: Query<&mut Transform, With<Camera>>,433positions: Res<CameraPositions>,434) {435let Ok(mut transform) = camera.single_mut() else {436return;437};438if input.just_pressed(KeyCode::KeyI) {439info!("{:?}", transform);440}441if input.just_pressed(KeyCode::Digit1) {442*transform = positions[0]443}444if input.just_pressed(KeyCode::Digit2) {445*transform = positions[1]446}447if input.just_pressed(KeyCode::Digit3) {448*transform = positions[2]449}450}451452fn lerp<T>(a: T, b: T, t: f32) -> T453where454T: Copy + Add<Output = T> + Sub<Output = T> + Mul<f32, Output = T>,455{456a + (b - a) * t457}458459fn follow_path(points: &[Transform], progress: f32) -> Transform {460let total_segments = (points.len() - 1) as f32;461let progress = progress.clamp(0.0, 1.0);462let mut segment_progress = progress * total_segments;463let segment_index = segment_progress.floor() as usize;464segment_progress -= segment_index as f32;465let a = points[segment_index];466let b = points[(segment_index + 1).min(points.len() - 1)];467Transform {468translation: lerp(a.translation, b.translation, segment_progress),469rotation: lerp(a.rotation, b.rotation, segment_progress),470scale: lerp(a.scale, b.scale, segment_progress),471}472}473474fn run_animation(475time: Res<Time>,476input: Res<ButtonInput<KeyCode>>,477mut animation_active: Local<bool>,478mut camera: Query<&mut Transform, With<Camera>>,479) {480let Ok(mut cam_tr) = camera.single_mut() else {481return;482};483if input.just_pressed(KeyCode::Space) {484*animation_active = !*animation_active;485}486if !*animation_active {487return;488}489let progress = (time.elapsed_secs() * ANIM_SPEED).fract();490let cycle = 1.0 - (progress * 2.0 - 1.0).abs();491let path_state = follow_path(&ANIM_CAM, cycle);492cam_tr.translation = lerp(cam_tr.translation, path_state.translation, ANIM_HYSTERESIS);493cam_tr.rotation = lerp(cam_tr.rotation, path_state.rotation, ANIM_HYSTERESIS).normalize();494}495496fn spin(497camera: Single<Entity, With<Camera>>,498mut things_to_spin: Query<&mut Transform, With<Spin>>,499time: Res<Time>,500args: Res<Args>,501mut positions: ResMut<CameraPositions>,502) {503if args.spin {504let camera_position = things_to_spin.get(*camera).unwrap().translation;505let spin = |thing_to_spin: &mut Transform| {506thing_to_spin.rotate_around(camera_position, Quat::from_rotation_y(time.delta_secs()));507};508things_to_spin.iter_mut().for_each(|mut s| spin(s.as_mut())); // WHY509positions.iter_mut().for_each(spin);510}511}512513#[allow(clippy::too_many_arguments)]514fn benchmark(515input: Res<ButtonInput<KeyCode>>,516mut camera_transform: Single<&mut Transform, With<Camera>>,517materials: Res<Assets<StandardMaterial>>,518meshes: Res<Assets<Mesh>>,519has_std_mat: Query<&MeshMaterial3d<StandardMaterial>>,520has_mesh: Query<&Mesh3d>,521mut bench_started: Local<Option<Instant>>,522mut bench_frame: Local<u32>,523mut count_per_step: Local<u32>,524time: Res<Time>,525positions: Res<CameraPositions>,526mut low_high: ResMut<FrameLowHigh>,527) {528if input.just_pressed(KeyCode::KeyB) && bench_started.is_none() {529low_high.bench_reset();530*bench_started = Some(Instant::now());531*bench_frame = 0;532// Try to render for around 3s or at least 60 frames per step533*count_per_step = ((3.0 / time.delta_secs()) as u32).max(60);534println!(535"Starting Benchmark with {} frames per step",536*count_per_step537);538}539if bench_started.is_none() {540return;541}542if *bench_frame == 0 {543**camera_transform = positions[0]544} else if *bench_frame == *count_per_step {545**camera_transform = positions[1]546} else if *bench_frame == *count_per_step * 2 {547**camera_transform = positions[2]548} else if *bench_frame == *count_per_step * 3 {549let elapsed = bench_started.unwrap().elapsed().as_secs_f32();550println!(551"{:>7.2}ms Benchmark avg cpu frame time",552(elapsed / *bench_frame as f32) * 1000.0553);554let r = 1.0 / *bench_frame as f64;555println!("{:>7.2}ms avg 1% low", low_high.sum_one_percent_low * r);556println!("{:>7.2}ms avg 1% high", low_high.sum_one_percent_high * r);557println!(558"{:>7} Meshes\n{:>7} Mesh Instances\n{:>7} Materials\n{:>7} Material Instances",559meshes.len(),560has_mesh.iter().len(),561materials.len(),562has_std_mat.iter().len(),563);564*bench_started = None;565*bench_frame = 0;566**camera_transform = positions[0];567}568*bench_frame += 1;569low_high.bench_step();570}571572pub fn add_no_frustum_culling(573mut commands: Commands,574convert_query: Query<575Entity,576(577Without<NoFrustumCulling>,578With<MeshMaterial3d<StandardMaterial>>,579),580>,581) {582for entity in convert_query.iter() {583commands.entity(entity).insert(NoFrustumCulling);584}585}586587#[derive(Resource, Default)]588struct FrameLowHigh {589one_percent_low: f64,590one_percent_high: f64,591sum_one_percent_low: f64,592sum_one_percent_high: f64,593}594595impl FrameLowHigh {596fn bench_reset(&mut self) {597self.sum_one_percent_high = 0.0;598self.sum_one_percent_low = 0.0;599}600fn bench_step(&mut self) {601self.sum_one_percent_high += self.one_percent_high;602self.sum_one_percent_low += self.one_percent_low;603}604}605606fn frame_time_system(607diagnostics: Res<DiagnosticsStore>,608mut text: Query<&mut Text, With<FrameTimeText>>,609mut measurements: Local<Vec<f64>>,610mut low_high: ResMut<FrameLowHigh>,611) {612if let Some(frame_time) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FRAME_TIME) {613let mut string = format!(614"\n{:>7.2}ms ema\n{:>7.2}ms sma\n",615frame_time.smoothed().unwrap_or_default(),616frame_time.average().unwrap_or_default()617);618619if frame_time.history_len() >= 100 {620measurements.clear();621measurements.extend(frame_time.measurements().map(|t| t.value));622measurements.sort_by(|a, b| a.partial_cmp(b).unwrap());623let count = measurements.len() / 100;624low_high.one_percent_low = measurements.iter().take(count).sum::<f64>() / count as f64;625low_high.one_percent_high =626measurements.iter().rev().take(count).sum::<f64>() / count as f64;627628string.push_str(&format!(629"{:>7.2}ms 1% low\n{:>7.2}ms 1% high\n",630low_high.one_percent_low, low_high.one_percent_high631));632}633634for mut t in &mut text {635t.0 = string.clone();636}637};638}639640641