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}