Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/usage/debug_frustum_culling.rs
30632 views
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
}
368
369