diff --git a/crates/bevy_camera/Cargo.toml b/crates/bevy_camera/Cargo.toml new file mode 100644 index 0000000000..65aadce0eb --- /dev/null +++ b/crates/bevy_camera/Cargo.toml @@ -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 diff --git a/crates/bevy_camera/LICENSE-APACHE b/crates/bevy_camera/LICENSE-APACHE new file mode 100644 index 0000000000..d9a10c0d8e --- /dev/null +++ b/crates/bevy_camera/LICENSE-APACHE @@ -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 diff --git a/crates/bevy_camera/LICENSE-MIT b/crates/bevy_camera/LICENSE-MIT new file mode 100644 index 0000000000..9cf106272a --- /dev/null +++ b/crates/bevy_camera/LICENSE-MIT @@ -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. diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_camera/src/camera.rs similarity index 57% rename from crates/bevy_render/src/camera/camera.rs rename to crates/bevy_camera/src/camera.rs index 20ec3f9c9f..a70cbeb39e 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_camera/src/camera.rs @@ -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, + pub clip_from_view: Mat4, + pub target_info: Option, // size of the `Viewport` - old_viewport_size: Option, - old_sub_camera_view: Option, + pub old_viewport_size: Option, + pub old_sub_camera_view: Option, } /// 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, } -fn warn_on_no_render_graph(world: DeferredWorld, HookContext { entity, caller, .. }: HookContext) { - if !world.entity(entity).contains::() { - 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(name: T) -> Self { - Self(name.intern()) - } - - /// Sets the graph name. - #[inline] - pub fn set(&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> { + 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) -> Option { - 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> { - 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, - 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, - manual_texture_views: &'a ManualTextureViews, - ) -> Option { - 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, - images: &Assets, - manual_texture_views: &ManualTextureViews, - ) -> Option { - 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, - changed_image_handles: &HashSet<&AssetId>, - ) -> 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) -- 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, - mut window_created_events: EventReader, - mut window_scale_factor_changed_events: EventReader, - mut image_asset_events: EventReader>, - primary_window: Query>, - windows: Query<(Entity, &Window)>, - images: Res>, - manual_texture_views: Res, - mut cameras: Query<(&mut Camera, &mut Projection)>, -) { - let primary_window = primary_window.iter().next(); - - let mut changed_window_ids = >::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_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, - pub physical_viewport_size: Option, - pub physical_target_size: Option, - pub viewport: Option, - 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, - Option<&ColorGrading>, - Option<&Exposure>, - Option<&TemporalJitter>, - Option<&MipBias>, - Option<&RenderLayers>, - Option<&Projection>, - Has, - )>, - >, - primary_window: Extract>>, - gpu_preprocessing_support: Res, - mapper: Extract>, -) { - 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::(); - } - - if let Some(mip_bias) = mip_bias { - commands.insert(mip_bias.clone()); - } else { - commands.remove::(); - } - - if let Some(render_layers) = render_layers { - commands.insert(render_layers.clone()); - } else { - commands.remove::(); - } - - if let Some(perspective) = projection { - commands.insert(perspective.clone()); - } else { - commands.remove::(); - } - - if no_indirect_drawing - || !matches!( - gpu_preprocessing_support.max_supported_mode, - GpuPreprocessingMode::Culling - ) - { - commands.insert(NoIndirectDrawing); - } else { - commands.remove::(); - } - }; - } -} - -/// Cameras sorted by their order field. This is updated in the [`sort_cameras`] system. -#[derive(Resource, Default)] -pub struct SortedCameras(pub Vec); - -pub struct SortedCamera { - pub entity: Entity, - pub order: isize, - pub target: Option, - pub hdr: bool, -} - -pub fn sort_cameras( - mut sorted_cameras: ResMut, - 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 = >::default(); - let mut target_counts = >::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) - } -} diff --git a/crates/bevy_render/src/camera/clear_color.rs b/crates/bevy_camera/src/clear_color.rs similarity index 93% rename from crates/bevy_render/src/camera/clear_color.rs rename to crates/bevy_camera/src/clear_color.rs index 6183a1d4de..aeff7b3428 100644 --- a/crates/bevy_render/src/camera/clear_color.rs +++ b/crates/bevy_camera/src/clear_color.rs @@ -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); diff --git a/crates/bevy_camera/src/lib.rs b/crates/bevy_camera/src/lib.rs new file mode 100644 index 0000000000..6fd284d49d --- /dev/null +++ b/crates/bevy_camera/src/lib.rs @@ -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::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .init_resource::() + .add_plugins(( + CameraProjectionPlugin, + visibility::VisibilityPlugin, + visibility::VisibilityRangePlugin, + )); + } +} diff --git a/crates/bevy_render/src/primitives/mod.rs b/crates/bevy_camera/src/primitives.rs similarity index 94% rename from crates/bevy_render/src/primitives/mod.rs rename to crates/bevy_camera/src/primitives.rs index ca664fc338..ddde695423 100644 --- a/crates/bevy_render/src/primitives/mod.rs +++ b/crates/bevy_camera/src/primitives.rs @@ -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; +} + +impl MeshAabb for Mesh { + fn compute_aabb(&self) -> Option { + 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::*; diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_camera/src/projection.rs similarity index 94% rename from crates/bevy_render/src/camera/projection.rs rename to crates/bevy_camera/src/projection.rs index 9fa8831432..847714208d 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_camera/src/projection.rs @@ -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::() .register_type::() .register_type::() - .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`], shared across all `T`. -/// -/// [`camera_system`]: crate::camera::camera_system +/// Label for `camera_system`, 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. /// diff --git a/crates/bevy_camera/src/visibility/mod.rs b/crates/bevy_camera/src/visibility/mod.rs new file mode 100644 index 0000000000..684ac403c7 --- /dev/null +++ b/crates/bevy_camera/src/visibility/mod.rs @@ -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 for &Visibility { + #[inline] + fn eq(&self, other: &Visibility) -> bool { + // Use the base Visibility == Visibility implementation. + >::eq(*self, other) + } +} + +// Allows `Visibility == &Visibility` +impl PartialEq<&Visibility> for Visibility { + #[inline] + fn eq(&self, other: &&Visibility) -> bool { + // Use the base Visibility == Visibility implementation. + >::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::)] +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>, +} + +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 { + self.entities.entry(type_id).or_default() + } + + pub fn iter(&self, type_id: TypeId) -> impl DoubleEndedIterator { + 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::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_required_components::() + .register_required_components::() + .register_required_components::() + .register_required_components::() + .configure_sets( + PostUpdate, + (CalculateBounds, UpdateFrusta, VisibilityPropagate) + .before(CheckVisibility) + .after(TransformSystems::Propagate), + ) + .configure_sets( + PostUpdate, + MarkNewlyHiddenEntitiesInvisible.after(CheckVisibility), + ) + .init_resource::() + .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::() + .on_add(add_visibility_class::); + app.world_mut() + .register_component_hooks::() + .on_add(add_visibility_class::); + } +} + +/// 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>, + without_aabb: Query<(Entity, &Mesh3d), (Without, Without)>, +) { + 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, Changed)>, + >, +) { + 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, + Or<(Changed, Changed)>, + ), + >, + mut visibility_query: Query<(&Visibility, &mut InheritedVisibility)>, + children_query: Query<&Children, (With, With)>, +) { + 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 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, With)>, + // 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, +) { + 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>>>, + mut view_query: Query<( + Entity, + &mut VisibleEntities, + &Frustum, + Option<&RenderLayers>, + &Camera, + Has, + )>, + mut visible_aabb_query: Query<( + Entity, + &InheritedVisibility, + &mut ViewVisibility, + &VisibilityClass, + Option<&RenderLayers>, + Option<&Aabb>, + &GlobalTransform, + Has, + Has, + )>, + visible_entity_ranges: Option>, + mut previous_visible_entities: ResMut, +) { + 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, +) { + // 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::)] +/// struct MyComponent { +/// ... +/// } +/// ``` +pub fn add_visibility_class( + mut world: DeferredWorld<'_>, + HookContext { entity, .. }: HookContext, +) where + C: 'static, +{ + if let Some(mut visibility_class) = world.get_mut::(entity) { + visibility_class.push(TypeId::of::()); + } +} + +#[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::() + .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::() + .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::() + .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::>(); + + 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::().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::()); + assert_eq!(1, size_of::>()); + } +} diff --git a/crates/bevy_camera/src/visibility/range.rs b/crates/bevy_camera/src/visibility/range.rs new file mode 100644 index 0000000000..b85631c9d5 --- /dev/null +++ b/crates/bevy_camera/src/visibility/range.rs @@ -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::() + .init_resource::() + .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, + + /// 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, + + /// 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(&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, + + /// 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, +} + +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, + view_query: Query<(Entity, &GlobalTransform), With>, + mut par_local: Local>>, + 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()); +} diff --git a/crates/bevy_render/src/view/visibility/render_layers.rs b/crates/bevy_camera/src/visibility/render_layers.rs similarity index 100% rename from crates/bevy_render/src/view/visibility/render_layers.rs rename to crates/bevy_camera/src/visibility/render_layers.rs diff --git a/crates/bevy_core_pipeline/src/oit/mod.rs b/crates/bevy_core_pipeline/src/oit/mod.rs index 088d491c75..52d5d2ddc4 100644 --- a/crates/bevy_core_pipeline/src/oit/mod.rs +++ b/crates/bevy_core_pipeline/src/oit/mod.rs @@ -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}, diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index 52839f369d..89a3e10acd 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -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::>() + /// 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::>()) } - /// 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 { - world.component_id::>() + /// and should always correspond to the implementation of + /// [`register_event_key`](Event::register_event_key). + fn event_key(world: &World) -> Option { + world + .component_id::>() + .map(EventKey) } } @@ -421,3 +424,19 @@ pub(crate) struct EventInstance { pub event_id: EventId, 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 + } +} diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index d9ee15a2d8..ec930cd269 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -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; diff --git a/crates/bevy_ecs/src/event/registry.rs b/crates/bevy_ecs/src/event/registry.rs index 7889de62da..3a69a11e4e 100644 --- a/crates/bevy_ecs/src/event/registry.rs +++ b/crates/bevy_ecs/src/event/registry.rs @@ -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 resource @@ -51,7 +51,7 @@ impl EventRegistry { let component_id = world.init_resource::>(); let mut registry = world.get_resource_or_init::(); 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. @@ -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::(); registry .event_updates - .retain(|e| e.component_id != component_id); + .retain(|e| e.event_key.component_id() != component_id); world.remove_resource::>(); } } diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 86275cd87f..8a07cdc8e1 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -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::{ diff --git a/crates/bevy_ecs/src/lifecycle.rs b/crates/bevy_ecs/src/lifecycle.rs index e92c6cc7f9..08f178eda1 100644 --- a/crates/bevy_ecs/src/lifecycle.rs +++ b/crates/bevy_ecs/src/lifecycle.rs @@ -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`. diff --git a/crates/bevy_ecs/src/observer/centralized_storage.rs b/crates/bevy_ecs/src/observer/centralized_storage.rs index e3fa6c530a..544f2f1f6a 100644 --- a/crates/bevy_ecs/src/observer/centralized_storage.rs +++ b/crates/bevy_ecs/src/observer/centralized_storage.rs @@ -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, + cache: HashMap, } 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( mut world: DeferredWorld, - event_type: ComponentId, + event_key: EventKey, current_target: Option, original_target: Option, components: impl Iterator + 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 { + pub(crate) fn is_archetype_cached(event_key: EventKey) -> Option { 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), diff --git a/crates/bevy_ecs/src/observer/distributed_storage.rs b/crates/bevy_ecs/src/observer/distributed_storage.rs index a9a3645121..f0f30cdf2c 100644 --- a/crates/bevy_ecs/src/observer/distributed_storage.rs +++ b/crates/bevy_ecs/src/observer/distributed_storage.rs @@ -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, + pub(super) events: Vec, /// The components the observer is watching. pub(super) components: Vec, @@ -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) -> Self { + pub unsafe fn with_events(mut self, events: Vec) -> 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>( 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::(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(); diff --git a/crates/bevy_ecs/src/observer/entity_cloning.rs b/crates/bevy_ecs/src/observer/entity_cloning.rs index 7c7a4f69e9..bdbb1262bd 100644 --- a/crates/bevy_ecs/src/observer/entity_cloning.rs +++ b/crates/bevy_ecs/src/observer/entity_cloning.rs @@ -43,10 +43,10 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo .get_mut::(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); diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index e9036eee74..3fdc266f12 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -197,10 +197,10 @@ impl World { } pub(crate) fn trigger_with_caller(&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(&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( &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::(), @@ -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( &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( &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( &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::(); - 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::(); - 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 { diff --git a/crates/bevy_ecs/src/observer/system_param.rs b/crates/bevy_ecs/src/observer/system_param.rs index 27d6fef5b3..5d6d665564 100644 --- a/crates/bevy_ecs/src/observer/system_param.rs +++ b/crates/bevy_ecs/src/observer/system_param.rs @@ -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. diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 1699eadcff..0734aa9d8c 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -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, components: impl Iterator + Clone, caller: MaybeLocation, @@ -773,7 +773,7 @@ impl<'w> DeferredWorld<'w> { #[inline] pub(crate) unsafe fn trigger_observers_with_data( &mut self, - event: ComponentId, + event: EventKey, current_target: Option, original_target: Option, components: impl Iterator + Clone, diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 714c5e1eae..fbdf0c04af 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -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(&mut self) -> &mut ComponentHooks { let index = self.register_component::(); 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::()); diff --git a/crates/bevy_mesh/Cargo.toml b/crates/bevy_mesh/Cargo.toml index 7807acbb9d..a34b0e7436 100644 --- a/crates/bevy_mesh/Cargo.toml +++ b/crates/bevy_mesh/Cargo.toml @@ -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" diff --git a/crates/bevy_render/src/mesh/components.rs b/crates/bevy_mesh/src/components.rs similarity index 94% rename from crates/bevy_render/src/mesh/components.rs rename to crates/bevy_mesh/src/components.rs index 000de324e3..cff5eab7e4 100644 --- a/crates/bevy_render/src/mesh/components.rs +++ b/crates/bevy_mesh/src/components.rs @@ -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::)] +#[require(Transform)] pub struct Mesh2d(pub Handle); impl From for AssetId { @@ -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::)] +#[require(Transform)] pub struct Mesh3d(pub Handle); impl From for AssetId { diff --git a/crates/bevy_mesh/src/lib.rs b/crates/bevy_mesh/src/lib.rs index 58702d7d8b..635e36ead4 100644 --- a/crates/bevy_mesh/src/lib.rs +++ b/crates/bevy_mesh/src/lib.rs @@ -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::*; diff --git a/crates/bevy_picking/src/input.rs b/crates/bevy_picking/src/input.rs index d751e07c94..2995174d6d 100644 --- a/crates/bevy_picking/src/input.rs +++ b/crates/bevy_picking/src/input.rs @@ -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; diff --git a/crates/bevy_picking/src/pointer.rs b/crates/bevy_picking/src/pointer.rs index 0406cb61f5..95a44ab3ec 100644 --- a/crates/bevy_picking/src/pointer.rs +++ b/crates/bevy_picking/src/pointer.rs @@ -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; diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index a657da4f0e..e8f076f014 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -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", diff --git a/crates/bevy_render/src/camera.rs b/crates/bevy_render/src/camera.rs new file mode 100644 index 0000000000..b5dcc6baa5 --- /dev/null +++ b/crates/bevy_render/src/camera.rs @@ -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::() + .register_type::() + .register_type::() + .register_required_components::() + .register_required_components::() + .add_plugins(( + ExtractResourcePlugin::::default(), + ExtractComponentPlugin::::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::() + .on_add(warn_on_no_render_graph); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::() + .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::(); + 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::() { + 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 { + 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(name: T) -> Self { + Self(name.intern()) + } + + /// Sets the graph name. + #[inline] + pub fn set(&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) -> Option; +} + +impl ToNormalizedRenderTarget for RenderTarget { + fn normalize(&self, primary_window: Option) -> Option { + 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, + 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, + manual_texture_views: &'a ManualTextureViews, + ) -> Option { + 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, + images: &Assets, + manual_texture_views: &ManualTextureViews, + ) -> Option { + 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, + changed_image_handles: &HashSet<&AssetId>, + ) -> 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) -- 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, + mut window_created_events: EventReader, + mut window_scale_factor_changed_events: EventReader, + mut image_asset_events: EventReader>, + primary_window: Query>, + windows: Query<(Entity, &Window)>, + images: Res>, + manual_texture_views: Res, + mut cameras: Query<(&mut Camera, &mut Projection)>, +) { + let primary_window = primary_window.iter().next(); + + let mut changed_window_ids = >::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_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, + pub physical_viewport_size: Option, + pub physical_target_size: Option, + pub viewport: Option, + 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, + Option<&ColorGrading>, + Option<&Exposure>, + Option<&TemporalJitter>, + Option<&MipBias>, + Option<&RenderLayers>, + Option<&Projection>, + Has, + )>, + >, + primary_window: Extract>>, + gpu_preprocessing_support: Res, + mapper: Extract>, +) { + 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::(); + } + + if let Some(mip_bias) = mip_bias { + commands.insert(mip_bias.clone()); + } else { + commands.remove::(); + } + + if let Some(render_layers) = render_layers { + commands.insert(render_layers.clone()); + } else { + commands.remove::(); + } + + if let Some(perspective) = projection { + commands.insert(perspective.clone()); + } else { + commands.remove::(); + } + + if no_indirect_drawing + || !matches!( + gpu_preprocessing_support.max_supported_mode, + GpuPreprocessingMode::Culling + ) + { + commands.insert(NoIndirectDrawing); + } else { + commands.remove::(); + } + }; + } +} + +/// Cameras sorted by their order field. This is updated in the [`sort_cameras`] system. +#[derive(Resource, Default)] +pub struct SortedCameras(pub Vec); + +pub struct SortedCamera { + pub entity: Entity, + pub order: isize, + pub target: Option, + pub hdr: bool, +} + +pub fn sort_cameras( + mut sorted_cameras: ResMut, + 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 = >::default(); + let mut target_counts = >::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) + } +} diff --git a/crates/bevy_render/src/camera/mod.rs b/crates/bevy_render/src/camera/mod.rs deleted file mode 100644 index 1b2a3bdfd3..0000000000 --- a/crates/bevy_render/src/camera/mod.rs +++ /dev/null @@ -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::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .init_resource::() - .init_resource::() - .add_plugins(( - CameraProjectionPlugin, - ExtractResourcePlugin::::default(), - ExtractResourcePlugin::::default(), - ExtractComponentPlugin::::default(), - )); - - if let Some(render_app) = app.get_sub_app_mut(RenderApp) { - render_app - .init_resource::() - .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::(); - render_graph.add_node(crate::graph::CameraDriverLabel, camera_driver_node); - } - } -} diff --git a/crates/bevy_render/src/extract_component.rs b/crates/bevy_render/src/extract_component.rs index b7bb05e425..ce656c3579 100644 --- a/crates/bevy_render/src/extract_component.rs +++ b/crates/bevy_render/src/extract_component.rs @@ -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, diff --git a/crates/bevy_render/src/extract_instances.rs b/crates/bevy_render/src/extract_instances.rs index cd194b7c40..cf0d0c0cce 100644 --- a/crates/bevy_render/src/extract_instances.rs +++ b/crates/bevy_render/src/extract_instances.rs @@ -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. diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 76d8cb78e1..a7907178ba 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -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::() // These types cannot be registered in bevy_color, as it does not depend on the rest of Bevy .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() .register_type::() .register_type::() .register_type::() diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index c981e75cee..f7aa593a3a 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -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; -} - -impl MeshAabb for Mesh { - fn compute_aabb(&self) -> Option { - 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 { diff --git a/crates/bevy_render/src/camera/camera_driver_node.rs b/crates/bevy_render/src/render_graph/camera_driver_node.rs similarity index 97% rename from crates/bevy_render/src/camera/camera_driver_node.rs rename to crates/bevy_render/src/render_graph/camera_driver_node.rs index 8be5a345b4..d18c7d1133 100644 --- a/crates/bevy_render/src/camera/camera_driver_node.rs +++ b/crates/bevy_render/src/render_graph/camera_driver_node.rs @@ -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}; diff --git a/crates/bevy_render/src/render_graph/mod.rs b/crates/bevy_render/src/render_graph/mod.rs index fbed33a23c..6f98a30f4b 100644 --- a/crates/bevy_render/src/render_graph/mod.rs +++ b/crates/bevy_render/src/render_graph/mod.rs @@ -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::*; diff --git a/crates/bevy_render/src/render_phase/draw_state.rs b/crates/bevy_render/src/render_phase/draw_state.rs index a7b8acdc00..2024535504 100644 --- a/crates/bevy_render/src/render_phase/draw_state.rs +++ b/crates/bevy_render/src/render_phase/draw_state.rs @@ -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; diff --git a/crates/bevy_render/src/camera/manual_texture_view.rs b/crates/bevy_render/src/texture/manual_texture_view.rs similarity index 67% rename from crates/bevy_render/src/camera/manual_texture_view.rs rename to crates/bevy_render/src/texture/manual_texture_view.rs index 56eff5612a..b291463c29 100644 --- a/crates/bevy_render/src/camera/manual_texture_view.rs +++ b/crates/bevy_render/src/texture/manual_texture_view.rs @@ -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)] diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index 006ac2e5e8..78cc78a9a1 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -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::(); } - app.add_plugins(RenderAssetPlugin::::default()) - .register_type::() - .init_asset::() - .register_asset_reflect::(); + app.add_plugins(( + RenderAssetPlugin::::default(), + ExtractResourcePlugin::::default(), + )) + .init_resource::() + .register_type::() + .init_asset::() + .register_asset_reflect::(); let mut image_assets = app.world_mut().resource_mut::>(); diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index a348b361c7..cdda04cf2d 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -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::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() - .register_type::() + app.register_type::() .register_type::() .register_type::() // NOTE: windows.is_changed() handles cases where a window was resized @@ -114,8 +107,7 @@ impl Plugin for ViewPlugin { ExtractComponentPlugin::::default(), ExtractComponentPlugin::::default(), ExtractComponentPlugin::::default(), - VisibilityPlugin, - VisibilityRangePlugin, + RenderVisibilityRangePlugin, )); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { @@ -729,9 +721,6 @@ impl From for ColorGradingUniform { #[derive(Component, Default)] pub struct NoIndirectDrawing; -#[derive(Component, Default)] -pub struct NoCpuCulling; - impl ViewTarget { pub const TEXTURE_FORMAT_HDR: TextureFormat = TextureFormat::Rgba16Float; diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 11abdd8803..281b6967da 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -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 for &Visibility { - #[inline] - fn eq(&self, other: &Visibility) -> bool { - // Use the base Visibility == Visibility implementation. - >::eq(*self, other) - } -} - -// Allows `Visibility == &Visibility` -impl PartialEq<&Visibility> for Visibility { - #[inline] - fn eq(&self, other: &&Visibility) -> bool { - // Use the base Visibility == Visibility implementation. - >::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::)] -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>, -} - -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 { - self.entities.entry(type_id).or_default() - } - - pub fn iter(&self, type_id: TypeId) -> impl DoubleEndedIterator { - 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::().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::() - .configure_sets( - PostUpdate, - (CalculateBounds, UpdateFrusta, VisibilityPropagate) - .before(CheckVisibility) - .after(TransformSystems::Propagate), - ) - .configure_sets( - PostUpdate, - MarkNewlyHiddenEntitiesInvisible.after(CheckVisibility), - ) - .init_resource::() - .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>, - without_aabb: Query<(Entity, &Mesh3d), (Without, Without)>, -) { - 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, Changed)>, - >, -) { - 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, - Or<(Changed, Changed)>, - ), - >, - mut visibility_query: Query<(&Visibility, &mut InheritedVisibility)>, - children_query: Query<&Children, (With, With)>, -) { - 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 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, With)>, - // 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, -) { - 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>>>, - mut view_query: Query<( - Entity, - &mut VisibleEntities, - &Frustum, - Option<&RenderLayers>, - &Camera, - Has, - )>, - mut visible_aabb_query: Query<( - Entity, - &InheritedVisibility, - &mut ViewVisibility, - &VisibilityClass, - Option<&RenderLayers>, - Option<&Aabb>, - &GlobalTransform, - Has, - Has, - )>, - visible_entity_ranges: Option>, - mut previous_visible_entities: ResMut, -) { - 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, -) { - // 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::)] -/// struct MyComponent { -/// ... -/// } -/// ``` -pub fn add_visibility_class( - mut world: DeferredWorld<'_>, - HookContext { entity, .. }: HookContext, -) where - C: 'static, -{ - if let Some(mut visibility_class) = world.get_mut::(entity) { - visibility_class.push(TypeId::of::()); - } -} - -#[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::() - .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::() - .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::() - .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::>(); - - 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::().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::()); - assert_eq!(1, size_of::>()); - } -} diff --git a/crates/bevy_render/src/view/visibility/range.rs b/crates/bevy_render/src/view/visibility/range.rs index 80f89ce936..543f10f564 100644 --- a/crates/bevy_render/src/view/visibility/range.rs +++ b/crates/bevy_render/src/view/visibility/range.rs @@ -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::() - .init_resource::() - .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, - - /// 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, - - /// 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(&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, - - /// 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, -} - -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, - view_query: Query<(Entity, &GlobalTransform), With>, - mut par_local: Local>>, - 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( diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index 13dd2670ab..4e74ea5924 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -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, diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 3e15499dd0..fab5b2d993 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -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}, diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 549d26a262..384661f4d1 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -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; diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 4d5bec8f07..655ec17e90 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -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; diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index 0053e5a406..f8699fb7fc 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -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; diff --git a/crates/bevy_ui_render/src/box_shadow.rs b/crates/bevy_ui_render/src/box_shadow.rs index 87c223ec8e..4c97e714cc 100644 --- a/crates/bevy_ui_render/src/box_shadow.rs +++ b/crates/bevy_ui_render/src/box_shadow.rs @@ -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::() .init_resource::() .init_resource::>() + .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::(); - } - } } #[repr(C)] @@ -111,23 +106,23 @@ pub struct BoxShadowPipeline { pub shader: Handle, } -impl FromWorld for BoxShadowPipeline { - fn from_world(world: &mut World) -> Self { - let render_device = world.resource::(); +pub fn init_box_shadow_pipeline( + mut commands: Commands, + render_device: Res, + asset_server: Res, +) { + let view_layout = render_device.create_bind_group_layout( + "box_shadow_view_layout", + &BindGroupLayoutEntries::single( + ShaderStages::VERTEX_FRAGMENT, + uniform_buffer::(true), + ), + ); - let view_layout = render_device.create_bind_group_layout( - "box_shadow_view_layout", - &BindGroupLayoutEntries::single( - ShaderStages::VERTEX_FRAGMENT, - uniform_buffer::(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)] diff --git a/crates/bevy_ui_render/src/gradient.rs b/crates/bevy_ui_render/src/gradient.rs index 11de56edda..9bef5340cb 100644 --- a/crates/bevy_ui_render/src/gradient.rs +++ b/crates/bevy_ui_render/src/gradient.rs @@ -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::() .init_resource::() .init_resource::>() + .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::(); - } - } } #[derive(Component)] @@ -102,23 +97,23 @@ pub struct GradientPipeline { pub shader: Handle, } -impl FromWorld for GradientPipeline { - fn from_world(world: &mut World) -> Self { - let render_device = world.resource::(); +pub fn init_gradient_pipeline( + mut commands: Commands, + render_device: Res, + asset_server: Res, +) { + let view_layout = render_device.create_bind_group_layout( + "ui_gradient_view_layout", + &BindGroupLayoutEntries::single( + ShaderStages::VERTEX_FRAGMENT, + uniform_buffer::(true), + ), + ); - let view_layout = render_device.create_bind_group_layout( - "ui_gradient_view_layout", - &BindGroupLayoutEntries::single( - ShaderStages::VERTEX_FRAGMENT, - uniform_buffer::(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 { diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index a06426c255..74617a7269 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -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::(); - } } fn get_ui_graph(render_app: &mut SubApp) -> RenderGraph { diff --git a/crates/bevy_ui_render/src/pipeline.rs b/crates/bevy_ui_render/src/pipeline.rs index 7440c5abad..6315091271 100644 --- a/crates/bevy_ui_render/src/pipeline.rs +++ b/crates/bevy_ui_render/src/pipeline.rs @@ -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, } -impl FromWorld for UiPipeline { - fn from_world(world: &mut World) -> Self { - let render_device = world.resource::(); +pub fn init_ui_pipeline( + mut commands: Commands, + render_device: Res, + asset_server: Res, +) { + let view_layout = render_device.create_bind_group_layout( + "ui_view_layout", + &BindGroupLayoutEntries::single( + ShaderStages::VERTEX_FRAGMENT, + uniform_buffer::(true), + ), + ); - let view_layout = render_device.create_bind_group_layout( - "ui_view_layout", - &BindGroupLayoutEntries::single( - ShaderStages::VERTEX_FRAGMENT, - uniform_buffer::(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)] diff --git a/crates/bevy_ui_render/src/ui_material_pipeline.rs b/crates/bevy_ui_render/src/ui_material_pipeline.rs index 5dc0453816..eb4f5050cf 100644 --- a/crates/bevy_ui_render/src/ui_material_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_material_pipeline.rs @@ -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::>() .init_resource::>() .init_resource::>>() + .add_systems(RenderStartup, init_ui_material_pipeline::) .add_systems( ExtractSchedule, extract_ui_material_nodes::.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::>(); - } - } } #[derive(Resource)] @@ -185,41 +179,41 @@ where } } -impl FromWorld for UiMaterialPipeline { - fn from_world(world: &mut World) -> Self { - let asset_server = world.resource::(); - let render_device = world.resource::(); - let ui_layout = M::bind_group_layout(render_device); +pub fn init_ui_material_pipeline( + mut commands: Commands, + render_device: Res, + asset_server: Res, +) { + 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::(true), - uniform_buffer::(false), - ), + let view_layout = render_device.create_bind_group_layout( + "ui_view_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::VERTEX_FRAGMENT, + ( + uniform_buffer::(true), + uniform_buffer::(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:: { + 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 = ( diff --git a/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs b/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs index 547db6b06c..aa05f106ff 100644 --- a/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs @@ -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::() .init_resource::() .init_resource::>() + .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::(); - } - } } #[repr(C)] @@ -110,35 +105,35 @@ pub struct UiTextureSlicePipeline { pub shader: Handle, } -impl FromWorld for UiTextureSlicePipeline { - fn from_world(world: &mut World) -> Self { - let render_device = world.resource::(); +pub fn init_ui_texture_slice_pipeline( + mut commands: Commands, + render_device: Res, + asset_server: Res, +) { + let view_layout = render_device.create_bind_group_layout( + "ui_texture_slice_view_layout", + &BindGroupLayoutEntries::single( + ShaderStages::VERTEX_FRAGMENT, + uniform_buffer::(true), + ), + ); - let view_layout = render_device.create_bind_group_layout( - "ui_texture_slice_view_layout", - &BindGroupLayoutEntries::single( - ShaderStages::VERTEX_FRAGMENT, - uniform_buffer::(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)] diff --git a/release-content/migration-guides/bevy_render_reorganization.md b/release-content/migration-guides/bevy_render_reorganization.md new file mode 100644 index 0000000000..82a6ef9146 --- /dev/null +++ b/release-content/migration-guides/bevy_render_reorganization.md @@ -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. diff --git a/release-content/release-notes/observer_overhaul.md b/release-content/release-notes/observer_overhaul.md index c01aa561e9..e9da0204ad 100644 --- a/release-content/release-notes/observer_overhaul.md +++ b/release-content/release-notes/observer_overhaul.md @@ -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.