docs/bevy/examples/shader_advanced/custom_render_phase.rs 26.5 K · 719 lines · raw · history

1 //! This example demonstrates how to write a custom phase
2 //!
3 //! Render phases in bevy are used whenever you need to draw a group of meshes in a specific way.
4 //! For example, bevy's main pass has an opaque phase, a transparent phase for both 2d and 3d.
5 //! Sometimes, you may want to only draw a subset of meshes before or after the builtin phase. In
6 //! those situations you need to write your own phase.
7 //!
8 //! This example showcases how writing a custom phase to draw a stencil of a bevy mesh could look
9 //! like. Some shortcuts have been used for simplicity.
10 //!
11 //! This example was made for 3d, but a 2d equivalent would be almost identical.
12
13 use std::ops::Range;
14
15 use bevy::camera::Viewport;
16 use bevy::core_pipeline::core_3d::TransparentSortingInfo3d;
17 use bevy::mesh::{MeshAttributeCompressionFlags, MeshCompressionArgs};
18 use bevy::pbr::{self, MeshPipelineSystems, SetMeshViewEmptyBindGroup, ViewKeyCache};
19 use bevy::{
20 camera::MainPassResolutionOverride,
21 core_pipeline::{core_3d::main_opaque_pass_3d, schedule::Core3d, Core3dSystems},
22 ecs::{
23 entity::EntityHash,
24 system::{lifetimeless::SRes, SystemParamItem},
25 },
26 math::FloatOrd,
27 mesh::MeshVertexBufferLayoutRef,
28 pbr::{
29 DrawMesh, MeshInputUniform, MeshPipeline, MeshPipelineKey, MeshPipelineViewLayoutKey,
30 MeshUniform, RenderMeshInstances, SetMeshBindGroup, SetMeshViewBindGroup,
31 },
32 platform::collections::HashSet,
33 prelude::*,
34 render::{
35 batching::{
36 gpu_preprocessing::{
37 batch_and_prepare_sorted_render_phase, BatchedInstanceBuffers,
38 IndirectParametersMetadata, UntypedPhaseIndirectParametersBuffers,
39 },
40 GetBatchData, GetFullBatchData,
41 },
42 camera::{DirtySpecializations, ExtractedCamera, PendingQueues},
43 extract_component::{ExtractComponent, ExtractComponentPlugin},
44 mesh::{allocator::MeshAllocator, RenderMesh},
45 render_asset::RenderAssets,
46 render_phase::{
47 sort_phase_system, AddRenderCommand, CachedRenderPipelinePhaseItem, DrawFunctionId,
48 DrawFunctions, PhaseItem, PhaseItemExtraIndex, SetItemPipeline, SortedPhaseItem,
49 SortedRenderPhasePlugin, ViewSortedRenderPhases,
50 },
51 render_resource::{
52 CachedRenderPipelineId, ColorTargetState, ColorWrites, Face, FragmentState,
53 PipelineCache, PrimitiveState, RenderPassDescriptor, RenderPipelineDescriptor,
54 SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines,
55 VertexState,
56 },
57 renderer::{RenderContext, ViewQuery},
58 sync_world::{MainEntity, MainEntityHashSet},
59 view::{ExtractedView, RenderVisibleEntities, RetainedViewEntity, ViewTarget},
60 Extract, Render, RenderApp, RenderDebugFlags, RenderStartup, RenderSystems,
61 },
62 };
63 use indexmap::IndexMap;
64 use nonmax::NonMaxU32;
65
66 const SHADER_ASSET_PATH: &str = "shaders/custom_stencil.wesl";
67
68 fn main() {
69 App::new()
70 .add_plugins((DefaultPlugins, MeshStencilPhasePlugin))
71 .add_systems(Startup, setup)
72 .run();
73 }
74
75 fn setup(
76 mut commands: Commands,
77 mut meshes: ResMut<Assets<Mesh>>,
78 mut materials: ResMut<Assets<StandardMaterial>>,
79 ) {
80 // circular base
81 commands.spawn((
82 Mesh3d(meshes.add(Circle::new(4.0))),
83 MeshMaterial3d(materials.add(Color::WHITE)),
84 Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
85 ));
86 // cube
87 // This cube will be rendered by the main pass, but it will also be rendered by our custom
88 // pass. This should result in an unlit red cube
89 commands.spawn((
90 Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
91 MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),
92 Transform::from_xyz(0.0, 0.5, 0.0),
93 // This marker component is used to identify which mesh will be used in our custom pass
94 // The circle doesn't have it so it won't be rendered in our pass
95 DrawStencil,
96 ));
97 commands.spawn((
98 Mesh3d(
99 meshes.add(
100 Sphere::new(0.5)
101 .mesh()
102 .build()
103 .compressed_mesh(&MeshCompressionArgs::regular())
104 .unwrap(),
105 ),
106 ),
107 MeshMaterial3d(materials.add(Color::srgb_u8(124, 255, 144))),
108 Transform::from_xyz(2.0, 0.5, 0.0),
109 // This marker component is used to identify which mesh will be used in our custom pass
110 // The circle doesn't have it so it won't be rendered in our pass
111 DrawStencil,
112 ));
113 // light
114 commands.spawn((
115 PointLight {
116 shadow_maps_enabled: true,
117 ..default()
118 },
119 Transform::from_xyz(4.0, 8.0, 4.0),
120 ));
121 // camera
122 commands.spawn((
123 Camera3d::default(),
124 Transform::from_xyz(-2.0, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
125 // disable msaa for simplicity
126 Msaa::Off,
127 ));
128 }
129
130 #[derive(Component, ExtractComponent, Clone, Copy, Default)]
131 #[extract_app(RenderApp)]
132 struct DrawStencil;
133
134 struct MeshStencilPhasePlugin;
135 impl Plugin for MeshStencilPhasePlugin {
136 fn build(&self, app: &mut App) {
137 app.add_plugins((
138 ExtractComponentPlugin::<DrawStencil>::default(),
139 SortedRenderPhasePlugin::<Stencil3d, MeshPipeline>::new(RenderDebugFlags::default()),
140 ));
141 // We need to get the render app from the main app
142 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
143 return;
144 };
145 render_app
146 .init_resource::<SpecializedMeshPipelines<StencilPipeline>>()
147 .init_resource::<DrawFunctions<Stencil3d>>()
148 .add_render_command::<Stencil3d, DrawMesh3dStencil>()
149 .init_resource::<ViewSortedRenderPhases<Stencil3d>>()
150 .init_resource::<PendingCustomMeshQueues>()
151 .add_systems(
152 RenderStartup,
153 init_stencil_pipeline.after(MeshPipelineSystems),
154 )
155 .add_systems(ExtractSchedule, extract_camera_phases)
156 .add_systems(
157 Render,
158 (
159 queue_custom_meshes.in_set(RenderSystems::QueueMeshes),
160 sort_phase_system::<Stencil3d>.in_set(RenderSystems::PhaseSort),
161 batch_and_prepare_sorted_render_phase::<Stencil3d, StencilPipeline>
162 .in_set(RenderSystems::PrepareResources),
163 ),
164 )
165 .add_systems(
166 Core3d,
167 custom_draw_system
168 .after(main_opaque_pass_3d)
169 .in_set(Core3dSystems::MainPass),
170 );
171 }
172 }
173
174 #[derive(Resource)]
175 struct StencilPipeline {
176 /// The base mesh pipeline defined by bevy
177 ///
178 /// Since we want to draw a stencil of an existing bevy mesh we want to reuse the default
179 /// pipeline as much as possible
180 mesh_pipeline: MeshPipeline,
181 /// Stores the shader used for this pipeline directly on the pipeline.
182 /// This isn't required, it's only done like this for simplicity.
183 shader_handle: Handle<Shader>,
184 }
185
186 fn init_stencil_pipeline(
187 mut commands: Commands,
188 mesh_pipeline: Res<MeshPipeline>,
189 asset_server: Res<AssetServer>,
190 ) {
191 commands.insert_resource(StencilPipeline {
192 mesh_pipeline: mesh_pipeline.clone(),
193 shader_handle: asset_server.load(SHADER_ASSET_PATH),
194 });
195 }
196
197 // For more information on how SpecializedMeshPipeline work, please look at the
198 // specialized_mesh_pipeline example
199 impl SpecializedMeshPipeline for StencilPipeline {
200 type Key = MeshPipelineKey;
201
202 fn specialize(
203 &self,
204 key: Self::Key,
205 layout: &MeshVertexBufferLayoutRef,
206 ) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
207 let mut shader_defs = Vec::new();
208 // We will only use the position of the mesh in our shader so we only need to specify that
209 let mut vertex_attributes = Vec::new();
210 if layout.0.contains(Mesh::ATTRIBUTE_POSITION) {
211 // Handle compressed vertex positions.
212 if layout
213 .0
214 .get_attribute_compression()
215 .contains(MeshAttributeCompressionFlags::COMPRESS_POSITION)
216 {
217 shader_defs.push("VERTEX_POSITIONS_COMPRESSED".into());
218 }
219 // Make sure this matches the shader location
220 vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0));
221 }
222 // This will automatically generate the correct `VertexBufferLayout` based on the vertex attributes
223 let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?;
224 let view_layout = self
225 .mesh_pipeline
226 .get_view_layout(MeshPipelineViewLayoutKey::from(key));
227 Ok(RenderPipelineDescriptor {
228 label: Some("Specialized Mesh Pipeline".into()),
229 // We want to reuse the data from bevy so we use the same bind groups as the default
230 // mesh pipeline
231 layout: vec![
232 // Bind group 0 is the view uniform
233 view_layout.main_layout,
234 // Bind group 1 is empty
235 view_layout.empty_layout,
236 // Bind group 2 is the mesh uniform
237 self.mesh_pipeline.mesh_layouts.model_only.clone(),
238 ],
239 vertex: VertexState {
240 shader: self.shader_handle.clone(),
241 shader_defs: shader_defs.clone(),
242 buffers: vec![vertex_buffer_layout],
243 ..default()
244 },
245 fragment: Some(FragmentState {
246 shader: self.shader_handle.clone(),
247 shader_defs,
248 targets: vec![Some(ColorTargetState {
249 format: key.target_format(),
250 blend: None,
251 write_mask: ColorWrites::ALL,
252 })],
253 ..default()
254 }),
255 primitive: PrimitiveState {
256 topology: key.primitive_topology(),
257 strip_index_format: key.strip_index_format(),
258 cull_mode: Some(Face::Back),
259 ..default()
260 },
261 // It's generally recommended to specialize your pipeline for MSAA,
262 // but it's not always possible
263 ..default()
264 })
265 }
266 }
267
268 // We will reuse render commands already defined by bevy to draw a 3d mesh
269 type DrawMesh3dStencil = (
270 SetItemPipeline,
271 // This will set the view bindings in group 0
272 SetMeshViewBindGroup<0>,
273 // This will set an empty bind group in group 1
274 SetMeshViewEmptyBindGroup<1>,
275 // This will set the mesh bindings in group 2
276 SetMeshBindGroup<2>,
277 // This will draw the mesh
278 DrawMesh,
279 );
280
281 // This is the data required per entity drawn in a custom phase in bevy. More specifically this is the
282 // data required when using a ViewSortedRenderPhase. This would look differently if we wanted a
283 // batched render phase. Sorted phases are a bit easier to implement, but a batched phase would
284 // look similar.
285 //
286 // If you want to see how a batched phase implementation looks, you should look at the Opaque2d
287 // phase.
288 struct Stencil3d {
289 /// Information needed to sort the objects in the phase by distance to the
290 /// view.
291 pub sorting_info: TransparentSortingInfo3d,
292 pub distance: FloatOrd,
293 pub entity: (Entity, MainEntity),
294 pub pipeline: CachedRenderPipelineId,
295 pub draw_function: DrawFunctionId,
296 pub batch_range: Range<u32>,
297 pub extra_index: PhaseItemExtraIndex,
298 /// Whether the mesh in question is indexed (uses an index buffer in
299 /// addition to its vertex buffer).
300 pub indexed: bool,
301 }
302
303 // For more information about writing a phase item, please look at the custom_phase_item example
304 impl PhaseItem for Stencil3d {
305 #[inline]
306 fn entity(&self) -> Entity {
307 self.entity.0
308 }
309
310 #[inline]
311 fn main_entity(&self) -> MainEntity {
312 self.entity.1
313 }
314
315 #[inline]
316 fn draw_function(&self) -> DrawFunctionId {
317 self.draw_function
318 }
319
320 #[inline]
321 fn batch_range(&self) -> &Range<u32> {
322 &self.batch_range
323 }
324
325 #[inline]
326 fn batch_range_mut(&mut self) -> &mut Range<u32> {
327 &mut self.batch_range
328 }
329
330 #[inline]
331 fn extra_index(&self) -> PhaseItemExtraIndex {
332 self.extra_index.clone()
333 }
334
335 #[inline]
336 fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
337 (&mut self.batch_range, &mut self.extra_index)
338 }
339 }
340
341 impl SortedPhaseItem for Stencil3d {
342 type SortKey = FloatOrd;
343
344 #[inline]
345 fn sort_key(&self) -> Self::SortKey {
346 self.distance
347 }
348
349 #[inline]
350 fn sort(items: &mut IndexMap<(Entity, MainEntity), Stencil3d, EntityHash>) {
351 items.sort_by_key(|_, phase_item: &Stencil3d| phase_item.distance);
352 }
353
354 fn recalculate_sort_keys(
355 items: &mut IndexMap<(Entity, MainEntity), Self, EntityHash>,
356 view: &ExtractedView,
357 ) {
358 // Determine the distance to the view for each phase item.
359 let rangefinder = view.rangefinder3d();
360 for item in items.values_mut() {
361 item.distance = FloatOrd(item.sorting_info.sort_distance(&rangefinder));
362 }
363 }
364
365 #[inline]
366 fn indexed(&self) -> bool {
367 self.indexed
368 }
369 }
370
371 impl CachedRenderPipelinePhaseItem for Stencil3d {
372 #[inline]
373 fn cached_pipeline(&self) -> CachedRenderPipelineId {
374 self.pipeline
375 }
376 }
377
378 impl GetBatchData for StencilPipeline {
379 type Param = (SRes<RenderMeshInstances>, SRes<MeshAllocator>);
380 // Placing `AssetId<Mesh>` in the batch set compare data prevents Bevy from
381 // trying to multi-draw items with different meshes together. This is fine
382 // for this simple example.
383 type BatchSetCompareData = AssetId<Mesh>;
384 type BatchCompareData = ();
385 type BufferData = MeshUniform;
386
387 fn get_batch_data(
388 (mesh_instances, mesh_allocator): &SystemParamItem<Self::Param>,
389 (_entity, main_entity): (Entity, MainEntity),
390 ) -> Option<(
391 Self::BufferData,
392 Option<(Self::BatchSetCompareData, Self::BatchCompareData)>,
393 )> {
394 let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else {
395 error!(
396 "`get_batch_data` should never be called in GPU mesh uniform \
397 building mode"
398 );
399 return None;
400 };
401 let mesh_instance = mesh_instances.get(&main_entity)?;
402 let first_vertex_index =
403 match mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id()) {
404 Some(mesh_vertex_slice) => mesh_vertex_slice.range.start,
405 None => 0,
406 };
407 let metadata_index = mesh_allocator
408 .mesh_metadata_slice(&mesh_instance.mesh_asset_id())
409 .map(|mesh_metadata_slice| mesh_metadata_slice.range.start);
410
411 let mesh_uniform = MeshUniform::new(
412 &mesh_instance.transforms,
413 first_vertex_index,
414 mesh_instance.material_bindings_index().slot,
415 None,
416 None,
417 None,
418 Some(mesh_instance.tag()),
419 metadata_index,
420 );
421 Some((mesh_uniform, None))
422 }
423 }
424
425 impl GetFullBatchData for StencilPipeline {
426 type BufferInputData = MeshInputUniform;
427
428 fn get_index_and_compare_data(
429 (mesh_instances, _): &SystemParamItem<Self::Param>,
430 main_entity: MainEntity,
431 ) -> Option<(
432 NonMaxU32,
433 Option<(Self::BatchSetCompareData, Self::BatchCompareData)>,
434 )> {
435 // This should only be called during GPU building.
436 let RenderMeshInstances::GpuBuilding(ref mesh_instances) = **mesh_instances else {
437 error!(
438 "`get_index_and_compare_data` should never be called in CPU mesh uniform building \
439 mode"
440 );
441 return None;
442 };
443 let mesh_instance = mesh_instances.get(&main_entity)?;
444 Some((
445 NonMaxU32::new(mesh_instance.gpu_specific.current_uniform_index())?,
446 mesh_instance
447 .should_batch()
448 .then_some((mesh_instance.mesh_asset_id(), ())),
449 ))
450 }
451
452 fn get_binned_batch_data(
453 (mesh_instances, mesh_allocator): &SystemParamItem<Self::Param>,
454 main_entity: MainEntity,
455 ) -> Option<Self::BufferData> {
456 let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else {
457 error!(
458 "`get_binned_batch_data` should never be called in GPU mesh uniform building mode"
459 );
460 return None;
461 };
462 let mesh_instance = mesh_instances.get(&main_entity)?;
463 let first_vertex_index =
464 match mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id()) {
465 Some(mesh_vertex_slice) => mesh_vertex_slice.range.start,
466 None => 0,
467 };
468 let metadata_index = mesh_allocator
469 .mesh_metadata_slice(&mesh_instance.mesh_asset_id())
470 .map(|mesh_metadata_slice| mesh_metadata_slice.range.start);
471
472 Some(MeshUniform::new(
473 &mesh_instance.transforms,
474 first_vertex_index,
475 mesh_instance.material_bindings_index().slot,
476 None,
477 None,
478 None,
479 Some(mesh_instance.tag()),
480 metadata_index,
481 ))
482 }
483
484 fn write_batch_indirect_parameters_metadata(
485 indexed: bool,
486 base_output_index: u32,
487 batch_set_index: Option<NonMaxU32>,
488 indirect_parameters_buffers: &mut UntypedPhaseIndirectParametersBuffers,
489 indirect_parameters_offset: u32,
490 ) {
491 // Note that `IndirectParameters` covers both of these structures, even
492 // though they actually have distinct layouts. See the comment above that
493 // type for more information.
494 let indirect_parameters = IndirectParametersMetadata {
495 base_output_index,
496 batch_set_index: match batch_set_index {
497 None => !0,
498 Some(batch_set_index) => u32::from(batch_set_index),
499 },
500 // These fields are filled in by the GPU:
501 mesh_index: 0,
502 early_instance_count: 0,
503 late_instance_count: 0,
504 };
505
506 if indexed {
507 indirect_parameters_buffers
508 .indexed
509 .set(indirect_parameters_offset, indirect_parameters);
510 } else {
511 indirect_parameters_buffers
512 .non_indexed
513 .set(indirect_parameters_offset, indirect_parameters);
514 }
515 }
516
517 fn get_binned_index(
518 _param: &SystemParamItem<Self::Param>,
519 _query_item: MainEntity,
520 ) -> Option<NonMaxU32> {
521 None
522 }
523 }
524
525 // When defining a phase, we need to extract it from the main world and add it to a resource
526 // that will be used by the render world. We need to give that resource all views that will use
527 // that phase
528 fn extract_camera_phases(
529 mut stencil_phases: ResMut<ViewSortedRenderPhases<Stencil3d>>,
530 cameras: Extract<Query<(Entity, &Camera), With<Camera3d>>>,
531 mut live_entities: Local<HashSet<RetainedViewEntity>>,
532 ) {
533 live_entities.clear();
534 for (main_entity, camera) in &cameras {
535 if !camera.is_active {
536 continue;
537 }
538 // This is the main camera, so we use the first subview index (0)
539 let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0);
540
541 stencil_phases.prepare_for_new_frame(retained_view_entity);
542 live_entities.insert(retained_view_entity);
543 }
544
545 // Clear out all dead views.
546 stencil_phases.retain(|camera_entity, _| live_entities.contains(camera_entity));
547 }
548
549 /// A resource that stores meshes that couldn't be specialized yet because their
550 /// materials hadn't loaded.
551 ///
552 /// See the documentation for [`PendingQueues`] for more information.
553 #[derive(Default, Deref, DerefMut, Resource)]
554 struct PendingCustomMeshQueues(pub PendingQueues);
555
556 // This is a very important step when writing a custom phase.
557 //
558 // This system determines which meshes will be added to the phase.
559 fn queue_custom_meshes(
560 custom_draw_functions: Res<DrawFunctions<Stencil3d>>,
561 mut pipelines: ResMut<SpecializedMeshPipelines<StencilPipeline>>,
562 pipeline_cache: Res<PipelineCache>,
563 custom_draw_pipeline: Res<StencilPipeline>,
564 render_meshes: Res<RenderAssets<RenderMesh>>,
565 render_mesh_instances: Res<RenderMeshInstances>,
566 maybe_batched_instance_buffers: Option<
567 Res<BatchedInstanceBuffers<MeshUniform, MeshInputUniform>>,
568 >,
569 mut custom_render_phases: ResMut<ViewSortedRenderPhases<Stencil3d>>,
570 mut views: Query<(&ExtractedView, &RenderVisibleEntities)>,
571 view_key_cache: Res<ViewKeyCache>,
572 dirty_specializations: Res<DirtySpecializations>,
573 mut pending_custom_mesh_queues: ResMut<PendingCustomMeshQueues>,
574 has_marker: Query<(), With<DrawStencil>>,
575 mut mesh_instances_queued_this_iteration_scratch_space: Local<MainEntityHashSet>,
576 ) {
577 for (view, visible_entities) in &mut views {
578 let Some(custom_phase) = custom_render_phases.get_mut(&view.retained_view_entity) else {
579 continue;
580 };
581 let draw_custom = custom_draw_functions.read().id::<DrawMesh3dStencil>();
582
583 let Some(&view_key) = view_key_cache.get(&view.retained_view_entity) else {
584 continue;
585 };
586
587 // Since our phase can work on any 3d mesh we can reuse the default mesh 3d filter
588 let Some(render_visible_mesh_entities) = visible_entities.get::<Mesh3d>() else {
589 continue;
590 };
591
592 let view_pending_custom_mesh_queues =
593 pending_custom_mesh_queues.prepare_for_new_frame(view.retained_view_entity);
594
595 // First, remove meshes that need to be respecialized, and those that were removed, from the bins.
596 for &main_entity in dirty_specializations
597 .iter_to_dequeue(view.retained_view_entity, render_visible_mesh_entities)
598 {
599 custom_phase.remove(Entity::PLACEHOLDER, main_entity);
600 }
601
602 for (render_entity, visible_entity) in dirty_specializations.iter_to_queue(
603 view.retained_view_entity,
604 render_visible_mesh_entities,
605 &view_pending_custom_mesh_queues.prev_frame,
606 &mut mesh_instances_queued_this_iteration_scratch_space,
607 ) {
608 // We only want meshes with the marker component to be queued to our phase.
609 if has_marker.get(*render_entity).is_err() {
610 continue;
611 }
612 let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity)
613 else {
614 // We couldn't fetch the mesh, probably because it hasn't been
615 // loaded yet. Add the entity to the list of pending custom mesh
616 // queues and bail.
617 view_pending_custom_mesh_queues
618 .current_frame
619 .insert((*render_entity, *visible_entity));
620 continue;
621 };
622 let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id()) else {
623 continue;
624 };
625
626 // Specialize the key for the current mesh entity
627 // For this example we only specialize based on the mesh topology
628 // but you could have more complex keys and that's where you'd need to create those keys
629 let mut mesh_key = view_key;
630 mesh_key |= MeshPipelineKey::from_primitive_topology_and_strip_index(
631 mesh.primitive_topology(),
632 mesh.index_format(),
633 );
634
635 let pipeline_id = pipelines.specialize(
636 &pipeline_cache,
637 &custom_draw_pipeline,
638 mesh_key,
639 &mesh.layout,
640 );
641 let pipeline_id = match pipeline_id {
642 Ok(id) => id,
643 Err(err) => {
644 error!("{}", err);
645 continue;
646 }
647 };
648 // At this point we have all the data we need to create a phase item and add it to our
649 // phase
650 custom_phase.add_retained(Stencil3d {
651 sorting_info: TransparentSortingInfo3d::Sorted {
652 mesh_center: pbr::get_mesh_instance_world_from_local(
653 *visible_entity,
654 mesh_instance.current_uniform_index,
655 &render_mesh_instances,
656 maybe_batched_instance_buffers.as_deref(),
657 )
658 .transform_point3(
659 render_meshes
660 .get(mesh_instance.mesh_asset_id())
661 .unwrap()
662 .aabb_center,
663 ),
664 depth_bias: 0.0,
665 },
666 distance: FloatOrd(0.0),
667 entity: (Entity::PLACEHOLDER, *visible_entity),
668 pipeline: pipeline_id,
669 draw_function: draw_custom,
670 // Sorted phase items aren't batched
671 batch_range: 0..1,
672 extra_index: PhaseItemExtraIndex::None,
673 indexed: mesh.indexed(),
674 });
675 }
676 }
677 }
678
679 fn custom_draw_system(
680 world: &World,
681 view: ViewQuery<(
682 &ExtractedCamera,
683 &ExtractedView,
684 &ViewTarget,
685 Option<&MainPassResolutionOverride>,
686 )>,
687 stencil_phases: Res<ViewSortedRenderPhases<Stencil3d>>,
688 mut ctx: RenderContext,
689 ) {
690 let view_entity = view.entity();
691 let (camera, extracted_view, target, resolution_override) = view.into_inner();
692
693 let Some(stencil_phase) = stencil_phases.get(&extracted_view.retained_view_entity) else {
694 return;
695 };
696
697 let mut render_pass = ctx.begin_tracked_render_pass(RenderPassDescriptor {
698 label: Some("stencil pass"),
699 // For the purpose of the example, we will write directly to the view target. A real
700 // stencil pass would write to a custom texture and that texture would be used in later
701 // passes to render custom effects using it.
702 color_attachments: &[Some(target.get_color_attachment())],
703 // We don't bind any depth buffer for this pass
704 depth_stencil_attachment: None,
705 timestamp_writes: None,
706 occlusion_query_set: None,
707 multiview_mask: None,
708 });
709
710 if let Some(viewport) =
711 Viewport::from_viewport_and_override(camera.viewport.as_ref(), resolution_override)
712 {
713 render_pass.set_camera_viewport(&viewport);
714 }
715
716 if let Err(err) = stencil_phase.render(&mut render_pass, world, view_entity) {
717 error!("Error encountered while rendering the stencil phase {err:?}");
718 }
719 }