1use 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
41const OUTER_RADIUS: f32 = 3.0;
43
44const OUTER_SUBDIVISION_COUNT: u32 = 5;
46
47const ROTATION_SPEED: f32 = 0.01;
50
51const SMALL_CUBE_SIZE: f32 = 0.1;
53
54const LARGE_CUBE_SIZE: f32 = 2.0;
56
57#[derive(Default, Component)]
59struct SphereParent;
60
61#[derive(Default, Component)]
63struct LargeCube;
64
65struct ReadbackIndirectParametersPlugin;
68
69#[derive(Default)]
72struct ReadbackIndirectParametersNode;
73
74#[derive(Clone, PartialEq, Eq, Hash, Debug, RenderLabel)]
77struct ReadbackIndirectParameters;
78
79#[derive(Resource, Default)]
90struct IndirectParametersStagingBuffers {
91 data: Option<Buffer>,
96 batch_sets: Option<Buffer>,
100}
101
102#[derive(Clone, Resource, Deref, DerefMut)]
113struct SavedIndirectParameters(Arc<Mutex<Option<SavedIndirectParametersData>>>);
114
115struct SavedIndirectParametersData {
119 data: Vec<IndirectParametersIndexed>,
122 count: u32,
128 occlusion_culling_supported: bool,
130 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 occlusion_culling_introspection_supported: render_device
160 .features()
161 .contains(WgpuFeatures::MULTI_DRAW_INDIRECT_COUNT),
162 });
163}
164
165#[derive(Resource)]
167struct AppStatus {
168 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 let saved_indirect_parameters = SavedIndirectParameters::new();
223 app.insert_resource(saved_indirect_parameters.clone());
224
225 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
227 return;
228 };
229
230 render_app
231 .insert_resource(saved_indirect_parameters)
233 .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_render_graph_node::<ReadbackIndirectParametersNode>(
246 Core3d,
247 ReadbackIndirectParameters,
248 )
249 .add_render_graph_edges(
256 Core3d,
257 (
258 Node3d::EndMainPass,
259 ReadbackIndirectParameters,
260 Node3d::EndMainPassPostProcessing,
261 ),
262 );
263 }
264}
265
266fn 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
280fn spawn_small_cubes(
282 commands: &mut Commands,
283 meshes: &mut Assets<Mesh>,
284 materials: &mut Assets<StandardMaterial>,
285) {
286 let small_cube = meshes.add(Cuboid::new(
288 SMALL_CUBE_SIZE,
289 SMALL_CUBE_SIZE,
290 SMALL_CUBE_SIZE,
291 ));
292
293 let small_cube_material = materials.add(StandardMaterial {
295 base_color: SILVER.into(),
296 ..default()
297 });
298
299 let sphere_parent = commands
302 .spawn(Transform::from_translation(Vec3::ZERO))
303 .insert(Visibility::default())
304 .insert(SphereParent)
305 .id();
306
307 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 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
332fn 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
356fn 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
366fn 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
381fn 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
393fn 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
402fn 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 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 let Some(phase_indirect_parameters_buffers) =
435 indirect_parameters_buffers.get(&TypeId::of::<Opaque3d>())
436 else {
437 return Ok(());
438 };
439
440 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 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
481fn 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 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 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
529fn 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 let total_mesh_count = meshes.iter().count();
538
539 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 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 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
595fn readback_indirect_parameters(
598 mut indirect_parameters_staging_buffers: ResMut<IndirectParametersStagingBuffers>,
599 saved_indirect_parameters: Res<SavedIndirectParameters>,
600) {
601 if !saved_indirect_parameters
603 .lock()
604 .unwrap()
605 .as_ref()
606 .unwrap()
607 .occlusion_culling_supported
608 {
609 return;
610 }
611
612 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 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
641fn readback_buffer<T>(buffer: Buffer, callback: impl FnOnce(&[T]) + Send + 'static)
647where
648 T: Pod,
649{
650 let original_buffer = buffer.clone();
653 original_buffer
654 .slice(..)
655 .map_async(MapMode::Read, move |result| {
656 if result.is_err() {
658 return;
659 }
660
661 {
662 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 callback(indirect_parameters);
670 }
671
672 buffer.unmap();
675 });
676}
677
678fn 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 if !input.just_pressed(KeyCode::Space) {
688 return;
689 }
690
691 app_status.occlusion_culling = !app_status.occlusion_culling;
693
694 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}