1use 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
33const OUTER_RADIUS: f32 = 3.0;
35
36const OUTER_SUBDIVISION_COUNT: u32 = 5;
38
39const ROTATION_SPEED: f32 = 0.01;
42
43const SMALL_CUBE_SIZE: f32 = 0.1;
45
46const LARGE_CUBE_SIZE: f32 = 2.0;
48
49#[derive(Default, Component)]
51struct SphereParent;
52
53#[derive(Default, Component)]
55struct LargeCube;
56
57struct ReadbackIndirectParametersPlugin;
60
61#[derive(Resource, Default)]
72struct IndirectParametersStagingBuffers {
73 data: Option<Buffer>,
78 batch_sets: Option<Buffer>,
82}
83
84#[derive(Clone, Resource, Deref, DerefMut)]
95struct SavedIndirectParameters(Arc<Mutex<Option<SavedIndirectParametersData>>>);
96
97struct SavedIndirectParametersData {
101 data: Vec<IndirectParametersIndexed>,
104 count: u32,
110 occlusion_culling_supported: bool,
112 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 occlusion_culling_introspection_supported: render_device
142 .features()
143 .contains(WgpuFeatures::MULTI_DRAW_INDIRECT_COUNT),
144 });
145}
146
147#[derive(Resource)]
149struct AppStatus {
150 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 let saved_indirect_parameters = SavedIndirectParameters::new();
205 app.insert_resource(saved_indirect_parameters.clone());
206
207 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
209 return;
210 };
211
212 render_app
213 .insert_resource(saved_indirect_parameters)
215 .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 readback_indirect_parameters_node
230 .after(Core3dSystems::MainPass)
234 .before(Core3dSystems::PostProcess),
235 );
236 }
237}
238
239fn 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
253fn spawn_small_cubes(
255 commands: &mut Commands,
256 meshes: &mut Assets<Mesh>,
257 materials: &mut Assets<StandardMaterial>,
258) {
259 let small_cube = meshes.add(Cuboid::new(
261 SMALL_CUBE_SIZE,
262 SMALL_CUBE_SIZE,
263 SMALL_CUBE_SIZE,
264 ));
265
266 let small_cube_material = materials.add(StandardMaterial {
268 base_color: SILVER.into(),
269 ..default()
270 });
271
272 let sphere_parent = commands
275 .spawn(Transform::from_translation(Vec3::ZERO))
276 .insert(Visibility::default())
277 .insert(SphereParent)
278 .id();
279
280 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 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
305fn 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
329fn 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
339fn 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
354fn 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
366fn 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
375fn 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 let Some(phase_indirect_parameters_buffers) =
396 indirect_parameters_buffers.get(&TypeId::of::<Opaque3d>())
397 else {
398 return;
399 };
400
401 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 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
439fn 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 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 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
487fn 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 let total_mesh_count = meshes.iter().count();
496
497 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 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 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
553fn readback_indirect_parameters(
556 mut indirect_parameters_staging_buffers: ResMut<IndirectParametersStagingBuffers>,
557 saved_indirect_parameters: Res<SavedIndirectParameters>,
558) {
559 if !saved_indirect_parameters
561 .lock()
562 .unwrap()
563 .as_ref()
564 .unwrap()
565 .occlusion_culling_supported
566 {
567 return;
568 }
569
570 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 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
599fn readback_buffer<T>(buffer: Buffer, callback: impl FnOnce(&[T]) + Send + 'static)
605where
606 T: Pod,
607{
608 let original_buffer = buffer.clone();
611 original_buffer
612 .slice(..)
613 .map_async(MapMode::Read, move |result| {
614 if result.is_err() {
616 return;
617 }
618
619 {
620 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 callback(indirect_parameters);
628 }
629
630 buffer.unmap();
633 });
634}
635
636fn 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 if !input.just_pressed(KeyCode::Space) {
646 return;
647 }
648
649 app_status.occlusion_culling = !app_status.occlusion_culling;
651
652 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}