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