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