Merge branch 'main' into proper-json-schema
This commit is contained in:
commit
22eae74a38
44
crates/bevy_camera/Cargo.toml
Normal file
44
crates/bevy_camera/Cargo.toml
Normal file
@ -0,0 +1,44 @@
|
||||
[package]
|
||||
name = "bevy_camera"
|
||||
version = "0.17.0-dev"
|
||||
edition = "2024"
|
||||
description = "Provides a camera abstraction for Bevy Engine"
|
||||
homepage = "https://bevy.org"
|
||||
repository = "https://github.com/bevyengine/bevy"
|
||||
license = "MIT OR Apache-2.0"
|
||||
keywords = ["bevy"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_app = { path = "../bevy_app", version = "0.17.0-dev" }
|
||||
bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" }
|
||||
bevy_image = { path = "../bevy_image", version = "0.17.0-dev" }
|
||||
bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.17.0-dev" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" }
|
||||
bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" }
|
||||
bevy_color = { path = "../bevy_color", version = "0.17.0-dev", features = [
|
||||
"serialize",
|
||||
] }
|
||||
bevy_window = { path = "../bevy_window", version = "0.17.0-dev" }
|
||||
|
||||
# other
|
||||
wgpu-types = { version = "25", default-features = false }
|
||||
serde = { version = "1", default-features = false, features = ["derive"] }
|
||||
thiserror = { version = "2", default-features = false }
|
||||
downcast-rs = { version = "2", default-features = false, features = ["std"] }
|
||||
derive_more = { version = "2", default-features = false, features = ["from"] }
|
||||
smallvec = { version = "1.11", features = ["const_new"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"]
|
||||
all-features = true
|
||||
176
crates/bevy_camera/LICENSE-APACHE
Normal file
176
crates/bevy_camera/LICENSE-APACHE
Normal file
@ -0,0 +1,176 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
19
crates/bevy_camera/LICENSE-MIT
Normal file
19
crates/bevy_camera/LICENSE-MIT
Normal file
@ -0,0 +1,19 @@
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@ -1,53 +1,21 @@
|
||||
#![expect(
|
||||
clippy::module_inception,
|
||||
reason = "The parent module contains all things viewport-related, while this module handles cameras as a component. However, a rename/refactor which should clear up this lint is being discussed; see #17196."
|
||||
)]
|
||||
use super::{ClearColorConfig, Projection};
|
||||
use crate::{
|
||||
batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport},
|
||||
camera::{ManualTextureViewHandle, ManualTextureViews},
|
||||
primitives::Frustum,
|
||||
render_asset::RenderAssets,
|
||||
render_graph::{InternedRenderSubGraph, RenderSubGraph},
|
||||
render_resource::TextureView,
|
||||
sync_world::{RenderEntity, SyncToRenderWorld},
|
||||
texture::GpuImage,
|
||||
view::{
|
||||
ColorGrading, ExtractedView, ExtractedWindows, Hdr, Msaa, NoIndirectDrawing, RenderLayers,
|
||||
RenderVisibleEntities, RetainedViewEntity, ViewUniformOffset, Visibility, VisibleEntities,
|
||||
},
|
||||
Extract,
|
||||
};
|
||||
use bevy_asset::{AssetEvent, AssetId, Assets, Handle};
|
||||
use bevy_derive::{Deref, DerefMut};
|
||||
use bevy_ecs::{
|
||||
change_detection::DetectChanges,
|
||||
component::Component,
|
||||
entity::{ContainsEntity, Entity},
|
||||
event::EventReader,
|
||||
lifecycle::HookContext,
|
||||
prelude::With,
|
||||
query::Has,
|
||||
reflect::ReflectComponent,
|
||||
resource::Resource,
|
||||
system::{Commands, Query, Res, ResMut},
|
||||
world::DeferredWorld,
|
||||
use crate::primitives::Frustum;
|
||||
|
||||
use super::{
|
||||
visibility::{Visibility, VisibleEntities},
|
||||
ClearColorConfig,
|
||||
};
|
||||
use bevy_asset::Handle;
|
||||
use bevy_derive::Deref;
|
||||
use bevy_ecs::{component::Component, reflect::ReflectComponent};
|
||||
use bevy_image::Image;
|
||||
use bevy_math::{ops, vec2, Dir3, FloatOrd, Mat4, Ray3d, Rect, URect, UVec2, UVec4, Vec2, Vec3};
|
||||
use bevy_platform::collections::{HashMap, HashSet};
|
||||
use bevy_math::{ops, Dir3, FloatOrd, Mat4, Ray3d, Rect, URect, UVec2, Vec2, Vec3};
|
||||
use bevy_reflect::prelude::*;
|
||||
use bevy_render_macros::ExtractComponent;
|
||||
use bevy_transform::components::{GlobalTransform, Transform};
|
||||
use bevy_window::{
|
||||
NormalizedWindowRef, PrimaryWindow, Window, WindowCreated, WindowRef, WindowResized,
|
||||
WindowScaleFactorChanged,
|
||||
};
|
||||
use bevy_window::WindowRef;
|
||||
use core::ops::Range;
|
||||
use derive_more::derive::From;
|
||||
use thiserror::Error;
|
||||
use tracing::warn;
|
||||
use wgpu::{BlendState, TextureFormat, TextureUsages};
|
||||
use wgpu_types::{BlendState, TextureUsages};
|
||||
|
||||
/// Render viewport configuration for the [`Camera`] component.
|
||||
///
|
||||
@ -124,6 +92,19 @@ impl Viewport {
|
||||
}
|
||||
}
|
||||
|
||||
/// Override the resolution a 3d camera's main pass is rendered at.
|
||||
///
|
||||
/// Does not affect post processing.
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// * Insert this component on a 3d camera entity in the render world.
|
||||
/// * The resolution override must be smaller than the camera's viewport size.
|
||||
/// * The resolution override is specified in physical pixels.
|
||||
#[derive(Component, Reflect, Deref)]
|
||||
#[reflect(Component)]
|
||||
pub struct MainPassResolutionOverride(pub UVec2);
|
||||
|
||||
/// Settings to define a camera sub view.
|
||||
///
|
||||
/// When [`Camera::sub_camera_view`] is `Some`, only the sub-section of the
|
||||
@ -188,11 +169,11 @@ pub struct RenderTargetInfo {
|
||||
/// Holds internally computed [`Camera`] values.
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct ComputedCameraValues {
|
||||
clip_from_view: Mat4,
|
||||
target_info: Option<RenderTargetInfo>,
|
||||
pub clip_from_view: Mat4,
|
||||
pub target_info: Option<RenderTargetInfo>,
|
||||
// size of the `Viewport`
|
||||
old_viewport_size: Option<UVec2>,
|
||||
old_sub_camera_view: Option<SubCameraView>,
|
||||
pub old_viewport_size: Option<UVec2>,
|
||||
pub old_sub_camera_view: Option<SubCameraView>,
|
||||
}
|
||||
|
||||
/// How much energy a `Camera3d` absorbs from incoming light.
|
||||
@ -303,7 +284,7 @@ impl Default for PhysicalCameraParameters {
|
||||
pub enum ViewportConversionError {
|
||||
/// The pre-computed size of the viewport was not available.
|
||||
///
|
||||
/// This may be because the `Camera` was just created and [`camera_system`] has not been executed
|
||||
/// This may be because the `Camera` was just created and `camera_system` has not been executed
|
||||
/// yet, or because the [`RenderTarget`] is misconfigured in one of the following ways:
|
||||
/// - it references the [`PrimaryWindow`](RenderTarget::Window) when there is none,
|
||||
/// - it references a [`Window`](RenderTarget::Window) entity that doesn't exist or doesn't actually have a `Window` component,
|
||||
@ -322,8 +303,8 @@ pub enum ViewportConversionError {
|
||||
#[error("computed coordinate beyond `Camera`'s far plane")]
|
||||
PastFarPlane,
|
||||
/// The Normalized Device Coordinates could not be computed because the `camera_transform`, the
|
||||
/// `world_position`, or the projection matrix defined by [`Projection`] contained `NAN` (see
|
||||
/// [`world_to_ndc`][Camera::world_to_ndc] and [`ndc_to_world`][Camera::ndc_to_world]).
|
||||
/// `world_position`, or the projection matrix defined by [`Projection`](super::projection::Projection)
|
||||
/// contained `NAN` (see [`world_to_ndc`][Camera::world_to_ndc] and [`ndc_to_world`][Camera::ndc_to_world]).
|
||||
#[error("found NaN while computing NDC")]
|
||||
InvalidData,
|
||||
}
|
||||
@ -336,7 +317,7 @@ pub enum ViewportConversionError {
|
||||
/// to transform the 3D objects into a 2D image, as well as the render target into which that image
|
||||
/// is produced.
|
||||
///
|
||||
/// Note that a [`Camera`] needs a [`CameraRenderGraph`] to render anything.
|
||||
/// Note that a [`Camera`] needs a `CameraRenderGraph` to render anything.
|
||||
/// This is typically provided by adding a [`Camera2d`] or [`Camera3d`] component,
|
||||
/// but custom render graphs can also be defined. Inserting a [`Camera`] with no render
|
||||
/// graph will emit an error at runtime.
|
||||
@ -345,15 +326,12 @@ pub enum ViewportConversionError {
|
||||
/// [`Camera3d`]: https://docs.rs/bevy/latest/bevy/core_pipeline/core_3d/struct.Camera3d.html
|
||||
#[derive(Component, Debug, Reflect, Clone)]
|
||||
#[reflect(Component, Default, Debug, Clone)]
|
||||
#[component(on_add = warn_on_no_render_graph)]
|
||||
#[require(
|
||||
Frustum,
|
||||
CameraMainTextureUsages,
|
||||
VisibleEntities,
|
||||
Transform,
|
||||
Visibility,
|
||||
Msaa,
|
||||
SyncToRenderWorld
|
||||
Visibility
|
||||
)]
|
||||
pub struct Camera {
|
||||
/// If set, this camera will render to the given [`Viewport`] rectangle within the configured [`RenderTarget`].
|
||||
@ -383,12 +361,6 @@ pub struct Camera {
|
||||
pub sub_camera_view: Option<SubCameraView>,
|
||||
}
|
||||
|
||||
fn warn_on_no_render_graph(world: DeferredWorld, HookContext { entity, caller, .. }: HookContext) {
|
||||
if !world.entity(entity).contains::<CameraRenderGraph>() {
|
||||
warn!("{}Entity {entity} has a `Camera` component, but it doesn't have a render graph configured. Consider adding a `Camera2d` or `Camera3d` component, or manually adding a `CameraRenderGraph` component if you need a custom render graph.", caller.map(|location|format!("{location}: ")).unwrap_or_default());
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Camera {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@ -501,7 +473,7 @@ impl Camera {
|
||||
.map(|t: &RenderTargetInfo| t.scale_factor)
|
||||
}
|
||||
|
||||
/// The projection matrix computed using this camera's [`Projection`].
|
||||
/// The projection matrix computed using this camera's [`Projection`](super::projection::Projection).
|
||||
#[inline]
|
||||
pub fn clip_from_view(&self) -> Mat4 {
|
||||
self.computed.clip_from_view
|
||||
@ -666,7 +638,8 @@ impl Camera {
|
||||
/// To get the coordinates in the render target's viewport dimensions, you should use
|
||||
/// [`world_to_viewport`](Self::world_to_viewport).
|
||||
///
|
||||
/// Returns `None` if the `camera_transform`, the `world_position`, or the projection matrix defined by [`Projection`] contain `NAN`.
|
||||
/// Returns `None` if the `camera_transform`, the `world_position`, or the projection matrix defined by
|
||||
/// [`Projection`](super::projection::Projection) contain `NAN`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
@ -692,7 +665,8 @@ impl Camera {
|
||||
/// To get the world space coordinates with the viewport position, you should use
|
||||
/// [`world_to_viewport`](Self::world_to_viewport).
|
||||
///
|
||||
/// Returns `None` if the `camera_transform`, the `world_position`, or the projection matrix defined by [`Projection`] contain `NAN`.
|
||||
/// Returns `None` if the `camera_transform`, the `world_position`, or the projection matrix defined by
|
||||
/// [`Projection`](super::projection::Projection) contain `NAN`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
@ -754,27 +728,7 @@ impl Default for CameraOutputMode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Configures the [`RenderGraph`](crate::render_graph::RenderGraph) name assigned to be run for a given [`Camera`] entity.
|
||||
#[derive(Component, Debug, Deref, DerefMut, Reflect, Clone)]
|
||||
#[reflect(opaque)]
|
||||
#[reflect(Component, Debug, Clone)]
|
||||
pub struct CameraRenderGraph(InternedRenderSubGraph);
|
||||
|
||||
impl CameraRenderGraph {
|
||||
/// Creates a new [`CameraRenderGraph`] from any string-like type.
|
||||
#[inline]
|
||||
pub fn new<T: RenderSubGraph>(name: T) -> Self {
|
||||
Self(name.intern())
|
||||
}
|
||||
|
||||
/// Sets the graph name.
|
||||
#[inline]
|
||||
pub fn set<T: RenderSubGraph>(&mut self, name: T) {
|
||||
self.0 = name.intern();
|
||||
}
|
||||
}
|
||||
|
||||
/// The "target" that a [`Camera`] will render to. For example, this could be a [`Window`]
|
||||
/// The "target" that a [`Camera`] will render to. For example, this could be a `Window`
|
||||
/// swapchain or an [`Image`].
|
||||
#[derive(Debug, Clone, Reflect, From)]
|
||||
#[reflect(Clone)]
|
||||
@ -788,6 +742,23 @@ pub enum RenderTarget {
|
||||
TextureView(ManualTextureViewHandle),
|
||||
}
|
||||
|
||||
impl RenderTarget {
|
||||
/// Get a handle to the render target's image,
|
||||
/// or `None` if the render target is another variant.
|
||||
pub fn as_image(&self) -> Option<&Handle<Image>> {
|
||||
if let Self::Image(image_target) = self {
|
||||
Some(&image_target.handle)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A unique id that corresponds to a specific `ManualTextureView` in the `ManualTextureViews` collection.
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Component, Reflect)]
|
||||
#[reflect(Component, Default, Debug, PartialEq, Hash, Clone)]
|
||||
pub struct ManualTextureViewHandle(pub u32);
|
||||
|
||||
/// A render target that renders to an [`Image`].
|
||||
#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
#[reflect(Clone, PartialEq, Hash)]
|
||||
@ -820,254 +791,8 @@ impl Default for RenderTarget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalized version of the render target.
|
||||
///
|
||||
/// Once we have this we shouldn't need to resolve it down anymore.
|
||||
#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash, PartialOrd, Ord, From)]
|
||||
#[reflect(Clone, PartialEq, Hash)]
|
||||
pub enum NormalizedRenderTarget {
|
||||
/// Window to which the camera's view is rendered.
|
||||
Window(NormalizedWindowRef),
|
||||
/// Image to which the camera's view is rendered.
|
||||
Image(ImageRenderTarget),
|
||||
/// Texture View to which the camera's view is rendered.
|
||||
/// Useful when the texture view needs to be created outside of Bevy, for example OpenXR.
|
||||
TextureView(ManualTextureViewHandle),
|
||||
}
|
||||
|
||||
impl RenderTarget {
|
||||
/// Normalize the render target down to a more concrete value, mostly used for equality comparisons.
|
||||
pub fn normalize(&self, primary_window: Option<Entity>) -> Option<NormalizedRenderTarget> {
|
||||
match self {
|
||||
RenderTarget::Window(window_ref) => window_ref
|
||||
.normalize(primary_window)
|
||||
.map(NormalizedRenderTarget::Window),
|
||||
RenderTarget::Image(handle) => Some(NormalizedRenderTarget::Image(handle.clone())),
|
||||
RenderTarget::TextureView(id) => Some(NormalizedRenderTarget::TextureView(*id)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a handle to the render target's image,
|
||||
/// or `None` if the render target is another variant.
|
||||
pub fn as_image(&self) -> Option<&Handle<Image>> {
|
||||
if let Self::Image(image_target) = self {
|
||||
Some(&image_target.handle)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NormalizedRenderTarget {
|
||||
pub fn get_texture_view<'a>(
|
||||
&self,
|
||||
windows: &'a ExtractedWindows,
|
||||
images: &'a RenderAssets<GpuImage>,
|
||||
manual_texture_views: &'a ManualTextureViews,
|
||||
) -> Option<&'a TextureView> {
|
||||
match self {
|
||||
NormalizedRenderTarget::Window(window_ref) => windows
|
||||
.get(&window_ref.entity())
|
||||
.and_then(|window| window.swap_chain_texture_view.as_ref()),
|
||||
NormalizedRenderTarget::Image(image_target) => images
|
||||
.get(&image_target.handle)
|
||||
.map(|image| &image.texture_view),
|
||||
NormalizedRenderTarget::TextureView(id) => {
|
||||
manual_texture_views.get(id).map(|tex| &tex.texture_view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the [`TextureFormat`] of this render target, if it exists.
|
||||
pub fn get_texture_format<'a>(
|
||||
&self,
|
||||
windows: &'a ExtractedWindows,
|
||||
images: &'a RenderAssets<GpuImage>,
|
||||
manual_texture_views: &'a ManualTextureViews,
|
||||
) -> Option<TextureFormat> {
|
||||
match self {
|
||||
NormalizedRenderTarget::Window(window_ref) => windows
|
||||
.get(&window_ref.entity())
|
||||
.and_then(|window| window.swap_chain_texture_format),
|
||||
NormalizedRenderTarget::Image(image_target) => images
|
||||
.get(&image_target.handle)
|
||||
.map(|image| image.texture_format),
|
||||
NormalizedRenderTarget::TextureView(id) => {
|
||||
manual_texture_views.get(id).map(|tex| tex.format)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_render_target_info<'a>(
|
||||
&self,
|
||||
resolutions: impl IntoIterator<Item = (Entity, &'a Window)>,
|
||||
images: &Assets<Image>,
|
||||
manual_texture_views: &ManualTextureViews,
|
||||
) -> Option<RenderTargetInfo> {
|
||||
match self {
|
||||
NormalizedRenderTarget::Window(window_ref) => resolutions
|
||||
.into_iter()
|
||||
.find(|(entity, _)| *entity == window_ref.entity())
|
||||
.map(|(_, window)| RenderTargetInfo {
|
||||
physical_size: window.physical_size(),
|
||||
scale_factor: window.resolution.scale_factor(),
|
||||
}),
|
||||
NormalizedRenderTarget::Image(image_target) => {
|
||||
let image = images.get(&image_target.handle)?;
|
||||
Some(RenderTargetInfo {
|
||||
physical_size: image.size(),
|
||||
scale_factor: image_target.scale_factor.0,
|
||||
})
|
||||
}
|
||||
NormalizedRenderTarget::TextureView(id) => {
|
||||
manual_texture_views.get(id).map(|tex| RenderTargetInfo {
|
||||
physical_size: tex.size,
|
||||
scale_factor: 1.0,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this render target is contained in the given changed windows or images.
|
||||
fn is_changed(
|
||||
&self,
|
||||
changed_window_ids: &HashSet<Entity>,
|
||||
changed_image_handles: &HashSet<&AssetId<Image>>,
|
||||
) -> bool {
|
||||
match self {
|
||||
NormalizedRenderTarget::Window(window_ref) => {
|
||||
changed_window_ids.contains(&window_ref.entity())
|
||||
}
|
||||
NormalizedRenderTarget::Image(image_target) => {
|
||||
changed_image_handles.contains(&image_target.handle.id())
|
||||
}
|
||||
NormalizedRenderTarget::TextureView(_) => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// System in charge of updating a [`Camera`] when its window or projection changes.
|
||||
///
|
||||
/// The system detects window creation, resize, and scale factor change events to update the camera
|
||||
/// [`Projection`] if needed.
|
||||
///
|
||||
/// ## World Resources
|
||||
///
|
||||
/// [`Res<Assets<Image>>`](Assets<Image>) -- For cameras that render to an image, this resource is used to
|
||||
/// inspect information about the render target. This system will not access any other image assets.
|
||||
///
|
||||
/// [`OrthographicProjection`]: crate::camera::OrthographicProjection
|
||||
/// [`PerspectiveProjection`]: crate::camera::PerspectiveProjection
|
||||
pub fn camera_system(
|
||||
mut window_resized_events: EventReader<WindowResized>,
|
||||
mut window_created_events: EventReader<WindowCreated>,
|
||||
mut window_scale_factor_changed_events: EventReader<WindowScaleFactorChanged>,
|
||||
mut image_asset_events: EventReader<AssetEvent<Image>>,
|
||||
primary_window: Query<Entity, With<PrimaryWindow>>,
|
||||
windows: Query<(Entity, &Window)>,
|
||||
images: Res<Assets<Image>>,
|
||||
manual_texture_views: Res<ManualTextureViews>,
|
||||
mut cameras: Query<(&mut Camera, &mut Projection)>,
|
||||
) {
|
||||
let primary_window = primary_window.iter().next();
|
||||
|
||||
let mut changed_window_ids = <HashSet<_>>::default();
|
||||
changed_window_ids.extend(window_created_events.read().map(|event| event.window));
|
||||
changed_window_ids.extend(window_resized_events.read().map(|event| event.window));
|
||||
let scale_factor_changed_window_ids: HashSet<_> = window_scale_factor_changed_events
|
||||
.read()
|
||||
.map(|event| event.window)
|
||||
.collect();
|
||||
changed_window_ids.extend(scale_factor_changed_window_ids.clone());
|
||||
|
||||
let changed_image_handles: HashSet<&AssetId<Image>> = image_asset_events
|
||||
.read()
|
||||
.filter_map(|event| match event {
|
||||
AssetEvent::Modified { id } | AssetEvent::Added { id } => Some(id),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (mut camera, mut camera_projection) in &mut cameras {
|
||||
let mut viewport_size = camera
|
||||
.viewport
|
||||
.as_ref()
|
||||
.map(|viewport| viewport.physical_size);
|
||||
|
||||
if let Some(normalized_target) = camera.target.normalize(primary_window) {
|
||||
if normalized_target.is_changed(&changed_window_ids, &changed_image_handles)
|
||||
|| camera.is_added()
|
||||
|| camera_projection.is_changed()
|
||||
|| camera.computed.old_viewport_size != viewport_size
|
||||
|| camera.computed.old_sub_camera_view != camera.sub_camera_view
|
||||
{
|
||||
let new_computed_target_info = normalized_target.get_render_target_info(
|
||||
windows,
|
||||
&images,
|
||||
&manual_texture_views,
|
||||
);
|
||||
// Check for the scale factor changing, and resize the viewport if needed.
|
||||
// This can happen when the window is moved between monitors with different DPIs.
|
||||
// Without this, the viewport will take a smaller portion of the window moved to
|
||||
// a higher DPI monitor.
|
||||
if normalized_target
|
||||
.is_changed(&scale_factor_changed_window_ids, &HashSet::default())
|
||||
{
|
||||
if let (Some(new_scale_factor), Some(old_scale_factor)) = (
|
||||
new_computed_target_info
|
||||
.as_ref()
|
||||
.map(|info| info.scale_factor),
|
||||
camera
|
||||
.computed
|
||||
.target_info
|
||||
.as_ref()
|
||||
.map(|info| info.scale_factor),
|
||||
) {
|
||||
let resize_factor = new_scale_factor / old_scale_factor;
|
||||
if let Some(ref mut viewport) = camera.viewport {
|
||||
let resize = |vec: UVec2| (vec.as_vec2() * resize_factor).as_uvec2();
|
||||
viewport.physical_position = resize(viewport.physical_position);
|
||||
viewport.physical_size = resize(viewport.physical_size);
|
||||
viewport_size = Some(viewport.physical_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
// This check is needed because when changing WindowMode to Fullscreen, the viewport may have invalid
|
||||
// arguments due to a sudden change on the window size to a lower value.
|
||||
// If the size of the window is lower, the viewport will match that lower value.
|
||||
if let Some(viewport) = &mut camera.viewport {
|
||||
let target_info = &new_computed_target_info;
|
||||
if let Some(target) = target_info {
|
||||
viewport.clamp_to_size(target.physical_size);
|
||||
}
|
||||
}
|
||||
camera.computed.target_info = new_computed_target_info;
|
||||
if let Some(size) = camera.logical_viewport_size() {
|
||||
if size.x != 0.0 && size.y != 0.0 {
|
||||
camera_projection.update(size.x, size.y);
|
||||
camera.computed.clip_from_view = match &camera.sub_camera_view {
|
||||
Some(sub_view) => {
|
||||
camera_projection.get_clip_from_view_for_sub(sub_view)
|
||||
}
|
||||
None => camera_projection.get_clip_from_view(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if camera.computed.old_viewport_size != viewport_size {
|
||||
camera.computed.old_viewport_size = viewport_size;
|
||||
}
|
||||
|
||||
if camera.computed.old_sub_camera_view != camera.sub_camera_view {
|
||||
camera.computed.old_sub_camera_view = camera.sub_camera_view;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This component lets you control the [`TextureUsages`] field of the main texture generated for the camera
|
||||
#[derive(Component, ExtractComponent, Clone, Copy, Reflect)]
|
||||
#[derive(Component, Clone, Copy, Reflect)]
|
||||
#[reflect(opaque)]
|
||||
#[reflect(Component, Default, Clone)]
|
||||
pub struct CameraMainTextureUsages(pub TextureUsages);
|
||||
@ -1088,310 +813,3 @@ impl CameraMainTextureUsages {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
pub struct ExtractedCamera {
|
||||
pub target: Option<NormalizedRenderTarget>,
|
||||
pub physical_viewport_size: Option<UVec2>,
|
||||
pub physical_target_size: Option<UVec2>,
|
||||
pub viewport: Option<Viewport>,
|
||||
pub render_graph: InternedRenderSubGraph,
|
||||
pub order: isize,
|
||||
pub output_mode: CameraOutputMode,
|
||||
pub msaa_writeback: bool,
|
||||
pub clear_color: ClearColorConfig,
|
||||
pub sorted_camera_index_for_target: usize,
|
||||
pub exposure: f32,
|
||||
pub hdr: bool,
|
||||
}
|
||||
|
||||
pub fn extract_cameras(
|
||||
mut commands: Commands,
|
||||
query: Extract<
|
||||
Query<(
|
||||
Entity,
|
||||
RenderEntity,
|
||||
&Camera,
|
||||
&CameraRenderGraph,
|
||||
&GlobalTransform,
|
||||
&VisibleEntities,
|
||||
&Frustum,
|
||||
Has<Hdr>,
|
||||
Option<&ColorGrading>,
|
||||
Option<&Exposure>,
|
||||
Option<&TemporalJitter>,
|
||||
Option<&MipBias>,
|
||||
Option<&RenderLayers>,
|
||||
Option<&Projection>,
|
||||
Has<NoIndirectDrawing>,
|
||||
)>,
|
||||
>,
|
||||
primary_window: Extract<Query<Entity, With<PrimaryWindow>>>,
|
||||
gpu_preprocessing_support: Res<GpuPreprocessingSupport>,
|
||||
mapper: Extract<Query<&RenderEntity>>,
|
||||
) {
|
||||
let primary_window = primary_window.iter().next();
|
||||
for (
|
||||
main_entity,
|
||||
render_entity,
|
||||
camera,
|
||||
camera_render_graph,
|
||||
transform,
|
||||
visible_entities,
|
||||
frustum,
|
||||
hdr,
|
||||
color_grading,
|
||||
exposure,
|
||||
temporal_jitter,
|
||||
mip_bias,
|
||||
render_layers,
|
||||
projection,
|
||||
no_indirect_drawing,
|
||||
) in query.iter()
|
||||
{
|
||||
if !camera.is_active {
|
||||
commands.entity(render_entity).remove::<(
|
||||
ExtractedCamera,
|
||||
ExtractedView,
|
||||
RenderVisibleEntities,
|
||||
TemporalJitter,
|
||||
MipBias,
|
||||
RenderLayers,
|
||||
Projection,
|
||||
NoIndirectDrawing,
|
||||
ViewUniformOffset,
|
||||
)>();
|
||||
continue;
|
||||
}
|
||||
|
||||
let color_grading = color_grading.unwrap_or(&ColorGrading::default()).clone();
|
||||
|
||||
if let (
|
||||
Some(URect {
|
||||
min: viewport_origin,
|
||||
..
|
||||
}),
|
||||
Some(viewport_size),
|
||||
Some(target_size),
|
||||
) = (
|
||||
camera.physical_viewport_rect(),
|
||||
camera.physical_viewport_size(),
|
||||
camera.physical_target_size(),
|
||||
) {
|
||||
if target_size.x == 0 || target_size.y == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let render_visible_entities = RenderVisibleEntities {
|
||||
entities: visible_entities
|
||||
.entities
|
||||
.iter()
|
||||
.map(|(type_id, entities)| {
|
||||
let entities = entities
|
||||
.iter()
|
||||
.map(|entity| {
|
||||
let render_entity = mapper
|
||||
.get(*entity)
|
||||
.cloned()
|
||||
.map(|entity| entity.id())
|
||||
.unwrap_or(Entity::PLACEHOLDER);
|
||||
(render_entity, (*entity).into())
|
||||
})
|
||||
.collect();
|
||||
(*type_id, entities)
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let mut commands = commands.entity(render_entity);
|
||||
commands.insert((
|
||||
ExtractedCamera {
|
||||
target: camera.target.normalize(primary_window),
|
||||
viewport: camera.viewport.clone(),
|
||||
physical_viewport_size: Some(viewport_size),
|
||||
physical_target_size: Some(target_size),
|
||||
render_graph: camera_render_graph.0,
|
||||
order: camera.order,
|
||||
output_mode: camera.output_mode,
|
||||
msaa_writeback: camera.msaa_writeback,
|
||||
clear_color: camera.clear_color,
|
||||
// this will be set in sort_cameras
|
||||
sorted_camera_index_for_target: 0,
|
||||
exposure: exposure
|
||||
.map(Exposure::exposure)
|
||||
.unwrap_or_else(|| Exposure::default().exposure()),
|
||||
hdr,
|
||||
},
|
||||
ExtractedView {
|
||||
retained_view_entity: RetainedViewEntity::new(main_entity.into(), None, 0),
|
||||
clip_from_view: camera.clip_from_view(),
|
||||
world_from_view: *transform,
|
||||
clip_from_world: None,
|
||||
hdr,
|
||||
viewport: UVec4::new(
|
||||
viewport_origin.x,
|
||||
viewport_origin.y,
|
||||
viewport_size.x,
|
||||
viewport_size.y,
|
||||
),
|
||||
color_grading,
|
||||
},
|
||||
render_visible_entities,
|
||||
*frustum,
|
||||
));
|
||||
|
||||
if let Some(temporal_jitter) = temporal_jitter {
|
||||
commands.insert(temporal_jitter.clone());
|
||||
} else {
|
||||
commands.remove::<TemporalJitter>();
|
||||
}
|
||||
|
||||
if let Some(mip_bias) = mip_bias {
|
||||
commands.insert(mip_bias.clone());
|
||||
} else {
|
||||
commands.remove::<MipBias>();
|
||||
}
|
||||
|
||||
if let Some(render_layers) = render_layers {
|
||||
commands.insert(render_layers.clone());
|
||||
} else {
|
||||
commands.remove::<RenderLayers>();
|
||||
}
|
||||
|
||||
if let Some(perspective) = projection {
|
||||
commands.insert(perspective.clone());
|
||||
} else {
|
||||
commands.remove::<Projection>();
|
||||
}
|
||||
|
||||
if no_indirect_drawing
|
||||
|| !matches!(
|
||||
gpu_preprocessing_support.max_supported_mode,
|
||||
GpuPreprocessingMode::Culling
|
||||
)
|
||||
{
|
||||
commands.insert(NoIndirectDrawing);
|
||||
} else {
|
||||
commands.remove::<NoIndirectDrawing>();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Cameras sorted by their order field. This is updated in the [`sort_cameras`] system.
|
||||
#[derive(Resource, Default)]
|
||||
pub struct SortedCameras(pub Vec<SortedCamera>);
|
||||
|
||||
pub struct SortedCamera {
|
||||
pub entity: Entity,
|
||||
pub order: isize,
|
||||
pub target: Option<NormalizedRenderTarget>,
|
||||
pub hdr: bool,
|
||||
}
|
||||
|
||||
pub fn sort_cameras(
|
||||
mut sorted_cameras: ResMut<SortedCameras>,
|
||||
mut cameras: Query<(Entity, &mut ExtractedCamera)>,
|
||||
) {
|
||||
sorted_cameras.0.clear();
|
||||
for (entity, camera) in cameras.iter() {
|
||||
sorted_cameras.0.push(SortedCamera {
|
||||
entity,
|
||||
order: camera.order,
|
||||
target: camera.target.clone(),
|
||||
hdr: camera.hdr,
|
||||
});
|
||||
}
|
||||
// sort by order and ensure within an order, RenderTargets of the same type are packed together
|
||||
sorted_cameras
|
||||
.0
|
||||
.sort_by(|c1, c2| (c1.order, &c1.target).cmp(&(c2.order, &c2.target)));
|
||||
let mut previous_order_target = None;
|
||||
let mut ambiguities = <HashSet<_>>::default();
|
||||
let mut target_counts = <HashMap<_, _>>::default();
|
||||
for sorted_camera in &mut sorted_cameras.0 {
|
||||
let new_order_target = (sorted_camera.order, sorted_camera.target.clone());
|
||||
if let Some(previous_order_target) = previous_order_target {
|
||||
if previous_order_target == new_order_target {
|
||||
ambiguities.insert(new_order_target.clone());
|
||||
}
|
||||
}
|
||||
if let Some(target) = &sorted_camera.target {
|
||||
let count = target_counts
|
||||
.entry((target.clone(), sorted_camera.hdr))
|
||||
.or_insert(0usize);
|
||||
let (_, mut camera) = cameras.get_mut(sorted_camera.entity).unwrap();
|
||||
camera.sorted_camera_index_for_target = *count;
|
||||
*count += 1;
|
||||
}
|
||||
previous_order_target = Some(new_order_target);
|
||||
}
|
||||
|
||||
if !ambiguities.is_empty() {
|
||||
warn!(
|
||||
"Camera order ambiguities detected for active cameras with the following priorities: {:?}. \
|
||||
To fix this, ensure there is exactly one Camera entity spawned with a given order for a given RenderTarget. \
|
||||
Ambiguities should be resolved because either (1) multiple active cameras were spawned accidentally, which will \
|
||||
result in rendering multiple instances of the scene or (2) for cases where multiple active cameras is intentional, \
|
||||
ambiguities could result in unpredictable render results.",
|
||||
ambiguities
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A subpixel offset to jitter a perspective camera's frustum by.
|
||||
///
|
||||
/// Useful for temporal rendering techniques.
|
||||
///
|
||||
/// Do not use with [`OrthographicProjection`].
|
||||
///
|
||||
/// [`OrthographicProjection`]: crate::camera::OrthographicProjection
|
||||
#[derive(Component, Clone, Default, Reflect)]
|
||||
#[reflect(Default, Component, Clone)]
|
||||
pub struct TemporalJitter {
|
||||
/// Offset is in range [-0.5, 0.5].
|
||||
pub offset: Vec2,
|
||||
}
|
||||
|
||||
impl TemporalJitter {
|
||||
pub fn jitter_projection(&self, clip_from_view: &mut Mat4, view_size: Vec2) {
|
||||
if clip_from_view.w_axis.w == 1.0 {
|
||||
warn!(
|
||||
"TemporalJitter not supported with OrthographicProjection. Use PerspectiveProjection instead."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// https://github.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/blob/d7531ae47d8b36a5d4025663e731a47a38be882f/docs/techniques/media/super-resolution-temporal/jitter-space.svg
|
||||
let jitter = (self.offset * vec2(2.0, -2.0)) / view_size;
|
||||
|
||||
clip_from_view.z_axis.x += jitter.x;
|
||||
clip_from_view.z_axis.y += jitter.y;
|
||||
}
|
||||
}
|
||||
|
||||
/// Camera component specifying a mip bias to apply when sampling from material textures.
|
||||
///
|
||||
/// Often used in conjunction with antialiasing post-process effects to reduce textures blurriness.
|
||||
#[derive(Component, Reflect, Clone)]
|
||||
#[reflect(Default, Component)]
|
||||
pub struct MipBias(pub f32);
|
||||
|
||||
/// Override the resolution a 3d camera's main pass is rendered at.
|
||||
///
|
||||
/// Does not affect post processing.
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// * Insert this component on a 3d camera entity in the render world.
|
||||
/// * The resolution override must be smaller than the camera's viewport size.
|
||||
/// * The resolution override is specified in physical pixels.
|
||||
#[derive(Component, Reflect, Deref)]
|
||||
#[reflect(Component)]
|
||||
pub struct MainPassResolutionOverride(pub UVec2);
|
||||
|
||||
impl Default for MipBias {
|
||||
fn default() -> Self {
|
||||
Self(-1.0)
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
use crate::extract_resource::ExtractResource;
|
||||
use bevy_color::Color;
|
||||
use bevy_derive::{Deref, DerefMut};
|
||||
use bevy_ecs::prelude::*;
|
||||
@ -32,7 +31,7 @@ pub enum ClearColorConfig {
|
||||
/// clear color or opt out of clearing their viewport.
|
||||
///
|
||||
/// [`Camera.clear_color`]: crate::camera::Camera::clear_color
|
||||
#[derive(Resource, Clone, Debug, Deref, DerefMut, ExtractResource, Reflect)]
|
||||
#[derive(Resource, Clone, Debug, Deref, DerefMut, Reflect)]
|
||||
#[reflect(Resource, Default, Debug, Clone)]
|
||||
pub struct ClearColor(pub Color);
|
||||
|
||||
35
crates/bevy_camera/src/lib.rs
Normal file
35
crates/bevy_camera/src/lib.rs
Normal file
@ -0,0 +1,35 @@
|
||||
#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
|
||||
mod camera;
|
||||
mod clear_color;
|
||||
pub mod primitives;
|
||||
mod projection;
|
||||
pub mod visibility;
|
||||
|
||||
pub use camera::*;
|
||||
pub use clear_color::*;
|
||||
pub use projection::*;
|
||||
|
||||
use bevy_app::{App, Plugin};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CameraPlugin;
|
||||
|
||||
impl Plugin for CameraPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.register_type::<Camera>()
|
||||
.register_type::<ClearColor>()
|
||||
.register_type::<CameraMainTextureUsages>()
|
||||
.register_type::<Exposure>()
|
||||
.register_type::<MainPassResolutionOverride>()
|
||||
.register_type::<primitives::Aabb>()
|
||||
.register_type::<primitives::CascadesFrusta>()
|
||||
.register_type::<primitives::CubemapFrusta>()
|
||||
.register_type::<primitives::Frustum>()
|
||||
.init_resource::<ClearColor>()
|
||||
.add_plugins((
|
||||
CameraProjectionPlugin,
|
||||
visibility::VisibilityPlugin,
|
||||
visibility::VisibilityRangePlugin,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -2,8 +2,29 @@ use core::borrow::Borrow;
|
||||
|
||||
use bevy_ecs::{component::Component, entity::EntityHashMap, reflect::ReflectComponent};
|
||||
use bevy_math::{Affine3A, Mat3A, Mat4, Vec3, Vec3A, Vec4, Vec4Swizzles};
|
||||
use bevy_mesh::{Mesh, VertexAttributeValues};
|
||||
use bevy_reflect::prelude::*;
|
||||
|
||||
pub trait MeshAabb {
|
||||
/// Compute the Axis-Aligned Bounding Box of the mesh vertices in model space
|
||||
///
|
||||
/// Returns `None` if `self` doesn't have [`Mesh::ATTRIBUTE_POSITION`] of
|
||||
/// type [`VertexAttributeValues::Float32x3`], or if `self` doesn't have any vertices.
|
||||
fn compute_aabb(&self) -> Option<Aabb>;
|
||||
}
|
||||
|
||||
impl MeshAabb for Mesh {
|
||||
fn compute_aabb(&self) -> Option<Aabb> {
|
||||
let Some(VertexAttributeValues::Float32x3(values)) =
|
||||
self.attribute(Mesh::ATTRIBUTE_POSITION)
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Aabb::enclosing(values.iter().map(|p| Vec3::from_slice(p)))
|
||||
}
|
||||
}
|
||||
|
||||
/// An axis-aligned bounding box, defined by:
|
||||
/// - a center,
|
||||
/// - the distances from the center to each faces along the axis,
|
||||
@ -24,10 +45,10 @@ use bevy_reflect::prelude::*;
|
||||
/// It won't be updated automatically if the space occupied by the entity changes,
|
||||
/// for example if the vertex positions of a [`Mesh3d`] are updated.
|
||||
///
|
||||
/// [`Camera`]: crate::camera::Camera
|
||||
/// [`NoFrustumCulling`]: crate::view::visibility::NoFrustumCulling
|
||||
/// [`CalculateBounds`]: crate::view::visibility::VisibilitySystems::CalculateBounds
|
||||
/// [`Mesh3d`]: crate::mesh::Mesh
|
||||
/// [`Camera`]: crate::Camera
|
||||
/// [`NoFrustumCulling`]: crate::visibility::NoFrustumCulling
|
||||
/// [`CalculateBounds`]: crate::visibility::VisibilitySystems::CalculateBounds
|
||||
/// [`Mesh3d`]: bevy_mesh::Mesh
|
||||
#[derive(Component, Clone, Copy, Debug, Default, Reflect, PartialEq)]
|
||||
#[reflect(Component, Default, Debug, PartialEq, Clone)]
|
||||
pub struct Aabb {
|
||||
@ -56,7 +77,7 @@ impl Aabb {
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_math::{Vec3, Vec3A};
|
||||
/// # use bevy_render::primitives::Aabb;
|
||||
/// # use bevy_camera::primitives::Aabb;
|
||||
/// let bb = Aabb::enclosing([Vec3::X, Vec3::Z * 2.0, Vec3::Y * -0.5]).unwrap();
|
||||
/// assert_eq!(bb.min(), Vec3A::new(0.0, -0.5, 0.0));
|
||||
/// assert_eq!(bb.max(), Vec3A::new(1.0, 0.0, 2.0));
|
||||
@ -218,10 +239,10 @@ impl HalfSpace {
|
||||
/// It is usually updated automatically by [`update_frusta`] from the
|
||||
/// [`CameraProjection`] component and [`GlobalTransform`] of the camera entity.
|
||||
///
|
||||
/// [`Camera`]: crate::camera::Camera
|
||||
/// [`NoFrustumCulling`]: crate::view::visibility::NoFrustumCulling
|
||||
/// [`update_frusta`]: crate::view::visibility::update_frusta
|
||||
/// [`CameraProjection`]: crate::camera::CameraProjection
|
||||
/// [`Camera`]: crate::Camera
|
||||
/// [`NoFrustumCulling`]: crate::visibility::NoFrustumCulling
|
||||
/// [`update_frusta`]: crate::visibility::update_frusta
|
||||
/// [`CameraProjection`]: crate::CameraProjection
|
||||
/// [`GlobalTransform`]: bevy_transform::components::GlobalTransform
|
||||
#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
|
||||
#[reflect(Component, Default, Debug, Clone)]
|
||||
@ -356,7 +377,7 @@ mod tests {
|
||||
use bevy_math::{ops, Quat};
|
||||
use bevy_transform::components::GlobalTransform;
|
||||
|
||||
use crate::camera::{CameraProjection, PerspectiveProjection};
|
||||
use crate::{CameraProjection, PerspectiveProjection};
|
||||
|
||||
use super::*;
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
use core::fmt::Debug;
|
||||
use core::ops::{Deref, DerefMut};
|
||||
|
||||
use crate::{primitives::Frustum, view::VisibilitySystems};
|
||||
use bevy_app::{App, Plugin, PostStartup, PostUpdate};
|
||||
use bevy_asset::AssetEventSystems;
|
||||
use crate::{primitives::Frustum, visibility::VisibilitySystems};
|
||||
use bevy_app::{App, Plugin, PostUpdate};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_math::{ops, AspectRatio, Mat4, Rect, Vec2, Vec3A, Vec4};
|
||||
use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize};
|
||||
@ -23,28 +22,16 @@ impl Plugin for CameraProjectionPlugin {
|
||||
.register_type::<PerspectiveProjection>()
|
||||
.register_type::<OrthographicProjection>()
|
||||
.register_type::<CustomProjection>()
|
||||
.add_systems(
|
||||
PostStartup,
|
||||
crate::camera::camera_system.in_set(CameraUpdateSystems),
|
||||
)
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
(
|
||||
crate::camera::camera_system
|
||||
.in_set(CameraUpdateSystems)
|
||||
.before(AssetEventSystems),
|
||||
crate::view::update_frusta
|
||||
crate::visibility::update_frusta
|
||||
.in_set(VisibilitySystems::UpdateFrusta)
|
||||
.after(crate::camera::camera_system)
|
||||
.after(TransformSystems::Propagate),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Label for [`camera_system<T>`], shared across all `T`.
|
||||
///
|
||||
/// [`camera_system<T>`]: crate::camera::camera_system
|
||||
/// Label for `camera_system<T>`, shared across all `T`.
|
||||
#[derive(SystemSet, Clone, Eq, PartialEq, Hash, Debug)]
|
||||
pub struct CameraUpdateSystems;
|
||||
|
||||
@ -90,7 +77,7 @@ pub trait CameraProjection {
|
||||
|
||||
/// Compute camera frustum for camera with given projection and transform.
|
||||
///
|
||||
/// This code is called by [`update_frusta`](crate::view::visibility::update_frusta) system
|
||||
/// This code is called by [`update_frusta`](crate::visibility::update_frusta) system
|
||||
/// for each camera to update its frustum.
|
||||
fn compute_frustum(&self, camera_transform: &GlobalTransform) -> Frustum {
|
||||
let clip_from_world = self.get_clip_from_view() * camera_transform.to_matrix().inverse();
|
||||
@ -160,7 +147,7 @@ impl CustomProjection {
|
||||
/// Returns `None` if this dynamic object is not a projection of type `P`.
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_render::prelude::{Projection, PerspectiveProjection};
|
||||
/// # use bevy_camera::{Projection, PerspectiveProjection};
|
||||
/// // For simplicity's sake, use perspective as a custom projection:
|
||||
/// let projection = Projection::custom(PerspectiveProjection::default());
|
||||
/// let Projection::Custom(custom) = projection else { return };
|
||||
@ -183,7 +170,7 @@ impl CustomProjection {
|
||||
/// Returns `None` if this dynamic object is not a projection of type `P`.
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_render::prelude::{Projection, PerspectiveProjection};
|
||||
/// # use bevy_camera::{Projection, PerspectiveProjection};
|
||||
/// // For simplicity's sake, use perspective as a custom projection:
|
||||
/// let mut projection = Projection::custom(PerspectiveProjection::default());
|
||||
/// let Projection::Custom(mut custom) = projection else { return };
|
||||
@ -303,8 +290,8 @@ pub struct PerspectiveProjection {
|
||||
|
||||
/// The aspect ratio (width divided by height) of the viewing frustum.
|
||||
///
|
||||
/// Bevy's [`camera_system`](crate::camera::camera_system) automatically
|
||||
/// updates this value when the aspect ratio of the associated window changes.
|
||||
/// Bevy's `camera_system` automatically updates this value when the aspect ratio
|
||||
/// of the associated window changes.
|
||||
///
|
||||
/// Defaults to a value of `1.0`.
|
||||
pub aspect_ratio: f32,
|
||||
@ -422,7 +409,7 @@ impl Default for PerspectiveProjection {
|
||||
/// Configure the orthographic projection to two world units per window height:
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_render::camera::{OrthographicProjection, Projection, ScalingMode};
|
||||
/// # use bevy_camera::{OrthographicProjection, Projection, ScalingMode};
|
||||
/// let projection = Projection::Orthographic(OrthographicProjection {
|
||||
/// scaling_mode: ScalingMode::FixedVertical { viewport_height: 2.0 },
|
||||
/// ..OrthographicProjection::default_2d()
|
||||
@ -478,7 +465,7 @@ pub enum ScalingMode {
|
||||
/// Configure the orthographic projection to one world unit per 100 window pixels:
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_render::camera::{OrthographicProjection, Projection, ScalingMode};
|
||||
/// # use bevy_camera::{OrthographicProjection, Projection, ScalingMode};
|
||||
/// let projection = Projection::Orthographic(OrthographicProjection {
|
||||
/// scaling_mode: ScalingMode::WindowSize,
|
||||
/// scale: 0.01,
|
||||
@ -535,7 +522,7 @@ pub struct OrthographicProjection {
|
||||
pub scale: f32,
|
||||
/// The area that the projection covers relative to `viewport_origin`.
|
||||
///
|
||||
/// Bevy's [`camera_system`](crate::camera::camera_system) automatically
|
||||
/// Bevy's `camera_system` automatically
|
||||
/// updates this value when the viewport is resized depending on `OrthographicProjection`'s other fields.
|
||||
/// In this case, `area` should not be manually modified.
|
||||
///
|
||||
948
crates/bevy_camera/src/visibility/mod.rs
Normal file
948
crates/bevy_camera/src/visibility/mod.rs
Normal file
@ -0,0 +1,948 @@
|
||||
mod range;
|
||||
mod render_layers;
|
||||
|
||||
use core::any::TypeId;
|
||||
|
||||
use bevy_ecs::entity::EntityHashSet;
|
||||
use bevy_ecs::lifecycle::HookContext;
|
||||
use bevy_ecs::world::DeferredWorld;
|
||||
use derive_more::derive::{Deref, DerefMut};
|
||||
pub use range::*;
|
||||
pub use render_layers::*;
|
||||
|
||||
use bevy_app::{Plugin, PostUpdate};
|
||||
use bevy_asset::Assets;
|
||||
use bevy_ecs::{hierarchy::validate_parent_has_component, prelude::*};
|
||||
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
||||
use bevy_transform::{components::GlobalTransform, TransformSystems};
|
||||
use bevy_utils::{Parallel, TypeIdMap};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
camera::Camera,
|
||||
primitives::{Aabb, Frustum, MeshAabb, Sphere},
|
||||
Projection,
|
||||
};
|
||||
use bevy_mesh::{Mesh, Mesh2d, Mesh3d};
|
||||
|
||||
#[derive(Component, Default)]
|
||||
pub struct NoCpuCulling;
|
||||
|
||||
/// User indication of whether an entity is visible. Propagates down the entity hierarchy.
|
||||
///
|
||||
/// If an entity is hidden in this way, all [`Children`] (and all of their children and so on) who
|
||||
/// are set to [`Inherited`](Self::Inherited) will also be hidden.
|
||||
///
|
||||
/// This is done by the `visibility_propagate_system` which uses the entity hierarchy and
|
||||
/// `Visibility` to set the values of each entity's [`InheritedVisibility`] component.
|
||||
#[derive(Component, Clone, Copy, Reflect, Debug, PartialEq, Eq, Default)]
|
||||
#[reflect(Component, Default, Debug, PartialEq, Clone)]
|
||||
#[require(InheritedVisibility, ViewVisibility)]
|
||||
pub enum Visibility {
|
||||
/// An entity with `Visibility::Inherited` will inherit the Visibility of its [`ChildOf`] target.
|
||||
///
|
||||
/// A root-level entity that is set to `Inherited` will be visible.
|
||||
#[default]
|
||||
Inherited,
|
||||
/// An entity with `Visibility::Hidden` will be unconditionally hidden.
|
||||
Hidden,
|
||||
/// An entity with `Visibility::Visible` will be unconditionally visible.
|
||||
///
|
||||
/// Note that an entity with `Visibility::Visible` will be visible regardless of whether the
|
||||
/// [`ChildOf`] target entity is hidden.
|
||||
Visible,
|
||||
}
|
||||
|
||||
impl Visibility {
|
||||
/// Toggles between `Visibility::Inherited` and `Visibility::Visible`.
|
||||
/// If the value is `Visibility::Hidden`, it remains unaffected.
|
||||
#[inline]
|
||||
pub fn toggle_inherited_visible(&mut self) {
|
||||
*self = match *self {
|
||||
Visibility::Inherited => Visibility::Visible,
|
||||
Visibility::Visible => Visibility::Inherited,
|
||||
_ => *self,
|
||||
};
|
||||
}
|
||||
/// Toggles between `Visibility::Inherited` and `Visibility::Hidden`.
|
||||
/// If the value is `Visibility::Visible`, it remains unaffected.
|
||||
#[inline]
|
||||
pub fn toggle_inherited_hidden(&mut self) {
|
||||
*self = match *self {
|
||||
Visibility::Inherited => Visibility::Hidden,
|
||||
Visibility::Hidden => Visibility::Inherited,
|
||||
_ => *self,
|
||||
};
|
||||
}
|
||||
/// Toggles between `Visibility::Visible` and `Visibility::Hidden`.
|
||||
/// If the value is `Visibility::Inherited`, it remains unaffected.
|
||||
#[inline]
|
||||
pub fn toggle_visible_hidden(&mut self) {
|
||||
*self = match *self {
|
||||
Visibility::Visible => Visibility::Hidden,
|
||||
Visibility::Hidden => Visibility::Visible,
|
||||
_ => *self,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Allows `&Visibility == Visibility`
|
||||
impl PartialEq<Visibility> for &Visibility {
|
||||
#[inline]
|
||||
fn eq(&self, other: &Visibility) -> bool {
|
||||
// Use the base Visibility == Visibility implementation.
|
||||
<Visibility as PartialEq<Visibility>>::eq(*self, other)
|
||||
}
|
||||
}
|
||||
|
||||
// Allows `Visibility == &Visibility`
|
||||
impl PartialEq<&Visibility> for Visibility {
|
||||
#[inline]
|
||||
fn eq(&self, other: &&Visibility) -> bool {
|
||||
// Use the base Visibility == Visibility implementation.
|
||||
<Visibility as PartialEq<Visibility>>::eq(self, *other)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not an entity is visible in the hierarchy.
|
||||
/// This will not be accurate until [`VisibilityPropagate`] runs in the [`PostUpdate`] schedule.
|
||||
///
|
||||
/// If this is false, then [`ViewVisibility`] should also be false.
|
||||
///
|
||||
/// [`VisibilityPropagate`]: VisibilitySystems::VisibilityPropagate
|
||||
#[derive(Component, Deref, Debug, Default, Clone, Copy, Reflect, PartialEq, Eq)]
|
||||
#[reflect(Component, Default, Debug, PartialEq, Clone)]
|
||||
#[component(on_insert = validate_parent_has_component::<Self>)]
|
||||
pub struct InheritedVisibility(bool);
|
||||
|
||||
impl InheritedVisibility {
|
||||
/// An entity that is invisible in the hierarchy.
|
||||
pub const HIDDEN: Self = Self(false);
|
||||
/// An entity that is visible in the hierarchy.
|
||||
pub const VISIBLE: Self = Self(true);
|
||||
|
||||
/// Returns `true` if the entity is visible in the hierarchy.
|
||||
/// Otherwise, returns `false`.
|
||||
#[inline]
|
||||
pub fn get(self) -> bool {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A bucket into which we group entities for the purposes of visibility.
|
||||
///
|
||||
/// Bevy's various rendering subsystems (3D, 2D, etc.) want to be able to
|
||||
/// quickly winnow the set of entities to only those that the subsystem is
|
||||
/// tasked with rendering, to avoid spending time examining irrelevant entities.
|
||||
/// At the same time, Bevy wants the [`check_visibility`] system to determine
|
||||
/// all entities' visibilities at the same time, regardless of what rendering
|
||||
/// subsystem is responsible for drawing them. Additionally, your application
|
||||
/// may want to add more types of renderable objects that Bevy determines
|
||||
/// visibility for just as it does for Bevy's built-in objects.
|
||||
///
|
||||
/// The solution to this problem is *visibility classes*. A visibility class is
|
||||
/// a type, typically the type of a component, that represents the subsystem
|
||||
/// that renders it: for example, `Mesh3d`, `Mesh2d`, and `Sprite`. The
|
||||
/// [`VisibilityClass`] component stores the visibility class or classes that
|
||||
/// the entity belongs to. (Generally, an object will belong to only one
|
||||
/// visibility class, but in rare cases it may belong to multiple.)
|
||||
///
|
||||
/// When adding a new renderable component, you'll typically want to write an
|
||||
/// add-component hook that adds the type ID of that component to the
|
||||
/// [`VisibilityClass`] array. See `custom_phase_item` for an example.
|
||||
//
|
||||
// Note: This can't be a `ComponentId` because the visibility classes are copied
|
||||
// into the render world, and component IDs are per-world.
|
||||
#[derive(Clone, Component, Default, Reflect, Deref, DerefMut)]
|
||||
#[reflect(Component, Default, Clone)]
|
||||
pub struct VisibilityClass(pub SmallVec<[TypeId; 1]>);
|
||||
|
||||
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering.
|
||||
///
|
||||
/// Each frame, this will be reset to `false` during [`VisibilityPropagate`] systems in [`PostUpdate`].
|
||||
/// Later in the frame, systems in [`CheckVisibility`] will mark any visible entities using [`ViewVisibility::set`].
|
||||
/// Because of this, values of this type will be marked as changed every frame, even when they do not change.
|
||||
///
|
||||
/// If you wish to add custom visibility system that sets this value, make sure you add it to the [`CheckVisibility`] set.
|
||||
///
|
||||
/// [`VisibilityPropagate`]: VisibilitySystems::VisibilityPropagate
|
||||
/// [`CheckVisibility`]: VisibilitySystems::CheckVisibility
|
||||
#[derive(Component, Deref, Debug, Default, Clone, Copy, Reflect, PartialEq, Eq)]
|
||||
#[reflect(Component, Default, Debug, PartialEq, Clone)]
|
||||
pub struct ViewVisibility(bool);
|
||||
|
||||
impl ViewVisibility {
|
||||
/// An entity that cannot be seen from any views.
|
||||
pub const HIDDEN: Self = Self(false);
|
||||
|
||||
/// Returns `true` if the entity is visible in any view.
|
||||
/// Otherwise, returns `false`.
|
||||
#[inline]
|
||||
pub fn get(self) -> bool {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Sets the visibility to `true`. This should not be considered reversible for a given frame,
|
||||
/// as this component tracks whether or not the entity visible in _any_ view.
|
||||
///
|
||||
/// This will be automatically reset to `false` every frame in [`VisibilityPropagate`] and then set
|
||||
/// to the proper value in [`CheckVisibility`].
|
||||
///
|
||||
/// You should only manually set this if you are defining a custom visibility system,
|
||||
/// in which case the system should be placed in the [`CheckVisibility`] set.
|
||||
/// For normal user-defined entity visibility, see [`Visibility`].
|
||||
///
|
||||
/// [`VisibilityPropagate`]: VisibilitySystems::VisibilityPropagate
|
||||
/// [`CheckVisibility`]: VisibilitySystems::CheckVisibility
|
||||
#[inline]
|
||||
pub fn set(&mut self) {
|
||||
self.0 = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Use this component to opt-out of built-in frustum culling for entities, see
|
||||
/// [`Frustum`].
|
||||
///
|
||||
/// It can be used for example:
|
||||
/// - when a [`Mesh`] is updated but its [`Aabb`] is not, which might happen with animations,
|
||||
/// - when using some light effects, like wanting a [`Mesh`] out of the [`Frustum`]
|
||||
/// to appear in the reflection of a [`Mesh`] within.
|
||||
#[derive(Debug, Component, Default, Reflect)]
|
||||
#[reflect(Component, Default, Debug)]
|
||||
pub struct NoFrustumCulling;
|
||||
|
||||
/// Collection of entities visible from the current view.
|
||||
///
|
||||
/// This component contains all entities which are visible from the currently
|
||||
/// rendered view. The collection is updated automatically by the [`VisibilitySystems::CheckVisibility`]
|
||||
/// system set. Renderers can use the equivalent `RenderVisibleEntities` to optimize rendering of
|
||||
/// a particular view, to prevent drawing items not visible from that view.
|
||||
///
|
||||
/// This component is intended to be attached to the same entity as the [`Camera`] and
|
||||
/// the [`Frustum`] defining the view.
|
||||
#[derive(Clone, Component, Default, Debug, Reflect)]
|
||||
#[reflect(Component, Default, Debug, Clone)]
|
||||
pub struct VisibleEntities {
|
||||
#[reflect(ignore, clone)]
|
||||
pub entities: TypeIdMap<Vec<Entity>>,
|
||||
}
|
||||
|
||||
impl VisibleEntities {
|
||||
pub fn get(&self, type_id: TypeId) -> &[Entity] {
|
||||
match self.entities.get(&type_id) {
|
||||
Some(entities) => &entities[..],
|
||||
None => &[],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, type_id: TypeId) -> &mut Vec<Entity> {
|
||||
self.entities.entry(type_id).or_default()
|
||||
}
|
||||
|
||||
pub fn iter(&self, type_id: TypeId) -> impl DoubleEndedIterator<Item = &Entity> {
|
||||
self.get(type_id).iter()
|
||||
}
|
||||
|
||||
pub fn len(&self, type_id: TypeId) -> usize {
|
||||
self.get(type_id).len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self, type_id: TypeId) -> bool {
|
||||
self.get(type_id).is_empty()
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, type_id: TypeId) {
|
||||
self.get_mut(type_id).clear();
|
||||
}
|
||||
|
||||
pub fn clear_all(&mut self) {
|
||||
// Don't just nuke the hash table; we want to reuse allocations.
|
||||
for entities in self.entities.values_mut() {
|
||||
entities.clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, entity: Entity, type_id: TypeId) {
|
||||
self.get_mut(type_id).push(entity);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
|
||||
pub enum VisibilitySystems {
|
||||
/// Label for the [`calculate_bounds`], `calculate_bounds_2d` and `calculate_bounds_text2d` systems,
|
||||
/// calculating and inserting an [`Aabb`] to relevant entities.
|
||||
CalculateBounds,
|
||||
/// Label for [`update_frusta`] in [`CameraProjectionPlugin`](crate::CameraProjectionPlugin).
|
||||
UpdateFrusta,
|
||||
/// Label for the system propagating the [`InheritedVisibility`] in a
|
||||
/// [`ChildOf`] / [`Children`] hierarchy.
|
||||
VisibilityPropagate,
|
||||
/// Label for the [`check_visibility`] system updating [`ViewVisibility`]
|
||||
/// of each entity and the [`VisibleEntities`] of each view.\
|
||||
///
|
||||
/// System order ambiguities between systems in this set are ignored:
|
||||
/// the order of systems within this set is irrelevant, as [`check_visibility`]
|
||||
/// assumes that its operations are irreversible during the frame.
|
||||
CheckVisibility,
|
||||
/// Label for the `mark_newly_hidden_entities_invisible` system, which sets
|
||||
/// [`ViewVisibility`] to [`ViewVisibility::HIDDEN`] for entities that no
|
||||
/// view has marked as visible.
|
||||
MarkNewlyHiddenEntitiesInvisible,
|
||||
}
|
||||
|
||||
pub struct VisibilityPlugin;
|
||||
|
||||
impl Plugin for VisibilityPlugin {
|
||||
fn build(&self, app: &mut bevy_app::App) {
|
||||
use VisibilitySystems::*;
|
||||
|
||||
app.register_type::<VisibilityClass>()
|
||||
.register_type::<InheritedVisibility>()
|
||||
.register_type::<ViewVisibility>()
|
||||
.register_type::<NoFrustumCulling>()
|
||||
.register_type::<RenderLayers>()
|
||||
.register_type::<Visibility>()
|
||||
.register_type::<VisibleEntities>()
|
||||
.register_required_components::<Mesh3d, Visibility>()
|
||||
.register_required_components::<Mesh3d, VisibilityClass>()
|
||||
.register_required_components::<Mesh2d, Visibility>()
|
||||
.register_required_components::<Mesh2d, VisibilityClass>()
|
||||
.configure_sets(
|
||||
PostUpdate,
|
||||
(CalculateBounds, UpdateFrusta, VisibilityPropagate)
|
||||
.before(CheckVisibility)
|
||||
.after(TransformSystems::Propagate),
|
||||
)
|
||||
.configure_sets(
|
||||
PostUpdate,
|
||||
MarkNewlyHiddenEntitiesInvisible.after(CheckVisibility),
|
||||
)
|
||||
.init_resource::<PreviousVisibleEntities>()
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
(
|
||||
calculate_bounds.in_set(CalculateBounds),
|
||||
(visibility_propagate_system, reset_view_visibility)
|
||||
.in_set(VisibilityPropagate),
|
||||
check_visibility.in_set(CheckVisibility),
|
||||
mark_newly_hidden_entities_invisible.in_set(MarkNewlyHiddenEntitiesInvisible),
|
||||
),
|
||||
);
|
||||
app.world_mut()
|
||||
.register_component_hooks::<Mesh3d>()
|
||||
.on_add(add_visibility_class::<Mesh3d>);
|
||||
app.world_mut()
|
||||
.register_component_hooks::<Mesh2d>()
|
||||
.on_add(add_visibility_class::<Mesh2d>);
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes and adds an [`Aabb`] component to entities with a
|
||||
/// [`Mesh3d`] component and without a [`NoFrustumCulling`] component.
|
||||
///
|
||||
/// This system is used in system set [`VisibilitySystems::CalculateBounds`].
|
||||
pub fn calculate_bounds(
|
||||
mut commands: Commands,
|
||||
meshes: Res<Assets<Mesh>>,
|
||||
without_aabb: Query<(Entity, &Mesh3d), (Without<Aabb>, Without<NoFrustumCulling>)>,
|
||||
) {
|
||||
for (entity, mesh_handle) in &without_aabb {
|
||||
if let Some(mesh) = meshes.get(mesh_handle) {
|
||||
if let Some(aabb) = mesh.compute_aabb() {
|
||||
commands.entity(entity).try_insert(aabb);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates [`Frustum`].
|
||||
///
|
||||
/// This system is used in [`CameraProjectionPlugin`](crate::CameraProjectionPlugin).
|
||||
pub fn update_frusta(
|
||||
mut views: Query<
|
||||
(&GlobalTransform, &Projection, &mut Frustum),
|
||||
Or<(Changed<GlobalTransform>, Changed<Projection>)>,
|
||||
>,
|
||||
) {
|
||||
for (transform, projection, mut frustum) in &mut views {
|
||||
*frustum = projection.compute_frustum(transform);
|
||||
}
|
||||
}
|
||||
|
||||
fn visibility_propagate_system(
|
||||
changed: Query<
|
||||
(Entity, &Visibility, Option<&ChildOf>, Option<&Children>),
|
||||
(
|
||||
With<InheritedVisibility>,
|
||||
Or<(Changed<Visibility>, Changed<ChildOf>)>,
|
||||
),
|
||||
>,
|
||||
mut visibility_query: Query<(&Visibility, &mut InheritedVisibility)>,
|
||||
children_query: Query<&Children, (With<Visibility>, With<InheritedVisibility>)>,
|
||||
) {
|
||||
for (entity, visibility, child_of, children) in &changed {
|
||||
let is_visible = match visibility {
|
||||
Visibility::Visible => true,
|
||||
Visibility::Hidden => false,
|
||||
// fall back to true if no parent is found or parent lacks components
|
||||
Visibility::Inherited => child_of
|
||||
.and_then(|c| visibility_query.get(c.parent()).ok())
|
||||
.is_none_or(|(_, x)| x.get()),
|
||||
};
|
||||
let (_, mut inherited_visibility) = visibility_query
|
||||
.get_mut(entity)
|
||||
.expect("With<InheritedVisibility> ensures this query will return a value");
|
||||
|
||||
// Only update the visibility if it has changed.
|
||||
// This will also prevent the visibility from propagating multiple times in the same frame
|
||||
// if this entity's visibility has been updated recursively by its parent.
|
||||
if inherited_visibility.get() != is_visible {
|
||||
inherited_visibility.0 = is_visible;
|
||||
|
||||
// Recursively update the visibility of each child.
|
||||
for &child in children.into_iter().flatten() {
|
||||
let _ =
|
||||
propagate_recursive(is_visible, child, &mut visibility_query, &children_query);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn propagate_recursive(
|
||||
parent_is_visible: bool,
|
||||
entity: Entity,
|
||||
visibility_query: &mut Query<(&Visibility, &mut InheritedVisibility)>,
|
||||
children_query: &Query<&Children, (With<Visibility>, With<InheritedVisibility>)>,
|
||||
// BLOCKED: https://github.com/rust-lang/rust/issues/31436
|
||||
// We use a result here to use the `?` operator. Ideally we'd use a try block instead
|
||||
) -> Result<(), ()> {
|
||||
// Get the visibility components for the current entity.
|
||||
// If the entity does not have the required components, just return early.
|
||||
let (visibility, mut inherited_visibility) = visibility_query.get_mut(entity).map_err(drop)?;
|
||||
|
||||
let is_visible = match visibility {
|
||||
Visibility::Visible => true,
|
||||
Visibility::Hidden => false,
|
||||
Visibility::Inherited => parent_is_visible,
|
||||
};
|
||||
|
||||
// Only update the visibility if it has changed.
|
||||
if inherited_visibility.get() != is_visible {
|
||||
inherited_visibility.0 = is_visible;
|
||||
|
||||
// Recursively update the visibility of each child.
|
||||
for &child in children_query.get(entity).ok().into_iter().flatten() {
|
||||
let _ = propagate_recursive(is_visible, child, visibility_query, children_query);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stores all entities that were visible in the previous frame.
|
||||
///
|
||||
/// As systems that check visibility judge entities visible, they remove them
|
||||
/// from this set. Afterward, the `mark_newly_hidden_entities_invisible` system
|
||||
/// runs and marks every mesh still remaining in this set as hidden.
|
||||
#[derive(Resource, Default, Deref, DerefMut)]
|
||||
pub struct PreviousVisibleEntities(EntityHashSet);
|
||||
|
||||
/// Resets the view visibility of every entity.
|
||||
/// Entities that are visible will be marked as such later this frame
|
||||
/// by a [`VisibilitySystems::CheckVisibility`] system.
|
||||
fn reset_view_visibility(
|
||||
mut query: Query<(Entity, &ViewVisibility)>,
|
||||
mut previous_visible_entities: ResMut<PreviousVisibleEntities>,
|
||||
) {
|
||||
previous_visible_entities.clear();
|
||||
|
||||
query.iter_mut().for_each(|(entity, view_visibility)| {
|
||||
// Record the entities that were previously visible.
|
||||
if view_visibility.get() {
|
||||
previous_visible_entities.insert(entity);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// System updating the visibility of entities each frame.
|
||||
///
|
||||
/// The system is part of the [`VisibilitySystems::CheckVisibility`] set. Each
|
||||
/// frame, it updates the [`ViewVisibility`] of all entities, and for each view
|
||||
/// also compute the [`VisibleEntities`] for that view.
|
||||
///
|
||||
/// To ensure that an entity is checked for visibility, make sure that it has a
|
||||
/// [`VisibilityClass`] component and that that component is nonempty.
|
||||
pub fn check_visibility(
|
||||
mut thread_queues: Local<Parallel<TypeIdMap<Vec<Entity>>>>,
|
||||
mut view_query: Query<(
|
||||
Entity,
|
||||
&mut VisibleEntities,
|
||||
&Frustum,
|
||||
Option<&RenderLayers>,
|
||||
&Camera,
|
||||
Has<NoCpuCulling>,
|
||||
)>,
|
||||
mut visible_aabb_query: Query<(
|
||||
Entity,
|
||||
&InheritedVisibility,
|
||||
&mut ViewVisibility,
|
||||
&VisibilityClass,
|
||||
Option<&RenderLayers>,
|
||||
Option<&Aabb>,
|
||||
&GlobalTransform,
|
||||
Has<NoFrustumCulling>,
|
||||
Has<VisibilityRange>,
|
||||
)>,
|
||||
visible_entity_ranges: Option<Res<VisibleEntityRanges>>,
|
||||
mut previous_visible_entities: ResMut<PreviousVisibleEntities>,
|
||||
) {
|
||||
let visible_entity_ranges = visible_entity_ranges.as_deref();
|
||||
|
||||
for (view, mut visible_entities, frustum, maybe_view_mask, camera, no_cpu_culling) in
|
||||
&mut view_query
|
||||
{
|
||||
if !camera.is_active {
|
||||
continue;
|
||||
}
|
||||
|
||||
let view_mask = maybe_view_mask.unwrap_or_default();
|
||||
|
||||
visible_aabb_query.par_iter_mut().for_each_init(
|
||||
|| thread_queues.borrow_local_mut(),
|
||||
|queue, query_item| {
|
||||
let (
|
||||
entity,
|
||||
inherited_visibility,
|
||||
mut view_visibility,
|
||||
visibility_class,
|
||||
maybe_entity_mask,
|
||||
maybe_model_aabb,
|
||||
transform,
|
||||
no_frustum_culling,
|
||||
has_visibility_range,
|
||||
) = query_item;
|
||||
|
||||
// Skip computing visibility for entities that are configured to be hidden.
|
||||
// ViewVisibility has already been reset in `reset_view_visibility`.
|
||||
if !inherited_visibility.get() {
|
||||
return;
|
||||
}
|
||||
|
||||
let entity_mask = maybe_entity_mask.unwrap_or_default();
|
||||
if !view_mask.intersects(entity_mask) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If outside of the visibility range, cull.
|
||||
if has_visibility_range
|
||||
&& visible_entity_ranges.is_some_and(|visible_entity_ranges| {
|
||||
!visible_entity_ranges.entity_is_in_range_of_view(entity, view)
|
||||
})
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have an aabb, do frustum culling
|
||||
if !no_frustum_culling && !no_cpu_culling {
|
||||
if let Some(model_aabb) = maybe_model_aabb {
|
||||
let world_from_local = transform.affine();
|
||||
let model_sphere = Sphere {
|
||||
center: world_from_local.transform_point3a(model_aabb.center),
|
||||
radius: transform.radius_vec3a(model_aabb.half_extents),
|
||||
};
|
||||
// Do quick sphere-based frustum culling
|
||||
if !frustum.intersects_sphere(&model_sphere, false) {
|
||||
return;
|
||||
}
|
||||
// Do aabb-based frustum culling
|
||||
if !frustum.intersects_obb(model_aabb, &world_from_local, true, false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we don't trigger changed notifications
|
||||
// unnecessarily by checking whether the flag is set before
|
||||
// setting it.
|
||||
if !**view_visibility {
|
||||
view_visibility.set();
|
||||
}
|
||||
|
||||
// Add the entity to the queue for all visibility classes the
|
||||
// entity is in.
|
||||
for visibility_class_id in visibility_class.iter() {
|
||||
queue.entry(*visibility_class_id).or_default().push(entity);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
visible_entities.clear_all();
|
||||
|
||||
// Drain all the thread queues into the `visible_entities` list.
|
||||
for class_queues in thread_queues.iter_mut() {
|
||||
for (class, entities) in class_queues {
|
||||
let visible_entities_for_class = visible_entities.get_mut(*class);
|
||||
for entity in entities.drain(..) {
|
||||
// As we mark entities as visible, we remove them from the
|
||||
// `previous_visible_entities` list. At the end, all of the
|
||||
// entities remaining in `previous_visible_entities` will be
|
||||
// entities that were visible last frame but are no longer
|
||||
// visible this frame.
|
||||
previous_visible_entities.remove(&entity);
|
||||
|
||||
visible_entities_for_class.push(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks any entities that weren't judged visible this frame as invisible.
|
||||
///
|
||||
/// As visibility-determining systems run, they remove entities that they judge
|
||||
/// visible from [`PreviousVisibleEntities`]. At the end of visibility
|
||||
/// determination, all entities that remain in [`PreviousVisibleEntities`] must
|
||||
/// be invisible. This system goes through those entities and marks them newly
|
||||
/// invisible (which sets the change flag for them).
|
||||
fn mark_newly_hidden_entities_invisible(
|
||||
mut view_visibilities: Query<&mut ViewVisibility>,
|
||||
mut previous_visible_entities: ResMut<PreviousVisibleEntities>,
|
||||
) {
|
||||
// Whatever previous visible entities are left are entities that were
|
||||
// visible last frame but just became invisible.
|
||||
for entity in previous_visible_entities.drain() {
|
||||
if let Ok(mut view_visibility) = view_visibilities.get_mut(entity) {
|
||||
*view_visibility = ViewVisibility::HIDDEN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A generic component add hook that automatically adds the appropriate
|
||||
/// [`VisibilityClass`] to an entity.
|
||||
///
|
||||
/// This can be handy when creating custom renderable components. To use this
|
||||
/// hook, add it to your renderable component like this:
|
||||
///
|
||||
/// ```ignore
|
||||
/// #[derive(Component)]
|
||||
/// #[component(on_add = add_visibility_class::<MyComponent>)]
|
||||
/// struct MyComponent {
|
||||
/// ...
|
||||
/// }
|
||||
/// ```
|
||||
pub fn add_visibility_class<C>(
|
||||
mut world: DeferredWorld<'_>,
|
||||
HookContext { entity, .. }: HookContext,
|
||||
) where
|
||||
C: 'static,
|
||||
{
|
||||
if let Some(mut visibility_class) = world.get_mut::<VisibilityClass>(entity) {
|
||||
visibility_class.push(TypeId::of::<C>());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use bevy_app::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn visibility_propagation() {
|
||||
let mut app = App::new();
|
||||
app.add_systems(Update, visibility_propagate_system);
|
||||
|
||||
let root1 = app.world_mut().spawn(Visibility::Hidden).id();
|
||||
let root1_child1 = app.world_mut().spawn(Visibility::default()).id();
|
||||
let root1_child2 = app.world_mut().spawn(Visibility::Hidden).id();
|
||||
let root1_child1_grandchild1 = app.world_mut().spawn(Visibility::default()).id();
|
||||
let root1_child2_grandchild1 = app.world_mut().spawn(Visibility::default()).id();
|
||||
|
||||
app.world_mut()
|
||||
.entity_mut(root1)
|
||||
.add_children(&[root1_child1, root1_child2]);
|
||||
app.world_mut()
|
||||
.entity_mut(root1_child1)
|
||||
.add_children(&[root1_child1_grandchild1]);
|
||||
app.world_mut()
|
||||
.entity_mut(root1_child2)
|
||||
.add_children(&[root1_child2_grandchild1]);
|
||||
|
||||
let root2 = app.world_mut().spawn(Visibility::default()).id();
|
||||
let root2_child1 = app.world_mut().spawn(Visibility::default()).id();
|
||||
let root2_child2 = app.world_mut().spawn(Visibility::Hidden).id();
|
||||
let root2_child1_grandchild1 = app.world_mut().spawn(Visibility::default()).id();
|
||||
let root2_child2_grandchild1 = app.world_mut().spawn(Visibility::default()).id();
|
||||
|
||||
app.world_mut()
|
||||
.entity_mut(root2)
|
||||
.add_children(&[root2_child1, root2_child2]);
|
||||
app.world_mut()
|
||||
.entity_mut(root2_child1)
|
||||
.add_children(&[root2_child1_grandchild1]);
|
||||
app.world_mut()
|
||||
.entity_mut(root2_child2)
|
||||
.add_children(&[root2_child2_grandchild1]);
|
||||
|
||||
app.update();
|
||||
|
||||
let is_visible = |e: Entity| {
|
||||
app.world()
|
||||
.entity(e)
|
||||
.get::<InheritedVisibility>()
|
||||
.unwrap()
|
||||
.get()
|
||||
};
|
||||
assert!(
|
||||
!is_visible(root1),
|
||||
"invisibility propagates down tree from root"
|
||||
);
|
||||
assert!(
|
||||
!is_visible(root1_child1),
|
||||
"invisibility propagates down tree from root"
|
||||
);
|
||||
assert!(
|
||||
!is_visible(root1_child2),
|
||||
"invisibility propagates down tree from root"
|
||||
);
|
||||
assert!(
|
||||
!is_visible(root1_child1_grandchild1),
|
||||
"invisibility propagates down tree from root"
|
||||
);
|
||||
assert!(
|
||||
!is_visible(root1_child2_grandchild1),
|
||||
"invisibility propagates down tree from root"
|
||||
);
|
||||
|
||||
assert!(
|
||||
is_visible(root2),
|
||||
"visibility propagates down tree from root"
|
||||
);
|
||||
assert!(
|
||||
is_visible(root2_child1),
|
||||
"visibility propagates down tree from root"
|
||||
);
|
||||
assert!(
|
||||
!is_visible(root2_child2),
|
||||
"visibility propagates down tree from root, but local invisibility is preserved"
|
||||
);
|
||||
assert!(
|
||||
is_visible(root2_child1_grandchild1),
|
||||
"visibility propagates down tree from root"
|
||||
);
|
||||
assert!(
|
||||
!is_visible(root2_child2_grandchild1),
|
||||
"child's invisibility propagates down to grandchild"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_visibility_propagation_on_parent_change() {
|
||||
// Setup the world and schedule
|
||||
let mut app = App::new();
|
||||
|
||||
app.add_systems(Update, visibility_propagate_system);
|
||||
|
||||
// Create entities with visibility and hierarchy
|
||||
let parent1 = app.world_mut().spawn((Visibility::Hidden,)).id();
|
||||
let parent2 = app.world_mut().spawn((Visibility::Visible,)).id();
|
||||
let child1 = app.world_mut().spawn((Visibility::Inherited,)).id();
|
||||
let child2 = app.world_mut().spawn((Visibility::Inherited,)).id();
|
||||
|
||||
// Build hierarchy
|
||||
app.world_mut()
|
||||
.entity_mut(parent1)
|
||||
.add_children(&[child1, child2]);
|
||||
|
||||
// Run the system initially to set up visibility
|
||||
app.update();
|
||||
|
||||
// Change parent visibility to Hidden
|
||||
app.world_mut()
|
||||
.entity_mut(parent2)
|
||||
.insert(Visibility::Visible);
|
||||
// Simulate a change in the parent component
|
||||
app.world_mut().entity_mut(child2).insert(ChildOf(parent2)); // example of changing parent
|
||||
|
||||
// Run the system again to propagate changes
|
||||
app.update();
|
||||
|
||||
let is_visible = |e: Entity| {
|
||||
app.world()
|
||||
.entity(e)
|
||||
.get::<InheritedVisibility>()
|
||||
.unwrap()
|
||||
.get()
|
||||
};
|
||||
|
||||
// Retrieve and assert visibility
|
||||
|
||||
assert!(
|
||||
!is_visible(child1),
|
||||
"Child1 should inherit visibility from parent"
|
||||
);
|
||||
|
||||
assert!(
|
||||
is_visible(child2),
|
||||
"Child2 should inherit visibility from parent"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visibility_propagation_unconditional_visible() {
|
||||
use Visibility::{Hidden, Inherited, Visible};
|
||||
|
||||
let mut app = App::new();
|
||||
app.add_systems(Update, visibility_propagate_system);
|
||||
|
||||
let root1 = app.world_mut().spawn(Visible).id();
|
||||
let root1_child1 = app.world_mut().spawn(Inherited).id();
|
||||
let root1_child2 = app.world_mut().spawn(Hidden).id();
|
||||
let root1_child1_grandchild1 = app.world_mut().spawn(Visible).id();
|
||||
let root1_child2_grandchild1 = app.world_mut().spawn(Visible).id();
|
||||
|
||||
let root2 = app.world_mut().spawn(Inherited).id();
|
||||
let root3 = app.world_mut().spawn(Hidden).id();
|
||||
|
||||
app.world_mut()
|
||||
.entity_mut(root1)
|
||||
.add_children(&[root1_child1, root1_child2]);
|
||||
app.world_mut()
|
||||
.entity_mut(root1_child1)
|
||||
.add_children(&[root1_child1_grandchild1]);
|
||||
app.world_mut()
|
||||
.entity_mut(root1_child2)
|
||||
.add_children(&[root1_child2_grandchild1]);
|
||||
|
||||
app.update();
|
||||
|
||||
let is_visible = |e: Entity| {
|
||||
app.world()
|
||||
.entity(e)
|
||||
.get::<InheritedVisibility>()
|
||||
.unwrap()
|
||||
.get()
|
||||
};
|
||||
assert!(
|
||||
is_visible(root1),
|
||||
"an unconditionally visible root is visible"
|
||||
);
|
||||
assert!(
|
||||
is_visible(root1_child1),
|
||||
"an inheriting child of an unconditionally visible parent is visible"
|
||||
);
|
||||
assert!(
|
||||
!is_visible(root1_child2),
|
||||
"a hidden child on an unconditionally visible parent is hidden"
|
||||
);
|
||||
assert!(
|
||||
is_visible(root1_child1_grandchild1),
|
||||
"an unconditionally visible child of an inheriting parent is visible"
|
||||
);
|
||||
assert!(
|
||||
is_visible(root1_child2_grandchild1),
|
||||
"an unconditionally visible child of a hidden parent is visible"
|
||||
);
|
||||
assert!(is_visible(root2), "an inheriting root is visible");
|
||||
assert!(!is_visible(root3), "a hidden root is hidden");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visibility_propagation_change_detection() {
|
||||
let mut world = World::new();
|
||||
let mut schedule = Schedule::default();
|
||||
schedule.add_systems(visibility_propagate_system);
|
||||
|
||||
// Set up an entity hierarchy.
|
||||
|
||||
let id1 = world.spawn(Visibility::default()).id();
|
||||
|
||||
let id2 = world.spawn(Visibility::default()).id();
|
||||
world.entity_mut(id1).add_children(&[id2]);
|
||||
|
||||
let id3 = world.spawn(Visibility::Hidden).id();
|
||||
world.entity_mut(id2).add_children(&[id3]);
|
||||
|
||||
let id4 = world.spawn(Visibility::default()).id();
|
||||
world.entity_mut(id3).add_children(&[id4]);
|
||||
|
||||
// Test the hierarchy.
|
||||
|
||||
// Make sure the hierarchy is up-to-date.
|
||||
schedule.run(&mut world);
|
||||
world.clear_trackers();
|
||||
|
||||
let mut q = world.query::<Ref<InheritedVisibility>>();
|
||||
|
||||
assert!(!q.get(&world, id1).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id2).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id3).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id4).unwrap().is_changed());
|
||||
|
||||
world.clear_trackers();
|
||||
world.entity_mut(id1).insert(Visibility::Hidden);
|
||||
schedule.run(&mut world);
|
||||
|
||||
assert!(q.get(&world, id1).unwrap().is_changed());
|
||||
assert!(q.get(&world, id2).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id3).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id4).unwrap().is_changed());
|
||||
|
||||
world.clear_trackers();
|
||||
schedule.run(&mut world);
|
||||
|
||||
assert!(!q.get(&world, id1).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id2).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id3).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id4).unwrap().is_changed());
|
||||
|
||||
world.clear_trackers();
|
||||
world.entity_mut(id3).insert(Visibility::Inherited);
|
||||
schedule.run(&mut world);
|
||||
|
||||
assert!(!q.get(&world, id1).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id2).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id3).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id4).unwrap().is_changed());
|
||||
|
||||
world.clear_trackers();
|
||||
world.entity_mut(id2).insert(Visibility::Visible);
|
||||
schedule.run(&mut world);
|
||||
|
||||
assert!(!q.get(&world, id1).unwrap().is_changed());
|
||||
assert!(q.get(&world, id2).unwrap().is_changed());
|
||||
assert!(q.get(&world, id3).unwrap().is_changed());
|
||||
assert!(q.get(&world, id4).unwrap().is_changed());
|
||||
|
||||
world.clear_trackers();
|
||||
schedule.run(&mut world);
|
||||
|
||||
assert!(!q.get(&world, id1).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id2).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id3).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id4).unwrap().is_changed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visibility_propagation_with_invalid_parent() {
|
||||
let mut world = World::new();
|
||||
let mut schedule = Schedule::default();
|
||||
schedule.add_systems(visibility_propagate_system);
|
||||
|
||||
let parent = world.spawn(()).id();
|
||||
let child = world.spawn(Visibility::default()).id();
|
||||
world.entity_mut(parent).add_children(&[child]);
|
||||
|
||||
schedule.run(&mut world);
|
||||
world.clear_trackers();
|
||||
|
||||
let child_visible = world.entity(child).get::<InheritedVisibility>().unwrap().0;
|
||||
// defaults to same behavior of parent not found: visible = true
|
||||
assert!(child_visible);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_visibility_enum_size() {
|
||||
assert_eq!(1, size_of::<Visibility>());
|
||||
assert_eq!(1, size_of::<Option<Visibility>>());
|
||||
}
|
||||
}
|
||||
295
crates/bevy_camera/src/visibility/range.rs
Normal file
295
crates/bevy_camera/src/visibility/range.rs
Normal file
@ -0,0 +1,295 @@
|
||||
//! Specific distances from the camera in which entities are visible, also known
|
||||
//! as *hierarchical levels of detail* or *HLOD*s.
|
||||
|
||||
use core::{
|
||||
hash::{Hash, Hasher},
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
use bevy_app::{App, Plugin, PostUpdate};
|
||||
use bevy_ecs::{
|
||||
component::Component,
|
||||
entity::{Entity, EntityHashMap},
|
||||
query::With,
|
||||
reflect::ReflectComponent,
|
||||
resource::Resource,
|
||||
schedule::IntoScheduleConfigs as _,
|
||||
system::{Local, Query, ResMut},
|
||||
};
|
||||
use bevy_math::FloatOrd;
|
||||
use bevy_reflect::Reflect;
|
||||
use bevy_transform::components::GlobalTransform;
|
||||
use bevy_utils::Parallel;
|
||||
|
||||
use super::{check_visibility, VisibilitySystems};
|
||||
use crate::{camera::Camera, primitives::Aabb};
|
||||
|
||||
/// A plugin that enables [`VisibilityRange`]s, which allow entities to be
|
||||
/// hidden or shown based on distance to the camera.
|
||||
pub struct VisibilityRangePlugin;
|
||||
|
||||
impl Plugin for VisibilityRangePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.register_type::<VisibilityRange>()
|
||||
.init_resource::<VisibleEntityRanges>()
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
check_visibility_ranges
|
||||
.in_set(VisibilitySystems::CheckVisibility)
|
||||
.before(check_visibility),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Specifies the range of distances that this entity must be from the camera in
|
||||
/// order to be rendered.
|
||||
///
|
||||
/// This is also known as *hierarchical level of detail* or *HLOD*.
|
||||
///
|
||||
/// Use this component when you want to render a high-polygon mesh when the
|
||||
/// camera is close and a lower-polygon mesh when the camera is far away. This
|
||||
/// is a common technique for improving performance, because fine details are
|
||||
/// hard to see in a mesh at a distance. To avoid an artifact known as *popping*
|
||||
/// between levels, each level has a *margin*, within which the object
|
||||
/// transitions gradually from invisible to visible using a dithering effect.
|
||||
///
|
||||
/// You can also use this feature to replace multiple meshes with a single mesh
|
||||
/// when the camera is distant. This is the reason for the term "*hierarchical*
|
||||
/// level of detail". Reducing the number of meshes can be useful for reducing
|
||||
/// drawcall count. Note that you must place the [`VisibilityRange`] component
|
||||
/// on each entity you want to be part of a LOD group, as [`VisibilityRange`]
|
||||
/// isn't automatically propagated down to children.
|
||||
///
|
||||
/// A typical use of this feature might look like this:
|
||||
///
|
||||
/// | Entity | `start_margin` | `end_margin` |
|
||||
/// |-------------------------|----------------|--------------|
|
||||
/// | Root | N/A | N/A |
|
||||
/// | ├─ High-poly mesh | [0, 0) | [20, 25) |
|
||||
/// | ├─ Low-poly mesh | [20, 25) | [70, 75) |
|
||||
/// | └─ Billboard *imposter* | [70, 75) | [150, 160) |
|
||||
///
|
||||
/// With this setup, the user will see a high-poly mesh when the camera is
|
||||
/// closer than 20 units. As the camera zooms out, between 20 units to 25 units,
|
||||
/// the high-poly mesh will gradually fade to a low-poly mesh. When the camera
|
||||
/// is 70 to 75 units away, the low-poly mesh will fade to a single textured
|
||||
/// quad. And between 150 and 160 units, the object fades away entirely. Note
|
||||
/// that the `end_margin` of a higher LOD is always identical to the
|
||||
/// `start_margin` of the next lower LOD; this is important for the crossfade
|
||||
/// effect to function properly.
|
||||
#[derive(Component, Clone, PartialEq, Default, Reflect)]
|
||||
#[reflect(Component, PartialEq, Hash, Clone)]
|
||||
pub struct VisibilityRange {
|
||||
/// The range of distances, in world units, between which this entity will
|
||||
/// smoothly fade into view as the camera zooms out.
|
||||
///
|
||||
/// If the start and end of this range are identical, the transition will be
|
||||
/// abrupt, with no crossfading.
|
||||
///
|
||||
/// `start_margin.end` must be less than or equal to `end_margin.start`.
|
||||
pub start_margin: Range<f32>,
|
||||
|
||||
/// The range of distances, in world units, between which this entity will
|
||||
/// smoothly fade out of view as the camera zooms out.
|
||||
///
|
||||
/// If the start and end of this range are identical, the transition will be
|
||||
/// abrupt, with no crossfading.
|
||||
///
|
||||
/// `end_margin.start` must be greater than or equal to `start_margin.end`.
|
||||
pub end_margin: Range<f32>,
|
||||
|
||||
/// If set to true, Bevy will use the center of the axis-aligned bounding
|
||||
/// box ([`Aabb`]) as the position of the mesh for the purposes of
|
||||
/// visibility range computation.
|
||||
///
|
||||
/// Otherwise, if this field is set to false, Bevy will use the origin of
|
||||
/// the mesh as the mesh's position.
|
||||
///
|
||||
/// Usually you will want to leave this set to false, because different LODs
|
||||
/// may have different AABBs, and smooth crossfades between LOD levels
|
||||
/// require that all LODs of a mesh be at *precisely* the same position. If
|
||||
/// you aren't using crossfading, however, and your meshes aren't centered
|
||||
/// around their origins, then this flag may be useful.
|
||||
pub use_aabb: bool,
|
||||
}
|
||||
|
||||
impl Eq for VisibilityRange {}
|
||||
|
||||
impl Hash for VisibilityRange {
|
||||
fn hash<H>(&self, state: &mut H)
|
||||
where
|
||||
H: Hasher,
|
||||
{
|
||||
FloatOrd(self.start_margin.start).hash(state);
|
||||
FloatOrd(self.start_margin.end).hash(state);
|
||||
FloatOrd(self.end_margin.start).hash(state);
|
||||
FloatOrd(self.end_margin.end).hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl VisibilityRange {
|
||||
/// Creates a new *abrupt* visibility range, with no crossfade.
|
||||
///
|
||||
/// There will be no crossfade; the object will immediately vanish if the
|
||||
/// camera is closer than `start` units or farther than `end` units from the
|
||||
/// model.
|
||||
///
|
||||
/// The `start` value must be less than or equal to the `end` value.
|
||||
#[inline]
|
||||
pub fn abrupt(start: f32, end: f32) -> Self {
|
||||
Self {
|
||||
start_margin: start..start,
|
||||
end_margin: end..end,
|
||||
use_aabb: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if both the start and end transitions for this range are
|
||||
/// abrupt: that is, there is no crossfading.
|
||||
#[inline]
|
||||
pub fn is_abrupt(&self) -> bool {
|
||||
self.start_margin.start == self.start_margin.end
|
||||
&& self.end_margin.start == self.end_margin.end
|
||||
}
|
||||
|
||||
/// Returns true if the object will be visible at all, given a camera
|
||||
/// `camera_distance` units away.
|
||||
///
|
||||
/// Any amount of visibility, even with the heaviest dithering applied, is
|
||||
/// considered visible according to this check.
|
||||
#[inline]
|
||||
pub fn is_visible_at_all(&self, camera_distance: f32) -> bool {
|
||||
camera_distance >= self.start_margin.start && camera_distance < self.end_margin.end
|
||||
}
|
||||
|
||||
/// Returns true if the object is completely invisible, given a camera
|
||||
/// `camera_distance` units away.
|
||||
///
|
||||
/// This is equivalent to `!VisibilityRange::is_visible_at_all()`.
|
||||
#[inline]
|
||||
pub fn is_culled(&self, camera_distance: f32) -> bool {
|
||||
!self.is_visible_at_all(camera_distance)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores which entities are in within the [`VisibilityRange`]s of views.
|
||||
///
|
||||
/// This doesn't store the results of frustum or occlusion culling; use
|
||||
/// [`super::ViewVisibility`] for that. Thus entities in this list may not
|
||||
/// actually be visible.
|
||||
///
|
||||
/// For efficiency, these tables only store entities that have
|
||||
/// [`VisibilityRange`] components. Entities without such a component won't be
|
||||
/// in these tables at all.
|
||||
///
|
||||
/// The table is indexed by entity and stores a 32-bit bitmask with one bit for
|
||||
/// each camera, where a 0 bit corresponds to "out of range" and a 1 bit
|
||||
/// corresponds to "in range". Hence it's limited to storing information for 32
|
||||
/// views.
|
||||
#[derive(Resource, Default)]
|
||||
pub struct VisibleEntityRanges {
|
||||
/// Stores which bit index each view corresponds to.
|
||||
views: EntityHashMap<u8>,
|
||||
|
||||
/// Stores a bitmask in which each view has a single bit.
|
||||
///
|
||||
/// A 0 bit for a view corresponds to "out of range"; a 1 bit corresponds to
|
||||
/// "in range".
|
||||
entities: EntityHashMap<u32>,
|
||||
}
|
||||
|
||||
impl VisibleEntityRanges {
|
||||
/// Clears out the [`VisibleEntityRanges`] in preparation for a new frame.
|
||||
fn clear(&mut self) {
|
||||
self.views.clear();
|
||||
self.entities.clear();
|
||||
}
|
||||
|
||||
/// Returns true if the entity is in range of the given camera.
|
||||
///
|
||||
/// This only checks [`VisibilityRange`]s and doesn't perform any frustum or
|
||||
/// occlusion culling. Thus the entity might not *actually* be visible.
|
||||
///
|
||||
/// The entity is assumed to have a [`VisibilityRange`] component. If the
|
||||
/// entity doesn't have that component, this method will return false.
|
||||
#[inline]
|
||||
pub fn entity_is_in_range_of_view(&self, entity: Entity, view: Entity) -> bool {
|
||||
let Some(visibility_bitmask) = self.entities.get(&entity) else {
|
||||
return false;
|
||||
};
|
||||
let Some(view_index) = self.views.get(&view) else {
|
||||
return false;
|
||||
};
|
||||
(visibility_bitmask & (1 << view_index)) != 0
|
||||
}
|
||||
|
||||
/// Returns true if the entity is in range of any view.
|
||||
///
|
||||
/// This only checks [`VisibilityRange`]s and doesn't perform any frustum or
|
||||
/// occlusion culling. Thus the entity might not *actually* be visible.
|
||||
///
|
||||
/// The entity is assumed to have a [`VisibilityRange`] component. If the
|
||||
/// entity doesn't have that component, this method will return false.
|
||||
#[inline]
|
||||
pub fn entity_is_in_range_of_any_view(&self, entity: Entity) -> bool {
|
||||
self.entities.contains_key(&entity)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks all entities against all views in order to determine which entities
|
||||
/// with [`VisibilityRange`]s are potentially visible.
|
||||
///
|
||||
/// This only checks distance from the camera and doesn't frustum or occlusion
|
||||
/// cull.
|
||||
pub fn check_visibility_ranges(
|
||||
mut visible_entity_ranges: ResMut<VisibleEntityRanges>,
|
||||
view_query: Query<(Entity, &GlobalTransform), With<Camera>>,
|
||||
mut par_local: Local<Parallel<Vec<(Entity, u32)>>>,
|
||||
entity_query: Query<(Entity, &GlobalTransform, Option<&Aabb>, &VisibilityRange)>,
|
||||
) {
|
||||
visible_entity_ranges.clear();
|
||||
|
||||
// Early out if the visibility range feature isn't in use.
|
||||
if entity_query.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Assign an index to each view.
|
||||
let mut views = vec![];
|
||||
for (view, view_transform) in view_query.iter().take(32) {
|
||||
let view_index = views.len() as u8;
|
||||
visible_entity_ranges.views.insert(view, view_index);
|
||||
views.push((view, view_transform.translation_vec3a()));
|
||||
}
|
||||
|
||||
// Check each entity/view pair. Only consider entities with
|
||||
// [`VisibilityRange`] components.
|
||||
entity_query.par_iter().for_each(
|
||||
|(entity, entity_transform, maybe_model_aabb, visibility_range)| {
|
||||
let mut visibility = 0;
|
||||
for (view_index, &(_, view_position)) in views.iter().enumerate() {
|
||||
// If instructed to use the AABB and the model has one, use its
|
||||
// center as the model position. Otherwise, use the model's
|
||||
// translation.
|
||||
let model_position = match (visibility_range.use_aabb, maybe_model_aabb) {
|
||||
(true, Some(model_aabb)) => entity_transform
|
||||
.affine()
|
||||
.transform_point3a(model_aabb.center),
|
||||
_ => entity_transform.translation_vec3a(),
|
||||
};
|
||||
|
||||
if visibility_range.is_visible_at_all((view_position - model_position).length()) {
|
||||
visibility |= 1 << view_index;
|
||||
}
|
||||
}
|
||||
|
||||
// Invisible entities have no entry at all in the hash map. This speeds
|
||||
// up checks slightly in this common case.
|
||||
if visibility != 0 {
|
||||
par_local.borrow_local_mut().push((entity, visibility));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
visible_entity_ranges.entities.extend(par_local.drain());
|
||||
}
|
||||
@ -7,7 +7,7 @@ use bevy_platform::collections::HashSet;
|
||||
use bevy_platform::time::Instant;
|
||||
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
||||
use bevy_render::{
|
||||
camera::{Camera, ExtractedCamera},
|
||||
camera::{Camera, ExtractedCamera, ToNormalizedRenderTarget as _},
|
||||
extract_component::{ExtractComponent, ExtractComponentPlugin},
|
||||
load_shader_library,
|
||||
render_graph::{RenderGraphExt, ViewNodeRunner},
|
||||
|
||||
@ -94,10 +94,10 @@ use core::{
|
||||
note = "consider annotating `{Self}` with `#[derive(Event)]`"
|
||||
)]
|
||||
pub trait Event: Send + Sync + 'static {
|
||||
/// Generates the [`ComponentId`] for this event type.
|
||||
/// Generates the [`EventKey`] for this event type.
|
||||
///
|
||||
/// If this type has already been registered,
|
||||
/// this will return the existing [`ComponentId`].
|
||||
/// this will return the existing [`EventKey`].
|
||||
///
|
||||
/// This is used by various dynamically typed observer APIs,
|
||||
/// such as [`World::trigger_targets_dynamic`].
|
||||
@ -105,12 +105,12 @@ pub trait Event: Send + Sync + 'static {
|
||||
/// # Warning
|
||||
///
|
||||
/// This method should not be overridden by implementers,
|
||||
/// and should always correspond to the implementation of [`component_id`](Event::component_id).
|
||||
fn register_component_id(world: &mut World) -> ComponentId {
|
||||
world.register_component::<EventWrapperComponent<Self>>()
|
||||
/// and should always correspond to the implementation of [`event_key`](Event::event_key).
|
||||
fn register_event_key(world: &mut World) -> EventKey {
|
||||
EventKey(world.register_component::<EventWrapperComponent<Self>>())
|
||||
}
|
||||
|
||||
/// Fetches the [`ComponentId`] for this event type,
|
||||
/// Fetches the [`EventKey`] for this event type,
|
||||
/// if it has already been generated.
|
||||
///
|
||||
/// This is used by various dynamically typed observer APIs,
|
||||
@ -119,9 +119,12 @@ pub trait Event: Send + Sync + 'static {
|
||||
/// # Warning
|
||||
///
|
||||
/// This method should not be overridden by implementers,
|
||||
/// and should always correspond to the implementation of [`register_component_id`](Event::register_component_id).
|
||||
fn component_id(world: &World) -> Option<ComponentId> {
|
||||
world.component_id::<EventWrapperComponent<Self>>()
|
||||
/// and should always correspond to the implementation of
|
||||
/// [`register_event_key`](Event::register_event_key).
|
||||
fn event_key(world: &World) -> Option<EventKey> {
|
||||
world
|
||||
.component_id::<EventWrapperComponent<Self>>()
|
||||
.map(EventKey)
|
||||
}
|
||||
}
|
||||
|
||||
@ -421,3 +424,19 @@ pub(crate) struct EventInstance<E: BufferedEvent> {
|
||||
pub event_id: EventId<E>,
|
||||
pub event: E,
|
||||
}
|
||||
|
||||
/// A unique identifier for an [`Event`], used by [observers].
|
||||
///
|
||||
/// You can look up the key for your event by calling the [`Event::event_key`] method.
|
||||
///
|
||||
/// [observers]: crate::observer
|
||||
#[derive(Debug, Copy, Clone, Hash, Ord, PartialOrd, Eq, PartialEq)]
|
||||
pub struct EventKey(pub(crate) ComponentId);
|
||||
|
||||
impl EventKey {
|
||||
/// Returns the internal [`ComponentId`].
|
||||
#[inline]
|
||||
pub(crate) fn component_id(&self) -> ComponentId {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ mod update;
|
||||
mod writer;
|
||||
|
||||
pub(crate) use base::EventInstance;
|
||||
pub use base::{BufferedEvent, EntityEvent, Event, EventId};
|
||||
pub use base::{BufferedEvent, EntityEvent, Event, EventId, EventKey};
|
||||
pub use bevy_ecs_macros::{BufferedEvent, EntityEvent, Event};
|
||||
pub use collections::{Events, SendBatchIds};
|
||||
pub use event_cursor::EventCursor;
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
use alloc::vec::Vec;
|
||||
use bevy_ecs::{
|
||||
change_detection::{DetectChangesMut, MutUntyped},
|
||||
component::{ComponentId, Tick},
|
||||
event::{BufferedEvent, Events},
|
||||
component::Tick,
|
||||
event::{BufferedEvent, EventKey, Events},
|
||||
resource::Resource,
|
||||
world::World,
|
||||
};
|
||||
|
||||
#[doc(hidden)]
|
||||
struct RegisteredEvent {
|
||||
component_id: ComponentId,
|
||||
event_key: EventKey,
|
||||
// Required to flush the secondary buffer and drop events even if left unchanged.
|
||||
previously_updated: bool,
|
||||
// SAFETY: The component ID and the function must be used to fetch the Events<T> resource
|
||||
@ -51,7 +51,7 @@ impl EventRegistry {
|
||||
let component_id = world.init_resource::<Events<T>>();
|
||||
let mut registry = world.get_resource_or_init::<Self>();
|
||||
registry.event_updates.push(RegisteredEvent {
|
||||
component_id,
|
||||
event_key: EventKey(component_id),
|
||||
previously_updated: false,
|
||||
update: |ptr| {
|
||||
// SAFETY: The resource was initialized with the type Events<T>.
|
||||
@ -66,7 +66,9 @@ impl EventRegistry {
|
||||
pub fn run_updates(&mut self, world: &mut World, last_change_tick: Tick) {
|
||||
for registered_event in &mut self.event_updates {
|
||||
// Bypass the type ID -> Component ID lookup with the cached component ID.
|
||||
if let Some(events) = world.get_resource_mut_by_id(registered_event.component_id) {
|
||||
if let Some(events) =
|
||||
world.get_resource_mut_by_id(registered_event.event_key.component_id())
|
||||
{
|
||||
let has_changed = events.has_changed_since(last_change_tick);
|
||||
if registered_event.previously_updated || has_changed {
|
||||
// SAFETY: The update function pointer is called with the resource
|
||||
@ -87,7 +89,7 @@ impl EventRegistry {
|
||||
let mut registry = world.get_resource_or_init::<Self>();
|
||||
registry
|
||||
.event_updates
|
||||
.retain(|e| e.component_id != component_id);
|
||||
.retain(|e| e.event_key.component_id() != component_id);
|
||||
world.remove_resource::<Events<T>>();
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,7 +79,8 @@ pub mod prelude {
|
||||
entity::{ContainsEntity, Entity, EntityMapper},
|
||||
error::{BevyError, Result},
|
||||
event::{
|
||||
BufferedEvent, EntityEvent, Event, EventMutator, EventReader, EventWriter, Events,
|
||||
BufferedEvent, EntityEvent, Event, EventKey, EventMutator, EventReader, EventWriter,
|
||||
Events,
|
||||
},
|
||||
hierarchy::{ChildOf, ChildSpawner, ChildSpawnerCommands, Children},
|
||||
lifecycle::{
|
||||
|
||||
@ -55,7 +55,7 @@ use crate::{
|
||||
entity::Entity,
|
||||
event::{
|
||||
BufferedEvent, EntityEvent, Event, EventCursor, EventId, EventIterator,
|
||||
EventIteratorWithId, Events,
|
||||
EventIteratorWithId, EventKey, Events,
|
||||
},
|
||||
query::FilteredAccessSet,
|
||||
relationship::RelationshipHookMode,
|
||||
@ -314,16 +314,16 @@ impl ComponentHooks {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`ComponentId`] for [`Add`]
|
||||
pub const ADD: ComponentId = ComponentId::new(0);
|
||||
/// [`ComponentId`] for [`Insert`]
|
||||
pub const INSERT: ComponentId = ComponentId::new(1);
|
||||
/// [`ComponentId`] for [`Replace`]
|
||||
pub const REPLACE: ComponentId = ComponentId::new(2);
|
||||
/// [`ComponentId`] for [`Remove`]
|
||||
pub const REMOVE: ComponentId = ComponentId::new(3);
|
||||
/// [`ComponentId`] for [`Despawn`]
|
||||
pub const DESPAWN: ComponentId = ComponentId::new(4);
|
||||
/// [`EventKey`] for [`Add`]
|
||||
pub const ADD: EventKey = EventKey(ComponentId::new(0));
|
||||
/// [`EventKey`] for [`Insert`]
|
||||
pub const INSERT: EventKey = EventKey(ComponentId::new(1));
|
||||
/// [`EventKey`] for [`Replace`]
|
||||
pub const REPLACE: EventKey = EventKey(ComponentId::new(2));
|
||||
/// [`EventKey`] for [`Remove`]
|
||||
pub const REMOVE: EventKey = EventKey(ComponentId::new(3));
|
||||
/// [`EventKey`] for [`Despawn`]
|
||||
pub const DESPAWN: EventKey = EventKey(ComponentId::new(4));
|
||||
|
||||
/// Trigger emitted when a component is inserted onto an entity that does not already have that
|
||||
/// component. Runs before `Insert`.
|
||||
|
||||
@ -37,44 +37,44 @@ pub struct Observers {
|
||||
remove: CachedObservers,
|
||||
despawn: CachedObservers,
|
||||
// Map from trigger type to set of observers listening to that trigger
|
||||
cache: HashMap<ComponentId, CachedObservers>,
|
||||
cache: HashMap<EventKey, CachedObservers>,
|
||||
}
|
||||
|
||||
impl Observers {
|
||||
pub(crate) fn get_observers_mut(&mut self, event_type: ComponentId) -> &mut CachedObservers {
|
||||
pub(crate) fn get_observers_mut(&mut self, event_key: EventKey) -> &mut CachedObservers {
|
||||
use crate::lifecycle::*;
|
||||
|
||||
match event_type {
|
||||
match event_key {
|
||||
ADD => &mut self.add,
|
||||
INSERT => &mut self.insert,
|
||||
REPLACE => &mut self.replace,
|
||||
REMOVE => &mut self.remove,
|
||||
DESPAWN => &mut self.despawn,
|
||||
_ => self.cache.entry(event_type).or_default(),
|
||||
_ => self.cache.entry(event_key).or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to get the observers for the given `event_type`.
|
||||
/// Attempts to get the observers for the given `event_key`.
|
||||
///
|
||||
/// When accessing the observers for lifecycle events, such as [`Add`], [`Insert`], [`Replace`], [`Remove`], and [`Despawn`],
|
||||
/// use the [`ComponentId`] constants from the [`lifecycle`](crate::lifecycle) module.
|
||||
pub fn try_get_observers(&self, event_type: ComponentId) -> Option<&CachedObservers> {
|
||||
/// use the [`EventKey`] constants from the [`lifecycle`](crate::lifecycle) module.
|
||||
pub fn try_get_observers(&self, event_key: EventKey) -> Option<&CachedObservers> {
|
||||
use crate::lifecycle::*;
|
||||
|
||||
match event_type {
|
||||
match event_key {
|
||||
ADD => Some(&self.add),
|
||||
INSERT => Some(&self.insert),
|
||||
REPLACE => Some(&self.replace),
|
||||
REMOVE => Some(&self.remove),
|
||||
DESPAWN => Some(&self.despawn),
|
||||
_ => self.cache.get(&event_type),
|
||||
_ => self.cache.get(&event_key),
|
||||
}
|
||||
}
|
||||
|
||||
/// This will run the observers of the given `event_type`, targeting the given `entity` and `components`.
|
||||
/// This will run the observers of the given `event_key`, targeting the given `entity` and `components`.
|
||||
pub(crate) fn invoke<T>(
|
||||
mut world: DeferredWorld,
|
||||
event_type: ComponentId,
|
||||
event_key: EventKey,
|
||||
current_target: Option<Entity>,
|
||||
original_target: Option<Entity>,
|
||||
components: impl Iterator<Item = ComponentId> + Clone,
|
||||
@ -88,7 +88,7 @@ impl Observers {
|
||||
// SAFETY: There are no outstanding world references
|
||||
world.increment_trigger_id();
|
||||
let observers = world.observers();
|
||||
let Some(observers) = observers.try_get_observers(event_type) else {
|
||||
let Some(observers) = observers.try_get_observers(event_key) else {
|
||||
return;
|
||||
};
|
||||
// SAFETY: The only outstanding reference to world is `observers`
|
||||
@ -102,7 +102,7 @@ impl Observers {
|
||||
world.reborrow(),
|
||||
ObserverTrigger {
|
||||
observer,
|
||||
event_type,
|
||||
event_key,
|
||||
components: components.clone().collect(),
|
||||
current_target,
|
||||
original_target,
|
||||
@ -145,10 +145,10 @@ impl Observers {
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn is_archetype_cached(event_type: ComponentId) -> Option<ArchetypeFlags> {
|
||||
pub(crate) fn is_archetype_cached(event_key: EventKey) -> Option<ArchetypeFlags> {
|
||||
use crate::lifecycle::*;
|
||||
|
||||
match event_type {
|
||||
match event_key {
|
||||
ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER),
|
||||
INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER),
|
||||
REPLACE => Some(ArchetypeFlags::ON_REPLACE_OBSERVER),
|
||||
|
||||
@ -289,9 +289,9 @@ impl Observer {
|
||||
/// Observe the given `event`. This will cause the [`Observer`] to run whenever an event with the given [`ComponentId`]
|
||||
/// is triggered.
|
||||
/// # Safety
|
||||
/// The type of the `event` [`ComponentId`] _must_ match the actual value
|
||||
/// The type of the `event` [`EventKey`] _must_ match the actual value
|
||||
/// of the event passed into the observer system.
|
||||
pub unsafe fn with_event(mut self, event: ComponentId) -> Self {
|
||||
pub unsafe fn with_event(mut self, event: EventKey) -> Self {
|
||||
self.descriptor.events.push(event);
|
||||
self
|
||||
}
|
||||
@ -350,7 +350,7 @@ impl Component for Observer {
|
||||
#[derive(Default, Clone)]
|
||||
pub struct ObserverDescriptor {
|
||||
/// The events the observer is watching.
|
||||
pub(super) events: Vec<ComponentId>,
|
||||
pub(super) events: Vec<EventKey>,
|
||||
|
||||
/// The components the observer is watching.
|
||||
pub(super) components: Vec<ComponentId>,
|
||||
@ -362,9 +362,9 @@ pub struct ObserverDescriptor {
|
||||
impl ObserverDescriptor {
|
||||
/// Add the given `events` to the descriptor.
|
||||
/// # Safety
|
||||
/// The type of each [`ComponentId`] in `events` _must_ match the actual value
|
||||
/// The type of each [`EventKey`] in `events` _must_ match the actual value
|
||||
/// of the event passed into the observer.
|
||||
pub unsafe fn with_events(mut self, events: Vec<ComponentId>) -> Self {
|
||||
pub unsafe fn with_events(mut self, events: Vec<EventKey>) -> Self {
|
||||
self.events = events;
|
||||
self
|
||||
}
|
||||
@ -382,7 +382,7 @@ impl ObserverDescriptor {
|
||||
}
|
||||
|
||||
/// Returns the `events` that the observer is watching.
|
||||
pub fn events(&self) -> &[ComponentId] {
|
||||
pub fn events(&self) -> &[EventKey] {
|
||||
&self.events
|
||||
}
|
||||
|
||||
@ -410,13 +410,13 @@ fn hook_on_add<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
|
||||
HookContext { entity, .. }: HookContext,
|
||||
) {
|
||||
world.commands().queue(move |world: &mut World| {
|
||||
let event_id = E::register_component_id(world);
|
||||
let event_key = E::register_event_key(world);
|
||||
let mut components = alloc::vec![];
|
||||
B::component_ids(&mut world.components_registrator(), &mut |id| {
|
||||
components.push(id);
|
||||
});
|
||||
if let Some(mut observer) = world.get_mut::<Observer>(entity) {
|
||||
observer.descriptor.events.push(event_id);
|
||||
observer.descriptor.events.push(event_key);
|
||||
observer.descriptor.components.extend(components);
|
||||
|
||||
let system: &mut dyn Any = observer.system.as_mut();
|
||||
|
||||
@ -43,10 +43,10 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo
|
||||
.get_mut::<Observer>(observer_entity)
|
||||
.expect("Source observer entity must have Observer");
|
||||
observer_state.descriptor.entities.push(target);
|
||||
let event_types = observer_state.descriptor.events.clone();
|
||||
let event_keys = observer_state.descriptor.events.clone();
|
||||
let components = observer_state.descriptor.components.clone();
|
||||
for event_type in event_types {
|
||||
let observers = world.observers.get_observers_mut(event_type);
|
||||
for event_key in event_keys {
|
||||
let observers = world.observers.get_observers_mut(event_key);
|
||||
if components.is_empty() {
|
||||
if let Some(map) = observers.entity_observers.get(&source).cloned() {
|
||||
observers.entity_observers.insert(target, map);
|
||||
|
||||
@ -197,10 +197,10 @@ impl World {
|
||||
}
|
||||
|
||||
pub(crate) fn trigger_with_caller<E: Event>(&mut self, mut event: E, caller: MaybeLocation) {
|
||||
let event_id = E::register_component_id(self);
|
||||
// SAFETY: We just registered `event_id` with the type of `event`
|
||||
let event_key = E::register_event_key(self);
|
||||
// SAFETY: We just registered `event_key` with the type of `event`
|
||||
unsafe {
|
||||
self.trigger_dynamic_ref_with_caller(event_id, &mut event, caller);
|
||||
self.trigger_dynamic_ref_with_caller(event_key, &mut event, caller);
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,22 +210,22 @@ impl World {
|
||||
/// or use the event after it has been modified by observers.
|
||||
#[track_caller]
|
||||
pub fn trigger_ref<E: Event>(&mut self, event: &mut E) {
|
||||
let event_id = E::register_component_id(self);
|
||||
// SAFETY: We just registered `event_id` with the type of `event`
|
||||
unsafe { self.trigger_dynamic_ref_with_caller(event_id, event, MaybeLocation::caller()) };
|
||||
let event_key = E::register_event_key(self);
|
||||
// SAFETY: We just registered `event_key` with the type of `event`
|
||||
unsafe { self.trigger_dynamic_ref_with_caller(event_key, event, MaybeLocation::caller()) };
|
||||
}
|
||||
|
||||
unsafe fn trigger_dynamic_ref_with_caller<E: Event>(
|
||||
&mut self,
|
||||
event_id: ComponentId,
|
||||
event_key: EventKey,
|
||||
event_data: &mut E,
|
||||
caller: MaybeLocation,
|
||||
) {
|
||||
let mut world = DeferredWorld::from(self);
|
||||
// SAFETY: `event_data` is accessible as the type represented by `event_id`
|
||||
// SAFETY: `event_data` is accessible as the type represented by `event_key`
|
||||
unsafe {
|
||||
world.trigger_observers_with_data::<_, ()>(
|
||||
event_id,
|
||||
event_key,
|
||||
None,
|
||||
None,
|
||||
core::iter::empty::<ComponentId>(),
|
||||
@ -252,10 +252,10 @@ impl World {
|
||||
targets: impl TriggerTargets,
|
||||
caller: MaybeLocation,
|
||||
) {
|
||||
let event_id = E::register_component_id(self);
|
||||
// SAFETY: We just registered `event_id` with the type of `event`
|
||||
let event_key = E::register_event_key(self);
|
||||
// SAFETY: We just registered `event_key` with the type of `event`
|
||||
unsafe {
|
||||
self.trigger_targets_dynamic_ref_with_caller(event_id, &mut event, targets, caller);
|
||||
self.trigger_targets_dynamic_ref_with_caller(event_key, &mut event, targets, caller);
|
||||
}
|
||||
}
|
||||
|
||||
@ -270,9 +270,9 @@ impl World {
|
||||
event: &mut E,
|
||||
targets: impl TriggerTargets,
|
||||
) {
|
||||
let event_id = E::register_component_id(self);
|
||||
// SAFETY: We just registered `event_id` with the type of `event`
|
||||
unsafe { self.trigger_targets_dynamic_ref(event_id, event, targets) };
|
||||
let event_key = E::register_event_key(self);
|
||||
// SAFETY: We just registered `event_key` with the type of `event`
|
||||
unsafe { self.trigger_targets_dynamic_ref(event_key, event, targets) };
|
||||
}
|
||||
|
||||
/// Triggers the given [`EntityEvent`] for the given `targets`, which will run any [`Observer`]s watching for it.
|
||||
@ -283,17 +283,17 @@ impl World {
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Caller must ensure that `event_data` is accessible as the type represented by `event_id`.
|
||||
/// Caller must ensure that `event_data` is accessible as the type represented by `event_key`.
|
||||
#[track_caller]
|
||||
pub unsafe fn trigger_targets_dynamic<E: EntityEvent, Targets: TriggerTargets>(
|
||||
&mut self,
|
||||
event_id: ComponentId,
|
||||
event_key: EventKey,
|
||||
mut event_data: E,
|
||||
targets: Targets,
|
||||
) {
|
||||
// SAFETY: `event_data` is accessible as the type represented by `event_id`
|
||||
// SAFETY: `event_data` is accessible as the type represented by `event_key`
|
||||
unsafe {
|
||||
self.trigger_targets_dynamic_ref(event_id, &mut event_data, targets);
|
||||
self.trigger_targets_dynamic_ref(event_key, &mut event_data, targets);
|
||||
};
|
||||
}
|
||||
|
||||
@ -305,16 +305,16 @@ impl World {
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Caller must ensure that `event_data` is accessible as the type represented by `event_id`.
|
||||
/// Caller must ensure that `event_data` is accessible as the type represented by `event_key`.
|
||||
#[track_caller]
|
||||
pub unsafe fn trigger_targets_dynamic_ref<E: EntityEvent, Targets: TriggerTargets>(
|
||||
&mut self,
|
||||
event_id: ComponentId,
|
||||
event_key: EventKey,
|
||||
event_data: &mut E,
|
||||
targets: Targets,
|
||||
) {
|
||||
self.trigger_targets_dynamic_ref_with_caller(
|
||||
event_id,
|
||||
event_key,
|
||||
event_data,
|
||||
targets,
|
||||
MaybeLocation::caller(),
|
||||
@ -326,7 +326,7 @@ impl World {
|
||||
/// See `trigger_targets_dynamic_ref`
|
||||
unsafe fn trigger_targets_dynamic_ref_with_caller<E: EntityEvent, Targets: TriggerTargets>(
|
||||
&mut self,
|
||||
event_id: ComponentId,
|
||||
event_key: EventKey,
|
||||
event_data: &mut E,
|
||||
targets: Targets,
|
||||
caller: MaybeLocation,
|
||||
@ -334,10 +334,10 @@ impl World {
|
||||
let mut world = DeferredWorld::from(self);
|
||||
let mut entity_targets = targets.entities().peekable();
|
||||
if entity_targets.peek().is_none() {
|
||||
// SAFETY: `event_data` is accessible as the type represented by `event_id`
|
||||
// SAFETY: `event_data` is accessible as the type represented by `event_key`
|
||||
unsafe {
|
||||
world.trigger_observers_with_data::<_, E::Traversal>(
|
||||
event_id,
|
||||
event_key,
|
||||
None,
|
||||
None,
|
||||
targets.components(),
|
||||
@ -348,10 +348,10 @@ impl World {
|
||||
};
|
||||
} else {
|
||||
for target_entity in entity_targets {
|
||||
// SAFETY: `event_data` is accessible as the type represented by `event_id`
|
||||
// SAFETY: `event_data` is accessible as the type represented by `event_key`
|
||||
unsafe {
|
||||
world.trigger_observers_with_data::<_, E::Traversal>(
|
||||
event_id,
|
||||
event_key,
|
||||
Some(target_entity),
|
||||
Some(target_entity),
|
||||
targets.components(),
|
||||
@ -379,8 +379,8 @@ impl World {
|
||||
};
|
||||
let descriptor = &observer_state.descriptor;
|
||||
|
||||
for &event_type in &descriptor.events {
|
||||
let cache = observers.get_observers_mut(event_type);
|
||||
for &event_key in &descriptor.events {
|
||||
let cache = observers.get_observers_mut(event_key);
|
||||
|
||||
if descriptor.components.is_empty() && descriptor.entities.is_empty() {
|
||||
cache
|
||||
@ -400,7 +400,7 @@ impl World {
|
||||
.component_observers
|
||||
.entry(component)
|
||||
.or_insert_with(|| {
|
||||
if let Some(flag) = Observers::is_archetype_cached(event_type) {
|
||||
if let Some(flag) = Observers::is_archetype_cached(event_key) {
|
||||
archetypes.update_flags(component, flag, true);
|
||||
}
|
||||
CachedComponentObservers::default()
|
||||
@ -430,8 +430,8 @@ impl World {
|
||||
let archetypes = &mut self.archetypes;
|
||||
let observers = &mut self.observers;
|
||||
|
||||
for &event_type in &descriptor.events {
|
||||
let cache = observers.get_observers_mut(event_type);
|
||||
for &event_key in &descriptor.events {
|
||||
let cache = observers.get_observers_mut(event_key);
|
||||
if descriptor.components.is_empty() && descriptor.entities.is_empty() {
|
||||
cache.global_observers.remove(&entity);
|
||||
} else if descriptor.components.is_empty() {
|
||||
@ -470,7 +470,7 @@ impl World {
|
||||
&& observers.entity_component_observers.is_empty()
|
||||
{
|
||||
cache.component_observers.remove(component);
|
||||
if let Some(flag) = Observers::is_archetype_cached(event_type) {
|
||||
if let Some(flag) = Observers::is_archetype_cached(event_key) {
|
||||
if let Some(by_component) = archetypes.by_component.get(component) {
|
||||
for archetype in by_component.keys() {
|
||||
let archetype = &mut archetypes.archetypes[archetype.index()];
|
||||
@ -734,7 +734,7 @@ mod tests {
|
||||
fn observer_multiple_events() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<Order>();
|
||||
let on_remove = Remove::register_component_id(&mut world);
|
||||
let on_remove = Remove::register_event_key(&mut world);
|
||||
world.spawn(
|
||||
// SAFETY: Add and Remove are both unit types, so this is safe
|
||||
unsafe {
|
||||
@ -1008,7 +1008,7 @@ mod tests {
|
||||
fn observer_dynamic_trigger() {
|
||||
let mut world = World::new();
|
||||
world.init_resource::<Order>();
|
||||
let event_a = Remove::register_component_id(&mut world);
|
||||
let event_a = Remove::register_event_key(&mut world);
|
||||
|
||||
// SAFETY: we registered `event_a` above and it matches the type of EventA
|
||||
let observe = unsafe {
|
||||
|
||||
@ -49,8 +49,8 @@ impl<'w, E, B: Bundle> On<'w, E, B> {
|
||||
}
|
||||
|
||||
/// Returns the event type of this [`On`] instance.
|
||||
pub fn event_type(&self) -> ComponentId {
|
||||
self.trigger.event_type
|
||||
pub fn event_key(&self) -> EventKey {
|
||||
self.trigger.event_key
|
||||
}
|
||||
|
||||
/// Returns a reference to the triggered event.
|
||||
@ -182,7 +182,7 @@ pub struct ObserverTrigger {
|
||||
/// The [`Entity`] of the observer handling the trigger.
|
||||
pub observer: Entity,
|
||||
/// The [`Event`] the trigger targeted.
|
||||
pub event_type: ComponentId,
|
||||
pub event_key: EventKey,
|
||||
/// The [`ComponentId`]s the trigger targeted.
|
||||
pub components: SmallVec<[ComponentId; 2]>,
|
||||
/// The entity that the entity-event targeted, if any.
|
||||
|
||||
@ -7,7 +7,7 @@ use crate::{
|
||||
change_detection::{MaybeLocation, MutUntyped},
|
||||
component::{ComponentId, Mutable},
|
||||
entity::Entity,
|
||||
event::{BufferedEvent, EntityEvent, Event, EventId, Events, SendBatchIds},
|
||||
event::{BufferedEvent, EntityEvent, Event, EventId, EventKey, Events, SendBatchIds},
|
||||
lifecycle::{HookContext, INSERT, REPLACE},
|
||||
observer::{Observers, TriggerTargets},
|
||||
prelude::{Component, QueryState},
|
||||
@ -749,7 +749,7 @@ impl<'w> DeferredWorld<'w> {
|
||||
#[inline]
|
||||
pub(crate) unsafe fn trigger_observers(
|
||||
&mut self,
|
||||
event: ComponentId,
|
||||
event: EventKey,
|
||||
target: Option<Entity>,
|
||||
components: impl Iterator<Item = ComponentId> + Clone,
|
||||
caller: MaybeLocation,
|
||||
@ -773,7 +773,7 @@ impl<'w> DeferredWorld<'w> {
|
||||
#[inline]
|
||||
pub(crate) unsafe fn trigger_observers_with_data<E, T>(
|
||||
&mut self,
|
||||
event: ComponentId,
|
||||
event: EventKey,
|
||||
current_target: Option<Entity>,
|
||||
original_target: Option<Entity>,
|
||||
components: impl Iterator<Item = ComponentId> + Clone,
|
||||
|
||||
@ -152,19 +152,19 @@ impl World {
|
||||
#[inline]
|
||||
fn bootstrap(&mut self) {
|
||||
// The order that we register these events is vital to ensure that the constants are correct!
|
||||
let on_add = Add::register_component_id(self);
|
||||
let on_add = Add::register_event_key(self);
|
||||
assert_eq!(ADD, on_add);
|
||||
|
||||
let on_insert = Insert::register_component_id(self);
|
||||
let on_insert = Insert::register_event_key(self);
|
||||
assert_eq!(INSERT, on_insert);
|
||||
|
||||
let on_replace = Replace::register_component_id(self);
|
||||
let on_replace = Replace::register_event_key(self);
|
||||
assert_eq!(REPLACE, on_replace);
|
||||
|
||||
let on_remove = Remove::register_component_id(self);
|
||||
let on_remove = Remove::register_event_key(self);
|
||||
assert_eq!(REMOVE, on_remove);
|
||||
|
||||
let on_despawn = Despawn::register_component_id(self);
|
||||
let on_despawn = Despawn::register_event_key(self);
|
||||
assert_eq!(DESPAWN, on_despawn);
|
||||
|
||||
// This sets up `Disabled` as a disabling component, via the FromWorld impl
|
||||
@ -304,6 +304,7 @@ impl World {
|
||||
/// Returns a mutable reference to the [`ComponentHooks`] for a [`Component`] type.
|
||||
///
|
||||
/// Will panic if `T` exists in any archetypes.
|
||||
#[must_use]
|
||||
pub fn register_component_hooks<T: Component>(&mut self) -> &mut ComponentHooks {
|
||||
let index = self.register_component::<T>();
|
||||
assert!(!self.archetypes.archetypes.iter().any(|a| a.contains(index)), "Components hooks cannot be modified if the component already exists in an archetype, use register_component if {} may already be in use", core::any::type_name::<T>());
|
||||
|
||||
@ -34,6 +34,7 @@ serde = { version = "1", default-features = false, features = [
|
||||
hexasphere = "15.0"
|
||||
thiserror = { version = "2", default-features = false }
|
||||
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||
derive_more = { version = "2", default-features = false, features = ["from"] }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0.140"
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
use crate::{
|
||||
mesh::Mesh,
|
||||
view::{self, Visibility, VisibilityClass},
|
||||
};
|
||||
use crate::mesh::Mesh;
|
||||
use bevy_asset::{AsAssetId, AssetEvent, AssetId, Handle};
|
||||
use bevy_derive::{Deref, DerefMut};
|
||||
use bevy_ecs::{
|
||||
@ -42,8 +39,7 @@ use derive_more::derive::From;
|
||||
/// ```
|
||||
#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)]
|
||||
#[reflect(Component, Default, Clone, PartialEq)]
|
||||
#[require(Transform, Visibility, VisibilityClass)]
|
||||
#[component(on_add = view::add_visibility_class::<Mesh2d>)]
|
||||
#[require(Transform)]
|
||||
pub struct Mesh2d(pub Handle<Mesh>);
|
||||
|
||||
impl From<Mesh2d> for AssetId<Mesh> {
|
||||
@ -98,8 +94,7 @@ impl AsAssetId for Mesh2d {
|
||||
/// ```
|
||||
#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)]
|
||||
#[reflect(Component, Default, Clone, PartialEq)]
|
||||
#[require(Transform, Visibility, VisibilityClass)]
|
||||
#[component(on_add = view::add_visibility_class::<Mesh3d>)]
|
||||
#[require(Transform)]
|
||||
pub struct Mesh3d(pub Handle<Mesh>);
|
||||
|
||||
impl From<Mesh3d> for AssetId<Mesh> {
|
||||
@ -3,6 +3,7 @@
|
||||
extern crate alloc;
|
||||
extern crate core;
|
||||
|
||||
mod components;
|
||||
mod conversions;
|
||||
mod index;
|
||||
mod mesh;
|
||||
@ -12,6 +13,7 @@ pub mod primitives;
|
||||
pub mod skinning;
|
||||
mod vertex;
|
||||
use bitflags::bitflags;
|
||||
pub use components::*;
|
||||
pub use index::*;
|
||||
pub use mesh::*;
|
||||
pub use mikktspace::*;
|
||||
|
||||
@ -22,7 +22,7 @@ use bevy_input::{
|
||||
use bevy_math::Vec2;
|
||||
use bevy_platform::collections::{HashMap, HashSet};
|
||||
use bevy_reflect::prelude::*;
|
||||
use bevy_render::camera::RenderTarget;
|
||||
use bevy_render::camera::{RenderTarget, ToNormalizedRenderTarget as _};
|
||||
use bevy_window::{PrimaryWindow, WindowEvent, WindowRef};
|
||||
use tracing::debug;
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ use bevy_input::mouse::MouseScrollUnit;
|
||||
use bevy_math::Vec2;
|
||||
use bevy_platform::collections::HashMap;
|
||||
use bevy_reflect::prelude::*;
|
||||
use bevy_render::camera::{Camera, NormalizedRenderTarget};
|
||||
use bevy_render::camera::{Camera, NormalizedRenderTarget, ToNormalizedRenderTarget as _};
|
||||
use bevy_window::PrimaryWindow;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -76,6 +76,7 @@ bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", features = [
|
||||
bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev" }
|
||||
bevy_image = { path = "../bevy_image", version = "0.17.0-dev" }
|
||||
bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev" }
|
||||
bevy_camera = { path = "../bevy_camera", version = "0.17.0-dev" }
|
||||
bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [
|
||||
"std",
|
||||
"serialize",
|
||||
|
||||
668
crates/bevy_render/src/camera.rs
Normal file
668
crates/bevy_render/src/camera.rs
Normal file
@ -0,0 +1,668 @@
|
||||
use crate::{
|
||||
batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport},
|
||||
extract_component::{ExtractComponent, ExtractComponentPlugin},
|
||||
extract_resource::{ExtractResource, ExtractResourcePlugin},
|
||||
render_asset::RenderAssets,
|
||||
render_graph::{CameraDriverNode, InternedRenderSubGraph, RenderGraph, RenderSubGraph},
|
||||
render_resource::TextureView,
|
||||
sync_world::{RenderEntity, SyncToRenderWorld},
|
||||
texture::{GpuImage, ManualTextureViews},
|
||||
view::{
|
||||
ColorGrading, ExtractedView, ExtractedWindows, Hdr, Msaa, NoIndirectDrawing,
|
||||
RenderVisibleEntities, RetainedViewEntity, ViewUniformOffset,
|
||||
},
|
||||
Extract, ExtractSchedule, Render, RenderApp, RenderSystems,
|
||||
};
|
||||
|
||||
use bevy_app::{App, Plugin, PostStartup, PostUpdate};
|
||||
use bevy_asset::{AssetEvent, AssetEventSystems, AssetId, Assets};
|
||||
pub use bevy_camera::*;
|
||||
use bevy_camera::{
|
||||
primitives::Frustum,
|
||||
visibility::{RenderLayers, VisibleEntities},
|
||||
};
|
||||
use bevy_derive::{Deref, DerefMut};
|
||||
use bevy_ecs::{
|
||||
change_detection::DetectChanges,
|
||||
component::Component,
|
||||
entity::{ContainsEntity, Entity},
|
||||
event::EventReader,
|
||||
lifecycle::HookContext,
|
||||
prelude::With,
|
||||
query::Has,
|
||||
reflect::ReflectComponent,
|
||||
resource::Resource,
|
||||
schedule::IntoScheduleConfigs,
|
||||
system::{Commands, Query, Res, ResMut},
|
||||
world::DeferredWorld,
|
||||
};
|
||||
use bevy_image::Image;
|
||||
use bevy_math::{vec2, Mat4, URect, UVec2, UVec4, Vec2};
|
||||
use bevy_platform::collections::{HashMap, HashSet};
|
||||
use bevy_reflect::prelude::*;
|
||||
use bevy_transform::components::GlobalTransform;
|
||||
use bevy_window::{
|
||||
NormalizedWindowRef, PrimaryWindow, Window, WindowCreated, WindowResized,
|
||||
WindowScaleFactorChanged,
|
||||
};
|
||||
use derive_more::derive::From;
|
||||
use tracing::warn;
|
||||
use wgpu::TextureFormat;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CameraPlugin;
|
||||
|
||||
impl Plugin for CameraPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.register_type::<CameraRenderGraph>()
|
||||
.register_type::<TemporalJitter>()
|
||||
.register_type::<MipBias>()
|
||||
.register_required_components::<Camera, Msaa>()
|
||||
.register_required_components::<Camera, SyncToRenderWorld>()
|
||||
.add_plugins((
|
||||
ExtractResourcePlugin::<ClearColor>::default(),
|
||||
ExtractComponentPlugin::<CameraMainTextureUsages>::default(),
|
||||
bevy_camera::CameraPlugin,
|
||||
))
|
||||
.add_systems(PostStartup, camera_system.in_set(CameraUpdateSystems))
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
camera_system
|
||||
.in_set(CameraUpdateSystems)
|
||||
.before(AssetEventSystems)
|
||||
.before(visibility::update_frusta),
|
||||
);
|
||||
app.world_mut()
|
||||
.register_component_hooks::<Camera>()
|
||||
.on_add(warn_on_no_render_graph);
|
||||
|
||||
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||
render_app
|
||||
.init_resource::<SortedCameras>()
|
||||
.add_systems(ExtractSchedule, extract_cameras)
|
||||
.add_systems(Render, sort_cameras.in_set(RenderSystems::ManageViews));
|
||||
let camera_driver_node = CameraDriverNode::new(render_app.world_mut());
|
||||
let mut render_graph = render_app.world_mut().resource_mut::<RenderGraph>();
|
||||
render_graph.add_node(crate::graph::CameraDriverLabel, camera_driver_node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn warn_on_no_render_graph(world: DeferredWorld, HookContext { entity, caller, .. }: HookContext) {
|
||||
if !world.entity(entity).contains::<CameraRenderGraph>() {
|
||||
warn!("{}Entity {entity} has a `Camera` component, but it doesn't have a render graph configured. Consider adding a `Camera2d` or `Camera3d` component, or manually adding a `CameraRenderGraph` component if you need a custom render graph.", caller.map(|location|format!("{location}: ")).unwrap_or_default());
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtractResource for ClearColor {
|
||||
type Source = ClearColor;
|
||||
|
||||
fn extract_resource(source: &Self::Source) -> Self {
|
||||
source.clone()
|
||||
}
|
||||
}
|
||||
impl ExtractComponent for CameraMainTextureUsages {
|
||||
type QueryData = &'static Self;
|
||||
type QueryFilter = ();
|
||||
type Out = Self;
|
||||
|
||||
fn extract_component(
|
||||
item: bevy_ecs::query::QueryItem<'_, '_, Self::QueryData>,
|
||||
) -> Option<Self::Out> {
|
||||
Some(*item)
|
||||
}
|
||||
}
|
||||
|
||||
/// Configures the [`RenderGraph`] name assigned to be run for a given [`Camera`] entity.
|
||||
#[derive(Component, Debug, Deref, DerefMut, Reflect, Clone)]
|
||||
#[reflect(opaque)]
|
||||
#[reflect(Component, Debug, Clone)]
|
||||
pub struct CameraRenderGraph(InternedRenderSubGraph);
|
||||
|
||||
impl CameraRenderGraph {
|
||||
/// Creates a new [`CameraRenderGraph`] from any string-like type.
|
||||
#[inline]
|
||||
pub fn new<T: RenderSubGraph>(name: T) -> Self {
|
||||
Self(name.intern())
|
||||
}
|
||||
|
||||
/// Sets the graph name.
|
||||
#[inline]
|
||||
pub fn set<T: RenderSubGraph>(&mut self, name: T) {
|
||||
self.0 = name.intern();
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToNormalizedRenderTarget {
|
||||
/// Normalize the render target down to a more concrete value, mostly used for equality comparisons.
|
||||
fn normalize(&self, primary_window: Option<Entity>) -> Option<NormalizedRenderTarget>;
|
||||
}
|
||||
|
||||
impl ToNormalizedRenderTarget for RenderTarget {
|
||||
fn normalize(&self, primary_window: Option<Entity>) -> Option<NormalizedRenderTarget> {
|
||||
match self {
|
||||
RenderTarget::Window(window_ref) => window_ref
|
||||
.normalize(primary_window)
|
||||
.map(NormalizedRenderTarget::Window),
|
||||
RenderTarget::Image(handle) => Some(NormalizedRenderTarget::Image(handle.clone())),
|
||||
RenderTarget::TextureView(id) => Some(NormalizedRenderTarget::TextureView(*id)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalized version of the render target.
|
||||
///
|
||||
/// Once we have this we shouldn't need to resolve it down anymore.
|
||||
#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash, PartialOrd, Ord, From)]
|
||||
#[reflect(Clone, PartialEq, Hash)]
|
||||
pub enum NormalizedRenderTarget {
|
||||
/// Window to which the camera's view is rendered.
|
||||
Window(NormalizedWindowRef),
|
||||
/// Image to which the camera's view is rendered.
|
||||
Image(ImageRenderTarget),
|
||||
/// Texture View to which the camera's view is rendered.
|
||||
/// Useful when the texture view needs to be created outside of Bevy, for example OpenXR.
|
||||
TextureView(ManualTextureViewHandle),
|
||||
}
|
||||
|
||||
impl NormalizedRenderTarget {
|
||||
pub fn get_texture_view<'a>(
|
||||
&self,
|
||||
windows: &'a ExtractedWindows,
|
||||
images: &'a RenderAssets<GpuImage>,
|
||||
manual_texture_views: &'a ManualTextureViews,
|
||||
) -> Option<&'a TextureView> {
|
||||
match self {
|
||||
NormalizedRenderTarget::Window(window_ref) => windows
|
||||
.get(&window_ref.entity())
|
||||
.and_then(|window| window.swap_chain_texture_view.as_ref()),
|
||||
NormalizedRenderTarget::Image(image_target) => images
|
||||
.get(&image_target.handle)
|
||||
.map(|image| &image.texture_view),
|
||||
NormalizedRenderTarget::TextureView(id) => {
|
||||
manual_texture_views.get(id).map(|tex| &tex.texture_view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the [`TextureFormat`] of this render target, if it exists.
|
||||
pub fn get_texture_format<'a>(
|
||||
&self,
|
||||
windows: &'a ExtractedWindows,
|
||||
images: &'a RenderAssets<GpuImage>,
|
||||
manual_texture_views: &'a ManualTextureViews,
|
||||
) -> Option<TextureFormat> {
|
||||
match self {
|
||||
NormalizedRenderTarget::Window(window_ref) => windows
|
||||
.get(&window_ref.entity())
|
||||
.and_then(|window| window.swap_chain_texture_format),
|
||||
NormalizedRenderTarget::Image(image_target) => images
|
||||
.get(&image_target.handle)
|
||||
.map(|image| image.texture_format),
|
||||
NormalizedRenderTarget::TextureView(id) => {
|
||||
manual_texture_views.get(id).map(|tex| tex.format)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_render_target_info<'a>(
|
||||
&self,
|
||||
resolutions: impl IntoIterator<Item = (Entity, &'a Window)>,
|
||||
images: &Assets<Image>,
|
||||
manual_texture_views: &ManualTextureViews,
|
||||
) -> Option<RenderTargetInfo> {
|
||||
match self {
|
||||
NormalizedRenderTarget::Window(window_ref) => resolutions
|
||||
.into_iter()
|
||||
.find(|(entity, _)| *entity == window_ref.entity())
|
||||
.map(|(_, window)| RenderTargetInfo {
|
||||
physical_size: window.physical_size(),
|
||||
scale_factor: window.resolution.scale_factor(),
|
||||
}),
|
||||
NormalizedRenderTarget::Image(image_target) => {
|
||||
let image = images.get(&image_target.handle)?;
|
||||
Some(RenderTargetInfo {
|
||||
physical_size: image.size(),
|
||||
scale_factor: image_target.scale_factor.0,
|
||||
})
|
||||
}
|
||||
NormalizedRenderTarget::TextureView(id) => {
|
||||
manual_texture_views.get(id).map(|tex| RenderTargetInfo {
|
||||
physical_size: tex.size,
|
||||
scale_factor: 1.0,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this render target is contained in the given changed windows or images.
|
||||
fn is_changed(
|
||||
&self,
|
||||
changed_window_ids: &HashSet<Entity>,
|
||||
changed_image_handles: &HashSet<&AssetId<Image>>,
|
||||
) -> bool {
|
||||
match self {
|
||||
NormalizedRenderTarget::Window(window_ref) => {
|
||||
changed_window_ids.contains(&window_ref.entity())
|
||||
}
|
||||
NormalizedRenderTarget::Image(image_target) => {
|
||||
changed_image_handles.contains(&image_target.handle.id())
|
||||
}
|
||||
NormalizedRenderTarget::TextureView(_) => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// System in charge of updating a [`Camera`] when its window or projection changes.
|
||||
///
|
||||
/// The system detects window creation, resize, and scale factor change events to update the camera
|
||||
/// [`Projection`] if needed.
|
||||
///
|
||||
/// ## World Resources
|
||||
///
|
||||
/// [`Res<Assets<Image>>`](Assets<Image>) -- For cameras that render to an image, this resource is used to
|
||||
/// inspect information about the render target. This system will not access any other image assets.
|
||||
///
|
||||
/// [`OrthographicProjection`]: crate::camera::OrthographicProjection
|
||||
/// [`PerspectiveProjection`]: crate::camera::PerspectiveProjection
|
||||
pub fn camera_system(
|
||||
mut window_resized_events: EventReader<WindowResized>,
|
||||
mut window_created_events: EventReader<WindowCreated>,
|
||||
mut window_scale_factor_changed_events: EventReader<WindowScaleFactorChanged>,
|
||||
mut image_asset_events: EventReader<AssetEvent<Image>>,
|
||||
primary_window: Query<Entity, With<PrimaryWindow>>,
|
||||
windows: Query<(Entity, &Window)>,
|
||||
images: Res<Assets<Image>>,
|
||||
manual_texture_views: Res<ManualTextureViews>,
|
||||
mut cameras: Query<(&mut Camera, &mut Projection)>,
|
||||
) {
|
||||
let primary_window = primary_window.iter().next();
|
||||
|
||||
let mut changed_window_ids = <HashSet<_>>::default();
|
||||
changed_window_ids.extend(window_created_events.read().map(|event| event.window));
|
||||
changed_window_ids.extend(window_resized_events.read().map(|event| event.window));
|
||||
let scale_factor_changed_window_ids: HashSet<_> = window_scale_factor_changed_events
|
||||
.read()
|
||||
.map(|event| event.window)
|
||||
.collect();
|
||||
changed_window_ids.extend(scale_factor_changed_window_ids.clone());
|
||||
|
||||
let changed_image_handles: HashSet<&AssetId<Image>> = image_asset_events
|
||||
.read()
|
||||
.filter_map(|event| match event {
|
||||
AssetEvent::Modified { id } | AssetEvent::Added { id } => Some(id),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (mut camera, mut camera_projection) in &mut cameras {
|
||||
let mut viewport_size = camera
|
||||
.viewport
|
||||
.as_ref()
|
||||
.map(|viewport| viewport.physical_size);
|
||||
|
||||
if let Some(normalized_target) = &camera.target.normalize(primary_window) {
|
||||
if normalized_target.is_changed(&changed_window_ids, &changed_image_handles)
|
||||
|| camera.is_added()
|
||||
|| camera_projection.is_changed()
|
||||
|| camera.computed.old_viewport_size != viewport_size
|
||||
|| camera.computed.old_sub_camera_view != camera.sub_camera_view
|
||||
{
|
||||
let new_computed_target_info = normalized_target.get_render_target_info(
|
||||
windows,
|
||||
&images,
|
||||
&manual_texture_views,
|
||||
);
|
||||
// Check for the scale factor changing, and resize the viewport if needed.
|
||||
// This can happen when the window is moved between monitors with different DPIs.
|
||||
// Without this, the viewport will take a smaller portion of the window moved to
|
||||
// a higher DPI monitor.
|
||||
if normalized_target
|
||||
.is_changed(&scale_factor_changed_window_ids, &HashSet::default())
|
||||
{
|
||||
if let (Some(new_scale_factor), Some(old_scale_factor)) = (
|
||||
new_computed_target_info
|
||||
.as_ref()
|
||||
.map(|info| info.scale_factor),
|
||||
camera
|
||||
.computed
|
||||
.target_info
|
||||
.as_ref()
|
||||
.map(|info| info.scale_factor),
|
||||
) {
|
||||
let resize_factor = new_scale_factor / old_scale_factor;
|
||||
if let Some(ref mut viewport) = camera.viewport {
|
||||
let resize = |vec: UVec2| (vec.as_vec2() * resize_factor).as_uvec2();
|
||||
viewport.physical_position = resize(viewport.physical_position);
|
||||
viewport.physical_size = resize(viewport.physical_size);
|
||||
viewport_size = Some(viewport.physical_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
// This check is needed because when changing WindowMode to Fullscreen, the viewport may have invalid
|
||||
// arguments due to a sudden change on the window size to a lower value.
|
||||
// If the size of the window is lower, the viewport will match that lower value.
|
||||
if let Some(viewport) = &mut camera.viewport {
|
||||
let target_info = &new_computed_target_info;
|
||||
if let Some(target) = target_info {
|
||||
viewport.clamp_to_size(target.physical_size);
|
||||
}
|
||||
}
|
||||
camera.computed.target_info = new_computed_target_info;
|
||||
if let Some(size) = camera.logical_viewport_size() {
|
||||
if size.x != 0.0 && size.y != 0.0 {
|
||||
camera_projection.update(size.x, size.y);
|
||||
camera.computed.clip_from_view = match &camera.sub_camera_view {
|
||||
Some(sub_view) => {
|
||||
camera_projection.get_clip_from_view_for_sub(sub_view)
|
||||
}
|
||||
None => camera_projection.get_clip_from_view(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if camera.computed.old_viewport_size != viewport_size {
|
||||
camera.computed.old_viewport_size = viewport_size;
|
||||
}
|
||||
|
||||
if camera.computed.old_sub_camera_view != camera.sub_camera_view {
|
||||
camera.computed.old_sub_camera_view = camera.sub_camera_view;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
pub struct ExtractedCamera {
|
||||
pub target: Option<NormalizedRenderTarget>,
|
||||
pub physical_viewport_size: Option<UVec2>,
|
||||
pub physical_target_size: Option<UVec2>,
|
||||
pub viewport: Option<Viewport>,
|
||||
pub render_graph: InternedRenderSubGraph,
|
||||
pub order: isize,
|
||||
pub output_mode: CameraOutputMode,
|
||||
pub msaa_writeback: bool,
|
||||
pub clear_color: ClearColorConfig,
|
||||
pub sorted_camera_index_for_target: usize,
|
||||
pub exposure: f32,
|
||||
pub hdr: bool,
|
||||
}
|
||||
|
||||
pub fn extract_cameras(
|
||||
mut commands: Commands,
|
||||
query: Extract<
|
||||
Query<(
|
||||
Entity,
|
||||
RenderEntity,
|
||||
&Camera,
|
||||
&CameraRenderGraph,
|
||||
&GlobalTransform,
|
||||
&VisibleEntities,
|
||||
&Frustum,
|
||||
Has<Hdr>,
|
||||
Option<&ColorGrading>,
|
||||
Option<&Exposure>,
|
||||
Option<&TemporalJitter>,
|
||||
Option<&MipBias>,
|
||||
Option<&RenderLayers>,
|
||||
Option<&Projection>,
|
||||
Has<NoIndirectDrawing>,
|
||||
)>,
|
||||
>,
|
||||
primary_window: Extract<Query<Entity, With<PrimaryWindow>>>,
|
||||
gpu_preprocessing_support: Res<GpuPreprocessingSupport>,
|
||||
mapper: Extract<Query<&RenderEntity>>,
|
||||
) {
|
||||
let primary_window = primary_window.iter().next();
|
||||
for (
|
||||
main_entity,
|
||||
render_entity,
|
||||
camera,
|
||||
camera_render_graph,
|
||||
transform,
|
||||
visible_entities,
|
||||
frustum,
|
||||
hdr,
|
||||
color_grading,
|
||||
exposure,
|
||||
temporal_jitter,
|
||||
mip_bias,
|
||||
render_layers,
|
||||
projection,
|
||||
no_indirect_drawing,
|
||||
) in query.iter()
|
||||
{
|
||||
if !camera.is_active {
|
||||
commands.entity(render_entity).remove::<(
|
||||
ExtractedCamera,
|
||||
ExtractedView,
|
||||
RenderVisibleEntities,
|
||||
TemporalJitter,
|
||||
MipBias,
|
||||
RenderLayers,
|
||||
Projection,
|
||||
NoIndirectDrawing,
|
||||
ViewUniformOffset,
|
||||
)>();
|
||||
continue;
|
||||
}
|
||||
|
||||
let color_grading = color_grading.unwrap_or(&ColorGrading::default()).clone();
|
||||
|
||||
if let (
|
||||
Some(URect {
|
||||
min: viewport_origin,
|
||||
..
|
||||
}),
|
||||
Some(viewport_size),
|
||||
Some(target_size),
|
||||
) = (
|
||||
camera.physical_viewport_rect(),
|
||||
camera.physical_viewport_size(),
|
||||
camera.physical_target_size(),
|
||||
) {
|
||||
if target_size.x == 0 || target_size.y == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let render_visible_entities = RenderVisibleEntities {
|
||||
entities: visible_entities
|
||||
.entities
|
||||
.iter()
|
||||
.map(|(type_id, entities)| {
|
||||
let entities = entities
|
||||
.iter()
|
||||
.map(|entity| {
|
||||
let render_entity = mapper
|
||||
.get(*entity)
|
||||
.cloned()
|
||||
.map(|entity| entity.id())
|
||||
.unwrap_or(Entity::PLACEHOLDER);
|
||||
(render_entity, (*entity).into())
|
||||
})
|
||||
.collect();
|
||||
(*type_id, entities)
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let mut commands = commands.entity(render_entity);
|
||||
commands.insert((
|
||||
ExtractedCamera {
|
||||
target: camera.target.normalize(primary_window),
|
||||
viewport: camera.viewport.clone(),
|
||||
physical_viewport_size: Some(viewport_size),
|
||||
physical_target_size: Some(target_size),
|
||||
render_graph: camera_render_graph.0,
|
||||
order: camera.order,
|
||||
output_mode: camera.output_mode,
|
||||
msaa_writeback: camera.msaa_writeback,
|
||||
clear_color: camera.clear_color,
|
||||
// this will be set in sort_cameras
|
||||
sorted_camera_index_for_target: 0,
|
||||
exposure: exposure
|
||||
.map(Exposure::exposure)
|
||||
.unwrap_or_else(|| Exposure::default().exposure()),
|
||||
hdr,
|
||||
},
|
||||
ExtractedView {
|
||||
retained_view_entity: RetainedViewEntity::new(main_entity.into(), None, 0),
|
||||
clip_from_view: camera.clip_from_view(),
|
||||
world_from_view: *transform,
|
||||
clip_from_world: None,
|
||||
hdr,
|
||||
viewport: UVec4::new(
|
||||
viewport_origin.x,
|
||||
viewport_origin.y,
|
||||
viewport_size.x,
|
||||
viewport_size.y,
|
||||
),
|
||||
color_grading,
|
||||
},
|
||||
render_visible_entities,
|
||||
*frustum,
|
||||
));
|
||||
|
||||
if let Some(temporal_jitter) = temporal_jitter {
|
||||
commands.insert(temporal_jitter.clone());
|
||||
} else {
|
||||
commands.remove::<TemporalJitter>();
|
||||
}
|
||||
|
||||
if let Some(mip_bias) = mip_bias {
|
||||
commands.insert(mip_bias.clone());
|
||||
} else {
|
||||
commands.remove::<MipBias>();
|
||||
}
|
||||
|
||||
if let Some(render_layers) = render_layers {
|
||||
commands.insert(render_layers.clone());
|
||||
} else {
|
||||
commands.remove::<RenderLayers>();
|
||||
}
|
||||
|
||||
if let Some(perspective) = projection {
|
||||
commands.insert(perspective.clone());
|
||||
} else {
|
||||
commands.remove::<Projection>();
|
||||
}
|
||||
|
||||
if no_indirect_drawing
|
||||
|| !matches!(
|
||||
gpu_preprocessing_support.max_supported_mode,
|
||||
GpuPreprocessingMode::Culling
|
||||
)
|
||||
{
|
||||
commands.insert(NoIndirectDrawing);
|
||||
} else {
|
||||
commands.remove::<NoIndirectDrawing>();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Cameras sorted by their order field. This is updated in the [`sort_cameras`] system.
|
||||
#[derive(Resource, Default)]
|
||||
pub struct SortedCameras(pub Vec<SortedCamera>);
|
||||
|
||||
pub struct SortedCamera {
|
||||
pub entity: Entity,
|
||||
pub order: isize,
|
||||
pub target: Option<NormalizedRenderTarget>,
|
||||
pub hdr: bool,
|
||||
}
|
||||
|
||||
pub fn sort_cameras(
|
||||
mut sorted_cameras: ResMut<SortedCameras>,
|
||||
mut cameras: Query<(Entity, &mut ExtractedCamera)>,
|
||||
) {
|
||||
sorted_cameras.0.clear();
|
||||
for (entity, camera) in cameras.iter() {
|
||||
sorted_cameras.0.push(SortedCamera {
|
||||
entity,
|
||||
order: camera.order,
|
||||
target: camera.target.clone(),
|
||||
hdr: camera.hdr,
|
||||
});
|
||||
}
|
||||
// sort by order and ensure within an order, RenderTargets of the same type are packed together
|
||||
sorted_cameras
|
||||
.0
|
||||
.sort_by(|c1, c2| (c1.order, &c1.target).cmp(&(c2.order, &c2.target)));
|
||||
let mut previous_order_target = None;
|
||||
let mut ambiguities = <HashSet<_>>::default();
|
||||
let mut target_counts = <HashMap<_, _>>::default();
|
||||
for sorted_camera in &mut sorted_cameras.0 {
|
||||
let new_order_target = (sorted_camera.order, sorted_camera.target.clone());
|
||||
if let Some(previous_order_target) = previous_order_target {
|
||||
if previous_order_target == new_order_target {
|
||||
ambiguities.insert(new_order_target.clone());
|
||||
}
|
||||
}
|
||||
if let Some(target) = &sorted_camera.target {
|
||||
let count = target_counts
|
||||
.entry((target.clone(), sorted_camera.hdr))
|
||||
.or_insert(0usize);
|
||||
let (_, mut camera) = cameras.get_mut(sorted_camera.entity).unwrap();
|
||||
camera.sorted_camera_index_for_target = *count;
|
||||
*count += 1;
|
||||
}
|
||||
previous_order_target = Some(new_order_target);
|
||||
}
|
||||
|
||||
if !ambiguities.is_empty() {
|
||||
warn!(
|
||||
"Camera order ambiguities detected for active cameras with the following priorities: {:?}. \
|
||||
To fix this, ensure there is exactly one Camera entity spawned with a given order for a given RenderTarget. \
|
||||
Ambiguities should be resolved because either (1) multiple active cameras were spawned accidentally, which will \
|
||||
result in rendering multiple instances of the scene or (2) for cases where multiple active cameras is intentional, \
|
||||
ambiguities could result in unpredictable render results.",
|
||||
ambiguities
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A subpixel offset to jitter a perspective camera's frustum by.
|
||||
///
|
||||
/// Useful for temporal rendering techniques.
|
||||
///
|
||||
/// Do not use with [`OrthographicProjection`].
|
||||
///
|
||||
/// [`OrthographicProjection`]: crate::camera::OrthographicProjection
|
||||
#[derive(Component, Clone, Default, Reflect)]
|
||||
#[reflect(Default, Component, Clone)]
|
||||
pub struct TemporalJitter {
|
||||
/// Offset is in range [-0.5, 0.5].
|
||||
pub offset: Vec2,
|
||||
}
|
||||
|
||||
impl TemporalJitter {
|
||||
pub fn jitter_projection(&self, clip_from_view: &mut Mat4, view_size: Vec2) {
|
||||
if clip_from_view.w_axis.w == 1.0 {
|
||||
warn!(
|
||||
"TemporalJitter not supported with OrthographicProjection. Use PerspectiveProjection instead."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// https://github.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/blob/d7531ae47d8b36a5d4025663e731a47a38be882f/docs/techniques/media/super-resolution-temporal/jitter-space.svg
|
||||
let jitter = (self.offset * vec2(2.0, -2.0)) / view_size;
|
||||
|
||||
clip_from_view.z_axis.x += jitter.x;
|
||||
clip_from_view.z_axis.y += jitter.y;
|
||||
}
|
||||
}
|
||||
|
||||
/// Camera component specifying a mip bias to apply when sampling from material textures.
|
||||
///
|
||||
/// Often used in conjunction with antialiasing post-process effects to reduce textures blurriness.
|
||||
#[derive(Component, Reflect, Clone)]
|
||||
#[reflect(Default, Component)]
|
||||
pub struct MipBias(pub f32);
|
||||
|
||||
impl Default for MipBias {
|
||||
fn default() -> Self {
|
||||
Self(-1.0)
|
||||
}
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
mod camera;
|
||||
mod camera_driver_node;
|
||||
mod clear_color;
|
||||
mod manual_texture_view;
|
||||
mod projection;
|
||||
|
||||
pub use camera::*;
|
||||
pub use camera_driver_node::*;
|
||||
pub use clear_color::*;
|
||||
pub use manual_texture_view::*;
|
||||
pub use projection::*;
|
||||
|
||||
use crate::{
|
||||
extract_component::ExtractComponentPlugin, extract_resource::ExtractResourcePlugin,
|
||||
render_graph::RenderGraph, ExtractSchedule, Render, RenderApp, RenderSystems,
|
||||
};
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_ecs::schedule::IntoScheduleConfigs;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CameraPlugin;
|
||||
|
||||
impl Plugin for CameraPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.register_type::<Camera>()
|
||||
.register_type::<ClearColor>()
|
||||
.register_type::<CameraRenderGraph>()
|
||||
.register_type::<CameraMainTextureUsages>()
|
||||
.register_type::<Exposure>()
|
||||
.register_type::<TemporalJitter>()
|
||||
.register_type::<MipBias>()
|
||||
.register_type::<MainPassResolutionOverride>()
|
||||
.init_resource::<ManualTextureViews>()
|
||||
.init_resource::<ClearColor>()
|
||||
.add_plugins((
|
||||
CameraProjectionPlugin,
|
||||
ExtractResourcePlugin::<ManualTextureViews>::default(),
|
||||
ExtractResourcePlugin::<ClearColor>::default(),
|
||||
ExtractComponentPlugin::<CameraMainTextureUsages>::default(),
|
||||
));
|
||||
|
||||
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||
render_app
|
||||
.init_resource::<SortedCameras>()
|
||||
.add_systems(ExtractSchedule, extract_cameras)
|
||||
.add_systems(Render, sort_cameras.in_set(RenderSystems::ManageViews));
|
||||
let camera_driver_node = CameraDriverNode::new(render_app.world_mut());
|
||||
let mut render_graph = render_app.world_mut().resource_mut::<RenderGraph>();
|
||||
render_graph.add_node(crate::graph::CameraDriverLabel, camera_driver_node);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,10 +3,10 @@ use crate::{
|
||||
renderer::{RenderDevice, RenderQueue},
|
||||
sync_component::SyncComponentPlugin,
|
||||
sync_world::RenderEntity,
|
||||
view::ViewVisibility,
|
||||
Extract, ExtractSchedule, Render, RenderApp, RenderSystems,
|
||||
};
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_camera::visibility::ViewVisibility;
|
||||
use bevy_ecs::{
|
||||
bundle::NoBundleEffect,
|
||||
component::Component,
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
use core::marker::PhantomData;
|
||||
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_camera::visibility::ViewVisibility;
|
||||
use bevy_derive::{Deref, DerefMut};
|
||||
use bevy_ecs::{
|
||||
prelude::Entity,
|
||||
@ -16,7 +17,7 @@ use bevy_ecs::{
|
||||
};
|
||||
|
||||
use crate::sync_world::MainEntityHashMap;
|
||||
use crate::{prelude::ViewVisibility, Extract, ExtractSchedule, RenderApp};
|
||||
use crate::{Extract, ExtractSchedule, RenderApp};
|
||||
|
||||
/// Describes how to extract data needed for rendering from a component or
|
||||
/// components.
|
||||
|
||||
@ -38,7 +38,6 @@ pub mod gpu_readback;
|
||||
pub mod mesh;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod pipelined_rendering;
|
||||
pub mod primitives;
|
||||
pub mod render_asset;
|
||||
pub mod render_graph;
|
||||
pub mod render_phase;
|
||||
@ -50,6 +49,7 @@ pub mod sync_component;
|
||||
pub mod sync_world;
|
||||
pub mod texture;
|
||||
pub mod view;
|
||||
pub use bevy_camera::primitives;
|
||||
|
||||
/// The render prelude.
|
||||
///
|
||||
@ -58,19 +58,22 @@ pub mod prelude {
|
||||
#[doc(hidden)]
|
||||
pub use crate::{
|
||||
alpha::AlphaMode,
|
||||
camera::{
|
||||
Camera, ClearColor, ClearColorConfig, OrthographicProjection, PerspectiveProjection,
|
||||
Projection,
|
||||
},
|
||||
camera::ToNormalizedRenderTarget as _,
|
||||
mesh::{
|
||||
morph::MorphWeights, primitives::MeshBuilder, primitives::Meshable, Mesh, Mesh2d,
|
||||
Mesh3d,
|
||||
},
|
||||
render_resource::Shader,
|
||||
texture::ImagePlugin,
|
||||
texture::{ImagePlugin, ManualTextureViews},
|
||||
view::{InheritedVisibility, Msaa, ViewVisibility, Visibility},
|
||||
ExtractSchedule,
|
||||
};
|
||||
// TODO: Remove this in a follow-up
|
||||
#[doc(hidden)]
|
||||
pub use bevy_camera::{
|
||||
Camera, ClearColor, ClearColorConfig, OrthographicProjection, PerspectiveProjection,
|
||||
Projection,
|
||||
};
|
||||
}
|
||||
use batching::gpu_preprocessing::BatchingPlugin;
|
||||
|
||||
@ -481,10 +484,6 @@ impl Plugin for RenderPlugin {
|
||||
app.register_type::<alpha::AlphaMode>()
|
||||
// These types cannot be registered in bevy_color, as it does not depend on the rest of Bevy
|
||||
.register_type::<bevy_color::Color>()
|
||||
.register_type::<primitives::Aabb>()
|
||||
.register_type::<primitives::CascadesFrusta>()
|
||||
.register_type::<primitives::CubemapFrusta>()
|
||||
.register_type::<primitives::Frustum>()
|
||||
.register_type::<RenderEntity>()
|
||||
.register_type::<TemporaryRenderEntity>()
|
||||
.register_type::<MainEntity>()
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
use bevy_math::Vec3;
|
||||
use bevy_camera::visibility::VisibilitySystems;
|
||||
pub use bevy_mesh::*;
|
||||
use morph::{MeshMorphWeights, MorphWeights};
|
||||
pub mod allocator;
|
||||
mod components;
|
||||
use crate::{
|
||||
primitives::Aabb,
|
||||
render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets},
|
||||
render_resource::TextureView,
|
||||
texture::GpuImage,
|
||||
view::VisibilitySystems,
|
||||
RenderApp,
|
||||
};
|
||||
use allocator::MeshAllocatorPlugin;
|
||||
@ -21,7 +18,7 @@ use bevy_ecs::{
|
||||
SystemParamItem,
|
||||
},
|
||||
};
|
||||
pub use components::{mark_3d_meshes_as_changed_if_their_assets_changed, Mesh2d, Mesh3d, MeshTag};
|
||||
pub use bevy_mesh::{mark_3d_meshes_as_changed_if_their_assets_changed, Mesh2d, Mesh3d, MeshTag};
|
||||
use wgpu::IndexFormat;
|
||||
|
||||
/// Registers all [`MeshBuilder`] types.
|
||||
@ -112,26 +109,6 @@ pub fn inherit_weights(
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MeshAabb {
|
||||
/// Compute the Axis-Aligned Bounding Box of the mesh vertices in model space
|
||||
///
|
||||
/// Returns `None` if `self` doesn't have [`Mesh::ATTRIBUTE_POSITION`] of
|
||||
/// type [`VertexAttributeValues::Float32x3`], or if `self` doesn't have any vertices.
|
||||
fn compute_aabb(&self) -> Option<Aabb>;
|
||||
}
|
||||
|
||||
impl MeshAabb for Mesh {
|
||||
fn compute_aabb(&self) -> Option<Aabb> {
|
||||
let Some(VertexAttributeValues::Float32x3(values)) =
|
||||
self.attribute(Mesh::ATTRIBUTE_POSITION)
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Aabb::enclosing(values.iter().map(|p| Vec3::from_slice(p)))
|
||||
}
|
||||
}
|
||||
|
||||
/// The render world representation of a [`Mesh`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderMesh {
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
use crate::{
|
||||
camera::{ClearColor, ExtractedCamera, NormalizedRenderTarget, SortedCameras},
|
||||
camera::{ExtractedCamera, NormalizedRenderTarget, SortedCameras},
|
||||
render_graph::{Node, NodeRunError, RenderGraphContext},
|
||||
renderer::RenderContext,
|
||||
view::ExtractedWindows,
|
||||
};
|
||||
use bevy_camera::ClearColor;
|
||||
use bevy_ecs::{entity::ContainsEntity, prelude::QueryState, world::World};
|
||||
use bevy_platform::collections::HashSet;
|
||||
use wgpu::{LoadOp, Operations, RenderPassColorAttachment, RenderPassDescriptor, StoreOp};
|
||||
@ -1,4 +1,5 @@
|
||||
mod app;
|
||||
mod camera_driver_node;
|
||||
mod context;
|
||||
mod edge;
|
||||
mod graph;
|
||||
@ -6,6 +7,7 @@ mod node;
|
||||
mod node_slot;
|
||||
|
||||
pub use app::*;
|
||||
pub use camera_driver_node::*;
|
||||
pub use context::*;
|
||||
pub use edge::*;
|
||||
pub use graph::*;
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
use crate::{
|
||||
camera::Viewport,
|
||||
diagnostic::internal::{Pass, PassKind, WritePipelineStatistics, WriteTimestamp},
|
||||
render_resource::{
|
||||
BindGroup, BindGroupId, Buffer, BufferId, BufferSlice, RenderPipeline, RenderPipelineId,
|
||||
@ -7,6 +6,7 @@ use crate::{
|
||||
},
|
||||
renderer::RenderDevice,
|
||||
};
|
||||
use bevy_camera::Viewport;
|
||||
use bevy_color::LinearRgba;
|
||||
use bevy_utils::default;
|
||||
use core::ops::Range;
|
||||
|
||||
@ -1,15 +1,12 @@
|
||||
use crate::{extract_resource::ExtractResource, render_resource::TextureView};
|
||||
use bevy_ecs::{prelude::Component, reflect::ReflectComponent, resource::Resource};
|
||||
use bevy_image::BevyDefault as _;
|
||||
use bevy_camera::ManualTextureViewHandle;
|
||||
use bevy_ecs::{prelude::Component, resource::Resource};
|
||||
use bevy_image::BevyDefault;
|
||||
use bevy_math::UVec2;
|
||||
use bevy_platform::collections::HashMap;
|
||||
use bevy_reflect::prelude::*;
|
||||
use bevy_render_macros::ExtractResource;
|
||||
use wgpu::TextureFormat;
|
||||
|
||||
/// A unique id that corresponds to a specific [`ManualTextureView`] in the [`ManualTextureViews`] collection.
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Component, Reflect)]
|
||||
#[reflect(Component, Default, Debug, PartialEq, Hash, Clone)]
|
||||
pub struct ManualTextureViewHandle(pub u32);
|
||||
use crate::render_resource::TextureView;
|
||||
|
||||
/// A manually managed [`TextureView`] for use as a [`crate::camera::RenderTarget`].
|
||||
#[derive(Debug, Clone, Component)]
|
||||
@ -1,5 +1,6 @@
|
||||
mod fallback_image;
|
||||
mod gpu_image;
|
||||
mod manual_texture_view;
|
||||
mod texture_attachment;
|
||||
mod texture_cache;
|
||||
|
||||
@ -14,11 +15,13 @@ use bevy_image::{
|
||||
};
|
||||
pub use fallback_image::*;
|
||||
pub use gpu_image::*;
|
||||
pub use manual_texture_view::*;
|
||||
pub use texture_attachment::*;
|
||||
pub use texture_cache::*;
|
||||
|
||||
use crate::{
|
||||
render_asset::RenderAssetPlugin, renderer::RenderDevice, Render, RenderApp, RenderSystems,
|
||||
extract_resource::ExtractResourcePlugin, render_asset::RenderAssetPlugin,
|
||||
renderer::RenderDevice, Render, RenderApp, RenderSystems,
|
||||
};
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_asset::{uuid_handle, AssetApp, Assets, Handle};
|
||||
@ -74,7 +77,11 @@ impl Plugin for ImagePlugin {
|
||||
app.init_asset_loader::<HdrTextureLoader>();
|
||||
}
|
||||
|
||||
app.add_plugins(RenderAssetPlugin::<GpuImage>::default())
|
||||
app.add_plugins((
|
||||
RenderAssetPlugin::<GpuImage>::default(),
|
||||
ExtractResourcePlugin::<ManualTextureViews>::default(),
|
||||
))
|
||||
.init_resource::<ManualTextureViews>()
|
||||
.register_type::<Image>()
|
||||
.init_asset::<Image>()
|
||||
.register_asset_reflect::<Image>();
|
||||
|
||||
@ -1,27 +1,26 @@
|
||||
pub mod visibility;
|
||||
pub mod window;
|
||||
|
||||
use bevy_camera::{
|
||||
primitives::Frustum, CameraMainTextureUsages, ClearColor, ClearColorConfig, Exposure,
|
||||
};
|
||||
use bevy_diagnostic::FrameCount;
|
||||
pub use visibility::*;
|
||||
pub use window::*;
|
||||
|
||||
use crate::{
|
||||
camera::{
|
||||
CameraMainTextureUsages, ClearColor, ClearColorConfig, Exposure, ExtractedCamera,
|
||||
ManualTextureViews, MipBias, NormalizedRenderTarget, TemporalJitter,
|
||||
},
|
||||
camera::{ExtractedCamera, MipBias, NormalizedRenderTarget, TemporalJitter},
|
||||
experimental::occlusion_culling::OcclusionCulling,
|
||||
extract_component::ExtractComponentPlugin,
|
||||
load_shader_library,
|
||||
primitives::Frustum,
|
||||
render_asset::RenderAssets,
|
||||
render_phase::ViewRangefinder3d,
|
||||
render_resource::{DynamicUniformBuffer, ShaderType, Texture, TextureView},
|
||||
renderer::{RenderDevice, RenderQueue},
|
||||
sync_world::MainEntity,
|
||||
texture::{
|
||||
CachedTexture, ColorAttachment, DepthAttachment, GpuImage, OutputColorAttachment,
|
||||
TextureCache,
|
||||
CachedTexture, ColorAttachment, DepthAttachment, GpuImage, ManualTextureViews,
|
||||
OutputColorAttachment, TextureCache,
|
||||
},
|
||||
Render, RenderApp, RenderSystems,
|
||||
};
|
||||
@ -100,13 +99,7 @@ impl Plugin for ViewPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
load_shader_library!(app, "view.wgsl");
|
||||
|
||||
app.register_type::<InheritedVisibility>()
|
||||
.register_type::<ViewVisibility>()
|
||||
.register_type::<Msaa>()
|
||||
.register_type::<NoFrustumCulling>()
|
||||
.register_type::<RenderLayers>()
|
||||
.register_type::<Visibility>()
|
||||
.register_type::<VisibleEntities>()
|
||||
app.register_type::<Msaa>()
|
||||
.register_type::<ColorGrading>()
|
||||
.register_type::<OcclusionCulling>()
|
||||
// NOTE: windows.is_changed() handles cases where a window was resized
|
||||
@ -114,8 +107,7 @@ impl Plugin for ViewPlugin {
|
||||
ExtractComponentPlugin::<Hdr>::default(),
|
||||
ExtractComponentPlugin::<Msaa>::default(),
|
||||
ExtractComponentPlugin::<OcclusionCulling>::default(),
|
||||
VisibilityPlugin,
|
||||
VisibilityRangePlugin,
|
||||
RenderVisibilityRangePlugin,
|
||||
));
|
||||
|
||||
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||
@ -729,9 +721,6 @@ impl From<ColorGrading> for ColorGradingUniform {
|
||||
#[derive(Component, Default)]
|
||||
pub struct NoIndirectDrawing;
|
||||
|
||||
#[derive(Component, Default)]
|
||||
pub struct NoCpuCulling;
|
||||
|
||||
impl ViewTarget {
|
||||
pub const TEXTURE_FORMAT_HDR: TextureFormat = TextureFormat::Rgba16Float;
|
||||
|
||||
|
||||
@ -1,269 +1,14 @@
|
||||
mod range;
|
||||
mod render_layers;
|
||||
|
||||
use core::any::TypeId;
|
||||
|
||||
use bevy_ecs::entity::EntityHashSet;
|
||||
use bevy_ecs::lifecycle::HookContext;
|
||||
use bevy_ecs::world::DeferredWorld;
|
||||
use derive_more::derive::{Deref, DerefMut};
|
||||
use bevy_ecs::{component::Component, entity::Entity, prelude::ReflectComponent};
|
||||
use bevy_reflect::{prelude::ReflectDefault, Reflect};
|
||||
use bevy_utils::TypeIdMap;
|
||||
|
||||
use crate::sync_world::MainEntity;
|
||||
|
||||
mod range;
|
||||
pub use bevy_camera::visibility::*;
|
||||
pub use range::*;
|
||||
pub use render_layers::*;
|
||||
|
||||
use bevy_app::{Plugin, PostUpdate};
|
||||
use bevy_asset::Assets;
|
||||
use bevy_ecs::{hierarchy::validate_parent_has_component, prelude::*};
|
||||
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
||||
use bevy_transform::{components::GlobalTransform, TransformSystems};
|
||||
use bevy_utils::{Parallel, TypeIdMap};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::NoCpuCulling;
|
||||
use crate::{
|
||||
camera::{Camera, Projection},
|
||||
mesh::{Mesh, Mesh3d, MeshAabb},
|
||||
primitives::{Aabb, Frustum, Sphere},
|
||||
sync_world::MainEntity,
|
||||
};
|
||||
|
||||
/// User indication of whether an entity is visible. Propagates down the entity hierarchy.
|
||||
///
|
||||
/// If an entity is hidden in this way, all [`Children`] (and all of their children and so on) who
|
||||
/// are set to [`Inherited`](Self::Inherited) will also be hidden.
|
||||
///
|
||||
/// This is done by the `visibility_propagate_system` which uses the entity hierarchy and
|
||||
/// `Visibility` to set the values of each entity's [`InheritedVisibility`] component.
|
||||
#[derive(Component, Clone, Copy, Reflect, Debug, PartialEq, Eq, Default)]
|
||||
#[reflect(Component, Default, Debug, PartialEq, Clone)]
|
||||
#[require(InheritedVisibility, ViewVisibility)]
|
||||
pub enum Visibility {
|
||||
/// An entity with `Visibility::Inherited` will inherit the Visibility of its [`ChildOf`] target.
|
||||
///
|
||||
/// A root-level entity that is set to `Inherited` will be visible.
|
||||
#[default]
|
||||
Inherited,
|
||||
/// An entity with `Visibility::Hidden` will be unconditionally hidden.
|
||||
Hidden,
|
||||
/// An entity with `Visibility::Visible` will be unconditionally visible.
|
||||
///
|
||||
/// Note that an entity with `Visibility::Visible` will be visible regardless of whether the
|
||||
/// [`ChildOf`] target entity is hidden.
|
||||
Visible,
|
||||
}
|
||||
|
||||
impl Visibility {
|
||||
/// Toggles between `Visibility::Inherited` and `Visibility::Visible`.
|
||||
/// If the value is `Visibility::Hidden`, it remains unaffected.
|
||||
#[inline]
|
||||
pub fn toggle_inherited_visible(&mut self) {
|
||||
*self = match *self {
|
||||
Visibility::Inherited => Visibility::Visible,
|
||||
Visibility::Visible => Visibility::Inherited,
|
||||
_ => *self,
|
||||
};
|
||||
}
|
||||
/// Toggles between `Visibility::Inherited` and `Visibility::Hidden`.
|
||||
/// If the value is `Visibility::Visible`, it remains unaffected.
|
||||
#[inline]
|
||||
pub fn toggle_inherited_hidden(&mut self) {
|
||||
*self = match *self {
|
||||
Visibility::Inherited => Visibility::Hidden,
|
||||
Visibility::Hidden => Visibility::Inherited,
|
||||
_ => *self,
|
||||
};
|
||||
}
|
||||
/// Toggles between `Visibility::Visible` and `Visibility::Hidden`.
|
||||
/// If the value is `Visibility::Inherited`, it remains unaffected.
|
||||
#[inline]
|
||||
pub fn toggle_visible_hidden(&mut self) {
|
||||
*self = match *self {
|
||||
Visibility::Visible => Visibility::Hidden,
|
||||
Visibility::Hidden => Visibility::Visible,
|
||||
_ => *self,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Allows `&Visibility == Visibility`
|
||||
impl PartialEq<Visibility> for &Visibility {
|
||||
#[inline]
|
||||
fn eq(&self, other: &Visibility) -> bool {
|
||||
// Use the base Visibility == Visibility implementation.
|
||||
<Visibility as PartialEq<Visibility>>::eq(*self, other)
|
||||
}
|
||||
}
|
||||
|
||||
// Allows `Visibility == &Visibility`
|
||||
impl PartialEq<&Visibility> for Visibility {
|
||||
#[inline]
|
||||
fn eq(&self, other: &&Visibility) -> bool {
|
||||
// Use the base Visibility == Visibility implementation.
|
||||
<Visibility as PartialEq<Visibility>>::eq(self, *other)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not an entity is visible in the hierarchy.
|
||||
/// This will not be accurate until [`VisibilityPropagate`] runs in the [`PostUpdate`] schedule.
|
||||
///
|
||||
/// If this is false, then [`ViewVisibility`] should also be false.
|
||||
///
|
||||
/// [`VisibilityPropagate`]: VisibilitySystems::VisibilityPropagate
|
||||
#[derive(Component, Deref, Debug, Default, Clone, Copy, Reflect, PartialEq, Eq)]
|
||||
#[reflect(Component, Default, Debug, PartialEq, Clone)]
|
||||
#[component(on_insert = validate_parent_has_component::<Self>)]
|
||||
pub struct InheritedVisibility(bool);
|
||||
|
||||
impl InheritedVisibility {
|
||||
/// An entity that is invisible in the hierarchy.
|
||||
pub const HIDDEN: Self = Self(false);
|
||||
/// An entity that is visible in the hierarchy.
|
||||
pub const VISIBLE: Self = Self(true);
|
||||
|
||||
/// Returns `true` if the entity is visible in the hierarchy.
|
||||
/// Otherwise, returns `false`.
|
||||
#[inline]
|
||||
pub fn get(self) -> bool {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A bucket into which we group entities for the purposes of visibility.
|
||||
///
|
||||
/// Bevy's various rendering subsystems (3D, 2D, etc.) want to be able to
|
||||
/// quickly winnow the set of entities to only those that the subsystem is
|
||||
/// tasked with rendering, to avoid spending time examining irrelevant entities.
|
||||
/// At the same time, Bevy wants the [`check_visibility`] system to determine
|
||||
/// all entities' visibilities at the same time, regardless of what rendering
|
||||
/// subsystem is responsible for drawing them. Additionally, your application
|
||||
/// may want to add more types of renderable objects that Bevy determines
|
||||
/// visibility for just as it does for Bevy's built-in objects.
|
||||
///
|
||||
/// The solution to this problem is *visibility classes*. A visibility class is
|
||||
/// a type, typically the type of a component, that represents the subsystem
|
||||
/// that renders it: for example, `Mesh3d`, `Mesh2d`, and `Sprite`. The
|
||||
/// [`VisibilityClass`] component stores the visibility class or classes that
|
||||
/// the entity belongs to. (Generally, an object will belong to only one
|
||||
/// visibility class, but in rare cases it may belong to multiple.)
|
||||
///
|
||||
/// When adding a new renderable component, you'll typically want to write an
|
||||
/// add-component hook that adds the type ID of that component to the
|
||||
/// [`VisibilityClass`] array. See `custom_phase_item` for an example.
|
||||
//
|
||||
// Note: This can't be a `ComponentId` because the visibility classes are copied
|
||||
// into the render world, and component IDs are per-world.
|
||||
#[derive(Clone, Component, Default, Reflect, Deref, DerefMut)]
|
||||
#[reflect(Component, Default, Clone)]
|
||||
pub struct VisibilityClass(pub SmallVec<[TypeId; 1]>);
|
||||
|
||||
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering.
|
||||
///
|
||||
/// Each frame, this will be reset to `false` during [`VisibilityPropagate`] systems in [`PostUpdate`].
|
||||
/// Later in the frame, systems in [`CheckVisibility`] will mark any visible entities using [`ViewVisibility::set`].
|
||||
/// Because of this, values of this type will be marked as changed every frame, even when they do not change.
|
||||
///
|
||||
/// If you wish to add custom visibility system that sets this value, make sure you add it to the [`CheckVisibility`] set.
|
||||
///
|
||||
/// [`VisibilityPropagate`]: VisibilitySystems::VisibilityPropagate
|
||||
/// [`CheckVisibility`]: VisibilitySystems::CheckVisibility
|
||||
#[derive(Component, Deref, Debug, Default, Clone, Copy, Reflect, PartialEq, Eq)]
|
||||
#[reflect(Component, Default, Debug, PartialEq, Clone)]
|
||||
pub struct ViewVisibility(bool);
|
||||
|
||||
impl ViewVisibility {
|
||||
/// An entity that cannot be seen from any views.
|
||||
pub const HIDDEN: Self = Self(false);
|
||||
|
||||
/// Returns `true` if the entity is visible in any view.
|
||||
/// Otherwise, returns `false`.
|
||||
#[inline]
|
||||
pub fn get(self) -> bool {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Sets the visibility to `true`. This should not be considered reversible for a given frame,
|
||||
/// as this component tracks whether or not the entity visible in _any_ view.
|
||||
///
|
||||
/// This will be automatically reset to `false` every frame in [`VisibilityPropagate`] and then set
|
||||
/// to the proper value in [`CheckVisibility`].
|
||||
///
|
||||
/// You should only manually set this if you are defining a custom visibility system,
|
||||
/// in which case the system should be placed in the [`CheckVisibility`] set.
|
||||
/// For normal user-defined entity visibility, see [`Visibility`].
|
||||
///
|
||||
/// [`VisibilityPropagate`]: VisibilitySystems::VisibilityPropagate
|
||||
/// [`CheckVisibility`]: VisibilitySystems::CheckVisibility
|
||||
#[inline]
|
||||
pub fn set(&mut self) {
|
||||
self.0 = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Use this component to opt-out of built-in frustum culling for entities, see
|
||||
/// [`Frustum`].
|
||||
///
|
||||
/// It can be used for example:
|
||||
/// - when a [`Mesh`] is updated but its [`Aabb`] is not, which might happen with animations,
|
||||
/// - when using some light effects, like wanting a [`Mesh`] out of the [`Frustum`]
|
||||
/// to appear in the reflection of a [`Mesh`] within.
|
||||
#[derive(Debug, Component, Default, Reflect)]
|
||||
#[reflect(Component, Default, Debug)]
|
||||
pub struct NoFrustumCulling;
|
||||
|
||||
/// Collection of entities visible from the current view.
|
||||
///
|
||||
/// This component contains all entities which are visible from the currently
|
||||
/// rendered view. The collection is updated automatically by the [`VisibilitySystems::CheckVisibility`]
|
||||
/// system set. Renderers can use the equivalent [`RenderVisibleEntities`] to optimize rendering of
|
||||
/// a particular view, to prevent drawing items not visible from that view.
|
||||
///
|
||||
/// This component is intended to be attached to the same entity as the [`Camera`] and
|
||||
/// the [`Frustum`] defining the view.
|
||||
#[derive(Clone, Component, Default, Debug, Reflect)]
|
||||
#[reflect(Component, Default, Debug, Clone)]
|
||||
pub struct VisibleEntities {
|
||||
#[reflect(ignore, clone)]
|
||||
pub entities: TypeIdMap<Vec<Entity>>,
|
||||
}
|
||||
|
||||
impl VisibleEntities {
|
||||
pub fn get(&self, type_id: TypeId) -> &[Entity] {
|
||||
match self.entities.get(&type_id) {
|
||||
Some(entities) => &entities[..],
|
||||
None => &[],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, type_id: TypeId) -> &mut Vec<Entity> {
|
||||
self.entities.entry(type_id).or_default()
|
||||
}
|
||||
|
||||
pub fn iter(&self, type_id: TypeId) -> impl DoubleEndedIterator<Item = &Entity> {
|
||||
self.get(type_id).iter()
|
||||
}
|
||||
|
||||
pub fn len(&self, type_id: TypeId) -> usize {
|
||||
self.get(type_id).len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self, type_id: TypeId) -> bool {
|
||||
self.get(type_id).is_empty()
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, type_id: TypeId) {
|
||||
self.get_mut(type_id).clear();
|
||||
}
|
||||
|
||||
pub fn clear_all(&mut self) {
|
||||
// Don't just nuke the hash table; we want to reuse allocations.
|
||||
for entities in self.entities.values_mut() {
|
||||
entities.clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, entity: Entity, type_id: TypeId) {
|
||||
self.get_mut(type_id).push(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/// Collection of entities visible from the current view.
|
||||
///
|
||||
@ -307,667 +52,3 @@ impl RenderVisibleEntities {
|
||||
self.get::<QF>().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
|
||||
pub enum VisibilitySystems {
|
||||
/// Label for the [`calculate_bounds`], `calculate_bounds_2d` and `calculate_bounds_text2d` systems,
|
||||
/// calculating and inserting an [`Aabb`] to relevant entities.
|
||||
CalculateBounds,
|
||||
/// Label for [`update_frusta`] in [`CameraProjectionPlugin`](crate::camera::CameraProjectionPlugin).
|
||||
UpdateFrusta,
|
||||
/// Label for the system propagating the [`InheritedVisibility`] in a
|
||||
/// [`ChildOf`] / [`Children`] hierarchy.
|
||||
VisibilityPropagate,
|
||||
/// Label for the [`check_visibility`] system updating [`ViewVisibility`]
|
||||
/// of each entity and the [`VisibleEntities`] of each view.\
|
||||
///
|
||||
/// System order ambiguities between systems in this set are ignored:
|
||||
/// the order of systems within this set is irrelevant, as [`check_visibility`]
|
||||
/// assumes that its operations are irreversible during the frame.
|
||||
CheckVisibility,
|
||||
/// Label for the `mark_newly_hidden_entities_invisible` system, which sets
|
||||
/// [`ViewVisibility`] to [`ViewVisibility::HIDDEN`] for entities that no
|
||||
/// view has marked as visible.
|
||||
MarkNewlyHiddenEntitiesInvisible,
|
||||
}
|
||||
|
||||
pub struct VisibilityPlugin;
|
||||
|
||||
impl Plugin for VisibilityPlugin {
|
||||
fn build(&self, app: &mut bevy_app::App) {
|
||||
use VisibilitySystems::*;
|
||||
|
||||
app.register_type::<VisibilityClass>()
|
||||
.configure_sets(
|
||||
PostUpdate,
|
||||
(CalculateBounds, UpdateFrusta, VisibilityPropagate)
|
||||
.before(CheckVisibility)
|
||||
.after(TransformSystems::Propagate),
|
||||
)
|
||||
.configure_sets(
|
||||
PostUpdate,
|
||||
MarkNewlyHiddenEntitiesInvisible.after(CheckVisibility),
|
||||
)
|
||||
.init_resource::<PreviousVisibleEntities>()
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
(
|
||||
calculate_bounds.in_set(CalculateBounds),
|
||||
(visibility_propagate_system, reset_view_visibility)
|
||||
.in_set(VisibilityPropagate),
|
||||
check_visibility.in_set(CheckVisibility),
|
||||
mark_newly_hidden_entities_invisible.in_set(MarkNewlyHiddenEntitiesInvisible),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes and adds an [`Aabb`] component to entities with a
|
||||
/// [`Mesh3d`] component and without a [`NoFrustumCulling`] component.
|
||||
///
|
||||
/// This system is used in system set [`VisibilitySystems::CalculateBounds`].
|
||||
pub fn calculate_bounds(
|
||||
mut commands: Commands,
|
||||
meshes: Res<Assets<Mesh>>,
|
||||
without_aabb: Query<(Entity, &Mesh3d), (Without<Aabb>, Without<NoFrustumCulling>)>,
|
||||
) {
|
||||
for (entity, mesh_handle) in &without_aabb {
|
||||
if let Some(mesh) = meshes.get(mesh_handle) {
|
||||
if let Some(aabb) = mesh.compute_aabb() {
|
||||
commands.entity(entity).try_insert(aabb);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates [`Frustum`].
|
||||
///
|
||||
/// This system is used in [`CameraProjectionPlugin`](crate::camera::CameraProjectionPlugin).
|
||||
pub fn update_frusta(
|
||||
mut views: Query<
|
||||
(&GlobalTransform, &Projection, &mut Frustum),
|
||||
Or<(Changed<GlobalTransform>, Changed<Projection>)>,
|
||||
>,
|
||||
) {
|
||||
for (transform, projection, mut frustum) in &mut views {
|
||||
*frustum = projection.compute_frustum(transform);
|
||||
}
|
||||
}
|
||||
|
||||
fn visibility_propagate_system(
|
||||
changed: Query<
|
||||
(Entity, &Visibility, Option<&ChildOf>, Option<&Children>),
|
||||
(
|
||||
With<InheritedVisibility>,
|
||||
Or<(Changed<Visibility>, Changed<ChildOf>)>,
|
||||
),
|
||||
>,
|
||||
mut visibility_query: Query<(&Visibility, &mut InheritedVisibility)>,
|
||||
children_query: Query<&Children, (With<Visibility>, With<InheritedVisibility>)>,
|
||||
) {
|
||||
for (entity, visibility, child_of, children) in &changed {
|
||||
let is_visible = match visibility {
|
||||
Visibility::Visible => true,
|
||||
Visibility::Hidden => false,
|
||||
// fall back to true if no parent is found or parent lacks components
|
||||
Visibility::Inherited => child_of
|
||||
.and_then(|c| visibility_query.get(c.parent()).ok())
|
||||
.is_none_or(|(_, x)| x.get()),
|
||||
};
|
||||
let (_, mut inherited_visibility) = visibility_query
|
||||
.get_mut(entity)
|
||||
.expect("With<InheritedVisibility> ensures this query will return a value");
|
||||
|
||||
// Only update the visibility if it has changed.
|
||||
// This will also prevent the visibility from propagating multiple times in the same frame
|
||||
// if this entity's visibility has been updated recursively by its parent.
|
||||
if inherited_visibility.get() != is_visible {
|
||||
inherited_visibility.0 = is_visible;
|
||||
|
||||
// Recursively update the visibility of each child.
|
||||
for &child in children.into_iter().flatten() {
|
||||
let _ =
|
||||
propagate_recursive(is_visible, child, &mut visibility_query, &children_query);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn propagate_recursive(
|
||||
parent_is_visible: bool,
|
||||
entity: Entity,
|
||||
visibility_query: &mut Query<(&Visibility, &mut InheritedVisibility)>,
|
||||
children_query: &Query<&Children, (With<Visibility>, With<InheritedVisibility>)>,
|
||||
// BLOCKED: https://github.com/rust-lang/rust/issues/31436
|
||||
// We use a result here to use the `?` operator. Ideally we'd use a try block instead
|
||||
) -> Result<(), ()> {
|
||||
// Get the visibility components for the current entity.
|
||||
// If the entity does not have the required components, just return early.
|
||||
let (visibility, mut inherited_visibility) = visibility_query.get_mut(entity).map_err(drop)?;
|
||||
|
||||
let is_visible = match visibility {
|
||||
Visibility::Visible => true,
|
||||
Visibility::Hidden => false,
|
||||
Visibility::Inherited => parent_is_visible,
|
||||
};
|
||||
|
||||
// Only update the visibility if it has changed.
|
||||
if inherited_visibility.get() != is_visible {
|
||||
inherited_visibility.0 = is_visible;
|
||||
|
||||
// Recursively update the visibility of each child.
|
||||
for &child in children_query.get(entity).ok().into_iter().flatten() {
|
||||
let _ = propagate_recursive(is_visible, child, visibility_query, children_query);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stores all entities that were visible in the previous frame.
|
||||
///
|
||||
/// As systems that check visibility judge entities visible, they remove them
|
||||
/// from this set. Afterward, the `mark_newly_hidden_entities_invisible` system
|
||||
/// runs and marks every mesh still remaining in this set as hidden.
|
||||
#[derive(Resource, Default, Deref, DerefMut)]
|
||||
pub struct PreviousVisibleEntities(EntityHashSet);
|
||||
|
||||
/// Resets the view visibility of every entity.
|
||||
/// Entities that are visible will be marked as such later this frame
|
||||
/// by a [`VisibilitySystems::CheckVisibility`] system.
|
||||
fn reset_view_visibility(
|
||||
mut query: Query<(Entity, &ViewVisibility)>,
|
||||
mut previous_visible_entities: ResMut<PreviousVisibleEntities>,
|
||||
) {
|
||||
previous_visible_entities.clear();
|
||||
|
||||
query.iter_mut().for_each(|(entity, view_visibility)| {
|
||||
// Record the entities that were previously visible.
|
||||
if view_visibility.get() {
|
||||
previous_visible_entities.insert(entity);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// System updating the visibility of entities each frame.
|
||||
///
|
||||
/// The system is part of the [`VisibilitySystems::CheckVisibility`] set. Each
|
||||
/// frame, it updates the [`ViewVisibility`] of all entities, and for each view
|
||||
/// also compute the [`VisibleEntities`] for that view.
|
||||
///
|
||||
/// To ensure that an entity is checked for visibility, make sure that it has a
|
||||
/// [`VisibilityClass`] component and that that component is nonempty.
|
||||
pub fn check_visibility(
|
||||
mut thread_queues: Local<Parallel<TypeIdMap<Vec<Entity>>>>,
|
||||
mut view_query: Query<(
|
||||
Entity,
|
||||
&mut VisibleEntities,
|
||||
&Frustum,
|
||||
Option<&RenderLayers>,
|
||||
&Camera,
|
||||
Has<NoCpuCulling>,
|
||||
)>,
|
||||
mut visible_aabb_query: Query<(
|
||||
Entity,
|
||||
&InheritedVisibility,
|
||||
&mut ViewVisibility,
|
||||
&VisibilityClass,
|
||||
Option<&RenderLayers>,
|
||||
Option<&Aabb>,
|
||||
&GlobalTransform,
|
||||
Has<NoFrustumCulling>,
|
||||
Has<VisibilityRange>,
|
||||
)>,
|
||||
visible_entity_ranges: Option<Res<VisibleEntityRanges>>,
|
||||
mut previous_visible_entities: ResMut<PreviousVisibleEntities>,
|
||||
) {
|
||||
let visible_entity_ranges = visible_entity_ranges.as_deref();
|
||||
|
||||
for (view, mut visible_entities, frustum, maybe_view_mask, camera, no_cpu_culling) in
|
||||
&mut view_query
|
||||
{
|
||||
if !camera.is_active {
|
||||
continue;
|
||||
}
|
||||
|
||||
let view_mask = maybe_view_mask.unwrap_or_default();
|
||||
|
||||
visible_aabb_query.par_iter_mut().for_each_init(
|
||||
|| thread_queues.borrow_local_mut(),
|
||||
|queue, query_item| {
|
||||
let (
|
||||
entity,
|
||||
inherited_visibility,
|
||||
mut view_visibility,
|
||||
visibility_class,
|
||||
maybe_entity_mask,
|
||||
maybe_model_aabb,
|
||||
transform,
|
||||
no_frustum_culling,
|
||||
has_visibility_range,
|
||||
) = query_item;
|
||||
|
||||
// Skip computing visibility for entities that are configured to be hidden.
|
||||
// ViewVisibility has already been reset in `reset_view_visibility`.
|
||||
if !inherited_visibility.get() {
|
||||
return;
|
||||
}
|
||||
|
||||
let entity_mask = maybe_entity_mask.unwrap_or_default();
|
||||
if !view_mask.intersects(entity_mask) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If outside of the visibility range, cull.
|
||||
if has_visibility_range
|
||||
&& visible_entity_ranges.is_some_and(|visible_entity_ranges| {
|
||||
!visible_entity_ranges.entity_is_in_range_of_view(entity, view)
|
||||
})
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have an aabb, do frustum culling
|
||||
if !no_frustum_culling && !no_cpu_culling {
|
||||
if let Some(model_aabb) = maybe_model_aabb {
|
||||
let world_from_local = transform.affine();
|
||||
let model_sphere = Sphere {
|
||||
center: world_from_local.transform_point3a(model_aabb.center),
|
||||
radius: transform.radius_vec3a(model_aabb.half_extents),
|
||||
};
|
||||
// Do quick sphere-based frustum culling
|
||||
if !frustum.intersects_sphere(&model_sphere, false) {
|
||||
return;
|
||||
}
|
||||
// Do aabb-based frustum culling
|
||||
if !frustum.intersects_obb(model_aabb, &world_from_local, true, false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we don't trigger changed notifications
|
||||
// unnecessarily by checking whether the flag is set before
|
||||
// setting it.
|
||||
if !**view_visibility {
|
||||
view_visibility.set();
|
||||
}
|
||||
|
||||
// Add the entity to the queue for all visibility classes the
|
||||
// entity is in.
|
||||
for visibility_class_id in visibility_class.iter() {
|
||||
queue.entry(*visibility_class_id).or_default().push(entity);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
visible_entities.clear_all();
|
||||
|
||||
// Drain all the thread queues into the `visible_entities` list.
|
||||
for class_queues in thread_queues.iter_mut() {
|
||||
for (class, entities) in class_queues {
|
||||
let visible_entities_for_class = visible_entities.get_mut(*class);
|
||||
for entity in entities.drain(..) {
|
||||
// As we mark entities as visible, we remove them from the
|
||||
// `previous_visible_entities` list. At the end, all of the
|
||||
// entities remaining in `previous_visible_entities` will be
|
||||
// entities that were visible last frame but are no longer
|
||||
// visible this frame.
|
||||
previous_visible_entities.remove(&entity);
|
||||
|
||||
visible_entities_for_class.push(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks any entities that weren't judged visible this frame as invisible.
|
||||
///
|
||||
/// As visibility-determining systems run, they remove entities that they judge
|
||||
/// visible from [`PreviousVisibleEntities`]. At the end of visibility
|
||||
/// determination, all entities that remain in [`PreviousVisibleEntities`] must
|
||||
/// be invisible. This system goes through those entities and marks them newly
|
||||
/// invisible (which sets the change flag for them).
|
||||
fn mark_newly_hidden_entities_invisible(
|
||||
mut view_visibilities: Query<&mut ViewVisibility>,
|
||||
mut previous_visible_entities: ResMut<PreviousVisibleEntities>,
|
||||
) {
|
||||
// Whatever previous visible entities are left are entities that were
|
||||
// visible last frame but just became invisible.
|
||||
for entity in previous_visible_entities.drain() {
|
||||
if let Ok(mut view_visibility) = view_visibilities.get_mut(entity) {
|
||||
*view_visibility = ViewVisibility::HIDDEN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A generic component add hook that automatically adds the appropriate
|
||||
/// [`VisibilityClass`] to an entity.
|
||||
///
|
||||
/// This can be handy when creating custom renderable components. To use this
|
||||
/// hook, add it to your renderable component like this:
|
||||
///
|
||||
/// ```ignore
|
||||
/// #[derive(Component)]
|
||||
/// #[component(on_add = add_visibility_class::<MyComponent>)]
|
||||
/// struct MyComponent {
|
||||
/// ...
|
||||
/// }
|
||||
/// ```
|
||||
pub fn add_visibility_class<C>(
|
||||
mut world: DeferredWorld<'_>,
|
||||
HookContext { entity, .. }: HookContext,
|
||||
) where
|
||||
C: 'static,
|
||||
{
|
||||
if let Some(mut visibility_class) = world.get_mut::<VisibilityClass>(entity) {
|
||||
visibility_class.push(TypeId::of::<C>());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use bevy_app::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn visibility_propagation() {
|
||||
let mut app = App::new();
|
||||
app.add_systems(Update, visibility_propagate_system);
|
||||
|
||||
let root1 = app.world_mut().spawn(Visibility::Hidden).id();
|
||||
let root1_child1 = app.world_mut().spawn(Visibility::default()).id();
|
||||
let root1_child2 = app.world_mut().spawn(Visibility::Hidden).id();
|
||||
let root1_child1_grandchild1 = app.world_mut().spawn(Visibility::default()).id();
|
||||
let root1_child2_grandchild1 = app.world_mut().spawn(Visibility::default()).id();
|
||||
|
||||
app.world_mut()
|
||||
.entity_mut(root1)
|
||||
.add_children(&[root1_child1, root1_child2]);
|
||||
app.world_mut()
|
||||
.entity_mut(root1_child1)
|
||||
.add_children(&[root1_child1_grandchild1]);
|
||||
app.world_mut()
|
||||
.entity_mut(root1_child2)
|
||||
.add_children(&[root1_child2_grandchild1]);
|
||||
|
||||
let root2 = app.world_mut().spawn(Visibility::default()).id();
|
||||
let root2_child1 = app.world_mut().spawn(Visibility::default()).id();
|
||||
let root2_child2 = app.world_mut().spawn(Visibility::Hidden).id();
|
||||
let root2_child1_grandchild1 = app.world_mut().spawn(Visibility::default()).id();
|
||||
let root2_child2_grandchild1 = app.world_mut().spawn(Visibility::default()).id();
|
||||
|
||||
app.world_mut()
|
||||
.entity_mut(root2)
|
||||
.add_children(&[root2_child1, root2_child2]);
|
||||
app.world_mut()
|
||||
.entity_mut(root2_child1)
|
||||
.add_children(&[root2_child1_grandchild1]);
|
||||
app.world_mut()
|
||||
.entity_mut(root2_child2)
|
||||
.add_children(&[root2_child2_grandchild1]);
|
||||
|
||||
app.update();
|
||||
|
||||
let is_visible = |e: Entity| {
|
||||
app.world()
|
||||
.entity(e)
|
||||
.get::<InheritedVisibility>()
|
||||
.unwrap()
|
||||
.get()
|
||||
};
|
||||
assert!(
|
||||
!is_visible(root1),
|
||||
"invisibility propagates down tree from root"
|
||||
);
|
||||
assert!(
|
||||
!is_visible(root1_child1),
|
||||
"invisibility propagates down tree from root"
|
||||
);
|
||||
assert!(
|
||||
!is_visible(root1_child2),
|
||||
"invisibility propagates down tree from root"
|
||||
);
|
||||
assert!(
|
||||
!is_visible(root1_child1_grandchild1),
|
||||
"invisibility propagates down tree from root"
|
||||
);
|
||||
assert!(
|
||||
!is_visible(root1_child2_grandchild1),
|
||||
"invisibility propagates down tree from root"
|
||||
);
|
||||
|
||||
assert!(
|
||||
is_visible(root2),
|
||||
"visibility propagates down tree from root"
|
||||
);
|
||||
assert!(
|
||||
is_visible(root2_child1),
|
||||
"visibility propagates down tree from root"
|
||||
);
|
||||
assert!(
|
||||
!is_visible(root2_child2),
|
||||
"visibility propagates down tree from root, but local invisibility is preserved"
|
||||
);
|
||||
assert!(
|
||||
is_visible(root2_child1_grandchild1),
|
||||
"visibility propagates down tree from root"
|
||||
);
|
||||
assert!(
|
||||
!is_visible(root2_child2_grandchild1),
|
||||
"child's invisibility propagates down to grandchild"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_visibility_propagation_on_parent_change() {
|
||||
// Setup the world and schedule
|
||||
let mut app = App::new();
|
||||
|
||||
app.add_systems(Update, visibility_propagate_system);
|
||||
|
||||
// Create entities with visibility and hierarchy
|
||||
let parent1 = app.world_mut().spawn((Visibility::Hidden,)).id();
|
||||
let parent2 = app.world_mut().spawn((Visibility::Visible,)).id();
|
||||
let child1 = app.world_mut().spawn((Visibility::Inherited,)).id();
|
||||
let child2 = app.world_mut().spawn((Visibility::Inherited,)).id();
|
||||
|
||||
// Build hierarchy
|
||||
app.world_mut()
|
||||
.entity_mut(parent1)
|
||||
.add_children(&[child1, child2]);
|
||||
|
||||
// Run the system initially to set up visibility
|
||||
app.update();
|
||||
|
||||
// Change parent visibility to Hidden
|
||||
app.world_mut()
|
||||
.entity_mut(parent2)
|
||||
.insert(Visibility::Visible);
|
||||
// Simulate a change in the parent component
|
||||
app.world_mut().entity_mut(child2).insert(ChildOf(parent2)); // example of changing parent
|
||||
|
||||
// Run the system again to propagate changes
|
||||
app.update();
|
||||
|
||||
let is_visible = |e: Entity| {
|
||||
app.world()
|
||||
.entity(e)
|
||||
.get::<InheritedVisibility>()
|
||||
.unwrap()
|
||||
.get()
|
||||
};
|
||||
|
||||
// Retrieve and assert visibility
|
||||
|
||||
assert!(
|
||||
!is_visible(child1),
|
||||
"Child1 should inherit visibility from parent"
|
||||
);
|
||||
|
||||
assert!(
|
||||
is_visible(child2),
|
||||
"Child2 should inherit visibility from parent"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visibility_propagation_unconditional_visible() {
|
||||
use Visibility::{Hidden, Inherited, Visible};
|
||||
|
||||
let mut app = App::new();
|
||||
app.add_systems(Update, visibility_propagate_system);
|
||||
|
||||
let root1 = app.world_mut().spawn(Visible).id();
|
||||
let root1_child1 = app.world_mut().spawn(Inherited).id();
|
||||
let root1_child2 = app.world_mut().spawn(Hidden).id();
|
||||
let root1_child1_grandchild1 = app.world_mut().spawn(Visible).id();
|
||||
let root1_child2_grandchild1 = app.world_mut().spawn(Visible).id();
|
||||
|
||||
let root2 = app.world_mut().spawn(Inherited).id();
|
||||
let root3 = app.world_mut().spawn(Hidden).id();
|
||||
|
||||
app.world_mut()
|
||||
.entity_mut(root1)
|
||||
.add_children(&[root1_child1, root1_child2]);
|
||||
app.world_mut()
|
||||
.entity_mut(root1_child1)
|
||||
.add_children(&[root1_child1_grandchild1]);
|
||||
app.world_mut()
|
||||
.entity_mut(root1_child2)
|
||||
.add_children(&[root1_child2_grandchild1]);
|
||||
|
||||
app.update();
|
||||
|
||||
let is_visible = |e: Entity| {
|
||||
app.world()
|
||||
.entity(e)
|
||||
.get::<InheritedVisibility>()
|
||||
.unwrap()
|
||||
.get()
|
||||
};
|
||||
assert!(
|
||||
is_visible(root1),
|
||||
"an unconditionally visible root is visible"
|
||||
);
|
||||
assert!(
|
||||
is_visible(root1_child1),
|
||||
"an inheriting child of an unconditionally visible parent is visible"
|
||||
);
|
||||
assert!(
|
||||
!is_visible(root1_child2),
|
||||
"a hidden child on an unconditionally visible parent is hidden"
|
||||
);
|
||||
assert!(
|
||||
is_visible(root1_child1_grandchild1),
|
||||
"an unconditionally visible child of an inheriting parent is visible"
|
||||
);
|
||||
assert!(
|
||||
is_visible(root1_child2_grandchild1),
|
||||
"an unconditionally visible child of a hidden parent is visible"
|
||||
);
|
||||
assert!(is_visible(root2), "an inheriting root is visible");
|
||||
assert!(!is_visible(root3), "a hidden root is hidden");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visibility_propagation_change_detection() {
|
||||
let mut world = World::new();
|
||||
let mut schedule = Schedule::default();
|
||||
schedule.add_systems(visibility_propagate_system);
|
||||
|
||||
// Set up an entity hierarchy.
|
||||
|
||||
let id1 = world.spawn(Visibility::default()).id();
|
||||
|
||||
let id2 = world.spawn(Visibility::default()).id();
|
||||
world.entity_mut(id1).add_children(&[id2]);
|
||||
|
||||
let id3 = world.spawn(Visibility::Hidden).id();
|
||||
world.entity_mut(id2).add_children(&[id3]);
|
||||
|
||||
let id4 = world.spawn(Visibility::default()).id();
|
||||
world.entity_mut(id3).add_children(&[id4]);
|
||||
|
||||
// Test the hierarchy.
|
||||
|
||||
// Make sure the hierarchy is up-to-date.
|
||||
schedule.run(&mut world);
|
||||
world.clear_trackers();
|
||||
|
||||
let mut q = world.query::<Ref<InheritedVisibility>>();
|
||||
|
||||
assert!(!q.get(&world, id1).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id2).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id3).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id4).unwrap().is_changed());
|
||||
|
||||
world.clear_trackers();
|
||||
world.entity_mut(id1).insert(Visibility::Hidden);
|
||||
schedule.run(&mut world);
|
||||
|
||||
assert!(q.get(&world, id1).unwrap().is_changed());
|
||||
assert!(q.get(&world, id2).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id3).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id4).unwrap().is_changed());
|
||||
|
||||
world.clear_trackers();
|
||||
schedule.run(&mut world);
|
||||
|
||||
assert!(!q.get(&world, id1).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id2).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id3).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id4).unwrap().is_changed());
|
||||
|
||||
world.clear_trackers();
|
||||
world.entity_mut(id3).insert(Visibility::Inherited);
|
||||
schedule.run(&mut world);
|
||||
|
||||
assert!(!q.get(&world, id1).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id2).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id3).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id4).unwrap().is_changed());
|
||||
|
||||
world.clear_trackers();
|
||||
world.entity_mut(id2).insert(Visibility::Visible);
|
||||
schedule.run(&mut world);
|
||||
|
||||
assert!(!q.get(&world, id1).unwrap().is_changed());
|
||||
assert!(q.get(&world, id2).unwrap().is_changed());
|
||||
assert!(q.get(&world, id3).unwrap().is_changed());
|
||||
assert!(q.get(&world, id4).unwrap().is_changed());
|
||||
|
||||
world.clear_trackers();
|
||||
schedule.run(&mut world);
|
||||
|
||||
assert!(!q.get(&world, id1).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id2).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id3).unwrap().is_changed());
|
||||
assert!(!q.get(&world, id4).unwrap().is_changed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visibility_propagation_with_invalid_parent() {
|
||||
let mut world = World::new();
|
||||
let mut schedule = Schedule::default();
|
||||
schedule.add_systems(visibility_propagate_system);
|
||||
|
||||
let parent = world.spawn(()).id();
|
||||
let child = world.spawn(Visibility::default()).id();
|
||||
world.entity_mut(parent).add_children(&[child]);
|
||||
|
||||
schedule.run(&mut world);
|
||||
world.clear_trackers();
|
||||
|
||||
let child_visible = world.entity(child).get::<InheritedVisibility>().unwrap().0;
|
||||
// defaults to same behavior of parent not found: visible = true
|
||||
assert!(child_visible);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_visibility_enum_size() {
|
||||
assert_eq!(1, size_of::<Visibility>());
|
||||
assert_eq!(1, size_of::<Option<Visibility>>());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,37 +1,26 @@
|
||||
//! Specific distances from the camera in which entities are visible, also known
|
||||
//! as *hierarchical levels of detail* or *HLOD*s.
|
||||
|
||||
use core::{
|
||||
hash::{Hash, Hasher},
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
use bevy_app::{App, Plugin, PostUpdate};
|
||||
use super::VisibilityRange;
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_ecs::{
|
||||
component::Component,
|
||||
entity::{Entity, EntityHashMap},
|
||||
entity::Entity,
|
||||
lifecycle::RemovedComponents,
|
||||
query::{Changed, With},
|
||||
reflect::ReflectComponent,
|
||||
query::Changed,
|
||||
resource::Resource,
|
||||
schedule::IntoScheduleConfigs as _,
|
||||
system::{Local, Query, Res, ResMut},
|
||||
system::{Query, Res, ResMut},
|
||||
};
|
||||
use bevy_math::{vec4, FloatOrd, Vec4};
|
||||
use bevy_math::{vec4, Vec4};
|
||||
use bevy_platform::collections::HashMap;
|
||||
use bevy_reflect::Reflect;
|
||||
use bevy_transform::components::GlobalTransform;
|
||||
use bevy_utils::{prelude::default, Parallel};
|
||||
use bevy_utils::prelude::default;
|
||||
use nonmax::NonMaxU16;
|
||||
use wgpu::{BufferBindingType, BufferUsages};
|
||||
|
||||
use super::{check_visibility, VisibilitySystems};
|
||||
use crate::sync_world::{MainEntity, MainEntityHashMap};
|
||||
use crate::{
|
||||
camera::Camera,
|
||||
primitives::Aabb,
|
||||
render_resource::BufferVec,
|
||||
renderer::{RenderDevice, RenderQueue},
|
||||
sync_world::{MainEntity, MainEntityHashMap},
|
||||
Extract, ExtractSchedule, Render, RenderApp, RenderSystems,
|
||||
};
|
||||
|
||||
@ -48,21 +37,12 @@ pub const VISIBILITY_RANGES_STORAGE_BUFFER_COUNT: u32 = 4;
|
||||
/// buffer instead (most notably, on WebGL 2).
|
||||
const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: usize = 64;
|
||||
|
||||
/// A plugin that enables [`VisibilityRange`]s, which allow entities to be
|
||||
/// A plugin that enables [`RenderVisibilityRanges`]s, which allow entities to be
|
||||
/// hidden or shown based on distance to the camera.
|
||||
pub struct VisibilityRangePlugin;
|
||||
pub struct RenderVisibilityRangePlugin;
|
||||
|
||||
impl Plugin for VisibilityRangePlugin {
|
||||
impl Plugin for RenderVisibilityRangePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.register_type::<VisibilityRange>()
|
||||
.init_resource::<VisibleEntityRanges>()
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
check_visibility_ranges
|
||||
.in_set(VisibilitySystems::CheckVisibility)
|
||||
.before(check_visibility),
|
||||
);
|
||||
|
||||
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
||||
return;
|
||||
};
|
||||
@ -77,137 +57,6 @@ impl Plugin for VisibilityRangePlugin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Specifies the range of distances that this entity must be from the camera in
|
||||
/// order to be rendered.
|
||||
///
|
||||
/// This is also known as *hierarchical level of detail* or *HLOD*.
|
||||
///
|
||||
/// Use this component when you want to render a high-polygon mesh when the
|
||||
/// camera is close and a lower-polygon mesh when the camera is far away. This
|
||||
/// is a common technique for improving performance, because fine details are
|
||||
/// hard to see in a mesh at a distance. To avoid an artifact known as *popping*
|
||||
/// between levels, each level has a *margin*, within which the object
|
||||
/// transitions gradually from invisible to visible using a dithering effect.
|
||||
///
|
||||
/// You can also use this feature to replace multiple meshes with a single mesh
|
||||
/// when the camera is distant. This is the reason for the term "*hierarchical*
|
||||
/// level of detail". Reducing the number of meshes can be useful for reducing
|
||||
/// drawcall count. Note that you must place the [`VisibilityRange`] component
|
||||
/// on each entity you want to be part of a LOD group, as [`VisibilityRange`]
|
||||
/// isn't automatically propagated down to children.
|
||||
///
|
||||
/// A typical use of this feature might look like this:
|
||||
///
|
||||
/// | Entity | `start_margin` | `end_margin` |
|
||||
/// |-------------------------|----------------|--------------|
|
||||
/// | Root | N/A | N/A |
|
||||
/// | ├─ High-poly mesh | [0, 0) | [20, 25) |
|
||||
/// | ├─ Low-poly mesh | [20, 25) | [70, 75) |
|
||||
/// | └─ Billboard *imposter* | [70, 75) | [150, 160) |
|
||||
///
|
||||
/// With this setup, the user will see a high-poly mesh when the camera is
|
||||
/// closer than 20 units. As the camera zooms out, between 20 units to 25 units,
|
||||
/// the high-poly mesh will gradually fade to a low-poly mesh. When the camera
|
||||
/// is 70 to 75 units away, the low-poly mesh will fade to a single textured
|
||||
/// quad. And between 150 and 160 units, the object fades away entirely. Note
|
||||
/// that the `end_margin` of a higher LOD is always identical to the
|
||||
/// `start_margin` of the next lower LOD; this is important for the crossfade
|
||||
/// effect to function properly.
|
||||
#[derive(Component, Clone, PartialEq, Default, Reflect)]
|
||||
#[reflect(Component, PartialEq, Hash, Clone)]
|
||||
pub struct VisibilityRange {
|
||||
/// The range of distances, in world units, between which this entity will
|
||||
/// smoothly fade into view as the camera zooms out.
|
||||
///
|
||||
/// If the start and end of this range are identical, the transition will be
|
||||
/// abrupt, with no crossfading.
|
||||
///
|
||||
/// `start_margin.end` must be less than or equal to `end_margin.start`.
|
||||
pub start_margin: Range<f32>,
|
||||
|
||||
/// The range of distances, in world units, between which this entity will
|
||||
/// smoothly fade out of view as the camera zooms out.
|
||||
///
|
||||
/// If the start and end of this range are identical, the transition will be
|
||||
/// abrupt, with no crossfading.
|
||||
///
|
||||
/// `end_margin.start` must be greater than or equal to `start_margin.end`.
|
||||
pub end_margin: Range<f32>,
|
||||
|
||||
/// If set to true, Bevy will use the center of the axis-aligned bounding
|
||||
/// box ([`Aabb`]) as the position of the mesh for the purposes of
|
||||
/// visibility range computation.
|
||||
///
|
||||
/// Otherwise, if this field is set to false, Bevy will use the origin of
|
||||
/// the mesh as the mesh's position.
|
||||
///
|
||||
/// Usually you will want to leave this set to false, because different LODs
|
||||
/// may have different AABBs, and smooth crossfades between LOD levels
|
||||
/// require that all LODs of a mesh be at *precisely* the same position. If
|
||||
/// you aren't using crossfading, however, and your meshes aren't centered
|
||||
/// around their origins, then this flag may be useful.
|
||||
pub use_aabb: bool,
|
||||
}
|
||||
|
||||
impl Eq for VisibilityRange {}
|
||||
|
||||
impl Hash for VisibilityRange {
|
||||
fn hash<H>(&self, state: &mut H)
|
||||
where
|
||||
H: Hasher,
|
||||
{
|
||||
FloatOrd(self.start_margin.start).hash(state);
|
||||
FloatOrd(self.start_margin.end).hash(state);
|
||||
FloatOrd(self.end_margin.start).hash(state);
|
||||
FloatOrd(self.end_margin.end).hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl VisibilityRange {
|
||||
/// Creates a new *abrupt* visibility range, with no crossfade.
|
||||
///
|
||||
/// There will be no crossfade; the object will immediately vanish if the
|
||||
/// camera is closer than `start` units or farther than `end` units from the
|
||||
/// model.
|
||||
///
|
||||
/// The `start` value must be less than or equal to the `end` value.
|
||||
#[inline]
|
||||
pub fn abrupt(start: f32, end: f32) -> Self {
|
||||
Self {
|
||||
start_margin: start..start,
|
||||
end_margin: end..end,
|
||||
use_aabb: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if both the start and end transitions for this range are
|
||||
/// abrupt: that is, there is no crossfading.
|
||||
#[inline]
|
||||
pub fn is_abrupt(&self) -> bool {
|
||||
self.start_margin.start == self.start_margin.end
|
||||
&& self.end_margin.start == self.end_margin.end
|
||||
}
|
||||
|
||||
/// Returns true if the object will be visible at all, given a camera
|
||||
/// `camera_distance` units away.
|
||||
///
|
||||
/// Any amount of visibility, even with the heaviest dithering applied, is
|
||||
/// considered visible according to this check.
|
||||
#[inline]
|
||||
pub fn is_visible_at_all(&self, camera_distance: f32) -> bool {
|
||||
camera_distance >= self.start_margin.start && camera_distance < self.end_margin.end
|
||||
}
|
||||
|
||||
/// Returns true if the object is completely invisible, given a camera
|
||||
/// `camera_distance` units away.
|
||||
///
|
||||
/// This is equivalent to `!VisibilityRange::is_visible_at_all()`.
|
||||
#[inline]
|
||||
pub fn is_culled(&self, camera_distance: f32) -> bool {
|
||||
!self.is_visible_at_all(camera_distance)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores information related to [`VisibilityRange`]s in the render world.
|
||||
#[derive(Resource)]
|
||||
pub struct RenderVisibilityRanges {
|
||||
@ -313,128 +162,6 @@ impl RenderVisibilityRanges {
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores which entities are in within the [`VisibilityRange`]s of views.
|
||||
///
|
||||
/// This doesn't store the results of frustum or occlusion culling; use
|
||||
/// [`super::ViewVisibility`] for that. Thus entities in this list may not
|
||||
/// actually be visible.
|
||||
///
|
||||
/// For efficiency, these tables only store entities that have
|
||||
/// [`VisibilityRange`] components. Entities without such a component won't be
|
||||
/// in these tables at all.
|
||||
///
|
||||
/// The table is indexed by entity and stores a 32-bit bitmask with one bit for
|
||||
/// each camera, where a 0 bit corresponds to "out of range" and a 1 bit
|
||||
/// corresponds to "in range". Hence it's limited to storing information for 32
|
||||
/// views.
|
||||
#[derive(Resource, Default)]
|
||||
pub struct VisibleEntityRanges {
|
||||
/// Stores which bit index each view corresponds to.
|
||||
views: EntityHashMap<u8>,
|
||||
|
||||
/// Stores a bitmask in which each view has a single bit.
|
||||
///
|
||||
/// A 0 bit for a view corresponds to "out of range"; a 1 bit corresponds to
|
||||
/// "in range".
|
||||
entities: EntityHashMap<u32>,
|
||||
}
|
||||
|
||||
impl VisibleEntityRanges {
|
||||
/// Clears out the [`VisibleEntityRanges`] in preparation for a new frame.
|
||||
fn clear(&mut self) {
|
||||
self.views.clear();
|
||||
self.entities.clear();
|
||||
}
|
||||
|
||||
/// Returns true if the entity is in range of the given camera.
|
||||
///
|
||||
/// This only checks [`VisibilityRange`]s and doesn't perform any frustum or
|
||||
/// occlusion culling. Thus the entity might not *actually* be visible.
|
||||
///
|
||||
/// The entity is assumed to have a [`VisibilityRange`] component. If the
|
||||
/// entity doesn't have that component, this method will return false.
|
||||
#[inline]
|
||||
pub fn entity_is_in_range_of_view(&self, entity: Entity, view: Entity) -> bool {
|
||||
let Some(visibility_bitmask) = self.entities.get(&entity) else {
|
||||
return false;
|
||||
};
|
||||
let Some(view_index) = self.views.get(&view) else {
|
||||
return false;
|
||||
};
|
||||
(visibility_bitmask & (1 << view_index)) != 0
|
||||
}
|
||||
|
||||
/// Returns true if the entity is in range of any view.
|
||||
///
|
||||
/// This only checks [`VisibilityRange`]s and doesn't perform any frustum or
|
||||
/// occlusion culling. Thus the entity might not *actually* be visible.
|
||||
///
|
||||
/// The entity is assumed to have a [`VisibilityRange`] component. If the
|
||||
/// entity doesn't have that component, this method will return false.
|
||||
#[inline]
|
||||
pub fn entity_is_in_range_of_any_view(&self, entity: Entity) -> bool {
|
||||
self.entities.contains_key(&entity)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks all entities against all views in order to determine which entities
|
||||
/// with [`VisibilityRange`]s are potentially visible.
|
||||
///
|
||||
/// This only checks distance from the camera and doesn't frustum or occlusion
|
||||
/// cull.
|
||||
pub fn check_visibility_ranges(
|
||||
mut visible_entity_ranges: ResMut<VisibleEntityRanges>,
|
||||
view_query: Query<(Entity, &GlobalTransform), With<Camera>>,
|
||||
mut par_local: Local<Parallel<Vec<(Entity, u32)>>>,
|
||||
entity_query: Query<(Entity, &GlobalTransform, Option<&Aabb>, &VisibilityRange)>,
|
||||
) {
|
||||
visible_entity_ranges.clear();
|
||||
|
||||
// Early out if the visibility range feature isn't in use.
|
||||
if entity_query.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Assign an index to each view.
|
||||
let mut views = vec![];
|
||||
for (view, view_transform) in view_query.iter().take(32) {
|
||||
let view_index = views.len() as u8;
|
||||
visible_entity_ranges.views.insert(view, view_index);
|
||||
views.push((view, view_transform.translation_vec3a()));
|
||||
}
|
||||
|
||||
// Check each entity/view pair. Only consider entities with
|
||||
// [`VisibilityRange`] components.
|
||||
entity_query.par_iter().for_each(
|
||||
|(entity, entity_transform, maybe_model_aabb, visibility_range)| {
|
||||
let mut visibility = 0;
|
||||
for (view_index, &(_, view_position)) in views.iter().enumerate() {
|
||||
// If instructed to use the AABB and the model has one, use its
|
||||
// center as the model position. Otherwise, use the model's
|
||||
// translation.
|
||||
let model_position = match (visibility_range.use_aabb, maybe_model_aabb) {
|
||||
(true, Some(model_aabb)) => entity_transform
|
||||
.affine()
|
||||
.transform_point3a(model_aabb.center),
|
||||
_ => entity_transform.translation_vec3a(),
|
||||
};
|
||||
|
||||
if visibility_range.is_visible_at_all((view_position - model_position).length()) {
|
||||
visibility |= 1 << view_index;
|
||||
}
|
||||
}
|
||||
|
||||
// Invisible entities have no entry at all in the hash map. This speeds
|
||||
// up checks slightly in this common case.
|
||||
if visibility != 0 {
|
||||
par_local.borrow_local_mut().push((entity, visibility));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
visible_entity_ranges.entities.extend(par_local.drain());
|
||||
}
|
||||
|
||||
/// Extracts all [`VisibilityRange`] components from the main world to the
|
||||
/// render world and inserts them into [`RenderVisibilityRanges`].
|
||||
pub fn extract_visibility_ranges(
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use super::ExtractedWindows;
|
||||
use crate::{
|
||||
camera::{ManualTextureViewHandle, ManualTextureViews, NormalizedRenderTarget, RenderTarget},
|
||||
camera::{NormalizedRenderTarget, ToNormalizedRenderTarget as _},
|
||||
gpu_readback,
|
||||
prelude::Shader,
|
||||
render_asset::{RenderAssetUsages, RenderAssets},
|
||||
@ -11,13 +11,14 @@ use crate::{
|
||||
SpecializedRenderPipelines, Texture, TextureUsages, TextureView, VertexState,
|
||||
},
|
||||
renderer::RenderDevice,
|
||||
texture::{GpuImage, OutputColorAttachment},
|
||||
texture::{GpuImage, ManualTextureViews, OutputColorAttachment},
|
||||
view::{prepare_view_attachments, prepare_view_targets, ViewTargetAttachments, WindowSurfaces},
|
||||
ExtractSchedule, MainWorld, Render, RenderApp, RenderSystems,
|
||||
};
|
||||
use alloc::{borrow::Cow, sync::Arc};
|
||||
use bevy_app::{First, Plugin, Update};
|
||||
use bevy_asset::{embedded_asset, load_embedded_asset, Handle};
|
||||
use bevy_camera::{ManualTextureViewHandle, RenderTarget};
|
||||
use bevy_derive::{Deref, DerefMut};
|
||||
use bevy_ecs::{
|
||||
entity::EntityHashMap, event::event_update_system, prelude::*, system::SystemState,
|
||||
|
||||
@ -51,8 +51,8 @@ use bevy_image::{prelude::*, TextureAtlasPlugin};
|
||||
use bevy_render::{
|
||||
batching::sort_binned_render_phase,
|
||||
load_shader_library,
|
||||
mesh::{Mesh, Mesh2d, MeshAabb},
|
||||
primitives::Aabb,
|
||||
mesh::{Mesh, Mesh2d},
|
||||
primitives::{Aabb, MeshAabb},
|
||||
render_phase::AddRenderCommand,
|
||||
render_resource::SpecializedRenderPipelines,
|
||||
view::{NoFrustumCulling, VisibilitySystems},
|
||||
|
||||
@ -12,7 +12,11 @@ use bevy_input::{mouse::MouseButton, touch::Touches, ButtonInput};
|
||||
use bevy_math::Vec2;
|
||||
use bevy_platform::collections::HashMap;
|
||||
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
||||
use bevy_render::{camera::NormalizedRenderTarget, prelude::Camera, view::InheritedVisibility};
|
||||
use bevy_render::{
|
||||
camera::{NormalizedRenderTarget, ToNormalizedRenderTarget as _},
|
||||
prelude::Camera,
|
||||
view::InheritedVisibility,
|
||||
};
|
||||
use bevy_window::{PrimaryWindow, Window};
|
||||
|
||||
use smallvec::SmallVec;
|
||||
|
||||
@ -368,7 +368,7 @@ mod tests {
|
||||
use bevy_image::Image;
|
||||
use bevy_math::{Rect, UVec2, Vec2};
|
||||
use bevy_platform::collections::HashMap;
|
||||
use bevy_render::{camera::ManualTextureViews, prelude::Camera};
|
||||
use bevy_render::{prelude::Camera, texture::ManualTextureViews};
|
||||
use bevy_transform::systems::mark_dirty_trees;
|
||||
use bevy_transform::systems::{propagate_parent_transforms, sync_simple_transforms};
|
||||
use bevy_utils::prelude::default;
|
||||
|
||||
@ -221,8 +221,8 @@ mod tests {
|
||||
use bevy_image::Image;
|
||||
use bevy_math::UVec2;
|
||||
use bevy_render::camera::Camera;
|
||||
use bevy_render::camera::ManualTextureViews;
|
||||
use bevy_render::camera::RenderTarget;
|
||||
use bevy_render::texture::ManualTextureViews;
|
||||
use bevy_utils::default;
|
||||
use bevy_window::PrimaryWindow;
|
||||
use bevy_window::Window;
|
||||
|
||||
@ -16,7 +16,6 @@ use bevy_ecs::{
|
||||
use bevy_image::BevyDefault as _;
|
||||
use bevy_math::{vec2, Affine2, FloatOrd, Rect, Vec2};
|
||||
use bevy_render::sync_world::{MainEntity, TemporaryRenderEntity};
|
||||
use bevy_render::RenderApp;
|
||||
use bevy_render::{
|
||||
render_phase::*,
|
||||
render_resource::{binding_types::uniform_buffer, *},
|
||||
@ -24,6 +23,7 @@ use bevy_render::{
|
||||
view::*,
|
||||
Extract, ExtractSchedule, Render, RenderSystems,
|
||||
};
|
||||
use bevy_render::{RenderApp, RenderStartup};
|
||||
use bevy_ui::{
|
||||
BoxShadow, CalculatedClip, ComputedNode, ComputedNodeTarget, ResolvedBorderRadius,
|
||||
UiGlobalTransform, Val,
|
||||
@ -48,6 +48,7 @@ impl Plugin for BoxShadowPlugin {
|
||||
.init_resource::<ExtractedBoxShadows>()
|
||||
.init_resource::<BoxShadowMeta>()
|
||||
.init_resource::<SpecializedRenderPipelines<BoxShadowPipeline>>()
|
||||
.add_systems(RenderStartup, init_box_shadow_pipeline)
|
||||
.add_systems(
|
||||
ExtractSchedule,
|
||||
extract_shadows.in_set(RenderUiSystems::ExtractBoxShadows),
|
||||
@ -61,12 +62,6 @@ impl Plugin for BoxShadowPlugin {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&self, app: &mut App) {
|
||||
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||
render_app.init_resource::<BoxShadowPipeline>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
@ -111,10 +106,11 @@ pub struct BoxShadowPipeline {
|
||||
pub shader: Handle<Shader>,
|
||||
}
|
||||
|
||||
impl FromWorld for BoxShadowPipeline {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let render_device = world.resource::<RenderDevice>();
|
||||
|
||||
pub fn init_box_shadow_pipeline(
|
||||
mut commands: Commands,
|
||||
render_device: Res<RenderDevice>,
|
||||
asset_server: Res<AssetServer>,
|
||||
) {
|
||||
let view_layout = render_device.create_bind_group_layout(
|
||||
"box_shadow_view_layout",
|
||||
&BindGroupLayoutEntries::single(
|
||||
@ -123,11 +119,10 @@ impl FromWorld for BoxShadowPipeline {
|
||||
),
|
||||
);
|
||||
|
||||
BoxShadowPipeline {
|
||||
commands.insert_resource(BoxShadowPipeline {
|
||||
view_layout,
|
||||
shader: load_embedded_asset!(world, "box_shadow.wgsl"),
|
||||
}
|
||||
}
|
||||
shader: load_embedded_asset!(asset_server.as_ref(), "box_shadow.wgsl"),
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
|
||||
|
||||
@ -21,7 +21,6 @@ use bevy_math::{
|
||||
FloatOrd, Rect, Vec2,
|
||||
};
|
||||
use bevy_math::{Affine2, Vec2Swizzles};
|
||||
use bevy_render::sync_world::MainEntity;
|
||||
use bevy_render::{
|
||||
render_phase::*,
|
||||
render_resource::{binding_types::uniform_buffer, *},
|
||||
@ -30,6 +29,7 @@ use bevy_render::{
|
||||
view::*,
|
||||
Extract, ExtractSchedule, Render, RenderSystems,
|
||||
};
|
||||
use bevy_render::{sync_world::MainEntity, RenderStartup};
|
||||
use bevy_sprite::BorderRect;
|
||||
use bevy_ui::{
|
||||
BackgroundGradient, BorderGradient, ColorStop, ConicGradient, Gradient,
|
||||
@ -51,6 +51,7 @@ impl Plugin for GradientPlugin {
|
||||
.init_resource::<ExtractedColorStops>()
|
||||
.init_resource::<GradientMeta>()
|
||||
.init_resource::<SpecializedRenderPipelines<GradientPipeline>>()
|
||||
.add_systems(RenderStartup, init_gradient_pipeline)
|
||||
.add_systems(
|
||||
ExtractSchedule,
|
||||
extract_gradients
|
||||
@ -66,12 +67,6 @@ impl Plugin for GradientPlugin {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&self, app: &mut App) {
|
||||
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||
render_app.init_resource::<GradientPipeline>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
@ -102,10 +97,11 @@ pub struct GradientPipeline {
|
||||
pub shader: Handle<Shader>,
|
||||
}
|
||||
|
||||
impl FromWorld for GradientPipeline {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let render_device = world.resource::<RenderDevice>();
|
||||
|
||||
pub fn init_gradient_pipeline(
|
||||
mut commands: Commands,
|
||||
render_device: Res<RenderDevice>,
|
||||
asset_server: Res<AssetServer>,
|
||||
) {
|
||||
let view_layout = render_device.create_bind_group_layout(
|
||||
"ui_gradient_view_layout",
|
||||
&BindGroupLayoutEntries::single(
|
||||
@ -114,11 +110,10 @@ impl FromWorld for GradientPipeline {
|
||||
),
|
||||
);
|
||||
|
||||
GradientPipeline {
|
||||
commands.insert_resource(GradientPipeline {
|
||||
view_layout,
|
||||
shader: load_embedded_asset!(world, "gradient.wgsl"),
|
||||
}
|
||||
}
|
||||
shader: load_embedded_asset!(asset_server.as_ref(), "gradient.wgsl"),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn compute_gradient_line_length(angle: f32, size: Vec2) -> f32 {
|
||||
|
||||
@ -36,7 +36,6 @@ use bevy_ecs::prelude::*;
|
||||
use bevy_ecs::system::SystemParam;
|
||||
use bevy_image::prelude::*;
|
||||
use bevy_math::{Affine2, FloatOrd, Mat4, Rect, UVec4, Vec2};
|
||||
use bevy_render::load_shader_library;
|
||||
use bevy_render::render_graph::{NodeRunError, RenderGraphContext};
|
||||
use bevy_render::render_phase::ViewSortedRenderPhases;
|
||||
use bevy_render::renderer::RenderContext;
|
||||
@ -53,6 +52,7 @@ use bevy_render::{
|
||||
view::{ExtractedView, ViewUniforms},
|
||||
Extract, RenderApp, RenderSystems,
|
||||
};
|
||||
use bevy_render::{load_shader_library, RenderStartup};
|
||||
use bevy_render::{
|
||||
render_phase::{PhaseItem, PhaseItemExtraIndex},
|
||||
sync_world::{RenderEntity, TemporaryRenderEntity},
|
||||
@ -243,6 +243,7 @@ impl Plugin for UiRenderPlugin {
|
||||
)
|
||||
.chain(),
|
||||
)
|
||||
.add_systems(RenderStartup, init_ui_pipeline)
|
||||
.add_systems(
|
||||
ExtractSchedule,
|
||||
(
|
||||
@ -292,14 +293,6 @@ impl Plugin for UiRenderPlugin {
|
||||
app.add_plugins(GradientPlugin);
|
||||
app.add_plugins(BoxShadowPlugin);
|
||||
}
|
||||
|
||||
fn finish(&self, app: &mut App) {
|
||||
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
||||
return;
|
||||
};
|
||||
|
||||
render_app.init_resource::<UiPipeline>();
|
||||
}
|
||||
}
|
||||
|
||||
fn get_ui_graph(render_app: &mut SubApp) -> RenderGraph {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use bevy_asset::{load_embedded_asset, Handle};
|
||||
use bevy_asset::{load_embedded_asset, AssetServer, Handle};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_image::BevyDefault as _;
|
||||
use bevy_render::{
|
||||
@ -18,10 +18,11 @@ pub struct UiPipeline {
|
||||
pub shader: Handle<Shader>,
|
||||
}
|
||||
|
||||
impl FromWorld for UiPipeline {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let render_device = world.resource::<RenderDevice>();
|
||||
|
||||
pub fn init_ui_pipeline(
|
||||
mut commands: Commands,
|
||||
render_device: Res<RenderDevice>,
|
||||
asset_server: Res<AssetServer>,
|
||||
) {
|
||||
let view_layout = render_device.create_bind_group_layout(
|
||||
"ui_view_layout",
|
||||
&BindGroupLayoutEntries::single(
|
||||
@ -41,12 +42,11 @@ impl FromWorld for UiPipeline {
|
||||
),
|
||||
);
|
||||
|
||||
UiPipeline {
|
||||
commands.insert_resource(UiPipeline {
|
||||
view_layout,
|
||||
image_layout,
|
||||
shader: load_embedded_asset!(world, "ui.wgsl"),
|
||||
}
|
||||
}
|
||||
shader: load_embedded_asset!(asset_server.as_ref(), "ui.wgsl"),
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
|
||||
|
||||
@ -8,11 +8,9 @@ use bevy_ecs::{
|
||||
lifetimeless::{Read, SRes},
|
||||
*,
|
||||
},
|
||||
world::{FromWorld, World},
|
||||
};
|
||||
use bevy_image::BevyDefault as _;
|
||||
use bevy_math::{Affine2, FloatOrd, Rect, Vec2};
|
||||
use bevy_render::RenderApp;
|
||||
use bevy_render::{
|
||||
globals::{GlobalsBuffer, GlobalsUniform},
|
||||
load_shader_library,
|
||||
@ -24,6 +22,7 @@ use bevy_render::{
|
||||
view::*,
|
||||
Extract, ExtractSchedule, Render, RenderSystems,
|
||||
};
|
||||
use bevy_render::{RenderApp, RenderStartup};
|
||||
use bevy_sprite::BorderRect;
|
||||
use bevy_utils::default;
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
@ -61,6 +60,7 @@ where
|
||||
.init_resource::<ExtractedUiMaterialNodes<M>>()
|
||||
.init_resource::<UiMaterialMeta<M>>()
|
||||
.init_resource::<SpecializedRenderPipelines<UiMaterialPipeline<M>>>()
|
||||
.add_systems(RenderStartup, init_ui_material_pipeline::<M>)
|
||||
.add_systems(
|
||||
ExtractSchedule,
|
||||
extract_ui_material_nodes::<M>.in_set(RenderUiSystems::ExtractBackgrounds),
|
||||
@ -74,12 +74,6 @@ where
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&self, app: &mut App) {
|
||||
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||
render_app.init_resource::<UiMaterialPipeline<M>>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
@ -185,11 +179,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: UiMaterial> FromWorld for UiMaterialPipeline<M> {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let asset_server = world.resource::<AssetServer>();
|
||||
let render_device = world.resource::<RenderDevice>();
|
||||
let ui_layout = M::bind_group_layout(render_device);
|
||||
pub fn init_ui_material_pipeline<M: UiMaterial>(
|
||||
mut commands: Commands,
|
||||
render_device: Res<RenderDevice>,
|
||||
asset_server: Res<AssetServer>,
|
||||
) {
|
||||
let ui_layout = M::bind_group_layout(&render_device);
|
||||
|
||||
let view_layout = render_device.create_bind_group_layout(
|
||||
"ui_view_layout",
|
||||
@ -202,9 +197,9 @@ impl<M: UiMaterial> FromWorld for UiMaterialPipeline<M> {
|
||||
),
|
||||
);
|
||||
|
||||
let load_default = || load_embedded_asset!(asset_server, "ui_material.wgsl");
|
||||
let load_default = || load_embedded_asset!(asset_server.as_ref(), "ui_material.wgsl");
|
||||
|
||||
UiMaterialPipeline {
|
||||
commands.insert_resource(UiMaterialPipeline::<M> {
|
||||
ui_layout,
|
||||
view_layout,
|
||||
vertex_shader: match M::vertex_shader() {
|
||||
@ -218,8 +213,7 @@ impl<M: UiMaterial> FromWorld for UiMaterialPipeline<M> {
|
||||
ShaderRef::Path(path) => asset_server.load(path),
|
||||
},
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub type DrawUiMaterial<M> = (
|
||||
|
||||
@ -13,7 +13,6 @@ use bevy_ecs::{
|
||||
use bevy_image::prelude::*;
|
||||
use bevy_math::{Affine2, FloatOrd, Rect, Vec2};
|
||||
use bevy_platform::collections::HashMap;
|
||||
use bevy_render::sync_world::MainEntity;
|
||||
use bevy_render::{
|
||||
render_asset::RenderAssets,
|
||||
render_phase::*,
|
||||
@ -23,6 +22,7 @@ use bevy_render::{
|
||||
view::*,
|
||||
Extract, ExtractSchedule, Render, RenderSystems,
|
||||
};
|
||||
use bevy_render::{sync_world::MainEntity, RenderStartup};
|
||||
use bevy_sprite::{SliceScaleMode, SpriteAssetEvents, SpriteImageMode, TextureSlicer};
|
||||
use bevy_ui::widget;
|
||||
use bevy_utils::default;
|
||||
@ -42,6 +42,7 @@ impl Plugin for UiTextureSlicerPlugin {
|
||||
.init_resource::<UiTextureSliceMeta>()
|
||||
.init_resource::<UiTextureSliceImageBindGroups>()
|
||||
.init_resource::<SpecializedRenderPipelines<UiTextureSlicePipeline>>()
|
||||
.add_systems(RenderStartup, init_ui_texture_slice_pipeline)
|
||||
.add_systems(
|
||||
ExtractSchedule,
|
||||
extract_ui_texture_slices.in_set(RenderUiSystems::ExtractTextureSlice),
|
||||
@ -55,12 +56,6 @@ impl Plugin for UiTextureSlicerPlugin {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&self, app: &mut App) {
|
||||
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||
render_app.init_resource::<UiTextureSlicePipeline>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
@ -110,10 +105,11 @@ pub struct UiTextureSlicePipeline {
|
||||
pub shader: Handle<Shader>,
|
||||
}
|
||||
|
||||
impl FromWorld for UiTextureSlicePipeline {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let render_device = world.resource::<RenderDevice>();
|
||||
|
||||
pub fn init_ui_texture_slice_pipeline(
|
||||
mut commands: Commands,
|
||||
render_device: Res<RenderDevice>,
|
||||
asset_server: Res<AssetServer>,
|
||||
) {
|
||||
let view_layout = render_device.create_bind_group_layout(
|
||||
"ui_texture_slice_view_layout",
|
||||
&BindGroupLayoutEntries::single(
|
||||
@ -133,12 +129,11 @@ impl FromWorld for UiTextureSlicePipeline {
|
||||
),
|
||||
);
|
||||
|
||||
UiTextureSlicePipeline {
|
||||
commands.insert_resource(UiTextureSlicePipeline {
|
||||
view_layout,
|
||||
image_layout,
|
||||
shader: load_embedded_asset!(world, "ui_texture_slice.wgsl"),
|
||||
}
|
||||
}
|
||||
shader: load_embedded_asset!(asset_server.as_ref(), "ui_texture_slice.wgsl"),
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
---
|
||||
title: `bevy_render` reorganization
|
||||
pull_requests: [19949]
|
||||
---
|
||||
|
||||
You must now import `ToNormalizedRenderTarget` to use `RenderTarget::normalize`
|
||||
`ManualTextureViews` is now in `bevy_render::texture`
|
||||
Camera and visibility types have been moved to a new crate, `bevy_camera`, but continue to be re-exported by bevy_render.
|
||||
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Observer Overhaul
|
||||
authors: ["@Jondolf", "@alice-i-cecile", "@hukasu]
|
||||
pull_requests: [19596, 19663, 19611]
|
||||
authors: ["@Jondolf", "@alice-i-cecile", "@hukasu", "oscar-benderstone", "Zeophlite"]
|
||||
pull_requests: [19596, 19663, 19611, 19935]
|
||||
---
|
||||
|
||||
## Rename `Trigger` to `On`
|
||||
@ -45,3 +45,8 @@ This was handy! We've enabled this functionality for all entity-events: simply c
|
||||
|
||||
The name of the Observer's system is now accessible through `Observer::system_name`,
|
||||
this opens up the possibility for the debug tools to show more meaningful names for observers.
|
||||
|
||||
## Use `EventKey` instead of `ComponentId`
|
||||
|
||||
Internally, each `Event` type would generate a `Component` type, allowing us to use the corresponding `ComponentId` to track the event.
|
||||
We have newtyped this to `EventKey` to help separate these concerns.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user