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