specialized_mesh_pipeline/
specialized_mesh_pipeline.rs

1//! Demonstrates how to define and use specialized mesh pipeline
2//!
3//! This example shows how to use the built-in [`SpecializedMeshPipeline`]
4//! functionality with a custom [`RenderCommand`] to allow custom mesh rendering with
5//! more flexibility than the material api.
6//!
7//! [`SpecializedMeshPipeline`] let's you customize the entire pipeline used when rendering a mesh.
8
9use bevy::{
10    core_pipeline::core_3d::{Opaque3d, Opaque3dBatchSetKey, Opaque3dBinKey, CORE_3D_DEPTH_FORMAT},
11    ecs::{component::Tick, system::StaticSystemParam},
12    math::{vec3, vec4},
13    pbr::{
14        DrawMesh, MeshPipeline, MeshPipelineKey, MeshPipelineViewLayoutKey, RenderMeshInstances,
15        SetMeshBindGroup, SetMeshViewBindGroup,
16    },
17    prelude::*,
18    render::{
19        batching::{
20            gpu_preprocessing::{
21                self, PhaseBatchedInstanceBuffers, PhaseIndirectParametersBuffers,
22                PreprocessWorkItem, UntypedPhaseBatchedInstanceBuffers,
23            },
24            GetBatchData, GetFullBatchData,
25        },
26        experimental::occlusion_culling::OcclusionCulling,
27        extract_component::{ExtractComponent, ExtractComponentPlugin},
28        mesh::{Indices, MeshVertexBufferLayoutRef, PrimitiveTopology, RenderMesh},
29        render_asset::{RenderAssetUsages, RenderAssets},
30        render_phase::{
31            AddRenderCommand, BinnedRenderPhaseType, DrawFunctions, SetItemPipeline,
32            ViewBinnedRenderPhases,
33        },
34        render_resource::{
35            ColorTargetState, ColorWrites, CompareFunction, DepthStencilState, Face, FragmentState,
36            FrontFace, MultisampleState, PipelineCache, PolygonMode, PrimitiveState,
37            RenderPipelineDescriptor, SpecializedMeshPipeline, SpecializedMeshPipelineError,
38            SpecializedMeshPipelines, TextureFormat, VertexState,
39        },
40        view::NoIndirectDrawing,
41        view::{self, ExtractedView, RenderVisibleEntities, ViewTarget, VisibilityClass},
42        Render, RenderApp, RenderSet,
43    },
44};
45
46const SHADER_ASSET_PATH: &str = "shaders/specialized_mesh_pipeline.wgsl";
47
48fn main() {
49    App::new()
50        .add_plugins(DefaultPlugins)
51        .add_plugins(CustomRenderedMeshPipelinePlugin)
52        .add_systems(Startup, setup)
53        .run();
54}
55
56/// Spawns the objects in the scene.
57fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>) {
58    // Build a custom triangle mesh with colors
59    // We define a custom mesh because the examples only uses a limited
60    // set of vertex attributes for simplicity
61    let mesh = Mesh::new(
62        PrimitiveTopology::TriangleList,
63        RenderAssetUsages::default(),
64    )
65    .with_inserted_indices(Indices::U32(vec![0, 1, 2]))
66    .with_inserted_attribute(
67        Mesh::ATTRIBUTE_POSITION,
68        vec![
69            vec3(-0.5, -0.5, 0.0),
70            vec3(0.5, -0.5, 0.0),
71            vec3(0.0, 0.25, 0.0),
72        ],
73    )
74    .with_inserted_attribute(
75        Mesh::ATTRIBUTE_COLOR,
76        vec![
77            vec4(1.0, 0.0, 0.0, 1.0),
78            vec4(0.0, 1.0, 0.0, 1.0),
79            vec4(0.0, 0.0, 1.0, 1.0),
80        ],
81    );
82
83    // spawn 3 triangles to show that batching works
84    for (x, y) in [-0.5, 0.0, 0.5].into_iter().zip([-0.25, 0.5, -0.25]) {
85        // Spawn an entity with all the required components for it to be rendered with our custom pipeline
86        commands.spawn((
87            // We use a marker component to identify the mesh that will be rendered
88            // with our specialized pipeline
89            CustomRenderedEntity,
90            // We need to add the mesh handle to the entity
91            Mesh3d(meshes.add(mesh.clone())),
92            Transform::from_xyz(x, y, 0.0),
93        ));
94    }
95
96    // Spawn the camera.
97    commands.spawn((
98        Camera3d::default(),
99        // Move the camera back a bit to see all the triangles
100        Transform::from_xyz(0.0, 0.0, 3.0).looking_at(Vec3::ZERO, Vec3::Y),
101    ));
102}
103
104// When writing custom rendering code it's generally recommended to use a plugin.
105// The main reason for this is that it gives you access to the finish() hook
106// which is called after rendering resources are initialized.
107struct CustomRenderedMeshPipelinePlugin;
108impl Plugin for CustomRenderedMeshPipelinePlugin {
109    fn build(&self, app: &mut App) {
110        app.add_plugins(ExtractComponentPlugin::<CustomRenderedEntity>::default());
111
112        // We make sure to add these to the render app, not the main app.
113        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
114            return;
115        };
116        render_app
117            // This is needed to tell bevy about your custom pipeline
118            .init_resource::<SpecializedMeshPipelines<CustomMeshPipeline>>()
119            // We need to use a custom draw command so we need to register it
120            .add_render_command::<Opaque3d, DrawSpecializedPipelineCommands>()
121            .add_systems(Render, queue_custom_mesh_pipeline.in_set(RenderSet::Queue));
122    }
123
124    fn finish(&self, app: &mut App) {
125        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
126            return;
127        };
128        // Creating this pipeline needs the RenderDevice and RenderQueue
129        // which are only available once rendering plugins are initialized.
130        render_app.init_resource::<CustomMeshPipeline>();
131    }
132}
133
134/// A marker component that represents an entity that is to be rendered using
135/// our specialized pipeline.
136///
137/// Note the [`ExtractComponent`] trait implementation: this is necessary to
138/// tell Bevy that this object should be pulled into the render world. Also note
139/// the `on_add` hook, which is needed to tell Bevy's `check_visibility` system
140/// that entities with this component need to be examined for visibility.
141#[derive(Clone, Component, ExtractComponent)]
142#[require(VisibilityClass)]
143#[component(on_add = view::add_visibility_class::<CustomRenderedEntity>)]
144struct CustomRenderedEntity;
145
146/// The custom draw commands that Bevy executes for each entity we enqueue into
147/// the render phase.
148type DrawSpecializedPipelineCommands = (
149    // Set the pipeline
150    SetItemPipeline,
151    // Set the view uniform at bind group 0
152    SetMeshViewBindGroup<0>,
153    // Set the mesh uniform at bind group 1
154    SetMeshBindGroup<1>,
155    // Draw the mesh
156    DrawMesh,
157);
158
159// This contains the state needed to specialize a mesh pipeline
160#[derive(Resource)]
161struct CustomMeshPipeline {
162    /// The base mesh pipeline defined by bevy
163    ///
164    /// This isn't required, but if you want to use a bevy `Mesh` it's easier when you
165    /// have access to the base `MeshPipeline` that bevy already defines
166    mesh_pipeline: MeshPipeline,
167    /// Stores the shader used for this pipeline directly on the pipeline.
168    /// This isn't required, it's only done like this for simplicity.
169    shader_handle: Handle<Shader>,
170}
171impl FromWorld for CustomMeshPipeline {
172    fn from_world(world: &mut World) -> Self {
173        // Load the shader
174        let shader_handle: Handle<Shader> = world.resource::<AssetServer>().load(SHADER_ASSET_PATH);
175        Self {
176            mesh_pipeline: MeshPipeline::from_world(world),
177            shader_handle,
178        }
179    }
180}
181
182impl SpecializedMeshPipeline for CustomMeshPipeline {
183    /// Pipeline use keys to determine how to specialize it.
184    /// The key is also used by the pipeline cache to determine if
185    /// it needs to create a new pipeline or not
186    ///
187    /// In this example we just use the base `MeshPipelineKey` defined by bevy, but this could be anything.
188    /// For example, if you want to make a pipeline with a procedural shader you could add the Handle<Shader> to the key.
189    type Key = MeshPipelineKey;
190
191    fn specialize(
192        &self,
193        mesh_key: Self::Key,
194        layout: &MeshVertexBufferLayoutRef,
195    ) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
196        // Define the vertex attributes based on a standard bevy [`Mesh`]
197        let mut vertex_attributes = Vec::new();
198        if layout.0.contains(Mesh::ATTRIBUTE_POSITION) {
199            // Make sure this matches the shader location
200            vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0));
201        }
202        if layout.0.contains(Mesh::ATTRIBUTE_COLOR) {
203            // Make sure this matches the shader location
204            vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(1));
205        }
206        // This will automatically generate the correct `VertexBufferLayout` based on the vertex attributes
207        let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?;
208
209        Ok(RenderPipelineDescriptor {
210            label: Some("Specialized Mesh Pipeline".into()),
211            layout: vec![
212                // Bind group 0 is the view uniform
213                self.mesh_pipeline
214                    .get_view_layout(MeshPipelineViewLayoutKey::from(mesh_key))
215                    .clone(),
216                // Bind group 1 is the mesh uniform
217                self.mesh_pipeline.mesh_layouts.model_only.clone(),
218            ],
219            push_constant_ranges: vec![],
220            vertex: VertexState {
221                shader: self.shader_handle.clone(),
222                shader_defs: vec![],
223                entry_point: "vertex".into(),
224                // Customize how to store the meshes' vertex attributes in the vertex buffer
225                buffers: vec![vertex_buffer_layout],
226            },
227            fragment: Some(FragmentState {
228                shader: self.shader_handle.clone(),
229                shader_defs: vec![],
230                entry_point: "fragment".into(),
231                targets: vec![Some(ColorTargetState {
232                    // This isn't required, but bevy supports HDR and non-HDR rendering
233                    // so it's generally recommended to specialize the pipeline for that
234                    format: if mesh_key.contains(MeshPipelineKey::HDR) {
235                        ViewTarget::TEXTURE_FORMAT_HDR
236                    } else {
237                        TextureFormat::bevy_default()
238                    },
239                    // For this example we only use opaque meshes,
240                    // but if you wanted to use alpha blending you would need to set it here
241                    blend: None,
242                    write_mask: ColorWrites::ALL,
243                })],
244            }),
245            primitive: PrimitiveState {
246                topology: mesh_key.primitive_topology(),
247                front_face: FrontFace::Ccw,
248                cull_mode: Some(Face::Back),
249                polygon_mode: PolygonMode::Fill,
250                ..default()
251            },
252            // Note that if your view has no depth buffer this will need to be
253            // changed.
254            depth_stencil: Some(DepthStencilState {
255                format: CORE_3D_DEPTH_FORMAT,
256                depth_write_enabled: true,
257                depth_compare: CompareFunction::GreaterEqual,
258                stencil: default(),
259                bias: default(),
260            }),
261            // It's generally recommended to specialize your pipeline for MSAA,
262            // but it's not always possible
263            multisample: MultisampleState {
264                count: mesh_key.msaa_samples(),
265                ..MultisampleState::default()
266            },
267            zero_initialize_workgroup_memory: false,
268        })
269    }
270}
271
272/// A render-world system that enqueues the entity with custom rendering into
273/// the opaque render phases of each view.
274fn queue_custom_mesh_pipeline(
275    pipeline_cache: Res<PipelineCache>,
276    custom_mesh_pipeline: Res<CustomMeshPipeline>,
277    (mut opaque_render_phases, opaque_draw_functions): (
278        ResMut<ViewBinnedRenderPhases<Opaque3d>>,
279        Res<DrawFunctions<Opaque3d>>,
280    ),
281    mut specialized_mesh_pipelines: ResMut<SpecializedMeshPipelines<CustomMeshPipeline>>,
282    views: Query<(
283        &RenderVisibleEntities,
284        &ExtractedView,
285        &Msaa,
286        Has<NoIndirectDrawing>,
287        Has<OcclusionCulling>,
288    )>,
289    (render_meshes, render_mesh_instances): (
290        Res<RenderAssets<RenderMesh>>,
291        Res<RenderMeshInstances>,
292    ),
293    param: StaticSystemParam<<MeshPipeline as GetBatchData>::Param>,
294    mut phase_batched_instance_buffers: ResMut<
295        PhaseBatchedInstanceBuffers<Opaque3d, <MeshPipeline as GetBatchData>::BufferData>,
296    >,
297    mut phase_indirect_parameters_buffers: ResMut<PhaseIndirectParametersBuffers<Opaque3d>>,
298    mut change_tick: Local<Tick>,
299) {
300    let system_param_item = param.into_inner();
301
302    let UntypedPhaseBatchedInstanceBuffers {
303        ref mut data_buffer,
304        ref mut work_item_buffers,
305        ref mut late_indexed_indirect_parameters_buffer,
306        ref mut late_non_indexed_indirect_parameters_buffer,
307        ..
308    } = phase_batched_instance_buffers.buffers;
309
310    // Get the id for our custom draw function
311    let draw_function_id = opaque_draw_functions
312        .read()
313        .id::<DrawSpecializedPipelineCommands>();
314
315    // Render phases are per-view, so we need to iterate over all views so that
316    // the entity appears in them. (In this example, we have only one view, but
317    // it's good practice to loop over all views anyway.)
318    for (view_visible_entities, view, msaa, no_indirect_drawing, gpu_occlusion_culling) in
319        views.iter()
320    {
321        let Some(opaque_phase) = opaque_render_phases.get_mut(&view.retained_view_entity) else {
322            continue;
323        };
324
325        // Create *work item buffers* if necessary. Work item buffers store the
326        // indices of meshes that are to be rendered when indirect drawing is
327        // enabled.
328        let work_item_buffer = gpu_preprocessing::get_or_create_work_item_buffer::<Opaque3d>(
329            work_item_buffers,
330            view.retained_view_entity,
331            no_indirect_drawing,
332            gpu_occlusion_culling,
333        );
334
335        // Initialize those work item buffers in preparation for this new frame.
336        gpu_preprocessing::init_work_item_buffers(
337            work_item_buffer,
338            late_indexed_indirect_parameters_buffer,
339            late_non_indexed_indirect_parameters_buffer,
340        );
341
342        // Create the key based on the view. In this case we only care about MSAA and HDR
343        let view_key = MeshPipelineKey::from_msaa_samples(msaa.samples())
344            | MeshPipelineKey::from_hdr(view.hdr);
345
346        // Set up a slot to hold information about the batch set we're going to
347        // create. If there are any of our custom meshes in the scene, we'll
348        // need this information in order for Bevy to kick off the rendering.
349        let mut mesh_batch_set_info = None;
350
351        // Find all the custom rendered entities that are visible from this
352        // view.
353        for &(render_entity, visible_entity) in
354            view_visible_entities.get::<CustomRenderedEntity>().iter()
355        {
356            // Get the mesh instance
357            let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(visible_entity)
358            else {
359                continue;
360            };
361
362            // Get the mesh data
363            let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else {
364                continue;
365            };
366
367            // Specialize the key for the current mesh entity
368            // For this example we only specialize based on the mesh topology
369            // but you could have more complex keys and that's where you'd need to create those keys
370            let mut mesh_key = view_key;
371            mesh_key |= MeshPipelineKey::from_primitive_topology(mesh.primitive_topology());
372
373            // Initialize the batch set information if this was the first custom
374            // mesh we saw. We'll need that information later to create the
375            // batch set.
376            if mesh_batch_set_info.is_none() {
377                mesh_batch_set_info = Some(MeshBatchSetInfo {
378                    indirect_parameters_index: phase_indirect_parameters_buffers
379                        .buffers
380                        .allocate(mesh.indexed(), 1),
381                    is_indexed: mesh.indexed(),
382                });
383            }
384            let mesh_info = mesh_batch_set_info.unwrap();
385
386            // Allocate some input and output indices. We'll need these to
387            // create the *work item* below.
388            let Some(input_index) =
389                MeshPipeline::get_binned_index(&system_param_item, visible_entity)
390            else {
391                continue;
392            };
393            let output_index = data_buffer.add() as u32;
394
395            // Finally, we can specialize the pipeline based on the key
396            let pipeline_id = specialized_mesh_pipelines
397                .specialize(
398                    &pipeline_cache,
399                    &custom_mesh_pipeline,
400                    mesh_key,
401                    &mesh.layout,
402                )
403                // This should never with this example, but if your pipeline specialization
404                // can fail you need to handle the error here
405                .expect("Failed to specialize mesh pipeline");
406
407            // Bump the change tick so that Bevy is forced to rebuild the bin.
408            let next_change_tick = change_tick.get() + 1;
409            change_tick.set(next_change_tick);
410
411            // Add the mesh with our specialized pipeline
412            opaque_phase.add(
413                Opaque3dBatchSetKey {
414                    draw_function: draw_function_id,
415                    pipeline: pipeline_id,
416                    material_bind_group_index: None,
417                    vertex_slab: default(),
418                    index_slab: None,
419                    lightmap_slab: None,
420                },
421                // The asset ID is arbitrary; we simply use [`AssetId::invalid`],
422                // but you can use anything you like. Note that the asset ID need
423                // not be the ID of a [`Mesh`].
424                Opaque3dBinKey {
425                    asset_id: AssetId::<Mesh>::invalid().untyped(),
426                },
427                (render_entity, visible_entity),
428                mesh_instance.current_uniform_index,
429                // This example supports batching, but if your pipeline doesn't
430                // support it you can use `BinnedRenderPhaseType::UnbatchableMesh`
431                BinnedRenderPhaseType::BatchableMesh,
432                *change_tick,
433            );
434
435            // Create a *work item*. A work item tells the Bevy renderer to
436            // transform the mesh on GPU.
437            work_item_buffer.push(
438                mesh.indexed(),
439                PreprocessWorkItem {
440                    input_index: input_index.into(),
441                    output_or_indirect_parameters_index: if no_indirect_drawing {
442                        output_index
443                    } else {
444                        mesh_info.indirect_parameters_index
445                    },
446                },
447            );
448        }
449
450        // Now if there were any meshes, we need to add a command to the
451        // indirect parameters buffer, so that the renderer will end up
452        // enqueuing a command to draw the mesh.
453        if let Some(mesh_info) = mesh_batch_set_info {
454            phase_indirect_parameters_buffers
455                .buffers
456                .add_batch_set(mesh_info.is_indexed, mesh_info.indirect_parameters_index);
457        }
458    }
459}
460
461// If we end up having any custom meshes to draw, this contains information
462// needed to create the batch set.
463#[derive(Clone, Copy)]
464struct MeshBatchSetInfo {
465    /// The first index of the mesh batch in the indirect parameters buffer.
466    indirect_parameters_index: u32,
467    /// Whether the mesh is indexed (has an index buffer).
468    is_indexed: bool,
469}