occlusion_culling/
occlusion_culling.rs

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