forked from bevyengine/bevy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathocclusion_culling.rs
669 lines (610 loc) · 24.1 KB
/
occlusion_culling.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
//! Demonstrates occlusion culling.
//!
//! This demo rotates many small cubes around a rotating large cube at the
//! origin. At all times, the large cube will be occluding several of the small
//! cubes. The demo displays the number of cubes that were actually rendered, so
//! the effects of occlusion culling can be seen.
use std::{
f32::consts::PI,
fmt::Write as _,
result::Result,
sync::{Arc, Mutex},
};
use bevy::{
color::palettes::css::{SILVER, WHITE},
core_pipeline::{
core_3d::graph::{Core3d, Node3d},
prepass::DepthPrepass,
},
prelude::*,
render::{
batching::gpu_preprocessing::{
GpuPreprocessingMode, GpuPreprocessingSupport, IndirectParametersBuffers,
IndirectParametersIndexed,
},
experimental::occlusion_culling::OcclusionCulling,
render_graph::{self, NodeRunError, RenderGraphApp, RenderGraphContext, RenderLabel},
render_resource::{Buffer, BufferDescriptor, BufferUsages, MapMode},
renderer::{RenderAdapter, RenderContext, RenderDevice},
settings::WgpuFeatures,
Render, RenderApp, RenderPlugin, RenderSet,
},
};
use bytemuck::Pod;
/// The radius of the spinning sphere of cubes.
const OUTER_RADIUS: f32 = 3.0;
/// The density of cubes in the other sphere.
const OUTER_SUBDIVISION_COUNT: u32 = 5;
/// The speed at which the outer sphere and large cube rotate in radians per
/// frame.
const ROTATION_SPEED: f32 = 0.01;
/// The length of each side of the small cubes, in meters.
const SMALL_CUBE_SIZE: f32 = 0.1;
/// The length of each side of the large cube, in meters.
const LARGE_CUBE_SIZE: f32 = 2.0;
/// A marker component for the immediate parent of the large sphere of cubes.
#[derive(Default, Component)]
struct SphereParent;
/// A marker component for the large spinning cube at the origin.
#[derive(Default, Component)]
struct LargeCube;
/// A plugin for the render app that reads the number of culled meshes from the
/// GPU back to the CPU.
struct ReadbackIndirectParametersPlugin;
/// The node that we insert into the render graph in order to read the number of
/// culled meshes from the GPU back to the CPU.
#[derive(Default)]
struct ReadbackIndirectParametersNode;
/// The [`RenderLabel`] that we use to identify the
/// [`ReadbackIndirectParametersNode`].
#[derive(Clone, PartialEq, Eq, Hash, Debug, RenderLabel)]
struct ReadbackIndirectParameters;
/// The intermediate staging buffers that we use to read back the indirect
/// parameters from the GPU to the CPU.
///
/// We read back the GPU indirect parameters so that we can determine the number
/// of meshes that were culled.
///
/// `wgpu` doesn't allow us to read indirect buffers back from the GPU to the
/// CPU directly. Instead, we have to copy them to a temporary staging buffer
/// first, and then read *those* buffers back from the GPU to the CPU. This
/// resource holds those temporary buffers.
#[derive(Resource, Default)]
struct IndirectParametersStagingBuffers {
/// The buffer that stores the indirect draw commands.
///
/// See [`IndirectParametersIndexed`] for more information about the memory
/// layout of this buffer.
data: Option<Buffer>,
/// The buffer that stores the *number* of indirect draw commands.
///
/// We only care about the first `u32` in this buffer.
batch_sets: Option<Buffer>,
}
/// A resource, shared between the main world and the render world, that saves a
/// CPU-side copy of the GPU buffer that stores the indirect draw parameters.
///
/// This is needed so that we can display the number of meshes that were culled.
/// It's reference counted, and protected by a lock, because we don't precisely
/// know when the GPU will be ready to present the CPU with the buffer copy.
/// Even though the rendering runs at least a frame ahead of the main app logic,
/// we don't require more precise synchronization than the lock because we don't
/// really care how up-to-date the counter of culled meshes is. If it's off by a
/// few frames, that's no big deal.
#[derive(Clone, Resource, Deref, DerefMut)]
struct SavedIndirectParameters(Arc<Mutex<SavedIndirectParametersData>>);
/// A CPU-side copy of the GPU buffer that stores the indirect draw parameters.
///
/// This is needed so that we can display the number of meshes that were culled.
struct SavedIndirectParametersData {
/// The CPU-side copy of the GPU buffer that stores the indirect draw
/// parameters.
data: Vec<IndirectParametersIndexed>,
/// The CPU-side copy of the GPU buffer that stores the *number* of indirect
/// draw parameters that we have.
///
/// All we care about is the number of indirect draw parameters for a single
/// view, so this is only one word in size.
count: u32,
/// True if occlusion culling is supported at all; false if it's not.
occlusion_culling_supported: bool,
/// True if we support inspecting the number of meshes that were culled on
/// this platform; false if we don't.
///
/// If `multi_draw_indirect_count` isn't supported, then we would have to
/// employ a more complicated approach in order to determine the number of
/// meshes that are occluded, and that would be out of scope for this
/// example.
occlusion_culling_introspection_supported: bool,
}
impl FromWorld for SavedIndirectParameters {
fn from_world(world: &mut World) -> SavedIndirectParameters {
let render_adapter = world.resource::<RenderAdapter>();
SavedIndirectParameters(Arc::new(Mutex::new(SavedIndirectParametersData {
data: vec![],
count: 0,
// This gets set to false in `readback_indirect_buffers` if we don't
// support GPU preprocessing.
occlusion_culling_supported: true,
// In order to determine how many meshes were culled, we look at the
// indirect count buffer that Bevy only populates if the platform
// supports `multi_draw_indirect_count`. So, if we don't have that
// feature, then we don't bother to display how many meshes were
// culled.
occlusion_culling_introspection_supported: render_adapter
.features()
.contains(WgpuFeatures::MULTI_DRAW_INDIRECT_COUNT),
})))
}
}
/// The demo's current settings.
#[derive(Resource)]
struct AppStatus {
/// Whether occlusion culling is presently enabled.
///
/// By default, this is set to true.
occlusion_culling: bool,
}
impl Default for AppStatus {
fn default() -> Self {
AppStatus {
occlusion_culling: true,
}
}
}
fn main() {
App::new()
.add_plugins(
DefaultPlugins
.set(WindowPlugin {
primary_window: Some(Window {
title: "Bevy Occlusion Culling Example".into(),
..default()
}),
..default()
})
.set(RenderPlugin {
allow_copies_from_indirect_parameters: true,
..default()
}),
)
.add_plugins(ReadbackIndirectParametersPlugin)
.init_resource::<AppStatus>()
.add_systems(Startup, setup)
.add_systems(Update, spin_small_cubes)
.add_systems(Update, spin_large_cube)
.add_systems(Update, update_status_text)
.add_systems(Update, toggle_occlusion_culling_on_request)
.run();
}
impl Plugin for ReadbackIndirectParametersPlugin {
fn build(&self, app: &mut App) {
// Fetch the render app.
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<IndirectParametersStagingBuffers>()
.add_systems(ExtractSchedule, readback_indirect_parameters)
.add_systems(
Render,
create_indirect_parameters_staging_buffers.in_set(RenderSet::PrepareResourcesFlush),
)
// Add the node that allows us to read the indirect parameters back
// from the GPU to the CPU, which allows us to determine how many
// meshes were culled.
.add_render_graph_node::<ReadbackIndirectParametersNode>(
Core3d,
ReadbackIndirectParameters,
)
// We read back the indirect parameters any time after
// `EndMainPass`. Readback doesn't particularly need to execute
// before `EndMainPassPostProcessing`, but we specify that anyway
// because we want to make the indirect parameters run before
// *something* in the graph, and `EndMainPassPostProcessing` is a
// good a node as any other.
.add_render_graph_edges(
Core3d,
(
Node3d::EndMainPass,
ReadbackIndirectParameters,
Node3d::EndMainPassPostProcessing,
),
);
}
fn finish(&self, app: &mut App) {
// Create the `SavedIndirectParameters` resource that we're going to use
// to communicate between the thread that the GPU-to-CPU readback
// callback runs on and the main application threads. This resource is
// atomically reference counted. We store one reference to the
// `SavedIndirectParameters` in the main app and another reference in
// the render app.
let saved_indirect_parameters = SavedIndirectParameters::from_world(app.world_mut());
app.insert_resource(saved_indirect_parameters.clone());
// Fetch the render app.
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
// Insert another reference to the `SavedIndirectParameters`.
.insert_resource(saved_indirect_parameters);
}
}
/// Spawns all the objects in the scene.
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
spawn_small_cubes(&mut commands, &mut meshes, &mut materials);
spawn_large_cube(&mut commands, &asset_server, &mut meshes, &mut materials);
spawn_light(&mut commands);
spawn_camera(&mut commands);
spawn_help_text(&mut commands);
}
/// Spawns the rotating sphere of small cubes.
fn spawn_small_cubes(
commands: &mut Commands,
meshes: &mut Assets<Mesh>,
materials: &mut Assets<StandardMaterial>,
) {
// Add the cube mesh.
let small_cube = meshes.add(Cuboid::new(
SMALL_CUBE_SIZE,
SMALL_CUBE_SIZE,
SMALL_CUBE_SIZE,
));
// Add the cube material.
let small_cube_material = materials.add(StandardMaterial {
base_color: SILVER.into(),
..default()
});
// Create the entity that the small cubes will be parented to. This is the
// entity that we rotate.
let sphere_parent = commands
.spawn(Transform::from_translation(Vec3::ZERO))
.insert(Visibility::default())
.insert(SphereParent)
.id();
// Now we have to figure out where to place the cubes. To do that, we create
// a sphere mesh, but we don't add it to the scene. Instead, we inspect the
// sphere mesh to find the positions of its vertices, and spawn a small cube
// at each one. That way, we end up with a bunch of cubes arranged in a
// spherical shape.
// Create the sphere mesh, and extract the positions of its vertices.
let sphere = Sphere::new(OUTER_RADIUS)
.mesh()
.ico(OUTER_SUBDIVISION_COUNT)
.unwrap();
let sphere_positions = sphere.attribute(Mesh::ATTRIBUTE_POSITION).unwrap();
// At each vertex, create a small cube.
for sphere_position in sphere_positions.as_float3().unwrap() {
let sphere_position = Vec3::from_slice(sphere_position);
let small_cube = commands
.spawn(Mesh3d(small_cube.clone()))
.insert(MeshMaterial3d(small_cube_material.clone()))
.insert(Transform::from_translation(sphere_position))
.id();
commands.entity(sphere_parent).add_child(small_cube);
}
}
/// Spawns the large cube at the center of the screen.
///
/// This cube rotates chaotically and occludes small cubes behind it.
fn spawn_large_cube(
commands: &mut Commands,
asset_server: &AssetServer,
meshes: &mut Assets<Mesh>,
materials: &mut Assets<StandardMaterial>,
) {
commands
.spawn(Mesh3d(meshes.add(Cuboid::new(
LARGE_CUBE_SIZE,
LARGE_CUBE_SIZE,
LARGE_CUBE_SIZE,
))))
.insert(MeshMaterial3d(materials.add(StandardMaterial {
base_color: WHITE.into(),
base_color_texture: Some(asset_server.load("branding/icon.png")),
..default()
})))
.insert(Transform::IDENTITY)
.insert(LargeCube);
}
// Spins the outer sphere a bit every frame.
//
// This ensures that the set of cubes that are hidden and shown varies over
// time.
fn spin_small_cubes(mut sphere_parents: Query<&mut Transform, With<SphereParent>>) {
for mut sphere_parent_transform in &mut sphere_parents {
sphere_parent_transform.rotate_y(ROTATION_SPEED);
}
}
/// Spins the large cube a bit every frame.
///
/// The chaotic rotation adds a bit of randomness to the scene to better
/// demonstrate the dynamicity of the occlusion culling.
fn spin_large_cube(mut large_cubes: Query<&mut Transform, With<LargeCube>>) {
for mut transform in &mut large_cubes {
transform.rotate(Quat::from_euler(
EulerRot::XYZ,
0.13 * ROTATION_SPEED,
0.29 * ROTATION_SPEED,
0.35 * ROTATION_SPEED,
));
}
}
/// Spawns a directional light to illuminate the scene.
fn spawn_light(commands: &mut Commands) {
commands
.spawn(DirectionalLight::default())
.insert(Transform::from_rotation(Quat::from_euler(
EulerRot::ZYX,
0.0,
PI * -0.15,
PI * -0.15,
)));
}
/// Spawns a camera that includes the depth prepass and occlusion culling.
fn spawn_camera(commands: &mut Commands) {
commands
.spawn(Camera3d::default())
.insert(Transform::from_xyz(0.0, 0.0, 9.0).looking_at(Vec3::ZERO, Vec3::Y))
.insert(DepthPrepass)
.insert(OcclusionCulling);
}
/// Spawns the help text at the upper left of the screen.
fn spawn_help_text(commands: &mut Commands) {
commands.spawn((
Text::new(""),
Node {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
));
}
impl render_graph::Node for ReadbackIndirectParametersNode {
fn run<'w>(
&self,
_: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
world: &'w World,
) -> Result<(), NodeRunError> {
// Extract the buffers that hold the GPU indirect draw parameters from
// the world resources. We're going to read those buffers to determine
// how many meshes were actually drawn.
let (Some(indirect_parameters_buffers), Some(indirect_parameters_mapping_buffers)) = (
world.get_resource::<IndirectParametersBuffers>(),
world.get_resource::<IndirectParametersStagingBuffers>(),
) else {
return Ok(());
};
// Grab both the buffers we're copying from and the staging buffers
// we're copying to. Remember that we can't map the indirect parameters
// buffers directly, so we have to copy their contents to a staging
// buffer.
let (
Some(indexed_data_buffer),
Some(indexed_batch_sets_buffer),
Some(indirect_parameters_staging_data_buffer),
Some(indirect_parameters_staging_batch_sets_buffer),
) = (
indirect_parameters_buffers.indexed_data_buffer(),
indirect_parameters_buffers.indexed_batch_sets_buffer(),
indirect_parameters_mapping_buffers.data.as_ref(),
indirect_parameters_mapping_buffers.batch_sets.as_ref(),
)
else {
return Ok(());
};
// Copy from the indirect parameters buffers to the staging buffers.
render_context.command_encoder().copy_buffer_to_buffer(
indexed_data_buffer,
0,
indirect_parameters_staging_data_buffer,
0,
indexed_data_buffer.size(),
);
render_context.command_encoder().copy_buffer_to_buffer(
indexed_batch_sets_buffer,
0,
indirect_parameters_staging_batch_sets_buffer,
0,
indexed_batch_sets_buffer.size(),
);
Ok(())
}
}
/// Creates the staging buffers that we use to read back the indirect parameters
/// from the GPU to the CPU.
///
/// We read the indirect parameters from the GPU to the CPU in order to display
/// the number of meshes that were culled each frame.
///
/// We need these staging buffers because `wgpu` doesn't allow us to read the
/// contents of the indirect parameters buffers directly. We must first copy
/// them from the GPU to a staging buffer, and then read the staging buffer.
fn create_indirect_parameters_staging_buffers(
mut indirect_parameters_staging_buffers: ResMut<IndirectParametersStagingBuffers>,
indirect_parameters_buffers: Res<IndirectParametersBuffers>,
render_device: Res<RenderDevice>,
) {
// Fetch the indirect parameters buffers that we're going to copy from.
let (Some(indexed_data_buffer), Some(indexed_batch_set_buffer)) = (
indirect_parameters_buffers.indexed_data_buffer(),
indirect_parameters_buffers.indexed_batch_sets_buffer(),
) else {
return;
};
// Build the staging buffers. Make sure they have the same sizes as the
// buffers we're copying from.
indirect_parameters_staging_buffers.data =
Some(render_device.create_buffer(&BufferDescriptor {
label: Some("indexed data staging buffer"),
size: indexed_data_buffer.size(),
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
mapped_at_creation: false,
}));
indirect_parameters_staging_buffers.batch_sets =
Some(render_device.create_buffer(&BufferDescriptor {
label: Some("indexed batch set staging buffer"),
size: indexed_batch_set_buffer.size(),
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
mapped_at_creation: false,
}));
}
/// Updates the app status text at the top of the screen.
fn update_status_text(
saved_indirect_parameters: Res<SavedIndirectParameters>,
mut texts: Query<&mut Text>,
meshes: Query<Entity, With<Mesh3d>>,
app_status: Res<AppStatus>,
) {
// How many meshes are in the scene?
let total_mesh_count = meshes.iter().count();
// Sample the rendered object count. Note that we don't synchronize beyond
// locking the data and therefore this will value will generally at least
// one frame behind. This is fine; this app is just a demonstration after
// all.
let (
rendered_object_count,
occlusion_culling_supported,
occlusion_culling_introspection_supported,
): (u32, bool, bool) = {
let saved_indirect_parameters = saved_indirect_parameters.lock().unwrap();
(
saved_indirect_parameters
.data
.iter()
.take(saved_indirect_parameters.count as usize)
.map(|indirect_parameters| indirect_parameters.instance_count)
.sum(),
saved_indirect_parameters.occlusion_culling_supported,
saved_indirect_parameters.occlusion_culling_introspection_supported,
)
};
// Change the text.
for mut text in &mut texts {
text.0 = String::new();
if !occlusion_culling_supported {
text.0
.push_str("Occlusion culling not supported on this platform");
continue;
}
let _ = writeln!(
&mut text.0,
"Occlusion culling {} (Press Space to toggle)",
if app_status.occlusion_culling {
"ON"
} else {
"OFF"
},
);
if !occlusion_culling_introspection_supported {
continue;
}
let _ = write!(
&mut text.0,
"{}/{} meshes rendered",
rendered_object_count, total_mesh_count
);
}
}
/// A system that reads the indirect parameters back from the GPU so that we can
/// report how many meshes were culled.
fn readback_indirect_parameters(
mut indirect_parameters_staging_buffers: ResMut<IndirectParametersStagingBuffers>,
saved_indirect_parameters: Res<SavedIndirectParameters>,
gpu_preprocessing_support: Res<GpuPreprocessingSupport>,
) {
// If culling isn't supported on this platform, note that, and bail.
if gpu_preprocessing_support.max_supported_mode != GpuPreprocessingMode::Culling {
saved_indirect_parameters
.lock()
.unwrap()
.occlusion_culling_supported = false;
return;
}
// Grab the staging buffers.
let (Some(data_buffer), Some(batch_sets_buffer)) = (
indirect_parameters_staging_buffers.data.take(),
indirect_parameters_staging_buffers.batch_sets.take(),
) else {
return;
};
// Read the GPU buffers back.
let saved_indirect_parameters_0 = (**saved_indirect_parameters).clone();
let saved_indirect_parameters_1 = (**saved_indirect_parameters).clone();
readback_buffer::<IndirectParametersIndexed>(data_buffer, move |indirect_parameters| {
saved_indirect_parameters_0.lock().unwrap().data = indirect_parameters.to_vec();
});
readback_buffer::<u32>(batch_sets_buffer, move |indirect_parameters_count| {
saved_indirect_parameters_1.lock().unwrap().count = indirect_parameters_count[0];
});
}
// A helper function to asynchronously read an array of [`Pod`] values back from
// the GPU to the CPU.
//
// The given callback is invoked when the data is ready. The buffer will
// automatically be unmapped after the callback executes.
fn readback_buffer<T>(buffer: Buffer, callback: impl FnOnce(&[T]) + Send + 'static)
where
T: Pod,
{
// We need to make another reference to the buffer so that we can move the
// original reference into the closure below.
let original_buffer = buffer.clone();
original_buffer
.slice(..)
.map_async(MapMode::Read, move |result| {
// Make sure we succeeded.
if result.is_err() {
return;
}
{
// Cast the raw bytes in the GPU buffer to the appropriate type.
let buffer_view = buffer.slice(..).get_mapped_range();
let indirect_parameters: &[T] = bytemuck::cast_slice(
&buffer_view[0..(buffer_view.len() / size_of::<T>() * size_of::<T>())],
);
// Invoke the callback.
callback(indirect_parameters);
}
// Unmap the buffer. We have to do this before submitting any more
// GPU command buffers, or `wgpu` will assert.
buffer.unmap();
});
}
/// Adds or removes the [`OcclusionCulling`] and [`DepthPrepass`] components
/// when the user presses the spacebar.
fn toggle_occlusion_culling_on_request(
mut commands: Commands,
input: Res<ButtonInput<KeyCode>>,
mut app_status: ResMut<AppStatus>,
cameras: Query<Entity, With<Camera3d>>,
) {
// Only run when the user presses the spacebar.
if !input.just_pressed(KeyCode::Space) {
return;
}
// Toggle the occlusion culling flag in `AppStatus`.
app_status.occlusion_culling = !app_status.occlusion_culling;
// Add or remove the `OcclusionCulling` and `DepthPrepass` components as
// requested.
for camera in &cameras {
if app_status.occlusion_culling {
commands
.entity(camera)
.insert(DepthPrepass)
.insert(OcclusionCulling);
} else {
commands
.entity(camera)
.remove::<DepthPrepass>()
.remove::<OcclusionCulling>();
}
}
}