From 0546e40172192f3ea9ffb83742d9bdf5a6fea3b8 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Mon, 17 Jun 2024 17:26:48 -0700 Subject: [PATCH 1/5] Improve MeshletMesh::from_mesh performance 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 smaller) list. There is still some redundancy as if two meshes are sharing 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. --- crates/bevy_pbr/src/meshlet/from_mesh.rs | 108 +++++++++++++---------- 1 file changed, 62 insertions(+), 46 deletions(-) diff --git a/crates/bevy_pbr/src/meshlet/from_mesh.rs b/crates/bevy_pbr/src/meshlet/from_mesh.rs index bae7feb1177d9..f45afe98658d9 100644 --- a/crates/bevy_pbr/src/meshlet/from_mesh.rs +++ b/crates/bevy_pbr/src/meshlet/from_mesh.rs @@ -3,7 +3,7 @@ use bevy_render::{ mesh::{Indices, Mesh}, render_resource::PrimitiveTopology, }; -use bevy_utils::{HashMap, HashSet}; +use bevy_utils::HashMap; use itertools::Itertools; use meshopt::{ build_meshlets, compute_cluster_bounds, compute_meshlet_bounds, @@ -54,18 +54,15 @@ impl MeshletMesh { .iter() .map(|m| m.triangle_count as u64) .sum(); + let mesh_scale = simplify_scale(&vertices); // Build further LODs let mut simplification_queue = 0..meshlets.len(); let mut lod_level = 1; while simplification_queue.len() > 1 { - // For each meshlet build a set of triangle edges - let triangle_edges_per_meshlet = - collect_triangle_edges_per_meshlet(simplification_queue.clone(), &meshlets); - // For each meshlet build a list of connected meshlets (meshlets that share a triangle edge) let connected_meshlets_per_meshlet = - find_connected_meshlets(simplification_queue.clone(), &triangle_edges_per_meshlet); + find_connected_meshlets(simplification_queue.clone(), &meshlets); // Group meshlets into roughly groups of 4, grouping meshlets with a high number of shared edges // http://glaros.dtc.umn.edu/gkhome/fetch/sw/metis/manual.pdf @@ -78,9 +75,13 @@ impl MeshletMesh { for group_meshlets in groups.values().filter(|group| group.len() > 1) { // Simplify the group to ~50% triangle count - let Some((simplified_group_indices, mut group_error)) = - simplify_meshlet_groups(group_meshlets, &meshlets, &vertices, lod_level) - else { + let Some((simplified_group_indices, mut group_error)) = simplify_meshlet_groups( + group_meshlets, + &meshlets, + &vertices, + lod_level, + mesh_scale, + ) else { continue; }; @@ -194,53 +195,67 @@ fn compute_meshlets(indices: &[u32], vertices: &VertexDataAdapter) -> Meshlets { meshlets } -fn collect_triangle_edges_per_meshlet( +fn find_connected_meshlets( simplification_queue: Range, meshlets: &Meshlets, -) -> HashMap> { - let mut triangle_edges_per_meshlet = HashMap::new(); - for meshlet_id in simplification_queue { +) -> HashMap> { + // for each edge, gather all meshlets that use it + let mut edges_to_meshlets = HashMap::new(); + + for meshlet_id in simplification_queue.clone() { let meshlet = meshlets.get(meshlet_id); - let meshlet_triangle_edges = triangle_edges_per_meshlet - .entry(meshlet_id) - .or_insert(HashSet::new()); for i in meshlet.triangles.chunks(3) { - let v0 = meshlet.vertices[i[0] as usize]; - let v1 = meshlet.vertices[i[1] as usize]; - let v2 = meshlet.vertices[i[2] as usize]; - meshlet_triangle_edges.insert((v0.min(v1), v0.max(v1))); - meshlet_triangle_edges.insert((v0.min(v2), v0.max(v2))); - meshlet_triangle_edges.insert((v1.min(v2), v1.max(v2))); + for k in 0..3 { + let v0 = meshlet.vertices[i[k] as usize]; + let v1 = meshlet.vertices[i[(k + 1) % 3] as usize]; + let edge = (v0.min(v1), v0.max(v1)); + + let vec = edges_to_meshlets.entry(edge).or_insert_with(Vec::new); + // meshlets are processed in order, so we can just check the last element to deduplicate + if vec.last() != Some(&meshlet_id) { + vec.push(meshlet_id); + } + } } } - triangle_edges_per_meshlet -} -fn find_connected_meshlets( - simplification_queue: Range, - triangle_edges_per_meshlet: &HashMap>, -) -> HashMap> { - let mut connected_meshlets_per_meshlet = HashMap::new(); + // for each meshlet pair, count how many edges they share + let mut shared_counts = HashMap::new(); + + for (_, meshlet_ids) in edges_to_meshlets { + for (meshlet_id1, meshlet_id2) in meshlet_ids.into_iter().tuple_combinations() { + let count = shared_counts + .entry((meshlet_id1.min(meshlet_id2), meshlet_id1.max(meshlet_id2))) + .or_insert(0); + *count += 1; + } + } + + // for each meshlet, gather all meshlets that share at least one edge along with shared edge count + let mut connected_meshlets = HashMap::new(); + for meshlet_id in simplification_queue.clone() { - connected_meshlets_per_meshlet.insert(meshlet_id, Vec::new()); + connected_meshlets.insert(meshlet_id, Vec::new()); } - for (meshlet_id1, meshlet_id2) in simplification_queue.tuple_combinations() { - let shared_edge_count = triangle_edges_per_meshlet[&meshlet_id1] - .intersection(&triangle_edges_per_meshlet[&meshlet_id2]) - .count(); - if shared_edge_count != 0 { - connected_meshlets_per_meshlet - .get_mut(&meshlet_id1) - .unwrap() - .push((meshlet_id2, shared_edge_count)); - connected_meshlets_per_meshlet - .get_mut(&meshlet_id2) - .unwrap() - .push((meshlet_id1, shared_edge_count)); - } + for ((meshlet_id1, meshlet_id2), shared_count) in shared_counts { + // note, we record id1->id2 and id2->id1 as adjacency is symmetrical + connected_meshlets + .get_mut(&meshlet_id1) + .unwrap() + .push((meshlet_id2, shared_count)); + connected_meshlets + .get_mut(&meshlet_id2) + .unwrap() + .push((meshlet_id1, shared_count)); } - connected_meshlets_per_meshlet + + // the order of meshlets depends on hash traversal order; to produce deterministic results, sort them + for (_, connected_meshlets) in connected_meshlets.iter_mut() { + connected_meshlets.sort(); + } + + connected_meshlets } fn group_meshlets( @@ -284,6 +299,7 @@ fn simplify_meshlet_groups( meshlets: &Meshlets, vertices: &VertexDataAdapter<'_>, lod_level: u32, + mesh_scale: f32, ) -> Option<(Vec, f32)> { // Build a new index buffer into the mesh vertex data by combining all meshlet data in the group let mut group_indices = Vec::new(); @@ -316,7 +332,7 @@ fn simplify_meshlet_groups( } // Convert error to object-space and convert from diameter to radius - error *= simplify_scale(vertices) * 0.5; + error *= mesh_scale * 0.5; Some((simplified_group_indices, error)) } From d23b050e35dfe9438e9e69ef1d42874f772f78ac Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Mon, 17 Jun 2024 18:32:53 -0700 Subject: [PATCH 2/5] Use SmallVec/2 instead of Vec for edges_to_meshlets We usually just have a single element in the vector (for interior edges) or two elements (for shared edges); everything else is uncommon. SmallVec reduces the number of allocations here and makes processing a little faster. --- crates/bevy_pbr/src/meshlet/from_mesh.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/bevy_pbr/src/meshlet/from_mesh.rs b/crates/bevy_pbr/src/meshlet/from_mesh.rs index f45afe98658d9..4f547f9f8ad66 100644 --- a/crates/bevy_pbr/src/meshlet/from_mesh.rs +++ b/crates/bevy_pbr/src/meshlet/from_mesh.rs @@ -11,6 +11,7 @@ use meshopt::{ simplify, simplify_scale, Meshlets, SimplifyOptions, VertexDataAdapter, }; use metis::Graph; +use smallvec::SmallVec; use std::{borrow::Cow, ops::Range}; impl MeshletMesh { @@ -210,8 +211,10 @@ fn find_connected_meshlets( let v1 = meshlet.vertices[i[(k + 1) % 3] as usize]; let edge = (v0.min(v1), v0.max(v1)); - let vec = edges_to_meshlets.entry(edge).or_insert_with(Vec::new); - // meshlets are processed in order, so we can just check the last element to deduplicate + let vec = edges_to_meshlets + .entry(edge) + .or_insert_with(SmallVec::<[usize; 2]>::new); + // meshlets are added in order, so we can just check the last element to deduplicate if vec.last() != Some(&meshlet_id) { vec.push(meshlet_id); } From 01591a911236dfb16e4bdd4382d484bbd3a9d857 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Mon, 17 Jun 2024 22:01:01 -0700 Subject: [PATCH 3/5] Capitalize comments and expand vec.last() comment --- crates/bevy_pbr/src/meshlet/from_mesh.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/bevy_pbr/src/meshlet/from_mesh.rs b/crates/bevy_pbr/src/meshlet/from_mesh.rs index 4f547f9f8ad66..5a7b16a2c5729 100644 --- a/crates/bevy_pbr/src/meshlet/from_mesh.rs +++ b/crates/bevy_pbr/src/meshlet/from_mesh.rs @@ -200,7 +200,7 @@ fn find_connected_meshlets( simplification_queue: Range, meshlets: &Meshlets, ) -> HashMap> { - // for each edge, gather all meshlets that use it + // For each edge, gather all meshlets that use it let mut edges_to_meshlets = HashMap::new(); for meshlet_id in simplification_queue.clone() { @@ -214,7 +214,8 @@ fn find_connected_meshlets( let vec = edges_to_meshlets .entry(edge) .or_insert_with(SmallVec::<[usize; 2]>::new); - // meshlets are added in order, so we can just check the last element to deduplicate + // Meshlets are added in order, so we can just check the last element to deduplicate, + // in the case of two triangles sharing the same edge within a single meshlet if vec.last() != Some(&meshlet_id) { vec.push(meshlet_id); } @@ -222,7 +223,7 @@ fn find_connected_meshlets( } } - // for each meshlet pair, count how many edges they share + // For each meshlet pair, count how many edges they share let mut shared_counts = HashMap::new(); for (_, meshlet_ids) in edges_to_meshlets { @@ -234,7 +235,7 @@ fn find_connected_meshlets( } } - // for each meshlet, gather all meshlets that share at least one edge along with shared edge count + // For each meshlet, gather all meshlets that share at least one edge along with shared edge count let mut connected_meshlets = HashMap::new(); for meshlet_id in simplification_queue.clone() { @@ -242,7 +243,7 @@ fn find_connected_meshlets( } for ((meshlet_id1, meshlet_id2), shared_count) in shared_counts { - // note, we record id1->id2 and id2->id1 as adjacency is symmetrical + // We record id1->id2 and id2->id1 as adjacency is symmetrical connected_meshlets .get_mut(&meshlet_id1) .unwrap() @@ -253,7 +254,7 @@ fn find_connected_meshlets( .push((meshlet_id1, shared_count)); } - // the order of meshlets depends on hash traversal order; to produce deterministic results, sort them + // The order of meshlets depends on hash traversal order; to produce deterministic results, sort them for (_, connected_meshlets) in connected_meshlets.iter_mut() { connected_meshlets.sort(); } From 7b3348a1e5f559427557000ca8559b72a4f7bb14 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Mon, 17 Jun 2024 22:02:25 -0700 Subject: [PATCH 4/5] Use unstable sort: the meshlet id is unique so we don't need stability. --- crates/bevy_pbr/src/meshlet/from_mesh.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_pbr/src/meshlet/from_mesh.rs b/crates/bevy_pbr/src/meshlet/from_mesh.rs index 5a7b16a2c5729..1a2e355f836a8 100644 --- a/crates/bevy_pbr/src/meshlet/from_mesh.rs +++ b/crates/bevy_pbr/src/meshlet/from_mesh.rs @@ -256,7 +256,7 @@ fn find_connected_meshlets( // The order of meshlets depends on hash traversal order; to produce deterministic results, sort them for (_, connected_meshlets) in connected_meshlets.iter_mut() { - connected_meshlets.sort(); + connected_meshlets.sort_unstable(); } connected_meshlets From 25f2e1b8950a9687a03a67889865ed3b5a34c2f0 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Mon, 17 Jun 2024 22:03:30 -0700 Subject: [PATCH 5/5] Rename shared_counts to shared_edge_count --- crates/bevy_pbr/src/meshlet/from_mesh.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_pbr/src/meshlet/from_mesh.rs b/crates/bevy_pbr/src/meshlet/from_mesh.rs index 1a2e355f836a8..3bae6d9011e46 100644 --- a/crates/bevy_pbr/src/meshlet/from_mesh.rs +++ b/crates/bevy_pbr/src/meshlet/from_mesh.rs @@ -224,11 +224,11 @@ fn find_connected_meshlets( } // For each meshlet pair, count how many edges they share - let mut shared_counts = HashMap::new(); + let mut shared_edge_count = HashMap::new(); for (_, meshlet_ids) in edges_to_meshlets { for (meshlet_id1, meshlet_id2) in meshlet_ids.into_iter().tuple_combinations() { - let count = shared_counts + let count = shared_edge_count .entry((meshlet_id1.min(meshlet_id2), meshlet_id1.max(meshlet_id2))) .or_insert(0); *count += 1; @@ -242,7 +242,7 @@ fn find_connected_meshlets( connected_meshlets.insert(meshlet_id, Vec::new()); } - for ((meshlet_id1, meshlet_id2), shared_count) in shared_counts { + for ((meshlet_id1, meshlet_id2), shared_count) in shared_edge_count { // We record id1->id2 and id2->id1 as adjacency is symmetrical connected_meshlets .get_mut(&meshlet_id1)