visibility_range/
visibility_range.rs

1//! Demonstrates visibility ranges, also known as HLODs.
2
3use 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
14// Where the camera is focused.
15const CAMERA_FOCAL_POINT: Vec3 = vec3(0.0, 0.3, 0.0);
16// Speed in units per frame.
17const CAMERA_KEYBOARD_ZOOM_SPEED: f32 = 0.05;
18// Speed in radians per frame.
19const CAMERA_KEYBOARD_PAN_SPEED: f32 = 0.01;
20// Speed in units per frame.
21const CAMERA_MOUSE_MOVEMENT_SPEED: f32 = 0.25;
22// The minimum distance that the camera is allowed to be from the model.
23const MIN_ZOOM_DISTANCE: f32 = 0.5;
24
25// The visibility ranges for high-poly and low-poly models respectively, when
26// both models are being shown.
27static 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
38// A visibility model that we use to always show a model (until the camera is so
39// far zoomed out that it's culled entirely).
40static 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
46// A visibility range that we use to completely hide a model.
47static 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// Allows us to identify the main model.
54#[derive(Component, Debug, Clone, Copy, PartialEq)]
55enum MainModel {
56    // The high-poly version.
57    HighPoly,
58    // The low-poly version.
59    LowPoly,
60}
61
62// The current mode.
63#[derive(Default, Resource)]
64struct AppStatus {
65    // Whether to show only one model.
66    show_one_model_only: Option<MainModel>,
67    // Whether to enable the prepass.
68    prepass: bool,
69}
70
71// Sets up the app.
72fn 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
96// Set up a simple 3D scene. Load the two meshes.
97fn 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    // Spawn a plane.
105    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    // Spawn the two HLODs.
111
112    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    // Spawn a light.
131    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    // Spawn a camera.
147    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    // Create the text.
160    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
171// We need to add the `VisibilityRange` components manually, as glTF currently
172// has no way to specify visibility ranges. This system watches for new meshes,
173// determines which `Scene` they're under, and adds the `VisibilityRange`
174// component as appropriate.
175fn set_visibility_ranges(
176    mut commands: Commands,
177    mut new_meshes: Query<Entity, Added<Mesh3d>>,
178    children: Query<(Option<&ChildOf>, Option<&MainModel>)>,
179) {
180    // Loop over each newly-added mesh.
181    for new_mesh in new_meshes.iter_mut() {
182        // Search for the nearest ancestor `MainModel` component.
183        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        // Add the `VisibilityRange` component.
196        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
214// Process the movement controls.
215fn 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    // Process zoom in and out via the keyboard.
223    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    // Process left and right pan via the keyboard.
230    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    // Process zoom in and out via the mouse wheel.
237    for mouse_wheel in mouse_wheel_reader.read() {
238        zoom_delta -= mouse_wheel.y * CAMERA_MOUSE_MOVEMENT_SPEED;
239    }
240
241    // Update the camera transform.
242    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
256// Toggles modes if the user requests.
257fn update_mode(
258    mut meshes: Query<(&mut VisibilityRange, &MainModel)>,
259    keyboard_input: Res<ButtonInput<KeyCode>>,
260    mut app_status: ResMut<AppStatus>,
261) {
262    // Toggle the mode as requested.
263    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    // Update the visibility ranges as appropriate.
279    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
295// Toggles the prepass if the user requests.
296fn 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
323// A system that updates the help text.
324fn 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    // Creates and returns help text reflecting the app status.
332    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}