Skip to content

Commit a0faf9c

Browse files
authored
More triangles/vertices per meshlet (#15023)
### Builder changes - Increased meshlet max vertices/triangles from 64v/64t to 255v/128t (meshoptimizer won't allow 256v sadly). This gives us a much greater percentage of meshlets with max triangle count (128). Still not perfect, we still end up with some tiny <=10 triangle meshlets that never really get simplified, but it's progress. - Removed the error target limit. Now we allow meshoptimizer to simplify as much as possible. No reason to cap this out, as the cluster culling code will choose a good LOD level anyways. Again leads to higher quality LOD trees. - After some discussion and consulting the Nanite slides again, changed meshlet group error from _adding_ the max child's error to the group error, to doing `group_error = max(group_error, max_child_error)`. Error is already cumulative between LODs as the edges we're collapsing during simplification get longer each time. - Bumped the 65% simplification threshold to allow up to 95% of the original geometry (e.g. accept simplification as valid even if we only simplified 5% of the triangles). This gives us closer to log2(initial_meshlet_count) LOD levels, and fewer meshlet roots in the DAG. Still more work to be done in the future here. Maybe trying METIS for meshlet building instead of meshoptimizer. Using ~8 clusters per group instead of ~4 might also make a big difference. The Nanite slides say that they have 8-32 meshlets per group, suggesting some kind of heuristic. Unfortunately meshopt's compute_cluster_bounds won't work with large groups atm (zeux/meshoptimizer#750 (comment)) so hard to test. Based on discussion from #14998, zeux/meshoptimizer#750, and discord. ### Runtime changes - cluster:triangle packed IDs are now stored 25:7 instead of 26:6 bits, as max triangles per cluster are now 128 instead of 64 - Hardware raster now spawns 128 * 3 vertices instead of 64 * 3 vertices to account for the new max triangles limit - Hardware raster now outputs NaN triangles (0 / 0) instead of zero-positioned triangles for extra vertex invocations over the cluster triangle count. Shouldn't really be a difference idt, but I did it anyways. - Software raster now does 128 threads per workgroup instead of 64 threads. Each thread now loads, projects, and caches a vertex (vertices 0-127), and then if needed does so again (vertices 128-254). Each thread then rasterizes one of 128 triangles. - Fixed a bug with `needs_dispatch_remap`. I had the condition backwards in my last PR, I probably committed it by accident after testing the non-default code path on my GPU.
1 parent 8fb69dc commit a0faf9c

File tree

8 files changed

+53
-61
lines changed

8 files changed

+53
-61
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1075,7 +1075,7 @@ setup = [
10751075
"curl",
10761076
"-o",
10771077
"assets/models/bunny.meshlet_mesh",
1078-
"https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/b6c712cfc87c65de419f856845401aba336a7bcd/bunny.meshlet_mesh",
1078+
"https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/e3da1533b4c69fb967f233c817e9b0921134d317/bunny.meshlet_mesh",
10791079
],
10801080
]
10811081

crates/bevy_pbr/src/meshlet/from_mesh.rs

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use bevy_utils::HashMap;
77
use itertools::Itertools;
88
use meshopt::{
99
build_meshlets, compute_cluster_bounds, compute_meshlet_bounds, ffi::meshopt_Bounds, simplify,
10-
simplify_scale, Meshlets, SimplifyOptions, VertexDataAdapter,
10+
Meshlets, SimplifyOptions, VertexDataAdapter,
1111
};
1212
use metis::Graph;
1313
use smallvec::SmallVec;
@@ -49,11 +49,9 @@ impl MeshletMesh {
4949
},
5050
})
5151
.collect::<Vec<_>>();
52-
let mesh_scale = simplify_scale(&vertices);
5352

