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.
This commit is contained in:
Patrick Walton 2024-06-27 09:13:03 -07:00 committed by GitHub
parent 1baa1a11b7
commit 44db8b7fac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 647 additions and 86 deletions

View file

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

View file

@ -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<f32>,
// The color of the vertex.
@location(1) color: vec3<f32>,
};
// Information passed from the vertex shader to the fragment shader.
struct VertexOutput {
// The clip-space position of the vertex.
@builtin(position) clip_position: vec4<f32>,
// The color of the vertex.
@location(0) color: vec3<f32>,
};
// 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<f32> {
return vec4(vertex_output.color, 1.0);
}

View file

@ -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<std::cmp::Ordering> {
if self.type_id() != other.type_id() {
None
} else {
Some(self.internal().cmp(&other.internal()))
}
Some(self.cmp(other))
}
}

View file

@ -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<Mesh>,
/// 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.
///

View file

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

View file

@ -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<Mesh>,
/// The ID of the asset.
pub asset_id: UntypedAssetId,
/// The ID of a bind group specific to the material.
///

View file

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

View file

@ -763,11 +763,15 @@ pub fn queue_material_meshes<M: Material>(
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<M: Material>(
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()),
);
}
}

View file

@ -860,22 +860,22 @@ pub fn queue_prepass_material_meshes<M: Material>(
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<M: Material>(
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()),
);
}
}

View file

@ -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<M: Material>(
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<Mesh>,
/// The object.
pub asset_id: UntypedAssetId,
}
impl PhaseItem for Shadow {

View file

@ -523,9 +523,9 @@ pub fn batch_and_prepare_binned_render_phase<BPI, GFBD>(
// Prepare batchables.
for key in &phase.batchable_keys {
for key in &phase.batchable_mesh_keys {
let mut batch: Option<BinnedRenderPhaseBatch> = 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<BPI, GFBD>(
}
// 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;

View file

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

View file

@ -104,9 +104,9 @@ pub fn batch_and_prepare_binned_render_phase<BPI, GFBD>(
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<BPI, GFBD>(
}
// 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 {

View file

@ -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<BPI::BinKey>,
pub batchable_mesh_keys: Vec<BPI::BinKey>,
/// The batchable bins themselves.
///
/// Each bin corresponds to a single batch set. For unbatchable entities,
/// prefer `unbatchable_values` instead.
pub(crate) batchable_values: HashMap<BPI::BinKey, Vec<Entity>>,
pub(crate) batchable_mesh_values: HashMap<BPI::BinKey, Vec<Entity>>,
/// 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<BPI::BinKey>,
pub unbatchable_mesh_keys: Vec<BPI::BinKey>,
/// The unbatchable bins.
///
/// Each entity here is rendered in a separate drawcall.
pub(crate) unbatchable_values: HashMap<BPI::BinKey, UnbatchableBinnedEntities>,
pub(crate) unbatchable_mesh_values: HashMap<BPI::BinKey, UnbatchableBinnedEntities>,
/// 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<T> From<GpuArrayBufferIndex<T>> 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::<DrawFunctions<BPI>>();
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::<DrawFunctions<BPI>>();
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::<DrawFunctions<BPI>>();
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::<DrawFunctions<BPI>>();
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
}
}
}

View file

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

View file

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

View file

@ -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<Shader>,
}
/// A [`RenderCommand`] that binds the vertex and index buffers and issues the
/// draw command for our custom phase item.
struct DrawCustomPhaseItem;
impl<P> RenderCommand<P> for DrawCustomPhaseItem
where
P: PhaseItem,
{
type Param = SRes<CustomPhaseItemBuffers>;
type ViewQuery = ();
type ItemQuery = ();
fn render<'w>(
_: &P,
_: ROQueryItem<'w, Self::ViewQuery>,
_: Option<ROQueryItem<'w, Self::ItemQuery>>,
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<Vertex>,
/// The indices of the single triangle.
///
/// As above, this is a [`RawBufferVec`] because `u32` values have trivial
/// size and alignment.
indices: RawBufferVec<u32>,
}
/// 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<CustomRenderedEntity>;
/// 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::<CustomRenderedEntity>::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::<WithCustomRenderedEntity>
.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::<CustomPhasePipeline>()
.init_resource::<SpecializedRenderPipelines<CustomPhasePipeline>>()
.add_render_command::<Opaque3d, DrawCustomPhaseItemCommands>()
.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::<CustomPhaseItemBuffers>();
}
/// 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<PipelineCache>,
custom_phase_pipeline: Res<CustomPhasePipeline>,
msaa: Res<Msaa>,
mut opaque_render_phases: ResMut<ViewBinnedRenderPhases<Opaque3d>>,
opaque_draw_functions: Res<DrawFunctions<Opaque3d>>,
mut specialized_render_pipelines: ResMut<SpecializedRenderPipelines<CustomPhasePipeline>>,
views: Query<(Entity, &VisibleEntities), With<ExtractedView>>,
) {
let draw_custom_phase_item = opaque_draw_functions
.read()
.id::<DrawCustomPhaseItemCommands>();
// 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::<WithCustomRenderedEntity>()
.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::<Mesh>::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::<Vertex>() 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::<RenderDevice>();
let render_queue = world.resource::<RenderQueue>();
// 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::<AssetServer>();
CustomPhasePipeline {
shader: asset_server.load("shaders/custom_phase_item.wgsl"),
}
}
}