Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/shader_advanced/custom_render_phase.rs
9341 views
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::math::Affine3Ext;
17
use bevy::pbr::{SetMeshViewEmptyBindGroup, ViewKeyCache};
18
use bevy::{
19
camera::MainPassResolutionOverride,
20
core_pipeline::{core_3d::main_opaque_pass_3d, schedule::Core3d, Core3dSystems},
21
ecs::system::{lifetimeless::SRes, SystemParamItem},
22
math::FloatOrd,
23
mesh::MeshVertexBufferLayoutRef,
24
pbr::{
25
DrawMesh, MeshInputUniform, MeshPipeline, MeshPipelineKey, MeshPipelineViewLayoutKey,
26
MeshUniform, RenderMeshInstances, SetMeshBindGroup, SetMeshViewBindGroup,
27
},
28
platform::collections::HashSet,
29
prelude::*,
30
render::{
31
batching::{
32
gpu_preprocessing::{
33
batch_and_prepare_sorted_render_phase, IndirectParametersCpuMetadata,
34
UntypedPhaseIndirectParametersBuffers,
35
},
36
GetBatchData, GetFullBatchData,
37
},
38
camera::ExtractedCamera,
39
extract_component::{ExtractComponent, ExtractComponentPlugin},
40
mesh::{allocator::MeshAllocator, RenderMesh},
41
render_asset::RenderAssets,
42
render_phase::{
43
sort_phase_system, AddRenderCommand, CachedRenderPipelinePhaseItem, DrawFunctionId,
44
DrawFunctions, PhaseItem, PhaseItemExtraIndex, SetItemPipeline, SortedPhaseItem,
45
SortedRenderPhasePlugin, ViewSortedRenderPhases,
46
},
47
render_resource::{
48
CachedRenderPipelineId, ColorTargetState, ColorWrites, Face, FragmentState,
49
PipelineCache, PrimitiveState, RenderPassDescriptor, RenderPipelineDescriptor,
50
SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines,
51
TextureFormat, VertexState,
52
},
53
renderer::{RenderContext, ViewQuery},
54
sync_world::MainEntity,
55
view::{ExtractedView, RenderVisibleEntities, RetainedViewEntity, ViewTarget},
56
Extract, Render, RenderApp, RenderDebugFlags, RenderStartup, RenderSystems,
57
},
58
};
59
use nonmax::NonMaxU32;
60
61
const SHADER_ASSET_PATH: &str = "shaders/custom_stencil.wgsl";
62
63
fn main() {
64
App::new()
65
.add_plugins((DefaultPlugins, MeshStencilPhasePlugin))
66
.add_systems(Startup, setup)
67
.run();
68
}
69
70
fn setup(
71
mut commands: Commands,
72
mut meshes: ResMut<Assets<Mesh>>,
73
mut materials: ResMut<Assets<StandardMaterial>>,
74
) {
75
// circular base
76
commands.spawn((
77
Mesh3d(meshes.add(Circle::new(4.0))),
78
MeshMaterial3d(materials.add(Color::WHITE)),
79
Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
80
));
81
// cube
82
// This cube will be rendered by the main pass, but it will also be rendered by our custom
83
// pass. This should result in an unlit red cube
84
commands.spawn((
85
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
86
MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),
87
Transform::from_xyz(0.0, 0.5, 0.0),
88
// This marker component is used to identify which mesh will be used in our custom pass
89
// The circle doesn't have it so it won't be rendered in our pass
90
DrawStencil,
91
));
92
// light
93
commands.spawn((
94
PointLight {
95
shadow_maps_enabled: true,
96
..default()
97
},
98
Transform::from_xyz(4.0, 8.0, 4.0),
99
));
100
// camera
101
commands.spawn((
102
Camera3d::default(),
103
Transform::from_xyz(-2.0, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
104
// disable msaa for simplicity
105
Msaa::Off,
106
));
107
}
108
109
#[derive(Component, ExtractComponent, Clone, Copy, Default)]
110
struct DrawStencil;
111
112
struct MeshStencilPhasePlugin;
113
impl Plugin for MeshStencilPhasePlugin {
114
fn build(&self, app: &mut App) {
115
app.add_plugins((
116
ExtractComponentPlugin::<DrawStencil>::default(),
117
SortedRenderPhasePlugin::<Stencil3d, MeshPipeline>::new(RenderDebugFlags::default()),
118
));
119
// We need to get the render app from the main app
120
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
121
return;
122
};
123
render_app
124
.init_resource::<SpecializedMeshPipelines<StencilPipeline>>()
125
.init_resource::<DrawFunctions<Stencil3d>>()
126
.add_render_command::<Stencil3d, DrawMesh3dStencil>()
127
.init_resource::<ViewSortedRenderPhases<Stencil3d>>()
128
.add_systems(RenderStartup, init_stencil_pipeline)
129
.add_systems(ExtractSchedule, extract_camera_phases)
130
.add_systems(
131
Render,
132
(
133
queue_custom_meshes.in_set(RenderSystems::QueueMeshes),
134
sort_phase_system::<Stencil3d>.in_set(RenderSystems::PhaseSort),
135
batch_and_prepare_sorted_render_phase::<Stencil3d, StencilPipeline>
136
.in_set(RenderSystems::PrepareResources),
137
),
138
)
139
.add_systems(
140
Core3d,
141
custom_draw_system
142
.after(main_opaque_pass_3d)
143
.in_set(Core3dSystems::MainPass),
144
);
145
}
146
}
147
148
#[derive(Resource)]
149
struct StencilPipeline {
150
/// The base mesh pipeline defined by bevy
151
///
152
/// Since we want to draw a stencil of an existing bevy mesh we want to reuse the default
153
/// pipeline as much as possible
154
mesh_pipeline: MeshPipeline,
155
/// Stores the shader used for this pipeline directly on the pipeline.
156
/// This isn't required, it's only done like this for simplicity.
157
shader_handle: Handle<Shader>,
158
}
159
160
fn init_stencil_pipeline(
161
mut commands: Commands,
162
mesh_pipeline: Res<MeshPipeline>,
163
asset_server: Res<AssetServer>,
164
) {
165
commands.insert_resource(StencilPipeline {
166
mesh_pipeline: mesh_pipeline.clone(),
167
shader_handle: asset_server.load(SHADER_ASSET_PATH),
168
});
169
}
170
171
// For more information on how SpecializedMeshPipeline work, please look at the
172
// specialized_mesh_pipeline example
173
impl SpecializedMeshPipeline for StencilPipeline {
174
type Key = MeshPipelineKey;
175
176
fn specialize(
177
&self,
178
key: Self::Key,
179
layout: &MeshVertexBufferLayoutRef,
180
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
181
// We will only use the position of the mesh in our shader so we only need to specify that
182
let mut vertex_attributes = Vec::new();
183
if layout.0.contains(Mesh::ATTRIBUTE_POSITION) {
184
// Make sure this matches the shader location
185
vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0));
186
}
187
// This will automatically generate the correct `VertexBufferLayout` based on the vertex attributes
188
let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?;
189
let view_layout = self
190
.mesh_pipeline
191
.get_view_layout(MeshPipelineViewLayoutKey::from(key));
192
Ok(RenderPipelineDescriptor {
193
label: Some("Specialized Mesh Pipeline".into()),
194
// We want to reuse the data from bevy so we use the same bind groups as the default
195
// mesh pipeline
196
layout: vec![
197
// Bind group 0 is the view uniform
198
view_layout.main_layout.clone(),
199
// Bind group 1 is empty
200
view_layout.empty_layout.clone(),
201
// Bind group 2 is the mesh uniform
202
self.mesh_pipeline.mesh_layouts.model_only.clone(),
203
],
204
vertex: VertexState {
205
shader: self.shader_handle.clone(),
206
buffers: vec![vertex_buffer_layout],
207
..default()
208
},
209
fragment: Some(FragmentState {
210
shader: self.shader_handle.clone(),
211
targets: vec![Some(ColorTargetState {
212
format: TextureFormat::bevy_default(),
213
blend: None,
214
write_mask: ColorWrites::ALL,
215
})],
216
..default()
217
}),
218
primitive: PrimitiveState {
219
topology: key.primitive_topology(),
220
cull_mode: Some(Face::Back),
221
..default()
222
},
223
// It's generally recommended to specialize your pipeline for MSAA,
224
// but it's not always possible
225
..default()
226
})
227
}
228
}
229
230
// We will reuse render commands already defined by bevy to draw a 3d mesh
231
type DrawMesh3dStencil = (
232
SetItemPipeline,
233
// This will set the view bindings in group 0
234
SetMeshViewBindGroup<0>,
235
// This will set an empty bind group in group 1
236
SetMeshViewEmptyBindGroup<1>,
237
// This will set the mesh bindings in group 2
238
SetMeshBindGroup<2>,
239
// This will draw the mesh
240
DrawMesh,
241
);
242
243
// This is the data required per entity drawn in a custom phase in bevy. More specifically this is the
244
// data required when using a ViewSortedRenderPhase. This would look differently if we wanted a
245
// batched render phase. Sorted phases are a bit easier to implement, but a batched phase would
246
// look similar.
247
//
248
// If you want to see how a batched phase implementation looks, you should look at the Opaque2d
249
// phase.
250
struct Stencil3d {
251
pub sort_key: FloatOrd,
252
pub entity: (Entity, MainEntity),
253
pub pipeline: CachedRenderPipelineId,
254
pub draw_function: DrawFunctionId,
255
pub batch_range: Range<u32>,
256
pub extra_index: PhaseItemExtraIndex,
257
/// Whether the mesh in question is indexed (uses an index buffer in
258
/// addition to its vertex buffer).
259
pub indexed: bool,
260
}
261
262
// For more information about writing a phase item, please look at the custom_phase_item example
263
impl PhaseItem for Stencil3d {
264
#[inline]
265
fn entity(&self) -> Entity {
266
self.entity.0
267
}
268
269
#[inline]
270
fn main_entity(&self) -> MainEntity {
271
self.entity.1
272
}
273
274
#[inline]
275
fn draw_function(&self) -> DrawFunctionId {
276
self.draw_function
277
}
278
279
#[inline]
280
fn batch_range(&self) -> &Range<u32> {
281
&self.batch_range
282
}
283
284
#[inline]
285
fn batch_range_mut(&mut self) -> &mut Range<u32> {
286
&mut self.batch_range
287
}
288
289
#[inline]
290
fn extra_index(&self) -> PhaseItemExtraIndex {
291
self.extra_index.clone()
292
}
293
294
#[inline]
295
fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
296
(&mut self.batch_range, &mut self.extra_index)
297
}
298
}
299
300
impl SortedPhaseItem for Stencil3d {
301
type SortKey = FloatOrd;
302
303
#[inline]
304
fn sort_key(&self) -> Self::SortKey {
305
self.sort_key
306
}
307
308
#[inline]
309
fn sort(items: &mut [Self]) {
310
// bevy normally uses radsort instead of the std slice::sort_by_key
311
// radsort is a stable radix sort that performed better than `slice::sort_by_key` or `slice::sort_unstable_by_key`.
312
// Since it is not re-exported by bevy, we just use the std sort for the purpose of the example
313
items.sort_by_key(SortedPhaseItem::sort_key);
314
}
315
316
#[inline]
317
fn indexed(&self) -> bool {
318
self.indexed
319
}
320
}
321
322
impl CachedRenderPipelinePhaseItem for Stencil3d {
323
#[inline]
324
fn cached_pipeline(&self) -> CachedRenderPipelineId {
325
self.pipeline
326
}
327
}
328
329
impl GetBatchData for StencilPipeline {
330
type Param = (
331
SRes<RenderMeshInstances>,
332
SRes<RenderAssets<RenderMesh>>,
333
SRes<MeshAllocator>,
334
);
335
type CompareData = AssetId<Mesh>;
336
type BufferData = MeshUniform;
337
338
fn get_batch_data(
339
(mesh_instances, _render_assets, mesh_allocator): &SystemParamItem<Self::Param>,
340
(_entity, main_entity): (Entity, MainEntity),
341
) -> Option<(Self::BufferData, Option<Self::CompareData>)> {
342
let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else {
343
error!(
344
"`get_batch_data` should never be called in GPU mesh uniform \
345
building mode"
346
);
347
return None;
348
};
349
let mesh_instance = mesh_instances.get(&main_entity)?;
350
let first_vertex_index =
351
match mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id) {
352
Some(mesh_vertex_slice) => mesh_vertex_slice.range.start,
353
None => 0,
354
};
355
let mesh_uniform = {
356
let mesh_transforms = &mesh_instance.transforms;
357
let (local_from_world_transpose_a, local_from_world_transpose_b) =
358
mesh_transforms.world_from_local.inverse_transpose_3x3();
359
MeshUniform {
360
world_from_local: mesh_transforms.world_from_local.to_transpose(),
361
previous_world_from_local: mesh_transforms.previous_world_from_local.to_transpose(),
362
lightmap_uv_rect: UVec2::ZERO,
363
local_from_world_transpose_a,
364
local_from_world_transpose_b,
365
flags: mesh_transforms.flags,
366
first_vertex_index,
367
current_skin_index: u32::MAX,
368
material_and_lightmap_bind_group_slot: 0,
369
tag: 0,
370
pad: 0,
371
}
372
};
373
Some((mesh_uniform, None))
374
}
375
}
376
377
impl GetFullBatchData for StencilPipeline {
378
type BufferInputData = MeshInputUniform;
379
380
fn get_index_and_compare_data(
381
(mesh_instances, _, _): &SystemParamItem<Self::Param>,
382
main_entity: MainEntity,
383
) -> Option<(NonMaxU32, Option<Self::CompareData>)> {
384
// This should only be called during GPU building.
385
let RenderMeshInstances::GpuBuilding(ref mesh_instances) = **mesh_instances else {
386
error!(
387
"`get_index_and_compare_data` should never be called in CPU mesh uniform building \
388
mode"
389
);
390
return None;
391
};
392
let mesh_instance = mesh_instances.get(&main_entity)?;
393
Some((
394
mesh_instance.current_uniform_index,
395
mesh_instance
396
.should_batch()
397
.then_some(mesh_instance.mesh_asset_id),
398
))
399
}
400
401
fn get_binned_batch_data(
402
(mesh_instances, _render_assets, mesh_allocator): &SystemParamItem<Self::Param>,
403
main_entity: MainEntity,
404
) -> Option<Self::BufferData> {
405
let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else {
406
error!(
407
"`get_binned_batch_data` should never be called in GPU mesh uniform building mode"
408
);
409
return None;
410
};
411
let mesh_instance = mesh_instances.get(&main_entity)?;
412
let first_vertex_index =
413
match mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id) {
414
Some(mesh_vertex_slice) => mesh_vertex_slice.range.start,
415
None => 0,
416
};
417
418
Some(MeshUniform::new(
419
&mesh_instance.transforms,
420
first_vertex_index,
421
mesh_instance.material_bindings_index.slot,
422
None,
423
None,
424
None,
425
))
426
}
427
428
fn write_batch_indirect_parameters_metadata(
429
indexed: bool,
430
base_output_index: u32,
431
batch_set_index: Option<NonMaxU32>,
432
indirect_parameters_buffers: &mut UntypedPhaseIndirectParametersBuffers,
433
indirect_parameters_offset: u32,
434
) {
435
// Note that `IndirectParameters` covers both of these structures, even
436
// though they actually have distinct layouts. See the comment above that
437
// type for more information.
438
let indirect_parameters = IndirectParametersCpuMetadata {
439
base_output_index,
440
batch_set_index: match batch_set_index {
441
None => !0,
442
Some(batch_set_index) => u32::from(batch_set_index),
443
},
444
};
445
446
if indexed {
447
indirect_parameters_buffers
448
.indexed
449
.set(indirect_parameters_offset, indirect_parameters);
450
} else {
451
indirect_parameters_buffers
452
.non_indexed
453
.set(indirect_parameters_offset, indirect_parameters);
454
}
455
}
456
457
fn get_binned_index(
458
_param: &SystemParamItem<Self::Param>,
459
_query_item: MainEntity,
460
) -> Option<NonMaxU32> {
461
None
462
}
463
}
464
465
// When defining a phase, we need to extract it from the main world and add it to a resource
466
// that will be used by the render world. We need to give that resource all views that will use
467
// that phase
468
fn extract_camera_phases(
469
mut stencil_phases: ResMut<ViewSortedRenderPhases<Stencil3d>>,
470
cameras: Extract<Query<(Entity, &Camera), With<Camera3d>>>,
471
mut live_entities: Local<HashSet<RetainedViewEntity>>,
472
) {
473
live_entities.clear();
474
for (main_entity, camera) in &cameras {
475
if !camera.is_active {
476
continue;
477
}
478
// This is the main camera, so we use the first subview index (0)
479
let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0);
480
481
stencil_phases.insert_or_clear(retained_view_entity);
482
live_entities.insert(retained_view_entity);
483
}
484
485
// Clear out all dead views.
486
stencil_phases.retain(|camera_entity, _| live_entities.contains(camera_entity));
487
}
488
489
// This is a very important step when writing a custom phase.
490
//
491
// This system determines which meshes will be added to the phase.
492
fn queue_custom_meshes(
493
custom_draw_functions: Res<DrawFunctions<Stencil3d>>,
494
mut pipelines: ResMut<SpecializedMeshPipelines<StencilPipeline>>,
495
pipeline_cache: Res<PipelineCache>,
496
custom_draw_pipeline: Res<StencilPipeline>,
497
render_meshes: Res<RenderAssets<RenderMesh>>,
498
render_mesh_instances: Res<RenderMeshInstances>,
499
mut custom_render_phases: ResMut<ViewSortedRenderPhases<Stencil3d>>,
500
mut views: Query<(&ExtractedView, &RenderVisibleEntities)>,
501
view_key_cache: Res<ViewKeyCache>,
502
has_marker: Query<(), With<DrawStencil>>,
503
) {
504
for (view, visible_entities) in &mut views {
505
let Some(custom_phase) = custom_render_phases.get_mut(&view.retained_view_entity) else {
506
continue;
507
};
508
let draw_custom = custom_draw_functions.read().id::<DrawMesh3dStencil>();
509
510
let Some(&view_key) = view_key_cache.get(&view.retained_view_entity) else {
511
continue;
512
};
513
514
let rangefinder = view.rangefinder3d();
515
// Since our phase can work on any 3d mesh we can reuse the default mesh 3d filter
516
for (render_entity, visible_entity) in visible_entities.iter::<Mesh3d>() {
517
// We only want meshes with the marker component to be queued to our phase.
518
if has_marker.get(*render_entity).is_err() {
519
continue;
520
}
521
let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity)
522
else {
523
continue;
524
};
525
let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else {
526
continue;
527
};
528
529
// Specialize the key for the current mesh entity
530
// For this example we only specialize based on the mesh topology
531
// but you could have more complex keys and that's where you'd need to create those keys
532
let mut mesh_key = view_key;
533
mesh_key |= MeshPipelineKey::from_primitive_topology(mesh.primitive_topology());
534
535
let pipeline_id = pipelines.specialize(
536
&pipeline_cache,
537
&custom_draw_pipeline,
538
mesh_key,
539
&mesh.layout,
540
);
541
let pipeline_id = match pipeline_id {
542
Ok(id) => id,
543
Err(err) => {
544
error!("{}", err);
545
continue;
546
}
547
};
548
let distance = rangefinder.distance(&mesh_instance.center);
549
// At this point we have all the data we need to create a phase item and add it to our
550
// phase
551
custom_phase.add(Stencil3d {
552
// Sort the data based on the distance to the view
553
sort_key: FloatOrd(distance),
554
entity: (*render_entity, *visible_entity),
555
pipeline: pipeline_id,
556
draw_function: draw_custom,
557
// Sorted phase items aren't batched
558
batch_range: 0..1,
559
extra_index: PhaseItemExtraIndex::None,
560
indexed: mesh.indexed(),
561
});
562
}
563
}
564
}
565
566
fn custom_draw_system(
567
world: &World,
568
view: ViewQuery<(
569
&ExtractedCamera,
570
&ExtractedView,
571
&ViewTarget,
572
Option<&MainPassResolutionOverride>,
573
)>,
574
stencil_phases: Res<ViewSortedRenderPhases<Stencil3d>>,
575
mut ctx: RenderContext,
576
) {
577
let view_entity = view.entity();
578
let (camera, extracted_view, target, resolution_override) = view.into_inner();
579
580
let Some(stencil_phase) = stencil_phases.get(&extracted_view.retained_view_entity) else {
581
return;
582
};
583
584
let mut render_pass = ctx.begin_tracked_render_pass(RenderPassDescriptor {
585
label: Some("stencil pass"),
586
// For the purpose of the example, we will write directly to the view target. A real
587
// stencil pass would write to a custom texture and that texture would be used in later
588
// passes to render custom effects using it.
589
color_attachments: &[Some(target.get_color_attachment())],
590
// We don't bind any depth buffer for this pass
591
depth_stencil_attachment: None,
592
timestamp_writes: None,
593
occlusion_query_set: None,
594
multiview_mask: None,
595
});
596
597
if let Some(viewport) =
598
Viewport::from_viewport_and_override(camera.viewport.as_ref(), resolution_override)
599
{
600
render_pass.set_camera_viewport(&viewport);
601
}
602
603
if let Err(err) = stencil_phase.render(&mut render_pass, world, view_entity) {
604
error!("Error encountered while rendering the stencil phase {err:?}");
605
}
606
}
607
608