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}