docs/bevy/examples/usage/debug_frustum_culling.rs 13.3 K · 367 lines · raw · history

1 //! This example demonstrates how to debug and visualize frustum culling,
2 //! a process (and Bevy [`system`](bevy::camera::visibility::check_visibility)) that determines
3 //! which entities are visible and should be rendered within
4 //! a camera's view. If an entity's [`Aabb`](bevy::camera::primitives::Aabb)
5 //! (Axis-Aligned Bounding Box) does not intersect with a camera's
6 //! [`Frustum`](bevy::camera::primitives::Frustum), that entity is said to be "culled"
7 //! from the camera's view frustum.
8 //!
9 //! To debug and visualize frustum culling, this example uses Aabb and Frustum gizmos provided
10 //! by bevy's [`Gizmo`] library. [`Aabb gizmos`](bevy::gizmos::aabb) are used to visualize the
11 //! [`Aabb`](bevy::camera::primitives::Aabb) of entities.
12 //! [`Frustum gizmos`](bevy::gizmos::frustum) are used to visualize the
13 //! [`Frustum`](bevy::camera::primitives::Frustum) of a camera.
14 //! Both can be used together to visualize which entities have been culled
15 //! from a given camera's view, which entities are visible, and when
16 //! that change happens during an entity's Aabb interaction with a camera's Frustum.
17 //!
18 //! This example contains a scene with a camera `MyCamera` that has its
19 //! [`Frustum`](bevy::camera::primitives::Frustum) gizmo visible.
20 //! A collection of `MyShape`s, with their individual
21 //! [`Aabb`](bevy::camera::primitives::Aabb) gizmos visible, periodically move in and
22 //! out of the camera's frustum. The [`Aabb`](bevy::camera::primitives::Aabb)
23 //! gizmos are colored red when they have been culled from `MyCamera`'s view.
24 //! The gizmos change color to green when the shape is considered visible by the
25 //! camera and would be extracted for rendering.
26 //!
27 //! A second active camera, controllable via the [`FreeCameraPlugin`], is used to observe the scene.
28 //! This second camera's view occupies most of the window. `MyCamera`'s view is visible in the
29 //! bottom right ninth of the screen.
30
31 use bevy::{
32 camera::{
33 visibility::{VisibilitySystems, VisibleEntities},
34 Viewport,
35 },
36 camera_controller::free_camera::{FreeCamera, FreeCameraPlugin, FreeCameraState},
37 gizmos::aabb::ShowAabbGizmo,
38 input::common_conditions::input_just_pressed,
39 prelude::*,
40 };
41 use std::f32::consts::PI;
42
43 fn main() {
44 App::new()
45 .add_plugins((
46 DefaultPlugins.set(WindowPlugin {
47 primary_window: Some(Window {
48 resizable: false,
49 ..default()
50 }),
51 ..default()
52 }),
53 FreeCameraPlugin,
54 ))
55 .add_systems(Startup, setup)
56 .add_systems(
57 Update,
58 (
59 move_shapes,
60 move_free_camera_to_my_camera.run_if(input_just_pressed(KeyCode::Digit1)),
61 move_free_camera_to_original_position.run_if(input_just_pressed(KeyCode::Digit2)),
62 ),
63 )
64 .add_systems(
65 // Frustum culling happens in PostUpdate.
66 // Our system will update the color of aabb's upon reading
67 // the results of frustum culling after CheckVisibility runs.
68 PostUpdate,
69 update_shape_aabb_colors.after(VisibilitySystems::CheckVisibility),
70 )
71 .run();
72 }
73
74 /// A marker component for the ring some shapes will rotate on
75 #[derive(Component)]
76 struct ShapeRing;
77
78 /// A marker component for our shapes so they can be queried separately from the planes.
79 /// The `ShowAabbGizmo` component will be automatically added to `MyShape` to make their Aabbs
80 /// visible.
81 #[derive(Component, Default)]
82 #[require(ShowAabbGizmo)]
83 struct MyShape;
84
85 /// A marker component for the shape behind the wall.
86 #[derive(Component)]
87 #[require(MyShape)]
88 struct WallShape;
89
90 /// A marker component for the camera that is being debugged
91 /// The `ShowFrustumGizmo` component will be automatically added to `MyCamera` to make
92 /// its view frustum visible.
93 #[derive(Component)]
94 #[require(ShowFrustumGizmo)]
95 struct MyCamera;
96
97 const SHAPE_RING_RADIUS: f32 = 10.0;
98 const WALL_SHAPE_TIMER_DURATION_SECS: f32 = 8.0;
99 const FREE_CAMERA_START_TRANSFORM: Transform = Transform::from_xyz(-20., 10., 22.);
100 const FREE_CAMERA_START_TARGET: Vec3 = Vec3::new(7., 1.5, 0.);
101
102 fn setup(
103 mut commands: Commands,
104 windows: Query<&Window>,
105 mut config_store: ResMut<GizmoConfigStore>,
106 mut meshes: ResMut<Assets<Mesh>>,
107 mut materials: ResMut<Assets<StandardMaterial>>,
108 ) -> Result {
109 let window = windows.single()?;
110 // The camera that the user controls to observe the scene.
111 let free_camera = commands
112 .spawn((
113 Camera3d::default(),
114 FREE_CAMERA_START_TRANSFORM.looking_at(FREE_CAMERA_START_TARGET, Vec3::Y),
115 FreeCamera::default(),
116 ))
117 .id();
118
119 // The camera that we want to debug frustum culling for. This will be rendered
120 // as a picture-in-picture in the lower right ninth of the screen.
121 let my_camera = commands
122 .spawn((
123 Camera3d::default(),
124 Transform::from_xyz(0., 1.5, 0.).looking_at(Vec3::new(1.0, 1.5, 0.), Vec3::Y),
125 Camera {
126 order: 1,
127 // The camera-to-debug's view will be in the lower right ninth of the screen.
128 viewport: Some(Viewport {
129 physical_position: window.physical_size() * 2 / 3,
130 physical_size: window.physical_size() / 3,
131 ..default()
132 }),
133 // Do not write the free camera's view rendering back into the P-I-P
134 msaa_writeback: MsaaWriteback::Off,
135 ..default()
136 },
137 MyCamera,
138 ))
139 .id();
140
141 // Instructions placed on top of the free_camera view
142 commands.spawn((
143 UiTargetCamera(free_camera),
144 Node {
145 width: percent(100),
146 height: percent(100),
147 ..default()
148 },
149 children![(
150 Text::new(
151 "This example utilizes free camera controls i.e. move with WASD and mouse grab to change orientation.\n\
152 Press '1' to move the free camera to where MyCamera is, matching its view frustum.\n\
153 Press '2' to move the free camera to its initial position in the example.",
154 ),
155 Node {
156 position_type: PositionType::Absolute,
157 top: px(12),
158 left: px(12),
159 ..default()
160 },
161 )]
162 ));
163 // Label for the picture-in-picture view of MyCamera
164 commands.spawn((
165 UiTargetCamera(my_camera),
166 Node {
167 width: percent(100),
168 height: percent(100),
169 ..default()
170 },
171 children![(
172 Text::new("View of MyCamera"),
173 Node {
174 position_type: PositionType::Absolute,
175 bottom: px(12),
176 right: px(100),
177 ..default()
178 },
179 )],
180 ));
181
182 // Green Floor Plane
183 commands.spawn((
184 Mesh3d(
185 meshes.add(
186 Plane3d::default()
187 .mesh()
188 .size(SHAPE_RING_RADIUS * 4., SHAPE_RING_RADIUS * 4.),
189 ),
190 ),
191 MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
192 ));
193 // Blue Wall Plane
194 commands.spawn((
195 Mesh3d(meshes.add(Plane3d::default().mesh().size(5., 5.))),
196 MeshMaterial3d(materials.add(Color::srgb(0.3, 0.3, 0.5))),
197 Transform::from_xyz(20., 2.5, 10.).with_rotation(Quat::from_rotation_z(PI / 2.)),
198 ));
199 // Light
200 commands.spawn((
201 PointLight {
202 shadow_maps_enabled: true,
203 ..default()
204 },
205 Transform::from_xyz(0.0, 10.0, 0.0),
206 ));
207
208 // Configure all AABB's to have a default color of red
209 let (_, aabb_gizmo_config) = config_store.config_mut::<AabbGizmoConfigGroup>();
210 aabb_gizmo_config.default_color = Some(Color::LinearRgba(LinearRgba::RED));
211
212 // Configure the shapes on the ring that will have their AABB's drawn and updated
213 let white_matl = materials.add(Color::WHITE);
214 let shapes = [
215 meshes.add(Cuboid {
216 half_size: Vec3::new(2., 0.5, 1.),
217 }),
218 meshes.add(Tetrahedron {
219 vertices: [
220 Vec3::new(3., 4., 3.),
221 Vec3::new(-0.5, 4., -0.5),
222 Vec3::new(-0.5, -0.5, 3.),
223 Vec3::new(3., -0.5, -0.5),
224 ],
225 }),
226 meshes.add(Cylinder {
227 radius: 0.1,
228 half_height: 1.5,
229 }),
230 meshes.add(Cuboid {
231 half_size: Vec3::new(1., 0.1, 2.),
232 }),
233 meshes.add(Sphere::default().mesh().ico(5).unwrap()),
234 ];
235 let shapes_len = shapes.len() as f32;
236 let mut shape_ring = commands.spawn((Transform::default(), Visibility::default(), ShapeRing));
237 for (i, shape) in shapes.into_iter().enumerate() {
238 // Space the shapes out evenly along the ring
239 let shape_angle = i as f32 * 2. * PI / shapes_len;
240 let (s, c) = ops::sin_cos(shape_angle);
241 let (x, z) = (SHAPE_RING_RADIUS * c, SHAPE_RING_RADIUS * s);
242 shape_ring.with_child((
243 Mesh3d(shape),
244 MeshMaterial3d(white_matl.clone()),
245 Transform::from_xyz(x, 1.5, z).with_rotation(Quat::from_rotation_x(-PI / 4.)),
246 MyShape,
247 ));
248 }
249
250 // Configure the shape that peeks out of the wall plane
251 let wall_shape = meshes.add(Torus::default());
252 commands.spawn((
253 Mesh3d(wall_shape),
254 MeshMaterial3d(white_matl.clone()),
255 Transform::from_xyz(25., 1.5, 12.5).with_rotation(Quat::from_rotation_x(-PI / 4.)),
256 WallShape,
257 ));
258
259 Ok(())
260 }
261
262 // A system that:
263 // - rotates shapes in place
264 // - moves the ring shapes in a circle around MyCamera
265 // - moves the wall shape up and down
266 fn move_shapes(
267 time: Res<Time>,
268 mut timer: Local<Timer>,
269 mut ring_query: Query<&mut Transform, (With<ShapeRing>, Without<MyShape>)>,
270 mut shape_query: Query<(&mut Transform, Has<WallShape>), (With<MyShape>, Without<ShapeRing>)>,
271 ) -> Result {
272 // Initialize the wall shape's movement timer on the first run.
273 if timer.duration().is_zero() {
274 *timer = Timer::from_seconds(WALL_SHAPE_TIMER_DURATION_SECS, TimerMode::Repeating);
275 }
276 timer.tick(time.delta());
277 let dt = time.delta_secs();
278
279 // Rotate the shapes themselves on their own axis
280 for (mut transform, has_wall_shape) in &mut shape_query {
281 transform.rotate_y(dt / 2.);
282 if has_wall_shape {
283 // the wall shape moves up for 4 seconds and then down for 4 seconds.
284 // it oscillates between y = 1.5 and 15.0
285 transform.translation.y = if timer.elapsed_secs() < WALL_SHAPE_TIMER_DURATION_SECS / 2.0
286 {
287 1.5 + 15.0 * timer.elapsed_secs() / (WALL_SHAPE_TIMER_DURATION_SECS / 2.0)
288 } else {
289 1.5 + 15.0 * (WALL_SHAPE_TIMER_DURATION_SECS - timer.elapsed_secs())
290 / (WALL_SHAPE_TIMER_DURATION_SECS / 2.0)
291 }
292 }
293 }
294
295 // Rotate the ring
296 let transform = &mut ring_query.single_mut()?;
297 transform.rotate_y(dt / 3.);
298
299 Ok(())
300 }
301
302 // A system that changes the color of the [`AabbGizmo`](bevy::gizmos::Aabb)
303 // if they are considered visible by the camera.
304 fn update_shape_aabb_colors(
305 view_query: Query<&VisibleEntities, With<MyCamera>>,
306 mut gizmo_query: Query<&mut ShowAabbGizmo, With<MyShape>>,
307 ) -> Result {
308 // Reset the color to use the config's default color
309 for mut shape_gizmo in &mut gizmo_query {
310 shape_gizmo.color = None;
311 }
312
313 // Query for the shape entities visible for this camera
314 // Update the gizmo on any such shape entity to be green
315 let visible_entities = view_query.single()?;
316 for entity in visible_entities.entities.values().flatten() {
317 if let Ok(mut shape_gizmo) = gizmo_query.get_mut(*entity) {
318 shape_gizmo.color = Some(Color::LinearRgba(LinearRgba::GREEN));
319 }
320 }
321 Ok(())
322 }
323
324 // A system that moves the free camera to `MyCamera`, matching its view frustum.
325 // From here, the camera orientation can be moved to more easily see the transition of
326 // entities' visibilities with respect to `MyCamera` by looking at the frustum edges.
327 fn move_free_camera_to_my_camera(
328 view_query: Query<&Transform, With<MyCamera>>,
329 free_camera_query: Query<
330 (&mut Transform, &mut FreeCameraState),
331 (With<Camera3d>, Without<MyCamera>),
332 >,
333 ) -> Result {
334 let my_camera_transform = view_query.single()?;
335 move_free_camera(*my_camera_transform, free_camera_query)
336 }
337
338 // A system that moves the free camera back to its starting position in the example.
339 fn move_free_camera_to_original_position(
340 free_camera_query: Query<
341 (&mut Transform, &mut FreeCameraState),
342 (With<Camera3d>, Without<MyCamera>),
343 >,
344 ) -> Result {
345 move_free_camera(
346 FREE_CAMERA_START_TRANSFORM.looking_at(FREE_CAMERA_START_TARGET, Vec3::Y),
347 free_camera_query,
348 )
349 }
350
351 fn move_free_camera(
352 new_transform: Transform,
353 mut free_camera_query: Query<
354 (&mut Transform, &mut FreeCameraState),
355 (With<Camera3d>, Without<MyCamera>),
356 >,
357 ) -> Result {
358 let (mut transform, mut state) = free_camera_query.single_mut()?;
359 *transform = new_transform;
360
361 // Update the yaw and pitch so that free camera orientation is updated correctly upon mouse grab
362 let (yaw, pitch, _roll) = transform.rotation.to_euler(EulerRot::YXZ);
363 state.yaw = yaw;
364 state.pitch = pitch;
365
366 Ok(())
367 }