Skip to content
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1172,6 +1172,17 @@ category = "3D Rendering"
# TAA not supported by WebGL
wasm = false

[[example]]
name = "only_shadow_caster"
path = "examples/3d/only_shadow_caster.rs"
doc-scrape-examples = true

[package.metadata.example.only_shadow_caster]
name = "Only Shadow Caster"
description = "Makes an entity invisible to the main camera while still casting shadows"
category = "3D Rendering"
wasm = true

[[example]]
name = "atmospheric_fog"
path = "examples/3d/atmospheric_fog.rs"
Expand Down
17 changes: 17 additions & 0 deletions crates/bevy_pbr/src/material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,7 @@ pub fn queue_material_meshes(
render_materials: Res<ErasedRenderAssets<PreparedMaterial>>,
render_mesh_instances: Res<RenderMeshInstances>,
render_material_instances: Res<RenderMaterialInstances>,
render_passes: Query<&RenderPasses>,
mesh_allocator: Res<MeshAllocator>,
gpu_preprocessing_support: Res<GpuPreprocessingSupport>,
mut opaque_render_phases: ResMut<ViewBinnedRenderPhases<Opaque3d>>,
Expand Down Expand Up @@ -1186,6 +1187,10 @@ pub fn queue_material_meshes(

let rangefinder = view.rangefinder3d();
for (render_entity, visible_entity) in visible_entities.iter::<Mesh3d>() {
let pass_mask = render_passes
.get(*render_entity)
.map_or(RenderPassMask::ALL, |passes| passes.0);

let Some((current_change_tick, pipeline_id)) = view_specialized_material_pipeline_cache
.get(visible_entity)
.map(|(current_change_tick, pipeline_id)| (*current_change_tick, *pipeline_id))
Expand Down Expand Up @@ -1217,6 +1222,9 @@ pub fn queue_material_meshes(

match material.properties.render_phase_type {
RenderPhaseType::Transmissive => {
if !pass_mask.contains(RenderPassMask::TRANSMISSIVE_MAIN) {
continue;
}
let distance = rangefinder.distance(&mesh_instance.center)
+ material.properties.depth_bias;
let Some(draw_function) = material
Expand All @@ -1236,6 +1244,9 @@ pub fn queue_material_meshes(
});
}
RenderPhaseType::Opaque => {
if !pass_mask.contains(RenderPassMask::OPAQUE_MAIN) {
continue;
}
if material.properties.render_method == OpaqueRendererMethod::Deferred {
// Even though we aren't going to insert the entity into
// a bin, we still want to update its cache entry. That
Expand Down Expand Up @@ -1275,6 +1286,9 @@ pub fn queue_material_meshes(
}
// Alpha mask
RenderPhaseType::AlphaMask => {
if !pass_mask.contains(RenderPassMask::ALPHA_MASK_MAIN) {
continue;
}
let Some(draw_function) = material
.properties
.get_draw_function(MainPassAlphaMaskDrawFunction)
Expand Down Expand Up @@ -1304,6 +1318,9 @@ pub fn queue_material_meshes(
);
}
RenderPhaseType::Transparent => {
if !pass_mask.contains(RenderPassMask::TRANSPARENT_MAIN) {
continue;
}
let distance = rangefinder.distance(&mesh_instance.center)
+ material.properties.depth_bias;
let Some(draw_function) = material
Expand Down
20 changes: 19 additions & 1 deletion crates/bevy_pbr/src/prepass/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ use bevy_render::{
ExtractedView, Msaa, RenderVisibilityRanges, RetainedViewEntity, ViewUniform,
ViewUniformOffset, ViewUniforms, VISIBILITY_RANGES_STORAGE_BUFFER_COUNT,
},
Extract, ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderStartup, RenderSystems,
Extract, ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderPassMask, RenderPasses,
RenderStartup, RenderSystems,
};
use bevy_shader::{load_shader_library, Shader, ShaderDefVal};
use bevy_transform::prelude::GlobalTransform;
Expand Down Expand Up @@ -1009,6 +1010,7 @@ pub fn queue_prepass_material_meshes(
render_mesh_instances: Res<RenderMeshInstances>,
render_materials: Res<ErasedRenderAssets<PreparedMaterial>>,
render_material_instances: Res<RenderMaterialInstances>,
render_passes: Query<&RenderPasses>,
mesh_allocator: Res<MeshAllocator>,
gpu_preprocessing_support: Res<GpuPreprocessingSupport>,
mut opaque_prepass_render_phases: ResMut<ViewBinnedRenderPhases<Opaque3dPrepass>>,
Expand Down Expand Up @@ -1047,6 +1049,10 @@ pub fn queue_prepass_material_meshes(
}

for (render_entity, visible_entity) in visible_entities.iter::<Mesh3d>() {
let pass_mask = render_passes
.get(*render_entity)
.map_or(RenderPassMask::ALL, |passes| passes.0);

let Some((current_change_tick, pipeline_id)) =
view_specialized_material_pipeline_cache.get(visible_entity)
else {
Expand Down Expand Up @@ -1088,6 +1094,9 @@ pub fn queue_prepass_material_meshes(
match material.properties.render_phase_type {
RenderPhaseType::Opaque => {
if deferred {
if !pass_mask.contains(RenderPassMask::OPAQUE_MAIN) {
continue;
}
let Some(draw_function) = material
.properties
.get_draw_function(DeferredOpaqueDrawFunction)
Expand All @@ -1114,6 +1123,9 @@ pub fn queue_prepass_material_meshes(
*current_change_tick,
);
} else if let Some(opaque_phase) = opaque_phase.as_mut() {
if !pass_mask.intersects(RenderPassMask::PREPASS) {
continue;
}
let (vertex_slab, index_slab) =
mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id);
let Some(draw_function) = material
Expand Down Expand Up @@ -1145,6 +1157,9 @@ pub fn queue_prepass_material_meshes(
}
RenderPhaseType::AlphaMask => {
if deferred {
if !pass_mask.contains(RenderPassMask::ALPHA_MASK_MAIN) {
continue;
}
let (vertex_slab, index_slab) =
mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id);
let Some(draw_function) = material
Expand Down Expand Up @@ -1175,6 +1190,9 @@ pub fn queue_prepass_material_meshes(
*current_change_tick,
);
} else if let Some(alpha_mask_phase) = alpha_mask_phase.as_mut() {
if !pass_mask.intersects(RenderPassMask::PREPASS) {
continue;
}
let (vertex_slab, index_slab) =
mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id);
let Some(draw_function) = material
Expand Down
8 changes: 8 additions & 0 deletions crates/bevy_pbr/src/render/light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ use bevy_render::{
camera::SortedCameras,
mesh::allocator::MeshAllocator,
view::{NoIndirectDrawing, RetainedViewEntity},
RenderPassMask, RenderPasses,
};
use bevy_render::{
diagnostic::RecordDiagnostics,
Expand Down Expand Up @@ -1946,6 +1947,7 @@ pub fn queue_shadows(
render_mesh_instances: Res<RenderMeshInstances>,
render_materials: Res<ErasedRenderAssets<PreparedMaterial>>,
render_material_instances: Res<RenderMaterialInstances>,
render_passes: Query<&RenderPasses>,
mut shadow_render_phases: ResMut<ViewBinnedRenderPhases<Shadow>>,
gpu_preprocessing_support: Res<GpuPreprocessingSupport>,
mesh_allocator: Res<MeshAllocator>,
Expand Down Expand Up @@ -2003,6 +2005,12 @@ pub fn queue_shadows(
};

for (entity, main_entity) in visible_entities.iter().copied() {
if let Ok(render_passes) = render_passes.get(entity)
&& !render_passes.0.contains(RenderPassMask::SHADOW)
{
continue;
}

let Some((current_change_tick, pipeline_id)) =
view_specialized_material_pipeline_cache.get(&main_entity)
else {
Expand Down
7 changes: 7 additions & 0 deletions crates/bevy_pbr/src/wireframe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,7 @@ pub fn specialize_wireframes(
fn queue_wireframes(
custom_draw_functions: Res<DrawFunctions<Wireframe3d>>,
render_mesh_instances: Res<RenderMeshInstances>,
render_passes: Query<&RenderPasses>,
gpu_preprocessing_support: Res<GpuPreprocessingSupport>,
mesh_allocator: Res<MeshAllocator>,
specialized_wireframe_pipeline_cache: Res<SpecializedWireframePipelineCache>,
Expand All @@ -883,6 +884,12 @@ fn queue_wireframes(
};

for (render_entity, visible_entity) in visible_entities.iter::<Mesh3d>() {
if let Ok(render_passes) = render_passes.get(*render_entity)
&& !render_passes.0.contains(RenderPassMask::MAIN)
{
continue;
}

let Some(wireframe_instance) = render_wireframe_instances.get(visible_entity) else {
continue;
};
Expand Down
105 changes: 103 additions & 2 deletions crates/bevy_render/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,14 @@ pub mod view;
pub mod prelude {
#[doc(hidden)]
pub use crate::{
alpha::AlphaMode, camera::NormalizedRenderTargetExt as _, texture::ManualTextureViews,
view::Msaa, ExtractSchedule,
alpha::AlphaMode,
camera::NormalizedRenderTargetExt as _,
texture::ManualTextureViews,
view::Msaa,
ExtractSchedule,
// Render pass participation mask component and flags
RenderPassMask,
RenderPasses,
};
}

Expand All @@ -92,6 +98,7 @@ use alloc::sync::Arc;
use batching::gpu_preprocessing::BatchingPlugin;
use bevy_app::{App, AppLabel, Plugin, SubApp};
use bevy_asset::{AssetApp, AssetServer};
use bevy_ecs::query::QueryItem;
use bevy_ecs::{
prelude::*,
schedule::{ScheduleBuildSettings, ScheduleLabel},
Expand Down Expand Up @@ -143,6 +150,75 @@ bitflags! {
}
}

bitflags! {
/// Which render passes an entity participates in.
///
/// This mask can be used to exclude an entity from specific render
/// passes such as the main camera pass, prepasses, or shadow passes.
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
pub struct RenderPassMask: u8 {
/// No participation in any pass.
const NONE = 0;
/// Participation in the main opaque pass.
const OPAQUE_MAIN = 1 << 0;
/// Participation in the main alpha-mask pass.
const ALPHA_MASK_MAIN = 1 << 1;
/// Participation in the main transparent pass.
const TRANSPARENT_MAIN = 1 << 2;
/// Participation in the main transmissive pass.
const TRANSMISSIVE_MAIN = 1 << 3;

/// Participation in the depth prepass.
const DEPTH_PREPASS = 1 << 4;
/// Participation in the normal prepass.
const NORMAL_PREPASS = 1 << 5;
/// Participation in the motion vector prepass.
const MOTION_VECTOR_PREPASS = 1 << 6;

/// Participation in shadow-view passes.
const SHADOW = 1 << 7;

/// Participation in main camera passes.
const MAIN = Self::OPAQUE_MAIN.bits()
| Self::ALPHA_MASK_MAIN.bits()
| Self::TRANSPARENT_MAIN.bits()
| Self::TRANSMISSIVE_MAIN.bits();

/// Participation in prepasses (depth / normal / motion vectors).
///
/// Note: today, Bevy's "prepass" is commonly executed as a single pass that can write
/// multiple outputs depending on view configuration. These bits exist to allow more
/// fine-grained control and future expansion.
const PREPASS = Self::DEPTH_PREPASS.bits()
| Self::NORMAL_PREPASS.bits()
| Self::MOTION_VECTOR_PREPASS.bits();
/// Participation in all known passes.
const ALL = Self::MAIN.bits() | Self::PREPASS.bits() | Self::SHADOW.bits();
}
}

/// Component to control which render passes this entity should be queued into.
///
/// Defaults to `RenderPassMask::ALL` (participates in all passes).
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug)]
pub struct RenderPasses(pub RenderPassMask);

impl Default for RenderPasses {
fn default() -> Self {
Self(RenderPassMask::ALL)
}
}

impl extract_component::ExtractComponent for RenderPasses {
type QueryData = &'static Self;
type QueryFilter = ();
type Out = Self;

fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option<Self::Out> {
Some(*item)
}
}

/// The systems sets of the default [`App`] rendering schedule.
///
/// These can be useful for ordering, but you almost never want to add your systems to these sets.
Expand Down Expand Up @@ -363,6 +439,7 @@ impl Plugin for RenderPlugin {
WindowRenderPlugin,
CameraPlugin,
ViewPlugin,
extract_component::ExtractComponentPlugin::<RenderPasses>::default(),
MeshRenderAssetPlugin,
GlobalsPlugin,
#[cfg(feature = "morph")]
Expand Down Expand Up @@ -443,6 +520,30 @@ impl Plugin for RenderPlugin {
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn render_passes_default_is_all() {
assert_eq!(RenderPasses::default().0, RenderPassMask::ALL);
}

#[test]
fn render_pass_mask_all_contains_expected_bits() {
assert!(RenderPassMask::ALL.contains(RenderPassMask::OPAQUE_MAIN));
assert!(RenderPassMask::ALL.contains(RenderPassMask::ALPHA_MASK_MAIN));
assert!(RenderPassMask::ALL.contains(RenderPassMask::TRANSPARENT_MAIN));
assert!(RenderPassMask::ALL.contains(RenderPassMask::TRANSMISSIVE_MAIN));
assert!(RenderPassMask::ALL.contains(RenderPassMask::MAIN));
assert!(RenderPassMask::ALL.contains(RenderPassMask::DEPTH_PREPASS));
assert!(RenderPassMask::ALL.contains(RenderPassMask::NORMAL_PREPASS));
assert!(RenderPassMask::ALL.contains(RenderPassMask::MOTION_VECTOR_PREPASS));
assert!(RenderPassMask::ALL.contains(RenderPassMask::PREPASS));
assert!(RenderPassMask::ALL.contains(RenderPassMask::SHADOW));
}
}

/// A "scratch" world used to avoid allocating new worlds every frame when
/// swapping out the [`MainWorld`] for [`ExtractSchedule`].
#[derive(Resource, Default)]
Expand Down
Loading