bevy_render/view/visibility/range.rs
1//! Specific distances from the camera in which entities are visible, also known
2//! as *hierarchical levels of detail* or *HLOD*s.
3
4use std::{
5 hash::{Hash, Hasher},
6 ops::Range,
7};
8
9use bevy_app::{App, Plugin, PostUpdate};
10use bevy_ecs::{
11 component::Component,
12 entity::Entity,
13 query::{Changed, With},
14 schedule::IntoSystemConfigs as _,
15 system::{Query, Res, ResMut, Resource},
16};
17use bevy_math::{vec4, FloatOrd, Vec4};
18use bevy_reflect::Reflect;
19use bevy_transform::components::GlobalTransform;
20use bevy_utils::{prelude::default, EntityHashMap, HashMap};
21use nonmax::NonMaxU16;
22use wgpu::{BufferBindingType, BufferUsages};
23
24use crate::{
25 camera::Camera,
26 render_resource::BufferVec,
27 renderer::{RenderDevice, RenderQueue},
28 Extract, ExtractSchedule, Render, RenderApp, RenderSet,
29};
30
31use super::{check_visibility, VisibilitySystems, WithMesh};
32
33/// We need at least 4 storage buffer bindings available to enable the
34/// visibility range buffer.
35///
36/// Even though we only use one storage buffer, the first 3 available storage
37/// buffers will go to various light-related buffers. We will grab the fourth
38/// buffer slot.
39pub const VISIBILITY_RANGES_STORAGE_BUFFER_COUNT: u32 = 4;
40
41/// The size of the visibility ranges buffer in elements (not bytes) when fewer
42/// than 6 storage buffers are available and we're forced to use a uniform
43/// buffer instead (most notably, on WebGL 2).
44const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: usize = 64;
45
46/// A plugin that enables [`VisibilityRange`]s, which allow entities to be
47/// hidden or shown based on distance to the camera.
48pub struct VisibilityRangePlugin;
49
50impl Plugin for VisibilityRangePlugin {
51 fn build(&self, app: &mut App) {
52 app.register_type::<VisibilityRange>()
53 .init_resource::<VisibleEntityRanges>()
54 .add_systems(
55 PostUpdate,
56 check_visibility_ranges
57 .in_set(VisibilitySystems::CheckVisibility)
58 .before(check_visibility::<WithMesh>),
59 );
60
61 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
62 return;
63 };
64
65 render_app
66 .init_resource::<RenderVisibilityRanges>()
67 .add_systems(ExtractSchedule, extract_visibility_ranges)
68 .add_systems(
69 Render,
70 write_render_visibility_ranges.in_set(RenderSet::PrepareResourcesFlush),
71 );
72 }
73}
74
75/// Specifies the range of distances that this entity must be from the camera in
76/// order to be rendered.
77///
78/// This is also known as *hierarchical level of detail* or *HLOD*.
79///
80/// Use this component when you want to render a high-polygon mesh when the
81/// camera is close and a lower-polygon mesh when the camera is far away. This
82/// is a common technique for improving performance, because fine details are
83/// hard to see in a mesh at a distance. To avoid an artifact known as *popping*
84/// between levels, each level has a *margin*, within which the object
85/// transitions gradually from invisible to visible using a dithering effect.
86///
87/// You can also use this feature to replace multiple meshes with a single mesh
88/// when the camera is distant. This is the reason for the term "*hierarchical*
89/// level of detail". Reducing the number of meshes can be useful for reducing
90/// drawcall count. Note that you must place the [`VisibilityRange`] component
91/// on each entity you want to be part of a LOD group, as [`VisibilityRange`]
92/// isn't automatically propagated down to children.
93///
94/// A typical use of this feature might look like this:
95///
96/// | Entity | `start_margin` | `end_margin` |
97/// |-------------------------|----------------|--------------|
98/// | Root | N/A | N/A |
99/// | ├─ High-poly mesh | [0, 0) | [20, 25) |
100/// | ├─ Low-poly mesh | [20, 25) | [70, 75) |
101/// | └─ Billboard *imposter* | [70, 75) | [150, 160) |
102///
103/// With this setup, the user will see a high-poly mesh when the camera is
104/// closer than 20 units. As the camera zooms out, between 20 units to 25 units,
105/// the high-poly mesh will gradually fade to a low-poly mesh. When the camera
106/// is 70 to 75 units away, the low-poly mesh will fade to a single textured
107/// quad. And between 150 and 160 units, the object fades away entirely. Note
108/// that the `end_margin` of a higher LOD is always identical to the
109/// `start_margin` of the next lower LOD; this is important for the crossfade
110/// effect to function properly.
111#[derive(Component, Clone, PartialEq, Reflect)]
112pub struct VisibilityRange {
113 /// The range of distances, in world units, between which this entity will
114 /// smoothly fade into view as the camera zooms out.
115 ///
116 /// If the start and end of this range are identical, the transition will be
117 /// abrupt, with no crossfading.
118 ///
119 /// `start_margin.end` must be less than or equal to `end_margin.start`.
120 pub start_margin: Range<f32>,
121
122 /// The range of distances, in world units, between which this entity will
123 /// smoothly fade out of view as the camera zooms out.
124 ///
125 /// If the start and end of this range are identical, the transition will be
126 /// abrupt, with no crossfading.
127 ///
128 /// `end_margin.start` must be greater than or equal to `start_margin.end`.
129 pub end_margin: Range<f32>,
130}
131
132impl Eq for VisibilityRange {}
133
134impl Hash for VisibilityRange {
135 fn hash<H>(&self, state: &mut H)
136 where
137 H: Hasher,
138 {
139 FloatOrd(self.start_margin.start).hash(state);
140 FloatOrd(self.start_margin.end).hash(state);
141 FloatOrd(self.end_margin.start).hash(state);
142 FloatOrd(self.end_margin.end).hash(state);
143 }
144}
145
146impl VisibilityRange {
147 /// Creates a new *abrupt* visibility range, with no crossfade.
148 ///
149 /// There will be no crossfade; the object will immediately vanish if the
150 /// camera is closer than `start` units or farther than `end` units from the
151 /// model.
152 ///
153 /// The `start` value must be less than or equal to the `end` value.
154 #[inline]
155 pub fn abrupt(start: f32, end: f32) -> Self {
156 Self {
157 start_margin: start..start,
158 end_margin: end..end,
159 }
160 }
161
162 /// Returns true if both the start and end transitions for this range are
163 /// abrupt: that is, there is no crossfading.
164 #[inline]
165 pub fn is_abrupt(&self) -> bool {
166 self.start_margin.start == self.start_margin.end
167 && self.end_margin.start == self.end_margin.end
168 }
169
170 /// Returns true if the object will be visible at all, given a camera
171 /// `camera_distance` units away.
172 ///
173 /// Any amount of visibility, even with the heaviest dithering applied, is
174 /// considered visible according to this check.
175 #[inline]
176 pub fn is_visible_at_all(&self, camera_distance: f32) -> bool {
177 camera_distance >= self.start_margin.start && camera_distance < self.end_margin.end
178 }
179
180 /// Returns true if the object is completely invisible, given a camera
181 /// `camera_distance` units away.
182 ///
183 /// This is equivalent to `!VisibilityRange::is_visible_at_all()`.
184 #[inline]
185 pub fn is_culled(&self, camera_distance: f32) -> bool {
186 !self.is_visible_at_all(camera_distance)
187 }
188}
189
190/// Stores information related to [`VisibilityRange`]s in the render world.
191#[derive(Resource)]
192pub struct RenderVisibilityRanges {
193 /// Information corresponding to each entity.
194 entities: EntityHashMap<Entity, RenderVisibilityEntityInfo>,
195
196 /// Maps a [`VisibilityRange`] to its index within the `buffer`.
197 ///
198 /// This map allows us to deduplicate identical visibility ranges, which
199 /// saves GPU memory.
200 range_to_index: HashMap<VisibilityRange, NonMaxU16>,
201
202 /// The GPU buffer that stores [`VisibilityRange`]s.
203 ///
204 /// Each [`Vec4`] contains the start margin start, start margin end, end
205 /// margin start, and end margin end distances, in that order.
206 buffer: BufferVec<Vec4>,
207
208 /// True if the buffer has been changed since the last frame and needs to be
209 /// reuploaded to the GPU.
210 buffer_dirty: bool,
211}
212
213/// Per-entity information related to [`VisibilityRange`]s.
214struct RenderVisibilityEntityInfo {
215 /// The index of the range within the GPU buffer.
216 buffer_index: NonMaxU16,
217 /// True if the range is abrupt: i.e. has no crossfade.
218 is_abrupt: bool,
219}
220
221impl Default for RenderVisibilityRanges {
222 fn default() -> Self {
223 Self {
224 entities: default(),
225 range_to_index: default(),
226 buffer: BufferVec::new(
227 BufferUsages::STORAGE | BufferUsages::UNIFORM | BufferUsages::VERTEX,
228 ),
229 buffer_dirty: true,
230 }
231 }
232}
233
234impl RenderVisibilityRanges {
235 /// Clears out the [`RenderVisibilityRanges`] in preparation for a new
236 /// frame.
237 fn clear(&mut self) {
238 self.entities.clear();
239 self.range_to_index.clear();
240 self.buffer.clear();
241 self.buffer_dirty = true;
242 }
243
244 /// Inserts a new entity into the [`RenderVisibilityRanges`].
245 fn insert(&mut self, entity: Entity, visibility_range: &VisibilityRange) {
246 // Grab a slot in the GPU buffer, or take the existing one if there
247 // already is one.
248 let buffer_index = *self
249 .range_to_index
250 .entry(visibility_range.clone())
251 .or_insert_with(|| {
252 NonMaxU16::try_from(self.buffer.push(vec4(
253 visibility_range.start_margin.start,
254 visibility_range.start_margin.end,
255 visibility_range.end_margin.start,
256 visibility_range.end_margin.end,
257 )) as u16)
258 .unwrap_or_default()
259 });
260
261 self.entities.insert(
262 entity,
263 RenderVisibilityEntityInfo {
264 buffer_index,
265 is_abrupt: visibility_range.is_abrupt(),
266 },
267 );
268 }
269
270 /// Returns the index in the GPU buffer corresponding to the visible range
271 /// for the given entity.
272 ///
273 /// If the entity has no visible range, returns `None`.
274 #[inline]
275 pub fn lod_index_for_entity(&self, entity: Entity) -> Option<NonMaxU16> {
276 self.entities.get(&entity).map(|info| info.buffer_index)
277 }
278
279 /// Returns true if the entity has a visibility range and it isn't abrupt:
280 /// i.e. if it has a crossfade.
281 #[inline]
282 pub fn entity_has_crossfading_visibility_ranges(&self, entity: Entity) -> bool {
283 self.entities
284 .get(&entity)
285 .is_some_and(|info| !info.is_abrupt)
286 }
287
288 /// Returns a reference to the GPU buffer that stores visibility ranges.
289 #[inline]
290 pub fn buffer(&self) -> &BufferVec<Vec4> {
291 &self.buffer
292 }
293}
294
295/// Stores which entities are in within the [`VisibilityRange`]s of views.
296///
297/// This doesn't store the results of frustum or occlusion culling; use
298/// [`super::ViewVisibility`] for that. Thus entities in this list may not
299/// actually be visible.
300///
301/// For efficiency, these tables only store entities that have
302/// [`VisibilityRange`] components. Entities without such a component won't be
303/// in these tables at all.
304///
305/// The table is indexed by entity and stores a 32-bit bitmask with one bit for
306/// each camera, where a 0 bit corresponds to "out of range" and a 1 bit
307/// corresponds to "in range". Hence it's limited to storing information for 32
308/// views.
309#[derive(Resource, Default)]
310pub struct VisibleEntityRanges {
311 /// Stores which bit index each view corresponds to.
312 views: EntityHashMap<Entity, u8>,
313
314 /// Stores a bitmask in which each view has a single bit.
315 ///
316 /// A 0 bit for a view corresponds to "out of range"; a 1 bit corresponds to
317 /// "in range".
318 entities: EntityHashMap<Entity, u32>,
319}
320
321impl VisibleEntityRanges {
322 /// Clears out the [`VisibleEntityRanges`] in preparation for a new frame.
323 fn clear(&mut self) {
324 self.views.clear();
325 self.entities.clear();
326 }
327
328 /// Returns true if the entity is in range of the given camera.
329 ///
330 /// This only checks [`VisibilityRange`]s and doesn't perform any frustum or
331 /// occlusion culling. Thus the entity might not *actually* be visible.
332 ///
333 /// The entity is assumed to have a [`VisibilityRange`] component. If the
334 /// entity doesn't have that component, this method will return false.
335 #[inline]
336 pub fn entity_is_in_range_of_view(&self, entity: Entity, view: Entity) -> bool {
337 let Some(visibility_bitmask) = self.entities.get(&entity) else {
338 return false;
339 };
340 let Some(view_index) = self.views.get(&view) else {
341 return false;
342 };
343 (visibility_bitmask & (1 << view_index)) != 0
344 }
345
346 /// Returns true if the entity is in range of any view.
347 ///
348 /// This only checks [`VisibilityRange`]s and doesn't perform any frustum or
349 /// occlusion culling. Thus the entity might not *actually* be visible.
350 ///
351 /// The entity is assumed to have a [`VisibilityRange`] component. If the
352 /// entity doesn't have that component, this method will return false.
353 #[inline]
354 pub fn entity_is_in_range_of_any_view(&self, entity: Entity) -> bool {
355 self.entities.contains_key(&entity)
356 }
357}
358
359/// Checks all entities against all views in order to determine which entities
360/// with [`VisibilityRange`]s are potentially visible.
361///
362/// This only checks distance from the camera and doesn't frustum or occlusion
363/// cull.
364pub fn check_visibility_ranges(
365 mut visible_entity_ranges: ResMut<VisibleEntityRanges>,
366 view_query: Query<(Entity, &GlobalTransform), With<Camera>>,
367 mut entity_query: Query<(Entity, &GlobalTransform, &VisibilityRange)>,
368) {
369 visible_entity_ranges.clear();
370
371 // Early out if the visibility range feature isn't in use.
372 if entity_query.is_empty() {
373 return;
374 }
375
376 // Assign an index to each view.
377 let mut views = vec![];
378 for (view, view_transform) in view_query.iter().take(32) {
379 let view_index = views.len() as u8;
380 visible_entity_ranges.views.insert(view, view_index);
381 views.push((view, view_transform.translation_vec3a()));
382 }
383
384 // Check each entity/view pair. Only consider entities with
385 // [`VisibilityRange`] components.
386 for (entity, entity_transform, visibility_range) in entity_query.iter_mut() {
387 let mut visibility = 0;
388 for (view_index, &(_, view_position)) in views.iter().enumerate() {
389 if visibility_range
390 .is_visible_at_all((view_position - entity_transform.translation_vec3a()).length())
391 {
392 visibility |= 1 << view_index;
393 }
394 }
395
396 // Invisible entities have no entry at all in the hash map. This speeds
397 // up checks slightly in this common case.
398 if visibility != 0 {
399 visible_entity_ranges.entities.insert(entity, visibility);
400 }
401 }
402}
403
404/// Extracts all [`VisibilityRange`] components from the main world to the
405/// render world and inserts them into [`RenderVisibilityRanges`].
406pub fn extract_visibility_ranges(
407 mut render_visibility_ranges: ResMut<RenderVisibilityRanges>,
408 visibility_ranges_query: Extract<Query<(Entity, &VisibilityRange)>>,
409 changed_ranges_query: Extract<Query<Entity, Changed<VisibilityRange>>>,
410) {
411 if changed_ranges_query.is_empty() {
412 return;
413 }
414
415 render_visibility_ranges.clear();
416 for (entity, visibility_range) in visibility_ranges_query.iter() {
417 render_visibility_ranges.insert(entity, visibility_range);
418 }
419}
420
421/// Writes the [`RenderVisibilityRanges`] table to the GPU.
422pub fn write_render_visibility_ranges(
423 render_device: Res<RenderDevice>,
424 render_queue: Res<RenderQueue>,
425 mut render_visibility_ranges: ResMut<RenderVisibilityRanges>,
426) {
427 // If there haven't been any changes, early out.
428 if !render_visibility_ranges.buffer_dirty {
429 return;
430 }
431
432 // Mess with the length of the buffer to meet API requirements if necessary.
433 match render_device.get_supported_read_only_binding_type(VISIBILITY_RANGES_STORAGE_BUFFER_COUNT)
434 {
435 // If we're using a uniform buffer, we must have *exactly*
436 // `VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE` elements.
437 BufferBindingType::Uniform
438 if render_visibility_ranges.buffer.len() > VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE =>
439 {
440 render_visibility_ranges
441 .buffer
442 .truncate(VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE);
443 }
444 BufferBindingType::Uniform
445 if render_visibility_ranges.buffer.len() < VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE =>
446 {
447 while render_visibility_ranges.buffer.len() < VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE {
448 render_visibility_ranges.buffer.push(default());
449 }
450 }
451
452 // Otherwise, if we're using a storage buffer, just ensure there's
453 // something in the buffer, or else it won't get allocated.
454 BufferBindingType::Storage { .. } if render_visibility_ranges.buffer.is_empty() => {
455 render_visibility_ranges.buffer.push(default());
456 }
457
458 _ => {}
459 }
460
461 // Schedule the write.
462 render_visibility_ranges
463 .buffer
464 .write_buffer(&render_device, &render_queue);
465 render_visibility_ranges.buffer_dirty = false;
466}