Skip to main content

custom_phase_item/
custom_phase_item.rs

1//! Demonstrates how to enqueue custom draw commands in a render phase.
2//!
3//! This example shows how to use the built-in
4//! [`bevy_render::render_phase::BinnedRenderPhase`] functionality with a
5//! custom [`RenderCommand`] to allow inserting arbitrary GPU drawing logic
6//! into Bevy's pipeline. This is not the only way to add custom rendering code
7//! into Bevy—render nodes are another, lower-level method—but it does allow
8//! for better reuse of parts of Bevy's built-in mesh rendering logic.
9
10use bevy::{
11    camera::{
12        primitives::Aabb,
13        visibility::{self, VisibilityClass},
14    },
15    core_pipeline::core_3d::{Opaque3d, Opaque3dBatchSetKey, Opaque3dBinKey, CORE_3D_DEPTH_FORMAT},
16    ecs::{
17        query::ROQueryItem,
18        system::{lifetimeless::SRes, SystemParamItem},
19    },
20    mesh::VertexBufferLayout,
21    prelude::*,
22    render::{
23        camera::{DirtySpecializations, PendingQueues},
24        extract_component::{ExtractComponent, ExtractComponentPlugin},
25        mesh::allocator::MeshSlabs,
26        render_phase::{
27            AddRenderCommand, BinnedRenderPhaseType, DrawFunctions, InputUniformIndex, PhaseItem,
28            RenderCommand, RenderCommandResult, SetItemPipeline, TrackedRenderPass,
29            ViewBinnedRenderPhases,
30        },
31        render_resource::{
32            BufferUsages, Canonical, ColorTargetState, ColorWrites, CompareFunction,
33            DepthStencilState, FragmentState, IndexFormat, PipelineCache, RawBufferVec,
34            RenderPipeline, RenderPipelineDescriptor, Specializer, SpecializerKey, TextureFormat,
35            Variants, VertexAttribute, VertexFormat, VertexState, VertexStepMode,
36        },
37        renderer::{RenderDevice, RenderQueue},
38        view::{ExtractedView, RenderVisibleEntities},
39        Render, RenderApp, RenderSystems,
40    },
41};
42use bytemuck::{Pod, Zeroable};
43
44/// A marker component that represents an entity that is to be rendered using
45/// our custom phase item.
46///
47/// Note the [`ExtractComponent`] trait implementation: this is necessary to
48/// tell Bevy that this object should be pulled into the render world. Also note
49/// the `on_add` hook, which is needed to tell Bevy's `check_visibility` system
50/// that entities with this component need to be examined for visibility.
51#[derive(Clone, Component, ExtractComponent)]
52#[require(VisibilityClass)]
53#[component(on_add = visibility::add_visibility_class::<CustomRenderedEntity>)]
54struct CustomRenderedEntity;
55
56/// A [`RenderCommand`] that binds the vertex and index buffers and issues the
57/// draw command for our custom phase item.
58struct DrawCustomPhaseItem;
59
60impl<P> RenderCommand<P> for DrawCustomPhaseItem
61where
62    P: PhaseItem,
63{
64    type Param = SRes<CustomPhaseItemBuffers>;
65
66    type ViewQuery = ();
67
68    type ItemQuery = ();
69
70    fn render<'w>(
71        _: &P,
72        _: ROQueryItem<'w, '_, Self::ViewQuery>,
73        _: Option<ROQueryItem<'w, '_, Self::ItemQuery>>,
74        custom_phase_item_buffers: SystemParamItem<'w, '_, Self::Param>,
75        pass: &mut TrackedRenderPass<'w>,
76    ) -> RenderCommandResult {
77        // Borrow check workaround.
78        let custom_phase_item_buffers = custom_phase_item_buffers.into_inner();
79
80        // Tell the GPU where the vertices are.
81        pass.set_vertex_buffer(
82            0,
83            custom_phase_item_buffers
84                .vertices
85                .buffer()
86                .unwrap()
87                .slice(..),
88        );
89
90        // Tell the GPU where the indices are.
91        pass.set_index_buffer(
92            custom_phase_item_buffers
93                .indices
94                .buffer()
95                .unwrap()
96                .slice(..),
97            IndexFormat::Uint32,
98        );
99
100        // Draw one triangle (3 vertices).
101        pass.draw_indexed(0..3, 0, 0..1);
102
103        RenderCommandResult::Success
104    }
105}
106
107/// The GPU vertex and index buffers for our custom phase item.
108///
109/// As the custom phase item is a single triangle, these are uploaded once and
110/// then left alone.
111#[derive(Resource)]
112struct CustomPhaseItemBuffers {
113    /// The vertices for the single triangle.
114    ///
115    /// This is a [`RawBufferVec`] because that's the simplest and fastest type
116    /// of GPU buffer, and [`Vertex`] objects are simple.
117    vertices: RawBufferVec<Vertex>,
118
119    /// The indices of the single triangle.
120    ///
121    /// As above, this is a [`RawBufferVec`] because `u32` values have trivial
122    /// size and alignment.
123    indices: RawBufferVec<u32>,
124}
125
126/// The CPU-side structure that describes a single vertex of the triangle.
127#[derive(Clone, Copy, Pod, Zeroable)]
128#[repr(C)]
129struct Vertex {
130    /// The 3D position of the triangle vertex.
131    position: Vec3,
132    /// Padding.
133    pad0: u32,
134    /// The color of the triangle vertex.
135    color: Vec3,
136    /// Padding.
137    pad1: u32,
138}
139
140impl Vertex {
141    /// Creates a new vertex structure.
142    const fn new(position: Vec3, color: Vec3) -> Vertex {
143        Vertex {
144            position,
145            color,
146            pad0: 0,
147            pad1: 0,
148        }
149    }
150}
151
152/// The custom draw commands that Bevy executes for each entity we enqueue into
153/// the render phase.
154type DrawCustomPhaseItemCommands = (SetItemPipeline, DrawCustomPhaseItem);
155
156/// A single triangle's worth of vertices, for demonstration purposes.
157static VERTICES: [Vertex; 3] = [
158    Vertex::new(vec3(-0.866, -0.5, 0.5), vec3(1.0, 0.0, 0.0)),
159    Vertex::new(vec3(0.866, -0.5, 0.5), vec3(0.0, 1.0, 0.0)),
160    Vertex::new(vec3(0.0, 1.0, 0.5), vec3(0.0, 0.0, 1.0)),
161];
162
163/// The entry point.
164fn main() {
165    let mut app = App::new();
166    app.add_plugins(DefaultPlugins)
167        .add_plugins(ExtractComponentPlugin::<CustomRenderedEntity>::default())
168        .add_systems(Startup, setup);
169
170    // We make sure to add these to the render app, not the main app.
171    app.sub_app_mut(RenderApp)
172        .init_resource::<CustomPhasePipeline>()
173        .init_resource::<PendingCustomPhaseItemQueues>()
174        .add_render_command::<Opaque3d, DrawCustomPhaseItemCommands>()
175        .add_systems(
176            Render,
177            prepare_custom_phase_item_buffers.in_set(RenderSystems::Prepare),
178        )
179        .add_systems(Render, queue_custom_phase_item.in_set(RenderSystems::Queue));
180
181    app.run();
182}
183
184/// Spawns the objects in the scene.
185fn setup(mut commands: Commands) {
186    // Spawn a single entity that has custom rendering. It'll be extracted into
187    // the render world via [`ExtractComponent`].
188    commands.spawn((
189        Visibility::default(),
190        Transform::default(),
191        // This `Aabb` is necessary for the visibility checks to work.
192        Aabb {
193            center: Vec3A::ZERO,
194            half_extents: Vec3A::splat(0.5),
195        },
196        CustomRenderedEntity,
197    ));
198
199    // Spawn the camera.
200    commands.spawn((
201        Camera3d::default(),
202        Transform::from_xyz(0.0, 0.0, 1.0).looking_at(Vec3::ZERO, Vec3::Y),
203    ));
204}
205
206/// Creates the [`CustomPhaseItemBuffers`] resource.
207///
208/// This must be done in a startup system because it needs the [`RenderDevice`]
209/// and [`RenderQueue`] to exist, and they don't until [`App::run`] is called.
210fn prepare_custom_phase_item_buffers(mut commands: Commands) {
211    commands.init_resource::<CustomPhaseItemBuffers>();
212}
213
214/// A resource that holds entities that couldn't be specialized and/or queued
215/// yet because their dependent assets haven't loaded yet.
216///
217/// In this particular example, entities with custom rendering can always be
218/// specialized, so this resource goes unused in practice. However, we still
219/// need it, because [`DirtySpecializations`] requires such a resource.
220///
221/// See the documentation of [`PendingQueues`] for more information.
222#[derive(Default, Deref, DerefMut, Resource)]
223pub struct PendingCustomPhaseItemQueues(pub PendingQueues);
224
225/// A render-world system that enqueues the entity with custom rendering into
226/// the opaque render phases of each view.
227fn queue_custom_phase_item(
228    pipeline_cache: Res<PipelineCache>,
229    mut pipeline: ResMut<CustomPhasePipeline>,
230    mut opaque_render_phases: ResMut<ViewBinnedRenderPhases<Opaque3d>>,
231    opaque_draw_functions: Res<DrawFunctions<Opaque3d>>,
232    views: Query<(&ExtractedView, &RenderVisibleEntities, &Msaa)>,
233    dirty_specializations: Res<DirtySpecializations>,
234    mut pending_custom_phase_item_queues: ResMut<PendingCustomPhaseItemQueues>,
235) {
236    let draw_custom_phase_item = opaque_draw_functions
237        .read()
238        .id::<DrawCustomPhaseItemCommands>();
239
240    // Render phases are per-view, so we need to iterate over all views so that
241    // the entity appears in them. (In this example, we have only one view, but
242    // it's good practice to loop over all views anyway.)
243    for (view, view_visible_entities, msaa) in views.iter() {
244        let Some(opaque_phase) = opaque_render_phases.get_mut(&view.retained_view_entity) else {
245            continue;
246        };
247
248        // Fetch the list of visible entities in the `CustomRenderedEntity`
249        // class. If there are no such entities, then we have no entities to
250        // render, and we're done.
251        let Some(render_visible_mesh_entities) =
252            view_visible_entities.get::<CustomRenderedEntity>()
253        else {
254            continue;
255        };
256
257        let view_pending_custom_phase_item_queues =
258            pending_custom_phase_item_queues.prepare_for_new_frame(view.retained_view_entity);
259
260        // First, remove meshes that need to be respecialized, and those that
261        // were removed, from the bins.
262        for &main_entity in dirty_specializations
263            .iter_to_dequeue(view.retained_view_entity, render_visible_mesh_entities)
264        {
265            opaque_phase.remove(main_entity);
266        }
267
268        // Find all the custom rendered entities that are visible from this
269        // view.
270        for (render_entity, main_entity) in dirty_specializations.iter_to_queue(
271            view.retained_view_entity,
272            render_visible_mesh_entities,
273            &view_pending_custom_phase_item_queues.prev_frame,
274        ) {
275            // Ordinarily, the [`SpecializedRenderPipeline::Key`] would contain
276            // some per-view settings, such as whether the view is HDR, but for
277            // simplicity's sake we simply hard-code the view's characteristics,
278            // with the exception of number of MSAA samples.
279            let Ok(pipeline_id) = pipeline
280                .variants
281                .specialize(&pipeline_cache, CustomPhaseKey(*msaa))
282            else {
283                continue;
284            };
285
286            // Add the custom render item. We use the
287            // [`BinnedRenderPhaseType::NonMesh`] type to skip the special
288            // handling that Bevy has for meshes (preprocessing, indirect
289            // draws, etc.)
290            //
291            // The asset ID is arbitrary; we simply use [`AssetId::invalid`],
292            // but you can use anything you like. Note that the asset ID need
293            // not be the ID of a [`Mesh`].
294            opaque_phase.add(
295                Opaque3dBatchSetKey {
296                    draw_function: draw_custom_phase_item,
297                    pipeline: pipeline_id,
298                    material_bind_group_index: None,
299                    lightmap_slab: None,
300                    slabs: MeshSlabs::default(),
301                },
302                Opaque3dBinKey {
303                    asset_id: AssetId::<Mesh>::invalid().untyped(),
304                },
305                (*render_entity, *main_entity),
306                InputUniformIndex::default(),
307                BinnedRenderPhaseType::NonMesh,
308            );
309        }
310    }
311}
312
313struct CustomPhaseSpecializer;
314
315#[derive(Resource)]
316struct CustomPhasePipeline {
317    /// the `variants` collection holds onto the shader handle through the base descriptor
318    variants: Variants<RenderPipeline, CustomPhaseSpecializer>,
319}
320
321impl FromWorld for CustomPhasePipeline {
322    fn from_world(world: &mut World) -> Self {
323        let asset_server = world.resource::<AssetServer>();
324        let shader = asset_server.load("shaders/custom_phase_item.wgsl");
325
326        let base_descriptor = RenderPipelineDescriptor {
327            label: Some("custom render pipeline".into()),
328            vertex: VertexState {
329                shader: shader.clone(),
330                buffers: vec![VertexBufferLayout {
331                    array_stride: size_of::<Vertex>() as u64,
332                    step_mode: VertexStepMode::Vertex,
333                    // This needs to match the layout of [`Vertex`].
334                    attributes: vec![
335                        VertexAttribute {
336                            format: VertexFormat::Float32x3,
337                            offset: 0,
338                            shader_location: 0,
339                        },
340                        VertexAttribute {
341                            format: VertexFormat::Float32x3,
342                            offset: 16,
343                            shader_location: 1,
344                        },
345                    ],
346                }],
347                ..default()
348            },
349            fragment: Some(FragmentState {
350                shader: shader.clone(),
351                targets: vec![Some(ColorTargetState {
352                    // Ordinarily, you'd want to check whether the view has the
353                    // HDR format and substitute the appropriate texture format
354                    // here, but we omit that for simplicity.
355                    format: TextureFormat::Rgba8UnormSrgb,
356                    blend: None,
357                    write_mask: ColorWrites::ALL,
358                })],
359                ..default()
360            }),
361            // Note that if your view has no depth buffer this will need to be
362            // changed.
363            depth_stencil: Some(DepthStencilState {
364                format: CORE_3D_DEPTH_FORMAT,
365                depth_write_enabled: Some(false),
366                depth_compare: Some(CompareFunction::Always),
367                stencil: default(),
368                bias: default(),
369            }),
370            ..default()
371        };
372
373        let variants = Variants::new(CustomPhaseSpecializer, base_descriptor);
374
375        Self { variants }
376    }
377}
378
379#[derive(Copy, Clone, PartialEq, Eq, Hash, SpecializerKey)]
380struct CustomPhaseKey(Msaa);
381
382impl Specializer<RenderPipeline> for CustomPhaseSpecializer {
383    type Key = CustomPhaseKey;
384
385    fn specialize(
386        &self,
387        key: Self::Key,
388        descriptor: &mut RenderPipelineDescriptor,
389    ) -> Result<Canonical<Self::Key>, BevyError> {
390        descriptor.multisample.count = key.0.samples();
391        Ok(key)
392    }
393}
394
395impl FromWorld for CustomPhaseItemBuffers {
396    fn from_world(world: &mut World) -> Self {
397        let render_device = world.resource::<RenderDevice>();
398        let render_queue = world.resource::<RenderQueue>();
399
400        // Create the vertex and index buffers.
401        let mut vbo = RawBufferVec::new(BufferUsages::VERTEX);
402        let mut ibo = RawBufferVec::new(BufferUsages::INDEX);
403
404        for vertex in &VERTICES {
405            vbo.push(*vertex);
406        }
407        for index in 0..3 {
408            ibo.push(index);
409        }
410
411        // These two lines are required in order to trigger the upload to GPU.
412        vbo.write_buffer(render_device, render_queue);
413        ibo.write_buffer(render_device, render_queue);
414
415        CustomPhaseItemBuffers {
416            vertices: vbo,
417            indices: ibo,
418        }
419    }
420}