From 0f5a1ee7f9275295a91f1575f7002d35fdec1c9c Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Thu, 27 Jun 2024 09:13:03 -0700 Subject: [PATCH] Allow phase items not associated with meshes to be binned. (#14029) As reported in #14004, many third-party plugins, such as Hanabi, enqueue entities that don't have meshes into render phases. However, the introduction of indirect mode added a dependency on mesh-specific data, breaking this workflow. This is because GPU preprocessing requires that the render phases manage indirect draw parameters, which don't apply to objects that aren't meshes. The existing code skips over binned entities that don't have indirect draw parameters, which causes the rendering to be skipped for such objects. To support this workflow, this commit adds a new field, `non_mesh_items`, to `BinnedRenderPhase`. This field contains a simple list of (bin key, entity) pairs. After drawing batchable and unbatchable objects, the non-mesh items are drawn one after another. Bevy itself doesn't enqueue any items into this list; it exists solely for the application and/or plugins to use. Additionally, this commit switches the asset ID in the standard bin keys to be an untyped asset ID rather than that of a mesh. This allows more flexibility, allowing bins to be keyed off any type of asset. This patch adds a new example, `custom_phase_item`, which simultaneously serves to demonstrate how to use this new feature and to act as a regression test so this doesn't break again. Fixes #14004. ## Changelog ### Added * `BinnedRenderPhase` now contains a `non_mesh_items` field for plugins to add custom items to. --- Cargo.toml | 11 + assets/shaders/custom_phase_item.wgsl | 36 ++ crates/bevy_asset/src/id.rs | 14 +- crates/bevy_core_pipeline/src/core_3d/mod.rs | 12 +- .../bevy_core_pipeline/src/deferred/node.rs | 4 +- crates/bevy_core_pipeline/src/prepass/mod.rs | 9 +- crates/bevy_core_pipeline/src/prepass/node.rs | 4 +- crates/bevy_pbr/src/material.rs | 12 +- crates/bevy_pbr/src/prepass/mod.rs | 16 +- crates/bevy_pbr/src/render/light.rs | 12 +- .../src/batching/gpu_preprocessing.rs | 8 +- crates/bevy_render/src/batching/mod.rs | 4 +- .../src/batching/no_gpu_preprocessing.rs | 8 +- crates/bevy_render/src/render_phase/mod.rs | 189 +++++++-- crates/bevy_render/src/view/mod.rs | 2 +- examples/README.md | 1 + examples/shader/custom_phase_item.rs | 391 ++++++++++++++++++ 17 files changed, 647 insertions(+), 86 deletions(-) create mode 100644 assets/shaders/custom_phase_item.wgsl create mode 100644 examples/shader/custom_phase_item.rs diff --git a/Cargo.toml b/Cargo.toml index 2670c694d84131..84f3c40594fe74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3231,6 +3231,17 @@ description = "Displays an example model with anisotropy" category = "3D Rendering" wasm = false +[[example]] +name = "custom_phase_item" +path = "examples/shader/custom_phase_item.rs" +doc-scrape-examples = true + +[package.metadata.example.custom_phase_item] +name = "Custom phase item" +description = "Demonstrates how to enqueue custom draw commands in a render phase" +category = "Shaders" +wasm = true + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/assets/shaders/custom_phase_item.wgsl b/assets/shaders/custom_phase_item.wgsl new file mode 100644 index 00000000000000..86d71ed677c1d3 --- /dev/null +++ b/assets/shaders/custom_phase_item.wgsl @@ -0,0 +1,36 @@ +// `custom_phase_item.wgsl` +// +// This shader goes with the `custom_phase_item` example. It demonstrates how to +// enqueue custom rendering logic in a `RenderPhase`. + +// The GPU-side vertex structure. +struct Vertex { + // The world-space position of the vertex. + @location(0) position: vec3, + // The color of the vertex. + @location(1) color: vec3, +}; + +// Information passed from the vertex shader to the fragment shader. +struct VertexOutput { + // The clip-space position of the vertex. + @builtin(position) clip_position: vec4, + // The color of the vertex. + @location(0) color: vec3, +}; + +// The vertex shader entry point. +@vertex +fn vertex(vertex: Vertex) -> VertexOutput { + // Use an orthographic projection. + var vertex_output: VertexOutput; + vertex_output.clip_position = vec4(vertex.position.xyz, 1.0); + vertex_output.color = vertex.color; + return vertex_output; +} + +// The fragment shader entry point. +@fragment +fn fragment(vertex_output: VertexOutput) -> @location(0) vec4 { + return vec4(vertex_output.color, 1.0); +} diff --git a/crates/bevy_asset/src/id.rs b/crates/bevy_asset/src/id.rs index f4c784d952abc6..c4b48a06d6cee2 100644 --- a/crates/bevy_asset/src/id.rs +++ b/crates/bevy_asset/src/id.rs @@ -288,13 +288,17 @@ impl Hash for UntypedAssetId { } } +impl Ord for UntypedAssetId { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.type_id() + .cmp(&other.type_id()) + .then_with(|| self.internal().cmp(&other.internal())) + } +} + impl PartialOrd for UntypedAssetId { fn partial_cmp(&self, other: &Self) -> Option { - if self.type_id() != other.type_id() { - None - } else { - Some(self.internal().cmp(&other.internal())) - } + Some(self.cmp(other)) } } diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index 63696adcbfa787..625455aab89dbe 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -64,7 +64,7 @@ pub const DEPTH_TEXTURE_SAMPLING_SUPPORTED: bool = true; use std::ops::Range; -use bevy_asset::AssetId; +use bevy_asset::{AssetId, UntypedAssetId}; use bevy_color::LinearRgba; pub use camera_3d::*; pub use main_opaque_pass_3d_node::*; @@ -76,7 +76,6 @@ use bevy_math::FloatOrd; use bevy_render::{ camera::{Camera, ExtractedCamera}, extract_component::ExtractComponentPlugin, - mesh::Mesh, prelude::Msaa, render_graph::{EmptyNode, RenderGraphApp, ViewNodeRunner}, render_phase::{ @@ -221,7 +220,7 @@ pub struct Opaque3d { pub extra_index: PhaseItemExtraIndex, } -/// Data that must be identical in order to batch meshes together. +/// Data that must be identical in order to batch phase items together. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Opaque3dBinKey { /// The identifier of the render pipeline. @@ -230,8 +229,11 @@ pub struct Opaque3dBinKey { /// The function used to draw. pub draw_function: DrawFunctionId, - /// The mesh. - pub asset_id: AssetId, + /// The asset that this phase item is associated with. + /// + /// Normally, this is the ID of the mesh, but for non-mesh items it might be + /// the ID of another type of asset. + pub asset_id: UntypedAssetId, /// The ID of a bind group specific to the material. /// diff --git a/crates/bevy_core_pipeline/src/deferred/node.rs b/crates/bevy_core_pipeline/src/deferred/node.rs index 5ee78c5d49f996..21df5f4ed9f6a8 100644 --- a/crates/bevy_core_pipeline/src/deferred/node.rs +++ b/crates/bevy_core_pipeline/src/deferred/node.rs @@ -144,8 +144,8 @@ impl ViewNode for DeferredGBufferPrepassNode { } // Opaque draws - if !opaque_deferred_phase.batchable_keys.is_empty() - || !opaque_deferred_phase.unbatchable_keys.is_empty() + if !opaque_deferred_phase.batchable_mesh_keys.is_empty() + || !opaque_deferred_phase.unbatchable_mesh_keys.is_empty() { #[cfg(feature = "trace")] let _opaque_prepass_span = info_span!("opaque_deferred_prepass").entered(); diff --git a/crates/bevy_core_pipeline/src/prepass/mod.rs b/crates/bevy_core_pipeline/src/prepass/mod.rs index 861bba12b6dc80..1f03eb8220f39e 100644 --- a/crates/bevy_core_pipeline/src/prepass/mod.rs +++ b/crates/bevy_core_pipeline/src/prepass/mod.rs @@ -29,12 +29,11 @@ pub mod node; use std::ops::Range; -use bevy_asset::AssetId; +use bevy_asset::UntypedAssetId; use bevy_ecs::prelude::*; use bevy_math::Mat4; use bevy_reflect::Reflect; use bevy_render::{ - mesh::Mesh, render_phase::{ BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem, PhaseItemExtraIndex, @@ -147,7 +146,7 @@ pub struct Opaque3dPrepass { } // TODO: Try interning these. -/// The data used to bin each opaque 3D mesh in the prepass and deferred pass. +/// The data used to bin each opaque 3D object in the prepass and deferred pass. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct OpaqueNoLightmap3dBinKey { /// The ID of the GPU pipeline. @@ -156,8 +155,8 @@ pub struct OpaqueNoLightmap3dBinKey { /// The function used to draw the mesh. pub draw_function: DrawFunctionId, - /// The ID of the mesh. - pub asset_id: AssetId, + /// The ID of the asset. + pub asset_id: UntypedAssetId, /// The ID of a bind group specific to the material. /// diff --git a/crates/bevy_core_pipeline/src/prepass/node.rs b/crates/bevy_core_pipeline/src/prepass/node.rs index d493a20c70c4c6..203581a2bf0d6b 100644 --- a/crates/bevy_core_pipeline/src/prepass/node.rs +++ b/crates/bevy_core_pipeline/src/prepass/node.rs @@ -120,8 +120,8 @@ impl ViewNode for PrepassNode { } // Opaque draws - if !opaque_prepass_phase.batchable_keys.is_empty() - || !opaque_prepass_phase.unbatchable_keys.is_empty() + if !opaque_prepass_phase.batchable_mesh_keys.is_empty() + || !opaque_prepass_phase.unbatchable_mesh_keys.is_empty() { #[cfg(feature = "trace")] let _opaque_prepass_span = info_span!("opaque_prepass").entered(); diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index a9c78957816652..a1c07e74135013 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -763,11 +763,15 @@ pub fn queue_material_meshes( let bin_key = Opaque3dBinKey { draw_function: draw_opaque_pbr, pipeline: pipeline_id, - asset_id: mesh_instance.mesh_asset_id, + asset_id: mesh_instance.mesh_asset_id.into(), material_bind_group_id: material.get_bind_group_id().0, lightmap_image, }; - opaque_phase.add(bin_key, *visible_entity, mesh_instance.should_batch()); + opaque_phase.add( + bin_key, + *visible_entity, + BinnedRenderPhaseType::mesh(mesh_instance.should_batch()), + ); } } // Alpha mask @@ -787,13 +791,13 @@ pub fn queue_material_meshes( let bin_key = OpaqueNoLightmap3dBinKey { draw_function: draw_alpha_mask_pbr, pipeline: pipeline_id, - asset_id: mesh_instance.mesh_asset_id, + asset_id: mesh_instance.mesh_asset_id.into(), material_bind_group_id: material.get_bind_group_id().0, }; alpha_mask_phase.add( bin_key, *visible_entity, - mesh_instance.should_batch(), + BinnedRenderPhaseType::mesh(mesh_instance.should_batch()), ); } } diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 1f728571bd806b..5330e055ee3c36 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -860,22 +860,22 @@ pub fn queue_prepass_material_meshes( OpaqueNoLightmap3dBinKey { draw_function: opaque_draw_deferred, pipeline: pipeline_id, - asset_id: mesh_instance.mesh_asset_id, + asset_id: mesh_instance.mesh_asset_id.into(), material_bind_group_id: material.get_bind_group_id().0, }, *visible_entity, - mesh_instance.should_batch(), + BinnedRenderPhaseType::mesh(mesh_instance.should_batch()), ); } else if let Some(opaque_phase) = opaque_phase.as_mut() { opaque_phase.add( OpaqueNoLightmap3dBinKey { draw_function: opaque_draw_prepass, pipeline: pipeline_id, - asset_id: mesh_instance.mesh_asset_id, + asset_id: mesh_instance.mesh_asset_id.into(), material_bind_group_id: material.get_bind_group_id().0, }, *visible_entity, - mesh_instance.should_batch(), + BinnedRenderPhaseType::mesh(mesh_instance.should_batch()), ); } } @@ -885,25 +885,25 @@ pub fn queue_prepass_material_meshes( let bin_key = OpaqueNoLightmap3dBinKey { pipeline: pipeline_id, draw_function: alpha_mask_draw_deferred, - asset_id: mesh_instance.mesh_asset_id, + asset_id: mesh_instance.mesh_asset_id.into(), material_bind_group_id: material.get_bind_group_id().0, }; alpha_mask_deferred_phase.as_mut().unwrap().add( bin_key, *visible_entity, - mesh_instance.should_batch(), + BinnedRenderPhaseType::mesh(mesh_instance.should_batch()), ); } else if let Some(alpha_mask_phase) = alpha_mask_phase.as_mut() { let bin_key = OpaqueNoLightmap3dBinKey { pipeline: pipeline_id, draw_function: alpha_mask_draw_prepass, - asset_id: mesh_instance.mesh_asset_id, + asset_id: mesh_instance.mesh_asset_id.into(), material_bind_group_id: material.get_bind_group_id().0, }; alpha_mask_phase.add( bin_key, *visible_entity, - mesh_instance.should_batch(), + BinnedRenderPhaseType::mesh(mesh_instance.should_batch()), ); } } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 0f17c2cabc8f95..1356e94825ccc8 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1,11 +1,10 @@ -use bevy_asset::AssetId; +use bevy_asset::UntypedAssetId; use bevy_color::ColorToComponents; use bevy_core_pipeline::core_3d::CORE_3D_DEPTH_FORMAT; use bevy_ecs::entity::EntityHashSet; use bevy_ecs::prelude::*; use bevy_ecs::{entity::EntityHashMap, system::lifetimeless::Read}; use bevy_math::{Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; -use bevy_render::mesh::Mesh; use bevy_render::{ diagnostic::RecordDiagnostics, mesh::GpuMesh, @@ -1286,10 +1285,10 @@ pub fn queue_shadows( ShadowBinKey { draw_function: draw_shadow_mesh, pipeline: pipeline_id, - asset_id: mesh_instance.mesh_asset_id, + asset_id: mesh_instance.mesh_asset_id.into(), }, entity, - mesh_instance.should_batch(), + BinnedRenderPhaseType::mesh(mesh_instance.should_batch()), ); } } @@ -1303,6 +1302,7 @@ pub struct Shadow { pub extra_index: PhaseItemExtraIndex, } +/// Data used to bin each object in the shadow map phase. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ShadowBinKey { /// The identifier of the render pipeline. @@ -1311,8 +1311,8 @@ pub struct ShadowBinKey { /// The function used to draw. pub draw_function: DrawFunctionId, - /// The mesh. - pub asset_id: AssetId, + /// The object. + pub asset_id: UntypedAssetId, } impl PhaseItem for Shadow { diff --git a/crates/bevy_render/src/batching/gpu_preprocessing.rs b/crates/bevy_render/src/batching/gpu_preprocessing.rs index 14e2c5e5941ec5..8551aa016e0071 100644 --- a/crates/bevy_render/src/batching/gpu_preprocessing.rs +++ b/crates/bevy_render/src/batching/gpu_preprocessing.rs @@ -523,9 +523,9 @@ pub fn batch_and_prepare_binned_render_phase( // Prepare batchables. - for key in &phase.batchable_keys { + for key in &phase.batchable_mesh_keys { let mut batch: Option = None; - for &entity in &phase.batchable_values[key] { + for &entity in &phase.batchable_mesh_values[key] { let Some(input_index) = GFBD::get_binned_index(&system_param_item, entity) else { continue; }; @@ -583,8 +583,8 @@ pub fn batch_and_prepare_binned_render_phase( } // Prepare unbatchables. - for key in &phase.unbatchable_keys { - let unbatchables = phase.unbatchable_values.get_mut(key).unwrap(); + for key in &phase.unbatchable_mesh_keys { + let unbatchables = phase.unbatchable_mesh_values.get_mut(key).unwrap(); for &entity in &unbatchables.entities { let Some(input_index) = GFBD::get_binned_index(&system_param_item, entity) else { continue; diff --git a/crates/bevy_render/src/batching/mod.rs b/crates/bevy_render/src/batching/mod.rs index b7bd3e892c8062..6287911e06249a 100644 --- a/crates/bevy_render/src/batching/mod.rs +++ b/crates/bevy_render/src/batching/mod.rs @@ -156,8 +156,8 @@ where BPI: BinnedPhaseItem, { for phase in phases.values_mut() { - phase.batchable_keys.sort_unstable(); - phase.unbatchable_keys.sort_unstable(); + phase.batchable_mesh_keys.sort_unstable(); + phase.unbatchable_mesh_keys.sort_unstable(); } } diff --git a/crates/bevy_render/src/batching/no_gpu_preprocessing.rs b/crates/bevy_render/src/batching/no_gpu_preprocessing.rs index 98df8098ca7eb4..51176cb42b2403 100644 --- a/crates/bevy_render/src/batching/no_gpu_preprocessing.rs +++ b/crates/bevy_render/src/batching/no_gpu_preprocessing.rs @@ -104,9 +104,9 @@ pub fn batch_and_prepare_binned_render_phase( for phase in phases.values_mut() { // Prepare batchables. - for key in &phase.batchable_keys { + for key in &phase.batchable_mesh_keys { let mut batch_set: SmallVec<[BinnedRenderPhaseBatch; 1]> = smallvec![]; - for &entity in &phase.batchable_values[key] { + for &entity in &phase.batchable_mesh_values[key] { let Some(buffer_data) = GFBD::get_binned_batch_data(&system_param_item, entity) else { continue; @@ -141,8 +141,8 @@ pub fn batch_and_prepare_binned_render_phase( } // Prepare unbatchables. - for key in &phase.unbatchable_keys { - let unbatchables = phase.unbatchable_values.get_mut(key).unwrap(); + for key in &phase.unbatchable_mesh_keys { + let unbatchables = phase.unbatchable_mesh_values.get_mut(key).unwrap(); for &entity in &unbatchables.entities { let Some(buffer_data) = GFBD::get_binned_batch_data(&system_param_item, entity) else { diff --git a/crates/bevy_render/src/render_phase/mod.rs b/crates/bevy_render/src/render_phase/mod.rs index f431074d0e2a84..17b266406e3925 100644 --- a/crates/bevy_render/src/render_phase/mod.rs +++ b/crates/bevy_render/src/render_phase/mod.rs @@ -94,24 +94,33 @@ where /// /// These are accumulated in `queue_material_meshes` and then sorted in /// `batch_and_prepare_binned_render_phase`. - pub batchable_keys: Vec, + pub batchable_mesh_keys: Vec, /// The batchable bins themselves. /// /// Each bin corresponds to a single batch set. For unbatchable entities, /// prefer `unbatchable_values` instead. - pub(crate) batchable_values: HashMap>, + pub(crate) batchable_mesh_values: HashMap>, /// A list of `BinKey`s for unbatchable items. /// /// These are accumulated in `queue_material_meshes` and then sorted in /// `batch_and_prepare_binned_render_phase`. - pub unbatchable_keys: Vec, + pub unbatchable_mesh_keys: Vec, /// The unbatchable bins. /// /// Each entity here is rendered in a separate drawcall. - pub(crate) unbatchable_values: HashMap, + pub(crate) unbatchable_mesh_values: HashMap, + + /// Items in the bin that aren't meshes at all. + /// + /// Bevy itself doesn't place anything in this list, but plugins or your app + /// can in order to execute custom drawing commands. Draw functions for each + /// entity are simply called in order at rendering time. + /// + /// See the `custom_phase_item` example for an example of how to use this. + pub non_mesh_items: Vec<(BPI::BinKey, Entity)>, /// Information on each batch set. /// @@ -199,6 +208,30 @@ pub(crate) struct UnbatchableBinnedEntityIndices { pub(crate) extra_index: PhaseItemExtraIndex, } +/// Identifies the list within [`BinnedRenderPhase`] that a phase item is to be +/// placed in. +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum BinnedRenderPhaseType { + /// The item is a mesh that's eligible for indirect rendering and can be + /// batched with other meshes of the same type. + BatchableMesh, + + /// The item is a mesh that's eligible for indirect rendering, but can't be + /// batched with other meshes of the same type. + /// + /// At the moment, this is used for skinned meshes. + UnbatchableMesh, + + /// The item isn't a mesh at all. + /// + /// Bevy will simply invoke the drawing commands for such items one after + /// another, with no further processing. + /// + /// The engine itself doesn't enqueue any items of this type, but it's + /// available for use in your application and/or plugins. + NonMesh, +} + impl From> for UnbatchableBinnedEntityIndices where T: Clone + ShaderSize + WriteInto, @@ -240,28 +273,38 @@ where { /// Bins a new entity. /// - /// `batchable` specifies whether the entity can be batched with other - /// entities of the same type. - pub fn add(&mut self, key: BPI::BinKey, entity: Entity, batchable: bool) { - if batchable { - match self.batchable_values.entry(key.clone()) { - Entry::Occupied(mut entry) => entry.get_mut().push(entity), - Entry::Vacant(entry) => { - self.batchable_keys.push(key); - entry.insert(vec![entity]); + /// The `phase_type` parameter specifies whether the entity is a + /// preprocessable mesh and whether it can be binned with meshes of the same + /// type. + pub fn add(&mut self, key: BPI::BinKey, entity: Entity, phase_type: BinnedRenderPhaseType) { + match phase_type { + BinnedRenderPhaseType::BatchableMesh => { + match self.batchable_mesh_values.entry(key.clone()) { + Entry::Occupied(mut entry) => entry.get_mut().push(entity), + Entry::Vacant(entry) => { + self.batchable_mesh_keys.push(key); + entry.insert(vec![entity]); + } } } - } else { - match self.unbatchable_values.entry(key.clone()) { - Entry::Occupied(mut entry) => entry.get_mut().entities.push(entity), - Entry::Vacant(entry) => { - self.unbatchable_keys.push(key); - entry.insert(UnbatchableBinnedEntities { - entities: vec![entity], - buffer_indices: default(), - }); + + BinnedRenderPhaseType::UnbatchableMesh => { + match self.unbatchable_mesh_values.entry(key.clone()) { + Entry::Occupied(mut entry) => entry.get_mut().entities.push(entity), + Entry::Vacant(entry) => { + self.unbatchable_mesh_keys.push(key); + entry.insert(UnbatchableBinnedEntities { + entities: vec![entity], + buffer_indices: default(), + }); + } } } + + BinnedRenderPhaseType::NonMesh => { + // We don't process these items further. + self.non_mesh_items.push((key, entity)); + } } } @@ -271,14 +314,33 @@ where render_pass: &mut TrackedRenderPass<'w>, world: &'w World, view: Entity, + ) { + { + let draw_functions = world.resource::>(); + let mut draw_functions = draw_functions.write(); + draw_functions.prepare(world); + // Make sure to drop the reader-writer lock here to avoid recursive + // locks. + } + + self.render_batchable_meshes(render_pass, world, view); + self.render_unbatchable_meshes(render_pass, world, view); + self.render_non_meshes(render_pass, world, view); + } + + /// Renders all batchable meshes queued in this phase. + fn render_batchable_meshes<'w>( + &self, + render_pass: &mut TrackedRenderPass<'w>, + world: &'w World, + view: Entity, ) { let draw_functions = world.resource::>(); let mut draw_functions = draw_functions.write(); - draw_functions.prepare(world); - // Encode draws for batchables. - debug_assert_eq!(self.batchable_keys.len(), self.batch_sets.len()); - for (key, batch_set) in self.batchable_keys.iter().zip(self.batch_sets.iter()) { + debug_assert_eq!(self.batchable_mesh_keys.len(), self.batch_sets.len()); + + for (key, batch_set) in self.batchable_mesh_keys.iter().zip(self.batch_sets.iter()) { for batch in batch_set { let binned_phase_item = BPI::new( key.clone(), @@ -296,11 +358,20 @@ where draw_function.draw(world, render_pass, view, &binned_phase_item); } } + } - // Encode draws for unbatchables. + /// Renders all unbatchable meshes queued in this phase. + fn render_unbatchable_meshes<'w>( + &self, + render_pass: &mut TrackedRenderPass<'w>, + world: &'w World, + view: Entity, + ) { + let draw_functions = world.resource::>(); + let mut draw_functions = draw_functions.write(); - for key in &self.unbatchable_keys { - let unbatchable_entities = &self.unbatchable_values[key]; + for key in &self.unbatchable_mesh_keys { + let unbatchable_entities = &self.unbatchable_mesh_values[key]; for (entity_index, &entity) in unbatchable_entities.entities.iter().enumerate() { let unbatchable_dynamic_offset = match &unbatchable_entities.buffer_indices { UnbatchableBinnedEntityIndexSet::NoEntities => { @@ -346,15 +417,44 @@ where } } + /// Renders all objects of type [`BinnedRenderPhaseType::NonMesh`]. + /// + /// These will have been added by plugins or the application. + fn render_non_meshes<'w>( + &self, + render_pass: &mut TrackedRenderPass<'w>, + world: &'w World, + view: Entity, + ) { + let draw_functions = world.resource::>(); + let mut draw_functions = draw_functions.write(); + + for &(ref key, entity) in &self.non_mesh_items { + // Come up with a fake batch range and extra index. The draw + // function is expected to manage any sort of batching logic itself. + let binned_phase_item = BPI::new(key.clone(), entity, 0..1, PhaseItemExtraIndex(0)); + + let Some(draw_function) = draw_functions.get_mut(binned_phase_item.draw_function()) + else { + continue; + }; + + draw_function.draw(world, render_pass, view, &binned_phase_item); + } + } + pub fn is_empty(&self) -> bool { - self.batchable_keys.is_empty() && self.unbatchable_keys.is_empty() + self.batchable_mesh_keys.is_empty() + && self.unbatchable_mesh_keys.is_empty() + && self.non_mesh_items.is_empty() } pub fn clear(&mut self) { - self.batchable_keys.clear(); - self.batchable_values.clear(); - self.unbatchable_keys.clear(); - self.unbatchable_values.clear(); + self.batchable_mesh_keys.clear(); + self.batchable_mesh_values.clear(); + self.unbatchable_mesh_keys.clear(); + self.unbatchable_mesh_values.clear(); + self.non_mesh_items.clear(); self.batch_sets.clear(); } } @@ -365,10 +465,11 @@ where { fn default() -> Self { Self { - batchable_keys: vec![], - batchable_values: HashMap::default(), - unbatchable_keys: vec![], - unbatchable_values: HashMap::default(), + batchable_mesh_keys: vec![], + batchable_mesh_values: HashMap::default(), + unbatchable_mesh_keys: vec![], + unbatchable_mesh_values: HashMap::default(), + non_mesh_items: vec![], batch_sets: vec![], } } @@ -995,3 +1096,15 @@ where phase.sort(); } } + +impl BinnedRenderPhaseType { + /// Creates the appropriate [`BinnedRenderPhaseType`] for a mesh, given its + /// batchability. + pub fn mesh(batchable: bool) -> BinnedRenderPhaseType { + if batchable { + BinnedRenderPhaseType::BatchableMesh + } else { + BinnedRenderPhaseType::UnbatchableMesh + } + } +} diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 17f626d410c5ec..8d42322c099062 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -154,7 +154,7 @@ impl Plugin for ViewPlugin { /// .run(); /// ``` #[derive( - Resource, Default, Clone, Copy, ExtractResource, Reflect, PartialEq, PartialOrd, Debug, + Resource, Default, Clone, Copy, ExtractResource, Reflect, PartialEq, PartialOrd, Eq, Hash, Debug, )] #[reflect(Resource, Default)] pub enum Msaa { diff --git a/examples/README.md b/examples/README.md index 4cccac78600f8f..c0a4a76780fa22 100644 --- a/examples/README.md +++ b/examples/README.md @@ -368,6 +368,7 @@ Example | Description [Array Texture](../examples/shader/array_texture.rs) | A shader that shows how to reuse the core bevy PBR shading functionality in a custom material that obtains the base color from an array texture. [Compute - Game of Life](../examples/shader/compute_shader_game_of_life.rs) | A compute shader that simulates Conway's Game of Life [Custom Vertex Attribute](../examples/shader/custom_vertex_attribute.rs) | A shader that reads a mesh's custom vertex attribute +[Custom phase item](../examples/shader/custom_phase_item.rs) | Demonstrates how to enqueue custom draw commands in a render phase [Extended Material](../examples/shader/extended_material.rs) | A custom shader that builds on the standard material [GPU readback](../examples/shader/gpu_readback.rs) | A very simple compute shader that writes to a buffer that is read by the cpu [Instancing](../examples/shader/shader_instancing.rs) | A shader that renders a mesh multiple times in one draw call diff --git a/examples/shader/custom_phase_item.rs b/examples/shader/custom_phase_item.rs new file mode 100644 index 00000000000000..299c64bf8fdab3 --- /dev/null +++ b/examples/shader/custom_phase_item.rs @@ -0,0 +1,391 @@ +//! Demonstrates how to enqueue custom draw commands in a render phase. +//! +//! This example shows how to use the built-in +//! [`bevy_render::render_phase::BinnedRenderPhase`] functionality with a +//! custom [`RenderCommand`] to allow inserting arbitrary GPU drawing logic +//! into Bevy's pipeline. This is not the only way to add custom rendering code +//! into Bevy—render nodes are another, lower-level method—but it does allow +//! for better reuse of parts of Bevy's built-in mesh rendering logic. + +use std::mem; + +use bevy::{ + core_pipeline::core_3d::{Opaque3d, Opaque3dBinKey, CORE_3D_DEPTH_FORMAT}, + ecs::{ + query::ROQueryItem, + system::{lifetimeless::SRes, SystemParamItem}, + }, + math::{vec3, Vec3A}, + prelude::*, + render::{ + extract_component::{ExtractComponent, ExtractComponentPlugin}, + primitives::Aabb, + render_phase::{ + AddRenderCommand, BinnedRenderPhaseType, DrawFunctions, PhaseItem, RenderCommand, + RenderCommandResult, SetItemPipeline, TrackedRenderPass, ViewBinnedRenderPhases, + }, + render_resource::{ + BufferUsages, ColorTargetState, ColorWrites, CompareFunction, DepthStencilState, + FragmentState, IndexFormat, MultisampleState, PipelineCache, PrimitiveState, + RawBufferVec, RenderPipelineDescriptor, SpecializedRenderPipeline, + SpecializedRenderPipelines, TextureFormat, VertexAttribute, VertexBufferLayout, + VertexFormat, VertexState, VertexStepMode, + }, + renderer::{RenderDevice, RenderQueue}, + texture::BevyDefault as _, + view::{self, ExtractedView, VisibilitySystems, VisibleEntities}, + Render, RenderApp, RenderSet, + }, +}; +use bytemuck::{Pod, Zeroable}; + +/// A marker component that represents an entity that is to be rendered using +/// our custom phase item. +/// +/// Note the [`ExtractComponent`] trait implementation. This is necessary to +/// tell Bevy that this object should be pulled into the render world. +#[derive(Clone, Component, ExtractComponent)] +struct CustomRenderedEntity; + +/// Holds a reference to our shader. +/// +/// This is loaded at app creation time. +#[derive(Resource)] +struct CustomPhasePipeline { + shader: Handle, +} + +/// A [`RenderCommand`] that binds the vertex and index buffers and issues the +/// draw command for our custom phase item. +struct DrawCustomPhaseItem; + +impl

RenderCommand

for DrawCustomPhaseItem +where + P: PhaseItem, +{ + type Param = SRes; + + type ViewQuery = (); + + type ItemQuery = (); + + fn render<'w>( + _: &P, + _: ROQueryItem<'w, Self::ViewQuery>, + _: Option>, + custom_phase_item_buffers: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + // Borrow check workaround. + let custom_phase_item_buffers = custom_phase_item_buffers.into_inner(); + + // Tell the GPU where the vertices are. + pass.set_vertex_buffer( + 0, + custom_phase_item_buffers + .vertices + .buffer() + .unwrap() + .slice(..), + ); + + // Tell the GPU where the indices are. + pass.set_index_buffer( + custom_phase_item_buffers + .indices + .buffer() + .unwrap() + .slice(..), + 0, + IndexFormat::Uint32, + ); + + // Draw one triangle (3 vertices). + pass.draw_indexed(0..3, 0, 0..1); + + RenderCommandResult::Success + } +} + +/// The GPU vertex and index buffers for our custom phase item. +/// +/// As the custom phase item is a single triangle, these are uploaded once and +/// then left alone. +#[derive(Resource)] +struct CustomPhaseItemBuffers { + /// The vertices for the single triangle. + /// + /// This is a [`RawBufferVec`] because that's the simplest and fastest type + /// of GPU buffer, and [`Vertex`] objects are simple. + vertices: RawBufferVec, + + /// The indices of the single triangle. + /// + /// As above, this is a [`RawBufferVec`] because `u32` values have trivial + /// size and alignment. + indices: RawBufferVec, +} + +/// The CPU-side structure that describes a single vertex of the triangle. +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(C)] +struct Vertex { + /// The 3D position of the triangle vertex. + position: Vec3, + /// Padding. + pad0: u32, + /// The color of the triangle vertex. + color: Vec3, + /// Padding. + pad1: u32, +} + +impl Vertex { + /// Creates a new vertex structure. + const fn new(position: Vec3, color: Vec3) -> Vertex { + Vertex { + position, + color, + pad0: 0, + pad1: 0, + } + } +} + +/// The custom draw commands that Bevy executes for each entity we enqueue into +/// the render phase. +type DrawCustomPhaseItemCommands = (SetItemPipeline, DrawCustomPhaseItem); + +/// A query filter that tells [`view::check_visibility`] about our custom +/// rendered entity. +type WithCustomRenderedEntity = With; + +/// A single triangle's worth of vertices, for demonstration purposes. +static VERTICES: [Vertex; 3] = [ + Vertex::new(vec3(-0.866, -0.5, 0.5), vec3(1.0, 0.0, 0.0)), + Vertex::new(vec3(0.866, -0.5, 0.5), vec3(0.0, 1.0, 0.0)), + Vertex::new(vec3(0.0, 1.0, 0.5), vec3(0.0, 0.0, 1.0)), +]; + +/// The entry point. +fn main() { + let mut app = App::new(); + app.add_plugins(DefaultPlugins) + .add_plugins(ExtractComponentPlugin::::default()) + .add_systems(Startup, setup) + // Make sure to tell Bevy to check our entity for visibility. Bevy won't + // do this by default, for efficiency reasons. + .add_systems( + PostUpdate, + view::check_visibility:: + .in_set(VisibilitySystems::CheckVisibility), + ); + + // We make sure to add these to the render app, not the main app. + app.get_sub_app_mut(RenderApp) + .unwrap() + .init_resource::() + .init_resource::>() + .add_render_command::() + .add_systems( + Render, + prepare_custom_phase_item_buffers.in_set(RenderSet::Prepare), + ) + .add_systems(Render, queue_custom_phase_item.in_set(RenderSet::Queue)); + + app.run(); +} + +/// Spawns the objects in the scene. +fn setup(mut commands: Commands) { + // Spawn a single entity that has custom rendering. It'll be extracted into + // the render world via [`ExtractComponent`]. + commands + .spawn(SpatialBundle { + visibility: Visibility::Visible, + transform: Transform::IDENTITY, + ..default() + }) + // This `Aabb` is necessary for the visibility checks to work. + .insert(Aabb { + center: Vec3A::ZERO, + half_extents: Vec3A::splat(0.5), + }) + .insert(CustomRenderedEntity); + + // Spawn the camera. + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(0.0, 0.0, 1.0).looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }); +} + +/// Creates the [`CustomPhaseItemBuffers`] resource. +/// +/// This must be done in a startup system because it needs the [`RenderDevice`] +/// and [`RenderQueue`] to exist, and they don't until [`App::run`] is called. +fn prepare_custom_phase_item_buffers(mut commands: Commands) { + commands.init_resource::(); +} + +/// A render-world system that enqueues the entity with custom rendering into +/// the opaque render phases of each view. +fn queue_custom_phase_item( + pipeline_cache: Res, + custom_phase_pipeline: Res, + msaa: Res, + mut opaque_render_phases: ResMut>, + opaque_draw_functions: Res>, + mut specialized_render_pipelines: ResMut>, + views: Query<(Entity, &VisibleEntities), With>, +) { + let draw_custom_phase_item = opaque_draw_functions + .read() + .id::(); + + // Render phases are per-view, so we need to iterate over all views so that + // the entity appears in them. (In this example, we have only one view, but + // it's good practice to loop over all views anyway.) + for (view_entity, view_visible_entities) in views.iter() { + let Some(opaque_phase) = opaque_render_phases.get_mut(&view_entity) else { + continue; + }; + + // Find all the custom rendered entities that are visible from this + // view. + for &entity in view_visible_entities + .get::() + .iter() + { + // Ordinarily, the [`SpecializedRenderPipeline::Key`] would contain + // some per-view settings, such as whether the view is HDR, but for + // simplicity's sake we simply hard-code the view's characteristics, + // with the exception of number of MSAA samples. + let pipeline_id = specialized_render_pipelines.specialize( + &pipeline_cache, + &custom_phase_pipeline, + *msaa, + ); + + // Add the custom render item. We use the + // [`BinnedRenderPhaseType::NonMesh`] type to skip the special + // handling that Bevy has for meshes (preprocessing, indirect + // draws, etc.) + // + // The asset ID is arbitrary; we simply use [`AssetId::invalid`], + // but you can use anything you like. Note that the asset ID need + // not be the ID of a [`Mesh`]. + opaque_phase.add( + Opaque3dBinKey { + draw_function: draw_custom_phase_item, + pipeline: pipeline_id, + asset_id: AssetId::::invalid().untyped(), + material_bind_group_id: None, + lightmap_image: None, + }, + entity, + BinnedRenderPhaseType::NonMesh, + ); + } + } +} + +impl SpecializedRenderPipeline for CustomPhasePipeline { + type Key = Msaa; + + fn specialize(&self, msaa: Self::Key) -> RenderPipelineDescriptor { + RenderPipelineDescriptor { + label: Some("custom render pipeline".into()), + layout: vec![], + push_constant_ranges: vec![], + vertex: VertexState { + shader: self.shader.clone(), + shader_defs: vec![], + entry_point: "vertex".into(), + buffers: vec![VertexBufferLayout { + array_stride: mem::size_of::() as u64, + step_mode: VertexStepMode::Vertex, + // This needs to match the layout of [`Vertex`]. + attributes: vec![ + VertexAttribute { + format: VertexFormat::Float32x3, + offset: 0, + shader_location: 0, + }, + VertexAttribute { + format: VertexFormat::Float32x3, + offset: 16, + shader_location: 1, + }, + ], + }], + }, + fragment: Some(FragmentState { + shader: self.shader.clone(), + shader_defs: vec![], + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + // Ordinarily, you'd want to check whether the view has the + // HDR format and substitute the appropriate texture format + // here, but we omit that for simplicity. + format: TextureFormat::bevy_default(), + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + primitive: PrimitiveState::default(), + // Note that if your view has no depth buffer this will need to be + // changed. + depth_stencil: Some(DepthStencilState { + format: CORE_3D_DEPTH_FORMAT, + depth_write_enabled: false, + depth_compare: CompareFunction::Always, + stencil: default(), + bias: default(), + }), + multisample: MultisampleState { + count: msaa.samples(), + mask: !0, + alpha_to_coverage_enabled: false, + }, + } + } +} + +impl FromWorld for CustomPhaseItemBuffers { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let render_queue = world.resource::(); + + // Create the vertex and index buffers. + let mut vbo = RawBufferVec::new(BufferUsages::VERTEX); + let mut ibo = RawBufferVec::new(BufferUsages::INDEX); + + for vertex in &VERTICES { + vbo.push(*vertex); + } + for index in 0..3 { + ibo.push(index); + } + + // These two lines are required in order to trigger the upload to GPU. + vbo.write_buffer(render_device, render_queue); + ibo.write_buffer(render_device, render_queue); + + CustomPhaseItemBuffers { + vertices: vbo, + indices: ibo, + } + } +} + +impl FromWorld for CustomPhasePipeline { + fn from_world(world: &mut World) -> Self { + // Load and compile the shader in the background. + let asset_server = world.resource::(); + + CustomPhasePipeline { + shader: asset_server.load("shaders/custom_phase_item.wgsl"), + } + } +}