Skip to main content

debug_frustum_culling/
debug_frustum_culling.rs

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
31use 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};
41use std::f32::consts::PI;
42
43fn 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)]
76struct 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)]
83struct MyShape;
84
85/// A marker component for the shape behind the wall.
86#[derive(Component)]
87#[require(MyShape)]
88struct 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)]
95struct MyCamera;
96
97const SHAPE_RING_RADIUS: f32 = 10.0;
98const WALL_SHAPE_TIMER_DURATION_SECS: f32 = 8.0;
99const FREE_CAMERA_START_TRANSFORM: Transform = Transform::from_xyz(-20., 10., 22.);
100const FREE_CAMERA_START_TARGET: Vec3 = Vec3::new(7., 1.5, 0.);
101
102fn 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
266fn 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.
304fn 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.
327fn 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.
339fn 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
351fn 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}