Skip to content

Commit e369a8a

Browse files
parasytecart
andcommitted
Mesh vertex buffer layouts (#3959)
This PR makes a number of changes to how meshes and vertex attributes are handled, which the goal of enabling easy and flexible custom vertex attributes: * Reworks the `Mesh` type to use the newly added `VertexAttribute` internally * `VertexAttribute` defines the name, a unique `VertexAttributeId`, and a `VertexFormat` * `VertexAttributeId` is used to produce consistent sort orders for vertex buffer generation, replacing the more expensive and often surprising "name based sorting" * Meshes can be used to generate a `MeshVertexBufferLayout`, which defines the layout of the gpu buffer produced by the mesh. `MeshVertexBufferLayouts` can then be used to generate actual `VertexBufferLayouts` according to the requirements of a specific pipeline. This decoupling of "mesh layout" vs "pipeline vertex buffer layout" is what enables custom attributes. We don't need to standardize _mesh layouts_ or contort meshes to meet the needs of a specific pipeline. As long as the mesh has what the pipeline needs, it will work transparently. * Mesh-based pipelines now specialize on `&MeshVertexBufferLayout` via the new `SpecializedMeshPipeline` trait (which behaves like `SpecializedPipeline`, but adds `&MeshVertexBufferLayout`). The integrity of the pipeline cache is maintained because the `MeshVertexBufferLayout` is treated as part of the key (which is fully abstracted from implementers of the trait ... no need to add any additional info to the specialization key). * Hashing `MeshVertexBufferLayout` is too expensive to do for every entity, every frame. To make this scalable, I added a generalized "pre-hashing" solution to `bevy_utils`: `Hashed<T>` keys and `PreHashMap<K, V>` (which uses `Hashed<T>` internally) . Why didn't I just do the quick and dirty in-place "pre-compute hash and use that u64 as a key in a hashmap" that we've done in the past? Because its wrong! Hashes by themselves aren't enough because two different values can produce the same hash. Re-hashing a hash is even worse! I decided to build a generalized solution because this pattern has come up in the past and we've chosen to do the wrong thing. Now we can do the right thing! This did unfortunately require pulling in `hashbrown` and using that in `bevy_utils`, because avoiding re-hashes requires the `raw_entry_mut` api, which isn't stabilized yet (and may never be ... `entry_ref` has favor now, but also isn't available yet). If std's HashMap ever provides the tools we need, we can move back to that. Note that adding `hashbrown` doesn't increase our dependency count because it was already in our tree. I will probably break these changes out into their own PR. * Specializing on `MeshVertexBufferLayout` has one non-obvious behavior: it can produce identical pipelines for two different MeshVertexBufferLayouts. To optimize the number of active pipelines / reduce re-binds while drawing, I de-duplicate pipelines post-specialization using the final `VertexBufferLayout` as the key. For example, consider a pipeline that needs the layout `(position, normal)` and is specialized using two meshes: `(position, normal, uv)` and `(position, normal, other_vec2)`. If both of these meshes result in `(position, normal)` specializations, we can use the same pipeline! Now we do. Cool! To briefly illustrate, this is what the relevant section of `MeshPipeline`'s specialization code looks like now: ```rust impl SpecializedMeshPipeline for MeshPipeline { type Key = MeshPipelineKey; fn specialize( &self, key: Self::Key, layout: &MeshVertexBufferLayout, ) -> RenderPipelineDescriptor { let mut vertex_attributes = vec![ Mesh::ATTRIBUTE_POSITION.at_shader_location(0), Mesh::ATTRIBUTE_NORMAL.at_shader_location(1), Mesh::ATTRIBUTE_UV_0.at_shader_location(2), ]; let mut shader_defs = Vec::new(); if layout.contains(Mesh::ATTRIBUTE_TANGENT) { shader_defs.push(String::from("VERTEX_TANGENTS")); vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(3)); } let vertex_buffer_layout = layout .get_layout(&vertex_attributes) .expect("Mesh is missing a vertex attribute"); ``` Notice that this is _much_ simpler than it was before. And now any mesh with any layout can be used with this pipeline, provided it has vertex postions, normals, and uvs. We even got to remove `HAS_TANGENTS` from MeshPipelineKey and `has_tangents` from `GpuMesh`, because that information is redundant with `MeshVertexBufferLayout`. This is still a draft because I still need to: * Add more docs * Experiment with adding error handling to mesh pipeline specialization (which would print errors at runtime when a mesh is missing a vertex attribute required by a pipeline). If it doesn't tank perf, we'll keep it. * Consider breaking out the PreHash / hashbrown changes into a separate PR. * Add an example illustrating this change * Verify that the "mesh-specialized pipeline de-duplication code" works properly Please dont yell at me for not doing these things yet :) Just trying to get this in peoples' hands asap. Alternative to #3120 Fixes #3030 Co-authored-by: Carter Anderson <[email protected]>
1 parent b3a2cbb commit e369a8a

30 files changed

+1025
-544
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,10 @@ name = "scene"
447447
path = "examples/scene/scene.rs"
448448

449449
# Shaders
450+
[[example]]
451+
name = "custom_vertex_attribute"
452+
path = "examples/shader/custom_vertex_attribute.rs"
453+
450454
[[example]]
451455
name = "shader_defs"
452456
path = "examples/shader/shader_defs.rs"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#import bevy_pbr::mesh_view_bind_group
2+
#import bevy_pbr::mesh_struct
3+
4+
struct Vertex {
5+
[[location(0)]] position: vec3<f32>;
6+
[[location(1)]] blend_color: vec4<f32>;
7+
};
8+
9+
struct CustomMaterial {
10+
color: vec4<f32>;
11+
};
12+
[[group(1), binding(0)]]
13+
var<uniform> material: CustomMaterial;
14+
15+
[[group(2), binding(0)]]
16+
var<uniform> mesh: Mesh;
17+
18+
struct VertexOutput {
19+
[[builtin(position)]] clip_position: vec4<f32>;
20+
[[location(0)]] blend_color: vec4<f32>;
21+
};
22+
23+
[[stage(vertex)]]
24+
fn vertex(vertex: Vertex) -> VertexOutput {
25+
let world_position = mesh.model * vec4<f32>(vertex.position, 1.0);
26+
27+
var out: VertexOutput;
28+
out.clip_position = view.view_proj * world_position;
29+
out.blend_color = vertex.blend_color;
30+
return out;
31+
}
32+
33+
struct FragmentInput {
34+
[[location(0)]] blend_color: vec4<f32>;
35+
};
36+
37+
[[stage(fragment)]]
38+
fn fragment(input: FragmentInput) -> [[location(0)]] vec4<f32> {
39+
return material.color * input.blend_color;
40+
}

crates/bevy_gltf/src/loader.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,40 +125,40 @@ async fn load_gltf<'a, 'b>(
125125
.read_positions()
126126
.map(|v| VertexAttributeValues::Float32x3(v.collect()))
127127
{
128-
mesh.set_attribute(Mesh::ATTRIBUTE_POSITION, vertex_attribute);
128+
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, vertex_attribute);
129129
}
130130

131131
if let Some(vertex_attribute) = reader
132132
.read_normals()
133133
.map(|v| VertexAttributeValues::Float32x3(v.collect()))
134134
{
135-
mesh.set_attribute(Mesh::ATTRIBUTE_NORMAL, vertex_attribute);
135+
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, vertex_attribute);
136136
}
137137

