Merge branch 'main' into proper-json-schema

This commit is contained in:
MevLyshkin 2025-07-05 11:48:53 +02:00 committed by GitHub
commit 22eae74a38
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 2637 additions and 2286 deletions

View 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

View 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

View 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.

View File

@ -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)
}
}

View File

@ -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);

View 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,
));
}
}

View File

@ -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::*;

View File

@ -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
.in_set(VisibilitySystems::UpdateFrusta)
.after(crate::camera::camera_system)
.after(TransformSystems::Propagate),
),
crate::visibility::update_frusta
.in_set(VisibilitySystems::UpdateFrusta)
.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.
///

View 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>>());
}
}

View 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());
}

View File

@ -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},

View File

@ -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
}
}

View File

@ -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;

View File

@ -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>>();
}
}

View File

@ -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::{

View File

@ -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`.

View File

@ -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),

View File

@ -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();

View File

@ -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);

View File

@ -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 {

View File

@ -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.

View File

@ -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,

View File

@ -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>());

View File

@ -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"

View File

@ -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> {

View File

@ -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::*;

View File

@ -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;

View File

@ -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;

View File

@ -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",

View 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)
}
}

View File

@ -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);
}
}
}

View File

@ -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,

View File

@ -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.

View File

@ -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>()

View File

@ -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 {

View File

@ -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};

View File

@ -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::*;

View File

@ -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;

View File

@ -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)]

View File

@ -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,10 +77,14 @@ impl Plugin for ImagePlugin {
app.init_asset_loader::<HdrTextureLoader>();
}
app.add_plugins(RenderAssetPlugin::<GpuImage>::default())
.register_type::<Image>()
.init_asset::<Image>()
.register_asset_reflect::<Image>();
app.add_plugins((
RenderAssetPlugin::<GpuImage>::default(),
ExtractResourcePlugin::<ManualTextureViews>::default(),
))
.init_resource::<ManualTextureViews>()
.register_type::<Image>()
.init_asset::<Image>()
.register_asset_reflect::<Image>();
let mut image_assets = app.world_mut().resource_mut::<Assets<Image>>();

View File

@ -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;

View File

@ -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>>());
}
}

View File

@ -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(

View File

@ -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,

View File

@ -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},

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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,23 +106,23 @@ 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(
ShaderStages::VERTEX_FRAGMENT,
uniform_buffer::<ViewUniform>(true),
),
);
let view_layout = render_device.create_bind_group_layout(
"box_shadow_view_layout",
&BindGroupLayoutEntries::single(
ShaderStages::VERTEX_FRAGMENT,
uniform_buffer::<ViewUniform>(true),
),
);
BoxShadowPipeline {
view_layout,
shader: load_embedded_asset!(world, "box_shadow.wgsl"),
}
}
commands.insert_resource(BoxShadowPipeline {
view_layout,
shader: load_embedded_asset!(asset_server.as_ref(), "box_shadow.wgsl"),
});
}
#[derive(Clone, Copy, Hash, PartialEq, Eq)]

View File

@ -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,23 +97,23 @@ 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(
ShaderStages::VERTEX_FRAGMENT,
uniform_buffer::<ViewUniform>(true),
),
);
let view_layout = render_device.create_bind_group_layout(
"ui_gradient_view_layout",
&BindGroupLayoutEntries::single(
ShaderStages::VERTEX_FRAGMENT,
uniform_buffer::<ViewUniform>(true),
),
);
GradientPipeline {
view_layout,
shader: load_embedded_asset!(world, "gradient.wgsl"),
}
}
commands.insert_resource(GradientPipeline {
view_layout,
shader: load_embedded_asset!(asset_server.as_ref(), "gradient.wgsl"),
});
}
pub fn compute_gradient_line_length(angle: f32, size: Vec2) -> f32 {

View File

@ -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 {

View File

@ -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,35 +18,35 @@ 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(
ShaderStages::VERTEX_FRAGMENT,
uniform_buffer::<ViewUniform>(true),
),
);
let view_layout = render_device.create_bind_group_layout(
"ui_view_layout",
&BindGroupLayoutEntries::single(
ShaderStages::VERTEX_FRAGMENT,
uniform_buffer::<ViewUniform>(true),
let image_layout = render_device.create_bind_group_layout(
"ui_image_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
),
);
),
);
let image_layout = render_device.create_bind_group_layout(
"ui_image_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
),
),
);
UiPipeline {
view_layout,
image_layout,
shader: load_embedded_asset!(world, "ui.wgsl"),
}
}
commands.insert_resource(UiPipeline {
view_layout,
image_layout,
shader: load_embedded_asset!(asset_server.as_ref(), "ui.wgsl"),
});
}
#[derive(Clone, Copy, Hash, PartialEq, Eq)]

View File

@ -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,41 +179,41 @@ 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",
&BindGroupLayoutEntries::sequential(
ShaderStages::VERTEX_FRAGMENT,
(
uniform_buffer::<ViewUniform>(true),
uniform_buffer::<GlobalsUniform>(false),
),
let view_layout = render_device.create_bind_group_layout(
"ui_view_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::VERTEX_FRAGMENT,
(
uniform_buffer::<ViewUniform>(true),
uniform_buffer::<GlobalsUniform>(false),
),
);
),
);
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 {
ui_layout,
view_layout,
vertex_shader: match M::vertex_shader() {
ShaderRef::Default => load_default(),
ShaderRef::Handle(handle) => handle,
ShaderRef::Path(path) => asset_server.load(path),
},
fragment_shader: match M::fragment_shader() {
ShaderRef::Default => load_default(),
ShaderRef::Handle(handle) => handle,
ShaderRef::Path(path) => asset_server.load(path),
},
marker: PhantomData,
}
}
commands.insert_resource(UiMaterialPipeline::<M> {
ui_layout,
view_layout,
vertex_shader: match M::vertex_shader() {
ShaderRef::Default => load_default(),
ShaderRef::Handle(handle) => handle,
ShaderRef::Path(path) => asset_server.load(path),
},
fragment_shader: match M::fragment_shader() {
ShaderRef::Default => load_default(),
ShaderRef::Handle(handle) => handle,
ShaderRef::Path(path) => asset_server.load(path),
},
marker: PhantomData,
});
}
pub type DrawUiMaterial<M> = (

View File

@ -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,35 +105,35 @@ 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(
ShaderStages::VERTEX_FRAGMENT,
uniform_buffer::<ViewUniform>(true),
),
);
let view_layout = render_device.create_bind_group_layout(
"ui_texture_slice_view_layout",
&BindGroupLayoutEntries::single(
ShaderStages::VERTEX_FRAGMENT,
uniform_buffer::<ViewUniform>(true),
let image_layout = render_device.create_bind_group_layout(
"ui_texture_slice_image_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
),
);
),
);
let image_layout = render_device.create_bind_group_layout(
"ui_texture_slice_image_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
),
),
);
UiTextureSlicePipeline {
view_layout,
image_layout,
shader: load_embedded_asset!(world, "ui_texture_slice.wgsl"),
}
}
commands.insert_resource(UiTextureSlicePipeline {
view_layout,
image_layout,
shader: load_embedded_asset!(asset_server.as_ref(), "ui_texture_slice.wgsl"),
});
}
#[derive(Clone, Copy, Hash, PartialEq, Eq)]

View File

@ -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.

View File

@ -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.