Skip to content

Commit cc17647

Browse files
zeuxmockersf
authored andcommitted
Improve MeshletMesh::from_mesh performance (#13904)
This change reworks `find_connected_meshlets` to scale more linearly with the mesh size, which significantly reduces the cost of building meshlet representations. As a small extra complexity reduction, it moves `simplify_scale` call out of the loop so that it's called once (it only depends on the vertex data => is safe to cache). The new implementation of connectivity analysis builds edge=>meshlet list data structure, which allows us to only iterate through `tuple_combinations` of a (usually) small list. There is still some redundancy as if two meshlets share two edges, they will be represented in the meshlet lists twice, but it's overall much faster. Since the hash traversal is non-deterministic, to keep this part of the algorithm deterministic for reproducible results we sort the output adjacency lists. Overall this reduces the time to process bunny mesh from ~4.2s to ~1.7s when using release; in unoptimized builds the delta is even more significant. This was tested by using #13431 and: a) comparing the result of `find_connected_meshlets` using old and new code; they are equal in all steps of the clustering process b) comparing the rendered result of the old code vs new code *after* making the rest of the algorithm deterministic: right now the loop that iterates through the result of `group_meshlets()` call executes in different order between program runs. This is orthogonal to this change and can be fixed separately. Note: a future change can shrink the processing time further from ~1.7s to ~0.4s with a small diff but that requires an update to meshopt crate which is pending in gwihlidal/meshopt-rs#42. This change is independent.
1 parent 99db59c commit cc17647

File tree

1 file changed

+66
-46
lines changed

1 file changed

+66
-46
lines changed

crates/bevy_pbr/src/meshlet/from_mesh.rs

Lines changed: 66 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ use bevy_render::{
33
mesh::{Indices, Mesh},
44
render_resource::PrimitiveTopology,
55
};
6-
use bevy_utils::{HashMap, HashSet};
6+
use bevy_utils::HashMap;
77
use itertools::Itertools;
88
use meshopt::{
99
build_meshlets, compute_cluster_bounds, compute_meshlet_bounds,
1010
ffi::{meshopt_Bounds, meshopt_optimizeMeshlet},
1111
simplify, simplify_scale, Meshlets, SimplifyOptions, VertexDataAdapter,
1212
};
1313
use metis::Graph;
14+
use smallvec::SmallVec;
1415
use std::{borrow::Cow, ops::Range};
1516

1617
impl MeshletMesh {
@@ -54,18 +55,15 @@ impl MeshletMesh {
5455
.iter()
5556
.map(|m| m.triangle_count as u64)
5657
.sum();
58+
let mesh_scale = simplify_scale(&vertices);
5759

5860
// Build further LODs
5961
let mut simplification_queue = 0..meshlets.len();
6062
let mut lod_level = 1;
6163
while simplification_queue.len() > 1 {
62-
// For each meshlet build a set of triangle edges
63-
let triangle_edges_per_meshlet =
64-
collect_triangle_edges_per_meshlet(simplification_queue.clone(), &meshlets);
65-
6664
// For each meshlet build a list of connected meshlets (meshlets that share a triangle edge)
6765
let connected_meshlets_per_meshlet =
68-
find_connected_meshlets(simplification_queue.clone(), &triangle_edges_per_meshlet);
66+
find_connected_meshlets(simplification_queue.clone(), &meshlets);
6967

7068
// Group meshlets into roughly groups of 4, grouping meshlets with a high number of shared edges
7169
// http://glaros.dtc.umn.edu/gkhome/fetch/sw/metis/manual.pdf
@@ -78,9 +76,13 @@ impl MeshletMesh {
7876

7977
for group_meshlets in groups.values().filter(|group| group.len() > 1) {
8078
// Simplify the group to ~50% triangle count
81-
let Some((simplified_group_indices, mut group_error)) =
82-
simplify_meshlet_groups(group_meshlets, &meshlets, &vertices, lod_level)
83-
else {
79+
let Some((simplified_group_indices, mut group_error)) = simplify_meshlet_groups(
80+
group_meshlets,
81+
&meshlets,
82+
&vertices,
83+
lod_level,
84+
mesh_scale,
85+
) else {
8486
continue;
8587
};
8688

@@ -194,53 +196,70 @@ fn compute_meshlets(indices: &[u32], vertices: &VertexDataAdapter) -> Meshlets {
194196
meshlets
195197
}
196198

197-
fn collect_triangle_edges_per_meshlet(
199+
fn find_connected_meshlets(
198200
simplification_queue: Range<usize>,
199201
meshlets: &Meshlets,
200-
) -> HashMap<usize, HashSet<(u32, u32)>> {
201-
let mut triangle_edges_per_meshlet = HashMap::new();
202-
for meshlet_id in simplification_queue {
202+
) -> HashMap<usize, Vec<(usize, usize)>> {
203+
// For each edge, gather all meshlets that use it
204+
let mut edges_to_meshlets = HashMap::new();
205+
206+
for meshlet_id in simplification_queue.clone() {
203207
let meshlet = meshlets.get(meshlet_id);
204-
let meshlet_triangle_edges = triangle_edges_per_meshlet
205-
.entry(meshlet_id)
206-
.or_insert(HashSet::new());
207208
for i in meshlet.triangles.chunks(3) {
208-
let v0 = meshlet.vertices[i[0] as usize];
209-
let v1 = meshlet.vertices[i[1] as usize];
210-
let v2 = meshlet.vertices[i[2] as usize];
211-
meshlet_triangle_edges.insert((v0.min(v1), v0.max(v1)));
212-
meshlet_triangle_edges.insert((v0.min(v2), v0.max(v2)));
213-
meshlet_triangle_edges.insert((v1.min(v2), v1.max(v2)));
209+
for k in 0..3 {
210+
let v0 = meshlet.vertices[i[k] as usize];
211+
let v1 = meshlet.vertices[i[(k + 1) % 3] as usize];
212+
let edge = (v0.min(v1), v0.max(v1));
213+
214+
let vec = edges_to_meshlets
215+
.entry(edge)
216+
.or_insert_with(SmallVec::<[usize; 2]>::new);
217+
// Meshlets are added in order, so we can just check the last element to deduplicate,
218+
// in the case of two triangles sharing the same edge within a single meshlet
219+
if vec.last() != Some(&meshlet_id) {
220+
vec.push(meshlet_id);
221+
}
222+
}
214223
}
215224
}
216-
triangle_edges_per_meshlet
217-
}
218225

219-
fn find_connected_meshlets(
220-
simplification_queue: Range<usize>,
221-
triangle_edges_per_meshlet: &HashMap<usize, HashSet<(u32, u32)>>,
222-
) -> HashMap<usize, Vec<(usize, usize)>> {
223-
let mut connected_meshlets_per_meshlet = HashMap::new();
226+
// For each meshlet pair, count how many edges they share
227+
let mut shared_edge_count = HashMap::new();
228+
229+
for (_, meshlet_ids) in edges_to_meshlets {
230+
for (meshlet_id1, meshlet_id2) in meshlet_ids.into_iter().tuple_combinations() {
231+
let count = shared_edge_count
232+
.entry((meshlet_id1.min(meshlet_id2), meshlet_id1.max(meshlet_id2)))
233+
.or_insert(0);
234+
*count += 1;
235+
}
236+
}
237+
238+
// For each meshlet, gather all meshlets that share at least one edge along with shared edge count
239+
let mut connected_meshlets = HashMap::new();
240+
224241
for meshlet_id in simplification_queue.clone() {
225-
connected_meshlets_per_meshlet.insert(meshlet_id, Vec::new());
242+
connected_meshlets.insert(meshlet_id, Vec::new());
226243
}
227244

228-
for (meshlet_id1, meshlet_id2) in simplification_queue.tuple_combinations() {
229-
let shared_edge_count = triangle_edges_per_meshlet[&meshlet_id1]
230-
.intersection(&triangle_edges_per_meshlet[&meshlet_id2])
231-
.count();
232-
if shared_edge_count != 0 {
233-
connected_meshlets_per_meshlet
234-
.get_mut(&meshlet_id1)
235-
.unwrap()
236-
.push((meshlet_id2, shared_edge_count));
237-
connected_meshlets_per_meshlet
238-
.get_mut(&meshlet_id2)
239-
.unwrap()
240-
.push((meshlet_id1, shared_edge_count));
241-
}
245+
for ((meshlet_id1, meshlet_id2), shared_count) in shared_edge_count {
246+
// We record id1->id2 and id2->id1 as adjacency is symmetrical
247+
connected_meshlets
248+
.get_mut(&meshlet_id1)
249+
.unwrap()
250+
.push((meshlet_id2, shared_count));
251+
connected_meshlets
252+
.get_mut(&meshlet_id2)
253+
.unwrap()
254+
.push((meshlet_id1, shared_count));
242255
}
243-
connected_meshlets_per_meshlet
256+
257+
// The order of meshlets depends on hash traversal order; to produce deterministic results, sort them
258+
for (_, connected_meshlets) in connected_meshlets.iter_mut() {
259+
connected_meshlets.sort_unstable();
260+
}
261+
262+
connected_meshlets
244263
}
245264

246265
fn group_meshlets(
@@ -284,6 +303,7 @@ fn simplify_meshlet_groups(
284303
meshlets: &Meshlets,
285304
vertices: &VertexDataAdapter<'_>,
286305
lod_level: u32,
306+
mesh_scale: f32,
287307
) -> Option<(Vec<u32>, f32)> {
288308
// Build a new index buffer into the mesh vertex data by combining all meshlet data in the group
289309
let mut group_indices = Vec::new();
@@ -316,7 +336,7 @@ fn simplify_meshlet_groups(
316336
}
317337

318338
// Convert error to object-space and convert from diameter to radius
319-
error *= simplify_scale(vertices) * 0.5;
339+
error *= mesh_scale * 0.5;
320340

321341
Some((simplified_group_indices, error))
322342
}

0 commit comments

Comments
 (0)