docs/bevy/examples/3d/occlusion_culling.rs 25 K · 710 lines · raw · history

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
8 use std::{
9 any::TypeId,
10 f32::consts::PI,
11 fmt::{self, Formatter, Write as _},
12 sync::{Arc, Mutex},
13 };
14
15 use 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 };
33 use radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
34
35 #[path = "../helpers/radio.rs"]
36 mod radio;
37
38 #[path = "../helpers/theme.rs"]
39 mod theme;
40
41 use bytemuck::Pod;
42
43 /// The radius of the spinning sphere of cubes.
44 const OUTER_RADIUS: f32 = 3.0;
45
46 /// The density of cubes in the other sphere.
47 const OUTER_SUBDIVISION_COUNT: u32 = 5;
48
49 /// The speed at which the outer sphere and large cube rotate in radians per
50 /// frame.
51 const ROTATION_SPEED: f32 = 0.01;
52
53 /// The length of each side of the small cubes, in meters.
54 const SMALL_CUBE_SIZE: f32 = 0.1;
55
56 /// The length of each side of the large cube, in meters.
57 const 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)]
61 struct SphereParent;
62
63 /// A marker component for the large spinning cube at the origin.
64 #[derive(Default, Component)]
65 struct LargeCube;
66
67 /// A plugin for the render app that reads the number of culled meshes from the
68 /// GPU back to the CPU.
69 struct 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)]
82 struct 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)]
105 struct 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.
110 struct 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
132 impl SavedIndirectParameters {
133 fn new() -> Self {
134 Self(Arc::new(Mutex::new(None)))
135 }
136 }
137
138 fn 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)]
159 struct AppStatus {
160 /// Whether occlusion culling is presently enabled.
161 ///
162 /// By default, this is set to true.
163 occlusion_culling: OcclusionCullingSetting,
164 }
165
166 impl Default for AppStatus {
167 fn default() -> Self {
168 AppStatus {
169 occlusion_culling: OcclusionCullingSetting::On,
170 }
171 }
172 }
173
174 fn 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
209 impl 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.
253 fn 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.
268 fn 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.
322 fn 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.
347 fn 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.
357 fn 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.
369 fn 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.
381 fn 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.
390 fn 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
403 fn 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.
463 fn 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.
503 fn 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.
559 fn 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.
608 fn readback_buffer<T>(buffer: Buffer, callback: impl FnOnce(&[T]) + Send + 'static)
609 where
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)]
642 struct StatusText;
643
644 /// Whether occlusion culling is on or off.
645 #[derive(Clone, Copy, Component, Default, PartialEq, Debug)]
646 enum OcclusionCullingSetting {
647 #[default]
648 On,
649 Off,
650 }
651
652 impl 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.
663 fn 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.
681 fn 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 }