Path: blob/main/crates/bevy_camera/src/visibility/range.rs
6599 views
//! Specific distances from the camera in which entities are visible, also known1//! as *hierarchical levels of detail* or *HLOD*s.23use core::{4hash::{Hash, Hasher},5ops::Range,6};78use bevy_app::{App, Plugin, PostUpdate};9use bevy_ecs::{10component::Component,11entity::{Entity, EntityHashMap},12query::With,13reflect::ReflectComponent,14resource::Resource,15schedule::IntoScheduleConfigs as _,16system::{Local, Query, ResMut},17};18use bevy_math::FloatOrd;19use bevy_reflect::Reflect;20use bevy_transform::components::GlobalTransform;21use bevy_utils::Parallel;2223use super::{check_visibility, VisibilitySystems};24use crate::{camera::Camera, primitives::Aabb};2526/// A plugin that enables [`VisibilityRange`]s, which allow entities to be27/// hidden or shown based on distance to the camera.28pub struct VisibilityRangePlugin;2930impl Plugin for VisibilityRangePlugin {31fn build(&self, app: &mut App) {32app.init_resource::<VisibleEntityRanges>().add_systems(33PostUpdate,34check_visibility_ranges35.in_set(VisibilitySystems::CheckVisibility)36.before(check_visibility),37);38}39}4041/// Specifies the range of distances that this entity must be from the camera in42/// order to be rendered.43///44/// This is also known as *hierarchical level of detail* or *HLOD*.45///46/// Use this component when you want to render a high-polygon mesh when the47/// camera is close and a lower-polygon mesh when the camera is far away. This48/// is a common technique for improving performance, because fine details are49/// hard to see in a mesh at a distance. To avoid an artifact known as *popping*50/// between levels, each level has a *margin*, within which the object51/// transitions gradually from invisible to visible using a dithering effect.52///53/// You can also use this feature to replace multiple meshes with a single mesh54/// when the camera is distant. This is the reason for the term "*hierarchical*55/// level of detail". Reducing the number of meshes can be useful for reducing56/// drawcall count. Note that you must place the [`VisibilityRange`] component57/// on each entity you want to be part of a LOD group, as [`VisibilityRange`]58/// isn't automatically propagated down to children.59///60/// A typical use of this feature might look like this:61///62/// | Entity | `start_margin` | `end_margin` |63/// |-------------------------|----------------|--------------|64/// | Root | N/A | N/A |65/// | ├─ High-poly mesh | [0, 0) | [20, 25) |66/// | ├─ Low-poly mesh | [20, 25) | [70, 75) |67/// | └─ Billboard *imposter* | [70, 75) | [150, 160) |68///69/// With this setup, the user will see a high-poly mesh when the camera is70/// closer than 20 units. As the camera zooms out, between 20 units to 25 units,71/// the high-poly mesh will gradually fade to a low-poly mesh. When the camera72/// is 70 to 75 units away, the low-poly mesh will fade to a single textured73/// quad. And between 150 and 160 units, the object fades away entirely. Note74/// that the `end_margin` of a higher LOD is always identical to the75/// `start_margin` of the next lower LOD; this is important for the crossfade76/// effect to function properly.77#[derive(Component, Clone, PartialEq, Default, Reflect)]78#[reflect(Component, PartialEq, Hash, Clone)]79pub struct VisibilityRange {80/// The range of distances, in world units, between which this entity will81/// smoothly fade into view as the camera zooms out.82///83/// If the start and end of this range are identical, the transition will be84/// abrupt, with no crossfading.85///86/// `start_margin.end` must be less than or equal to `end_margin.start`.87pub start_margin: Range<f32>,8889/// The range of distances, in world units, between which this entity will90/// smoothly fade out of view as the camera zooms out.91///92/// If the start and end of this range are identical, the transition will be93/// abrupt, with no crossfading.94///95/// `end_margin.start` must be greater than or equal to `start_margin.end`.96pub end_margin: Range<f32>,9798/// If set to true, Bevy will use the center of the axis-aligned bounding99/// box ([`Aabb`]) as the position of the mesh for the purposes of100/// visibility range computation.101///102/// Otherwise, if this field is set to false, Bevy will use the origin of103/// the mesh as the mesh's position.104///105/// Usually you will want to leave this set to false, because different LODs106/// may have different AABBs, and smooth crossfades between LOD levels107/// require that all LODs of a mesh be at *precisely* the same position. If108/// you aren't using crossfading, however, and your meshes aren't centered109/// around their origins, then this flag may be useful.110pub use_aabb: bool,111}112113impl Eq for VisibilityRange {}114115impl Hash for VisibilityRange {116fn hash<H>(&self, state: &mut H)117where118H: Hasher,119{120FloatOrd(self.start_margin.start).hash(state);121FloatOrd(self.start_margin.end).hash(state);122FloatOrd(self.end_margin.start).hash(state);123FloatOrd(self.end_margin.end).hash(state);124}125}126127impl VisibilityRange {128/// Creates a new *abrupt* visibility range, with no crossfade.129///130/// There will be no crossfade; the object will immediately vanish if the131/// camera is closer than `start` units or farther than `end` units from the132/// model.133///134/// The `start` value must be less than or equal to the `end` value.135#[inline]136pub fn abrupt(start: f32, end: f32) -> Self {137Self {138start_margin: start..start,139end_margin: end..end,140use_aabb: false,141}142}143144/// Returns true if both the start and end transitions for this range are145/// abrupt: that is, there is no crossfading.146#[inline]147pub fn is_abrupt(&self) -> bool {148self.start_margin.start == self.start_margin.end149&& self.end_margin.start == self.end_margin.end150}151152/// Returns true if the object will be visible at all, given a camera153/// `camera_distance` units away.154///155/// Any amount of visibility, even with the heaviest dithering applied, is156/// considered visible according to this check.157#[inline]158pub fn is_visible_at_all(&self, camera_distance: f32) -> bool {159camera_distance >= self.start_margin.start && camera_distance < self.end_margin.end160}161162/// Returns true if the object is completely invisible, given a camera163/// `camera_distance` units away.164///165/// This is equivalent to `!VisibilityRange::is_visible_at_all()`.166#[inline]167pub fn is_culled(&self, camera_distance: f32) -> bool {168!self.is_visible_at_all(camera_distance)169}170}171172/// Stores which entities are in within the [`VisibilityRange`]s of views.173///174/// This doesn't store the results of frustum or occlusion culling; use175/// [`ViewVisibility`](`super::ViewVisibility`) for that. Thus entities in this list may not176/// actually be visible.177///178/// For efficiency, these tables only store entities that have179/// [`VisibilityRange`] components. Entities without such a component won't be180/// in these tables at all.181///182/// The table is indexed by entity and stores a 32-bit bitmask with one bit for183/// each camera, where a 0 bit corresponds to "out of range" and a 1 bit184/// corresponds to "in range". Hence it's limited to storing information for 32185/// views.186#[derive(Resource, Default)]187pub struct VisibleEntityRanges {188/// Stores which bit index each view corresponds to.189views: EntityHashMap<u8>,190191/// Stores a bitmask in which each view has a single bit.192///193/// A 0 bit for a view corresponds to "out of range"; a 1 bit corresponds to194/// "in range".195entities: EntityHashMap<u32>,196}197198impl VisibleEntityRanges {199/// Clears out the [`VisibleEntityRanges`] in preparation for a new frame.200fn clear(&mut self) {201self.views.clear();202self.entities.clear();203}204205/// Returns true if the entity is in range of the given camera.206///207/// This only checks [`VisibilityRange`]s and doesn't perform any frustum or208/// occlusion culling. Thus the entity might not *actually* be visible.209///210/// The entity is assumed to have a [`VisibilityRange`] component. If the211/// entity doesn't have that component, this method will return false.212#[inline]213pub fn entity_is_in_range_of_view(&self, entity: Entity, view: Entity) -> bool {214let Some(visibility_bitmask) = self.entities.get(&entity) else {215return false;216};217let Some(view_index) = self.views.get(&view) else {218return false;219};220(visibility_bitmask & (1 << view_index)) != 0221}222223/// Returns true if the entity is in range of any view.224///225/// This only checks [`VisibilityRange`]s and doesn't perform any frustum or226/// occlusion culling. Thus the entity might not *actually* be visible.227///228/// The entity is assumed to have a [`VisibilityRange`] component. If the229/// entity doesn't have that component, this method will return false.230#[inline]231pub fn entity_is_in_range_of_any_view(&self, entity: Entity) -> bool {232self.entities.contains_key(&entity)233}234}235236/// Checks all entities against all views in order to determine which entities237/// with [`VisibilityRange`]s are potentially visible.238///239/// This only checks distance from the camera and doesn't frustum or occlusion240/// cull.241pub fn check_visibility_ranges(242mut visible_entity_ranges: ResMut<VisibleEntityRanges>,243view_query: Query<(Entity, &GlobalTransform), With<Camera>>,244mut par_local: Local<Parallel<Vec<(Entity, u32)>>>,245entity_query: Query<(Entity, &GlobalTransform, Option<&Aabb>, &VisibilityRange)>,246) {247visible_entity_ranges.clear();248249// Early out if the visibility range feature isn't in use.250if entity_query.is_empty() {251return;252}253254// Assign an index to each view.255let mut views = vec![];256for (view, view_transform) in view_query.iter().take(32) {257let view_index = views.len() as u8;258visible_entity_ranges.views.insert(view, view_index);259views.push((view, view_transform.translation_vec3a()));260}261262// Check each entity/view pair. Only consider entities with263// [`VisibilityRange`] components.264entity_query.par_iter().for_each(265|(entity, entity_transform, maybe_model_aabb, visibility_range)| {266let mut visibility = 0;267for (view_index, &(_, view_position)) in views.iter().enumerate() {268// If instructed to use the AABB and the model has one, use its269// center as the model position. Otherwise, use the model's270// translation.271let model_position = match (visibility_range.use_aabb, maybe_model_aabb) {272(true, Some(model_aabb)) => entity_transform273.affine()274.transform_point3a(model_aabb.center),275_ => entity_transform.translation_vec3a(),276};277278if visibility_range.is_visible_at_all((view_position - model_position).length()) {279visibility |= 1 << view_index;280}281}282283// Invisible entities have no entry at all in the hash map. This speeds284// up checks slightly in this common case.285if visibility != 0 {286par_local.borrow_local_mut().push((entity, visibility));287}288},289);290291visible_entity_ranges.entities.extend(par_local.drain());292}293294295