1use std::f32::consts::PI;
4
5use bevy::{
6 camera::visibility::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
14const CAMERA_FOCAL_POINT: Vec3 = vec3(0.0, 0.3, 0.0);
16const CAMERA_KEYBOARD_ZOOM_SPEED: f32 = 0.05;
18const CAMERA_KEYBOARD_PAN_SPEED: f32 = 0.01;
20const CAMERA_MOUSE_MOVEMENT_SPEED: f32 = 0.25;
22const MIN_ZOOM_DISTANCE: f32 = 0.5;
24
25static NORMAL_VISIBILITY_RANGE_HIGH_POLY: VisibilityRange = VisibilityRange {
28 start_margin: 0.0..0.0,
29 end_margin: 3.0..4.0,
30 use_aabb: false,
31};
32static NORMAL_VISIBILITY_RANGE_LOW_POLY: VisibilityRange = VisibilityRange {
33 start_margin: 3.0..4.0,
34 end_margin: 8.0..9.0,
35 use_aabb: false,
36};
37
38static SINGLE_MODEL_VISIBILITY_RANGE: VisibilityRange = VisibilityRange {
41 start_margin: 0.0..0.0,
42 end_margin: 8.0..9.0,
43 use_aabb: false,
44};
45
46static INVISIBLE_VISIBILITY_RANGE: VisibilityRange = VisibilityRange {
48 start_margin: 0.0..0.0,
49 end_margin: 0.0..0.0,
50 use_aabb: false,
51};
52
53#[derive(Component, Debug, Clone, Copy, PartialEq)]
55enum MainModel {
56 HighPoly,
58 LowPoly,
60}
61
62#[derive(Default, Resource)]
64struct AppStatus {
65 show_one_model_only: Option<MainModel>,
67 prepass: bool,
69}
70
71fn main() {
73 App::new()
74 .add_plugins(DefaultPlugins.set(WindowPlugin {
75 primary_window: Some(Window {
76 title: "Bevy Visibility Range Example".into(),
77 ..default()
78 }),
79 ..default()
80 }))
81 .init_resource::<AppStatus>()
82 .add_systems(Startup, setup)
83 .add_systems(
84 Update,
85 (
86 move_camera,
87 set_visibility_ranges,
88 update_help_text,
89 update_mode,
90 toggle_prepass,
91 ),
92 )
93 .run();
94}
95
96fn setup(
98 mut commands: Commands,
99 mut meshes: ResMut<Assets<Mesh>>,
100 mut materials: ResMut<Assets<StandardMaterial>>,
101 asset_server: Res<AssetServer>,
102 app_status: Res<AppStatus>,
103) {
104 commands.spawn((
106 Mesh3d(meshes.add(Plane3d::default().mesh().size(50.0, 50.0))),
107 MeshMaterial3d(materials.add(Color::srgb(0.1, 0.2, 0.1))),
108 ));
109
110 commands.spawn((
113 SceneRoot(
114 asset_server
115 .load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")),
116 ),
117 MainModel::HighPoly,
118 ));
119
120 commands.spawn((
121 SceneRoot(
122 asset_server.load(
123 GltfAssetLabel::Scene(0)
124 .from_asset("models/FlightHelmetLowPoly/FlightHelmetLowPoly.gltf"),
125 ),
126 ),
127 MainModel::LowPoly,
128 ));
129
130 commands.spawn((
132 DirectionalLight {
133 illuminance: FULL_DAYLIGHT,
134 shadows_enabled: true,
135 ..default()
136 },
137 Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, PI * -0.15, PI * -0.15)),
138 CascadeShadowConfigBuilder {
139 maximum_distance: 30.0,
140 first_cascade_far_bound: 0.9,
141 ..default()
142 }
143 .build(),
144 ));
145
146 commands
148 .spawn((
149 Camera3d::default(),
150 Transform::from_xyz(0.7, 0.7, 1.0).looking_at(CAMERA_FOCAL_POINT, Vec3::Y),
151 ))
152 .insert(EnvironmentMapLight {
153 diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
154 specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
155 intensity: 150.0,
156 ..default()
157 });
158
159 commands.spawn((
161 app_status.create_text(),
162 Node {
163 position_type: PositionType::Absolute,
164 bottom: px(12),
165 left: px(12),
166 ..default()
167 },
168 ));
169}
170
171fn set_visibility_ranges(
176 mut commands: Commands,
177 mut new_meshes: Query<Entity, Added<Mesh3d>>,
178 children: Query<(Option<&ChildOf>, Option<&MainModel>)>,
179) {
180 for new_mesh in new_meshes.iter_mut() {
182 let (mut current, mut main_model) = (new_mesh, None);
184 while let Ok((child_of, maybe_main_model)) = children.get(current) {
185 if let Some(model) = maybe_main_model {
186 main_model = Some(model);
187 break;
188 }
189 match child_of {
190 Some(child_of) => current = child_of.parent(),
191 None => break,
192 }
193 }
194
195 match main_model {
197 Some(MainModel::HighPoly) => {
198 commands
199 .entity(new_mesh)
200 .insert(NORMAL_VISIBILITY_RANGE_HIGH_POLY.clone())
201 .insert(MainModel::HighPoly);
202 }
203 Some(MainModel::LowPoly) => {
204 commands
205 .entity(new_mesh)
206 .insert(NORMAL_VISIBILITY_RANGE_LOW_POLY.clone())
207 .insert(MainModel::LowPoly);
208 }
209 None => {}
210 }
211 }
212}
213
214fn move_camera(
216 keyboard_input: Res<ButtonInput<KeyCode>>,
217 mut mouse_wheel_reader: MessageReader<MouseWheel>,
218 mut cameras: Query<&mut Transform, With<Camera3d>>,
219) {
220 let (mut zoom_delta, mut theta_delta) = (0.0, 0.0);
221
222 if keyboard_input.pressed(KeyCode::KeyW) || keyboard_input.pressed(KeyCode::ArrowUp) {
224 zoom_delta -= CAMERA_KEYBOARD_ZOOM_SPEED;
225 } else if keyboard_input.pressed(KeyCode::KeyS) || keyboard_input.pressed(KeyCode::ArrowDown) {
226 zoom_delta += CAMERA_KEYBOARD_ZOOM_SPEED;
227 }
228
229 if keyboard_input.pressed(KeyCode::KeyA) || keyboard_input.pressed(KeyCode::ArrowLeft) {
231 theta_delta -= CAMERA_KEYBOARD_PAN_SPEED;
232 } else if keyboard_input.pressed(KeyCode::KeyD) || keyboard_input.pressed(KeyCode::ArrowRight) {
233 theta_delta += CAMERA_KEYBOARD_PAN_SPEED;
234 }
235
236 for mouse_wheel in mouse_wheel_reader.read() {
238 zoom_delta -= mouse_wheel.y * CAMERA_MOUSE_MOVEMENT_SPEED;
239 }
240
241 for transform in cameras.iter_mut() {
243 let transform = transform.into_inner();
244
245 let direction = transform.translation.normalize_or_zero();
246 let magnitude = transform.translation.length();
247
248 let new_direction = Mat3::from_rotation_y(theta_delta) * direction;
249 let new_magnitude = (magnitude + zoom_delta).max(MIN_ZOOM_DISTANCE);
250
251 transform.translation = new_direction * new_magnitude;
252 transform.look_at(CAMERA_FOCAL_POINT, Vec3::Y);
253 }
254}
255
256fn update_mode(
258 mut meshes: Query<(&mut VisibilityRange, &MainModel)>,
259 keyboard_input: Res<ButtonInput<KeyCode>>,
260 mut app_status: ResMut<AppStatus>,
261) {
262 if keyboard_input.just_pressed(KeyCode::Digit1) || keyboard_input.just_pressed(KeyCode::Numpad1)
264 {
265 app_status.show_one_model_only = None;
266 } else if keyboard_input.just_pressed(KeyCode::Digit2)
267 || keyboard_input.just_pressed(KeyCode::Numpad2)
268 {
269 app_status.show_one_model_only = Some(MainModel::HighPoly);
270 } else if keyboard_input.just_pressed(KeyCode::Digit3)
271 || keyboard_input.just_pressed(KeyCode::Numpad3)
272 {
273 app_status.show_one_model_only = Some(MainModel::LowPoly);
274 } else {
275 return;
276 }
277
278 for (mut visibility_range, main_model) in meshes.iter_mut() {
280 *visibility_range = match (main_model, app_status.show_one_model_only) {
281 (&MainModel::HighPoly, Some(MainModel::LowPoly))
282 | (&MainModel::LowPoly, Some(MainModel::HighPoly)) => {
283 INVISIBLE_VISIBILITY_RANGE.clone()
284 }
285 (&MainModel::HighPoly, Some(MainModel::HighPoly))
286 | (&MainModel::LowPoly, Some(MainModel::LowPoly)) => {
287 SINGLE_MODEL_VISIBILITY_RANGE.clone()
288 }
289 (&MainModel::HighPoly, None) => NORMAL_VISIBILITY_RANGE_HIGH_POLY.clone(),
290 (&MainModel::LowPoly, None) => NORMAL_VISIBILITY_RANGE_LOW_POLY.clone(),
291 }
292 }
293}
294
295fn toggle_prepass(
297 mut commands: Commands,
298 cameras: Query<Entity, With<Camera3d>>,
299 keyboard_input: Res<ButtonInput<KeyCode>>,
300 mut app_status: ResMut<AppStatus>,
301) {
302 if !keyboard_input.just_pressed(KeyCode::Space) {
303 return;
304 }
305
306 app_status.prepass = !app_status.prepass;
307
308 for camera in cameras.iter() {
309 if app_status.prepass {
310 commands
311 .entity(camera)
312 .insert(DepthPrepass)
313 .insert(NormalPrepass);
314 } else {
315 commands
316 .entity(camera)
317 .remove::<DepthPrepass>()
318 .remove::<NormalPrepass>();
319 }
320 }
321}
322
323fn update_help_text(mut text_query: Query<&mut Text>, app_status: Res<AppStatus>) {
325 for mut text in text_query.iter_mut() {
326 *text = app_status.create_text();
327 }
328}
329
330impl AppStatus {
331 fn create_text(&self) -> Text {
333 format!(
334 "\
335{} (1) Switch from high-poly to low-poly based on camera distance
336{} (2) Show only the high-poly model
337{} (3) Show only the low-poly model
338Press 1, 2, or 3 to switch which model is shown
339Press WASD or use the mouse wheel to move the camera
340Press Space to {} the prepass",
341 if self.show_one_model_only.is_none() {
342 '>'
343 } else {
344 ' '
345 },
346 if self.show_one_model_only == Some(MainModel::HighPoly) {
347 '>'
348 } else {
349 ' '
350 },
351 if self.show_one_model_only == Some(MainModel::LowPoly) {
352 '>'
353 } else {
354 ' '
355 },
356 if self.prepass { "disable" } else { "enable" }
357 )
358 .into()
359 }
360}