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    asset::RenderAssetUsages,
11    camera::visibility::{self, VisibilityClass},
12    core_pipeline::core_3d::{Opaque3d, Opaque3dBatchSetKey, Opaque3dBinKey, CORE_3D_DEPTH_FORMAT},
13    ecs::component::Tick,
14    math::{vec3, vec4},
15    mesh::{Indices, MeshVertexBufferLayoutRef, PrimitiveTopology},
16    pbr::{
17        DrawMesh, MeshPipeline, MeshPipelineKey, MeshPipelineViewLayoutKey, RenderMeshInstances,
18        SetMeshBindGroup, SetMeshViewBindGroup, SetMeshViewEmptyBindGroup,
19    },
20    prelude::*,
21    render::{
22        batching::gpu_preprocessing::GpuPreprocessingSupport,
23        extract_component::{ExtractComponent, ExtractComponentPlugin},
24        mesh::{allocator::MeshAllocator, RenderMesh},
25        render_asset::RenderAssets,
26        render_phase::{
27            AddRenderCommand, BinnedRenderPhaseType, DrawFunctions, SetItemPipeline,
28            ViewBinnedRenderPhases,
29        },
30        render_resource::{
31            ColorTargetState, ColorWrites, CompareFunction, DepthStencilState, Face, FragmentState,
32            FrontFace, MultisampleState, PipelineCache, PolygonMode, PrimitiveState,
33            RenderPipelineDescriptor, SpecializedMeshPipeline, SpecializedMeshPipelineError,
34            SpecializedMeshPipelines, TextureFormat, VertexState,
35        },
36        view::{ExtractedView, RenderVisibleEntities, ViewTarget},
37        Render, RenderApp, RenderStartup, RenderSystems,
38    },
39};
40
41const SHADER_ASSET_PATH: &str = "shaders/specialized_mesh_pipeline.wgsl";
42
43fn main() {
44    App::new()
45        .add_plugins(DefaultPlugins)
46        .add_plugins(CustomRenderedMeshPipelinePlugin)
47        .add_systems(Startup, setup)
48        .run();
49}
50
51/// Spawns the objects in the scene.
52fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>) {
53    // Build a custom triangle mesh with colors
54    // We define a custom mesh because the examples only uses a limited
55    // set of vertex attributes for simplicity
56    let mesh = Mesh::new(
57        PrimitiveTopology::TriangleList,
58        RenderAssetUsages::default(),
59    )
60    .with_inserted_indices(Indices::U32(vec![0, 1, 2]))
61    .with_inserted_attribute(
62        Mesh::ATTRIBUTE_POSITION,
63        vec![
64            vec3(-0.5, -0.5, 0.0),
65            vec3(0.5, -0.5, 0.0),
66            vec3(0.0, 0.25, 0.0),
67        ],
68    )
69    .with_inserted_attribute(
70        Mesh::ATTRIBUTE_COLOR,
71        vec![
72            vec4(1.0, 0.0, 0.0, 1.0),
73            vec4(0.0, 1.0, 0.0, 1.0),
74            vec4(0.0, 0.0, 1.0, 1.0),
75        ],
76    );
77
78    // spawn 3 triangles to show that batching works
79    for (x, y) in [-0.5, 0.0, 0.5].into_iter().zip([-0.25, 0.5, -0.25]) {
80        // Spawn an entity with all the required components for it to be rendered with our custom pipeline
81        commands.spawn((
82            // We use a marker component to identify the mesh that will be rendered
83            // with our specialized pipeline
84            CustomRenderedEntity,
85            // We need to add the mesh handle to the entity
86            Mesh3d(meshes.add(mesh.clone())),
87            Transform::from_xyz(x, y, 0.0),
88        ));
89    }
90
91    // Spawn the camera.
92    commands.spawn((
93        Camera3d::default(),
94        // Move the camera back a bit to see all the triangles
95        Transform::from_xyz(0.0, 0.0, 3.0).looking_at(Vec3::ZERO, Vec3::Y),
96    ));
97}
98
99// When writing custom rendering code it's generally recommended to use a plugin.
100// The main reason for this is that it gives you access to the finish() hook
101// which is called after rendering resources are initialized.
102struct CustomRenderedMeshPipelinePlugin;
103impl Plugin for CustomRenderedMeshPipelinePlugin {
104    fn build(&self, app: &mut App) {
105        app.add_plugins(ExtractComponentPlugin::<CustomRenderedEntity>::default());
106
107        // We make sure to add these to the render app, not the main app.
108        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
109            return;
110        };
111        render_app
112            // This is needed to tell bevy about your custom pipeline
113            .init_resource::<SpecializedMeshPipelines<CustomMeshPipeline>>()
114            // We need to use a custom draw command so we need to register it
115            .add_render_command::<Opaque3d, DrawSpecializedPipelineCommands>()
116            .add_systems(RenderStartup, init_custom_mesh_pipeline)
117            .add_systems(
118                Render,
119                queue_custom_mesh_pipeline.in_set(RenderSystems::Queue),
120            );
121    }
122}
123
124/// A marker component that represents an entity that is to be rendered using
125/// our specialized pipeline.
126///
127/// Note the [`ExtractComponent`] trait implementation: this is necessary to
128/// tell Bevy that this object should be pulled into the render world. Also note
129/// the `on_add` hook, which is needed to tell Bevy's `check_visibility` system
130/// that entities with this component need to be examined for visibility.
131#[derive(Clone, Component, ExtractComponent)]
132#[require(VisibilityClass)]
133#[component(on_add = visibility::add_visibility_class::<CustomRenderedEntity>)]
134struct CustomRenderedEntity;
135
136/// The custom draw commands that Bevy executes for each entity we enqueue into
137/// the render phase.
138type DrawSpecializedPipelineCommands = (
139    // Set the pipeline
140    SetItemPipeline,
141    // Set the view uniform at bind group 0
142    SetMeshViewBindGroup<0>,
143    // Set an empty material bind group at bind group 1
144    SetMeshViewEmptyBindGroup<1>,
145    // Set the mesh uniform at bind group 2
146    SetMeshBindGroup<2>,
147    // Draw the mesh
148    DrawMesh,
149);
150
151// This contains the state needed to specialize a mesh pipeline
152#[derive(Resource)]
153struct CustomMeshPipeline {
154    /// The base mesh pipeline defined by bevy
155    ///
156    /// This isn't required, but if you want to use a bevy `Mesh` it's easier when you
157    /// have access to the base `MeshPipeline` that bevy already defines
158    mesh_pipeline: MeshPipeline,
159    /// Stores the shader used for this pipeline directly on the pipeline.
160    /// This isn't required, it's only done like this for simplicity.
161    shader_handle: Handle<Shader>,
162}
163
164fn init_custom_mesh_pipeline(
165    mut commands: Commands,
166    asset_server: Res<AssetServer>,
167    mesh_pipeline: Res<MeshPipeline>,
168) {
169    // Load the shader
170    let shader_handle: Handle<Shader> = asset_server.load(SHADER_ASSET_PATH);
171    commands.insert_resource(CustomMeshPipeline {
172        mesh_pipeline: mesh_pipeline.clone(),
173        shader_handle,
174    });
175}
176
177impl SpecializedMeshPipeline for CustomMeshPipeline {
178    /// Pipeline use keys to determine how to specialize it.
179    /// The key is also used by the pipeline cache to determine if
180    /// it needs to create a new pipeline or not
181    ///
182    /// In this example we just use the base `MeshPipelineKey` defined by bevy, but this could be anything.
183    /// For example, if you want to make a pipeline with a procedural shader you could add the Handle<Shader> to the key.
184    type Key = MeshPipelineKey;
185
186    fn specialize(
187        &self,
188        mesh_key: Self::Key,
189        layout: &MeshVertexBufferLayoutRef,
190    ) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
191        // Define the vertex attributes based on a standard bevy [`Mesh`]
192        let mut vertex_attributes = Vec::new();
193        if layout.0.contains(Mesh::ATTRIBUTE_POSITION) {
194            // Make sure this matches the shader location
195            vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0));
196        }
197        if layout.0.contains(Mesh::ATTRIBUTE_COLOR) {
198            // Make sure this matches the shader location
199            vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(1));
200        }
201        // This will automatically generate the correct `VertexBufferLayout` based on the vertex attributes
202        let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?;
203
204        let view_layout = self
205            .mesh_pipeline
206            .get_view_layout(MeshPipelineViewLayoutKey::from(mesh_key));
207
208        Ok(RenderPipelineDescriptor {
209            label: Some("Specialized Mesh Pipeline".into()),
210            layout: vec![
211                view_layout.main_layout.clone(),
212                view_layout.empty_layout.clone(),
213                self.mesh_pipeline.mesh_layouts.model_only.clone(),
214            ],
215            vertex: VertexState {
216                shader: self.shader_handle.clone(),
217                // Customize how to store the meshes' vertex attributes in the vertex buffer
218                buffers: vec![vertex_buffer_layout],
219                ..default()
220            },
221            fragment: Some(FragmentState {
222                shader: self.shader_handle.clone(),
223                targets: vec![Some(ColorTargetState {
224                    // This isn't required, but bevy supports HDR and non-HDR rendering
225                    // so it's generally recommended to specialize the pipeline for that
226                    format: if mesh_key.contains(MeshPipelineKey::HDR) {
227                        ViewTarget::TEXTURE_FORMAT_HDR
228                    } else {
229                        TextureFormat::bevy_default()
230                    },
231                    // For this example we only use opaque meshes,
232                    // but if you wanted to use alpha blending you would need to set it here
233                    blend: None,
234                    write_mask: ColorWrites::ALL,
235                })],
236                ..default()
237            }),
238            primitive: PrimitiveState {
239                topology: mesh_key.primitive_topology(),
240                front_face: FrontFace::Ccw,
241                cull_mode: Some(Face::Back),
242                polygon_mode: PolygonMode::Fill,
243                ..default()
244            },
245            // Note that if your view has no depth buffer this will need to be
246            // changed.
247            depth_stencil: Some(DepthStencilState {
248                format: CORE_3D_DEPTH_FORMAT,
249                depth_write_enabled: true,
250                depth_compare: CompareFunction::GreaterEqual,
251                stencil: default(),
252                bias: default(),
253            }),
254            // It's generally recommended to specialize your pipeline for MSAA,
255            // but it's not always possible
256            multisample: MultisampleState {
257                count: mesh_key.msaa_samples(),
258                ..default()
259            },
260            ..default()
261        })
262    }
263}
264
265/// A render-world system that enqueues the entity with custom rendering into
266/// the opaque render phases of each view.
267fn queue_custom_mesh_pipeline(
268    pipeline_cache: Res<PipelineCache>,
269    custom_mesh_pipeline: Res<CustomMeshPipeline>,
270    (mut opaque_render_phases, opaque_draw_functions): (
271        ResMut<ViewBinnedRenderPhases<Opaque3d>>,
272        Res<DrawFunctions<Opaque3d>>,
273    ),
274    mut specialized_mesh_pipelines: ResMut<SpecializedMeshPipelines<CustomMeshPipeline>>,
275    views: Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
276    (render_meshes, render_mesh_instances): (
277        Res<RenderAssets<RenderMesh>>,
278        Res<RenderMeshInstances>,
279    ),
280    mut change_tick: Local<Tick>,
281    mesh_allocator: Res<MeshAllocator>,
282    gpu_preprocessing_support: Res<GpuPreprocessingSupport>,
283) {
284    // Get the id for our custom draw function
285    let draw_function = opaque_draw_functions
286        .read()
287        .id::<DrawSpecializedPipelineCommands>();
288
289    // Render phases are per-view, so we need to iterate over all views so that
290    // the entity appears in them. (In this example, we have only one view, but
291    // it's good practice to loop over all views anyway.)
292    for (view_visible_entities, view, msaa) in views.iter() {
293        let Some(opaque_phase) = opaque_render_phases.get_mut(&view.retained_view_entity) else {
294            continue;
295        };
296
297        // Create the key based on the view. In this case we only care about MSAA and HDR
298        let view_key = MeshPipelineKey::from_msaa_samples(msaa.samples())
299            | MeshPipelineKey::from_hdr(view.hdr);
300
301        // Find all the custom rendered entities that are visible from this
302        // view.
303        for &(render_entity, visible_entity) in
304            view_visible_entities.get::<CustomRenderedEntity>().iter()
305        {
306            // Get the mesh instance
307            let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(visible_entity)
308            else {
309                continue;
310            };
311
312            // Get the mesh data
313            let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else {
314                continue;
315            };
316
317            let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id);
318
319            // Specialize the key for the current mesh entity
320            // For this example we only specialize based on the mesh topology
321            // but you could have more complex keys and that's where you'd need to create those keys
322            let mut mesh_key = view_key;
323            mesh_key |= MeshPipelineKey::from_primitive_topology(mesh.primitive_topology());
324
325            // Finally, we can specialize the pipeline based on the key
326            let pipeline_id = specialized_mesh_pipelines
327                .specialize(
328                    &pipeline_cache,
329                    &custom_mesh_pipeline,
330                    mesh_key,
331                    &mesh.layout,
332                )
333                // This should never happen with this example, but if your pipeline
334                // specialization can fail you need to handle the error here
335                .expect("Failed to specialize mesh pipeline");
336
337            // Bump the change tick so that Bevy is forced to rebuild the bin.
338            let next_change_tick = change_tick.get() + 1;
339            change_tick.set(next_change_tick);
340
341            // Add the mesh with our specialized pipeline
342            opaque_phase.add(
343                Opaque3dBatchSetKey {
344                    draw_function,
345                    pipeline: pipeline_id,
346                    material_bind_group_index: None,
347                    vertex_slab: vertex_slab.unwrap_or_default(),
348                    index_slab,
349                    lightmap_slab: None,
350                },
351                // For this example we can use the mesh asset id as the bin key,
352                // but you can use any asset_id as a key
353                Opaque3dBinKey {
354                    asset_id: mesh_instance.mesh_asset_id.into(),
355                },
356                (render_entity, visible_entity),
357                mesh_instance.current_uniform_index,
358                // This example supports batching and multi draw indirect,
359                // but if your pipeline doesn't support it you can use
360                // `BinnedRenderPhaseType::UnbatchableMesh`
361                BinnedRenderPhaseType::mesh(
362                    mesh_instance.should_batch(),
363                    &gpu_preprocessing_support,
364                ),
365                *change_tick,
366            );
367        }
368    }
369}