Skip to main content

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