docs/bevy/examples/3d/visibility_range.rs 12 K · 382 lines · raw · history

1 //! Demonstrates visibility ranges, also known as HLODs.
2
3 use std::f32::consts::PI;
4
5 use bevy::{
6 camera::visibility::{NoCpuCulling, VisibilityRange},
7 core_pipeline::prepass::{DepthPrepass, NormalPrepass},
8 input::mouse::MouseWheel,
9 light::{light_consts::lux::FULL_DAYLIGHT, CascadeShadowConfigBuilder},
10 math::vec3,
11 prelude::*,
12 };
13
14 use argh::FromArgs;
15
16 // Where the camera is focused.
17 const CAMERA_FOCAL_POINT: Vec3 = vec3(0.0, 0.3, 0.0);
18 // Speed in units per frame.
19 const CAMERA_KEYBOARD_ZOOM_SPEED: f32 = 0.05;
20 // Speed in radians per frame.
21 const CAMERA_KEYBOARD_PAN_SPEED: f32 = 0.01;
22 // Speed in units per frame.
23 const CAMERA_MOUSE_MOVEMENT_SPEED: f32 = 0.25;
24 // The minimum distance that the camera is allowed to be from the model.
25 const MIN_ZOOM_DISTANCE: f32 = 0.5;
26
27 // The visibility ranges for high-poly and low-poly models respectively, when
28 // both models are being shown.
29 static NORMAL_VISIBILITY_RANGE_HIGH_POLY: VisibilityRange = VisibilityRange {
30 start_margin: 0.0..0.0,
31 end_margin: 3.0..4.0,
32 use_aabb: false,
33 };
34 static NORMAL_VISIBILITY_RANGE_LOW_POLY: VisibilityRange = VisibilityRange {
35 start_margin: 3.0..4.0,
36 end_margin: 8.0..9.0,
37 use_aabb: false,
38 };
39
40 // A visibility model that we use to always show a model (until the camera is so
41 // far zoomed out that it's culled entirely).
42 static SINGLE_MODEL_VISIBILITY_RANGE: VisibilityRange = VisibilityRange {
43 start_margin: 0.0..0.0,
44 end_margin: 8.0..9.0,
45 use_aabb: false,
46 };
47
48 // A visibility range that we use to completely hide a model.
49 static INVISIBLE_VISIBILITY_RANGE: VisibilityRange = VisibilityRange {
50 start_margin: 0.0..0.0,
51 end_margin: 0.0..0.0,
52 use_aabb: false,
53 };
54
55 // Allows us to identify the main model.
56 #[derive(Component, Debug, Clone, Copy, PartialEq)]
57 enum MainModel {
58 // The high-poly version.
59 HighPoly,
60 // The low-poly version.
61 LowPoly,
62 }
63
64 // The current mode.
65 #[derive(Default, Resource)]
66 struct AppStatus {
67 // Whether to show only one model.
68 show_one_model_only: Option<MainModel>,
69 // Whether to enable the prepass.
70 prepass: bool,
71 }
72
73 /// Demonstrates visibility ranges, also known as HLODs
74 #[derive(FromArgs, Resource)]
75 struct Args {
76 /// whether to use GPU culling only
77 #[argh(switch)]
78 no_cpu_culling: bool,
79 }
80
81 fn main() {
82 #[cfg(not(target_arch = "wasm32"))]
83 let args: Args = argh::from_env();
84 #[cfg(target_arch = "wasm32")]
85 let args = Args::from_args(&[], &[]).unwrap();
86
87 App::new()
88 .add_plugins(DefaultPlugins.set(WindowPlugin {
89 primary_window: Some(Window {
90 title: "Bevy Visibility Range Example".into(),
91 ..default()
92 }),
93 ..default()
94 }))
95 .init_resource::<AppStatus>()
96 .insert_resource(args)
97 .add_systems(Startup, setup)
98 .add_systems(
99 Update,
100 (
101 move_camera,
102 set_visibility_ranges,
103 update_help_text,
104 update_mode,
105 toggle_prepass,
106 ),
107 )
108 .run();
109 }
110
111 // Set up a simple 3D scene. Load the two meshes.
112 fn setup(
113 mut commands: Commands,
114 mut meshes: ResMut<Assets<Mesh>>,
115 mut materials: ResMut<Assets<StandardMaterial>>,
116 asset_server: Res<AssetServer>,
117 app_status: Res<AppStatus>,
118 ) {
119 // Spawn a plane.
120 commands.spawn((
121 Mesh3d(meshes.add(Plane3d::default().mesh().size(50.0, 50.0))),
122 MeshMaterial3d(materials.add(Color::srgb(0.1, 0.2, 0.1))),
123 ));
124
125 // Spawn the two HLODs.
126
127 commands.spawn((
128 WorldAssetRoot(
129 asset_server
130 .load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")),
131 ),
132 MainModel::HighPoly,
133 ));
134
135 commands.spawn((
136 WorldAssetRoot(
137 asset_server.load(
138 GltfAssetLabel::Scene(0)
139 .from_asset("models/FlightHelmetLowPoly/FlightHelmetLowPoly.gltf"),
140 ),
141 ),
142 MainModel::LowPoly,
143 ));
144
145 // Spawn a light.
146 commands.spawn((
147 DirectionalLight {
148 illuminance: FULL_DAYLIGHT,
149 shadow_maps_enabled: true,
150 ..default()
151 },
152 Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, PI * -0.15, PI * -0.15)),
153 CascadeShadowConfigBuilder {
154 maximum_distance: 30.0,
155 first_cascade_far_bound: 0.9,
156 ..default()
157 }
158 .build(),
159 ));
160
161 // Spawn a camera.
162 commands
163 .spawn((
164 Camera3d::default(),
165 Transform::from_xyz(0.7, 0.7, 1.0).looking_at(CAMERA_FOCAL_POINT, Vec3::Y),
166 ))
167 .insert(EnvironmentMapLight {
168 diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
169 specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
170 intensity: 150.0,
171 ..default()
172 });
173
174 // Create the text.
175 commands.spawn((
176 app_status.create_text(),
177 Node {
178 position_type: PositionType::Absolute,
179 bottom: px(12),
180 left: px(12),
181 ..default()
182 },
183 ));
184 }
185
186 // We need to add the `VisibilityRange` components manually, as glTF currently
187 // has no way to specify visibility ranges. This system watches for new meshes,
188 // determines which `Scene` they're under, and adds the `VisibilityRange`
189 // component as appropriate.
190 fn set_visibility_ranges(
191 mut commands: Commands,
192 mut new_meshes: Query<Entity, Added<Mesh3d>>,
193 children: Query<(Option<&ChildOf>, Option<&MainModel>)>,
194 args: Res<Args>,
195 ) {
196 // Loop over each newly-added mesh.
197 for new_mesh in new_meshes.iter_mut() {
198 // Search for the nearest ancestor `MainModel` component.
199 let (mut current, mut main_model) = (new_mesh, None);
200 while let Ok((child_of, maybe_main_model)) = children.get(current) {
201 if let Some(model) = maybe_main_model {
202 main_model = Some(model);
203 break;
204 }
205 match child_of {
206 Some(child_of) => current = child_of.parent(),
207 None => break,
208 }
209 }
210
211 // Add the `VisibilityRange` component.
212 match main_model {
213 Some(MainModel::HighPoly) => {
214 let mut entity_commands = commands.entity(new_mesh);
215 entity_commands
216 .insert(NORMAL_VISIBILITY_RANGE_HIGH_POLY.clone())
217 .insert(MainModel::HighPoly);
218 if args.no_cpu_culling {
219 entity_commands.insert(NoCpuCulling);
220 }
221 }
222 Some(MainModel::LowPoly) => {
223 let mut entity_commands = commands.entity(new_mesh);
224 entity_commands
225 .insert(NORMAL_VISIBILITY_RANGE_LOW_POLY.clone())
226 .insert(MainModel::LowPoly);
227 if args.no_cpu_culling {
228 entity_commands.insert(NoCpuCulling);
229 }
230 }
231 None => {}
232 }
233 }
234 }
235
236 // Process the movement controls.
237 fn move_camera(
238 keyboard_input: Res<ButtonInput<KeyCode>>,
239 mut mouse_wheel_reader: MessageReader<MouseWheel>,
240 mut cameras: Query<&mut Transform, With<Camera3d>>,
241 ) {
242 let (mut zoom_delta, mut theta_delta) = (0.0, 0.0);
243
244 // Process zoom in and out via the keyboard.
245 if keyboard_input.pressed(KeyCode::KeyW) || keyboard_input.pressed(KeyCode::ArrowUp) {
246 zoom_delta -= CAMERA_KEYBOARD_ZOOM_SPEED;
247 } else if keyboard_input.pressed(KeyCode::KeyS) || keyboard_input.pressed(KeyCode::ArrowDown) {
248 zoom_delta += CAMERA_KEYBOARD_ZOOM_SPEED;
249 }
250
251 // Process left and right pan via the keyboard.
252 if keyboard_input.pressed(KeyCode::KeyA) || keyboard_input.pressed(KeyCode::ArrowLeft) {
253 theta_delta -= CAMERA_KEYBOARD_PAN_SPEED;
254 } else if keyboard_input.pressed(KeyCode::KeyD) || keyboard_input.pressed(KeyCode::ArrowRight) {
255 theta_delta += CAMERA_KEYBOARD_PAN_SPEED;
256 }
257
258 // Process zoom in and out via the mouse wheel.
259 for mouse_wheel in mouse_wheel_reader.read() {
260 zoom_delta -= mouse_wheel.y * CAMERA_MOUSE_MOVEMENT_SPEED;
261 }
262
263 // Update the camera transform.
264 for transform in cameras.iter_mut() {
265 let transform = transform.into_inner();
266
267 let direction = transform.translation.normalize_or_zero();
268 let magnitude = transform.translation.length();
269
270 let new_direction = Mat3::from_rotation_y(theta_delta) * direction;
271 let new_magnitude = (magnitude + zoom_delta).max(MIN_ZOOM_DISTANCE);
272
273 transform.translation = new_direction * new_magnitude;
274 transform.look_at(CAMERA_FOCAL_POINT, Vec3::Y);
275 }
276 }
277
278 // Toggles modes if the user requests.
279 fn update_mode(
280 mut meshes: Query<(&mut VisibilityRange, &MainModel)>,
281 keyboard_input: Res<ButtonInput<KeyCode>>,
282 mut app_status: ResMut<AppStatus>,
283 ) {
284 // Toggle the mode as requested.
285 if keyboard_input.just_pressed(KeyCode::Digit1) || keyboard_input.just_pressed(KeyCode::Numpad1)
286 {
287 app_status.show_one_model_only = None;
288 } else if keyboard_input.just_pressed(KeyCode::Digit2)
289 || keyboard_input.just_pressed(KeyCode::Numpad2)
290 {
291 app_status.show_one_model_only = Some(MainModel::HighPoly);
292 } else if keyboard_input.just_pressed(KeyCode::Digit3)
293 || keyboard_input.just_pressed(KeyCode::Numpad3)
294 {
295 app_status.show_one_model_only = Some(MainModel::LowPoly);
296 } else {
297 return;
298 }
299
300 // Update the visibility ranges as appropriate.
301 for (mut visibility_range, main_model) in meshes.iter_mut() {
302 *visibility_range = match (main_model, app_status.show_one_model_only) {
303 (&MainModel::HighPoly, Some(MainModel::LowPoly))
304 | (&MainModel::LowPoly, Some(MainModel::HighPoly)) => {
305 INVISIBLE_VISIBILITY_RANGE.clone()
306 }
307 (&MainModel::HighPoly, Some(MainModel::HighPoly))
308 | (&MainModel::LowPoly, Some(MainModel::LowPoly)) => {
309 SINGLE_MODEL_VISIBILITY_RANGE.clone()
310 }
311 (&MainModel::HighPoly, None) => NORMAL_VISIBILITY_RANGE_HIGH_POLY.clone(),
312 (&MainModel::LowPoly, None) => NORMAL_VISIBILITY_RANGE_LOW_POLY.clone(),
313 }
314 }
315 }
316
317 // Toggles the prepass if the user requests.
318 fn toggle_prepass(
319 mut commands: Commands,
320 cameras: Query<Entity, With<Camera3d>>,
321 keyboard_input: Res<ButtonInput<KeyCode>>,
322 mut app_status: ResMut<AppStatus>,
323 ) {
324 if !keyboard_input.just_pressed(KeyCode::Space) {
325 return;
326 }
327
328 app_status.prepass = !app_status.prepass;
329
330 for camera in cameras.iter() {
331 if app_status.prepass {
332 commands
333 .entity(camera)
334 .insert(DepthPrepass)
335 .insert(NormalPrepass);
336 } else {
337 commands
338 .entity(camera)
339 .remove::<DepthPrepass>()
340 .remove::<NormalPrepass>();
341 }
342 }
343 }
344
345 // A system that updates the help text.
346 fn update_help_text(mut text_query: Query<&mut Text>, app_status: Res<AppStatus>) {
347 for mut text in text_query.iter_mut() {
348 *text = app_status.create_text();
349 }
350 }
351
352 impl AppStatus {
353 // Creates and returns help text reflecting the app status.
354 fn create_text(&self) -> Text {
355 format!(
356 "\
357 {} (1) Switch from high-poly to low-poly based on camera distance
358 {} (2) Show only the high-poly model
359 {} (3) Show only the low-poly model
360 Press 1, 2, or 3 to switch which model is shown
361 Press WASD or use the mouse wheel to move the camera
362 Press Space to {} the prepass",
363 if self.show_one_model_only.is_none() {
364 '>'
365 } else {
366 ' '
367 },
368 if self.show_one_model_only == Some(MainModel::HighPoly) {
369 '>'
370 } else {
371 ' '
372 },
373 if self.show_one_model_only == Some(MainModel::LowPoly) {
374 '>'
375 } else {
376 ' '
377 },
378 if self.prepass { "disable" } else { "enable" }
379 )
380 .into()
381 }
382 }