diff --git a/Cargo.toml b/Cargo.toml index 64bc15ad0bca2..cd15bad1cbbe7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3234,6 +3234,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 0000000000000..86d71ed677c1d --- /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 f4c784d952abc..c4b48a06d6cee 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 63696adcbfa78..625455aab89db 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 5ee78c5d49f99..21df5f4ed9f6a 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 861bba12b6dc8..1f03eb8220f39 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 d493a20c70c4c..203581a2bf0d6 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 a9c7895781665..a1c07e7413501 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 1f728571bd806..5330e055ee3c3 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 0f17c2cabc8f9..1356e94825ccc 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 14e2c5e5941ec..8551aa016e007 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 b7bd3e892c806..6287911e06249 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 98df8098ca7eb..51176cb42b240 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 f431074d0e2a8..17b266406e392 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 17f626d410c5e..8d42322c09906 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 4cccac78600f8..c0a4a76780fa2 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 0000000000000..299c64bf8fdab --- /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"), + } + } +}