138138
if let Some(vertex_attribute) = reader
139139
.read_tangents()
140140
.map(|v| VertexAttributeValues::Float32x4(v.collect()))
141141
{
142-
mesh.set_attribute(Mesh::ATTRIBUTE_TANGENT, vertex_attribute);
142+
mesh.insert_attribute(Mesh::ATTRIBUTE_TANGENT, vertex_attribute);
143143
}
144144

145145
if let Some(vertex_attribute) = reader
146146
.read_tex_coords(0)
147147
.map(|v| VertexAttributeValues::Float32x2(v.into_f32().collect()))
148148
{
149-
mesh.set_attribute(Mesh::ATTRIBUTE_UV_0, vertex_attribute);
149+
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, vertex_attribute);
150150
} else {
151151
let len = mesh.count_vertices();
152152
let uvs = vec![[0.0, 0.0]; len];
153153
bevy_log::debug!("missing `TEXCOORD_0` vertex attribute, loading zeroed out UVs");
154-
mesh.set_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
154+
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
155155
}
156156

157157
// if let Some(vertex_attribute) = reader
158158
// .read_colors(0)
159159
// .map(|v| VertexAttributeValues::Float32x4(v.into_rgba_f32().collect()))
160160
// {
161-
// mesh.set_attribute(Mesh::ATTRIBUTE_COLOR, vertex_attribute);
161+
// mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, vertex_attribute);
162162
// }
163163

