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}