//! This example demonstrates how to write a custom phase //! //! Render phases in bevy are used whenever you need to draw a group of meshes in a specific way. //! For example, bevy's main pass has an opaque phase, a transparent phase for both 2d and 3d. //! Sometimes, you may want to only draw a subset of meshes before or after the builtin phase. In //! those situations you need to write your own phase. //! //! This example showcases how writing a custom phase to draw a stencil of a bevy mesh could look //! like. Some shortcuts have been used for simplicity. //! //! This example was made for 3d, but a 2d equivalent would be almost identical. use std::ops::Range; use bevy::camera::Viewport; use bevy::core_pipeline::core_3d::TransparentSortingInfo3d; use bevy::mesh::{MeshAttributeCompressionFlags, MeshCompressionArgs}; use bevy::pbr::{self, MeshPipelineSystems, SetMeshViewEmptyBindGroup, ViewKeyCache}; use bevy::{ camera::MainPassResolutionOverride, core_pipeline::{core_3d::main_opaque_pass_3d, schedule::Core3d, Core3dSystems}, ecs::{ entity::EntityHash, system::{lifetimeless::SRes, SystemParamItem}, }, math::FloatOrd, mesh::MeshVertexBufferLayoutRef, pbr::{ DrawMesh, MeshInputUniform, MeshPipeline, MeshPipelineKey, MeshPipelineViewLayoutKey, MeshUniform, RenderMeshInstances, SetMeshBindGroup, SetMeshViewBindGroup, }, platform::collections::HashSet, prelude::*, render::{ batching::{ gpu_preprocessing::{ batch_and_prepare_sorted_render_phase, BatchedInstanceBuffers, IndirectParametersMetadata, UntypedPhaseIndirectParametersBuffers, }, GetBatchData, GetFullBatchData, }, camera::{DirtySpecializations, ExtractedCamera, PendingQueues}, extract_component::{ExtractComponent, ExtractComponentPlugin}, mesh::{allocator::MeshAllocator, RenderMesh}, render_asset::RenderAssets, render_phase::{ sort_phase_system, AddRenderCommand, CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem, PhaseItemExtraIndex, SetItemPipeline, SortedPhaseItem, SortedRenderPhasePlugin, ViewSortedRenderPhases, }, render_resource::{ CachedRenderPipelineId, ColorTargetState, ColorWrites, Face, FragmentState, PipelineCache, PrimitiveState, RenderPassDescriptor, RenderPipelineDescriptor, SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines, VertexState, }, renderer::{RenderContext, ViewQuery}, sync_world::{MainEntity, MainEntityHashSet}, view::{ExtractedView, RenderVisibleEntities, RetainedViewEntity, ViewTarget}, Extract, Render, RenderApp, RenderDebugFlags, RenderStartup, RenderSystems, }, }; use indexmap::IndexMap; use nonmax::NonMaxU32; const SHADER_ASSET_PATH: &str = "shaders/custom_stencil.wesl"; fn main() { App::new() .add_plugins((DefaultPlugins, MeshStencilPhasePlugin)) .add_systems(Startup, setup) .run(); } fn setup( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, ) { // circular base commands.spawn(( Mesh3d(meshes.add(Circle::new(4.0))), MeshMaterial3d(materials.add(Color::WHITE)), Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), )); // cube // This cube will be rendered by the main pass, but it will also be rendered by our custom // pass. This should result in an unlit red cube commands.spawn(( Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))), Transform::from_xyz(0.0, 0.5, 0.0), // This marker component is used to identify which mesh will be used in our custom pass // The circle doesn't have it so it won't be rendered in our pass DrawStencil, )); commands.spawn(( Mesh3d( meshes.add( Sphere::new(0.5) .mesh() .build() .compressed_mesh(&MeshCompressionArgs::regular()) .unwrap(), ), ), MeshMaterial3d(materials.add(Color::srgb_u8(124, 255, 144))), Transform::from_xyz(2.0, 0.5, 0.0), // This marker component is used to identify which mesh will be used in our custom pass // The circle doesn't have it so it won't be rendered in our pass DrawStencil, )); // light commands.spawn(( PointLight { shadow_maps_enabled: true, ..default() }, Transform::from_xyz(4.0, 8.0, 4.0), )); // camera commands.spawn(( Camera3d::default(), Transform::from_xyz(-2.0, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), // disable msaa for simplicity Msaa::Off, )); } #[derive(Component, ExtractComponent, Clone, Copy, Default)] #[extract_app(RenderApp)] struct DrawStencil; struct MeshStencilPhasePlugin; impl Plugin for MeshStencilPhasePlugin { fn build(&self, app: &mut App) { app.add_plugins(( ExtractComponentPlugin::::default(), SortedRenderPhasePlugin::::new(RenderDebugFlags::default()), )); // We need to get the render app from the main app let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; render_app .init_resource::>() .init_resource::>() .add_render_command::() .init_resource::>() .init_resource::() .add_systems( RenderStartup, init_stencil_pipeline.after(MeshPipelineSystems), ) .add_systems(ExtractSchedule, extract_camera_phases) .add_systems( Render, ( queue_custom_meshes.in_set(RenderSystems::QueueMeshes), sort_phase_system::.in_set(RenderSystems::PhaseSort), batch_and_prepare_sorted_render_phase:: .in_set(RenderSystems::PrepareResources), ), ) .add_systems( Core3d, custom_draw_system .after(main_opaque_pass_3d) .in_set(Core3dSystems::MainPass), ); } } #[derive(Resource)] struct StencilPipeline { /// The base mesh pipeline defined by bevy /// /// Since we want to draw a stencil of an existing bevy mesh we want to reuse the default /// pipeline as much as possible mesh_pipeline: MeshPipeline, /// Stores the shader used for this pipeline directly on the pipeline. /// This isn't required, it's only done like this for simplicity. shader_handle: Handle, } fn init_stencil_pipeline( mut commands: Commands, mesh_pipeline: Res, asset_server: Res, ) { commands.insert_resource(StencilPipeline { mesh_pipeline: mesh_pipeline.clone(), shader_handle: asset_server.load(SHADER_ASSET_PATH), }); } // For more information on how SpecializedMeshPipeline work, please look at the // specialized_mesh_pipeline example impl SpecializedMeshPipeline for StencilPipeline { type Key = MeshPipelineKey; fn specialize( &self, key: Self::Key, layout: &MeshVertexBufferLayoutRef, ) -> Result { let mut shader_defs = Vec::new(); // We will only use the position of the mesh in our shader so we only need to specify that let mut vertex_attributes = Vec::new(); if layout.0.contains(Mesh::ATTRIBUTE_POSITION) { // Handle compressed vertex positions. if layout .0 .get_attribute_compression() .contains(MeshAttributeCompressionFlags::COMPRESS_POSITION) { shader_defs.push("VERTEX_POSITIONS_COMPRESSED".into()); } // Make sure this matches the shader location vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0)); } // This will automatically generate the correct `VertexBufferLayout` based on the vertex attributes let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?; let view_layout = self .mesh_pipeline .get_view_layout(MeshPipelineViewLayoutKey::from(key)); Ok(RenderPipelineDescriptor { label: Some("Specialized Mesh Pipeline".into()), // We want to reuse the data from bevy so we use the same bind groups as the default // mesh pipeline layout: vec![ // Bind group 0 is the view uniform view_layout.main_layout, // Bind group 1 is empty view_layout.empty_layout, // Bind group 2 is the mesh uniform self.mesh_pipeline.mesh_layouts.model_only.clone(), ], vertex: VertexState { shader: self.shader_handle.clone(), shader_defs: shader_defs.clone(), buffers: vec![vertex_buffer_layout], ..default() }, fragment: Some(FragmentState { shader: self.shader_handle.clone(), shader_defs, targets: vec![Some(ColorTargetState { format: key.target_format(), blend: None, write_mask: ColorWrites::ALL, })], ..default() }), primitive: PrimitiveState { topology: key.primitive_topology(), strip_index_format: key.strip_index_format(), cull_mode: Some(Face::Back), ..default() }, // It's generally recommended to specialize your pipeline for MSAA, // but it's not always possible ..default() }) } } // We will reuse render commands already defined by bevy to draw a 3d mesh type DrawMesh3dStencil = ( SetItemPipeline, // This will set the view bindings in group 0 SetMeshViewBindGroup<0>, // This will set an empty bind group in group 1 SetMeshViewEmptyBindGroup<1>, // This will set the mesh bindings in group 2 SetMeshBindGroup<2>, // This will draw the mesh DrawMesh, ); // This is the data required per entity drawn in a custom phase in bevy. More specifically this is the // data required when using a ViewSortedRenderPhase. This would look differently if we wanted a // batched render phase. Sorted phases are a bit easier to implement, but a batched phase would // look similar. // // If you want to see how a batched phase implementation looks, you should look at the Opaque2d // phase. struct Stencil3d { /// Information needed to sort the objects in the phase by distance to the /// view. pub sorting_info: TransparentSortingInfo3d, pub distance: FloatOrd, pub entity: (Entity, MainEntity), pub pipeline: CachedRenderPipelineId, pub draw_function: DrawFunctionId, pub batch_range: Range, pub extra_index: PhaseItemExtraIndex, /// Whether the mesh in question is indexed (uses an index buffer in /// addition to its vertex buffer). pub indexed: bool, } // For more information about writing a phase item, please look at the custom_phase_item example impl PhaseItem for Stencil3d { #[inline] fn entity(&self) -> Entity { self.entity.0 } #[inline] fn main_entity(&self) -> MainEntity { self.entity.1 } #[inline] fn draw_function(&self) -> DrawFunctionId { self.draw_function } #[inline] fn batch_range(&self) -> &Range { &self.batch_range } #[inline] fn batch_range_mut(&mut self) -> &mut Range { &mut self.batch_range } #[inline] fn extra_index(&self) -> PhaseItemExtraIndex { self.extra_index.clone() } #[inline] fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex) { (&mut self.batch_range, &mut self.extra_index) } } impl SortedPhaseItem for Stencil3d { type SortKey = FloatOrd; #[inline] fn sort_key(&self) -> Self::SortKey { self.distance } #[inline] fn sort(items: &mut IndexMap<(Entity, MainEntity), Stencil3d, EntityHash>) { items.sort_by_key(|_, phase_item: &Stencil3d| phase_item.distance); } fn recalculate_sort_keys( items: &mut IndexMap<(Entity, MainEntity), Self, EntityHash>, view: &ExtractedView, ) { // Determine the distance to the view for each phase item. let rangefinder = view.rangefinder3d(); for item in items.values_mut() { item.distance = FloatOrd(item.sorting_info.sort_distance(&rangefinder)); } } #[inline] fn indexed(&self) -> bool { self.indexed } } impl CachedRenderPipelinePhaseItem for Stencil3d { #[inline] fn cached_pipeline(&self) -> CachedRenderPipelineId { self.pipeline } } impl GetBatchData for StencilPipeline { type Param = (SRes, SRes); // Placing `AssetId` in the batch set compare data prevents Bevy from // trying to multi-draw items with different meshes together. This is fine // for this simple example. type BatchSetCompareData = AssetId; type BatchCompareData = (); type BufferData = MeshUniform; fn get_batch_data( (mesh_instances, mesh_allocator): &SystemParamItem, (_entity, main_entity): (Entity, MainEntity), ) -> Option<( Self::BufferData, Option<(Self::BatchSetCompareData, Self::BatchCompareData)>, )> { let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else { error!( "`get_batch_data` should never be called in GPU mesh uniform \ building mode" ); return None; }; let mesh_instance = mesh_instances.get(&main_entity)?; let first_vertex_index = match mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id()) { Some(mesh_vertex_slice) => mesh_vertex_slice.range.start, None => 0, }; let metadata_index = mesh_allocator .mesh_metadata_slice(&mesh_instance.mesh_asset_id()) .map(|mesh_metadata_slice| mesh_metadata_slice.range.start); let mesh_uniform = MeshUniform::new( &mesh_instance.transforms, first_vertex_index, mesh_instance.material_bindings_index().slot, None, None, None, Some(mesh_instance.tag()), metadata_index, ); Some((mesh_uniform, None)) } } impl GetFullBatchData for StencilPipeline { type BufferInputData = MeshInputUniform; fn get_index_and_compare_data( (mesh_instances, _): &SystemParamItem, main_entity: MainEntity, ) -> Option<( NonMaxU32, Option<(Self::BatchSetCompareData, Self::BatchCompareData)>, )> { // This should only be called during GPU building. let RenderMeshInstances::GpuBuilding(ref mesh_instances) = **mesh_instances else { error!( "`get_index_and_compare_data` should never be called in CPU mesh uniform building \ mode" ); return None; }; let mesh_instance = mesh_instances.get(&main_entity)?; Some(( NonMaxU32::new(mesh_instance.gpu_specific.current_uniform_index())?, mesh_instance .should_batch() .then_some((mesh_instance.mesh_asset_id(), ())), )) } fn get_binned_batch_data( (mesh_instances, mesh_allocator): &SystemParamItem, main_entity: MainEntity, ) -> Option { let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else { error!( "`get_binned_batch_data` should never be called in GPU mesh uniform building mode" ); return None; }; let mesh_instance = mesh_instances.get(&main_entity)?; let first_vertex_index = match mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id()) { Some(mesh_vertex_slice) => mesh_vertex_slice.range.start, None => 0, }; let metadata_index = mesh_allocator .mesh_metadata_slice(&mesh_instance.mesh_asset_id()) .map(|mesh_metadata_slice| mesh_metadata_slice.range.start); Some(MeshUniform::new( &mesh_instance.transforms, first_vertex_index, mesh_instance.material_bindings_index().slot, None, None, None, Some(mesh_instance.tag()), metadata_index, )) } fn write_batch_indirect_parameters_metadata( indexed: bool, base_output_index: u32, batch_set_index: Option, indirect_parameters_buffers: &mut UntypedPhaseIndirectParametersBuffers, indirect_parameters_offset: u32, ) { // Note that `IndirectParameters` covers both of these structures, even // though they actually have distinct layouts. See the comment above that // type for more information. let indirect_parameters = IndirectParametersMetadata { base_output_index, batch_set_index: match batch_set_index { None => !0, Some(batch_set_index) => u32::from(batch_set_index), }, // These fields are filled in by the GPU: mesh_index: 0, early_instance_count: 0, late_instance_count: 0, }; if indexed { indirect_parameters_buffers .indexed .set(indirect_parameters_offset, indirect_parameters); } else { indirect_parameters_buffers .non_indexed .set(indirect_parameters_offset, indirect_parameters); } } fn get_binned_index( _param: &SystemParamItem, _query_item: MainEntity, ) -> Option { None } } // When defining a phase, we need to extract it from the main world and add it to a resource // that will be used by the render world. We need to give that resource all views that will use // that phase fn extract_camera_phases( mut stencil_phases: ResMut>, cameras: Extract>>, mut live_entities: Local>, ) { live_entities.clear(); for (main_entity, camera) in &cameras { if !camera.is_active { continue; } // This is the main camera, so we use the first subview index (0) let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0); stencil_phases.prepare_for_new_frame(retained_view_entity); live_entities.insert(retained_view_entity); } // Clear out all dead views. stencil_phases.retain(|camera_entity, _| live_entities.contains(camera_entity)); } /// A resource that stores meshes that couldn't be specialized yet because their /// materials hadn't loaded. /// /// See the documentation for [`PendingQueues`] for more information. #[derive(Default, Deref, DerefMut, Resource)] struct PendingCustomMeshQueues(pub PendingQueues); // This is a very important step when writing a custom phase. // // This system determines which meshes will be added to the phase. fn queue_custom_meshes( custom_draw_functions: Res>, mut pipelines: ResMut>, pipeline_cache: Res, custom_draw_pipeline: Res, render_meshes: Res>, render_mesh_instances: Res, maybe_batched_instance_buffers: Option< Res>, >, mut custom_render_phases: ResMut>, mut views: Query<(&ExtractedView, &RenderVisibleEntities)>, view_key_cache: Res, dirty_specializations: Res, mut pending_custom_mesh_queues: ResMut, has_marker: Query<(), With>, mut mesh_instances_queued_this_iteration_scratch_space: Local, ) { for (view, visible_entities) in &mut views { let Some(custom_phase) = custom_render_phases.get_mut(&view.retained_view_entity) else { continue; }; let draw_custom = custom_draw_functions.read().id::(); let Some(&view_key) = view_key_cache.get(&view.retained_view_entity) else { continue; }; // Since our phase can work on any 3d mesh we can reuse the default mesh 3d filter let Some(render_visible_mesh_entities) = visible_entities.get::() else { continue; }; let view_pending_custom_mesh_queues = pending_custom_mesh_queues.prepare_for_new_frame(view.retained_view_entity); // First, remove meshes that need to be respecialized, and those that were removed, from the bins. for &main_entity in dirty_specializations .iter_to_dequeue(view.retained_view_entity, render_visible_mesh_entities) { custom_phase.remove(Entity::PLACEHOLDER, main_entity); } for (render_entity, visible_entity) in dirty_specializations.iter_to_queue( view.retained_view_entity, render_visible_mesh_entities, &view_pending_custom_mesh_queues.prev_frame, &mut mesh_instances_queued_this_iteration_scratch_space, ) { // We only want meshes with the marker component to be queued to our phase. if has_marker.get(*render_entity).is_err() { continue; } let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) else { // We couldn't fetch the mesh, probably because it hasn't been // loaded yet. Add the entity to the list of pending custom mesh // queues and bail. view_pending_custom_mesh_queues .current_frame .insert((*render_entity, *visible_entity)); continue; }; let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id()) else { continue; }; // Specialize the key for the current mesh entity // For this example we only specialize based on the mesh topology // but you could have more complex keys and that's where you'd need to create those keys let mut mesh_key = view_key; mesh_key |= MeshPipelineKey::from_primitive_topology_and_strip_index( mesh.primitive_topology(), mesh.index_format(), ); let pipeline_id = pipelines.specialize( &pipeline_cache, &custom_draw_pipeline, mesh_key, &mesh.layout, ); let pipeline_id = match pipeline_id { Ok(id) => id, Err(err) => { error!("{}", err); continue; } }; // At this point we have all the data we need to create a phase item and add it to our // phase custom_phase.add_retained(Stencil3d { sorting_info: TransparentSortingInfo3d::Sorted { mesh_center: pbr::get_mesh_instance_world_from_local( *visible_entity, mesh_instance.current_uniform_index, &render_mesh_instances, maybe_batched_instance_buffers.as_deref(), ) .transform_point3( render_meshes .get(mesh_instance.mesh_asset_id()) .unwrap() .aabb_center, ), depth_bias: 0.0, }, distance: FloatOrd(0.0), entity: (Entity::PLACEHOLDER, *visible_entity), pipeline: pipeline_id, draw_function: draw_custom, // Sorted phase items aren't batched batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, indexed: mesh.indexed(), }); } } } fn custom_draw_system( world: &World, view: ViewQuery<( &ExtractedCamera, &ExtractedView, &ViewTarget, Option<&MainPassResolutionOverride>, )>, stencil_phases: Res>, mut ctx: RenderContext, ) { let view_entity = view.entity(); let (camera, extracted_view, target, resolution_override) = view.into_inner(); let Some(stencil_phase) = stencil_phases.get(&extracted_view.retained_view_entity) else { return; }; let mut render_pass = ctx.begin_tracked_render_pass(RenderPassDescriptor { label: Some("stencil pass"), // For the purpose of the example, we will write directly to the view target. A real // stencil pass would write to a custom texture and that texture would be used in later // passes to render custom effects using it. color_attachments: &[Some(target.get_color_attachment())], // We don't bind any depth buffer for this pass depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, multiview_mask: None, }); if let Some(viewport) = Viewport::from_viewport_and_override(camera.viewport.as_ref(), resolution_override) { render_pass.set_camera_viewport(&viewport); } if let Err(err) = stencil_phase.render(&mut render_pass, world, view_entity) { error!("Error encountered while rendering the stencil phase {err:?}"); } }