Skip to main content

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