diff --git a/Cargo.toml b/Cargo.toml index 8bb16b741db86..f0afd5bc44c24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -143,6 +143,7 @@ default = [ "bevy_pbr", "bevy_picking", "bevy_render", + "bevy_render_2d", "bevy_scene", "bevy_sprite", "bevy_sprite_picking_backend", @@ -242,6 +243,9 @@ bevy_picking = ["bevy_internal/bevy_picking"] # Provides rendering functionality bevy_render = ["bevy_internal/bevy_render", "bevy_color"] +# Provides functionality for rendering in 2d +bevy_render_2d = ["bevy_internal/bevy_render_2d"] + # Provides scene functionality bevy_scene = ["bevy_internal/bevy_scene", "bevy_asset"] @@ -249,6 +253,7 @@ bevy_scene = ["bevy_internal/bevy_scene", "bevy_asset"] bevy_sprite = [ "bevy_internal/bevy_sprite", "bevy_render", + "bevy_render_2d", "bevy_core_pipeline", "bevy_color", "bevy_anti_aliasing", diff --git a/crates/bevy_gizmos/Cargo.toml b/crates/bevy_gizmos/Cargo.toml index 3a264c6244609..45d68ca01d308 100644 --- a/crates/bevy_gizmos/Cargo.toml +++ b/crates/bevy_gizmos/Cargo.toml @@ -16,7 +16,7 @@ bevy_render = ["dep:bevy_render", "bevy_core_pipeline"] [dependencies] # Bevy bevy_pbr = { path = "../bevy_pbr", version = "0.16.0-dev", optional = true } -bevy_sprite = { path = "../bevy_sprite", version = "0.16.0-dev", optional = true } +bevy_render_2d = { path = "../bevy_render_2d", version = "0.16.0-dev", optional = true } bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } diff --git a/crates/bevy_gizmos/src/config.rs b/crates/bevy_gizmos/src/config.rs index 973fa1cf0fbce..8d37c2052c3a0 100644 --- a/crates/bevy_gizmos/src/config.rs +++ b/crates/bevy_gizmos/src/config.rs @@ -4,7 +4,7 @@ pub use bevy_gizmos_macros::GizmoConfigGroup; #[cfg(all( feature = "bevy_render", - any(feature = "bevy_pbr", feature = "bevy_sprite") + any(feature = "bevy_pbr", feature = "bevy_render_2d") ))] use {crate::GizmoAsset, bevy_asset::Handle, bevy_ecs::component::Component}; @@ -246,7 +246,7 @@ impl Default for GizmoLineConfig { #[cfg(all( feature = "bevy_render", - any(feature = "bevy_pbr", feature = "bevy_sprite") + any(feature = "bevy_pbr", feature = "bevy_render_2d") ))] #[derive(Component)] pub(crate) struct GizmoMeshConfig { diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 3cd2c7c40447a..149ef7190dc34 100755 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -26,7 +26,7 @@ extern crate self as bevy_gizmos; #[derive(SystemSet, Clone, Debug, Hash, PartialEq, Eq)] pub enum GizmoRenderSystem { /// Adds gizmos to the [`Transparent2d`](bevy_core_pipeline::core_2d::Transparent2d) render phase - #[cfg(feature = "bevy_sprite")] + #[cfg(feature = "bevy_render_2d")] QueueLineGizmos2d, /// Adds gizmos to the [`Transparent3d`](bevy_core_pipeline::core_3d::Transparent3d) render phase #[cfg(feature = "bevy_pbr")] @@ -50,7 +50,7 @@ pub mod rounded_box; #[cfg(all(feature = "bevy_pbr", feature = "bevy_render"))] pub mod light; -#[cfg(all(feature = "bevy_sprite", feature = "bevy_render"))] +#[cfg(all(feature = "bevy_render_2d", feature = "bevy_render"))] mod pipeline_2d; #[cfg(all(feature = "bevy_pbr", feature = "bevy_render"))] mod pipeline_3d; @@ -89,7 +89,7 @@ use bevy_reflect::TypePath; #[cfg(all( feature = "bevy_render", - any(feature = "bevy_pbr", feature = "bevy_sprite") + any(feature = "bevy_pbr", feature = "bevy_render_2d") ))] use crate::config::GizmoMeshConfig; @@ -127,7 +127,7 @@ use { #[cfg(all( feature = "bevy_render", - any(feature = "bevy_pbr", feature = "bevy_sprite"), + any(feature = "bevy_pbr", feature = "bevy_render_2d"), ))] use bevy_render::render_resource::{VertexAttribute, VertexBufferLayout, VertexStepMode}; use bevy_time::Fixed; @@ -148,7 +148,7 @@ const LINE_JOINT_SHADER_HANDLE: Handle = /// A [`Plugin`] that provides an immediate mode drawing api for visual debugging. /// -/// Requires to be loaded after [`PbrPlugin`](bevy_pbr::PbrPlugin) or [`SpritePlugin`](bevy_sprite::SpritePlugin). +/// Requires to be loaded after [`PbrPlugin`](bevy_pbr::PbrPlugin) or [`Mesh2dRenderPlugin`](bevy_render_2d::mesh_pipeline::Mesh2dRenderPlugin). #[derive(Default)] pub struct GizmoPlugin; @@ -190,11 +190,11 @@ impl Plugin for GizmoPlugin { render_app.add_systems(ExtractSchedule, (extract_gizmo_data, extract_linegizmos)); - #[cfg(feature = "bevy_sprite")] - if app.is_plugin_added::() { + #[cfg(feature = "bevy_render_2d")] + if app.is_plugin_added::() { app.add_plugins(pipeline_2d::LineGizmo2dPlugin); } else { - tracing::warn!("bevy_sprite feature is enabled but bevy_sprite::SpritePlugin was not detected. Are you sure you loaded GizmoPlugin after SpritePlugin?"); + tracing::warn!("bevy_render_2d feature is enabled but bevy_render_2d::mesh_pipeline::Mesh2dRenderPlugin was not detected. Are you sure you loaded GizmoPlugin after Mesh2dRenderPlugin?"); } #[cfg(feature = "bevy_pbr")] if app.is_plugin_added::() { @@ -474,7 +474,7 @@ fn extract_gizmo_data( #[cfg(feature = "webgl")] _padding: Default::default(), }, - #[cfg(any(feature = "bevy_pbr", feature = "bevy_sprite"))] + #[cfg(any(feature = "bevy_pbr", feature = "bevy_render_2d"))] GizmoMeshConfig { line_perspective: config.line.perspective, line_style: config.line.style, @@ -655,7 +655,7 @@ impl RenderCommand

for SetLineGizmoBindGroup struct DrawLineGizmo; #[cfg(all( feature = "bevy_render", - any(feature = "bevy_pbr", feature = "bevy_sprite") + any(feature = "bevy_pbr", feature = "bevy_render_2d") ))] impl RenderCommand

for DrawLineGizmo { type Param = SRes>; @@ -718,7 +718,7 @@ impl RenderCommand

for DrawLineGizmo struct DrawLineJointGizmo; #[cfg(all( feature = "bevy_render", - any(feature = "bevy_pbr", feature = "bevy_sprite") + any(feature = "bevy_pbr", feature = "bevy_render_2d") ))] impl RenderCommand

for DrawLineJointGizmo { type Param = SRes>; @@ -791,7 +791,7 @@ impl RenderCommand

for DrawLineJointGizmo { #[cfg(all( feature = "bevy_render", - any(feature = "bevy_pbr", feature = "bevy_sprite") + any(feature = "bevy_pbr", feature = "bevy_render_2d") ))] fn line_gizmo_vertex_buffer_layouts(strip: bool) -> Vec { use VertexFormat::*; @@ -849,7 +849,7 @@ fn line_gizmo_vertex_buffer_layouts(strip: bool) -> Vec { #[cfg(all( feature = "bevy_render", - any(feature = "bevy_pbr", feature = "bevy_sprite") + any(feature = "bevy_pbr", feature = "bevy_render_2d") ))] fn line_joint_gizmo_vertex_buffer_layouts() -> Vec { use VertexFormat::*; diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index 3a4305549108b..1346cba4a9e26 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -27,7 +27,10 @@ use bevy_render::{ view::{ExtractedView, Msaa, RenderLayers, ViewTarget}, Render, RenderApp, RenderSet, }; -use bevy_sprite::{Mesh2dPipeline, Mesh2dPipelineKey, SetMesh2dViewBindGroup}; +use bevy_render_2d::mesh_pipeline::{ + commands::SetMesh2dViewBindGroup, key::Mesh2dPipelineKey, pipeline::Mesh2dPipeline, +}; + use tracing::error; pub struct LineGizmo2dPlugin; @@ -48,10 +51,7 @@ impl Plugin for LineGizmo2dPlugin { Render, GizmoRenderSystem::QueueLineGizmos2d .in_set(RenderSet::Queue) - .ambiguous_with(bevy_sprite::queue_sprites) - .ambiguous_with( - bevy_sprite::queue_material2d_meshes::, - ), + .before(RenderSet::QueueMeshes), ) .add_systems( Render, diff --git a/crates/bevy_gizmos/src/retained.rs b/crates/bevy_gizmos/src/retained.rs index 88610b9744203..88f8b006ecb22 100644 --- a/crates/bevy_gizmos/src/retained.rs +++ b/crates/bevy_gizmos/src/retained.rs @@ -143,7 +143,7 @@ pub(crate) fn extract_linegizmos( #[cfg(feature = "webgl")] _padding: Default::default(), }, - #[cfg(any(feature = "bevy_pbr", feature = "bevy_sprite"))] + #[cfg(any(feature = "bevy_pbr", feature = "bevy_render_2d"))] crate::config::GizmoMeshConfig { line_perspective: gizmo.line_config.perspective, line_style: gizmo.line_config.style, diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index c9e3978a38a8e..d3e78eea7047f 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -170,7 +170,7 @@ bevy_ci_testing = ["bevy_dev_tools/bevy_ci_testing", "bevy_render?/ci_limits"] # Enable animation support, and glTF animation loading animation = ["bevy_animation", "bevy_gltf?/bevy_animation"] -bevy_sprite = ["dep:bevy_sprite", "bevy_gizmos?/bevy_sprite", "bevy_image"] +bevy_sprite = ["dep:bevy_sprite", "bevy_image"] bevy_pbr = ["dep:bevy_pbr", "bevy_gizmos?/bevy_pbr", "bevy_image"] bevy_window = ["dep:bevy_window", "dep:bevy_a11y"] bevy_core_pipeline = ["dep:bevy_core_pipeline", "bevy_image"] @@ -201,6 +201,8 @@ bevy_render = [ "bevy_color/encase", ] +bevy_render_2d = ["dep:bevy_render_2d", "bevy_gizmos?/bevy_render_2d"] + # Enable assertions to check the validity of parameters passed to glam glam_assert = ["bevy_math/glam_assert"] @@ -413,6 +415,7 @@ bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.16.0-dev" } bevy_picking = { path = "../bevy_picking", optional = true, version = "0.16.0-dev" } bevy_remote = { path = "../bevy_remote", optional = true, version = "0.16.0-dev" } bevy_render = { path = "../bevy_render", optional = true, version = "0.16.0-dev" } +bevy_render_2d = { path = "../bevy_render_2d", optional = true, version = "0.16.0-dev" } bevy_scene = { path = "../bevy_scene", optional = true, version = "0.16.0-dev" } bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.16.0-dev" } bevy_state = { path = "../bevy_state", optional = true, version = "0.16.0-dev", default-features = false, features = [ diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index db1152a362e31..100f0a6bf43f0 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -38,6 +38,8 @@ plugin_group! { bevy_render::pipelined_rendering:::PipelinedRenderingPlugin, #[cfg(feature = "bevy_core_pipeline")] bevy_core_pipeline:::CorePipelinePlugin, + #[cfg(feature = "bevy_render_2d")] + bevy_render_2d::mesh_pipeline:::Mesh2dRenderPlugin, #[cfg(feature = "bevy_anti_aliasing")] bevy_anti_aliasing:::AntiAliasingPlugin, #[cfg(feature = "bevy_sprite")] diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 84795bc6bb6ea..2d711c21c093f 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -58,6 +58,8 @@ pub use bevy_reflect as reflect; pub use bevy_remote as remote; #[cfg(feature = "bevy_render")] pub use bevy_render as render; +#[cfg(feature = "bevy_render_2d")] +pub use bevy_render_2d as render_2d; #[cfg(feature = "bevy_scene")] pub use bevy_scene as scene; #[cfg(feature = "bevy_sprite")] diff --git a/crates/bevy_internal/src/prelude.rs b/crates/bevy_internal/src/prelude.rs index 40a896b78e9b6..c67ce7a838be0 100644 --- a/crates/bevy_internal/src/prelude.rs +++ b/crates/bevy_internal/src/prelude.rs @@ -47,6 +47,10 @@ pub use crate::pbr::prelude::*; #[cfg(feature = "bevy_render")] pub use crate::render::prelude::*; +#[doc(hidden)] +#[cfg(feature = "bevy_render_2d")] +pub use crate::render_2d::prelude::*; + #[doc(hidden)] #[cfg(feature = "bevy_scene")] pub use crate::scene::prelude::*; diff --git a/crates/bevy_render_2d/Cargo.toml b/crates/bevy_render_2d/Cargo.toml new file mode 100644 index 0000000000000..0510d001805b5 --- /dev/null +++ b/crates/bevy_render_2d/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "bevy_render_2d" +version = "0.16.0-dev" +edition = "2024" +description = "Provides functionality for rendering in 2d for Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[features] + +[dependencies] +bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } +bevy_platform_support = { path = "../bevy_platform_support", version = "0.16.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } + +bitflags = "2.3" +derive_more = { version = "1", default-features = false, features = ["from"] } +nonmax = "0.5" +tracing = { version = "0.1", default-features = false, features = ["std"] } + +[dev-dependencies] +# used on doc tests +bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] +all-features = true diff --git a/crates/bevy_render_2d/LICENSE-APACHE b/crates/bevy_render_2d/LICENSE-APACHE new file mode 100644 index 0000000000000..d9a10c0d8e868 --- /dev/null +++ b/crates/bevy_render_2d/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_render_2d/LICENSE-MIT b/crates/bevy_render_2d/LICENSE-MIT new file mode 100644 index 0000000000000..9cf106272ac3b --- /dev/null +++ b/crates/bevy_render_2d/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_2d/README.md b/crates/bevy_render_2d/README.md new file mode 100644 index 0000000000000..e52e7eca7c01d --- /dev/null +++ b/crates/bevy_render_2d/README.md @@ -0,0 +1,7 @@ +# Bevy Render 2d + +[![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/bevyengine/bevy#license) +[![Crates.io](https://img.shields.io/crates/v/bevy_sprite.svg)](https://crates.io/crates/bevy_sprite) +[![Downloads](https://img.shields.io/crates/d/bevy_sprite.svg)](https://crates.io/crates/bevy_sprite) +[![Docs](https://docs.rs/bevy_sprite/badge.svg)](https://docs.rs/bevy_sprite/latest/bevy_sprite/) +[![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy) diff --git a/crates/bevy_render_2d/src/lib.rs b/crates/bevy_render_2d/src/lib.rs new file mode 100644 index 0000000000000..e0bb789826695 --- /dev/null +++ b/crates/bevy_render_2d/src/lib.rs @@ -0,0 +1,10 @@ +//! Provides functionality for rendering in 2d + +#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] + +pub mod material; +pub mod mesh_pipeline; + +pub mod prelude { + pub use super::material::{AlphaMode2d, Material2d, MeshMaterial2d}; +} diff --git a/crates/bevy_render_2d/src/material/commands.rs b/crates/bevy_render_2d/src/material/commands.rs new file mode 100644 index 0000000000000..a8a605439f71e --- /dev/null +++ b/crates/bevy_render_2d/src/material/commands.rs @@ -0,0 +1,57 @@ +use core::marker::PhantomData; + +use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem}; +use bevy_render::{ + render_asset::RenderAssets, + render_phase::{ + PhaseItem, RenderCommand, RenderCommandResult, SetItemPipeline, TrackedRenderPass, + }, +}; + +use crate::mesh_pipeline::commands::{DrawMesh2d, SetMesh2dBindGroup, SetMesh2dViewBindGroup}; + +use super::{ + render::{PreparedMaterial2d, RenderMaterial2dInstances}, + Material2d, +}; + +pub type DrawMaterial2d = ( + SetItemPipeline, + SetMesh2dViewBindGroup<0>, + SetMesh2dBindGroup<1>, + SetMaterial2dBindGroup, + DrawMesh2d, +); + +pub struct SetMaterial2dBindGroup(PhantomData); + +impl RenderCommand

+ for SetMaterial2dBindGroup +{ + type Param = ( + SRes>>, + SRes>, + ); + type ViewQuery = (); + type ItemQuery = (); + + #[inline] + fn render<'w>( + item: &P, + _view: (), + _item_query: Option<()>, + (materials, material_instances): SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let materials = materials.into_inner(); + let material_instances = material_instances.into_inner(); + let Some(material_instance) = material_instances.get(&item.main_entity()) else { + return RenderCommandResult::Skip; + }; + let Some(material2d) = materials.get(*material_instance) else { + return RenderCommandResult::Skip; + }; + pass.set_bind_group(I, &material2d.bind_group, &[]); + RenderCommandResult::Success + } +} diff --git a/crates/bevy_render_2d/src/material/key.rs b/crates/bevy_render_2d/src/material/key.rs new file mode 100644 index 0000000000000..c64bea40cd797 --- /dev/null +++ b/crates/bevy_render_2d/src/material/key.rs @@ -0,0 +1,106 @@ +use core::hash::Hash; + +use bevy_asset::AssetServer; +use bevy_ecs::world::{FromWorld, World}; +use bevy_render::{ + mesh::MeshVertexBufferLayoutRef, + render_resource::{ + RenderPipelineDescriptor, ShaderRef, SpecializedMeshPipeline, SpecializedMeshPipelineError, + }, + renderer::RenderDevice, +}; + +use crate::mesh_pipeline::{key::Mesh2dPipelineKey, pipeline::Mesh2dPipeline}; + +use super::{pipeline::Material2dPipeline, Material2d}; + +pub struct Material2dKey { + pub mesh_key: Mesh2dPipelineKey, + pub bind_group_data: M::Data, +} + +impl Eq for Material2dKey where M::Data: PartialEq {} + +impl PartialEq for Material2dKey +where + M::Data: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.mesh_key == other.mesh_key && self.bind_group_data == other.bind_group_data + } +} + +impl Clone for Material2dKey +where + M::Data: Clone, +{ + fn clone(&self) -> Self { + Self { + mesh_key: self.mesh_key, + bind_group_data: self.bind_group_data.clone(), + } + } +} + +impl Hash for Material2dKey +where + M::Data: Hash, +{ + fn hash(&self, state: &mut H) { + self.mesh_key.hash(state); + self.bind_group_data.hash(state); + } +} + +impl SpecializedMeshPipeline for Material2dPipeline +where + M::Data: PartialEq + Eq + Hash + Clone, +{ + type Key = Material2dKey; + + fn specialize( + &self, + key: Self::Key, + layout: &MeshVertexBufferLayoutRef, + ) -> Result { + let mut descriptor = self.mesh2d_pipeline.specialize(key.mesh_key, layout)?; + if let Some(vertex_shader) = &self.vertex_shader { + descriptor.vertex.shader = vertex_shader.clone(); + } + + if let Some(fragment_shader) = &self.fragment_shader { + descriptor.fragment.as_mut().unwrap().shader = fragment_shader.clone(); + } + descriptor.layout = vec![ + self.mesh2d_pipeline.view_layout.clone(), + self.mesh2d_pipeline.mesh_layout.clone(), + self.material2d_layout.clone(), + ]; + + M::specialize(&mut descriptor, layout, key)?; + Ok(descriptor) + } +} + +impl FromWorld for Material2dPipeline { + fn from_world(world: &mut World) -> Self { + let asset_server = world.resource::(); + let render_device = world.resource::(); + let material2d_layout = M::bind_group_layout(render_device); + + Material2dPipeline::new( + world.resource::().clone(), + material2d_layout, + match M::vertex_shader() { + ShaderRef::Default => None, + ShaderRef::Handle(handle) => Some(handle), + ShaderRef::Path(path) => Some(asset_server.load(path)), + }, + match M::fragment_shader() { + ShaderRef::Default => None, + ShaderRef::Handle(handle) => Some(handle), + ShaderRef::Path(path) => Some(asset_server.load(path)), + }, + ) + } +} diff --git a/crates/bevy_render_2d/src/material/mod.rs b/crates/bevy_render_2d/src/material/mod.rs new file mode 100644 index 0000000000000..440b024f3b084 --- /dev/null +++ b/crates/bevy_render_2d/src/material/mod.rs @@ -0,0 +1,235 @@ +//! Provides functionality for creating 2d materials + +mod commands; +pub mod key; +mod pipeline; +pub mod plugin; +mod render; + +use bevy_asset::{AsAssetId, Asset, AssetId, Handle}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{component::Component, reflect::ReflectComponent}; +use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_render::{ + mesh::MeshVertexBufferLayoutRef, + render_resource::{ + AsBindGroup, RenderPipelineDescriptor, ShaderRef, SpecializedMeshPipelineError, + }, +}; + +use derive_more::derive::From; +use key::Material2dKey; + +/// Materials are used alongside [`Material2dPlugin`](plugin::Material2dPlugin), +/// [`Mesh2d`](bevy_render::mesh::Mesh2d), and [`MeshMaterial2d`] +/// to spawn entities that are rendered with a specific [`Material2d`] type. They serve as an easy to use high level +/// way to render [`Mesh2d`](bevy_render::mesh::Mesh2d) entities with custom shader logic. +/// +/// Materials must implement [`AsBindGroup`] to define how data will be transferred to the GPU and bound in shaders. +/// [`AsBindGroup`] can be derived, which makes generating bindings straightforward. See the [`AsBindGroup`] docs for details. +/// +/// # Example +/// +/// Here is a simple [`Material2d`] implementation. The [`AsBindGroup`] derive has many features. To see what else is available, +/// check out the [`AsBindGroup`] documentation. +/// +/// ``` +/// # use bevy_render_2d::material::{Material2d, MeshMaterial2d}; +/// # use bevy_ecs::prelude::*; +/// # use bevy_image::Image; +/// # use bevy_reflect::TypePath; +/// # use bevy_render::{mesh::{Mesh, Mesh2d}, render_resource::{AsBindGroup, ShaderRef}}; +/// # use bevy_color::{LinearRgba, palettes::basic::RED}; +/// # use bevy_asset::{Handle, AssetServer, Assets, Asset}; +/// # use bevy_math::primitives::Circle; +/// # +/// #[derive(AsBindGroup, Debug, Clone, Asset, TypePath)] +/// pub struct CustomMaterial { +/// // Uniform bindings must implement `ShaderType`, which will be used to convert the value to +/// // its shader-compatible equivalent. Most core math types already implement `ShaderType`. +/// #[uniform(0)] +/// color: LinearRgba, +/// // Images can be bound as textures in shaders. If the Image's sampler is also needed, just +/// // add the sampler attribute with a different binding index. +/// #[texture(1)] +/// #[sampler(2)] +/// color_texture: Handle, +/// } +/// +/// // All functions on `Material2d` have default impls. You only need to implement the +/// // functions that are relevant for your material. +/// impl Material2d for CustomMaterial { +/// fn fragment_shader() -> ShaderRef { +/// "shaders/custom_material.wgsl".into() +/// } +/// } +/// +/// // Spawn an entity with a mesh using `CustomMaterial`. +/// fn setup( +/// mut commands: Commands, +/// mut meshes: ResMut>, +/// mut materials: ResMut>, +/// asset_server: Res, +/// ) { +/// commands.spawn(( +/// Mesh2d(meshes.add(Circle::new(50.0))), +/// MeshMaterial2d(materials.add(CustomMaterial { +/// color: RED.into(), +/// color_texture: asset_server.load("some_image.png"), +/// })), +/// )); +/// } +/// ``` +/// +/// In WGSL shaders, the material's binding would look like this: +/// +/// ```wgsl +/// struct CustomMaterial { +/// color: vec4, +/// } +/// +/// @group(2) @binding(0) var material: CustomMaterial; +/// @group(2) @binding(1) var color_texture: texture_2d; +/// @group(2) @binding(2) var color_sampler: sampler; +/// ``` +pub trait Material2d: AsBindGroup + Asset + Clone + Sized { + /// Returns this material's vertex shader. If [`ShaderRef::Default`] is returned, the default mesh vertex shader + /// will be used. + fn vertex_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's fragment shader. If [`ShaderRef::Default`] is returned, the default mesh fragment shader + /// will be used. + fn fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Add a bias to the view depth of the mesh which can be used to force a specific render order. + #[inline] + fn depth_bias(&self) -> f32 { + 0.0 + } + + fn alpha_mode(&self) -> AlphaMode2d { + AlphaMode2d::Opaque + } + + /// Customizes the default [`RenderPipelineDescriptor`]. + #[expect( + unused_variables, + reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion." + )] + #[inline] + fn specialize( + descriptor: &mut RenderPipelineDescriptor, + layout: &MeshVertexBufferLayoutRef, + key: Material2dKey, + ) -> Result<(), SpecializedMeshPipelineError> { + Ok(()) + } +} + +/// A [material](Material2d) used for rendering a [`Mesh2d`](bevy_render::mesh::Mesh2d). +/// +/// See [`Material2d`] for general information about 2D materials and how to implement your own materials. +/// +/// # Example +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_render::{mesh::{Mesh, Mesh2d}, render_resource::AsBindGroup}; +/// # use bevy_render_2d::material::{MeshMaterial2d, Material2d, AlphaMode2d}; +/// # use bevy_color::{Color, palettes::basic::RED}; +/// # use bevy_asset::{Asset, Assets}; +/// # use bevy_math::primitives::Circle; +/// # use bevy_reflect::Reflect; +/// # +/// # // Defining locally so that there is no need to depend on `bevy_sprite` +/// # #[derive(Asset, AsBindGroup, Reflect, Debug, Clone)] +/// # struct ColorMaterial {} +/// # impl ColorMaterial { +/// # pub fn from_color(_color: impl Into) -> Self { Self {} } +/// # } +/// # impl Material2d for ColorMaterial { +/// # fn alpha_mode(&self) -> AlphaMode2d { +/// # AlphaMode2d::Opaque +/// # } +/// # } +/// // Spawn an entity with a mesh using `ColorMaterial`. +/// fn setup( +/// mut commands: Commands, +/// mut meshes: ResMut>, +/// mut materials: ResMut>, +/// ) { +/// commands.spawn(( +/// Mesh2d(meshes.add(Circle::new(50.0))), +/// MeshMaterial2d(materials.add(ColorMaterial::from_color(RED))), +/// )); +/// } +/// ``` +/// +/// [`MeshMaterial2d`]: crate::material::MeshMaterial2d +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, From)] +#[reflect(Component, Default, Clone)] +pub struct MeshMaterial2d(pub Handle); + +impl Default for MeshMaterial2d { + fn default() -> Self { + Self(Handle::default()) + } +} + +impl PartialEq for MeshMaterial2d { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for MeshMaterial2d {} + +impl From> for AssetId { + fn from(material: MeshMaterial2d) -> Self { + material.id() + } +} + +impl From<&MeshMaterial2d> for AssetId { + fn from(material: &MeshMaterial2d) -> Self { + material.id() + } +} + +impl AsAssetId for MeshMaterial2d { + type Asset = M; + + fn as_asset_id(&self) -> AssetId { + self.id() + } +} + +/// Sets how a 2d material's base color alpha channel is used for transparency. +/// Currently, this only works with [`Mesh2d`](bevy_render::mesh::Mesh2d). Sprites are always transparent. +/// +/// This is very similar to [`AlphaMode`](bevy_render::alpha::AlphaMode) but this only applies to 2d meshes. +/// We use a separate type because 2d doesn't support all the transparency modes that 3d does. +#[derive(Debug, Default, Reflect, Copy, Clone, PartialEq)] +#[reflect(Default, Debug, Clone)] +pub enum AlphaMode2d { + /// Base color alpha values are overridden to be fully opaque (1.0). + #[default] + Opaque, + /// Reduce transparency to fully opaque or fully transparent + /// based on a threshold. + /// + /// Compares the base color alpha value to the specified threshold. + /// If the value is below the threshold, + /// considers the color to be fully transparent (alpha is set to 0.0). + /// If it is equal to or above the threshold, + /// considers the color to be fully opaque (alpha is set to 1.0). + Mask(f32), + /// The base color alpha value defines the opacity of the color. + /// Standard alpha-blending is used to blend the fragment's color + /// with the color behind it. + Blend, +} diff --git a/crates/bevy_render_2d/src/material/pipeline.rs b/crates/bevy_render_2d/src/material/pipeline.rs new file mode 100644 index 0000000000000..701c27d936969 --- /dev/null +++ b/crates/bevy_render_2d/src/material/pipeline.rs @@ -0,0 +1,48 @@ +use core::marker::PhantomData; + +use bevy_asset::Handle; +use bevy_ecs::resource::Resource; +use bevy_render::render_resource::{BindGroupLayout, Shader}; + +use crate::mesh_pipeline::pipeline::Mesh2dPipeline; + +use super::Material2d; + +/// Render pipeline data for a given [`Material2d`] +#[derive(Resource)] +pub struct Material2dPipeline { + pub mesh2d_pipeline: Mesh2dPipeline, + pub material2d_layout: BindGroupLayout, + pub vertex_shader: Option>, + pub fragment_shader: Option>, + marker: PhantomData, +} + +impl Material2dPipeline { + pub fn new( + mesh2d_pipeline: Mesh2dPipeline, + material2d_layout: BindGroupLayout, + vertex_shader: Option>, + fragment_shader: Option>, + ) -> Self { + Self { + mesh2d_pipeline, + material2d_layout, + vertex_shader, + fragment_shader, + marker: PhantomData, + } + } +} + +impl Clone for Material2dPipeline { + fn clone(&self) -> Self { + Self { + mesh2d_pipeline: self.mesh2d_pipeline.clone(), + material2d_layout: self.material2d_layout.clone(), + vertex_shader: self.vertex_shader.clone(), + fragment_shader: self.fragment_shader.clone(), + marker: PhantomData, + } + } +} diff --git a/crates/bevy_render_2d/src/material/plugin/mod.rs b/crates/bevy_render_2d/src/material/plugin/mod.rs new file mode 100644 index 0000000000000..4b6ca410dc574 --- /dev/null +++ b/crates/bevy_render_2d/src/material/plugin/mod.rs @@ -0,0 +1,90 @@ +mod systems; + +use core::{hash::Hash, marker::PhantomData}; + +use bevy_app::{App, Plugin, PostUpdate}; +use bevy_asset::{AssetApp, AssetEvents}; +use bevy_core_pipeline::core_2d::{AlphaMask2d, Opaque2d, Transparent2d}; +use bevy_ecs::schedule::IntoScheduleConfigs; +use bevy_render::{ + camera::extract_cameras, + mesh::RenderMesh, + render_asset::{prepare_assets, RenderAssetPlugin}, + render_phase::AddRenderCommand, + render_resource::SpecializedMeshPipelines, + ExtractSchedule, Render, RenderApp, RenderSet, +}; + +use super::{ + commands::DrawMaterial2d, + pipeline::Material2dPipeline, + render::{ + EntitiesNeedingSpecialization, EntitySpecializationTicks, PreparedMaterial2d, + RenderMaterial2dInstances, SpecializedMaterial2dPipelineCache, + }, + Material2d, MeshMaterial2d, +}; + +use systems::*; + +/// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`Material2d`] +/// asset type (which includes [`Material2d`] types). +pub struct Material2dPlugin(PhantomData); + +impl Default for Material2dPlugin { + fn default() -> Self { + Self(Default::default()) + } +} + +impl Plugin for Material2dPlugin +where + M::Data: PartialEq + Eq + Hash + Clone, +{ + fn build(&self, app: &mut App) { + app.init_asset::() + .init_resource::>() + .register_type::>() + .add_plugins(RenderAssetPlugin::>::default()) + .add_systems( + PostUpdate, + check_entities_needing_specialization::.after(AssetEvents), + ); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::>() + .init_resource::>() + .add_render_command::>() + .add_render_command::>() + .add_render_command::>() + .init_resource::>() + .init_resource::>>() + .add_systems( + ExtractSchedule, + ( + extract_entities_needs_specialization::.after(extract_cameras), + extract_mesh_materials_2d::, + ), + ) + .add_systems( + Render, + ( + specialize_material2d_meshes:: + .in_set(RenderSet::PrepareMeshes) + .after(prepare_assets::>) + .after(prepare_assets::), + queue_material2d_meshes:: + .in_set(RenderSet::QueueMeshes) + .after(prepare_assets::>), + ), + ); + } + } + + fn finish(&self, app: &mut App) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.init_resource::>(); + } + } +} diff --git a/crates/bevy_render_2d/src/material/plugin/systems.rs b/crates/bevy_render_2d/src/material/plugin/systems.rs new file mode 100644 index 0000000000000..5526209309c8c --- /dev/null +++ b/crates/bevy_render_2d/src/material/plugin/systems.rs @@ -0,0 +1,382 @@ +use core::hash::Hash; + +use bevy_asset::prelude::AssetChanged; +use bevy_core_pipeline::core_2d::{ + AlphaMask2d, AlphaMask2dBinKey, BatchSetKey2d, Opaque2d, Opaque2dBinKey, Transparent2d, +}; +use bevy_ecs::{ + entity::Entity, + query::{Changed, Or, With}, + removal_detection::RemovedComponents, + system::{Local, Query, Res, ResMut, SystemChangeTick}, +}; +use bevy_math::FloatOrd; +use bevy_render::{ + mesh::{Mesh2d, RenderMesh}, + render_asset::RenderAssets, + render_phase::{ + BinnedRenderPhaseType, InputUniformIndex, PhaseItemExtraIndex, ViewBinnedRenderPhases, + ViewSortedRenderPhases, + }, + render_resource::{PipelineCache, SpecializedMeshPipelines}, + sync_world::MainEntity, + view::{ExtractedView, RenderVisibleEntities, ViewVisibility}, + Extract, +}; +use bevy_utils::Parallel; + +use crate::{ + material::{ + key::Material2dKey, + pipeline::Material2dPipeline, + render::{ + EntitiesNeedingSpecialization, EntitySpecializationTicks, PreparedMaterial2d, + RenderMaterial2dInstances, SpecializedMaterial2dPipelineCache, + }, + AlphaMode2d, Material2d, MeshMaterial2d, + }, + mesh_pipeline::{ + key::Mesh2dPipelineKey, + render::{RenderMesh2dInstances, ViewKeyCache, ViewSpecializationTicks}, + }, +}; + +pub fn extract_mesh_materials_2d( + mut material_instances: ResMut>, + changed_meshes_query: Extract< + Query< + (Entity, &ViewVisibility, &MeshMaterial2d), + Or<(Changed, Changed>)>, + >, + >, + mut removed_visibilities_query: Extract>, + mut removed_materials_query: Extract>>, +) { + for (entity, view_visibility, material) in &changed_meshes_query { + if view_visibility.get() { + add_mesh_instance(entity, material, &mut material_instances); + } else { + remove_mesh_instance(entity, &mut material_instances); + } + } + + for entity in removed_visibilities_query + .read() + .chain(removed_materials_query.read()) + { + // Only queue a mesh for removal if we didn't pick it up above. + // It's possible that a necessary component was removed and re-added in + // the same frame. + if !changed_meshes_query.contains(entity) { + remove_mesh_instance(entity, &mut material_instances); + } + } + + // Adds or updates a mesh instance in the [`RenderMaterial2dInstances`] + // array. + fn add_mesh_instance( + entity: Entity, + material: &MeshMaterial2d, + material_instances: &mut RenderMaterial2dInstances, + ) where + M: Material2d, + { + material_instances.insert(entity.into(), material.id()); + } + + // Removes a mesh instance from the [`RenderMaterial2dInstances`] array. + fn remove_mesh_instance( + entity: Entity, + material_instances: &mut RenderMaterial2dInstances, + ) where + M: Material2d, + { + material_instances.remove(&MainEntity::from(entity)); + } +} + +pub fn extract_entities_needs_specialization( + entities_needing_specialization: Extract>>, + mut entity_specialization_ticks: ResMut>, + mut removed_mesh_material_components: Extract>>, + mut specialized_material2d_pipeline_cache: ResMut>, + views: Query<&MainEntity, With>, + ticks: SystemChangeTick, +) where + M: Material2d, +{ + for entity in entities_needing_specialization.iter() { + // Update the entity's specialization tick with this run's tick + entity_specialization_ticks.insert((*entity).into(), ticks.this_run()); + } + // Clean up any despawned entities + for entity in removed_mesh_material_components.read() { + entity_specialization_ticks.remove(&MainEntity::from(entity)); + for view in views { + if let Some(cache) = specialized_material2d_pipeline_cache.get_mut(view) { + cache.remove(&MainEntity::from(entity)); + } + } + } +} + +pub fn check_entities_needing_specialization( + needs_specialization: Query< + Entity, + ( + Or<( + Changed, + AssetChanged, + Changed>, + AssetChanged>, + )>, + With>, + ), + >, + mut par_local: Local>>, + mut entities_needing_specialization: ResMut>, +) where + M: Material2d, +{ + entities_needing_specialization.clear(); + + needs_specialization + .par_iter() + .for_each(|entity| par_local.borrow_local_mut().push(entity)); + + par_local.drain_into(&mut entities_needing_specialization); +} + +pub fn specialize_material2d_meshes( + material2d_pipeline: Res>, + mut pipelines: ResMut>>, + pipeline_cache: Res, + (render_meshes, render_materials): ( + Res>, + Res>>, + ), + mut render_mesh_instances: ResMut, + render_material_instances: Res>, + transparent_render_phases: Res>, + opaque_render_phases: Res>, + alpha_mask_render_phases: Res>, + views: Query<(&MainEntity, &ExtractedView, &RenderVisibleEntities)>, + view_key_cache: Res, + entity_specialization_ticks: Res>, + view_specialization_ticks: Res, + ticks: SystemChangeTick, + mut specialized_material_pipeline_cache: ResMut>, +) where + M::Data: PartialEq + Eq + Hash + Clone, +{ + if render_material_instances.is_empty() { + return; + } + + for (view_entity, view, visible_entities) in &views { + if !transparent_render_phases.contains_key(&view.retained_view_entity) + && !opaque_render_phases.contains_key(&view.retained_view_entity) + && !alpha_mask_render_phases.contains_key(&view.retained_view_entity) + { + continue; + } + + let Some(view_key) = view_key_cache.get(view_entity) else { + continue; + }; + + let view_tick = view_specialization_ticks.get(view_entity).unwrap(); + let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache + .entry(*view_entity) + .or_default(); + + for (_, visible_entity) in visible_entities.iter::() { + let Some(material_asset_id) = render_material_instances.get(visible_entity) else { + continue; + }; + let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap(); + let last_specialized_tick = view_specialized_material_pipeline_cache + .get(visible_entity) + .map(|(tick, _)| *tick); + let needs_specialization = last_specialized_tick.is_none_or(|tick| { + view_tick.is_newer_than(tick, ticks.this_run()) + || entity_tick.is_newer_than(tick, ticks.this_run()) + }); + if !needs_specialization { + continue; + } + let Some(mesh_instance) = render_mesh_instances.get_mut(visible_entity) else { + continue; + }; + let Some(material_2d) = render_materials.get(*material_asset_id) else { + continue; + }; + let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { + continue; + }; + let mesh_key = *view_key + | Mesh2dPipelineKey::from_primitive_topology(mesh.primitive_topology()) + | material_2d.properties.mesh_pipeline_key_bits; + + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &material2d_pipeline, + Material2dKey { + mesh_key, + bind_group_data: material_2d.key.clone(), + }, + &mesh.layout, + ); + + let pipeline_id = match pipeline_id { + Ok(id) => id, + Err(err) => { + tracing::error!("{}", err); + continue; + } + }; + + view_specialized_material_pipeline_cache + .insert(*visible_entity, (ticks.this_run(), pipeline_id)); + } + } +} + +pub fn queue_material2d_meshes( + (render_meshes, render_materials): ( + Res>, + Res>>, + ), + mut render_mesh_instances: ResMut, + render_material_instances: Res>, + mut transparent_render_phases: ResMut>, + mut opaque_render_phases: ResMut>, + mut alpha_mask_render_phases: ResMut>, + views: Query<(&MainEntity, &ExtractedView, &RenderVisibleEntities)>, + specialized_material_pipeline_cache: ResMut>, +) where + M::Data: PartialEq + Eq + Hash + Clone, +{ + if render_material_instances.is_empty() { + return; + } + + for (view_entity, view, visible_entities) in &views { + let Some(view_specialized_material_pipeline_cache) = + specialized_material_pipeline_cache.get(view_entity) + else { + continue; + }; + + let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity) + else { + continue; + }; + let Some(opaque_phase) = opaque_render_phases.get_mut(&view.retained_view_entity) else { + continue; + }; + let Some(alpha_mask_phase) = alpha_mask_render_phases.get_mut(&view.retained_view_entity) + else { + continue; + }; + + for (render_entity, visible_entity) in visible_entities.iter::() { + let Some((current_change_tick, pipeline_id)) = view_specialized_material_pipeline_cache + .get(visible_entity) + .map(|(current_change_tick, pipeline_id)| (*current_change_tick, *pipeline_id)) + else { + continue; + }; + + // Skip the entity if it's cached in a bin and up to date. + if opaque_phase.validate_cached_entity(*visible_entity, current_change_tick) + || alpha_mask_phase.validate_cached_entity(*visible_entity, current_change_tick) + { + continue; + } + + let Some(material_asset_id) = render_material_instances.get(visible_entity) else { + continue; + }; + let Some(mesh_instance) = render_mesh_instances.get_mut(visible_entity) else { + continue; + }; + let Some(material_2d) = render_materials.get(*material_asset_id) else { + continue; + }; + let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { + continue; + }; + + mesh_instance.material_bind_group_id = material_2d.get_bind_group_id(); + let mesh_z = mesh_instance.transforms.world_from_local.translation.z; + + // We don't support multidraw yet for 2D meshes, so we use this + // custom logic to generate the `BinnedRenderPhaseType` instead of + // `BinnedRenderPhaseType::mesh`, which can return + // `BinnedRenderPhaseType::MultidrawableMesh` if the hardware + // supports multidraw. + let binned_render_phase_type = if mesh_instance.automatic_batching { + BinnedRenderPhaseType::BatchableMesh + } else { + BinnedRenderPhaseType::UnbatchableMesh + }; + + match material_2d.properties.alpha_mode { + AlphaMode2d::Opaque => { + let bin_key = Opaque2dBinKey { + pipeline: pipeline_id, + draw_function: material_2d.properties.draw_function_id, + asset_id: mesh_instance.mesh_asset_id.into(), + material_bind_group_id: material_2d.get_bind_group_id().0, + }; + opaque_phase.add( + BatchSetKey2d { + indexed: mesh.indexed(), + }, + bin_key, + (*render_entity, *visible_entity), + InputUniformIndex::default(), + binned_render_phase_type, + current_change_tick, + ); + } + AlphaMode2d::Mask(_) => { + let bin_key = AlphaMask2dBinKey { + pipeline: pipeline_id, + draw_function: material_2d.properties.draw_function_id, + asset_id: mesh_instance.mesh_asset_id.into(), + material_bind_group_id: material_2d.get_bind_group_id().0, + }; + alpha_mask_phase.add( + BatchSetKey2d { + indexed: mesh.indexed(), + }, + bin_key, + (*render_entity, *visible_entity), + InputUniformIndex::default(), + binned_render_phase_type, + current_change_tick, + ); + } + AlphaMode2d::Blend => { + transparent_phase.add(Transparent2d { + entity: (*render_entity, *visible_entity), + draw_function: material_2d.properties.draw_function_id, + pipeline: pipeline_id, + // NOTE: Back-to-front ordering for transparent with ascending sort means far should have the + // lowest sort key and getting closer should increase. As we have + // -z in front of the camera, the largest distance is -far with values increasing toward the + // camera. As such we can just use mesh_z as the distance + sort_key: FloatOrd(mesh_z + material_2d.properties.depth_bias), + // Batching is done in batch_and_prepare_render_phase + batch_range: 0..1, + extra_index: PhaseItemExtraIndex::None, + extracted_index: usize::MAX, + indexed: mesh.indexed(), + }); + } + } + } + } +} diff --git a/crates/bevy_render_2d/src/material/render.rs b/crates/bevy_render_2d/src/material/render.rs new file mode 100644 index 0000000000000..c062b027b20f9 --- /dev/null +++ b/crates/bevy_render_2d/src/material/render.rs @@ -0,0 +1,198 @@ +use core::marker::PhantomData; + +use bevy_asset::AssetId; +use bevy_core_pipeline::core_2d::{AlphaMask2d, Opaque2d, Transparent2d}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Tick, + entity::Entity, + resource::Resource, + system::{lifetimeless::SRes, SystemParamItem}, +}; +use bevy_platform_support::collections::HashMap; +use bevy_render::{ + render_asset::{PrepareAssetError, RenderAsset}, + render_phase::{DrawFunctionId, DrawFunctions}, + render_resource::{AsBindGroupError, BindGroup, BindingResources, CachedRenderPipelineId}, + renderer::RenderDevice, + sync_world::MainEntityHashMap, +}; + +use crate::mesh_pipeline::{key::Mesh2dPipelineKey, render::Material2dBindGroupId}; + +use super::{commands::DrawMaterial2d, pipeline::Material2dPipeline, AlphaMode2d, Material2d}; + +#[derive(Resource, Deref, DerefMut)] +pub struct RenderMaterial2dInstances(MainEntityHashMap>); + +impl Default for RenderMaterial2dInstances { + fn default() -> Self { + Self(Default::default()) + } +} + +/// Data prepared for a [`Material2d`] instance. +pub struct PreparedMaterial2d { + #[expect(dead_code, reason = "`dead_code` under investigation")] + pub bindings: BindingResources, + pub bind_group: BindGroup, + pub key: T::Data, + pub properties: Material2dProperties, +} + +impl PreparedMaterial2d { + pub fn get_bind_group_id(&self) -> Material2dBindGroupId { + Material2dBindGroupId(Some(self.bind_group.id())) + } +} + +impl RenderAsset for PreparedMaterial2d { + type SourceAsset = M; + + type Param = ( + SRes, + SRes>, + SRes>, + SRes>, + SRes>, + M::Param, + ); + + fn prepare_asset( + material: Self::SourceAsset, + _: AssetId, + ( + render_device, + pipeline, + opaque_draw_functions, + alpha_mask_draw_functions, + transparent_draw_functions, + material_param, + ): &mut SystemParamItem, + ) -> Result> { + match material.as_bind_group(&pipeline.material2d_layout, render_device, material_param) { + Ok(prepared) => { + let mut mesh_pipeline_key_bits = Mesh2dPipelineKey::empty(); + mesh_pipeline_key_bits.insert(alpha_mode_pipeline_key(material.alpha_mode())); + + let draw_function_id = match material.alpha_mode() { + AlphaMode2d::Opaque => opaque_draw_functions.read().id::>(), + AlphaMode2d::Mask(_) => { + alpha_mask_draw_functions.read().id::>() + } + AlphaMode2d::Blend => { + transparent_draw_functions.read().id::>() + } + }; + + Ok(PreparedMaterial2d { + bindings: prepared.bindings, + bind_group: prepared.bind_group, + key: prepared.data, + properties: Material2dProperties { + depth_bias: material.depth_bias(), + alpha_mode: material.alpha_mode(), + mesh_pipeline_key_bits, + draw_function_id, + }, + }) + } + Err(AsBindGroupError::RetryNextUpdate) => { + Err(PrepareAssetError::RetryNextUpdate(material)) + } + Err(other) => Err(PrepareAssetError::AsBindGroupError(other)), + } + } +} + +const fn alpha_mode_pipeline_key(alpha_mode: AlphaMode2d) -> Mesh2dPipelineKey { + match alpha_mode { + AlphaMode2d::Blend => Mesh2dPipelineKey::BLEND_ALPHA, + AlphaMode2d::Mask(_) => Mesh2dPipelineKey::MAY_DISCARD, + _ => Mesh2dPipelineKey::NONE, + } +} + +/// Common [`Material2d`] properties, calculated for a specific material instance. +pub struct Material2dProperties { + /// The [`AlphaMode2d`] of this material. + pub alpha_mode: AlphaMode2d, + /// Add a bias to the view depth of the mesh which can be used to force a specific render order + /// for meshes with equal depth, to avoid z-fighting. + /// The bias is in depth-texture units so large values may + pub depth_bias: f32, + /// The bits in the [`Mesh2dPipelineKey`] for this material. + /// + /// [`Mesh2dPipelineKey`] are precalculated so that we can just "or" them together. + pub mesh_pipeline_key_bits: Mesh2dPipelineKey, + pub draw_function_id: DrawFunctionId, +} + +#[derive(Clone, Resource, Deref, DerefMut, Debug)] +pub struct EntitiesNeedingSpecialization { + #[deref] + pub entities: Vec, + _marker: PhantomData, +} + +impl Default for EntitiesNeedingSpecialization { + fn default() -> Self { + Self { + entities: Default::default(), + _marker: Default::default(), + } + } +} + +#[derive(Clone, Resource, Deref, DerefMut, Debug)] +pub struct EntitySpecializationTicks { + #[deref] + pub entities: MainEntityHashMap, + _marker: PhantomData, +} + +impl Default for EntitySpecializationTicks { + fn default() -> Self { + Self { + entities: MainEntityHashMap::default(), + _marker: Default::default(), + } + } +} + +/// Stores the [`SpecializedMaterial2dViewPipelineCache`] for each view. +#[derive(Resource, Deref, DerefMut)] +pub struct SpecializedMaterial2dPipelineCache { + // view_entity -> view pipeline cache + #[deref] + map: MainEntityHashMap>, + marker: PhantomData, +} + +/// Stores the cached render pipeline ID for each entity in a single view, as +/// well as the last time it was changed. +#[derive(Deref, DerefMut)] +pub struct SpecializedMaterial2dViewPipelineCache { + // material entity -> (tick, pipeline_id) + #[deref] + map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, + marker: PhantomData, +} + +impl Default for SpecializedMaterial2dPipelineCache { + fn default() -> Self { + Self { + map: HashMap::default(), + marker: PhantomData, + } + } +} + +impl Default for SpecializedMaterial2dViewPipelineCache { + fn default() -> Self { + Self { + map: HashMap::default(), + marker: PhantomData, + } + } +} diff --git a/crates/bevy_render_2d/src/mesh_pipeline/commands.rs b/crates/bevy_render_2d/src/mesh_pipeline/commands.rs new file mode 100644 index 0000000000000..c1b845215e8fc --- /dev/null +++ b/crates/bevy_render_2d/src/mesh_pipeline/commands.rs @@ -0,0 +1,134 @@ +use bevy_ecs::{ + query::ROQueryItem, + system::{ + lifetimeless::{Read, SRes}, + SystemParamItem, + }, +}; +use bevy_render::{ + mesh::{allocator::MeshAllocator, RenderMesh, RenderMeshBufferInfo}, + render_asset::RenderAssets, + render_phase::{ + PhaseItem, PhaseItemExtraIndex, RenderCommand, RenderCommandResult, TrackedRenderPass, + }, + view::ViewUniformOffset, +}; + +use super::render::{ + Mesh2dBindGroup, Mesh2dViewBindGroup, RenderMesh2dInstance, RenderMesh2dInstances, +}; + +pub struct SetMesh2dViewBindGroup; + +pub struct SetMesh2dBindGroup; + +pub struct DrawMesh2d; + +impl RenderCommand

for SetMesh2dViewBindGroup { + type Param = (); + type ViewQuery = (Read, Read); + type ItemQuery = (); + + #[inline] + fn render<'w>( + _item: &P, + (view_uniform, mesh2d_view_bind_group): ROQueryItem<'w, Self::ViewQuery>, + _view: Option<()>, + _param: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + pass.set_bind_group(I, &mesh2d_view_bind_group.value, &[view_uniform.offset]); + + RenderCommandResult::Success + } +} + +impl RenderCommand

for SetMesh2dBindGroup { + type Param = SRes; + type ViewQuery = (); + type ItemQuery = (); + + #[inline] + fn render<'w>( + item: &P, + _view: (), + _item_query: Option<()>, + mesh2d_bind_group: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let mut dynamic_offsets: [u32; 1] = Default::default(); + let mut offset_count = 0; + if let PhaseItemExtraIndex::DynamicOffset(dynamic_offset) = item.extra_index() { + dynamic_offsets[offset_count] = dynamic_offset; + offset_count += 1; + } + pass.set_bind_group( + I, + &mesh2d_bind_group.into_inner().value, + &dynamic_offsets[..offset_count], + ); + RenderCommandResult::Success + } +} + +impl RenderCommand

for DrawMesh2d { + type Param = ( + SRes>, + SRes, + SRes, + ); + type ViewQuery = (); + type ItemQuery = (); + + #[inline] + fn render<'w>( + item: &P, + _view: (), + _item_query: Option<()>, + (meshes, render_mesh2d_instances, mesh_allocator): SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let meshes = meshes.into_inner(); + let render_mesh2d_instances = render_mesh2d_instances.into_inner(); + let mesh_allocator = mesh_allocator.into_inner(); + + let Some(RenderMesh2dInstance { mesh_asset_id, .. }) = + render_mesh2d_instances.get(&item.main_entity()) + else { + return RenderCommandResult::Skip; + }; + let Some(gpu_mesh) = meshes.get(*mesh_asset_id) else { + return RenderCommandResult::Skip; + }; + let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(mesh_asset_id) else { + return RenderCommandResult::Skip; + }; + + pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..)); + + let batch_range = item.batch_range(); + match &gpu_mesh.buffer_info { + RenderMeshBufferInfo::Indexed { + index_format, + count, + } => { + let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(mesh_asset_id) + else { + return RenderCommandResult::Skip; + }; + + pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, *index_format); + + pass.draw_indexed( + index_buffer_slice.range.start..(index_buffer_slice.range.start + count), + vertex_buffer_slice.range.start as i32, + batch_range.clone(), + ); + } + RenderMeshBufferInfo::NonIndexed => { + pass.draw(vertex_buffer_slice.range, batch_range.clone()); + } + } + RenderCommandResult::Success + } +} diff --git a/crates/bevy_render_2d/src/mesh_pipeline/key.rs b/crates/bevy_render_2d/src/mesh_pipeline/key.rs new file mode 100644 index 0000000000000..489a3716369e7 --- /dev/null +++ b/crates/bevy_render_2d/src/mesh_pipeline/key.rs @@ -0,0 +1,92 @@ +use bevy_core_pipeline::tonemapping::Tonemapping; +use bevy_render::mesh::PrimitiveTopology; + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + #[repr(transparent)] + // NOTE: Apparently quadro drivers support up to 64x MSAA. + // MSAA uses the highest 3 bits for the MSAA log2(sample count) to support up to 128x MSAA. + // FIXME: make normals optional? + pub struct Mesh2dPipelineKey: u32 { + const NONE = 0; + const HDR = 1 << 0; + const TONEMAP_IN_SHADER = 1 << 1; + const DEBAND_DITHER = 1 << 2; + const BLEND_ALPHA = 1 << 3; + const MAY_DISCARD = 1 << 4; + const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; + const PRIMITIVE_TOPOLOGY_RESERVED_BITS = Self::PRIMITIVE_TOPOLOGY_MASK_BITS << Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS; + const TONEMAP_METHOD_RESERVED_BITS = Self::TONEMAP_METHOD_MASK_BITS << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_NONE = 0 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_REINHARD = 1 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_REINHARD_LUMINANCE = 2 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_ACES_FITTED = 3 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_AGX = 4 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM = 5 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_TONY_MC_MAPFACE = 6 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_BLENDER_FILMIC = 7 << Self::TONEMAP_METHOD_SHIFT_BITS; + } +} + +impl Mesh2dPipelineKey { + const MSAA_MASK_BITS: u32 = 0b111; + const MSAA_SHIFT_BITS: u32 = 32 - Self::MSAA_MASK_BITS.count_ones(); + const PRIMITIVE_TOPOLOGY_MASK_BITS: u32 = 0b111; + const PRIMITIVE_TOPOLOGY_SHIFT_BITS: u32 = Self::MSAA_SHIFT_BITS - 3; + const TONEMAP_METHOD_MASK_BITS: u32 = 0b111; + const TONEMAP_METHOD_SHIFT_BITS: u32 = + Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS - Self::TONEMAP_METHOD_MASK_BITS.count_ones(); + + pub fn from_msaa_samples(msaa_samples: u32) -> Self { + let msaa_bits = + (msaa_samples.trailing_zeros() & Self::MSAA_MASK_BITS) << Self::MSAA_SHIFT_BITS; + Self::from_bits_retain(msaa_bits) + } + + pub fn from_hdr(hdr: bool) -> Self { + if hdr { + Mesh2dPipelineKey::HDR + } else { + Mesh2dPipelineKey::NONE + } + } + + pub fn msaa_samples(&self) -> u32 { + 1 << ((self.bits() >> Self::MSAA_SHIFT_BITS) & Self::MSAA_MASK_BITS) + } + + pub fn from_primitive_topology(primitive_topology: PrimitiveTopology) -> Self { + let primitive_topology_bits = ((primitive_topology as u32) + & Self::PRIMITIVE_TOPOLOGY_MASK_BITS) + << Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS; + Self::from_bits_retain(primitive_topology_bits) + } + + pub fn primitive_topology(&self) -> PrimitiveTopology { + let primitive_topology_bits = (self.bits() >> Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS) + & Self::PRIMITIVE_TOPOLOGY_MASK_BITS; + match primitive_topology_bits { + x if x == PrimitiveTopology::PointList as u32 => PrimitiveTopology::PointList, + x if x == PrimitiveTopology::LineList as u32 => PrimitiveTopology::LineList, + x if x == PrimitiveTopology::LineStrip as u32 => PrimitiveTopology::LineStrip, + x if x == PrimitiveTopology::TriangleList as u32 => PrimitiveTopology::TriangleList, + x if x == PrimitiveTopology::TriangleStrip as u32 => PrimitiveTopology::TriangleStrip, + _ => PrimitiveTopology::default(), + } + } +} + +pub const fn tonemapping_pipeline_key(tonemapping: Tonemapping) -> Mesh2dPipelineKey { + match tonemapping { + Tonemapping::None => Mesh2dPipelineKey::TONEMAP_METHOD_NONE, + Tonemapping::Reinhard => Mesh2dPipelineKey::TONEMAP_METHOD_REINHARD, + Tonemapping::ReinhardLuminance => Mesh2dPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE, + Tonemapping::AcesFitted => Mesh2dPipelineKey::TONEMAP_METHOD_ACES_FITTED, + Tonemapping::AgX => Mesh2dPipelineKey::TONEMAP_METHOD_AGX, + Tonemapping::SomewhatBoringDisplayTransform => { + Mesh2dPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM + } + Tonemapping::TonyMcMapface => Mesh2dPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE, + Tonemapping::BlenderFilmic => Mesh2dPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC, + } +} diff --git a/crates/bevy_render_2d/src/mesh_pipeline/mod.rs b/crates/bevy_render_2d/src/mesh_pipeline/mod.rs new file mode 100644 index 0000000000000..a82e3eefce0d6 --- /dev/null +++ b/crates/bevy_render_2d/src/mesh_pipeline/mod.rs @@ -0,0 +1,155 @@ +//! Provides functionality for rendering meshes in 2d + +pub mod commands; +pub mod key; +pub mod pipeline; +pub mod render; +mod systems; + +use bevy_app::Plugin; +use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_core_pipeline::core_2d::{AlphaMask2d, Opaque2d, Transparent2d}; +use bevy_ecs::prelude::*; +use bevy_render::{ + batching::no_gpu_preprocessing::{ + self, batch_and_prepare_binned_render_phase, batch_and_prepare_sorted_render_phase, + write_batched_instance_buffer, BatchedInstanceBuffer, + }, + render_phase::sweep_old_entities, + render_resource::*, + renderer::RenderDevice, + ExtractSchedule, Render, RenderApp, + RenderSet::{self}, +}; + +use pipeline::Mesh2dPipeline; +use render::{Mesh2dUniform, RenderMesh2dInstances, ViewKeyCache, ViewSpecializationTicks}; +use systems::{ + check_views_need_specialization, extract_mesh2d, prepare_mesh2d_bind_group, + prepare_mesh2d_view_bind_groups, +}; + +const MESH2D_VERTEX_OUTPUT: Handle = weak_handle!("71e279c7-85a0-46ac-9a76-1586cbf506d0"); +const MESH2D_VIEW_TYPES_HANDLE: Handle = + weak_handle!("01087b0d-91e9-46ac-8628-dfe19a7d4b83"); +const MESH2D_VIEW_BINDINGS_HANDLE: Handle = + weak_handle!("fbdd8b80-503d-4688-bcec-db29ab4620b2"); +const MESH2D_TYPES_HANDLE: Handle = weak_handle!("199f2089-6e99-4348-9bb1-d82816640a7f"); +const MESH2D_BINDINGS_HANDLE: Handle = weak_handle!("a7bd44cc-0580-4427-9a00-721cf386b6e4"); +const MESH2D_FUNCTIONS_HANDLE: Handle = + weak_handle!("0d08ff71-68c1-4017-83e2-bfc34d285c51"); +const MESH2D_SHADER_HANDLE: Handle = weak_handle!("91a7602b-df95-4ea3-9d97-076abcb69d91"); + +#[derive(Default)] +pub struct Mesh2dRenderPlugin; + +impl Plugin for Mesh2dRenderPlugin { + fn build(&self, app: &mut bevy_app::App) { + load_internal_asset!( + app, + MESH2D_VERTEX_OUTPUT, + "shaders/mesh2d_vertex_output.wgsl", + Shader::from_wgsl + ); + load_internal_asset!( + app, + MESH2D_VIEW_TYPES_HANDLE, + "shaders/mesh2d_view_types.wgsl", + Shader::from_wgsl + ); + load_internal_asset!( + app, + MESH2D_VIEW_BINDINGS_HANDLE, + "shaders/mesh2d_view_bindings.wgsl", + Shader::from_wgsl + ); + load_internal_asset!( + app, + MESH2D_TYPES_HANDLE, + "shaders/mesh2d_types.wgsl", + Shader::from_wgsl + ); + load_internal_asset!( + app, + MESH2D_FUNCTIONS_HANDLE, + "shaders/mesh2d_functions.wgsl", + Shader::from_wgsl + ); + load_internal_asset!( + app, + MESH2D_SHADER_HANDLE, + "shaders/mesh2d.wgsl", + Shader::from_wgsl + ); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::() + .init_resource::() + .init_resource::>() + .add_systems(ExtractSchedule, extract_mesh2d) + .add_systems( + Render, + ( + ( + sweep_old_entities::, + sweep_old_entities::, + ) + .in_set(RenderSet::QueueSweep), + batch_and_prepare_binned_render_phase:: + .in_set(RenderSet::PrepareResources), + batch_and_prepare_binned_render_phase:: + .in_set(RenderSet::PrepareResources), + batch_and_prepare_sorted_render_phase:: + .in_set(RenderSet::PrepareResources), + write_batched_instance_buffer:: + .in_set(RenderSet::PrepareResourcesFlush), + prepare_mesh2d_bind_group.in_set(RenderSet::PrepareBindGroups), + prepare_mesh2d_view_bind_groups.in_set(RenderSet::PrepareBindGroups), + no_gpu_preprocessing::clear_batched_cpu_instance_buffers:: + .in_set(RenderSet::Cleanup) + .after(RenderSet::Render), + ), + ); + } + } + + fn finish(&self, app: &mut bevy_app::App) { + let mut mesh_bindings_shader_defs = Vec::with_capacity(1); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + let render_device = render_app.world().resource::(); + let batched_instance_buffer = + BatchedInstanceBuffer::::new(render_device); + + if let Some(per_object_buffer_batch_size) = + GpuArrayBuffer::::batch_size(render_device) + { + mesh_bindings_shader_defs.push(ShaderDefVal::UInt( + "PER_OBJECT_BUFFER_BATCH_SIZE".into(), + per_object_buffer_batch_size, + )); + } + + render_app + .insert_resource(batched_instance_buffer) + .init_resource::() + .init_resource::() + .init_resource::() + .add_systems( + Render, + check_views_need_specialization.in_set(RenderSet::PrepareAssets), + ); + } + + // Load the mesh_bindings shader module here as it depends on runtime information about + // whether storage buffers are supported, or the maximum uniform buffer binding size. + load_internal_asset!( + app, + MESH2D_BINDINGS_HANDLE, + "shaders/mesh2d_bindings.wgsl", + Shader::from_wgsl_with_defs, + mesh_bindings_shader_defs + ); + } +} diff --git a/crates/bevy_render_2d/src/mesh_pipeline/pipeline.rs b/crates/bevy_render_2d/src/mesh_pipeline/pipeline.rs new file mode 100644 index 0000000000000..6f6fbfb55c496 --- /dev/null +++ b/crates/bevy_render_2d/src/mesh_pipeline/pipeline.rs @@ -0,0 +1,393 @@ +use bevy_asset::{AssetId, Handle}; +use bevy_core_pipeline::{ + core_2d::CORE_2D_DEPTH_FORMAT, tonemapping::get_lut_bind_group_layout_entries, +}; +use bevy_ecs::{ + entity::Entity, + resource::Resource, + system::{lifetimeless::SRes, Res, SystemParamItem, SystemState}, + world::{FromWorld, World}, +}; +use bevy_image::{BevyDefault, Image, ImageSampler, TextureFormatPixelInfo}; +use bevy_render::{ + batching::{gpu_preprocessing::IndirectParametersCpuMetadata, GetBatchData, GetFullBatchData}, + globals::GlobalsUniform, + mesh::{allocator::MeshAllocator, Mesh, MeshVertexBufferLayoutRef, RenderMesh}, + render_asset::RenderAssets, + render_resource::{ + binding_types::uniform_buffer, BindGroupLayout, BindGroupLayoutEntries, BlendState, + ColorTargetState, ColorWrites, CompareFunction, DepthBiasState, DepthStencilState, + FragmentState, FrontFace, GpuArrayBuffer, MultisampleState, PolygonMode, PrimitiveState, + RenderPipelineDescriptor, Sampler, ShaderDefVal, ShaderStages, SpecializedMeshPipeline, + SpecializedMeshPipelineError, StencilFaceState, StencilState, TexelCopyBufferLayout, + TextureFormat, TextureView, TextureViewDescriptor, VertexState, + }, + renderer::{RenderDevice, RenderQueue}, + sync_world::MainEntity, + texture::{DefaultImageSampler, GpuImage}, + view::{ViewTarget, ViewUniform}, +}; + +use nonmax::NonMaxU32; + +use super::{ + key::Mesh2dPipelineKey, + render::{Material2dBindGroupId, Mesh2dUniform, RenderMesh2dInstances}, + MESH2D_SHADER_HANDLE, +}; + +#[derive(Resource, Clone)] +pub struct Mesh2dPipeline { + pub view_layout: BindGroupLayout, + pub mesh_layout: BindGroupLayout, + // This dummy white texture is to be used in place of optional textures + pub dummy_white_gpu_image: GpuImage, + pub per_object_buffer_batch_size: Option, +} + +impl FromWorld for Mesh2dPipeline { + fn from_world(world: &mut World) -> Self { + let mut system_state: SystemState<( + Res, + Res, + Res, + )> = SystemState::new(world); + let (render_device, render_queue, default_sampler) = system_state.get_mut(world); + let render_device = render_device.into_inner(); + let tonemapping_lut_entries = get_lut_bind_group_layout_entries(); + let view_layout = render_device.create_bind_group_layout( + "mesh2d_view_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX_FRAGMENT, + ( + (0, uniform_buffer::(true)), + (1, uniform_buffer::(false)), + ( + 2, + tonemapping_lut_entries[0].visibility(ShaderStages::FRAGMENT), + ), + ( + 3, + tonemapping_lut_entries[1].visibility(ShaderStages::FRAGMENT), + ), + ), + ), + ); + + let mesh_layout = render_device.create_bind_group_layout( + "mesh2d_layout", + &BindGroupLayoutEntries::single( + ShaderStages::VERTEX_FRAGMENT, + GpuArrayBuffer::::binding_layout(render_device), + ), + ); + // A 1x1x1 'all 1.0' texture to use as a dummy texture to use in place of optional StandardMaterial textures + let dummy_white_gpu_image = { + let image = Image::default(); + let texture = render_device.create_texture(&image.texture_descriptor); + let sampler = match image.sampler { + ImageSampler::Default => (**default_sampler).clone(), + ImageSampler::Descriptor(ref descriptor) => { + render_device.create_sampler(&descriptor.as_wgpu()) + } + }; + + let format_size = image.texture_descriptor.format.pixel_size(); + render_queue.write_texture( + texture.as_image_copy(), + image.data.as_ref().expect("Image has no data"), + TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(image.width() * format_size as u32), + rows_per_image: None, + }, + image.texture_descriptor.size, + ); + + let texture_view = texture.create_view(&TextureViewDescriptor::default()); + GpuImage { + texture, + texture_view, + texture_format: image.texture_descriptor.format, + sampler, + size: image.texture_descriptor.size, + mip_level_count: image.texture_descriptor.mip_level_count, + } + }; + Mesh2dPipeline { + view_layout, + mesh_layout, + dummy_white_gpu_image, + per_object_buffer_batch_size: GpuArrayBuffer::::batch_size( + render_device, + ), + } + } +} + +impl Mesh2dPipeline { + pub fn get_image_texture<'a>( + &'a self, + gpu_images: &'a RenderAssets, + handle_option: &Option>, + ) -> Option<(&'a TextureView, &'a Sampler)> { + if let Some(handle) = handle_option { + let gpu_image = gpu_images.get(handle)?; + Some((&gpu_image.texture_view, &gpu_image.sampler)) + } else { + Some(( + &self.dummy_white_gpu_image.texture_view, + &self.dummy_white_gpu_image.sampler, + )) + } + } +} + +impl GetBatchData for Mesh2dPipeline { + type Param = ( + SRes, + SRes>, + SRes, + ); + type CompareData = (Material2dBindGroupId, AssetId); + type BufferData = Mesh2dUniform; + + fn get_batch_data( + (mesh_instances, _, _): &SystemParamItem, + (_entity, main_entity): (Entity, MainEntity), + ) -> Option<(Self::BufferData, Option)> { + let mesh_instance = mesh_instances.get(&main_entity)?; + Some(( + Mesh2dUniform::from_components(&mesh_instance.transforms, mesh_instance.tag), + mesh_instance.automatic_batching.then_some(( + mesh_instance.material_bind_group_id, + mesh_instance.mesh_asset_id, + )), + )) + } +} + +impl GetFullBatchData for Mesh2dPipeline { + type BufferInputData = (); + + fn get_binned_batch_data( + (mesh_instances, _, _): &SystemParamItem, + main_entity: MainEntity, + ) -> Option { + let mesh_instance = mesh_instances.get(&main_entity)?; + Some(Mesh2dUniform::from_components( + &mesh_instance.transforms, + mesh_instance.tag, + )) + } + + fn get_index_and_compare_data( + _: &SystemParamItem, + _query_item: MainEntity, + ) -> Option<(NonMaxU32, Option)> { + tracing::error!( + "`get_index_and_compare_data` is only intended for GPU mesh uniform building, \ + but this is not yet implemented for 2d meshes" + ); + None + } + + fn get_binned_index( + _: &SystemParamItem, + _query_item: MainEntity, + ) -> Option { + tracing::error!( + "`get_binned_index` is only intended for GPU mesh uniform building, \ + but this is not yet implemented for 2d meshes" + ); + None + } + + fn write_batch_indirect_parameters_metadata( + indexed: bool, + base_output_index: u32, + batch_set_index: Option, + indirect_parameters_buffer: &mut bevy_render::batching::gpu_preprocessing::UntypedPhaseIndirectParametersBuffers, + indirect_parameters_offset: u32, + ) { + // Note that `IndirectParameters` covers both of these structures, even + // though they actually have distinct layouts. See the comment above that + // type for more information. + let indirect_parameters = IndirectParametersCpuMetadata { + base_output_index, + batch_set_index: match batch_set_index { + None => !0, + Some(batch_set_index) => u32::from(batch_set_index), + }, + }; + + if indexed { + indirect_parameters_buffer + .indexed + .set(indirect_parameters_offset, indirect_parameters); + } else { + indirect_parameters_buffer + .non_indexed + .set(indirect_parameters_offset, indirect_parameters); + } + } +} + +impl SpecializedMeshPipeline for Mesh2dPipeline { + type Key = Mesh2dPipelineKey; + + fn specialize( + &self, + key: Self::Key, + layout: &MeshVertexBufferLayoutRef, + ) -> Result { + let mut shader_defs = Vec::new(); + let mut vertex_attributes = Vec::new(); + + if layout.0.contains(Mesh::ATTRIBUTE_POSITION) { + shader_defs.push("VERTEX_POSITIONS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0)); + } + + if layout.0.contains(Mesh::ATTRIBUTE_NORMAL) { + shader_defs.push("VERTEX_NORMALS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_NORMAL.at_shader_location(1)); + } + + if layout.0.contains(Mesh::ATTRIBUTE_UV_0) { + shader_defs.push("VERTEX_UVS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_UV_0.at_shader_location(2)); + } + + if layout.0.contains(Mesh::ATTRIBUTE_TANGENT) { + shader_defs.push("VERTEX_TANGENTS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(3)); + } + + if layout.0.contains(Mesh::ATTRIBUTE_COLOR) { + shader_defs.push("VERTEX_COLORS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(4)); + } + + if key.contains(Mesh2dPipelineKey::TONEMAP_IN_SHADER) { + shader_defs.push("TONEMAP_IN_SHADER".into()); + shader_defs.push(ShaderDefVal::UInt( + "TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(), + 2, + )); + shader_defs.push(ShaderDefVal::UInt( + "TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(), + 3, + )); + + let method = key.intersection(Mesh2dPipelineKey::TONEMAP_METHOD_RESERVED_BITS); + + match method { + Mesh2dPipelineKey::TONEMAP_METHOD_NONE => { + shader_defs.push("TONEMAP_METHOD_NONE".into()); + } + Mesh2dPipelineKey::TONEMAP_METHOD_REINHARD => { + shader_defs.push("TONEMAP_METHOD_REINHARD".into()); + } + Mesh2dPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE => { + shader_defs.push("TONEMAP_METHOD_REINHARD_LUMINANCE".into()); + } + Mesh2dPipelineKey::TONEMAP_METHOD_ACES_FITTED => { + shader_defs.push("TONEMAP_METHOD_ACES_FITTED".into()); + } + Mesh2dPipelineKey::TONEMAP_METHOD_AGX => { + shader_defs.push("TONEMAP_METHOD_AGX".into()); + } + Mesh2dPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM => { + shader_defs.push("TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM".into()); + } + Mesh2dPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC => { + shader_defs.push("TONEMAP_METHOD_BLENDER_FILMIC".into()); + } + Mesh2dPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE => { + shader_defs.push("TONEMAP_METHOD_TONY_MC_MAPFACE".into()); + } + _ => {} + } + // Debanding is tied to tonemapping in the shader, cannot run without it. + if key.contains(Mesh2dPipelineKey::DEBAND_DITHER) { + shader_defs.push("DEBAND_DITHER".into()); + } + } + + if key.contains(Mesh2dPipelineKey::MAY_DISCARD) { + shader_defs.push("MAY_DISCARD".into()); + } + + let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?; + + let format = match key.contains(Mesh2dPipelineKey::HDR) { + true => ViewTarget::TEXTURE_FORMAT_HDR, + false => TextureFormat::bevy_default(), + }; + + let (depth_write_enabled, label, blend); + if key.contains(Mesh2dPipelineKey::BLEND_ALPHA) { + label = "transparent_mesh2d_pipeline"; + blend = Some(BlendState::ALPHA_BLENDING); + depth_write_enabled = false; + } else { + label = "opaque_mesh2d_pipeline"; + blend = None; + depth_write_enabled = true; + } + + Ok(RenderPipelineDescriptor { + vertex: VertexState { + shader: MESH2D_SHADER_HANDLE, + entry_point: "vertex".into(), + shader_defs: shader_defs.clone(), + buffers: vec![vertex_buffer_layout], + }, + fragment: Some(FragmentState { + shader: MESH2D_SHADER_HANDLE, + shader_defs, + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format, + blend, + write_mask: ColorWrites::ALL, + })], + }), + layout: vec![self.view_layout.clone(), self.mesh_layout.clone()], + push_constant_ranges: vec![], + primitive: PrimitiveState { + front_face: FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: PolygonMode::Fill, + conservative: false, + topology: key.primitive_topology(), + strip_index_format: None, + }, + depth_stencil: Some(DepthStencilState { + format: CORE_2D_DEPTH_FORMAT, + depth_write_enabled, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), + multisample: MultisampleState { + count: key.msaa_samples(), + mask: !0, + alpha_to_coverage_enabled: false, + }, + label: Some(label.into()), + zero_initialize_workgroup_memory: false, + }) + } +} diff --git a/crates/bevy_render_2d/src/mesh_pipeline/render.rs b/crates/bevy_render_2d/src/mesh_pipeline/render.rs new file mode 100644 index 0000000000000..973fe003ec33f --- /dev/null +++ b/crates/bevy_render_2d/src/mesh_pipeline/render.rs @@ -0,0 +1,90 @@ +use bevy_asset::AssetId; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::{Component, Tick}, + resource::Resource, +}; +use bevy_math::{Affine3, Vec4}; +use bevy_render::{ + mesh::Mesh, + render_resource::{BindGroup, BindGroupId, ShaderType}, + sync_world::MainEntityHashMap, +}; + +use super::key::Mesh2dPipelineKey; + +#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] +pub struct ViewKeyCache(MainEntityHashMap); + +#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] +pub struct ViewSpecializationTicks(MainEntityHashMap); + +#[derive(Resource)] +pub struct Mesh2dBindGroup { + pub value: BindGroup, +} + +#[derive(Component)] +pub struct Mesh2dViewBindGroup { + pub value: BindGroup, +} + +#[derive(Component, Clone, Copy, Default, PartialEq, Eq, Deref, DerefMut)] +pub struct Material2dBindGroupId(pub Option); + +#[derive(ShaderType, Clone, Copy)] +pub struct Mesh2dUniform { + // Affine 4x3 matrix transposed to 3x4 + pub world_from_local: [Vec4; 3], + // 3x3 matrix packed in mat2x4 and f32 as: + // [0].xyz, [1].x, + // [1].yz, [2].xy + // [2].z + pub local_from_world_transpose_a: [Vec4; 2], + pub local_from_world_transpose_b: f32, + pub flags: u32, + pub tag: u32, +} + +impl Mesh2dUniform { + pub fn from_components(mesh_transforms: &Mesh2dTransforms, tag: u32) -> Self { + let (local_from_world_transpose_a, local_from_world_transpose_b) = + mesh_transforms.world_from_local.inverse_transpose_3x3(); + Self { + world_from_local: mesh_transforms.world_from_local.to_transpose(), + local_from_world_transpose_a, + local_from_world_transpose_b, + flags: mesh_transforms.flags, + tag, + } + } +} + +pub struct RenderMesh2dInstance { + pub transforms: Mesh2dTransforms, + pub mesh_asset_id: AssetId, + pub material_bind_group_id: Material2dBindGroupId, + pub automatic_batching: bool, + pub tag: u32, +} + +#[derive(Default, Resource, Deref, DerefMut)] +pub struct RenderMesh2dInstances(MainEntityHashMap); + +#[derive(Component)] +pub struct Mesh2dTransforms { + pub world_from_local: Affine3, + pub flags: u32, +} + +// NOTE: These must match the bit flags in bevy_sprite/src/mesh2d/mesh2d.wgsl! +bitflags::bitflags! { + #[repr(transparent)] + pub struct MeshFlags: u32 { + const NONE = 0; + const UNINITIALIZED = 0xFFFF; + } +} + +#[derive(Component, Default)] +pub(super) struct Mesh2dMarker; diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d.wgsl b/crates/bevy_render_2d/src/mesh_pipeline/shaders/mesh2d.wgsl similarity index 98% rename from crates/bevy_sprite/src/mesh2d/mesh2d.wgsl rename to crates/bevy_render_2d/src/mesh_pipeline/shaders/mesh2d.wgsl index e909608be8ba1..a7f0cc678f021 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh2d.wgsl +++ b/crates/bevy_render_2d/src/mesh_pipeline/shaders/mesh2d.wgsl @@ -1,4 +1,4 @@ -#import bevy_sprite::{ +#import bevy_render_2d::{ mesh2d_functions as mesh_functions, mesh2d_vertex_output::VertexOutput, mesh2d_view_bindings::view, diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d_bindings.wgsl b/crates/bevy_render_2d/src/mesh_pipeline/shaders/mesh2d_bindings.wgsl similarity index 70% rename from crates/bevy_sprite/src/mesh2d/mesh2d_bindings.wgsl rename to crates/bevy_render_2d/src/mesh_pipeline/shaders/mesh2d_bindings.wgsl index fc2bf643d5437..e017c498e97d7 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh2d_bindings.wgsl +++ b/crates/bevy_render_2d/src/mesh_pipeline/shaders/mesh2d_bindings.wgsl @@ -1,6 +1,6 @@ -#define_import_path bevy_sprite::mesh2d_bindings +#define_import_path bevy_render_2d::mesh2d_bindings -#import bevy_sprite::mesh2d_types::Mesh2d +#import bevy_render_2d::mesh2d_types::Mesh2d #ifdef PER_OBJECT_BUFFER_BATCH_SIZE @group(1) @binding(0) var mesh: array; diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d_functions.wgsl b/crates/bevy_render_2d/src/mesh_pipeline/shaders/mesh2d_functions.wgsl similarity index 95% rename from crates/bevy_sprite/src/mesh2d/mesh2d_functions.wgsl rename to crates/bevy_render_2d/src/mesh_pipeline/shaders/mesh2d_functions.wgsl index dbd73fb171f3f..149fa31a0cc9a 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh2d_functions.wgsl +++ b/crates/bevy_render_2d/src/mesh_pipeline/shaders/mesh2d_functions.wgsl @@ -1,6 +1,6 @@ -#define_import_path bevy_sprite::mesh2d_functions +#define_import_path bevy_render_2d::mesh2d_functions -#import bevy_sprite::{ +#import bevy_render_2d::{ mesh2d_view_bindings::view, mesh2d_bindings::mesh, } diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d_types.wgsl b/crates/bevy_render_2d/src/mesh_pipeline/shaders/mesh2d_types.wgsl similarity index 91% rename from crates/bevy_sprite/src/mesh2d/mesh2d_types.wgsl rename to crates/bevy_render_2d/src/mesh_pipeline/shaders/mesh2d_types.wgsl index e29264e0bf4f3..a6376e26bca74 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh2d_types.wgsl +++ b/crates/bevy_render_2d/src/mesh_pipeline/shaders/mesh2d_types.wgsl @@ -1,4 +1,4 @@ -#define_import_path bevy_sprite::mesh2d_types +#define_import_path bevy_render_2d::mesh2d_types struct Mesh2d { // Affine 4x3 matrix transposed to 3x4 diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d_vertex_output.wgsl b/crates/bevy_render_2d/src/mesh_pipeline/shaders/mesh2d_vertex_output.wgsl similarity index 89% rename from crates/bevy_sprite/src/mesh2d/mesh2d_vertex_output.wgsl rename to crates/bevy_render_2d/src/mesh_pipeline/shaders/mesh2d_vertex_output.wgsl index c7839caffeebb..9cee35cb44cbe 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh2d_vertex_output.wgsl +++ b/crates/bevy_render_2d/src/mesh_pipeline/shaders/mesh2d_vertex_output.wgsl @@ -1,4 +1,4 @@ -#define_import_path bevy_sprite::mesh2d_vertex_output +#define_import_path bevy_render_2d::mesh2d_vertex_output struct VertexOutput { // this is `clip position` when the struct is used as a vertex stage output diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d_view_bindings.wgsl b/crates/bevy_render_2d/src/mesh_pipeline/shaders/mesh2d_view_bindings.wgsl similarity index 83% rename from crates/bevy_sprite/src/mesh2d/mesh2d_view_bindings.wgsl rename to crates/bevy_render_2d/src/mesh_pipeline/shaders/mesh2d_view_bindings.wgsl index cc43da65ce35c..4f3729b95ad4e 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh2d_view_bindings.wgsl +++ b/crates/bevy_render_2d/src/mesh_pipeline/shaders/mesh2d_view_bindings.wgsl @@ -1,4 +1,4 @@ -#define_import_path bevy_sprite::mesh2d_view_bindings +#define_import_path bevy_render_2d::mesh2d_view_bindings #import bevy_render::view::View #import bevy_render::globals::Globals diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d_view_types.wgsl b/crates/bevy_render_2d/src/mesh_pipeline/shaders/mesh2d_view_types.wgsl similarity index 50% rename from crates/bevy_sprite/src/mesh2d/mesh2d_view_types.wgsl rename to crates/bevy_render_2d/src/mesh_pipeline/shaders/mesh2d_view_types.wgsl index 7868a05d40ff8..13bccd6ad5424 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh2d_view_types.wgsl +++ b/crates/bevy_render_2d/src/mesh_pipeline/shaders/mesh2d_view_types.wgsl @@ -1,4 +1,4 @@ -#define_import_path bevy_sprite::mesh2d_view_types +#define_import_path bevy_render_2d::mesh2d_view_types #import bevy_render::view #import bevy_render::globals diff --git a/crates/bevy_render_2d/src/mesh_pipeline/systems.rs b/crates/bevy_render_2d/src/mesh_pipeline/systems.rs new file mode 100644 index 0000000000000..0e44f299ce01b --- /dev/null +++ b/crates/bevy_render_2d/src/mesh_pipeline/systems.rs @@ -0,0 +1,158 @@ +use bevy_core_pipeline::{ + core_2d::Camera2d, + tonemapping::{get_lut_bindings, DebandDither, Tonemapping, TonemappingLuts}, +}; +use bevy_ecs::{ + entity::Entity, + query::{Has, With}, + system::{Commands, Query, Res, ResMut, SystemChangeTick}, +}; +use bevy_render::{ + batching::{no_gpu_preprocessing::BatchedInstanceBuffer, NoAutomaticBatching}, + globals::GlobalsBuffer, + mesh::{Mesh2d, MeshTag}, + render_asset::RenderAssets, + render_resource::BindGroupEntries, + renderer::RenderDevice, + sync_world::MainEntity, + texture::{FallbackImage, GpuImage}, + view::{ExtractedView, Msaa, ViewUniforms, ViewVisibility}, + Extract, +}; +use bevy_transform::components::GlobalTransform; + +use super::{ + key::{tonemapping_pipeline_key, Mesh2dPipelineKey}, + pipeline::Mesh2dPipeline, + render::{ + Material2dBindGroupId, Mesh2dBindGroup, Mesh2dTransforms, Mesh2dUniform, + Mesh2dViewBindGroup, MeshFlags, RenderMesh2dInstance, RenderMesh2dInstances, ViewKeyCache, + ViewSpecializationTicks, + }, +}; + +pub fn prepare_mesh2d_bind_group( + mut commands: Commands, + mesh2d_pipeline: Res, + render_device: Res, + mesh2d_uniforms: Res>, +) { + if let Some(binding) = mesh2d_uniforms.instance_data_binding() { + commands.insert_resource(Mesh2dBindGroup { + value: render_device.create_bind_group( + "mesh2d_bind_group", + &mesh2d_pipeline.mesh_layout, + &BindGroupEntries::single(binding), + ), + }); + } +} + +pub fn prepare_mesh2d_view_bind_groups( + mut commands: Commands, + render_device: Res, + mesh2d_pipeline: Res, + view_uniforms: Res, + views: Query<(Entity, &Tonemapping), (With, With)>, + globals_buffer: Res, + tonemapping_luts: Res, + images: Res>, + fallback_image: Res, +) { + let (Some(view_binding), Some(globals)) = ( + view_uniforms.uniforms.binding(), + globals_buffer.buffer.binding(), + ) else { + return; + }; + + for (entity, tonemapping) in &views { + let lut_bindings = + get_lut_bindings(&images, &tonemapping_luts, tonemapping, &fallback_image); + let view_bind_group = render_device.create_bind_group( + "mesh2d_view_bind_group", + &mesh2d_pipeline.view_layout, + &BindGroupEntries::with_indices(( + (0, view_binding.clone()), + (1, globals.clone()), + (2, lut_bindings.0), + (3, lut_bindings.1), + )), + ); + + commands.entity(entity).insert(Mesh2dViewBindGroup { + value: view_bind_group, + }); + } +} + +pub fn extract_mesh2d( + mut render_mesh_instances: ResMut, + query: Extract< + Query<( + Entity, + &ViewVisibility, + &GlobalTransform, + &Mesh2d, + Option<&MeshTag>, + Has, + )>, + >, +) { + render_mesh_instances.clear(); + + for (entity, view_visibility, transform, handle, tag, no_automatic_batching) in &query { + if !view_visibility.get() { + continue; + } + render_mesh_instances.insert( + entity.into(), + RenderMesh2dInstance { + transforms: Mesh2dTransforms { + world_from_local: (&transform.affine()).into(), + flags: MeshFlags::empty().bits(), + }, + mesh_asset_id: handle.0.id(), + material_bind_group_id: Material2dBindGroupId::default(), + automatic_batching: !no_automatic_batching, + tag: tag.map_or(0, |i| **i), + }, + ); + } +} + +pub(super) fn check_views_need_specialization( + mut view_key_cache: ResMut, + mut view_specialization_ticks: ResMut, + views: Query<( + &MainEntity, + &ExtractedView, + &Msaa, + Option<&Tonemapping>, + Option<&DebandDither>, + )>, + ticks: SystemChangeTick, +) { + for (view_entity, view, msaa, tonemapping, dither) in &views { + let mut view_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()) + | Mesh2dPipelineKey::from_hdr(view.hdr); + + if !view.hdr { + if let Some(tonemapping) = tonemapping { + view_key |= Mesh2dPipelineKey::TONEMAP_IN_SHADER; + view_key |= tonemapping_pipeline_key(*tonemapping); + } + if let Some(DebandDither::Enabled) = dither { + view_key |= Mesh2dPipelineKey::DEBAND_DITHER; + } + } + + if !view_key_cache + .get_mut(view_entity) + .is_some_and(|current_key| *current_key == view_key) + { + view_key_cache.insert(*view_entity, view_key); + view_specialization_ticks.insert(*view_entity, ticks.this_run()); + } + } +} diff --git a/crates/bevy_sprite/Cargo.toml b/crates/bevy_sprite/Cargo.toml index 67c28f0ec8c4a..ad709701d3515 100644 --- a/crates/bevy_sprite/Cargo.toml +++ b/crates/bevy_sprite/Cargo.toml @@ -25,6 +25,7 @@ bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } bevy_picking = { path = "../bevy_picking", version = "0.16.0-dev", optional = true } bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } +bevy_render_2d = { path = "../bevy_render_2d", version = "0.16.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } bevy_window = { path = "../bevy_window", version = "0.16.0-dev", optional = true } diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 37d4d2d6e48ef..49be29663b0c2 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -30,7 +30,7 @@ pub mod prelude { pub use crate::{ sprite::{Sprite, SpriteImageMode}, texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer}, - ColorMaterial, MeshMaterial2d, ScalingMode, + ColorMaterial, ScalingMode, }; } @@ -96,7 +96,7 @@ impl Plugin for SpritePlugin { .register_type::() .register_type::() .register_type::() - .add_plugins((Mesh2dRenderPlugin, ColorMaterialPlugin)) + .add_plugins(ColorMaterialPlugin) .add_systems( PostUpdate, ( @@ -133,7 +133,7 @@ impl Plugin for SpritePlugin { ( queue_sprites .in_set(RenderSet::Queue) - .ambiguous_with(queue_material2d_meshes::), + .before(RenderSet::QueueMeshes), prepare_sprite_image_bind_groups.in_set(RenderSet::PrepareBindGroups), prepare_sprite_view_bind_groups.in_set(RenderSet::PrepareBindGroups), sort_binned_render_phase::.in_set(RenderSet::PhaseSort), diff --git a/crates/bevy_sprite/src/mesh2d/color_material.rs b/crates/bevy_sprite/src/mesh2d/color_material.rs index 83b69307769cb..3e17743b78f7d 100644 --- a/crates/bevy_sprite/src/mesh2d/color_material.rs +++ b/crates/bevy_sprite/src/mesh2d/color_material.rs @@ -1,4 +1,3 @@ -use crate::{AlphaMode2d, Material2d, Material2dPlugin}; use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, weak_handle, Asset, AssetApp, Assets, Handle}; use bevy_color::{Alpha, Color, ColorToComponents, LinearRgba}; @@ -6,6 +5,10 @@ use bevy_image::Image; use bevy_math::{Affine2, Mat3, Vec4}; use bevy_reflect::prelude::*; use bevy_render::{render_asset::RenderAssets, render_resource::*, texture::GpuImage}; +use bevy_render_2d::{ + material::plugin::Material2dPlugin, + prelude::{AlphaMode2d, Material2d}, +}; pub const COLOR_MATERIAL_SHADER_HANDLE: Handle = weak_handle!("92e0e6e9-ed0b-4db3-89ab-5f65d3678250"); diff --git a/crates/bevy_sprite/src/mesh2d/color_material.wgsl b/crates/bevy_sprite/src/mesh2d/color_material.wgsl index a2dbe4e055937..d6f97d219982d 100644 --- a/crates/bevy_sprite/src/mesh2d/color_material.wgsl +++ b/crates/bevy_sprite/src/mesh2d/color_material.wgsl @@ -1,4 +1,4 @@ -#import bevy_sprite::{ +#import bevy_render_2d::{ mesh2d_vertex_output::VertexOutput, mesh2d_view_bindings::view, } diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs deleted file mode 100644 index 5df02cfc7b33e..0000000000000 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ /dev/null @@ -1,1002 +0,0 @@ -use crate::{ - DrawMesh2d, Mesh2d, Mesh2dPipeline, Mesh2dPipelineKey, RenderMesh2dInstances, - SetMesh2dBindGroup, SetMesh2dViewBindGroup, ViewKeyCache, ViewSpecializationTicks, -}; -use bevy_app::{App, Plugin, PostUpdate}; -use bevy_asset::prelude::AssetChanged; -use bevy_asset::{AsAssetId, Asset, AssetApp, AssetEvents, AssetId, AssetServer, Handle}; -use bevy_core_pipeline::{ - core_2d::{ - AlphaMask2d, AlphaMask2dBinKey, BatchSetKey2d, Opaque2d, Opaque2dBinKey, Transparent2d, - }, - tonemapping::Tonemapping, -}; -use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::component::Tick; -use bevy_ecs::system::SystemChangeTick; -use bevy_ecs::{ - prelude::*, - system::{lifetimeless::SRes, SystemParamItem}, -}; -use bevy_math::FloatOrd; -use bevy_platform_support::collections::HashMap; -use bevy_reflect::{prelude::ReflectDefault, Reflect}; -use bevy_render::camera::extract_cameras; -use bevy_render::render_phase::{DrawFunctionId, InputUniformIndex}; -use bevy_render::render_resource::CachedRenderPipelineId; -use bevy_render::view::RenderVisibleEntities; -use bevy_render::{ - mesh::{MeshVertexBufferLayoutRef, RenderMesh}, - render_asset::{ - prepare_assets, PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets, - }, - render_phase::{ - AddRenderCommand, BinnedRenderPhaseType, DrawFunctions, PhaseItem, PhaseItemExtraIndex, - RenderCommand, RenderCommandResult, SetItemPipeline, TrackedRenderPass, - ViewBinnedRenderPhases, ViewSortedRenderPhases, - }, - render_resource::{ - AsBindGroup, AsBindGroupError, BindGroup, BindGroupId, BindGroupLayout, BindingResources, - PipelineCache, RenderPipelineDescriptor, Shader, ShaderRef, SpecializedMeshPipeline, - SpecializedMeshPipelineError, SpecializedMeshPipelines, - }, - renderer::RenderDevice, - sync_world::{MainEntity, MainEntityHashMap}, - view::{ExtractedView, ViewVisibility}, - Extract, ExtractSchedule, Render, RenderApp, RenderSet, -}; -use bevy_utils::Parallel; -use core::{hash::Hash, marker::PhantomData}; -use derive_more::derive::From; -use tracing::error; - -/// Materials are used alongside [`Material2dPlugin`], [`Mesh2d`], and [`MeshMaterial2d`] -/// to spawn entities that are rendered with a specific [`Material2d`] type. They serve as an easy to use high level -/// way to render [`Mesh2d`] entities with custom shader logic. -/// -/// Materials must implement [`AsBindGroup`] to define how data will be transferred to the GPU and bound in shaders. -/// [`AsBindGroup`] can be derived, which makes generating bindings straightforward. See the [`AsBindGroup`] docs for details. -/// -/// # Example -/// -/// Here is a simple [`Material2d`] implementation. The [`AsBindGroup`] derive has many features. To see what else is available, -/// check out the [`AsBindGroup`] documentation. -/// -/// ``` -/// # use bevy_sprite::{Material2d, MeshMaterial2d}; -/// # use bevy_ecs::prelude::*; -/// # use bevy_image::Image; -/// # use bevy_reflect::TypePath; -/// # use bevy_render::{mesh::{Mesh, Mesh2d}, render_resource::{AsBindGroup, ShaderRef}}; -/// # use bevy_color::LinearRgba; -/// # use bevy_color::palettes::basic::RED; -/// # use bevy_asset::{Handle, AssetServer, Assets, Asset}; -/// # use bevy_math::primitives::Circle; -/// # -/// #[derive(AsBindGroup, Debug, Clone, Asset, TypePath)] -/// pub struct CustomMaterial { -/// // Uniform bindings must implement `ShaderType`, which will be used to convert the value to -/// // its shader-compatible equivalent. Most core math types already implement `ShaderType`. -/// #[uniform(0)] -/// color: LinearRgba, -/// // Images can be bound as textures in shaders. If the Image's sampler is also needed, just -/// // add the sampler attribute with a different binding index. -/// #[texture(1)] -/// #[sampler(2)] -/// color_texture: Handle, -/// } -/// -/// // All functions on `Material2d` have default impls. You only need to implement the -/// // functions that are relevant for your material. -/// impl Material2d for CustomMaterial { -/// fn fragment_shader() -> ShaderRef { -/// "shaders/custom_material.wgsl".into() -/// } -/// } -/// -/// // Spawn an entity with a mesh using `CustomMaterial`. -/// fn setup( -/// mut commands: Commands, -/// mut meshes: ResMut>, -/// mut materials: ResMut>, -/// asset_server: Res, -/// ) { -/// commands.spawn(( -/// Mesh2d(meshes.add(Circle::new(50.0))), -/// MeshMaterial2d(materials.add(CustomMaterial { -/// color: RED.into(), -/// color_texture: asset_server.load("some_image.png"), -/// })), -/// )); -/// } -/// ``` -/// -/// In WGSL shaders, the material's binding would look like this: -/// -/// ```wgsl -/// struct CustomMaterial { -/// color: vec4, -/// } -/// -/// @group(2) @binding(0) var material: CustomMaterial; -/// @group(2) @binding(1) var color_texture: texture_2d; -/// @group(2) @binding(2) var color_sampler: sampler; -/// ``` -pub trait Material2d: AsBindGroup + Asset + Clone + Sized { - /// Returns this material's vertex shader. If [`ShaderRef::Default`] is returned, the default mesh vertex shader - /// will be used. - fn vertex_shader() -> ShaderRef { - ShaderRef::Default - } - - /// Returns this material's fragment shader. If [`ShaderRef::Default`] is returned, the default mesh fragment shader - /// will be used. - fn fragment_shader() -> ShaderRef { - ShaderRef::Default - } - - /// Add a bias to the view depth of the mesh which can be used to force a specific render order. - #[inline] - fn depth_bias(&self) -> f32 { - 0.0 - } - - fn alpha_mode(&self) -> AlphaMode2d { - AlphaMode2d::Opaque - } - - /// Customizes the default [`RenderPipelineDescriptor`]. - #[expect( - unused_variables, - reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion." - )] - #[inline] - fn specialize( - descriptor: &mut RenderPipelineDescriptor, - layout: &MeshVertexBufferLayoutRef, - key: Material2dKey, - ) -> Result<(), SpecializedMeshPipelineError> { - Ok(()) - } -} - -/// A [material](Material2d) used for rendering a [`Mesh2d`]. -/// -/// See [`Material2d`] for general information about 2D materials and how to implement your own materials. -/// -/// # Example -/// -/// ``` -/// # use bevy_sprite::{ColorMaterial, MeshMaterial2d}; -/// # use bevy_ecs::prelude::*; -/// # use bevy_render::mesh::{Mesh, Mesh2d}; -/// # use bevy_color::palettes::basic::RED; -/// # use bevy_asset::Assets; -/// # use bevy_math::primitives::Circle; -/// # -/// // Spawn an entity with a mesh using `ColorMaterial`. -/// fn setup( -/// mut commands: Commands, -/// mut meshes: ResMut>, -/// mut materials: ResMut>, -/// ) { -/// commands.spawn(( -/// Mesh2d(meshes.add(Circle::new(50.0))), -/// MeshMaterial2d(materials.add(ColorMaterial::from_color(RED))), -/// )); -/// } -/// ``` -/// -/// [`MeshMaterial2d`]: crate::MeshMaterial2d -#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, From)] -#[reflect(Component, Default, Clone)] -pub struct MeshMaterial2d(pub Handle); - -impl Default for MeshMaterial2d { - fn default() -> Self { - Self(Handle::default()) - } -} - -impl PartialEq for MeshMaterial2d { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - -impl Eq for MeshMaterial2d {} - -impl From> for AssetId { - fn from(material: MeshMaterial2d) -> Self { - material.id() - } -} - -impl From<&MeshMaterial2d> for AssetId { - fn from(material: &MeshMaterial2d) -> Self { - material.id() - } -} - -impl AsAssetId for MeshMaterial2d { - type Asset = M; - - fn as_asset_id(&self) -> AssetId { - self.id() - } -} - -/// Sets how a 2d material's base color alpha channel is used for transparency. -/// Currently, this only works with [`Mesh2d`]. Sprites are always transparent. -/// -/// This is very similar to [`AlphaMode`](bevy_render::alpha::AlphaMode) but this only applies to 2d meshes. -/// We use a separate type because 2d doesn't support all the transparency modes that 3d does. -#[derive(Debug, Default, Reflect, Copy, Clone, PartialEq)] -#[reflect(Default, Debug, Clone)] -pub enum AlphaMode2d { - /// Base color alpha values are overridden to be fully opaque (1.0). - #[default] - Opaque, - /// Reduce transparency to fully opaque or fully transparent - /// based on a threshold. - /// - /// Compares the base color alpha value to the specified threshold. - /// If the value is below the threshold, - /// considers the color to be fully transparent (alpha is set to 0.0). - /// If it is equal to or above the threshold, - /// considers the color to be fully opaque (alpha is set to 1.0). - Mask(f32), - /// The base color alpha value defines the opacity of the color. - /// Standard alpha-blending is used to blend the fragment's color - /// with the color behind it. - Blend, -} - -/// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`Material2d`] -/// asset type (which includes [`Material2d`] types). -pub struct Material2dPlugin(PhantomData); - -impl Default for Material2dPlugin { - fn default() -> Self { - Self(Default::default()) - } -} - -impl Plugin for Material2dPlugin -where - M::Data: PartialEq + Eq + Hash + Clone, -{ - fn build(&self, app: &mut App) { - app.init_asset::() - .init_resource::>() - .register_type::>() - .add_plugins(RenderAssetPlugin::>::default()) - .add_systems( - PostUpdate, - check_entities_needing_specialization::.after(AssetEvents), - ); - - if let Some(render_app) = app.get_sub_app_mut(RenderApp) { - render_app - .init_resource::>() - .init_resource::>() - .add_render_command::>() - .add_render_command::>() - .add_render_command::>() - .init_resource::>() - .init_resource::>>() - .add_systems( - ExtractSchedule, - ( - extract_entities_needs_specialization::.after(extract_cameras), - extract_mesh_materials_2d::, - ), - ) - .add_systems( - Render, - ( - specialize_material2d_meshes:: - .in_set(RenderSet::PrepareMeshes) - .after(prepare_assets::>) - .after(prepare_assets::), - queue_material2d_meshes:: - .in_set(RenderSet::QueueMeshes) - .after(prepare_assets::>), - ), - ); - } - } - - fn finish(&self, app: &mut App) { - if let Some(render_app) = app.get_sub_app_mut(RenderApp) { - render_app.init_resource::>(); - } - } -} - -#[derive(Resource, Deref, DerefMut)] -pub struct RenderMaterial2dInstances(MainEntityHashMap>); - -impl Default for RenderMaterial2dInstances { - fn default() -> Self { - Self(Default::default()) - } -} - -pub fn extract_mesh_materials_2d( - mut material_instances: ResMut>, - changed_meshes_query: Extract< - Query< - (Entity, &ViewVisibility, &MeshMaterial2d), - Or<(Changed, Changed>)>, - >, - >, - mut removed_visibilities_query: Extract>, - mut removed_materials_query: Extract>>, -) { - for (entity, view_visibility, material) in &changed_meshes_query { - if view_visibility.get() { - add_mesh_instance(entity, material, &mut material_instances); - } else { - remove_mesh_instance(entity, &mut material_instances); - } - } - - for entity in removed_visibilities_query - .read() - .chain(removed_materials_query.read()) - { - // Only queue a mesh for removal if we didn't pick it up above. - // It's possible that a necessary component was removed and re-added in - // the same frame. - if !changed_meshes_query.contains(entity) { - remove_mesh_instance(entity, &mut material_instances); - } - } - - // Adds or updates a mesh instance in the [`RenderMaterial2dInstances`] - // array. - fn add_mesh_instance( - entity: Entity, - material: &MeshMaterial2d, - material_instances: &mut RenderMaterial2dInstances, - ) where - M: Material2d, - { - material_instances.insert(entity.into(), material.id()); - } - - // Removes a mesh instance from the [`RenderMaterial2dInstances`] array. - fn remove_mesh_instance( - entity: Entity, - material_instances: &mut RenderMaterial2dInstances, - ) where - M: Material2d, - { - material_instances.remove(&MainEntity::from(entity)); - } -} - -/// Render pipeline data for a given [`Material2d`] -#[derive(Resource)] -pub struct Material2dPipeline { - pub mesh2d_pipeline: Mesh2dPipeline, - pub material2d_layout: BindGroupLayout, - pub vertex_shader: Option>, - pub fragment_shader: Option>, - marker: PhantomData, -} - -pub struct Material2dKey { - pub mesh_key: Mesh2dPipelineKey, - pub bind_group_data: M::Data, -} - -impl Eq for Material2dKey where M::Data: PartialEq {} - -impl PartialEq for Material2dKey -where - M::Data: PartialEq, -{ - fn eq(&self, other: &Self) -> bool { - self.mesh_key == other.mesh_key && self.bind_group_data == other.bind_group_data - } -} - -impl Clone for Material2dKey -where - M::Data: Clone, -{ - fn clone(&self) -> Self { - Self { - mesh_key: self.mesh_key, - bind_group_data: self.bind_group_data.clone(), - } - } -} - -impl Hash for Material2dKey -where - M::Data: Hash, -{ - fn hash(&self, state: &mut H) { - self.mesh_key.hash(state); - self.bind_group_data.hash(state); - } -} - -impl Clone for Material2dPipeline { - fn clone(&self) -> Self { - Self { - mesh2d_pipeline: self.mesh2d_pipeline.clone(), - material2d_layout: self.material2d_layout.clone(), - vertex_shader: self.vertex_shader.clone(), - fragment_shader: self.fragment_shader.clone(), - marker: PhantomData, - } - } -} - -impl SpecializedMeshPipeline for Material2dPipeline -where - M::Data: PartialEq + Eq + Hash + Clone, -{ - type Key = Material2dKey; - - fn specialize( - &self, - key: Self::Key, - layout: &MeshVertexBufferLayoutRef, - ) -> Result { - let mut descriptor = self.mesh2d_pipeline.specialize(key.mesh_key, layout)?; - if let Some(vertex_shader) = &self.vertex_shader { - descriptor.vertex.shader = vertex_shader.clone(); - } - - if let Some(fragment_shader) = &self.fragment_shader { - descriptor.fragment.as_mut().unwrap().shader = fragment_shader.clone(); - } - descriptor.layout = vec![ - self.mesh2d_pipeline.view_layout.clone(), - self.mesh2d_pipeline.mesh_layout.clone(), - self.material2d_layout.clone(), - ]; - - M::specialize(&mut descriptor, layout, key)?; - Ok(descriptor) - } -} - -impl FromWorld for Material2dPipeline { - fn from_world(world: &mut World) -> Self { - let asset_server = world.resource::(); - let render_device = world.resource::(); - let material2d_layout = M::bind_group_layout(render_device); - - Material2dPipeline { - mesh2d_pipeline: world.resource::().clone(), - material2d_layout, - vertex_shader: match M::vertex_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, - fragment_shader: match M::fragment_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, - marker: PhantomData, - } - } -} - -pub(super) type DrawMaterial2d = ( - SetItemPipeline, - SetMesh2dViewBindGroup<0>, - SetMesh2dBindGroup<1>, - SetMaterial2dBindGroup, - DrawMesh2d, -); - -pub struct SetMaterial2dBindGroup(PhantomData); -impl RenderCommand

- for SetMaterial2dBindGroup -{ - type Param = ( - SRes>>, - SRes>, - ); - type ViewQuery = (); - type ItemQuery = (); - - #[inline] - fn render<'w>( - item: &P, - _view: (), - _item_query: Option<()>, - (materials, material_instances): SystemParamItem<'w, '_, Self::Param>, - pass: &mut TrackedRenderPass<'w>, - ) -> RenderCommandResult { - let materials = materials.into_inner(); - let material_instances = material_instances.into_inner(); - let Some(material_instance) = material_instances.get(&item.main_entity()) else { - return RenderCommandResult::Skip; - }; - let Some(material2d) = materials.get(*material_instance) else { - return RenderCommandResult::Skip; - }; - pass.set_bind_group(I, &material2d.bind_group, &[]); - RenderCommandResult::Success - } -} - -pub const fn alpha_mode_pipeline_key(alpha_mode: AlphaMode2d) -> Mesh2dPipelineKey { - match alpha_mode { - AlphaMode2d::Blend => Mesh2dPipelineKey::BLEND_ALPHA, - AlphaMode2d::Mask(_) => Mesh2dPipelineKey::MAY_DISCARD, - _ => Mesh2dPipelineKey::NONE, - } -} - -pub const fn tonemapping_pipeline_key(tonemapping: Tonemapping) -> Mesh2dPipelineKey { - match tonemapping { - Tonemapping::None => Mesh2dPipelineKey::TONEMAP_METHOD_NONE, - Tonemapping::Reinhard => Mesh2dPipelineKey::TONEMAP_METHOD_REINHARD, - Tonemapping::ReinhardLuminance => Mesh2dPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE, - Tonemapping::AcesFitted => Mesh2dPipelineKey::TONEMAP_METHOD_ACES_FITTED, - Tonemapping::AgX => Mesh2dPipelineKey::TONEMAP_METHOD_AGX, - Tonemapping::SomewhatBoringDisplayTransform => { - Mesh2dPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM - } - Tonemapping::TonyMcMapface => Mesh2dPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE, - Tonemapping::BlenderFilmic => Mesh2dPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC, - } -} - -pub fn extract_entities_needs_specialization( - entities_needing_specialization: Extract>>, - mut entity_specialization_ticks: ResMut>, - mut removed_mesh_material_components: Extract>>, - mut specialized_material2d_pipeline_cache: ResMut>, - views: Query<&MainEntity, With>, - ticks: SystemChangeTick, -) where - M: Material2d, -{ - for entity in entities_needing_specialization.iter() { - // Update the entity's specialization tick with this run's tick - entity_specialization_ticks.insert((*entity).into(), ticks.this_run()); - } - // Clean up any despawned entities - for entity in removed_mesh_material_components.read() { - entity_specialization_ticks.remove(&MainEntity::from(entity)); - for view in views { - if let Some(cache) = specialized_material2d_pipeline_cache.get_mut(view) { - cache.remove(&MainEntity::from(entity)); - } - } - } -} - -#[derive(Clone, Resource, Deref, DerefMut, Debug)] -pub struct EntitiesNeedingSpecialization { - #[deref] - pub entities: Vec, - _marker: PhantomData, -} - -impl Default for EntitiesNeedingSpecialization { - fn default() -> Self { - Self { - entities: Default::default(), - _marker: Default::default(), - } - } -} - -#[derive(Clone, Resource, Deref, DerefMut, Debug)] -pub struct EntitySpecializationTicks { - #[deref] - pub entities: MainEntityHashMap, - _marker: PhantomData, -} - -impl Default for EntitySpecializationTicks { - fn default() -> Self { - Self { - entities: MainEntityHashMap::default(), - _marker: Default::default(), - } - } -} - -/// Stores the [`SpecializedMaterial2dViewPipelineCache`] for each view. -#[derive(Resource, Deref, DerefMut)] -pub struct SpecializedMaterial2dPipelineCache { - // view_entity -> view pipeline cache - #[deref] - map: MainEntityHashMap>, - marker: PhantomData, -} - -/// Stores the cached render pipeline ID for each entity in a single view, as -/// well as the last time it was changed. -#[derive(Deref, DerefMut)] -pub struct SpecializedMaterial2dViewPipelineCache { - // material entity -> (tick, pipeline_id) - #[deref] - map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, - marker: PhantomData, -} - -impl Default for SpecializedMaterial2dPipelineCache { - fn default() -> Self { - Self { - map: HashMap::default(), - marker: PhantomData, - } - } -} - -impl Default for SpecializedMaterial2dViewPipelineCache { - fn default() -> Self { - Self { - map: HashMap::default(), - marker: PhantomData, - } - } -} - -pub fn check_entities_needing_specialization( - needs_specialization: Query< - Entity, - ( - Or<( - Changed, - AssetChanged, - Changed>, - AssetChanged>, - )>, - With>, - ), - >, - mut par_local: Local>>, - mut entities_needing_specialization: ResMut>, -) where - M: Material2d, -{ - entities_needing_specialization.clear(); - - needs_specialization - .par_iter() - .for_each(|entity| par_local.borrow_local_mut().push(entity)); - - par_local.drain_into(&mut entities_needing_specialization); -} - -pub fn specialize_material2d_meshes( - material2d_pipeline: Res>, - mut pipelines: ResMut>>, - pipeline_cache: Res, - (render_meshes, render_materials): ( - Res>, - Res>>, - ), - mut render_mesh_instances: ResMut, - render_material_instances: Res>, - transparent_render_phases: Res>, - opaque_render_phases: Res>, - alpha_mask_render_phases: Res>, - views: Query<(&MainEntity, &ExtractedView, &RenderVisibleEntities)>, - view_key_cache: Res, - entity_specialization_ticks: Res>, - view_specialization_ticks: Res, - ticks: SystemChangeTick, - mut specialized_material_pipeline_cache: ResMut>, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ - if render_material_instances.is_empty() { - return; - } - - for (view_entity, view, visible_entities) in &views { - if !transparent_render_phases.contains_key(&view.retained_view_entity) - && !opaque_render_phases.contains_key(&view.retained_view_entity) - && !alpha_mask_render_phases.contains_key(&view.retained_view_entity) - { - continue; - } - - let Some(view_key) = view_key_cache.get(view_entity) else { - continue; - }; - - let view_tick = view_specialization_ticks.get(view_entity).unwrap(); - let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache - .entry(*view_entity) - .or_default(); - - for (_, visible_entity) in visible_entities.iter::() { - let Some(material_asset_id) = render_material_instances.get(visible_entity) else { - continue; - }; - let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap(); - let last_specialized_tick = view_specialized_material_pipeline_cache - .get(visible_entity) - .map(|(tick, _)| *tick); - let needs_specialization = last_specialized_tick.is_none_or(|tick| { - view_tick.is_newer_than(tick, ticks.this_run()) - || entity_tick.is_newer_than(tick, ticks.this_run()) - }); - if !needs_specialization { - continue; - } - let Some(mesh_instance) = render_mesh_instances.get_mut(visible_entity) else { - continue; - }; - let Some(material_2d) = render_materials.get(*material_asset_id) else { - continue; - }; - let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { - continue; - }; - let mesh_key = *view_key - | Mesh2dPipelineKey::from_primitive_topology(mesh.primitive_topology()) - | material_2d.properties.mesh_pipeline_key_bits; - - let pipeline_id = pipelines.specialize( - &pipeline_cache, - &material2d_pipeline, - Material2dKey { - mesh_key, - bind_group_data: material_2d.key.clone(), - }, - &mesh.layout, - ); - - let pipeline_id = match pipeline_id { - Ok(id) => id, - Err(err) => { - error!("{}", err); - continue; - } - }; - - view_specialized_material_pipeline_cache - .insert(*visible_entity, (ticks.this_run(), pipeline_id)); - } - } -} - -pub fn queue_material2d_meshes( - (render_meshes, render_materials): ( - Res>, - Res>>, - ), - mut render_mesh_instances: ResMut, - render_material_instances: Res>, - mut transparent_render_phases: ResMut>, - mut opaque_render_phases: ResMut>, - mut alpha_mask_render_phases: ResMut>, - views: Query<(&MainEntity, &ExtractedView, &RenderVisibleEntities)>, - specialized_material_pipeline_cache: ResMut>, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ - if render_material_instances.is_empty() { - return; - } - - for (view_entity, view, visible_entities) in &views { - let Some(view_specialized_material_pipeline_cache) = - specialized_material_pipeline_cache.get(view_entity) - else { - continue; - }; - - let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity) - else { - continue; - }; - let Some(opaque_phase) = opaque_render_phases.get_mut(&view.retained_view_entity) else { - continue; - }; - let Some(alpha_mask_phase) = alpha_mask_render_phases.get_mut(&view.retained_view_entity) - else { - continue; - }; - - for (render_entity, visible_entity) in visible_entities.iter::() { - let Some((current_change_tick, pipeline_id)) = view_specialized_material_pipeline_cache - .get(visible_entity) - .map(|(current_change_tick, pipeline_id)| (*current_change_tick, *pipeline_id)) - else { - continue; - }; - - // Skip the entity if it's cached in a bin and up to date. - if opaque_phase.validate_cached_entity(*visible_entity, current_change_tick) - || alpha_mask_phase.validate_cached_entity(*visible_entity, current_change_tick) - { - continue; - } - - let Some(material_asset_id) = render_material_instances.get(visible_entity) else { - continue; - }; - let Some(mesh_instance) = render_mesh_instances.get_mut(visible_entity) else { - continue; - }; - let Some(material_2d) = render_materials.get(*material_asset_id) else { - continue; - }; - let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { - continue; - }; - - mesh_instance.material_bind_group_id = material_2d.get_bind_group_id(); - let mesh_z = mesh_instance.transforms.world_from_local.translation.z; - - // We don't support multidraw yet for 2D meshes, so we use this - // custom logic to generate the `BinnedRenderPhaseType` instead of - // `BinnedRenderPhaseType::mesh`, which can return - // `BinnedRenderPhaseType::MultidrawableMesh` if the hardware - // supports multidraw. - let binned_render_phase_type = if mesh_instance.automatic_batching { - BinnedRenderPhaseType::BatchableMesh - } else { - BinnedRenderPhaseType::UnbatchableMesh - }; - - match material_2d.properties.alpha_mode { - AlphaMode2d::Opaque => { - let bin_key = Opaque2dBinKey { - pipeline: pipeline_id, - draw_function: material_2d.properties.draw_function_id, - asset_id: mesh_instance.mesh_asset_id.into(), - material_bind_group_id: material_2d.get_bind_group_id().0, - }; - opaque_phase.add( - BatchSetKey2d { - indexed: mesh.indexed(), - }, - bin_key, - (*render_entity, *visible_entity), - InputUniformIndex::default(), - binned_render_phase_type, - current_change_tick, - ); - } - AlphaMode2d::Mask(_) => { - let bin_key = AlphaMask2dBinKey { - pipeline: pipeline_id, - draw_function: material_2d.properties.draw_function_id, - asset_id: mesh_instance.mesh_asset_id.into(), - material_bind_group_id: material_2d.get_bind_group_id().0, - }; - alpha_mask_phase.add( - BatchSetKey2d { - indexed: mesh.indexed(), - }, - bin_key, - (*render_entity, *visible_entity), - InputUniformIndex::default(), - binned_render_phase_type, - current_change_tick, - ); - } - AlphaMode2d::Blend => { - transparent_phase.add(Transparent2d { - entity: (*render_entity, *visible_entity), - draw_function: material_2d.properties.draw_function_id, - pipeline: pipeline_id, - // NOTE: Back-to-front ordering for transparent with ascending sort means far should have the - // lowest sort key and getting closer should increase. As we have - // -z in front of the camera, the largest distance is -far with values increasing toward the - // camera. As such we can just use mesh_z as the distance - sort_key: FloatOrd(mesh_z + material_2d.properties.depth_bias), - // Batching is done in batch_and_prepare_render_phase - batch_range: 0..1, - extra_index: PhaseItemExtraIndex::None, - extracted_index: usize::MAX, - indexed: mesh.indexed(), - }); - } - } - } - } -} - -#[derive(Component, Clone, Copy, Default, PartialEq, Eq, Deref, DerefMut)] -pub struct Material2dBindGroupId(pub Option); - -/// Common [`Material2d`] properties, calculated for a specific material instance. -pub struct Material2dProperties { - /// The [`AlphaMode2d`] of this material. - pub alpha_mode: AlphaMode2d, - /// Add a bias to the view depth of the mesh which can be used to force a specific render order - /// for meshes with equal depth, to avoid z-fighting. - /// The bias is in depth-texture units so large values may - pub depth_bias: f32, - /// The bits in the [`Mesh2dPipelineKey`] for this material. - /// - /// These are precalculated so that we can just "or" them together in - /// [`queue_material2d_meshes`]. - pub mesh_pipeline_key_bits: Mesh2dPipelineKey, - pub draw_function_id: DrawFunctionId, -} - -/// Data prepared for a [`Material2d`] instance. -pub struct PreparedMaterial2d { - pub bindings: BindingResources, - pub bind_group: BindGroup, - pub key: T::Data, - pub properties: Material2dProperties, -} - -impl PreparedMaterial2d { - pub fn get_bind_group_id(&self) -> Material2dBindGroupId { - Material2dBindGroupId(Some(self.bind_group.id())) - } -} - -impl RenderAsset for PreparedMaterial2d { - type SourceAsset = M; - - type Param = ( - SRes, - SRes>, - SRes>, - SRes>, - SRes>, - M::Param, - ); - - fn prepare_asset( - material: Self::SourceAsset, - _: AssetId, - ( - render_device, - pipeline, - opaque_draw_functions, - alpha_mask_draw_functions, - transparent_draw_functions, - material_param, - ): &mut SystemParamItem, - ) -> Result> { - match material.as_bind_group(&pipeline.material2d_layout, render_device, material_param) { - Ok(prepared) => { - let mut mesh_pipeline_key_bits = Mesh2dPipelineKey::empty(); - mesh_pipeline_key_bits.insert(alpha_mode_pipeline_key(material.alpha_mode())); - - let draw_function_id = match material.alpha_mode() { - AlphaMode2d::Opaque => opaque_draw_functions.read().id::>(), - AlphaMode2d::Mask(_) => { - alpha_mask_draw_functions.read().id::>() - } - AlphaMode2d::Blend => { - transparent_draw_functions.read().id::>() - } - }; - - Ok(PreparedMaterial2d { - bindings: prepared.bindings, - bind_group: prepared.bind_group, - key: prepared.data, - properties: Material2dProperties { - depth_bias: material.depth_bias(), - alpha_mode: material.alpha_mode(), - mesh_pipeline_key_bits, - draw_function_id, - }, - }) - } - Err(AsBindGroupError::RetryNextUpdate) => { - Err(PrepareAssetError::RetryNextUpdate(material)) - } - Err(other) => Err(PrepareAssetError::AsBindGroupError(other)), - } - } -} diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs deleted file mode 100644 index 5822d47ed6231..0000000000000 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ /dev/null @@ -1,921 +0,0 @@ -use bevy_app::Plugin; -use bevy_asset::{load_internal_asset, weak_handle, AssetId, Handle}; - -use crate::{tonemapping_pipeline_key, Material2dBindGroupId}; -use bevy_core_pipeline::tonemapping::DebandDither; -use bevy_core_pipeline::{ - core_2d::{AlphaMask2d, Camera2d, Opaque2d, Transparent2d, CORE_2D_DEPTH_FORMAT}, - tonemapping::{ - get_lut_bind_group_layout_entries, get_lut_bindings, Tonemapping, TonemappingLuts, - }, -}; -use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::component::Tick; -use bevy_ecs::system::SystemChangeTick; -use bevy_ecs::{ - prelude::*, - query::ROQueryItem, - system::{lifetimeless::*, SystemParamItem, SystemState}, -}; -use bevy_image::{BevyDefault, Image, ImageSampler, TextureFormatPixelInfo}; -use bevy_math::{Affine3, Vec4}; -use bevy_render::mesh::MeshTag; -use bevy_render::prelude::Msaa; -use bevy_render::RenderSet::PrepareAssets; -use bevy_render::{ - batching::{ - gpu_preprocessing::IndirectParametersCpuMetadata, - no_gpu_preprocessing::{ - self, batch_and_prepare_binned_render_phase, batch_and_prepare_sorted_render_phase, - write_batched_instance_buffer, BatchedInstanceBuffer, - }, - GetBatchData, GetFullBatchData, NoAutomaticBatching, - }, - globals::{GlobalsBuffer, GlobalsUniform}, - mesh::{ - allocator::MeshAllocator, Mesh, Mesh2d, MeshVertexBufferLayoutRef, RenderMesh, - RenderMeshBufferInfo, - }, - render_asset::RenderAssets, - render_phase::{ - sweep_old_entities, PhaseItem, PhaseItemExtraIndex, RenderCommand, RenderCommandResult, - TrackedRenderPass, - }, - render_resource::{binding_types::uniform_buffer, *}, - renderer::{RenderDevice, RenderQueue}, - sync_world::{MainEntity, MainEntityHashMap}, - texture::{DefaultImageSampler, FallbackImage, GpuImage}, - view::{ - ExtractedView, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms, ViewVisibility, - }, - Extract, ExtractSchedule, Render, RenderApp, RenderSet, -}; -use bevy_transform::components::GlobalTransform; -use nonmax::NonMaxU32; -use tracing::error; - -#[derive(Default)] -pub struct Mesh2dRenderPlugin; - -pub const MESH2D_VERTEX_OUTPUT: Handle = - weak_handle!("71e279c7-85a0-46ac-9a76-1586cbf506d0"); -pub const MESH2D_VIEW_TYPES_HANDLE: Handle = - weak_handle!("01087b0d-91e9-46ac-8628-dfe19a7d4b83"); -pub const MESH2D_VIEW_BINDINGS_HANDLE: Handle = - weak_handle!("fbdd8b80-503d-4688-bcec-db29ab4620b2"); -pub const MESH2D_TYPES_HANDLE: Handle = - weak_handle!("199f2089-6e99-4348-9bb1-d82816640a7f"); -pub const MESH2D_BINDINGS_HANDLE: Handle = - weak_handle!("a7bd44cc-0580-4427-9a00-721cf386b6e4"); -pub const MESH2D_FUNCTIONS_HANDLE: Handle = - weak_handle!("0d08ff71-68c1-4017-83e2-bfc34d285c51"); -pub const MESH2D_SHADER_HANDLE: Handle = - weak_handle!("91a7602b-df95-4ea3-9d97-076abcb69d91"); - -impl Plugin for Mesh2dRenderPlugin { - fn build(&self, app: &mut bevy_app::App) { - load_internal_asset!( - app, - MESH2D_VERTEX_OUTPUT, - "mesh2d_vertex_output.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESH2D_VIEW_TYPES_HANDLE, - "mesh2d_view_types.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESH2D_VIEW_BINDINGS_HANDLE, - "mesh2d_view_bindings.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESH2D_TYPES_HANDLE, - "mesh2d_types.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESH2D_FUNCTIONS_HANDLE, - "mesh2d_functions.wgsl", - Shader::from_wgsl - ); - load_internal_asset!(app, MESH2D_SHADER_HANDLE, "mesh2d.wgsl", Shader::from_wgsl); - - if let Some(render_app) = app.get_sub_app_mut(RenderApp) { - render_app - .init_resource::() - .init_resource::() - .init_resource::>() - .add_systems(ExtractSchedule, extract_mesh2d) - .add_systems( - Render, - ( - ( - sweep_old_entities::, - sweep_old_entities::, - ) - .in_set(RenderSet::QueueSweep), - batch_and_prepare_binned_render_phase:: - .in_set(RenderSet::PrepareResources), - batch_and_prepare_binned_render_phase:: - .in_set(RenderSet::PrepareResources), - batch_and_prepare_sorted_render_phase:: - .in_set(RenderSet::PrepareResources), - write_batched_instance_buffer:: - .in_set(RenderSet::PrepareResourcesFlush), - prepare_mesh2d_bind_group.in_set(RenderSet::PrepareBindGroups), - prepare_mesh2d_view_bind_groups.in_set(RenderSet::PrepareBindGroups), - no_gpu_preprocessing::clear_batched_cpu_instance_buffers:: - .in_set(RenderSet::Cleanup) - .after(RenderSet::Render), - ), - ); - } - } - - fn finish(&self, app: &mut bevy_app::App) { - let mut mesh_bindings_shader_defs = Vec::with_capacity(1); - - if let Some(render_app) = app.get_sub_app_mut(RenderApp) { - let render_device = render_app.world().resource::(); - let batched_instance_buffer = - BatchedInstanceBuffer::::new(render_device); - - if let Some(per_object_buffer_batch_size) = - GpuArrayBuffer::::batch_size(render_device) - { - mesh_bindings_shader_defs.push(ShaderDefVal::UInt( - "PER_OBJECT_BUFFER_BATCH_SIZE".into(), - per_object_buffer_batch_size, - )); - } - - render_app - .insert_resource(batched_instance_buffer) - .init_resource::() - .init_resource::() - .init_resource::() - .add_systems( - Render, - check_views_need_specialization.in_set(PrepareAssets), - ); - } - - // Load the mesh_bindings shader module here as it depends on runtime information about - // whether storage buffers are supported, or the maximum uniform buffer binding size. - load_internal_asset!( - app, - MESH2D_BINDINGS_HANDLE, - "mesh2d_bindings.wgsl", - Shader::from_wgsl_with_defs, - mesh_bindings_shader_defs - ); - } -} - -#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] -pub struct ViewKeyCache(MainEntityHashMap); - -#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] -pub struct ViewSpecializationTicks(MainEntityHashMap); - -pub fn check_views_need_specialization( - mut view_key_cache: ResMut, - mut view_specialization_ticks: ResMut, - views: Query<( - &MainEntity, - &ExtractedView, - &Msaa, - Option<&Tonemapping>, - Option<&DebandDither>, - )>, - ticks: SystemChangeTick, -) { - for (view_entity, view, msaa, tonemapping, dither) in &views { - let mut view_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()) - | Mesh2dPipelineKey::from_hdr(view.hdr); - - if !view.hdr { - if let Some(tonemapping) = tonemapping { - view_key |= Mesh2dPipelineKey::TONEMAP_IN_SHADER; - view_key |= tonemapping_pipeline_key(*tonemapping); - } - if let Some(DebandDither::Enabled) = dither { - view_key |= Mesh2dPipelineKey::DEBAND_DITHER; - } - } - - if !view_key_cache - .get_mut(view_entity) - .is_some_and(|current_key| *current_key == view_key) - { - view_key_cache.insert(*view_entity, view_key); - view_specialization_ticks.insert(*view_entity, ticks.this_run()); - } - } -} - -#[derive(Component)] -pub struct Mesh2dTransforms { - pub world_from_local: Affine3, - pub flags: u32, -} - -#[derive(ShaderType, Clone, Copy)] -pub struct Mesh2dUniform { - // Affine 4x3 matrix transposed to 3x4 - pub world_from_local: [Vec4; 3], - // 3x3 matrix packed in mat2x4 and f32 as: - // [0].xyz, [1].x, - // [1].yz, [2].xy - // [2].z - pub local_from_world_transpose_a: [Vec4; 2], - pub local_from_world_transpose_b: f32, - pub flags: u32, - pub tag: u32, -} - -impl Mesh2dUniform { - fn from_components(mesh_transforms: &Mesh2dTransforms, tag: u32) -> Self { - let (local_from_world_transpose_a, local_from_world_transpose_b) = - mesh_transforms.world_from_local.inverse_transpose_3x3(); - Self { - world_from_local: mesh_transforms.world_from_local.to_transpose(), - local_from_world_transpose_a, - local_from_world_transpose_b, - flags: mesh_transforms.flags, - tag, - } - } -} - -// NOTE: These must match the bit flags in bevy_sprite/src/mesh2d/mesh2d.wgsl! -bitflags::bitflags! { - #[repr(transparent)] - pub struct MeshFlags: u32 { - const NONE = 0; - const UNINITIALIZED = 0xFFFF; - } -} - -pub struct RenderMesh2dInstance { - pub transforms: Mesh2dTransforms, - pub mesh_asset_id: AssetId, - pub material_bind_group_id: Material2dBindGroupId, - pub automatic_batching: bool, - pub tag: u32, -} - -#[derive(Default, Resource, Deref, DerefMut)] -pub struct RenderMesh2dInstances(MainEntityHashMap); - -#[derive(Component, Default)] -pub struct Mesh2dMarker; - -pub fn extract_mesh2d( - mut render_mesh_instances: ResMut, - query: Extract< - Query<( - Entity, - &ViewVisibility, - &GlobalTransform, - &Mesh2d, - Option<&MeshTag>, - Has, - )>, - >, -) { - render_mesh_instances.clear(); - - for (entity, view_visibility, transform, handle, tag, no_automatic_batching) in &query { - if !view_visibility.get() { - continue; - } - render_mesh_instances.insert( - entity.into(), - RenderMesh2dInstance { - transforms: Mesh2dTransforms { - world_from_local: (&transform.affine()).into(), - flags: MeshFlags::empty().bits(), - }, - mesh_asset_id: handle.0.id(), - material_bind_group_id: Material2dBindGroupId::default(), - automatic_batching: !no_automatic_batching, - tag: tag.map_or(0, |i| **i), - }, - ); - } -} - -#[derive(Resource, Clone)] -pub struct Mesh2dPipeline { - pub view_layout: BindGroupLayout, - pub mesh_layout: BindGroupLayout, - // This dummy white texture is to be used in place of optional textures - pub dummy_white_gpu_image: GpuImage, - pub per_object_buffer_batch_size: Option, -} - -impl FromWorld for Mesh2dPipeline { - fn from_world(world: &mut World) -> Self { - let mut system_state: SystemState<( - Res, - Res, - Res, - )> = SystemState::new(world); - let (render_device, render_queue, default_sampler) = system_state.get_mut(world); - let render_device = render_device.into_inner(); - let tonemapping_lut_entries = get_lut_bind_group_layout_entries(); - let view_layout = render_device.create_bind_group_layout( - "mesh2d_view_layout", - &BindGroupLayoutEntries::with_indices( - ShaderStages::VERTEX_FRAGMENT, - ( - (0, uniform_buffer::(true)), - (1, uniform_buffer::(false)), - ( - 2, - tonemapping_lut_entries[0].visibility(ShaderStages::FRAGMENT), - ), - ( - 3, - tonemapping_lut_entries[1].visibility(ShaderStages::FRAGMENT), - ), - ), - ), - ); - - let mesh_layout = render_device.create_bind_group_layout( - "mesh2d_layout", - &BindGroupLayoutEntries::single( - ShaderStages::VERTEX_FRAGMENT, - GpuArrayBuffer::::binding_layout(render_device), - ), - ); - // A 1x1x1 'all 1.0' texture to use as a dummy texture to use in place of optional StandardMaterial textures - let dummy_white_gpu_image = { - let image = Image::default(); - let texture = render_device.create_texture(&image.texture_descriptor); - let sampler = match image.sampler { - ImageSampler::Default => (**default_sampler).clone(), - ImageSampler::Descriptor(ref descriptor) => { - render_device.create_sampler(&descriptor.as_wgpu()) - } - }; - - let format_size = image.texture_descriptor.format.pixel_size(); - render_queue.write_texture( - texture.as_image_copy(), - image.data.as_ref().expect("Image has no data"), - TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(image.width() * format_size as u32), - rows_per_image: None, - }, - image.texture_descriptor.size, - ); - - let texture_view = texture.create_view(&TextureViewDescriptor::default()); - GpuImage { - texture, - texture_view, - texture_format: image.texture_descriptor.format, - sampler, - size: image.texture_descriptor.size, - mip_level_count: image.texture_descriptor.mip_level_count, - } - }; - Mesh2dPipeline { - view_layout, - mesh_layout, - dummy_white_gpu_image, - per_object_buffer_batch_size: GpuArrayBuffer::::batch_size( - render_device, - ), - } - } -} - -impl Mesh2dPipeline { - pub fn get_image_texture<'a>( - &'a self, - gpu_images: &'a RenderAssets, - handle_option: &Option>, - ) -> Option<(&'a TextureView, &'a Sampler)> { - if let Some(handle) = handle_option { - let gpu_image = gpu_images.get(handle)?; - Some((&gpu_image.texture_view, &gpu_image.sampler)) - } else { - Some(( - &self.dummy_white_gpu_image.texture_view, - &self.dummy_white_gpu_image.sampler, - )) - } - } -} - -impl GetBatchData for Mesh2dPipeline { - type Param = ( - SRes, - SRes>, - SRes, - ); - type CompareData = (Material2dBindGroupId, AssetId); - type BufferData = Mesh2dUniform; - - fn get_batch_data( - (mesh_instances, _, _): &SystemParamItem, - (_entity, main_entity): (Entity, MainEntity), - ) -> Option<(Self::BufferData, Option)> { - let mesh_instance = mesh_instances.get(&main_entity)?; - Some(( - Mesh2dUniform::from_components(&mesh_instance.transforms, mesh_instance.tag), - mesh_instance.automatic_batching.then_some(( - mesh_instance.material_bind_group_id, - mesh_instance.mesh_asset_id, - )), - )) - } -} - -impl GetFullBatchData for Mesh2dPipeline { - type BufferInputData = (); - - fn get_binned_batch_data( - (mesh_instances, _, _): &SystemParamItem, - main_entity: MainEntity, - ) -> Option { - let mesh_instance = mesh_instances.get(&main_entity)?; - Some(Mesh2dUniform::from_components( - &mesh_instance.transforms, - mesh_instance.tag, - )) - } - - fn get_index_and_compare_data( - _: &SystemParamItem, - _query_item: MainEntity, - ) -> Option<(NonMaxU32, Option)> { - error!( - "`get_index_and_compare_data` is only intended for GPU mesh uniform building, \ - but this is not yet implemented for 2d meshes" - ); - None - } - - fn get_binned_index( - _: &SystemParamItem, - _query_item: MainEntity, - ) -> Option { - error!( - "`get_binned_index` is only intended for GPU mesh uniform building, \ - but this is not yet implemented for 2d meshes" - ); - None - } - - fn write_batch_indirect_parameters_metadata( - indexed: bool, - base_output_index: u32, - batch_set_index: Option, - indirect_parameters_buffer: &mut bevy_render::batching::gpu_preprocessing::UntypedPhaseIndirectParametersBuffers, - indirect_parameters_offset: u32, - ) { - // Note that `IndirectParameters` covers both of these structures, even - // though they actually have distinct layouts. See the comment above that - // type for more information. - let indirect_parameters = IndirectParametersCpuMetadata { - base_output_index, - batch_set_index: match batch_set_index { - None => !0, - Some(batch_set_index) => u32::from(batch_set_index), - }, - }; - - if indexed { - indirect_parameters_buffer - .indexed - .set(indirect_parameters_offset, indirect_parameters); - } else { - indirect_parameters_buffer - .non_indexed - .set(indirect_parameters_offset, indirect_parameters); - } - } -} - -bitflags::bitflags! { - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] - #[repr(transparent)] - // NOTE: Apparently quadro drivers support up to 64x MSAA. - // MSAA uses the highest 3 bits for the MSAA log2(sample count) to support up to 128x MSAA. - // FIXME: make normals optional? - pub struct Mesh2dPipelineKey: u32 { - const NONE = 0; - const HDR = 1 << 0; - const TONEMAP_IN_SHADER = 1 << 1; - const DEBAND_DITHER = 1 << 2; - const BLEND_ALPHA = 1 << 3; - const MAY_DISCARD = 1 << 4; - const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; - const PRIMITIVE_TOPOLOGY_RESERVED_BITS = Self::PRIMITIVE_TOPOLOGY_MASK_BITS << Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS; - const TONEMAP_METHOD_RESERVED_BITS = Self::TONEMAP_METHOD_MASK_BITS << Self::TONEMAP_METHOD_SHIFT_BITS; - const TONEMAP_METHOD_NONE = 0 << Self::TONEMAP_METHOD_SHIFT_BITS; - const TONEMAP_METHOD_REINHARD = 1 << Self::TONEMAP_METHOD_SHIFT_BITS; - const TONEMAP_METHOD_REINHARD_LUMINANCE = 2 << Self::TONEMAP_METHOD_SHIFT_BITS; - const TONEMAP_METHOD_ACES_FITTED = 3 << Self::TONEMAP_METHOD_SHIFT_BITS; - const TONEMAP_METHOD_AGX = 4 << Self::TONEMAP_METHOD_SHIFT_BITS; - const TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM = 5 << Self::TONEMAP_METHOD_SHIFT_BITS; - const TONEMAP_METHOD_TONY_MC_MAPFACE = 6 << Self::TONEMAP_METHOD_SHIFT_BITS; - const TONEMAP_METHOD_BLENDER_FILMIC = 7 << Self::TONEMAP_METHOD_SHIFT_BITS; - } -} - -impl Mesh2dPipelineKey { - const MSAA_MASK_BITS: u32 = 0b111; - const MSAA_SHIFT_BITS: u32 = 32 - Self::MSAA_MASK_BITS.count_ones(); - const PRIMITIVE_TOPOLOGY_MASK_BITS: u32 = 0b111; - const PRIMITIVE_TOPOLOGY_SHIFT_BITS: u32 = Self::MSAA_SHIFT_BITS - 3; - const TONEMAP_METHOD_MASK_BITS: u32 = 0b111; - const TONEMAP_METHOD_SHIFT_BITS: u32 = - Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS - Self::TONEMAP_METHOD_MASK_BITS.count_ones(); - - pub fn from_msaa_samples(msaa_samples: u32) -> Self { - let msaa_bits = - (msaa_samples.trailing_zeros() & Self::MSAA_MASK_BITS) << Self::MSAA_SHIFT_BITS; - Self::from_bits_retain(msaa_bits) - } - - pub fn from_hdr(hdr: bool) -> Self { - if hdr { - Mesh2dPipelineKey::HDR - } else { - Mesh2dPipelineKey::NONE - } - } - - pub fn msaa_samples(&self) -> u32 { - 1 << ((self.bits() >> Self::MSAA_SHIFT_BITS) & Self::MSAA_MASK_BITS) - } - - pub fn from_primitive_topology(primitive_topology: PrimitiveTopology) -> Self { - let primitive_topology_bits = ((primitive_topology as u32) - & Self::PRIMITIVE_TOPOLOGY_MASK_BITS) - << Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS; - Self::from_bits_retain(primitive_topology_bits) - } - - pub fn primitive_topology(&self) -> PrimitiveTopology { - let primitive_topology_bits = (self.bits() >> Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS) - & Self::PRIMITIVE_TOPOLOGY_MASK_BITS; - match primitive_topology_bits { - x if x == PrimitiveTopology::PointList as u32 => PrimitiveTopology::PointList, - x if x == PrimitiveTopology::LineList as u32 => PrimitiveTopology::LineList, - x if x == PrimitiveTopology::LineStrip as u32 => PrimitiveTopology::LineStrip, - x if x == PrimitiveTopology::TriangleList as u32 => PrimitiveTopology::TriangleList, - x if x == PrimitiveTopology::TriangleStrip as u32 => PrimitiveTopology::TriangleStrip, - _ => PrimitiveTopology::default(), - } - } -} - -impl SpecializedMeshPipeline for Mesh2dPipeline { - type Key = Mesh2dPipelineKey; - - fn specialize( - &self, - key: Self::Key, - layout: &MeshVertexBufferLayoutRef, - ) -> Result { - let mut shader_defs = Vec::new(); - let mut vertex_attributes = Vec::new(); - - if layout.0.contains(Mesh::ATTRIBUTE_POSITION) { - shader_defs.push("VERTEX_POSITIONS".into()); - vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0)); - } - - if layout.0.contains(Mesh::ATTRIBUTE_NORMAL) { - shader_defs.push("VERTEX_NORMALS".into()); - vertex_attributes.push(Mesh::ATTRIBUTE_NORMAL.at_shader_location(1)); - } - - if layout.0.contains(Mesh::ATTRIBUTE_UV_0) { - shader_defs.push("VERTEX_UVS".into()); - vertex_attributes.push(Mesh::ATTRIBUTE_UV_0.at_shader_location(2)); - } - - if layout.0.contains(Mesh::ATTRIBUTE_TANGENT) { - shader_defs.push("VERTEX_TANGENTS".into()); - vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(3)); - } - - if layout.0.contains(Mesh::ATTRIBUTE_COLOR) { - shader_defs.push("VERTEX_COLORS".into()); - vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(4)); - } - - if key.contains(Mesh2dPipelineKey::TONEMAP_IN_SHADER) { - shader_defs.push("TONEMAP_IN_SHADER".into()); - shader_defs.push(ShaderDefVal::UInt( - "TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(), - 2, - )); - shader_defs.push(ShaderDefVal::UInt( - "TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(), - 3, - )); - - let method = key.intersection(Mesh2dPipelineKey::TONEMAP_METHOD_RESERVED_BITS); - - match method { - Mesh2dPipelineKey::TONEMAP_METHOD_NONE => { - shader_defs.push("TONEMAP_METHOD_NONE".into()); - } - Mesh2dPipelineKey::TONEMAP_METHOD_REINHARD => { - shader_defs.push("TONEMAP_METHOD_REINHARD".into()); - } - Mesh2dPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE => { - shader_defs.push("TONEMAP_METHOD_REINHARD_LUMINANCE".into()); - } - Mesh2dPipelineKey::TONEMAP_METHOD_ACES_FITTED => { - shader_defs.push("TONEMAP_METHOD_ACES_FITTED".into()); - } - Mesh2dPipelineKey::TONEMAP_METHOD_AGX => { - shader_defs.push("TONEMAP_METHOD_AGX".into()); - } - Mesh2dPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM => { - shader_defs.push("TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM".into()); - } - Mesh2dPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC => { - shader_defs.push("TONEMAP_METHOD_BLENDER_FILMIC".into()); - } - Mesh2dPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE => { - shader_defs.push("TONEMAP_METHOD_TONY_MC_MAPFACE".into()); - } - _ => {} - } - // Debanding is tied to tonemapping in the shader, cannot run without it. - if key.contains(Mesh2dPipelineKey::DEBAND_DITHER) { - shader_defs.push("DEBAND_DITHER".into()); - } - } - - if key.contains(Mesh2dPipelineKey::MAY_DISCARD) { - shader_defs.push("MAY_DISCARD".into()); - } - - let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?; - - let format = match key.contains(Mesh2dPipelineKey::HDR) { - true => ViewTarget::TEXTURE_FORMAT_HDR, - false => TextureFormat::bevy_default(), - }; - - let (depth_write_enabled, label, blend); - if key.contains(Mesh2dPipelineKey::BLEND_ALPHA) { - label = "transparent_mesh2d_pipeline"; - blend = Some(BlendState::ALPHA_BLENDING); - depth_write_enabled = false; - } else { - label = "opaque_mesh2d_pipeline"; - blend = None; - depth_write_enabled = true; - } - - Ok(RenderPipelineDescriptor { - vertex: VertexState { - shader: MESH2D_SHADER_HANDLE, - entry_point: "vertex".into(), - shader_defs: shader_defs.clone(), - buffers: vec![vertex_buffer_layout], - }, - fragment: Some(FragmentState { - shader: MESH2D_SHADER_HANDLE, - shader_defs, - entry_point: "fragment".into(), - targets: vec![Some(ColorTargetState { - format, - blend, - write_mask: ColorWrites::ALL, - })], - }), - layout: vec![self.view_layout.clone(), self.mesh_layout.clone()], - push_constant_ranges: vec![], - primitive: PrimitiveState { - front_face: FrontFace::Ccw, - cull_mode: None, - unclipped_depth: false, - polygon_mode: PolygonMode::Fill, - conservative: false, - topology: key.primitive_topology(), - strip_index_format: None, - }, - depth_stencil: Some(DepthStencilState { - format: CORE_2D_DEPTH_FORMAT, - depth_write_enabled, - depth_compare: CompareFunction::GreaterEqual, - stencil: StencilState { - front: StencilFaceState::IGNORE, - back: StencilFaceState::IGNORE, - read_mask: 0, - write_mask: 0, - }, - bias: DepthBiasState { - constant: 0, - slope_scale: 0.0, - clamp: 0.0, - }, - }), - multisample: MultisampleState { - count: key.msaa_samples(), - mask: !0, - alpha_to_coverage_enabled: false, - }, - label: Some(label.into()), - zero_initialize_workgroup_memory: false, - }) - } -} - -#[derive(Resource)] -pub struct Mesh2dBindGroup { - pub value: BindGroup, -} - -pub fn prepare_mesh2d_bind_group( - mut commands: Commands, - mesh2d_pipeline: Res, - render_device: Res, - mesh2d_uniforms: Res>, -) { - if let Some(binding) = mesh2d_uniforms.instance_data_binding() { - commands.insert_resource(Mesh2dBindGroup { - value: render_device.create_bind_group( - "mesh2d_bind_group", - &mesh2d_pipeline.mesh_layout, - &BindGroupEntries::single(binding), - ), - }); - } -} - -#[derive(Component)] -pub struct Mesh2dViewBindGroup { - pub value: BindGroup, -} - -pub fn prepare_mesh2d_view_bind_groups( - mut commands: Commands, - render_device: Res, - mesh2d_pipeline: Res, - view_uniforms: Res, - views: Query<(Entity, &Tonemapping), (With, With)>, - globals_buffer: Res, - tonemapping_luts: Res, - images: Res>, - fallback_image: Res, -) { - let (Some(view_binding), Some(globals)) = ( - view_uniforms.uniforms.binding(), - globals_buffer.buffer.binding(), - ) else { - return; - }; - - for (entity, tonemapping) in &views { - let lut_bindings = - get_lut_bindings(&images, &tonemapping_luts, tonemapping, &fallback_image); - let view_bind_group = render_device.create_bind_group( - "mesh2d_view_bind_group", - &mesh2d_pipeline.view_layout, - &BindGroupEntries::with_indices(( - (0, view_binding.clone()), - (1, globals.clone()), - (2, lut_bindings.0), - (3, lut_bindings.1), - )), - ); - - commands.entity(entity).insert(Mesh2dViewBindGroup { - value: view_bind_group, - }); - } -} - -pub struct SetMesh2dViewBindGroup; -impl RenderCommand

for SetMesh2dViewBindGroup { - type Param = (); - type ViewQuery = (Read, Read); - type ItemQuery = (); - - #[inline] - fn render<'w>( - _item: &P, - (view_uniform, mesh2d_view_bind_group): ROQueryItem<'w, Self::ViewQuery>, - _view: Option<()>, - _param: SystemParamItem<'w, '_, Self::Param>, - pass: &mut TrackedRenderPass<'w>, - ) -> RenderCommandResult { - pass.set_bind_group(I, &mesh2d_view_bind_group.value, &[view_uniform.offset]); - - RenderCommandResult::Success - } -} - -pub struct SetMesh2dBindGroup; -impl RenderCommand

for SetMesh2dBindGroup { - type Param = SRes; - type ViewQuery = (); - type ItemQuery = (); - - #[inline] - fn render<'w>( - item: &P, - _view: (), - _item_query: Option<()>, - mesh2d_bind_group: SystemParamItem<'w, '_, Self::Param>, - pass: &mut TrackedRenderPass<'w>, - ) -> RenderCommandResult { - let mut dynamic_offsets: [u32; 1] = Default::default(); - let mut offset_count = 0; - if let PhaseItemExtraIndex::DynamicOffset(dynamic_offset) = item.extra_index() { - dynamic_offsets[offset_count] = dynamic_offset; - offset_count += 1; - } - pass.set_bind_group( - I, - &mesh2d_bind_group.into_inner().value, - &dynamic_offsets[..offset_count], - ); - RenderCommandResult::Success - } -} - -pub struct DrawMesh2d; -impl RenderCommand

for DrawMesh2d { - type Param = ( - SRes>, - SRes, - SRes, - ); - type ViewQuery = (); - type ItemQuery = (); - - #[inline] - fn render<'w>( - item: &P, - _view: (), - _item_query: Option<()>, - (meshes, render_mesh2d_instances, mesh_allocator): SystemParamItem<'w, '_, Self::Param>, - pass: &mut TrackedRenderPass<'w>, - ) -> RenderCommandResult { - let meshes = meshes.into_inner(); - let render_mesh2d_instances = render_mesh2d_instances.into_inner(); - let mesh_allocator = mesh_allocator.into_inner(); - - let Some(RenderMesh2dInstance { mesh_asset_id, .. }) = - render_mesh2d_instances.get(&item.main_entity()) - else { - return RenderCommandResult::Skip; - }; - let Some(gpu_mesh) = meshes.get(*mesh_asset_id) else { - return RenderCommandResult::Skip; - }; - let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(mesh_asset_id) else { - return RenderCommandResult::Skip; - }; - - pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..)); - - let batch_range = item.batch_range(); - match &gpu_mesh.buffer_info { - RenderMeshBufferInfo::Indexed { - index_format, - count, - } => { - let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(mesh_asset_id) - else { - return RenderCommandResult::Skip; - }; - - pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, *index_format); - - pass.draw_indexed( - index_buffer_slice.range.start..(index_buffer_slice.range.start + count), - vertex_buffer_slice.range.start as i32, - batch_range.clone(), - ); - } - RenderMeshBufferInfo::NonIndexed => { - pass.draw(vertex_buffer_slice.range, batch_range.clone()); - } - } - RenderCommandResult::Success - } -} diff --git a/crates/bevy_sprite/src/mesh2d/mod.rs b/crates/bevy_sprite/src/mesh2d/mod.rs index 07e2eb0d6860e..2fa7382e1b943 100644 --- a/crates/bevy_sprite/src/mesh2d/mod.rs +++ b/crates/bevy_sprite/src/mesh2d/mod.rs @@ -1,9 +1,5 @@ mod color_material; -mod material; -mod mesh; mod wireframe2d; pub use color_material::*; -pub use material::*; -pub use mesh::*; pub use wireframe2d::*; diff --git a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs index 0aef1777655c1..e819c6e7dea1a 100644 --- a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs +++ b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs @@ -1,7 +1,5 @@ -use crate::{ - DrawMesh2d, Mesh2dPipeline, Mesh2dPipelineKey, RenderMesh2dInstances, SetMesh2dBindGroup, - SetMesh2dViewBindGroup, ViewKeyCache, ViewSpecializationTicks, -}; +use core::ops::Range; + use bevy_app::{App, Plugin, PostUpdate, Startup, Update}; use bevy_asset::{ load_internal_asset, prelude::AssetChanged, weak_handle, AsAssetId, Asset, AssetApp, @@ -51,7 +49,13 @@ use bevy_render::{ }, Extract, Render, RenderApp, RenderDebugFlags, RenderSet, }; -use core::{hash::Hash, ops::Range}; +use bevy_render_2d::mesh_pipeline::{ + commands::{DrawMesh2d, SetMesh2dBindGroup, SetMesh2dViewBindGroup}, + key::Mesh2dPipelineKey, + pipeline::Mesh2dPipeline, + render::{RenderMesh2dInstances, ViewKeyCache, ViewSpecializationTicks}, +}; + use tracing::error; pub const WIREFRAME_2D_SHADER_HANDLE: Handle = diff --git a/crates/bevy_sprite/src/mesh2d/wireframe2d.wgsl b/crates/bevy_sprite/src/mesh2d/wireframe2d.wgsl index c7bb3aa791b18..aee1544c48711 100644 --- a/crates/bevy_sprite/src/mesh2d/wireframe2d.wgsl +++ b/crates/bevy_sprite/src/mesh2d/wireframe2d.wgsl @@ -1,4 +1,4 @@ -#import bevy_sprite::mesh2d_vertex_output::VertexOutput +#import bevy_render_2d::mesh2d_vertex_output::VertexOutput struct PushConstants { color: vec4 diff --git a/docs/cargo_features.md b/docs/cargo_features.md index f15fa1c4c6683..d011bc56650f7 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -30,6 +30,7 @@ The default feature set enables most of the expected features of a game engine, |bevy_pbr|Adds PBR rendering| |bevy_picking|Provides picking functionality| |bevy_render|Provides rendering functionality| +|bevy_render_2d|Provides functionality for rendering in 2d| |bevy_scene|Provides scene functionality| |bevy_sprite|Provides sprite functionality| |bevy_sprite_picking_backend|Provides an implementation for picking sprites| diff --git a/examples/2d/custom_gltf_vertex_attribute.rs b/examples/2d/custom_gltf_vertex_attribute.rs index 0742e3616fe04..279c1eb2c0ef4 100644 --- a/examples/2d/custom_gltf_vertex_attribute.rs +++ b/examples/2d/custom_gltf_vertex_attribute.rs @@ -8,7 +8,7 @@ use bevy::{ mesh::{MeshVertexAttribute, MeshVertexBufferLayoutRef}, render_resource::*, }, - sprite::{Material2d, Material2dKey, Material2dPlugin}, + render_2d::material::{key::Material2dKey, plugin::Material2dPlugin}, }; /// This example uses a shader source file from the assets subdirectory diff --git a/examples/2d/mesh2d_alpha_mode.rs b/examples/2d/mesh2d_alpha_mode.rs index 60bb6ef3a6147..bc86f4808ba38 100644 --- a/examples/2d/mesh2d_alpha_mode.rs +++ b/examples/2d/mesh2d_alpha_mode.rs @@ -4,7 +4,6 @@ use bevy::{ color::palettes::css::{BLUE, GREEN, WHITE}, prelude::*, - sprite::AlphaMode2d, }; fn main() { diff --git a/examples/2d/mesh2d_manual.rs b/examples/2d/mesh2d_manual.rs index 82da9392c7164..f33502f26f1d8 100644 --- a/examples/2d/mesh2d_manual.rs +++ b/examples/2d/mesh2d_manual.rs @@ -30,10 +30,11 @@ use bevy::{ view::{ExtractedView, RenderVisibleEntities, ViewTarget}, Extract, Render, RenderApp, RenderSet, }, - sprite::{ - extract_mesh2d, DrawMesh2d, Material2dBindGroupId, Mesh2dPipeline, Mesh2dPipelineKey, - Mesh2dTransforms, MeshFlags, RenderMesh2dInstance, SetMesh2dBindGroup, - SetMesh2dViewBindGroup, + render_2d::mesh_pipeline::{ + commands::{DrawMesh2d, SetMesh2dBindGroup, SetMesh2dViewBindGroup}, + key::Mesh2dPipelineKey, + pipeline::Mesh2dPipeline, + render::{Material2dBindGroupId, Mesh2dTransforms, MeshFlags, RenderMesh2dInstance}, }, }; use std::f32::consts::PI; @@ -311,7 +312,9 @@ impl Plugin for ColoredMesh2dPlugin { .init_resource::() .add_systems( ExtractSchedule, - extract_colored_mesh2d.after(extract_mesh2d), + extract_colored_mesh2d + .after(RenderSet::QueueMeshes) + .in_set(RenderSet::Queue), ) .add_systems(Render, queue_colored_mesh2d.in_set(RenderSet::QueueMeshes)); } diff --git a/examples/camera/2d_screen_shake.rs b/examples/camera/2d_screen_shake.rs index dcdcd688110f4..784b623910038 100644 --- a/examples/camera/2d_screen_shake.rs +++ b/examples/camera/2d_screen_shake.rs @@ -6,7 +6,7 @@ //! |:-------------|:---------------------| //! | Space | Trigger screen shake | -use bevy::{prelude::*, render::camera::SubCameraView, sprite::MeshMaterial2d}; +use bevy::{prelude::*, render::camera::SubCameraView}; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; diff --git a/examples/shader/shader_material_2d.rs b/examples/shader/shader_material_2d.rs index ce092edd1191d..fd8577c038b38 100644 --- a/examples/shader/shader_material_2d.rs +++ b/examples/shader/shader_material_2d.rs @@ -4,7 +4,7 @@ use bevy::{ prelude::*, reflect::TypePath, render::render_resource::{AsBindGroup, ShaderRef}, - sprite::{AlphaMode2d, Material2d, Material2dPlugin}, + render_2d::material::plugin::Material2dPlugin, }; /// This example uses a shader source file from the assets subdirectory diff --git a/examples/stress_tests/bevymark.rs b/examples/stress_tests/bevymark.rs index d44fbfa7d28fa..8237f66bcd0fd 100644 --- a/examples/stress_tests/bevymark.rs +++ b/examples/stress_tests/bevymark.rs @@ -14,7 +14,6 @@ use bevy::{ render_asset::RenderAssetUsages, render_resource::{Extent3d, TextureDimension, TextureFormat}, }, - sprite::AlphaMode2d, window::{PresentMode, WindowResolution}, winit::{UpdateMode, WinitSettings}, }; diff --git a/release-content/migration-guides/bevy_render_2d.md b/release-content/migration-guides/bevy_render_2d.md new file mode 100644 index 0000000000000..9d327dd8aa1ce --- /dev/null +++ b/release-content/migration-guides/bevy_render_2d.md @@ -0,0 +1,78 @@ +--- +title: Create `bevy_render_2d` crate +pull_requests: [18467] +--- + +## Goal + +Extract from `bevy_sprite` code that is not exclusive to sprites and move it to +new crate `bevy_render_2d`. New locations for symbols are as follows: + +## Relocations + +### Structs + +Struct | `0.16` Path | `0.17` Path +--- | --- | --- +`MeshMaterial2d` | | `bevy_render_2d::material` +`Material2dKey` | | `bevy_render_2d::material::key` +`DrawMesh2d` | | `bevy_render_2d::mesh_pipeline::commands` +`SetMesh2dBindGroup` | | `bevy_render_2d::mesh_pipeline::commands` +`SetMesh2dViewBindGroup` | | `bevy_render_2d::mesh_pipeline::commands` +`Mesh2dPipeline` | | `bevy_render_2d::mesh_pipeline::pipeline` +`Mesh2dPipelineKey` | | `bevy_render_2d::mesh_pipeline::key` +`Material2dBindGroupId` | | `bevy_render_2d::mesh_pipeline::render` +`Mesh2dBindGroup` | | `bevy_render_2d::mesh_pipeline::render` +`Mesh2dTransforms` | | `bevy_render_2d::mesh_pipeline::render` +`Mesh2dUniform` | | `bevy_render_2d::mesh_pipeline::render` +`Mesh2dViewBindGroup` | | `bevy_render_2d::mesh_pipeline::render` +`Material2dBindGroupId` | | `bevy_render_2d::mesh_pipeline::render` +`MeshFlags` | | `bevy_render_2d::mesh_pipeline::render` +`RenderMesh2dInstance` | | `bevy_render_2d::mesh_pipeline::render` +`RenderMesh2dInstances` | | `bevy_render_2d::mesh_pipeline::render` +`ViewKeyCache` | | `bevy_render_2d::mesh_pipeline::render` +`ViewSpecializationTicks` | | `bevy_render_2d::mesh_pipeline::render` + +### Enum + +Enum | `0.16` Path | `0.17` Path +--- | --- | --- +`AlphaMode2d` | | `bevy_render_2d::material` + +### Traits + +Trait | `0.16` Path | `0.17` Path +--- | --- | --- +`Material2d` | | `bevy_render_2d::material` + +### Plugins + +Trait | `0.16` Path | `0.17` Path +--- | --- | --- +`Material2dPlugin` | | `bevy_render_2d::material::plugin` +`Mesh2dRenderPlugin` | | `bevy_render_2d::mesh_pipeline` + +### Methods + +Method | `0.16` Path | `0.17` Path +--- | --- | --- +tonemapping_pipeline_key | | `bevy_render_2d::mesh_pipeline::key` + +### Shader imports + +`0.16` Path | `0.17` Path +--- | --- +`bevy_sprite::mesh2d_bindings` | `bevy_render_2d::mesh2d_bindings` +`bevy_sprite::mesh2d_functions` | `bevy_render_2d::mesh2d_functions` +`bevy_sprite::mesh2d_types` | `bevy_render_2d::mesh2d_types` +`bevy_sprite::mesh2d_vertex_output` | `bevy_render_2d::mesh2d_vertex_output` +`bevy_sprite::mesh2d_view_bindings` | `bevy_render_2d::mesh2d_view_bindings` +`bevy_sprite::mesh2d_view_types` | `bevy_render_2d::mesh2d_view_types` + +## Prelude + +`bevy_render_2d`'s prelude contains: + +* `Material2d` +* `MeshMaterial2d` +* `AlphaMode2d`