Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/large_scenes/caldera_hotel/src/main.rs
9356 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::{f32::consts::PI, time::Instant};
5
6
use crate::light_consts::lux;
7
use argh::FromArgs;
8
use bevy::pbr::ContactShadows;
9
use bevy::{
10
anti_alias::taa::TemporalAntiAliasing,
11
camera::{
12
visibility::{NoCpuCulling, NoFrustumCulling},
13
Hdr,
14
},
15
camera_controller::free_camera::{FreeCamera, FreeCameraPlugin},
16
core_pipeline::prepass::{DeferredPrepass, DepthPrepass},
17
diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin},
18
image::{ImageAddressMode, ImageSampler, ImageSamplerDescriptor},
19
light::{CascadeShadowConfig, CascadeShadowConfigBuilder},
20
pbr::{DefaultOpaqueRendererMethod, ScreenSpaceAmbientOcclusion},
21
post_process::bloom::Bloom,
22
prelude::*,
23
render::{
24
batching::NoAutomaticBatching,
25
occlusion_culling::OcclusionCulling,
26
render_resource::{
27
Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
28
},
29
view::NoIndirectDrawing,
30
},
31
scene::SceneInstanceReady,
32
window::{PresentMode, WindowResolution},
33
winit::WinitSettings,
34
};
35
36
#[derive(FromArgs, Resource, Clone)]
37
/// Config
38
pub struct Args {
39
/// disable bloom, AO, AA, shadows
40
#[argh(switch)]
41
minimal: bool,
42
43
/// assign randomly generated materials to each unique mesh (mesh instances also share materials)
44
#[argh(switch)]
45
random_materials: bool,
46
47
/// quantity of unique textures sets to randomly select from. (A texture set being: base_color, roughness)
48
#[argh(option, default = "0")]
49
texture_count: u32,
50
51
/// quantity of hotel 01 models
52
#[argh(option, default = "1")]
53
count: u32,
54
55
/// use deferred shading
56
#[argh(switch)]
57
deferred: bool,
58
59
/// disable all frustum culling. Stresses queuing and batching as all mesh material entities in the scene are always drawn.
60
#[argh(switch)]
61
no_frustum_culling: bool,
62
63
/// disable automatic batching. Skips batching resulting in heavy stress on render pass draw command encoding.
64
#[argh(switch)]
65
no_automatic_batching: bool,
66
67
/// disable gpu occlusion culling for the camera
68
#[argh(switch)]
69
no_view_occlusion_culling: bool,
70
71
/// disable gpu occlusion culling for the directional light
72
#[argh(switch)]
73
no_shadow_occlusion_culling: bool,
74
75
/// disable indirect drawing.
76
#[argh(switch)]
77
no_indirect_drawing: bool,
78
79
/// disable CPU culling.
80
#[argh(switch)]
81
no_cpu_culling: bool,
82
83
/// spin the bistros and camera
84
#[argh(switch)]
85
spin: bool,
86
87
/// don't show frame time
88
#[argh(switch)]
89
hide_frame_time: bool,
90
}
91
92
pub fn main() {
93
let args: Args = argh::from_env();
94
95
let mut app = App::new();
96
97
app.init_resource::<CameraPositions>()
98
.init_resource::<FrameLowHigh>()
99
.insert_resource(args.clone())
100
.insert_resource(WinitSettings::continuous())
101
.add_plugins(DefaultPlugins.set(WindowPlugin {
102
primary_window: Some(Window {
103
present_mode: PresentMode::Immediate,
104
resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),
105
..default()
106
}),
107
..default()
108
}))
109
.add_plugins((
110
FrameTimeDiagnosticsPlugin {
111
max_history_length: 1000,
112
..default()
113
},
114
FreeCameraPlugin,
115
))
116
.add_systems(Startup, setup)
117
.add_systems(Update, (input, spin, frame_time_system, benchmark).chain());
118
119
if args.no_frustum_culling {
120
app.add_systems(Update, add_no_frustum_culling);
121
}
122
123
if args.deferred {
124
app.insert_resource(DefaultOpaqueRendererMethod::deferred());
125
}
126
127
app.run();
128
}
129
130
#[derive(Component)]
131
pub struct Spin;
132
133
#[derive(Component)]
134
struct FrameTimeText;
135
136
#[derive(Component)]
137
pub struct PostProcScene;
138
139
pub fn setup(
140
mut commands: Commands,
141
asset_server: Res<AssetServer>,
142
args: Res<Args>,
143
positions: Res<CameraPositions>,
144
) {
145
let hotel_01 = asset_server.load("hotel_01.glb#Scene0");
146
commands
147
.spawn((
148
SceneRoot(hotel_01.clone()),
149
Transform::from_scale(Vec3::splat(0.01)),
150
PostProcScene,
151
Spin,
152
))
153
.observe(assign_rng_materials);
154
155
let mut count = 0;
156
if args.count > 1 {
157
let quantity = args.count - 1;
158
let side = (quantity as f32).sqrt().ceil() as i32 / 2;
159
'outer: for x in -side..=side {
160
for z in -side..=side {
161
if count >= quantity {
162
break 'outer;
163
}
164
if x == 0 && z == 0 {
165
continue;
166
}
167
commands.spawn((
168
SceneRoot(hotel_01.clone()),
169
Transform::from_xyz(x as f32 * 50.0, 0.0, z as f32 * 50.0)
170
.with_scale(Vec3::splat(0.01)),
171
Spin,
172
));
173
count += 1;
174
}
175
}
176
}
177
178
// Sun
179
commands
180
.spawn((
181
Transform::from_rotation(Quat::from_euler(EulerRot::XYZ, PI * -0.35, PI * -0.13, 0.0)),
182
DirectionalLight {
183
color: Color::srgb(1.0, 0.87, 0.78),
184
illuminance: lux::FULL_DAYLIGHT,
185
shadow_maps_enabled: !args.minimal,
186
contact_shadows_enabled: !args.minimal,
187
shadow_depth_bias: 0.2,
188
shadow_normal_bias: 0.2,
189
..default()
190
},
191
CascadeShadowConfig::from(CascadeShadowConfigBuilder {
192
num_cascades: 3,
193
minimum_distance: 0.1,
194
maximum_distance: 80.0,
195
first_cascade_far_bound: 5.0,
196
overlap_proportion: 0.2,
197
}),
198
))
199
.insert_if(OcclusionCulling, || !args.no_shadow_occlusion_culling);
200
201
// Camera
202
let mut cam = commands.spawn((
203
Msaa::Off,
204
Camera3d::default(),
205
Hdr,
206
positions[0],
207
Projection::Perspective(PerspectiveProjection {
208
fov: std::f32::consts::PI / 3.0,
209
near: 0.1,
210
far: 1000.0,
211
..Default::default()
212
}),
213
EnvironmentMapLight {
214
diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
215
specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
216
intensity: 1000.0,
217
..default()
218
},
219
ContactShadows::default(),
220
FreeCamera::default(),
221
Spin,
222
));
223
224
cam.insert_if(DepthPrepass, || args.deferred)
225
.insert_if(DeferredPrepass, || args.deferred)
226
.insert_if(OcclusionCulling, || !args.no_view_occlusion_culling)
227
.insert_if(NoFrustumCulling, || args.no_frustum_culling)
228
.insert_if(NoAutomaticBatching, || args.no_automatic_batching)
229
.insert_if(NoIndirectDrawing, || args.no_indirect_drawing)
230
.insert_if(NoCpuCulling, || args.no_cpu_culling);
231
232
if !args.minimal {
233
cam.insert((
234
Bloom {
235
intensity: 0.02,
236
..default()
237
},
238
TemporalAntiAliasing::default(),
239
))
240
.insert(ScreenSpaceAmbientOcclusion::default());
241
}
242
243
if !args.hide_frame_time {
244
commands
245
.spawn((
246
Node {
247
left: px(1.5),
248
top: px(1.5),
249
..default()
250
},
251
GlobalZIndex(-1),
252
))
253
.with_children(|parent| {
254
parent.spawn((Text::new(""), TextColor(Color::BLACK), FrameTimeText));
255
});
256
commands.spawn(Node::default()).with_children(|parent| {
257
parent.spawn((Text::new(""), TextColor(Color::WHITE), FrameTimeText));
258
});
259
}
260
}
261
262
// Go though each unique mesh and randomly generate a material.
263
// Each unique so instances are maintained.
264
#[allow(clippy::too_many_arguments)]
265
pub fn assign_rng_materials(
266
scene_ready: On<SceneInstanceReady>,
267
mut commands: Commands,
268
mut materials: ResMut<Assets<StandardMaterial>>,
269
mut images: ResMut<Assets<Image>>,
270
meshes: Res<Assets<Mesh>>,
271
mesh_instances: Query<(Entity, &Mesh3d)>,
272
args: Res<Args>,
273
asset_server: Res<AssetServer>,
274
scenes: Query<&SceneRoot>,
275
) {
276
if !args.random_materials {
277
return;
278
}
279
280
let Ok(scene) = scenes.get(scene_ready.entity) else {
281
return;
282
};
283
284
let scene_loaded = asset_server
285
.get_recursive_dependency_load_state(&scene.0)
286
.map(|state| state.is_loaded())
287
.unwrap_or(false);
288
289
if !scene_loaded {
290
warn!("get_recursive_dependency_load_state not finished!");
291
}
292
293
const MESH_INSTANCE_QTY: usize = 35689;
294
if MESH_INSTANCE_QTY != mesh_instances.iter().len() {
295
warn!(
296
"Mesh quantity appears incorrect. Expected: {}. Found: {}!",
297
MESH_INSTANCE_QTY,
298
mesh_instances.iter().len()
299
)
300
}
301
302
let base_color_textures = (0..args.texture_count)
303
.map(|i| {
304
images.add(generate_random_compressed_texture_with_mipmaps(
305
2048, false, i,
306
))
307
})
308
.collect::<Vec<_>>();
309
let roughness_textures = (0..args.texture_count)
310
.map(|i| {
311
images.add(generate_random_compressed_texture_with_mipmaps(
312
2048,
313
false, // Using bc4 here seems to not work
314
i + 2048,
315
))
316
})
317
.collect::<Vec<_>>();
318
319
for (i, (mesh_h, _mesh)) in meshes.iter().enumerate() {
320
let mut base_color_texture = None;
321
let mut roughness_texture = None;
322
323
if !base_color_textures.is_empty() {
324
base_color_texture = Some(base_color_textures[i % base_color_textures.len()].clone());
325
}
326
if !roughness_textures.is_empty() {
327
roughness_texture = Some(roughness_textures[i % roughness_textures.len()].clone());
328
}
329
330
let unique_material = materials.add(StandardMaterial {
331
base_color: Color::srgb(
332
hash_noise(i as u32, 0, 0),
333
hash_noise(i as u32, 0, 1),
334
hash_noise(i as u32, 0, 2),
335
),
336
base_color_texture,
337
metallic_roughness_texture: roughness_texture,
338
..default()
339
});
340
for (entity, mesh_instance_h) in mesh_instances.iter() {
341
if mesh_instance_h.id() == mesh_h {
342
commands
343
.entity(entity)
344
.insert(MeshMaterial3d::from(unique_material.clone()));
345
}
346
}
347
}
348
}
349
350
fn generate_random_compressed_texture_with_mipmaps(size: u32, bc4: bool, seed: u32) -> Image {
351
let (bytes, mip_count) = calculate_bcn_image_size_with_mips(size, if bc4 { 8 } else { 16 });
352
let data = (0..bytes).map(|i| uhash(i, seed) as u8).collect::<Vec<_>>();
353
354
Image {
355
texture_descriptor: TextureDescriptor {
356
label: None,
357
size: Extent3d {
358
width: size,
359
height: size,
360
..default()
361
},
362
dimension: TextureDimension::D2,
363
format: if bc4 {
364
TextureFormat::Bc4RUnorm
365
} else {
366
TextureFormat::Bc7RgbaUnormSrgb
367
},
368
mip_level_count: mip_count,
369
sample_count: 1,
370
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
371
view_formats: &[],
372
},
373
sampler: ImageSampler::Descriptor(ImageSamplerDescriptor {
374
address_mode_u: ImageAddressMode::Repeat,
375
address_mode_v: ImageAddressMode::Repeat,
376
..default()
377
}),
378
379
data: Some(data),
380
..Default::default()
381
}
382
}
383
384
#[derive(Resource, Deref, DerefMut)]
385
pub struct CameraPositions([Transform; 3]);
386
387
impl Default for CameraPositions {
388
fn default() -> Self {
389
Self([
390
Transform {
391
translation: Vec3::new(-20.147331, 16.818098, 42.806145),
392
rotation: Quat::from_array([-0.22917402, -0.34915298, -0.08848568, 0.9042908]),
393
scale: Vec3::ONE,
394
},
395
Transform {
396
translation: Vec3::new(1.6168646, 1.8304176, -5.846825),
397
rotation: Quat::from_array([-0.0007061247, -0.99179053, 0.12775362, -0.005481863]),
398
scale: Vec3::ONE,
399
},
400
Transform {
401
translation: Vec3::new(23.97184, 1.8938808, 30.568554),
402
rotation: Quat::from_array([-0.0013945175, 0.4685419, 0.00073959737, 0.8834399]),
403
scale: Vec3::ONE,
404
},
405
])
406
}
407
}
408
409
fn input(
410
input: Res<ButtonInput<KeyCode>>,
411
mut camera: Query<&mut Transform, With<Camera>>,
412
positions: Res<CameraPositions>,
413
) {
414
let Ok(mut transform) = camera.single_mut() else {
415
return;
416
};
417
if input.just_pressed(KeyCode::KeyI) {
418
info!("{:?}", transform);
419
}
420
if input.just_pressed(KeyCode::Digit1) {
421
*transform = positions[0]
422
}
423
if input.just_pressed(KeyCode::Digit2) {
424
*transform = positions[1]
425
}
426
if input.just_pressed(KeyCode::Digit3) {
427
*transform = positions[2]
428
}
429
}
430
431
fn spin(
432
camera: Single<Entity, With<Camera>>,
433
mut things_to_spin: Query<&mut Transform, With<Spin>>,
434
time: Res<Time>,
435
args: Res<Args>,
436
mut positions: ResMut<CameraPositions>,
437
) {
438
if args.spin {
439
let camera_position = things_to_spin.get(*camera).unwrap().translation;
440
let spin = |thing_to_spin: &mut Transform| {
441
thing_to_spin.rotate_around(camera_position, Quat::from_rotation_y(time.delta_secs()));
442
};
443
things_to_spin.iter_mut().for_each(|mut s| spin(s.as_mut())); // WHY
444
positions.iter_mut().for_each(spin);
445
}
446
}
447
448
#[allow(clippy::too_many_arguments)]
449
fn benchmark(
450
input: Res<ButtonInput<KeyCode>>,
451
mut camera_transform: Single<&mut Transform, With<Camera>>,
452
materials: Res<Assets<StandardMaterial>>,
453
meshes: Res<Assets<Mesh>>,
454
has_std_mat: Query<&MeshMaterial3d<StandardMaterial>>,
455
has_mesh: Query<&Mesh3d>,
456
mut bench_started: Local<Option<Instant>>,
457
mut bench_frame: Local<u32>,
458
mut count_per_step: Local<u32>,
459
time: Res<Time>,
460
positions: Res<CameraPositions>,
461
mut low_high: ResMut<FrameLowHigh>,
462
) {
463
if input.just_pressed(KeyCode::KeyB) && bench_started.is_none() {
464
low_high.bench_reset();
465
*bench_started = Some(Instant::now());
466
*bench_frame = 0;
467
// Try to render for around 3s or at least 60 frames per step
468
*count_per_step = ((3.0 / time.delta_secs()) as u32).max(60);
469
println!(
470
"Starting Benchmark with {} frames per step",
471
*count_per_step
472
);
473
}
474
if bench_started.is_none() {
475
return;
476
}
477
if *bench_frame == 0 {
478
**camera_transform = positions[0]
479
} else if *bench_frame == *count_per_step {
480
**camera_transform = positions[1]
481
} else if *bench_frame == *count_per_step * 2 {
482
**camera_transform = positions[2]
483
} else if *bench_frame == *count_per_step * 3 {
484
let elapsed = bench_started.unwrap().elapsed().as_secs_f32();
485
println!(
486
"{:>7.2}ms Benchmark avg cpu frame time",
487
(elapsed / *bench_frame as f32) * 1000.0
488
);
489
let r = 1.0 / *bench_frame as f64;
490
println!("{:>7.2}ms avg 1% low", low_high.sum_one_percent_low * r);
491
println!("{:>7.2}ms avg 1% high", low_high.sum_one_percent_high * r);
492
println!(
493
"{:>7} Meshes\n{:>7} Mesh Instances\n{:>7} Materials\n{:>7} Material Instances",
494
meshes.len(),
495
has_mesh.iter().len(),
496
materials.len(),
497
has_std_mat.iter().len(),
498
);
499
*bench_started = None;
500
*bench_frame = 0;
501
**camera_transform = positions[0];
502
}
503
*bench_frame += 1;
504
low_high.bench_step();
505
}
506
507
pub fn add_no_frustum_culling(
508
mut commands: Commands,
509
convert_query: Query<
510
Entity,
511
(
512
Without<NoFrustumCulling>,
513
With<MeshMaterial3d<StandardMaterial>>,
514
),
515
>,
516
) {
517
for entity in convert_query.iter() {
518
commands.entity(entity).insert(NoFrustumCulling);
519
}
520
}
521
522
#[inline(always)]
523
pub fn uhash(a: u32, b: u32) -> u32 {
524
let mut x = (a.overflowing_mul(1597334673).0) ^ (b.overflowing_mul(3812015801).0);
525
// from https://nullprogram.com/blog/2018/07/31/
526
x = x ^ (x >> 16);
527
x = x.overflowing_mul(0x7feb352d).0;
528
x = x ^ (x >> 15);
529
x = x.overflowing_mul(0x846ca68b).0;
530
x = x ^ (x >> 16);
531
x
532
}
533
534
#[inline(always)]
535
pub fn unormf(n: u32) -> f32 {
536
n as f32 * (1.0 / 0xffffffffu32 as f32)
537
}
538
539
#[inline(always)]
540
pub fn hash_noise(x: u32, y: u32, z: u32) -> f32 {
541
let urnd = uhash(x, (y << 11) + z);
542
unormf(urnd)
543
}
544
545
// BC7 block is 16 bytes, BC4 block is 8 bytes
546
fn calculate_bcn_image_size_with_mips(size: u32, block_size: u32) -> (u32, u32) {
547
let mut total_size = 0;
548
let mut mip_size = size;
549
let mut mip_count = 0;
550
while mip_size > 4 {
551
mip_count += 1;
552
let num_blocks = mip_size / 4; // Round up
553
let mip_level_size = num_blocks * num_blocks * block_size;
554
total_size += mip_level_size;
555
mip_size = (mip_size / 2).max(1);
556
}
557
(total_size, mip_count.max(1))
558
}
559
560
#[derive(Resource, Default)]
561
struct FrameLowHigh {
562
one_percent_low: f64,
563
one_percent_high: f64,
564
sum_one_percent_low: f64,
565
sum_one_percent_high: f64,
566
}
567
568
impl FrameLowHigh {
569
fn bench_reset(&mut self) {
570
self.sum_one_percent_high = 0.0;
571
self.sum_one_percent_low = 0.0;
572
}
573
fn bench_step(&mut self) {
574
self.sum_one_percent_high += self.one_percent_high;
575
self.sum_one_percent_low += self.one_percent_low;
576
}
577
}
578
579
fn frame_time_system(
580
diagnostics: Res<DiagnosticsStore>,
581
mut text: Query<&mut Text, With<FrameTimeText>>,
582
mut measurements: Local<Vec<f64>>,
583
mut low_high: ResMut<FrameLowHigh>,
584
) {
585
if let Some(frame_time) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FRAME_TIME) {
586
let mut string = format!(
587
"\n{:>7.2}ms ema\n{:>7.2}ms sma\n",
588
frame_time.smoothed().unwrap_or_default(),
589
frame_time.average().unwrap_or_default()
590
);
591
592
if frame_time.history_len() >= 100 {
593
measurements.clear();
594
measurements.extend(frame_time.measurements().map(|t| t.value));
595
measurements.sort_by(|a, b| a.partial_cmp(b).unwrap());
596
let count = measurements.len() / 100;
597
low_high.one_percent_low = measurements.iter().take(count).sum::<f64>() / count as f64;
598
low_high.one_percent_high =
599
measurements.iter().rev().take(count).sum::<f64>() / count as f64;
600
601
string.push_str(&format!(
602
"{:>7.2}ms 1% low\n{:>7.2}ms 1% high\n",
603
low_high.one_percent_low, low_high.one_percent_high
604
));
605
}
606
607
for mut t in &mut text {
608
t.0 = string.clone();
609
}
610
};
611
}
612
613