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