164164
if let Some(indices) = reader.read_indices() {

crates/bevy_pbr/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ use bevy_render::{
4040
prelude::Color,
4141
render_graph::RenderGraph,
4242
render_phase::{sort_phase_system, AddRenderCommand, DrawFunctions},
43-
render_resource::{Shader, SpecializedPipelines},
43+
render_resource::{Shader, SpecializedMeshPipelines},
4444
view::VisibilitySystems,
4545
RenderApp, RenderStage,
4646
};
@@ -176,7 +176,7 @@ impl Plugin for PbrPlugin {
176176
.init_resource::<DrawFunctions<Shadow>>()
177177
.init_resource::<LightMeta>()
178178
.init_resource::<GlobalLightMeta>()
179-
.init_resource::<SpecializedPipelines<ShadowPipeline>>();
179+
.init_resource::<SpecializedMeshPipelines<ShadowPipeline>>();
180180

181181
let shadow_pass_node = ShadowPassNode::new(&mut render_app.world);
182182
render_app.add_render_command::<Shadow, DrawShadowMesh>();

crates/bevy_pbr/src/material.rs

Lines changed: 109 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use bevy_ecs::{
1515
world::FromWorld,
1616
};
1717
use bevy_render::{
18-
mesh::Mesh,
18+
mesh::{Mesh, MeshVertexBufferLayout},
1919
render_asset::{RenderAsset, RenderAssetPlugin, RenderAssets},
2020
render_component::ExtractComponentPlugin,
2121
render_phase::{
@@ -24,12 +24,13 @@ use bevy_render::{
2424
},
2525
render_resource::{
2626
BindGroup, BindGroupLayout, RenderPipelineCache, RenderPipelineDescriptor, Shader,
27-
SpecializedPipeline, SpecializedPipelines,
27+
SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines,
2828
},
2929
renderer::RenderDevice,
3030
view::{ExtractedView, Msaa, VisibleEntities},
3131
RenderApp, RenderStage,
3232
};
33+
use bevy_utils::tracing::error;
3334
use std::hash::Hash;
3435
use std::marker::PhantomData;
3536

@@ -72,6 +73,16 @@ pub trait Material: Asset + RenderAsset {
7273
fn dynamic_uniform_indices(material: &<Self as RenderAsset>::PreparedAsset) -> &[u32] {
7374
&[]
7475
}
76+
77+
/// Customizes the default [`RenderPipelineDescriptor`].
78+
#[allow(unused_variables)]
79+
#[inline]
80+
fn specialize(
81+
descriptor: &mut RenderPipelineDescriptor,
82+
layout: &MeshVertexBufferLayout,
83+
) -> Result<(), SpecializedMeshPipelineError> {
84+
Ok(())
85+
}
7586
}
7687

7788
impl<M: Material> SpecializedMaterial for M {
@@ -81,7 +92,13 @@ impl<M: Material> SpecializedMaterial for M {
8192
fn key(_material: &<Self as RenderAsset>::PreparedAsset) -> Self::Key {}
8293

8394
#[inline]
84-
fn specialize(_key: Self::Key, _descriptor: &mut RenderPipelineDescriptor) {}
95+
fn specialize(
96+
descriptor: &mut RenderPipelineDescriptor,
97+
_key: Self::Key,
98+
layout: &MeshVertexBufferLayout,
99+
) -> Result<(), SpecializedMeshPipelineError> {
100+
<M as Material>::specialize(descriptor, layout)
101+
}
85102

86103
#[inline]
87104
fn bind_group(material: &<Self as RenderAsset>::PreparedAsset) -> &BindGroup {
@@ -130,7 +147,11 @@ pub trait SpecializedMaterial: Asset + RenderAsset {
130147
fn key(material: &<Self as RenderAsset>::PreparedAsset) -> Self::Key;
131148

132149
/// Specializes the given `descriptor` according to the given `key`.
133-
fn specialize(key: Self::Key, descriptor: &mut RenderPipelineDescriptor);
150+
fn specialize(
151+
descriptor: &mut RenderPipelineDescriptor,
152+
key: Self::Key,
153+
layout: &MeshVertexBufferLayout,
154+
) -> Result<(), SpecializedMeshPipelineError>;
134155

135156
/// Returns this material's [`BindGroup`]. This should match the layout returned by [`SpecializedMaterial::bind_group_layout`].
136157
fn bind_group(material: &<Self as RenderAsset>::PreparedAsset) -> &BindGroup;
@@ -188,12 +209,18 @@ impl<M: SpecializedMaterial> Plugin for MaterialPlugin<M> {
188209
.add_render_command::<Opaque3d, DrawMaterial<M>>()
189210
.add_render_command::<AlphaMask3d, DrawMaterial<M>>()
190211
.init_resource::<MaterialPipeline<M>>()
191-
.init_resource::<SpecializedPipelines<MaterialPipeline<M>>>()
212+
.init_resource::<SpecializedMeshPipelines<MaterialPipeline<M>>>()
192213
.add_system_to_stage(RenderStage::Queue, queue_material_meshes::<M>);
193214
}
194215
}
195216
}
196217

218+
#[derive(Eq, PartialEq, Clone, Hash)]
219+
pub struct MaterialPipelineKey<T> {
220+
mesh_key: MeshPipelineKey,
221+
material_key: T,
222+
}
223+
197224
pub struct MaterialPipeline<M: SpecializedMaterial> {
198225
pub mesh_pipeline: MeshPipeline,
199226
pub material_layout: BindGroupLayout,
@@ -202,11 +229,15 @@ pub struct MaterialPipeline<M: SpecializedMaterial> {
202229
marker: PhantomData<M>,
203230
}
204231

205-
impl<M: SpecializedMaterial> SpecializedPipeline for MaterialPipeline<M> {
206-
type Key = (MeshPipelineKey, M::Key);
232+
impl<M: SpecializedMaterial> SpecializedMeshPipeline for MaterialPipeline<M> {
233+
type Key = MaterialPipelineKey<M::Key>;
207234

208-
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
209-
let mut descriptor = self.mesh_pipeline.specialize(key.0);
235+
fn specialize(
236+
&self,
237+
key: Self::Key,
238+
layout: &MeshVertexBufferLayout,
239+
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
240+
let mut descriptor = self.mesh_pipeline.specialize(key.mesh_key, layout)?;
210241
if let Some(vertex_shader) = &self.vertex_shader {
211242
descriptor.vertex.shader = vertex_shader.clone();
212243
}
@@ -220,8 +251,8 @@ impl<M: SpecializedMaterial> SpecializedPipeline for MaterialPipeline<M> {
220251
self.mesh_pipeline.mesh_layout.clone(),
221252
]);
222253

223-
M::specialize(key.1, &mut descriptor);
224-
descriptor
254+
M::specialize(&mut descriptor, key.material_key, layout)?;
255+
Ok(descriptor)
225256
}
226257
}
227258

@@ -275,7 +306,7 @@ pub fn queue_material_meshes<M: SpecializedMaterial>(
275306
alpha_mask_draw_functions: Res<DrawFunctions<AlphaMask3d>>,
276307
transparent_draw_functions: Res<DrawFunctions<Transparent3d>>,
277308
material_pipeline: Res<MaterialPipeline<M>>,
278-
mut pipelines: ResMut<SpecializedPipelines<MaterialPipeline<M>>>,
309+
mut pipelines: ResMut<SpecializedMeshPipelines<MaterialPipeline<M>>>,
279310
mut pipeline_cache: ResMut<RenderPipelineCache>,
280311
msaa: Res<Msaa>,
281312
render_meshes: Res<RenderAssets<Mesh>>,
@@ -307,72 +338,81 @@ pub fn queue_material_meshes<M: SpecializedMaterial>(
307338

308339
let inverse_view_matrix = view.transform.compute_matrix().inverse();
309340
let inverse_view_row_2 = inverse_view_matrix.row(2);
310-
let mesh_key = MeshPipelineKey::from_msaa_samples(msaa.samples);
341+
let msaa_key = MeshPipelineKey::from_msaa_samples(msaa.samples);
311342

312343
for visible_entity in &visible_entities.entities {
313344
if let Ok((material_handle, mesh_handle, mesh_uniform)) =
314345
material_meshes.get(*visible_entity)
315346
{
316347
if let Some(material) = render_materials.get(material_handle) {
317-
let mut mesh_key = mesh_key;
318348
if let Some(mesh) = render_meshes.get(mesh_handle) {
319-
if mesh.has_tangents {
320-
mesh_key |= MeshPipelineKey::VERTEX_TANGENTS;
349+
let mut mesh_key =
350+
MeshPipelineKey::from_primitive_topology(mesh.primitive_topology)
351+
| msaa_key;
352+
let alpha_mode = M::alpha_mode(material);
353+
if let AlphaMode::Blend = alpha_mode {
354+
mesh_key |= MeshPipelineKey::TRANSPARENT_MAIN_PASS;
321355
}
322-
mesh_key |=
323-
MeshPipelineKey::from_primitive_topology(mesh.primitive_topology);
324-
}
325-
let alpha_mode = M::alpha_mode(material);
326-
if let AlphaMode::Blend = alpha_mode {
327-
mesh_key |= MeshPipelineKey::TRANSPARENT_MAIN_PASS;
328-
}
329356

330-
let specialized_key = M::key(material);
331-
let pipeline_id = pipelines.specialize(
332-
&mut pipeline_cache,
333-
&material_pipeline,
334-
(mesh_key, specialized_key),
335-
);
336-
337-
// NOTE: row 2 of the inverse view matrix dotted with column 3 of the model matrix
338-
// gives the z component of translation of the mesh in view space
339-
let mesh_z = inverse_view_row_2.dot(mesh_uniform.transform.col(3));
340-
match alpha_mode {
341-
AlphaMode::Opaque => {
342-
opaque_phase.add(Opaque3d {
343-
entity: *visible_entity,
344-
draw_function: draw_opaque_pbr,
345-
pipeline: pipeline_id,
346-
// NOTE: Front-to-back ordering for opaque with ascending sort means near should have the
347-
// lowest sort key and getting further away should increase. As we have
348-
// -z in front of the camera, values in view space decrease away from the
349-
// camera. Flipping the sign of mesh_z results in the correct front-to-back ordering
350-
distance: -mesh_z,
351-
});
352-
}
353-
AlphaMode::Mask(_) => {
354-
alpha_mask_phase.add(AlphaMask3d {
355-
entity: *visible_entity,
356-
draw_function: draw_alpha_mask_pbr,
357-
pipeline: pipeline_id,
358-
// NOTE: Front-to-back ordering for alpha mask with ascending sort means near should have the
359-
// lowest sort key and getting further away should increase. As we have
360-
// -z in front of the camera, values in view space decrease away from the
361-
// camera. Flipping the sign of mesh_z results in the correct front-to-back ordering
362-
distance: -mesh_z,
363-
});
364-
}
365-
AlphaMode::Blend => {
366-
transparent_phase.add(Transparent3d {
367-
entity: *visible_entity,
368-
draw_function: draw_transparent_pbr,
369-
pipeline: pipeline_id,
370-
// NOTE: Back-to-front ordering for transparent with ascending sort means far should have the
371-
// lowest sort key and getting closer should increase. As we have
372-
// -z in front of the camera, the largest distance is -far with values increasing toward the
373-
// camera. As such we can just use mesh_z as the distance
374-
distance: mesh_z,
375-
});
357+
let material_key = M::key(material);
358+
359+
let pipeline_id = pipelines.specialize(
360+
&mut pipeline_cache,
361+
&material_pipeline,
362+
MaterialPipelineKey {
363+
mesh_key,
364+
material_key,
365+
},
366+
&mesh.layout,
367+
);
368+
let pipeline_id = match pipeline_id {
369+
Ok(id) => id,
370+
Err(err) => {
371+
error!("{}", err);
372+
continue;
373+
}
374+
};
375+
376+
// NOTE: row 2 of the inverse view matrix dotted with column 3 of the model matrix
377+
// gives the z component of translation of the mesh in view space
378+
let mesh_z = inverse_view_row_2.dot(mesh_uniform.transform.col(3));
379+
match alpha_mode {
380+
AlphaMode::Opaque => {
381+
opaque_phase.add(Opaque3d {
382+
entity: *visible_entity,
383+
draw_function: draw_opaque_pbr,
384+
pipeline: pipeline_id,
385+
// NOTE: Front-to-back ordering for opaque with ascending sort means near should have the
386+
// lowest sort key and getting further away should increase. As we have
387+
// -z in front of the camera, values in view space decrease away from the
388+
// camera. Flipping the sign of mesh_z results in the correct front-to-back ordering
389+
distance: -mesh_z,
390+
});
391+
}
392+
AlphaMode::Mask(_) => {
393+
alpha_mask_phase.add(AlphaMask3d {
394+
entity: *visible_entity,
395+
draw_function: draw_alpha_mask_pbr,
396+
pipeline: pipeline_id,
397+
// NOTE: Front-to-back ordering for alpha mask with ascending sort means near should have the
398+
// lowest sort key and getting further away should increase. As we have
399+
// -z in front of the camera, values in view space decrease away from the
400+
// camera. Flipping the sign of mesh_z results in the correct front-to-back ordering
401+
distance: -mesh_z,
402+
});
403+
}
404+
AlphaMode::Blend => {
405+
transparent_phase.add(Transparent3d {
406+
entity: *visible_entity,
407+
draw_function: draw_transparent_pbr,
408+
pipeline: pipeline_id,
409+
// NOTE: Back-to-front ordering for transparent with ascending sort means far should have the
410+
// lowest sort key and getting closer should increase. As we have
411+
// -z in front of the camera, the largest distance is -far with values increasing toward the
412+
// camera. As such we can just use mesh_z as the distance
413+
distance: mesh_z,
414+
});
415+
}
376416
}
377417
}
378418
}

0 commit comments

Comments
 (0)