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