Path: blob/main/examples/shader_advanced/custom_render_phase.rs
6849 views
//! This example demonstrates how to write a custom phase1//!2//! Render phases in bevy are used whenever you need to draw a group of meshes in a specific way.3//! For example, bevy's main pass has an opaque phase, a transparent phase for both 2d and 3d.4//! Sometimes, you may want to only draw a subset of meshes before or after the builtin phase. In5//! those situations you need to write your own phase.6//!7//! This example showcases how writing a custom phase to draw a stencil of a bevy mesh could look8//! like. Some shortcuts have been used for simplicity.9//!10//! This example was made for 3d, but a 2d equivalent would be almost identical.1112use std::ops::Range;1314use bevy::camera::Viewport;15use bevy::pbr::SetMeshViewEmptyBindGroup;16use bevy::{17camera::MainPassResolutionOverride,18core_pipeline::core_3d::graph::{Core3d, Node3d},19ecs::{20query::QueryItem,21system::{lifetimeless::SRes, SystemParamItem},22},23math::FloatOrd,24mesh::MeshVertexBufferLayoutRef,25pbr::{26DrawMesh, MeshInputUniform, MeshPipeline, MeshPipelineKey, MeshPipelineViewLayoutKey,27MeshUniform, RenderMeshInstances, SetMeshBindGroup, SetMeshViewBindGroup,28},29platform::collections::HashSet,30prelude::*,31render::{32batching::{33gpu_preprocessing::{34batch_and_prepare_sorted_render_phase, IndirectParametersCpuMetadata,35UntypedPhaseIndirectParametersBuffers,36},37GetBatchData, GetFullBatchData,38},39camera::ExtractedCamera,40extract_component::{ExtractComponent, ExtractComponentPlugin},41mesh::{allocator::MeshAllocator, RenderMesh},42render_asset::RenderAssets,43render_graph::{44NodeRunError, RenderGraphContext, RenderGraphExt, RenderLabel, ViewNode, ViewNodeRunner,45},46render_phase::{47sort_phase_system, AddRenderCommand, CachedRenderPipelinePhaseItem, DrawFunctionId,48DrawFunctions, PhaseItem, PhaseItemExtraIndex, SetItemPipeline, SortedPhaseItem,49SortedRenderPhasePlugin, ViewSortedRenderPhases,50},51render_resource::{52CachedRenderPipelineId, ColorTargetState, ColorWrites, Face, FragmentState,53PipelineCache, PrimitiveState, RenderPassDescriptor, RenderPipelineDescriptor,54SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines,55TextureFormat, VertexState,56},57renderer::RenderContext,58sync_world::MainEntity,59view::{ExtractedView, RenderVisibleEntities, RetainedViewEntity, ViewTarget},60Extract, Render, RenderApp, RenderDebugFlags, RenderStartup, RenderSystems,61},62};63use nonmax::NonMaxU32;6465const SHADER_ASSET_PATH: &str = "shaders/custom_stencil.wgsl";6667fn main() {68App::new()69.add_plugins((DefaultPlugins, MeshStencilPhasePlugin))70.add_systems(Startup, setup)71.run();72}7374fn setup(75mut commands: Commands,76mut meshes: ResMut<Assets<Mesh>>,77mut materials: ResMut<Assets<StandardMaterial>>,78) {79// circular base80commands.spawn((81Mesh3d(meshes.add(Circle::new(4.0))),82MeshMaterial3d(materials.add(Color::WHITE)),83Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),84));85// cube86// This cube will be rendered by the main pass, but it will also be rendered by our custom87// pass. This should result in an unlit red cube88commands.spawn((89Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),90MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),91Transform::from_xyz(0.0, 0.5, 0.0),92// This marker component is used to identify which mesh will be used in our custom pass93// The circle doesn't have it so it won't be rendered in our pass94DrawStencil,95));96// light97commands.spawn((98PointLight {99shadows_enabled: true,100..default()101},102Transform::from_xyz(4.0, 8.0, 4.0),103));104// camera105commands.spawn((106Camera3d::default(),107Transform::from_xyz(-2.0, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),108// disable msaa for simplicity109Msaa::Off,110));111}112113#[derive(Component, ExtractComponent, Clone, Copy, Default)]114struct DrawStencil;115116struct MeshStencilPhasePlugin;117impl Plugin for MeshStencilPhasePlugin {118fn build(&self, app: &mut App) {119app.add_plugins((120ExtractComponentPlugin::<DrawStencil>::default(),121SortedRenderPhasePlugin::<Stencil3d, MeshPipeline>::new(RenderDebugFlags::default()),122));123// We need to get the render app from the main app124let Some(render_app) = app.get_sub_app_mut(RenderApp) else {125return;126};127render_app128.init_resource::<SpecializedMeshPipelines<StencilPipeline>>()129.init_resource::<DrawFunctions<Stencil3d>>()130.add_render_command::<Stencil3d, DrawMesh3dStencil>()131.init_resource::<ViewSortedRenderPhases<Stencil3d>>()132.add_systems(RenderStartup, init_stencil_pipeline)133.add_systems(ExtractSchedule, extract_camera_phases)134.add_systems(135Render,136(137queue_custom_meshes.in_set(RenderSystems::QueueMeshes),138sort_phase_system::<Stencil3d>.in_set(RenderSystems::PhaseSort),139batch_and_prepare_sorted_render_phase::<Stencil3d, StencilPipeline>140.in_set(RenderSystems::PrepareResources),141),142);143144render_app145.add_render_graph_node::<ViewNodeRunner<CustomDrawNode>>(Core3d, CustomDrawPassLabel)146// Tell the node to run after the main pass147.add_render_graph_edges(Core3d, (Node3d::MainOpaquePass, CustomDrawPassLabel));148}149}150151#[derive(Resource)]152struct StencilPipeline {153/// The base mesh pipeline defined by bevy154///155/// Since we want to draw a stencil of an existing bevy mesh we want to reuse the default156/// pipeline as much as possible157mesh_pipeline: MeshPipeline,158/// Stores the shader used for this pipeline directly on the pipeline.159/// This isn't required, it's only done like this for simplicity.160shader_handle: Handle<Shader>,161}162163fn init_stencil_pipeline(164mut commands: Commands,165mesh_pipeline: Res<MeshPipeline>,166asset_server: Res<AssetServer>,167) {168commands.insert_resource(StencilPipeline {169mesh_pipeline: mesh_pipeline.clone(),170shader_handle: asset_server.load(SHADER_ASSET_PATH),171});172}173174// For more information on how SpecializedMeshPipeline work, please look at the175// specialized_mesh_pipeline example176impl SpecializedMeshPipeline for StencilPipeline {177type Key = MeshPipelineKey;178179fn specialize(180&self,181key: Self::Key,182layout: &MeshVertexBufferLayoutRef,183) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {184// We will only use the position of the mesh in our shader so we only need to specify that185let mut vertex_attributes = Vec::new();186if layout.0.contains(Mesh::ATTRIBUTE_POSITION) {187// Make sure this matches the shader location188vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0));189}190// This will automatically generate the correct `VertexBufferLayout` based on the vertex attributes191let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?;192let view_layout = self193.mesh_pipeline194.get_view_layout(MeshPipelineViewLayoutKey::from(key));195Ok(RenderPipelineDescriptor {196label: Some("Specialized Mesh Pipeline".into()),197// We want to reuse the data from bevy so we use the same bind groups as the default198// mesh pipeline199layout: vec![200// Bind group 0 is the view uniform201view_layout.main_layout.clone(),202// Bind group 1 is empty203view_layout.empty_layout.clone(),204// Bind group 2 is the mesh uniform205self.mesh_pipeline.mesh_layouts.model_only.clone(),206],207vertex: VertexState {208shader: self.shader_handle.clone(),209buffers: vec![vertex_buffer_layout],210..default()211},212fragment: Some(FragmentState {213shader: self.shader_handle.clone(),214targets: vec![Some(ColorTargetState {215format: TextureFormat::bevy_default(),216blend: None,217write_mask: ColorWrites::ALL,218})],219..default()220}),221primitive: PrimitiveState {222topology: key.primitive_topology(),223cull_mode: Some(Face::Back),224..default()225},226// It's generally recommended to specialize your pipeline for MSAA,227// but it's not always possible228..default()229})230}231}232233// We will reuse render commands already defined by bevy to draw a 3d mesh234type DrawMesh3dStencil = (235SetItemPipeline,236// This will set the view bindings in group 0237SetMeshViewBindGroup<0>,238// This will set an empty bind group in group 1239SetMeshViewEmptyBindGroup<1>,240// This will set the mesh bindings in group 2241SetMeshBindGroup<2>,242// This will draw the mesh243DrawMesh,244);245246// This is the data required per entity drawn in a custom phase in bevy. More specifically this is the247// data required when using a ViewSortedRenderPhase. This would look differently if we wanted a248// batched render phase. Sorted phases are a bit easier to implement, but a batched phase would249// look similar.250//251// If you want to see how a batched phase implementation looks, you should look at the Opaque2d252// phase.253struct Stencil3d {254pub sort_key: FloatOrd,255pub entity: (Entity, MainEntity),256pub pipeline: CachedRenderPipelineId,257pub draw_function: DrawFunctionId,258pub batch_range: Range<u32>,259pub extra_index: PhaseItemExtraIndex,260/// Whether the mesh in question is indexed (uses an index buffer in261/// addition to its vertex buffer).262pub indexed: bool,263}264265// For more information about writing a phase item, please look at the custom_phase_item example266impl PhaseItem for Stencil3d {267#[inline]268fn entity(&self) -> Entity {269self.entity.0270}271272#[inline]273fn main_entity(&self) -> MainEntity {274self.entity.1275}276277#[inline]278fn draw_function(&self) -> DrawFunctionId {279self.draw_function280}281282#[inline]283fn batch_range(&self) -> &Range<u32> {284&self.batch_range285}286287#[inline]288fn batch_range_mut(&mut self) -> &mut Range<u32> {289&mut self.batch_range290}291292#[inline]293fn extra_index(&self) -> PhaseItemExtraIndex {294self.extra_index.clone()295}296297#[inline]298fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {299(&mut self.batch_range, &mut self.extra_index)300}301}302303impl SortedPhaseItem for Stencil3d {304type SortKey = FloatOrd;305306#[inline]307fn sort_key(&self) -> Self::SortKey {308self.sort_key309}310311#[inline]312fn sort(items: &mut [Self]) {313// bevy normally uses radsort instead of the std slice::sort_by_key314// radsort is a stable radix sort that performed better than `slice::sort_by_key` or `slice::sort_unstable_by_key`.315// Since it is not re-exported by bevy, we just use the std sort for the purpose of the example316items.sort_by_key(SortedPhaseItem::sort_key);317}318319#[inline]320fn indexed(&self) -> bool {321self.indexed322}323}324325impl CachedRenderPipelinePhaseItem for Stencil3d {326#[inline]327fn cached_pipeline(&self) -> CachedRenderPipelineId {328self.pipeline329}330}331332impl GetBatchData for StencilPipeline {333type Param = (334SRes<RenderMeshInstances>,335SRes<RenderAssets<RenderMesh>>,336SRes<MeshAllocator>,337);338type CompareData = AssetId<Mesh>;339type BufferData = MeshUniform;340341fn get_batch_data(342(mesh_instances, _render_assets, mesh_allocator): &SystemParamItem<Self::Param>,343(_entity, main_entity): (Entity, MainEntity),344) -> Option<(Self::BufferData, Option<Self::CompareData>)> {345let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else {346error!(347"`get_batch_data` should never be called in GPU mesh uniform \348building mode"349);350return None;351};352let mesh_instance = mesh_instances.get(&main_entity)?;353let first_vertex_index =354match mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id) {355Some(mesh_vertex_slice) => mesh_vertex_slice.range.start,356None => 0,357};358let mesh_uniform = {359let mesh_transforms = &mesh_instance.transforms;360let (local_from_world_transpose_a, local_from_world_transpose_b) =361mesh_transforms.world_from_local.inverse_transpose_3x3();362MeshUniform {363world_from_local: mesh_transforms.world_from_local.to_transpose(),364previous_world_from_local: mesh_transforms.previous_world_from_local.to_transpose(),365lightmap_uv_rect: UVec2::ZERO,366local_from_world_transpose_a,367local_from_world_transpose_b,368flags: mesh_transforms.flags,369first_vertex_index,370current_skin_index: u32::MAX,371material_and_lightmap_bind_group_slot: 0,372tag: 0,373pad: 0,374}375};376Some((mesh_uniform, None))377}378}379380impl GetFullBatchData for StencilPipeline {381type BufferInputData = MeshInputUniform;382383fn get_index_and_compare_data(384(mesh_instances, _, _): &SystemParamItem<Self::Param>,385main_entity: MainEntity,386) -> Option<(NonMaxU32, Option<Self::CompareData>)> {387// This should only be called during GPU building.388let RenderMeshInstances::GpuBuilding(ref mesh_instances) = **mesh_instances else {389error!(390"`get_index_and_compare_data` should never be called in CPU mesh uniform building \391mode"392);393return None;394};395let mesh_instance = mesh_instances.get(&main_entity)?;396Some((397mesh_instance.current_uniform_index,398mesh_instance399.should_batch()400.then_some(mesh_instance.mesh_asset_id),401))402}403404fn get_binned_batch_data(405(mesh_instances, _render_assets, mesh_allocator): &SystemParamItem<Self::Param>,406main_entity: MainEntity,407) -> Option<Self::BufferData> {408let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else {409error!(410"`get_binned_batch_data` should never be called in GPU mesh uniform building mode"411);412return None;413};414let mesh_instance = mesh_instances.get(&main_entity)?;415let first_vertex_index =416match mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id) {417Some(mesh_vertex_slice) => mesh_vertex_slice.range.start,418None => 0,419};420421Some(MeshUniform::new(422&mesh_instance.transforms,423first_vertex_index,424mesh_instance.material_bindings_index.slot,425None,426None,427None,428))429}430431fn write_batch_indirect_parameters_metadata(432indexed: bool,433base_output_index: u32,434batch_set_index: Option<NonMaxU32>,435indirect_parameters_buffers: &mut UntypedPhaseIndirectParametersBuffers,436indirect_parameters_offset: u32,437) {438// Note that `IndirectParameters` covers both of these structures, even439// though they actually have distinct layouts. See the comment above that440// type for more information.441let indirect_parameters = IndirectParametersCpuMetadata {442base_output_index,443batch_set_index: match batch_set_index {444None => !0,445Some(batch_set_index) => u32::from(batch_set_index),446},447};448449if indexed {450indirect_parameters_buffers451.indexed452.set(indirect_parameters_offset, indirect_parameters);453} else {454indirect_parameters_buffers455.non_indexed456.set(indirect_parameters_offset, indirect_parameters);457}458}459460fn get_binned_index(461_param: &SystemParamItem<Self::Param>,462_query_item: MainEntity,463) -> Option<NonMaxU32> {464None465}466}467468// When defining a phase, we need to extract it from the main world and add it to a resource469// that will be used by the render world. We need to give that resource all views that will use470// that phase471fn extract_camera_phases(472mut stencil_phases: ResMut<ViewSortedRenderPhases<Stencil3d>>,473cameras: Extract<Query<(Entity, &Camera), With<Camera3d>>>,474mut live_entities: Local<HashSet<RetainedViewEntity>>,475) {476live_entities.clear();477for (main_entity, camera) in &cameras {478if !camera.is_active {479continue;480}481// This is the main camera, so we use the first subview index (0)482let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0);483484stencil_phases.insert_or_clear(retained_view_entity);485live_entities.insert(retained_view_entity);486}487488// Clear out all dead views.489stencil_phases.retain(|camera_entity, _| live_entities.contains(camera_entity));490}491492// This is a very important step when writing a custom phase.493//494// This system determines which meshes will be added to the phase.495fn queue_custom_meshes(496custom_draw_functions: Res<DrawFunctions<Stencil3d>>,497mut pipelines: ResMut<SpecializedMeshPipelines<StencilPipeline>>,498pipeline_cache: Res<PipelineCache>,499custom_draw_pipeline: Res<StencilPipeline>,500render_meshes: Res<RenderAssets<RenderMesh>>,501render_mesh_instances: Res<RenderMeshInstances>,502mut custom_render_phases: ResMut<ViewSortedRenderPhases<Stencil3d>>,503mut views: Query<(&ExtractedView, &RenderVisibleEntities, &Msaa)>,504has_marker: Query<(), With<DrawStencil>>,505) {506for (view, visible_entities, msaa) in &mut views {507let Some(custom_phase) = custom_render_phases.get_mut(&view.retained_view_entity) else {508continue;509};510let draw_custom = custom_draw_functions.read().id::<DrawMesh3dStencil>();511512// Create the key based on the view.513// In this case we only care about MSAA and HDR514let view_key = MeshPipelineKey::from_msaa_samples(msaa.samples())515| MeshPipelineKey::from_hdr(view.hdr);516517let rangefinder = view.rangefinder3d();518// Since our phase can work on any 3d mesh we can reuse the default mesh 3d filter519for (render_entity, visible_entity) in visible_entities.iter::<Mesh3d>() {520// We only want meshes with the marker component to be queued to our phase.521if has_marker.get(*render_entity).is_err() {522continue;523}524let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity)525else {526continue;527};528let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else {529continue;530};531532// Specialize the key for the current mesh entity533// For this example we only specialize based on the mesh topology534// but you could have more complex keys and that's where you'd need to create those keys535let mut mesh_key = view_key;536mesh_key |= MeshPipelineKey::from_primitive_topology(mesh.primitive_topology());537538let pipeline_id = pipelines.specialize(539&pipeline_cache,540&custom_draw_pipeline,541mesh_key,542&mesh.layout,543);544let pipeline_id = match pipeline_id {545Ok(id) => id,546Err(err) => {547error!("{}", err);548continue;549}550};551let distance = rangefinder.distance_translation(&mesh_instance.translation);552// At this point we have all the data we need to create a phase item and add it to our553// phase554custom_phase.add(Stencil3d {555// Sort the data based on the distance to the view556sort_key: FloatOrd(distance),557entity: (*render_entity, *visible_entity),558pipeline: pipeline_id,559draw_function: draw_custom,560// Sorted phase items aren't batched561batch_range: 0..1,562extra_index: PhaseItemExtraIndex::None,563indexed: mesh.indexed(),564});565}566}567}568569// Render label used to order our render graph node that will render our phase570#[derive(RenderLabel, Debug, Clone, Hash, PartialEq, Eq)]571struct CustomDrawPassLabel;572573#[derive(Default)]574struct CustomDrawNode;575impl ViewNode for CustomDrawNode {576type ViewQuery = (577&'static ExtractedCamera,578&'static ExtractedView,579&'static ViewTarget,580Option<&'static MainPassResolutionOverride>,581);582583fn run<'w>(584&self,585graph: &mut RenderGraphContext,586render_context: &mut RenderContext<'w>,587(camera, view, target, resolution_override): QueryItem<'w, '_, Self::ViewQuery>,588world: &'w World,589) -> Result<(), NodeRunError> {590// First, we need to get our phases resource591let Some(stencil_phases) = world.get_resource::<ViewSortedRenderPhases<Stencil3d>>() else {592return Ok(());593};594595// Get the view entity from the graph596let view_entity = graph.view_entity();597598// Get the phase for the current view running our node599let Some(stencil_phase) = stencil_phases.get(&view.retained_view_entity) else {600return Ok(());601};602603// Render pass setup604let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {605label: Some("stencil pass"),606// For the purpose of the example, we will write directly to the view target. A real607// stencil pass would write to a custom texture and that texture would be used in later608// passes to render custom effects using it.609color_attachments: &[Some(target.get_color_attachment())],610// We don't bind any depth buffer for this pass611depth_stencil_attachment: None,612timestamp_writes: None,613occlusion_query_set: None,614});615616if let Some(viewport) =617Viewport::from_viewport_and_override(camera.viewport.as_ref(), resolution_override)618{619render_pass.set_camera_viewport(&viewport);620}621622// Render the phase623// This will execute each draw functions of each phase items queued in this phase624if let Err(err) = stencil_phase.render(&mut render_pass, world, view_entity) {625error!("Error encountered while rendering the stencil phase {err:?}");626}627628Ok(())629}630}631632633