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        component::Tick,
18        query::ROQueryItem,
19        system::{lifetimeless::SRes, SystemParamItem},
20    },
21    mesh::VertexBufferLayout,
22    prelude::*,
23    render::{
24        extract_component::{ExtractComponent, ExtractComponentPlugin},
25        render_phase::{
26            AddRenderCommand, BinnedRenderPhaseType, DrawFunctions, InputUniformIndex, PhaseItem,
27            RenderCommand, RenderCommandResult, SetItemPipeline, TrackedRenderPass,
28            ViewBinnedRenderPhases,
29        },
30        render_resource::{
31            BufferUsages, Canonical, ColorTargetState, ColorWrites, CompareFunction,
32            DepthStencilState, FragmentState, IndexFormat, PipelineCache, RawBufferVec,
33            RenderPipeline, RenderPipelineDescriptor, Specializer, SpecializerKey, TextureFormat,
34            Variants, VertexAttribute, VertexFormat, VertexState, VertexStepMode,
35        },
36        renderer::{RenderDevice, RenderQueue},
37        view::{ExtractedView, RenderVisibleEntities},
38        Render, RenderApp, RenderSystems,
39    },
40};
41use bytemuck::{Pod, Zeroable};
42
43/// A marker component that represents an entity that is to be rendered using
44/// our custom phase item.
45///
46/// Note the [`ExtractComponent`] trait implementation: this is necessary to
47/// tell Bevy that this object should be pulled into the render world. Also note
48/// the `on_add` hook, which is needed to tell Bevy's `check_visibility` system
49/// that entities with this component need to be examined for visibility.
50#[derive(Clone, Component, ExtractComponent)]
51#[require(VisibilityClass)]
52#[component(on_add = visibility::add_visibility_class::<CustomRenderedEntity>)]
53struct CustomRenderedEntity;
54
55/// A [`RenderCommand`] that binds the vertex and index buffers and issues the
56/// draw command for our custom phase item.
57struct DrawCustomPhaseItem;
58
59impl<P> RenderCommand<P> for DrawCustomPhaseItem
60where
61    P: PhaseItem,
62{
63    type Param = SRes<CustomPhaseItemBuffers>;
64
65    type ViewQuery = ();
66
67    type ItemQuery = ();
68
69    fn render<'w>(
70        _: &P,
71        _: ROQueryItem<'w, '_, Self::ViewQuery>,
72        _: Option<ROQueryItem<'w, '_, Self::ItemQuery>>,
73        custom_phase_item_buffers: SystemParamItem<'w, '_, Self::Param>,
74        pass: &mut TrackedRenderPass<'w>,
75    ) -> RenderCommandResult {
76        // Borrow check workaround.
77        let custom_phase_item_buffers = custom_phase_item_buffers.into_inner();
78
79        // Tell the GPU where the vertices are.
80        pass.set_vertex_buffer(
81            0,
82            custom_phase_item_buffers
83                .vertices
84                .buffer()
85                .unwrap()
86                .slice(..),
87        );
88
89        // Tell the GPU where the indices are.
90        pass.set_index_buffer(
91            custom_phase_item_buffers
92                .indices
93                .buffer()
94                .unwrap()
95                .slice(..),
96            0,
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        .add_render_command::<Opaque3d, DrawCustomPhaseItemCommands>()
174        .add_systems(
175            Render,
176            prepare_custom_phase_item_buffers.in_set(RenderSystems::Prepare),
177        )
178        .add_systems(Render, queue_custom_phase_item.in_set(RenderSystems::Queue));
179
180    app.run();
181}
182
183/// Spawns the objects in the scene.
184fn setup(mut commands: Commands) {
185    // Spawn a single entity that has custom rendering. It'll be extracted into
186    // the render world via [`ExtractComponent`].
187    commands.spawn((
188        Visibility::default(),
189        Transform::default(),
190        // This `Aabb` is necessary for the visibility checks to work.
191        Aabb {
192            center: Vec3A::ZERO,
193            half_extents: Vec3A::splat(0.5),
194        },
195        CustomRenderedEntity,
196    ));
197
198    // Spawn the camera.
199    commands.spawn((
200        Camera3d::default(),
201        Transform::from_xyz(0.0, 0.0, 1.0).looking_at(Vec3::ZERO, Vec3::Y),
202    ));
203}
204
205/// Creates the [`CustomPhaseItemBuffers`] resource.
206///
207/// This must be done in a startup system because it needs the [`RenderDevice`]
208/// and [`RenderQueue`] to exist, and they don't until [`App::run`] is called.
209fn prepare_custom_phase_item_buffers(mut commands: Commands) {
210    commands.init_resource::<CustomPhaseItemBuffers>();
211}
212
213/// A render-world system that enqueues the entity with custom rendering into
214/// the opaque render phases of each view.
215fn queue_custom_phase_item(
216    pipeline_cache: Res<PipelineCache>,
217    mut pipeline: ResMut<CustomPhasePipeline>,
218    mut opaque_render_phases: ResMut<ViewBinnedRenderPhases<Opaque3d>>,
219    opaque_draw_functions: Res<DrawFunctions<Opaque3d>>,
220    views: Query<(&ExtractedView, &RenderVisibleEntities, &Msaa)>,
221    mut next_tick: Local<Tick>,
222) {
223    let draw_custom_phase_item = opaque_draw_functions
224        .read()
225        .id::<DrawCustomPhaseItemCommands>();
226
227    // Render phases are per-view, so we need to iterate over all views so that
228    // the entity appears in them. (In this example, we have only one view, but
229    // it's good practice to loop over all views anyway.)
230    for (view, view_visible_entities, msaa) in views.iter() {
231        let Some(opaque_phase) = opaque_render_phases.get_mut(&view.retained_view_entity) else {
232            continue;
233        };
234
235        // Find all the custom rendered entities that are visible from this
236        // view.
237        for &entity in view_visible_entities.get::<CustomRenderedEntity>().iter() {
238            // Ordinarily, the [`SpecializedRenderPipeline::Key`] would contain
239            // some per-view settings, such as whether the view is HDR, but for
240            // simplicity's sake we simply hard-code the view's characteristics,
241            // with the exception of number of MSAA samples.
242            let Ok(pipeline_id) = pipeline
243                .variants
244                .specialize(&pipeline_cache, CustomPhaseKey(*msaa))
245            else {
246                continue;
247            };
248
249            // Bump the change tick in order to force Bevy to rebuild the bin.
250            let this_tick = next_tick.get() + 1;
251            next_tick.set(this_tick);
252
253            // Add the custom render item. We use the
254            // [`BinnedRenderPhaseType::NonMesh`] type to skip the special
255            // handling that Bevy has for meshes (preprocessing, indirect
256            // draws, etc.)
257            //
258            // The asset ID is arbitrary; we simply use [`AssetId::invalid`],
259            // but you can use anything you like. Note that the asset ID need
260            // not be the ID of a [`Mesh`].
261            opaque_phase.add(
262                Opaque3dBatchSetKey {
263                    draw_function: draw_custom_phase_item,
264                    pipeline: pipeline_id,
265                    material_bind_group_index: None,
266                    lightmap_slab: None,
267                    vertex_slab: default(),
268                    index_slab: None,
269                },
270                Opaque3dBinKey {
271                    asset_id: AssetId::<Mesh>::invalid().untyped(),
272                },
273                entity,
274                InputUniformIndex::default(),
275                BinnedRenderPhaseType::NonMesh,
276                *next_tick,
277            );
278        }
279    }
280}
281
282struct CustomPhaseSpecializer;
283
284#[derive(Resource)]
285struct CustomPhasePipeline {
286    /// the `variants` collection holds onto the shader handle through the base descriptor
287    variants: Variants<RenderPipeline, CustomPhaseSpecializer>,
288}
289
290impl FromWorld for CustomPhasePipeline {
291    fn from_world(world: &mut World) -> Self {
292        let asset_server = world.resource::<AssetServer>();
293        let shader = asset_server.load("shaders/custom_phase_item.wgsl");
294
295        let base_descriptor = RenderPipelineDescriptor {
296            label: Some("custom render pipeline".into()),
297            vertex: VertexState {
298                shader: shader.clone(),
299                buffers: vec![VertexBufferLayout {
300                    array_stride: size_of::<Vertex>() as u64,
301                    step_mode: VertexStepMode::Vertex,
302                    // This needs to match the layout of [`Vertex`].
303                    attributes: vec![
304                        VertexAttribute {
305                            format: VertexFormat::Float32x3,
306                            offset: 0,
307                            shader_location: 0,
308                        },
309                        VertexAttribute {
310                            format: VertexFormat::Float32x3,
311                            offset: 16,
312                            shader_location: 1,
313                        },
314                    ],
315                }],
316                ..default()
317            },
318            fragment: Some(FragmentState {
319                shader: shader.clone(),
320                targets: vec![Some(ColorTargetState {
321                    // Ordinarily, you'd want to check whether the view has the
322                    // HDR format and substitute the appropriate texture format
323                    // here, but we omit that for simplicity.
324                    format: TextureFormat::bevy_default(),
325                    blend: None,
326                    write_mask: ColorWrites::ALL,
327                })],
328                ..default()
329            }),
330            // Note that if your view has no depth buffer this will need to be
331            // changed.
332            depth_stencil: Some(DepthStencilState {
333                format: CORE_3D_DEPTH_FORMAT,
334                depth_write_enabled: false,
335                depth_compare: CompareFunction::Always,
336                stencil: default(),
337                bias: default(),
338            }),
339            ..default()
340        };
341
342        let variants = Variants::new(CustomPhaseSpecializer, base_descriptor);
343
344        Self { variants }
345    }
346}
347
348#[derive(Copy, Clone, PartialEq, Eq, Hash, SpecializerKey)]
349struct CustomPhaseKey(Msaa);
350
351impl Specializer<RenderPipeline> for CustomPhaseSpecializer {
352    type Key = CustomPhaseKey;
353
354    fn specialize(
355        &self,
356        key: Self::Key,
357        descriptor: &mut RenderPipelineDescriptor,
358    ) -> Result<Canonical<Self::Key>, BevyError> {
359        descriptor.multisample.count = key.0.samples();
360        Ok(key)
361    }
362}
363
364impl FromWorld for CustomPhaseItemBuffers {
365    fn from_world(world: &mut World) -> Self {
366        let render_device = world.resource::<RenderDevice>();
367        let render_queue = world.resource::<RenderQueue>();
368
369        // Create the vertex and index buffers.
370        let mut vbo = RawBufferVec::new(BufferUsages::VERTEX);
371        let mut ibo = RawBufferVec::new(BufferUsages::INDEX);
372
373        for vertex in &VERTICES {
374            vbo.push(*vertex);
375        }
376        for index in 0..3 {
377            ibo.push(index);
378        }
379
380        // These two lines are required in order to trigger the upload to GPU.
381        vbo.write_buffer(render_device, render_queue);
382        ibo.write_buffer(render_device, render_queue);
383
384        CustomPhaseItemBuffers {
385            vertices: vbo,
386            indices: ibo,
387        }
388    }
389}