Skip to main content

custom_render_phase/
custom_render_phase.rs

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