5453
// Build further LODs
5554
let mut simplification_queue = 0..meshlets.len();
56-
let mut lod_level = 1;
5755
while simplification_queue.len() > 1 {
5856
// For each meshlet build a list of connected meshlets (meshlets that share a triangle edge)
5957
let connected_meshlets_per_meshlet =
@@ -70,19 +68,14 @@ impl MeshletMesh {
7068

7169
for group_meshlets in groups.into_iter().filter(|group| group.len() > 1) {
7270
// Simplify the group to ~50% triangle count
73-
let Some((simplified_group_indices, mut group_error)) = simplify_meshlet_groups(
74-
&group_meshlets,
75-
&meshlets,
76-
&vertices,
77-
lod_level,
78-
mesh_scale,
79-
) else {
71+
let Some((simplified_group_indices, mut group_error)) =
72+
simplify_meshlet_group(&group_meshlets, &meshlets, &vertices)
73+
else {
8074
continue;
8175
};
8276

83-
// Add the maximum child error to the parent error to make parent error cumulative from LOD 0
84-
// (we're currently building the parent from its children)
85-
group_error += group_meshlets.iter().fold(0.0f32, |acc, meshlet_id| {
77+
// Force parent error to be >= child error (we're currently building the parent from its children)
78+
group_error = group_meshlets.iter().fold(group_error, |acc, meshlet_id| {
8679
acc.max(bounding_spheres[*meshlet_id].self_lod.radius)
8780
});
8881

@@ -99,7 +92,7 @@ impl MeshletMesh {
9992
}
10093

10194
// Build new meshlets using the simplified group
102-
let new_meshlets_count = split_simplified_groups_into_new_meshlets(
95+
let new_meshlets_count = split_simplified_group_into_new_meshlets(
10396
&simplified_group_indices,
10497
&vertices,
10598
&mut meshlets,
@@ -125,7 +118,6 @@ impl MeshletMesh {
125118
}
126119

127120
simplification_queue = next_lod_start..meshlets.len();
128-
lod_level += 1;
129121
}
130122

131123
// Convert meshopt_Meshlet data to a custom format
@@ -172,7 +164,7 @@ fn validate_input_mesh(mesh: &Mesh) -> Result<Cow<'_, [u32]>, MeshToMeshletMeshC
172164
}
173165

174166
fn compute_meshlets(indices: &[u32], vertices: &VertexDataAdapter) -> Meshlets {
175-
build_meshlets(indices, vertices, 64, 64, 0.0)
167+
build_meshlets(indices, vertices, 255, 128, 0.0) // Meshoptimizer won't currently let us do 256 vertices
176168
}
177169

178170
fn find_connected_meshlets(
@@ -252,7 +244,7 @@ fn group_meshlets(
252244
xadj.push(adjncy.len() as i32);
253245

254246
let mut group_per_meshlet = vec![0; simplification_queue.len()];
255-
let partition_count = simplification_queue.len().div_ceil(4);
247+
let partition_count = simplification_queue.len().div_ceil(4); // TODO: Nanite uses groups of 8-32, probably based on some kind of heuristic
256248
Graph::new(1, partition_count as i32, &xadj, &adjncy)
257249
.unwrap()
258250
.set_adjwgt(&adjwgt)
@@ -267,12 +259,10 @@ fn group_meshlets(
267259
groups
268260
}
269261

270-
fn simplify_meshlet_groups(
262+
fn simplify_meshlet_group(
271263
group_meshlets: &[usize],
272264
meshlets: &Meshlets,
273265
vertices: &VertexDataAdapter<'_>,
274-
lod_level: u32,
275-
mesh_scale: f32,
276266
) -> Option<(Vec<u32>, f32)> {
277267
// Build a new index buffer into the mesh vertex data by combining all meshlet data in the group
278268
let mut group_indices = Vec::new();
@@ -283,25 +273,20 @@ fn simplify_meshlet_groups(
283273
}
284274
}
285275

286-
// Allow more deformation for high LOD levels (1% at LOD 1, 10% at LOD 20+)
287-
let t = (lod_level - 1) as f32 / 19.0;
288-
let target_error_relative = 0.1 * t + 0.01 * (1.0 - t);
289-
let target_error = target_error_relative * mesh_scale;
290-
291276
// Simplify the group to ~50% triangle count
292277
// TODO: Simplify using vertex attributes
293278
let mut error = 0.0;
294279
let simplified_group_indices = simplify(
295280
&group_indices,
296281
vertices,
297282
group_indices.len() / 2,
298-
target_error,
299-
SimplifyOptions::LockBorder | SimplifyOptions::Sparse | SimplifyOptions::ErrorAbsolute,
283+
f32::MAX,
284+
SimplifyOptions::LockBorder | SimplifyOptions::Sparse | SimplifyOptions::ErrorAbsolute, // TODO: Specify manual vertex locks instead of meshopt's overly-strict locks
300285
Some(&mut error),
301286
);
302287

303-
// Check if we were able to simplify to at least 65% triangle count
304-
if simplified_group_indices.len() as f32 / group_indices.len() as f32 > 0.65 {
288+
// Check if we were able to simplify at least a little (95% of the original triangle count)
289+
if simplified_group_indices.len() as f32 / group_indices.len() as f32 > 0.95 {
305290
return None;
306291
}
307292

@@ -311,7 +296,7 @@ fn simplify_meshlet_groups(
311296
Some((simplified_group_indices, error))
312297
}
313298

314-
fn split_simplified_groups_into_new_meshlets(
299+
fn split_simplified_group_into_new_meshlets(
315300
simplified_group_indices: &[u32],
316301
vertices: &VertexDataAdapter<'_>,
317302
meshlets: &mut Meshlets,

crates/bevy_pbr/src/meshlet/resolve_render_targets.wgsl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ fn resolve_material_depth(in: FullscreenVertexOutput) -> @builtin(frag_depth) f3
3131
let depth = visibility >> 32u;
3232
if depth == 0lu { return 0.0; }
3333

34-
let cluster_id = u32(visibility) >> 6u;
34+
let cluster_id = u32(visibility) >> 7u;
3535
let instance_id = meshlet_cluster_instance_ids[cluster_id];
3636
let material_id = meshlet_instance_material_ids[instance_id];
3737
return f32(material_id) / 65535.0;

crates/bevy_pbr/src/meshlet/resource_manager.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ pub struct ResourceManager {
6363
impl ResourceManager {
6464
pub fn new(cluster_buffer_slots: u32, render_device: &RenderDevice) -> Self {
6565
let needs_dispatch_remap =
66-
cluster_buffer_slots < render_device.limits().max_compute_workgroups_per_dimension;
66+
cluster_buffer_slots > render_device.limits().max_compute_workgroups_per_dimension;
6767

6868
Self {
6969
visibility_buffer_raster_clusters: render_device.create_buffer(&BufferDescriptor {
@@ -472,7 +472,7 @@ pub fn prepare_meshlet_per_frame_resources(
472472
.create_buffer_with_data(&BufferInitDescriptor {
473473
label: Some("meshlet_visibility_buffer_hardware_raster_indirect_args_first"),
474474
contents: DrawIndirectArgs {
475-
vertex_count: 64 * 3,
475+
vertex_count: 128 * 3,
476476
instance_count: 0,
477477
first_vertex: 0,
478478
first_instance: 0,
@@ -484,7 +484,7 @@ pub fn prepare_meshlet_per_frame_resources(
484484
.create_buffer_with_data(&BufferInitDescriptor {
485485
label: Some("visibility_buffer_hardware_raster_indirect_args_second"),
486486
contents: DrawIndirectArgs {
487-
vertex_count: 64 * 3,
487+
vertex_count: 128 * 3,
488488
instance_count: 0,
489489
first_vertex: 0,
490490
first_instance: 0,

crates/bevy_pbr/src/meshlet/visibility_buffer_hardware_raster.wgsl

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ fn vertex(@builtin(instance_index) instance_index: u32, @builtin(vertex_index) v
5656
return VertexOutput(
5757
clip_position,
5858
#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
59-
(cluster_id << 6u) | triangle_id,
59+
(cluster_id << 7u) | triangle_id,
6060
#endif
6161
#ifdef DEPTH_CLAMP_ORTHO
6262
unclamped_clip_depth,
@@ -83,7 +83,7 @@ fn fragment(vertex_output: VertexOutput) {
8383

8484
fn dummy_vertex() -> VertexOutput {
8585
return VertexOutput(
86-
vec4(0.0),
86+
vec4(divide(0.0, 0.0)), // NaN vertex position
8787
#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
8888
0u,
8989
#endif
@@ -92,3 +92,8 @@ fn dummy_vertex() -> VertexOutput {
9292
#endif
9393
);
9494
}
95+
96+
// Naga doesn't allow divide by zero literals, but this lets us work around it
97+
fn divide(a: f32, b: f32) -> f32 {
98+
return a / b;
99+
}

crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,11 @@ struct VertexOutput {
9797
fn resolve_vertex_output(frag_coord: vec4<f32>) -> VertexOutput {
9898
let frag_coord_1d = u32(frag_coord.y) * u32(view.viewport.z) + u32(frag_coord.x);
9999
let packed_ids = u32(meshlet_visibility_buffer[frag_coord_1d]); // TODO: Might be faster to load the correct u32 directly
100-
let cluster_id = packed_ids >> 6u;
100+
let cluster_id = packed_ids >> 7u;
101101
let meshlet_id = meshlet_cluster_meshlet_ids[cluster_id];
102102
let meshlet = meshlets[meshlet_id];
103103

104-
let triangle_id = extractBits(packed_ids, 0u, 6u);
104+
let triangle_id = extractBits(packed_ids, 0u, 7u);
105105
let index_ids = meshlet.start_index_id + (triangle_id * 3u) + vec3(0u, 1u, 2u);
106106
let indices = meshlet.start_vertex_id + vec3(get_meshlet_index(index_ids.x), get_meshlet_index(index_ids.y), get_meshlet_index(index_ids.z));
107107
let vertex_ids = vec3(meshlet_vertex_ids[indices.x], meshlet_vertex_ids[indices.y], meshlet_vertex_ids[indices.z]);

crates/bevy_pbr/src/meshlet/visibility_buffer_software_raster.wgsl

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@
2222

2323
// TODO: Subpixel precision and top-left rule
2424

25-
var<workgroup> viewport_vertices: array<vec3f, 64>;
25+
var<workgroup> viewport_vertices: array<vec3f, 255>;
2626

2727
@compute
28-
@workgroup_size(64, 1, 1) // 64 threads per workgroup, 1 vertex/triangle per thread, 1 cluster per workgroup
28+
@workgroup_size(128, 1, 1) // 128 threads per workgroup, 1-2 vertices per thread, 1 triangle per thread, 1 cluster per workgroup
2929
fn rasterize_cluster(
3030
@builtin(workgroup_id) workgroup_id: vec3<u32>,
3131
@builtin(local_invocation_index) local_invocation_index: u32,
@@ -44,28 +44,30 @@ fn rasterize_cluster(
4444
let meshlet_id = meshlet_cluster_meshlet_ids[cluster_id];
4545
let meshlet = meshlets[meshlet_id];
4646

47-
// Load and project 1 vertex per thread
48-
let vertex_id = local_invocation_index;
49-
if vertex_id < meshlet.vertex_count {
50-
let meshlet_vertex_id = meshlet_vertex_ids[meshlet.start_vertex_id + vertex_id];
51-
let vertex = unpack_meshlet_vertex(meshlet_vertex_data[meshlet_vertex_id]);
52-
53-
// Project vertex to viewport space
54-
let instance_id = meshlet_cluster_instance_ids[cluster_id];
55-
let instance_uniform = meshlet_instance_uniforms[instance_id];
56-
let world_from_local = affine3_to_square(instance_uniform.world_from_local);
57-
let world_position = mesh_position_local_to_world(world_from_local, vec4(vertex.position, 1.0));
58-
var clip_position = view.clip_from_world * vec4(world_position.xyz, 1.0);
59-
var ndc_position = clip_position.xyz / clip_position.w;
47+
let instance_id = meshlet_cluster_instance_ids[cluster_id];
48+
let instance_uniform = meshlet_instance_uniforms[instance_id];
49+
let world_from_local = affine3_to_square(instance_uniform.world_from_local);
50+
51+
// Load and project 1 vertex per thread, and then again if there are more than 128 vertices in the meshlet
52+
for (var i = 0u; i <= 128u; i += 128u) {
53+
let vertex_id = local_invocation_index + i;
54+
if vertex_id < meshlet.vertex_count {
55+
let meshlet_vertex_id = meshlet_vertex_ids[meshlet.start_vertex_id + vertex_id];
56+
let vertex = unpack_meshlet_vertex(meshlet_vertex_data[meshlet_vertex_id]);
57+
58+
// Project vertex to viewport space
59+
let world_position = mesh_position_local_to_world(world_from_local, vec4(vertex.position, 1.0));
60+
let clip_position = view.clip_from_world * vec4(world_position.xyz, 1.0);
61+
var ndc_position = clip_position.xyz / clip_position.w;
6062
#ifdef DEPTH_CLAMP_ORTHO
61-
ndc_position.z = 1.0 / clip_position.z;
63+
ndc_position.z = 1.0 / clip_position.z;
6264
#endif
63-
let viewport_position_xy = ndc_to_uv(ndc_position.xy) * view.viewport.zw;
65+
let viewport_position_xy = ndc_to_uv(ndc_position.xy) * view.viewport.zw;
6466

65-
// Write vertex to workgroup shared memory
66-
viewport_vertices[vertex_id] = vec3(viewport_position_xy, ndc_position.z);
67+
// Write vertex to workgroup shared memory
68+
viewport_vertices[vertex_id] = vec3(viewport_position_xy, ndc_position.z);
69+
}
6770
}
68-
6971
workgroupBarrier();
7072

7173
// Load 1 triangle's worth of vertex data per thread
@@ -76,7 +78,7 @@ fn rasterize_cluster(
7678
let vertex_0 = viewport_vertices[vertex_ids[2]];
7779
let vertex_1 = viewport_vertices[vertex_ids[1]];
7880
let vertex_2 = viewport_vertices[vertex_ids[0]];
79-
let packed_ids = (cluster_id << 6u) | triangle_id;
81+
let packed_ids = (cluster_id << 7u) | triangle_id;
8082

8183
// Compute triangle bounding box
8284
let min_x = u32(min3(vertex_0.x, vertex_1.x, vertex_2.x));

examples/3d/meshlet.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use camera_controller::{CameraController, CameraControllerPlugin};
1717
use std::{f32::consts::PI, path::Path, process::ExitCode};
1818

1919
const ASSET_URL: &str =
20-
"https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/10bb5471c7beedfe63ad1cf269599c92b0f10aa2/bunny.meshlet_mesh";
20+
"https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/e3da1533b4c69fb967f233c817e9b0921134d317/bunny.meshlet_mesh";
2121

2222
fn main() -> ExitCode {
2323
if !Path::new("./assets/models/bunny.meshlet_mesh").exists() {

0 commit comments

Comments
 (0)