Skip to main content

mesh2d_manual/
mesh2d_manual.rs

1//! This example shows how to manually render 2d items using "mid level render apis" with a custom
2//! pipeline for 2d meshes.
3//! It doesn't use the [`Material2d`] abstraction, but changes the vertex buffer to include vertex color.
4//! Check out the "mesh2d" example for simpler / higher level 2d meshes.
5//!
6//! [`Material2d`]: bevy::sprite_render::Material2d
7
8use bevy::{
9    asset::RenderAssetUsages,
10    color::palettes::basic::YELLOW,
11    core_pipeline::core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT},
12    math::{ops, FloatOrd},
13    mesh::{Indices, MeshVertexAttribute, VertexBufferLayout},
14    prelude::*,
15    render::{
16        mesh::RenderMesh,
17        render_asset::RenderAssets,
18        render_phase::{
19            AddRenderCommand, DrawFunctions, PhaseItemExtraIndex, SetItemPipeline,
20            ViewSortedRenderPhases,
21        },
22        render_resource::{
23            BlendState, ColorTargetState, ColorWrites, CompareFunction, DepthBiasState,
24            DepthStencilState, Face, FragmentState, MultisampleState, PipelineCache,
25            PrimitiveState, PrimitiveTopology, RenderPipelineDescriptor, SpecializedRenderPipeline,
26            SpecializedRenderPipelines, StencilFaceState, StencilState, VertexFormat, VertexState,
27            VertexStepMode,
28        },
29        sync_component::{SyncComponent, SyncComponentPlugin},
30        sync_world::{MainEntityHashMap, RenderEntity},
31        view::{ExtractedView, RenderVisibleEntities},
32        Extract, Render, RenderApp, RenderStartup, RenderSystems,
33    },
34    sprite_render::{
35        extract_mesh2d, init_mesh_2d_pipeline, DrawMesh2d, Material2dBindGroupId, Mesh2dPipeline,
36        Mesh2dPipelineKey, Mesh2dTransforms, MeshFlags, RenderMesh2dInstance, SetMesh2dBindGroup,
37        SetMesh2dViewBindGroup,
38    },
39};
40use std::f32::consts::PI;
41
42fn main() {
43    App::new()
44        .add_plugins((DefaultPlugins, ColoredMesh2dPlugin))
45        .add_systems(Startup, star)
46        .run();
47}
48
49fn star(
50    mut commands: Commands,
51    // We will add a new Mesh for the star being created
52    mut meshes: ResMut<Assets<Mesh>>,
53) {
54    // Let's define the mesh for the object we want to draw: a nice star.
55    // We will specify here what kind of topology is used to define the mesh,
56    // that is, how triangles are built from the vertices. We will use a
57    // triangle list, meaning that each vertex of the triangle has to be
58    // specified. We set `RenderAssetUsages::RENDER_WORLD`, meaning this mesh
59    // will not be accessible in future frames from the `meshes` resource, in
60    // order to save on memory once it has been uploaded to the GPU.
61    let mut star = Mesh::new(
62        PrimitiveTopology::TriangleList,
63        RenderAssetUsages::RENDER_WORLD,
64    );
65
66    // Vertices need to have a position attribute. We will use the following
67    // vertices (I hope you can spot the star in the schema).
68    //
69    //        1
70    //
71    //     10   2
72    // 9      0      3
73    //     8     4
74    //        6
75    //   7        5
76    //
77    // These vertices are specified in 3D space.
78    let mut v_pos = vec![[0.0, 0.0, 0.0]];
79    for i in 0..10 {
80        // The angle between each vertex is 1/10 of a full rotation.
81        let a = i as f32 * PI / 5.0;
82        // The radius of inner vertices (even indices) is 100. For outer vertices (odd indices) it's 200.
83        let r = (1 - i % 2) as f32 * 100.0 + 100.0;
84        // Add the vertex position.
85        v_pos.push([r * ops::sin(a), r * ops::cos(a), 0.0]);
86    }
87    // Set the position attribute
88    star.insert_attribute(Mesh::ATTRIBUTE_POSITION, v_pos);
89    // And a RGB color attribute as well. A built-in `Mesh::ATTRIBUTE_COLOR` exists, but we
90    // use a custom vertex attribute here for demonstration purposes.
91    let mut v_color: Vec<u32> = vec![LinearRgba::BLACK.as_u32()];
92    v_color.extend_from_slice(&[LinearRgba::from(YELLOW).as_u32(); 10]);
93    star.insert_attribute(
94        MeshVertexAttribute::new("Vertex_Color", 1, VertexFormat::Uint32),
95        v_color,
96    );
97
98    // Now, we specify the indices of the vertex that are going to compose the
99    // triangles in our star. Vertices in triangles have to be specified in CCW
100    // winding (that will be the front face, colored). Since we are using
101    // triangle list, we will specify each triangle as 3 vertices
102    //   First triangle: 0, 2, 1
103    //   Second triangle: 0, 3, 2
104    //   Third triangle: 0, 4, 3
105    //   etc
106    //   Last triangle: 0, 1, 10
107    let mut indices = vec![0, 1, 10];
108    for i in 2..=10 {
109        indices.extend_from_slice(&[0, i, i - 1]);
110    }
111    star.insert_indices(Indices::U32(indices));
112
113    // We can now spawn the entities for the star and the camera
114    commands.spawn((
115        // We use a marker component to identify the custom colored meshes
116        ColoredMesh2d,
117        // The `Handle<Mesh>` needs to be wrapped in a `Mesh2d` for 2D rendering
118        Mesh2d(meshes.add(star)),
119    ));
120
121    commands.spawn(Camera2d);
122}
123
124/// A marker component for colored 2d meshes
125#[derive(Component, Default)]
126pub struct ColoredMesh2d;
127
128impl SyncComponent for ColoredMesh2d {
129    type Target = Self;
130}
131
132/// Custom pipeline for 2d meshes with vertex colors
133#[derive(Resource)]
134pub struct ColoredMesh2dPipeline {
135    /// This pipeline wraps the standard [`Mesh2dPipeline`]
136    mesh2d_pipeline: Mesh2dPipeline,
137    /// The shader asset handle.
138    shader: Handle<Shader>,
139}
140
141fn init_colored_mesh_2d_pipeline(
142    mut commands: Commands,
143    mesh2d_pipeline: Res<Mesh2dPipeline>,
144    colored_mesh2d_shader: Res<ColoredMesh2dShader>,
145) {
146    commands.insert_resource(ColoredMesh2dPipeline {
147        mesh2d_pipeline: mesh2d_pipeline.clone(),
148        // Clone the shader from the shader resource we inserted in the plugin.
149        shader: colored_mesh2d_shader.0.clone(),
150    });
151}
152
153// We implement `SpecializedPipeline` to customize the default rendering from `Mesh2dPipeline`
154impl SpecializedRenderPipeline for ColoredMesh2dPipeline {
155    type Key = Mesh2dPipelineKey;
156
157    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
158        // Customize how to store the meshes' vertex attributes in the vertex buffer
159        // Our meshes only have position and color
160        let formats = vec![
161            // Position
162            VertexFormat::Float32x3,
163            // Color
164            VertexFormat::Uint32,
165        ];
166
167        let vertex_layout =
168            VertexBufferLayout::from_vertex_formats(VertexStepMode::Vertex, formats);
169
170        let format = key.target_format();
171
172        RenderPipelineDescriptor {
173            vertex: VertexState {
174                // Use our custom shader
175                shader: self.shader.clone(),
176                // Use our custom vertex buffer
177                buffers: vec![vertex_layout],
178                ..default()
179            },
180            fragment: Some(FragmentState {
181                // Use our custom shader
182                shader: self.shader.clone(),
183                targets: vec![Some(ColorTargetState {
184                    format,
185                    blend: Some(BlendState::ALPHA_BLENDING),
186                    write_mask: ColorWrites::ALL,
187                })],
188                ..default()
189            }),
190            // Use the two standard uniforms for 2d meshes
191            layout: vec![
192                // Bind group 0 is the view uniform
193                self.mesh2d_pipeline.view_layout.clone(),
194                // Bind group 1 is the mesh uniform
195                self.mesh2d_pipeline.mesh_layout.clone(),
196            ],
197            primitive: PrimitiveState {
198                cull_mode: Some(Face::Back),
199                topology: key.primitive_topology(),
200                strip_index_format: key.strip_index_format(),
201                ..default()
202            },
203            depth_stencil: Some(DepthStencilState {
204                format: CORE_2D_DEPTH_FORMAT,
205                depth_write_enabled: Some(false),
206                depth_compare: Some(CompareFunction::GreaterEqual),
207                stencil: StencilState {
208                    front: StencilFaceState::IGNORE,
209                    back: StencilFaceState::IGNORE,
210                    read_mask: 0,
211                    write_mask: 0,
212                },
213                bias: DepthBiasState {
214                    constant: 0,
215                    slope_scale: 0.0,
216                    clamp: 0.0,
217                },
218            }),
219            multisample: MultisampleState {
220                count: key.msaa_samples(),
221                mask: !0,
222                alpha_to_coverage_enabled: false,
223            },
224            label: Some("colored_mesh2d_pipeline".into()),
225            ..default()
226        }
227    }
228}
229
230// This specifies how to render a colored 2d mesh
231type DrawColoredMesh2d = (
232    // Set the pipeline
233    SetItemPipeline,
234    // Set the view uniform as bind group 0
235    SetMesh2dViewBindGroup<0>,
236    // Set the mesh uniform as bind group 1
237    SetMesh2dBindGroup<1>,
238    // Draw the mesh
239    DrawMesh2d,
240);
241
242// The custom shader can be inline like here, included from another file at build time
243// using `include_str!()`, or loaded like any other asset with `asset_server.load()`.
244const COLORED_MESH2D_SHADER: &str = r"
245// Import the standard 2d mesh uniforms and set their bind groups
246#import bevy_sprite::mesh2d_functions
247
248// The structure of the vertex buffer is as specified in `specialize()`
249struct Vertex {
250    @builtin(instance_index) instance_index: u32,
251    @location(0) position: vec3<f32>,
252    @location(1) color: u32,
253};
254
255struct VertexOutput {
256    // The vertex shader must set the on-screen position of the vertex
257    @builtin(position) clip_position: vec4<f32>,
258    // We pass the vertex color to the fragment shader in location 0
259    @location(0) color: vec4<f32>,
260};
261
262/// Entry point for the vertex shader
263@vertex
264fn vertex(vertex: Vertex) -> VertexOutput {
265    var out: VertexOutput;
266    // Project the world position of the mesh into screen position
267    let model = mesh2d_functions::get_world_from_local(vertex.instance_index);
268    out.clip_position = mesh2d_functions::mesh2d_position_local_to_clip(model, vec4<f32>(vertex.position, 1.0));
269    // Unpack the `u32` from the vertex buffer into the `vec4<f32>` used by the fragment shader
270    out.color = vec4<f32>((vec4<u32>(vertex.color) >> vec4<u32>(0u, 8u, 16u, 24u)) & vec4<u32>(255u)) / 255.0;
271    return out;
272}
273
274// The input of the fragment shader must correspond to the output of the vertex shader for all `location`s
275struct FragmentInput {
276    // The color is interpolated between vertices by default
277    @location(0) color: vec4<f32>,
278};
279
280/// Entry point for the fragment shader
281@fragment
282fn fragment(in: FragmentInput) -> @location(0) vec4<f32> {
283    return in.color;
284}
285";
286
287/// Plugin that renders [`ColoredMesh2d`]s
288pub struct ColoredMesh2dPlugin;
289
290/// A resource holding the shader asset handle for the pipeline to take. There are many ways to get
291/// the shader into the pipeline - this is just one option.
292#[derive(Resource)]
293struct ColoredMesh2dShader(Handle<Shader>);
294
295/// Our custom pipeline needs its own instance storage
296#[derive(Resource, Deref, DerefMut, Default)]
297pub struct RenderColoredMesh2dInstances(MainEntityHashMap<RenderMesh2dInstance>);
298
299impl Plugin for ColoredMesh2dPlugin {
300    fn build(&self, app: &mut App) {
301        // Load our custom shader
302        let mut shaders = app.world_mut().resource_mut::<Assets<Shader>>();
303        // Here, we construct and add the shader asset manually. There are many ways to load this
304        // shader, including `embedded_asset`/`load_embedded_asset`.
305        let shader = shaders.add(Shader::from_wgsl(COLORED_MESH2D_SHADER, file!()));
306
307        app.add_plugins(SyncComponentPlugin::<ColoredMesh2d>::default());
308
309        // Register our custom draw function, and add our render systems
310        app.get_sub_app_mut(RenderApp)
311            .unwrap()
312            .insert_resource(ColoredMesh2dShader(shader))
313            .add_render_command::<Transparent2d, DrawColoredMesh2d>()
314            .init_resource::<SpecializedRenderPipelines<ColoredMesh2dPipeline>>()
315            .init_resource::<RenderColoredMesh2dInstances>()
316            .add_systems(
317                RenderStartup,
318                init_colored_mesh_2d_pipeline.after(init_mesh_2d_pipeline),
319            )
320            .add_systems(
321                ExtractSchedule,
322                extract_colored_mesh2d.after(extract_mesh2d),
323            )
324            .add_systems(
325                Render,
326                queue_colored_mesh2d.in_set(RenderSystems::QueueMeshes),
327            );
328    }
329}
330
331/// Extract the [`ColoredMesh2d`] marker component into the render app
332pub fn extract_colored_mesh2d(
333    mut commands: Commands,
334    mut previous_len: Local<usize>,
335    // When extracting, you must use `Extract` to mark the `SystemParam`s
336    // which should be taken from the main world.
337    query: Extract<
338        Query<
339            (
340                Entity,
341                RenderEntity,
342                &ViewVisibility,
343                &GlobalTransform,
344                &Mesh2d,
345            ),
346            With<ColoredMesh2d>,
347        >,
348    >,
349    mut render_mesh_instances: ResMut<RenderColoredMesh2dInstances>,
350) {
351    let mut values = Vec::with_capacity(*previous_len);
352    for (entity, render_entity, view_visibility, transform, handle) in &query {
353        if !view_visibility.get() {
354            continue;
355        }
356
357        let transforms = Mesh2dTransforms {
358            world_from_local: transform.affine().into(),
359            flags: MeshFlags::empty().bits(),
360        };
361
362        values.push((render_entity, ColoredMesh2d));
363        render_mesh_instances.insert(
364            entity.into(),
365            RenderMesh2dInstance {
366                mesh_asset_id: handle.0.id(),
367                transforms,
368                material_bind_group_id: Material2dBindGroupId::default(),
369                automatic_batching: false,
370                tag: 0,
371            },
372        );
373    }
374    *previous_len = values.len();
375    commands.try_insert_batch(values);
376}
377
378/// Queue the 2d meshes marked with [`ColoredMesh2d`] using our custom pipeline and draw function
379pub fn queue_colored_mesh2d(
380    transparent_draw_functions: Res<DrawFunctions<Transparent2d>>,
381    colored_mesh2d_pipeline: Res<ColoredMesh2dPipeline>,
382    mut pipelines: ResMut<SpecializedRenderPipelines<ColoredMesh2dPipeline>>,
383    pipeline_cache: Res<PipelineCache>,
384    render_meshes: Res<RenderAssets<RenderMesh>>,
385    render_mesh_instances: Res<RenderColoredMesh2dInstances>,
386    mut transparent_render_phases: ResMut<ViewSortedRenderPhases<Transparent2d>>,
387    views: Query<(&RenderVisibleEntities, &ExtractedView, &Msaa)>,
388) {
389    if render_mesh_instances.is_empty() {
390        return;
391    }
392    // Iterate each view (a camera is a view)
393    for (visible_entities, view, msaa) in &views {
394        let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity)
395        else {
396            continue;
397        };
398
399        let draw_colored_mesh2d = transparent_draw_functions.read().id::<DrawColoredMesh2d>();
400
401        let mesh_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples())
402            | Mesh2dPipelineKey::from_target_format(view.target_format);
403
404        // Queue all entities visible to that view
405        let Some(visible_entities) = visible_entities.get::<Mesh2d>() else {
406            continue;
407        };
408        for (render_entity, visible_entity) in visible_entities.iter_visible() {
409            if let Some(mesh_instance) = render_mesh_instances.get(visible_entity) {
410                let mesh2d_handle = mesh_instance.mesh_asset_id;
411                let mesh2d_transforms = &mesh_instance.transforms;
412                // Get our specialized pipeline
413                let mut mesh2d_key = mesh_key;
414                let Some(mesh) = render_meshes.get(mesh2d_handle) else {
415                    continue;
416                };
417                mesh2d_key |= Mesh2dPipelineKey::from_primitive_topology_and_strip_index(
418                    mesh.primitive_topology(),
419                    mesh.index_format(),
420                );
421
422                let pipeline_id =
423                    pipelines.specialize(&pipeline_cache, &colored_mesh2d_pipeline, mesh2d_key);
424
425                let mesh_z = mesh2d_transforms.world_from_local.translation.z;
426                transparent_phase.add_retained(Transparent2d {
427                    entity: (*render_entity, *visible_entity),
428                    draw_function: draw_colored_mesh2d,
429                    pipeline: pipeline_id,
430                    // The 2d render items are sorted according to their z value before rendering,
431                    // in order to get correct transparency
432                    sort_key: FloatOrd(mesh_z),
433                    // This material is not batched
434                    batch_range: 0..1,
435                    extra_index: PhaseItemExtraIndex::None,
436                    extracted_index: usize::MAX,
437                    indexed: mesh.indexed(),
438                });
439            }
440        }
441    }
442}