Path: blob/main/examples/shader_advanced/custom_render_phase.rs
9341 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::math::Affine3Ext;16use bevy::pbr::{SetMeshViewEmptyBindGroup, ViewKeyCache};17use bevy::{18camera::MainPassResolutionOverride,19core_pipeline::{core_3d::main_opaque_pass_3d, schedule::Core3d, Core3dSystems},20ecs::system::{lifetimeless::SRes, SystemParamItem},21math::FloatOrd,22mesh::MeshVertexBufferLayoutRef,23pbr::{24DrawMesh, MeshInputUniform, MeshPipeline, MeshPipelineKey, MeshPipelineViewLayoutKey,25MeshUniform, RenderMeshInstances, SetMeshBindGroup, SetMeshViewBindGroup,26},27platform::collections::HashSet,28prelude::*,29render::{30batching::{31gpu_preprocessing::{32batch_and_prepare_sorted_render_phase, IndirectParametersCpuMetadata,33UntypedPhaseIndirectParametersBuffers,34},35GetBatchData, GetFullBatchData,36},37camera::ExtractedCamera,38extract_component::{ExtractComponent, ExtractComponentPlugin},39mesh::{allocator::MeshAllocator, RenderMesh},40render_asset::RenderAssets,41render_phase::{42sort_phase_system, AddRenderCommand, CachedRenderPipelinePhaseItem, DrawFunctionId,43DrawFunctions, PhaseItem, PhaseItemExtraIndex, SetItemPipeline, SortedPhaseItem,44SortedRenderPhasePlugin, ViewSortedRenderPhases,45},46render_resource::{47CachedRenderPipelineId, ColorTargetState, ColorWrites, Face, FragmentState,48PipelineCache, PrimitiveState, RenderPassDescriptor, RenderPipelineDescriptor,49SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines,50TextureFormat, VertexState,51},52renderer::{RenderContext, ViewQuery},53sync_world::MainEntity,54view::{ExtractedView, RenderVisibleEntities, RetainedViewEntity, ViewTarget},55Extract, Render, RenderApp, RenderDebugFlags, RenderStartup, RenderSystems,56},57};58use nonmax::NonMaxU32;5960const SHADER_ASSET_PATH: &str = "shaders/custom_stencil.wgsl";6162fn main() {63App::new()64.add_plugins((DefaultPlugins, MeshStencilPhasePlugin))65.add_systems(Startup, setup)66.run();67}6869fn setup(70mut commands: Commands,71mut meshes: ResMut<Assets<Mesh>>,72mut materials: ResMut<Assets<StandardMaterial>>,73) {74// circular base75commands.spawn((76Mesh3d(meshes.add(Circle::new(4.0))),77MeshMaterial3d(materials.add(Color::WHITE)),78Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),79));80// cube81// This cube will be rendered by the main pass, but it will also be rendered by our custom82// pass. This should result in an unlit red cube83commands.spawn((84Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),85MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),86Transform::from_xyz(0.0, 0.5, 0.0),87// This marker component is used to identify which mesh will be used in our custom pass88// The circle doesn't have it so it won't be rendered in our pass89DrawStencil,90));91// light92commands.spawn((93PointLight {94shadow_maps_enabled: true,95..default()96},97Transform::from_xyz(4.0, 8.0, 4.0),98));99// camera100commands.spawn((101Camera3d::default(),102Transform::from_xyz(-2.0, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),103// disable msaa for simplicity104Msaa::Off,105));106}107108#[derive(Component, ExtractComponent, Clone, Copy, Default)]109struct DrawStencil;110111struct MeshStencilPhasePlugin;112impl Plugin for MeshStencilPhasePlugin {113fn build(&self, app: &mut App) {114app.add_plugins((115ExtractComponentPlugin::<DrawStencil>::default(),116SortedRenderPhasePlugin::<Stencil3d, MeshPipeline>::new(RenderDebugFlags::default()),117));118// We need to get the render app from the main app119let Some(render_app) = app.get_sub_app_mut(RenderApp) else {120return;121};122render_app123.init_resource::<SpecializedMeshPipelines<StencilPipeline>>()124.init_resource::<DrawFunctions<Stencil3d>>()125.add_render_command::<Stencil3d, DrawMesh3dStencil>()126.init_resource::<ViewSortedRenderPhases<Stencil3d>>()127.add_systems(RenderStartup, init_stencil_pipeline)128.add_systems(ExtractSchedule, extract_camera_phases)129.add_systems(130Render,131(132queue_custom_meshes.in_set(RenderSystems::QueueMeshes),133sort_phase_system::<Stencil3d>.in_set(RenderSystems::PhaseSort),134batch_and_prepare_sorted_render_phase::<Stencil3d, StencilPipeline>135.in_set(RenderSystems::PrepareResources),136),137)138.add_systems(139Core3d,140custom_draw_system141.after(main_opaque_pass_3d)142.in_set(Core3dSystems::MainPass),143);144}145}146147#[derive(Resource)]148struct StencilPipeline {149/// The base mesh pipeline defined by bevy150///151/// Since we want to draw a stencil of an existing bevy mesh we want to reuse the default152/// pipeline as much as possible153mesh_pipeline: MeshPipeline,154/// Stores the shader used for this pipeline directly on the pipeline.155/// This isn't required, it's only done like this for simplicity.156shader_handle: Handle<Shader>,157}158159fn init_stencil_pipeline(160mut commands: Commands,161mesh_pipeline: Res<MeshPipeline>,162asset_server: Res<AssetServer>,163) {164commands.insert_resource(StencilPipeline {165mesh_pipeline: mesh_pipeline.clone(),166shader_handle: asset_server.load(SHADER_ASSET_PATH),167});168}169170// For more information on how SpecializedMeshPipeline work, please look at the171// specialized_mesh_pipeline example172impl SpecializedMeshPipeline for StencilPipeline {173type Key = MeshPipelineKey;174175fn specialize(176&self,177key: Self::Key,178layout: &MeshVertexBufferLayoutRef,179) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {180// We will only use the position of the mesh in our shader so we only need to specify that181let mut vertex_attributes = Vec::new();182if layout.0.contains(Mesh::ATTRIBUTE_POSITION) {183// Make sure this matches the shader location184vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0));185}186// This will automatically generate the correct `VertexBufferLayout` based on the vertex attributes187let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?;188let view_layout = self189.mesh_pipeline190.get_view_layout(MeshPipelineViewLayoutKey::from(key));191Ok(RenderPipelineDescriptor {192label: Some("Specialized Mesh Pipeline".into()),193// We want to reuse the data from bevy so we use the same bind groups as the default194// mesh pipeline195layout: vec![196// Bind group 0 is the view uniform197view_layout.main_layout.clone(),198// Bind group 1 is empty199view_layout.empty_layout.clone(),200// Bind group 2 is the mesh uniform201self.mesh_pipeline.mesh_layouts.model_only.clone(),202],203vertex: VertexState {204shader: self.shader_handle.clone(),205buffers: vec![vertex_buffer_layout],206..default()207},208fragment: Some(FragmentState {209shader: self.shader_handle.clone(),210targets: vec![Some(ColorTargetState {211format: TextureFormat::bevy_default(),212blend: None,213write_mask: ColorWrites::ALL,214})],215..default()216}),217primitive: PrimitiveState {218topology: key.primitive_topology(),219cull_mode: Some(Face::Back),220..default()221},222// It's generally recommended to specialize your pipeline for MSAA,223// but it's not always possible224..default()225})226}227}228229// We will reuse render commands already defined by bevy to draw a 3d mesh230type DrawMesh3dStencil = (231SetItemPipeline,232// This will set the view bindings in group 0233SetMeshViewBindGroup<0>,234// This will set an empty bind group in group 1235SetMeshViewEmptyBindGroup<1>,236// This will set the mesh bindings in group 2237SetMeshBindGroup<2>,238// This will draw the mesh239DrawMesh,240);241242// This is the data required per entity drawn in a custom phase in bevy. More specifically this is the243// data required when using a ViewSortedRenderPhase. This would look differently if we wanted a244// batched render phase. Sorted phases are a bit easier to implement, but a batched phase would245// look similar.246//247// If you want to see how a batched phase implementation looks, you should look at the Opaque2d248// phase.249struct Stencil3d {250pub sort_key: FloatOrd,251pub entity: (Entity, MainEntity),252pub pipeline: CachedRenderPipelineId,253pub draw_function: DrawFunctionId,254pub batch_range: Range<u32>,255pub extra_index: PhaseItemExtraIndex,256/// Whether the mesh in question is indexed (uses an index buffer in257/// addition to its vertex buffer).258pub indexed: bool,259}260261// For more information about writing a phase item, please look at the custom_phase_item example262impl PhaseItem for Stencil3d {263#[inline]264fn entity(&self) -> Entity {265self.entity.0266}267268#[inline]269fn main_entity(&self) -> MainEntity {270self.entity.1271}272273#[inline]274fn draw_function(&self) -> DrawFunctionId {275self.draw_function276}277278#[inline]279fn batch_range(&self) -> &Range<u32> {280&self.batch_range281}282283#[inline]284fn batch_range_mut(&mut self) -> &mut Range<u32> {285&mut self.batch_range286}287288#[inline]289fn extra_index(&self) -> PhaseItemExtraIndex {290self.extra_index.clone()291}292293#[inline]294fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {295(&mut self.batch_range, &mut self.extra_index)296}297}298299impl SortedPhaseItem for Stencil3d {300type SortKey = FloatOrd;301302#[inline]303fn sort_key(&self) -> Self::SortKey {304self.sort_key305}306307#[inline]308fn sort(items: &mut [Self]) {309// bevy normally uses radsort instead of the std slice::sort_by_key310// radsort is a stable radix sort that performed better than `slice::sort_by_key` or `slice::sort_unstable_by_key`.311// Since it is not re-exported by bevy, we just use the std sort for the purpose of the example312items.sort_by_key(SortedPhaseItem::sort_key);313}314315#[inline]316fn indexed(&self) -> bool {317self.indexed318}319}320321impl CachedRenderPipelinePhaseItem for Stencil3d {322#[inline]323fn cached_pipeline(&self) -> CachedRenderPipelineId {324self.pipeline325}326}327328impl GetBatchData for StencilPipeline {329type Param = (330SRes<RenderMeshInstances>,331SRes<RenderAssets<RenderMesh>>,332SRes<MeshAllocator>,333);334type CompareData = AssetId<Mesh>;335type BufferData = MeshUniform;336337fn get_batch_data(338(mesh_instances, _render_assets, mesh_allocator): &SystemParamItem<Self::Param>,339(_entity, main_entity): (Entity, MainEntity),340) -> Option<(Self::BufferData, Option<Self::CompareData>)> {341let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else {342error!(343"`get_batch_data` should never be called in GPU mesh uniform \344building mode"345);346return None;347};348let mesh_instance = mesh_instances.get(&main_entity)?;349let first_vertex_index =350match mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id) {351Some(mesh_vertex_slice) => mesh_vertex_slice.range.start,352None => 0,353};354let mesh_uniform = {355let mesh_transforms = &mesh_instance.transforms;356let (local_from_world_transpose_a, local_from_world_transpose_b) =357mesh_transforms.world_from_local.inverse_transpose_3x3();358MeshUniform {359world_from_local: mesh_transforms.world_from_local.to_transpose(),360previous_world_from_local: mesh_transforms.previous_world_from_local.to_transpose(),361lightmap_uv_rect: UVec2::ZERO,362local_from_world_transpose_a,363local_from_world_transpose_b,364flags: mesh_transforms.flags,365first_vertex_index,366current_skin_index: u32::MAX,367material_and_lightmap_bind_group_slot: 0,368tag: 0,369pad: 0,370}371};372Some((mesh_uniform, None))373}374}375376impl GetFullBatchData for StencilPipeline {377type BufferInputData = MeshInputUniform;378379fn get_index_and_compare_data(380(mesh_instances, _, _): &SystemParamItem<Self::Param>,381main_entity: MainEntity,382) -> Option<(NonMaxU32, Option<Self::CompareData>)> {383// This should only be called during GPU building.384let RenderMeshInstances::GpuBuilding(ref mesh_instances) = **mesh_instances else {385error!(386"`get_index_and_compare_data` should never be called in CPU mesh uniform building \387mode"388);389return None;390};391let mesh_instance = mesh_instances.get(&main_entity)?;392Some((393mesh_instance.current_uniform_index,394mesh_instance395.should_batch()396.then_some(mesh_instance.mesh_asset_id),397))398}399400fn get_binned_batch_data(401(mesh_instances, _render_assets, mesh_allocator): &SystemParamItem<Self::Param>,402main_entity: MainEntity,403) -> Option<Self::BufferData> {404let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else {405error!(406"`get_binned_batch_data` should never be called in GPU mesh uniform building mode"407);408return None;409};410let mesh_instance = mesh_instances.get(&main_entity)?;411let first_vertex_index =412match mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id) {413Some(mesh_vertex_slice) => mesh_vertex_slice.range.start,414None => 0,415};416417Some(MeshUniform::new(418&mesh_instance.transforms,419first_vertex_index,420mesh_instance.material_bindings_index.slot,421None,422None,423None,424))425}426427fn write_batch_indirect_parameters_metadata(428indexed: bool,429base_output_index: u32,430batch_set_index: Option<NonMaxU32>,431indirect_parameters_buffers: &mut UntypedPhaseIndirectParametersBuffers,432indirect_parameters_offset: u32,433) {434// Note that `IndirectParameters` covers both of these structures, even435// though they actually have distinct layouts. See the comment above that436// type for more information.437let indirect_parameters = IndirectParametersCpuMetadata {438base_output_index,439batch_set_index: match batch_set_index {440None => !0,441Some(batch_set_index) => u32::from(batch_set_index),442},443};444445if indexed {446indirect_parameters_buffers447.indexed448.set(indirect_parameters_offset, indirect_parameters);449} else {450indirect_parameters_buffers451.non_indexed452.set(indirect_parameters_offset, indirect_parameters);453}454}455456fn get_binned_index(457_param: &SystemParamItem<Self::Param>,458_query_item: MainEntity,459) -> Option<NonMaxU32> {460None461}462}463464// When defining a phase, we need to extract it from the main world and add it to a resource465// that will be used by the render world. We need to give that resource all views that will use466// that phase467fn extract_camera_phases(468mut stencil_phases: ResMut<ViewSortedRenderPhases<Stencil3d>>,469cameras: Extract<Query<(Entity, &Camera), With<Camera3d>>>,470mut live_entities: Local<HashSet<RetainedViewEntity>>,471) {472live_entities.clear();473for (main_entity, camera) in &cameras {474if !camera.is_active {475continue;476}477// This is the main camera, so we use the first subview index (0)478let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0);479480stencil_phases.insert_or_clear(retained_view_entity);481live_entities.insert(retained_view_entity);482}483484// Clear out all dead views.485stencil_phases.retain(|camera_entity, _| live_entities.contains(camera_entity));486}487488// This is a very important step when writing a custom phase.489//490// This system determines which meshes will be added to the phase.491fn queue_custom_meshes(492custom_draw_functions: Res<DrawFunctions<Stencil3d>>,493mut pipelines: ResMut<SpecializedMeshPipelines<StencilPipeline>>,494pipeline_cache: Res<PipelineCache>,495custom_draw_pipeline: Res<StencilPipeline>,496render_meshes: Res<RenderAssets<RenderMesh>>,497render_mesh_instances: Res<RenderMeshInstances>,498mut custom_render_phases: ResMut<ViewSortedRenderPhases<Stencil3d>>,499mut views: Query<(&ExtractedView, &RenderVisibleEntities)>,500view_key_cache: Res<ViewKeyCache>,501has_marker: Query<(), With<DrawStencil>>,502) {503for (view, visible_entities) in &mut views {504let Some(custom_phase) = custom_render_phases.get_mut(&view.retained_view_entity) else {505continue;506};507let draw_custom = custom_draw_functions.read().id::<DrawMesh3dStencil>();508509let Some(&view_key) = view_key_cache.get(&view.retained_view_entity) else {510continue;511};512513let rangefinder = view.rangefinder3d();514// Since our phase can work on any 3d mesh we can reuse the default mesh 3d filter515for (render_entity, visible_entity) in visible_entities.iter::<Mesh3d>() {516// We only want meshes with the marker component to be queued to our phase.517if has_marker.get(*render_entity).is_err() {518continue;519}520let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity)521else {522continue;523};524let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else {525continue;526};527528// Specialize the key for the current mesh entity529// For this example we only specialize based on the mesh topology530// but you could have more complex keys and that's where you'd need to create those keys531let mut mesh_key = view_key;532mesh_key |= MeshPipelineKey::from_primitive_topology(mesh.primitive_topology());533534let pipeline_id = pipelines.specialize(535&pipeline_cache,536&custom_draw_pipeline,537mesh_key,538&mesh.layout,539);540let pipeline_id = match pipeline_id {541Ok(id) => id,542Err(err) => {543error!("{}", err);544continue;545}546};547let distance = rangefinder.distance(&mesh_instance.center);548// At this point we have all the data we need to create a phase item and add it to our549// phase550custom_phase.add(Stencil3d {551// Sort the data based on the distance to the view552sort_key: FloatOrd(distance),553entity: (*render_entity, *visible_entity),554pipeline: pipeline_id,555draw_function: draw_custom,556// Sorted phase items aren't batched557batch_range: 0..1,558extra_index: PhaseItemExtraIndex::None,559indexed: mesh.indexed(),560});561}562}563}564565fn custom_draw_system(566world: &World,567view: ViewQuery<(568&ExtractedCamera,569&ExtractedView,570&ViewTarget,571Option<&MainPassResolutionOverride>,572)>,573stencil_phases: Res<ViewSortedRenderPhases<Stencil3d>>,574mut ctx: RenderContext,575) {576let view_entity = view.entity();577let (camera, extracted_view, target, resolution_override) = view.into_inner();578579let Some(stencil_phase) = stencil_phases.get(&extracted_view.retained_view_entity) else {580return;581};582583let mut render_pass = ctx.begin_tracked_render_pass(RenderPassDescriptor {584label: Some("stencil pass"),585// For the purpose of the example, we will write directly to the view target. A real586// stencil pass would write to a custom texture and that texture would be used in later587// passes to render custom effects using it.588color_attachments: &[Some(target.get_color_attachment())],589// We don't bind any depth buffer for this pass590depth_stencil_attachment: None,591timestamp_writes: None,592occlusion_query_set: None,593multiview_mask: None,594});595596if let Some(viewport) =597Viewport::from_viewport_and_override(camera.viewport.as_ref(), resolution_override)598{599render_pass.set_camera_viewport(&viewport);600}601602if let Err(err) = stencil_phase.render(&mut render_pass, world, view_entity) {603error!("Error encountered while rendering the stencil phase {err:?}");604}605}606607608