1use 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 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 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 DrawStencil,
96 ));
97 commands.spawn((
99 PointLight {
100 shadow_maps_enabled: true,
101 ..default()
102 },
103 Transform::from_xyz(4.0, 8.0, 4.0),
104 ));
105 commands.spawn((
107 Camera3d::default(),
108 Transform::from_xyz(-2.0, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
109 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 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 mesh_pipeline: MeshPipeline,
164 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
180impl 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 let mut vertex_attributes = Vec::new();
192 if layout.0.contains(Mesh::ATTRIBUTE_POSITION) {
193 vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0));
195 }
196 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 layout: vec![
206 view_layout.main_layout,
208 view_layout.empty_layout,
210 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 ..default()
236 })
237 }
238}
239
240type DrawMesh3dStencil = (
242 SetItemPipeline,
243 SetMeshViewBindGroup<0>,
245 SetMeshViewEmptyBindGroup<1>,
247 SetMeshBindGroup<2>,
249 DrawMesh,
251);
252
253struct Stencil3d {
261 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 pub indexed: bool,
273}
274
275impl 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 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 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 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 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
497fn 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 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 stencil_phases.retain(|camera_entity, _| live_entities.contains(camera_entity));
519}
520
521#[derive(Default, Deref, DerefMut, Resource)]
526struct PendingCustomMeshQueues(pub PendingQueues);
527
528fn 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 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 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 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 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 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 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 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 color_attachments: &[Some(target.get_color_attachment())],
673 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}