diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74f86c8..386aae9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,10 @@ name: continuous-integration -on: [push, pull_request] +on: + push: + branches: + - 'master' + pull_request: jobs: build_and_test: diff --git a/README.md b/README.md index b5097da..f6729ec 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ If you want to use the original C++ implementation from Rust, check out the [mes ## Features -`meshoptimizer` v0.20 feature level is the current support target. Planned but currently missing features: +`meshoptimizer` v0.21 feature level is the current support target. Planned but currently missing features: - [SIMD support](https://github.com/yzsolt/meshopt-rs/issues/1) - [WASM support](https://github.com/yzsolt/meshopt-rs/issues/2) diff --git a/examples/demo.rs b/examples/demo.rs index e95948c..68aa4a2 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -630,6 +630,7 @@ fn simplify_attr(mesh: &Mesh, threshold: f32) { &mesh.indices, &mesh.vertices, &ATTR_WEIGHTS, + None, target_index_count, target_error, SimplificationOptions::empty(), @@ -703,6 +704,9 @@ fn simplify_mesh_points(mesh: &Mesh, threshold: f32) { let start = Instant::now(); let target_vertex_count = (mesh.vertices.len() as f32 * threshold) as usize; + if target_vertex_count == 0 { + return; + } let mut indices = vec![0; target_vertex_count]; let size = simplify_points(&mut indices, &mesh.vertices, target_vertex_count, 0.0); @@ -869,6 +873,91 @@ fn simplify_mesh_complete(mesh: &Mesh) { } } +#[cfg(feature = "experimental")] +fn simplify_mesh_clusters(mesh: &Mesh, threshold: f32) { + // note: we use clusters that are larger than normal to give simplifier room to work; in practice you'd use cluster groups merged from smaller clusters and build a cluster DAG + const MAX_VERTICES: usize = 255; + const MAX_TRIANGLES: usize = 512; + + let start = Instant::now(); + + let max_meshlets = build_meshlets_bound(mesh.indices.len(), MAX_VERTICES, MAX_TRIANGLES); + let mut meshlets = vec![Meshlet::default(); max_meshlets]; + let mut meshlet_vertices = vec![0u32; max_meshlets * MAX_VERTICES]; + let mut meshlet_triangles = vec![0u8; max_meshlets * MAX_TRIANGLES * 3]; + + let size = build_meshlets( + &mut meshlets, + &mut meshlet_vertices, + &mut meshlet_triangles, + &mesh.indices, + &mesh.vertices, + MAX_VERTICES, + MAX_TRIANGLES, + 0.0, + ); + meshlets.truncate(size); + + let middle = Instant::now(); + + let scale = simplify_scale(&mesh.vertices); + + let mut lod = Vec::with_capacity(mesh.indices.len()); + + let mut error: f32 = 0.0; + + for m in meshlets.iter() { + let cluster_offset = lod.len(); + + for j in 0..m.triangle_count * 3 { + lod.push( + meshlet_vertices + [m.vertex_offset as usize + meshlet_triangles[m.triangle_offset as usize + j as usize] as usize], + ); + } + + let options = SimplificationOptions::SimplifyLockBorder + | SimplificationOptions::SimplifySparse + | SimplificationOptions::SimplifyErrorAbsolute; + + let index_count = m.triangle_count as usize * 3; + let mut dest = vec![0u32; index_count]; + + let cluster_target_error = 1e-2 * scale; + let cluster_target = (m.triangle_count as f32 * threshold) as usize * 3; + let mut cluster_error = 0.0; + + let cluster_size = simplify( + &mut dest, + &lod[cluster_offset..cluster_offset + index_count], + &mesh.vertices, + cluster_target, + cluster_target_error, + options, + Some(&mut cluster_error), + ); + + lod[cluster_offset..cluster_offset + cluster_size].copy_from_slice(&dest[0..cluster_size]); + + error = error.max(cluster_error); + + // simplified cluster is available in lod[cluster_offset..cluster_offset + cluster_size] + lod.resize(cluster_offset + cluster_size, 0); + } + + let end = Instant::now(); + + println!( + "{:9}: {} triangles => {} triangles ({:.2}% deviation) in {:.2} msec, clusterized in {:.2} msec", + "SimplifyN", // N for Nanite + mesh.indices.len() / 3, + lod.len() / 3, + error / scale * 100.0, + (end - middle).as_micros() as f64 / 1000.0, + (middle - start).as_micros() as f64 / 1000.0 + ); +} + fn stripify_mesh(mesh: &Mesh, use_restart: bool, desc: char) { let restart_index = if use_restart { INVALID_INDEX } else { 0 }; @@ -979,6 +1068,16 @@ fn meshlets(mesh: &Mesh, scan: bool) { meshlets.truncate(size); + #[cfg(feature = "experimental")] + for meshlet in &meshlets { + optimize_meshlet( + &mut meshlet_vertices + [meshlet.vertex_offset as usize..(meshlet.vertex_offset + meshlet.vertex_count) as usize], + &mut meshlet_triangles + [meshlet.triangle_offset as usize..(meshlet.triangle_offset + meshlet.triangle_count * 3) as usize], + ); + } + if !meshlets.is_empty() { let last = meshlets.last().unwrap(); @@ -1358,6 +1457,7 @@ fn process(mesh: &Mesh) { simplify_mesh_sloppy(mesh, 0.2); simplify_mesh_complete(mesh); simplify_mesh_points(mesh, 0.2); + simplify_mesh_clusters(mesh, 0.2); } spatial_sort_mesh(mesh); @@ -1366,9 +1466,8 @@ fn process(mesh: &Mesh) { spatial_sort_mesh_triangles(mesh); } -fn process_dev(#[allow(unused)] mesh: &Mesh) { - #[cfg(feature = "experimental")] - simplify_attr(mesh, 0.2); +fn process_dev(mesh: &Mesh) { + simplify_mesh(mesh); } fn main() -> Result<(), Box> { diff --git a/src/cluster.rs b/src/cluster.rs index 06e6406..33ea77a 100644 --- a/src/cluster.rs +++ b/src/cluster.rs @@ -639,7 +639,7 @@ fn get_neighbor_triangle( /// * `meshlets`: must contain enough space for all meshlets, worst case size can be computed with [build_meshlets_bound] /// * `meshlet_vertices`: must contain enough space for all meshlets, worst case size is equal to `max_meshlets` * `max_vertices` /// * `meshlet_triangles`: must contain enough space for all meshlets, worst case size is equal to `max_meshlets` * `max_triangles * 3` -/// * `max_vertices` and `max_triangles`: must not exceed implementation limits (`max_vertices` <= 255 - not 256!, `max_triangles` <= 512) +/// * `max_vertices` and `max_triangles`: must not exceed implementation limits (`max_vertices` <= 255 - not 256!, `max_triangles` <= 512; `max_triangles` must be divisible by 4) /// * `cone_weight`: should be set to 0 when cone culling is not used, and a value between 0 and 1 otherwise to balance between cluster size and cone culling efficiency #[allow(clippy::too_many_arguments)] pub fn build_meshlets( @@ -1026,6 +1026,100 @@ where compute_cluster_bounds(&indices[0..meshlet_triangles.len()], vertices) } +/// Reorders meshlet vertices and triangles to maximize locality to improve rasterizer throughput +/// +/// # Arguments +/// +/// * `meshlet_vertices` and `meshlet_triangles`: must refer to meshlet triangle and vertex index data; when [`build_meshlets`] is used, these +/// need to be computed from [Meshlet::vertex_offset] and [Meshlet::triangle_offset] +#[cfg(feature = "experimental")] +pub fn optimize_meshlet(meshlet_vertices: &mut [u32], meshlet_triangles: &mut [u8]) { + let triangle_count = meshlet_triangles.len() / 3; + let vertex_count = meshlet_vertices.len(); + + assert!(triangle_count <= MESHLET_MAX_TRIANGLES); + assert!(vertex_count <= MESHLET_MAX_VERTICES); + + let indices = meshlet_triangles; + let vertices = meshlet_vertices; + + // cache tracks vertex timestamps (corresponding to triangle index! all 3 vertices are added at the same time and never removed) + let mut cache = [0u8; MESHLET_MAX_VERTICES]; + + // note that we start from a value that means all vertices aren't in cache + let mut cache_last: u8 = 128; + const CACHE_CUTOFF: u8 = 3; // 3 triangles = ~5..9 vertices depending on reuse + + for i in 0..triangle_count { + let mut next = -1; + let mut next_match = -1; + + for (j, abc) in indices[i * 3..].as_chunks::<3>().0.iter().enumerate() { + assert!(abc.iter().all(|e| (*e as usize) < vertices.len())); + + // score each triangle by how many vertices are in cache + // note: the distance is computed using unsigned 8-bit values, so cache timestamp overflow is handled gracefully + let sum = abc + .iter() + .map(|i| ((cache_last - cache[*i as usize]) < CACHE_CUTOFF) as i32) + .sum(); + + if sum > next_match { + next = (i + j) as i32; + next_match = sum; + + // note that we could end up with all 3 vertices in the cache, but 2 is enough for ~strip traversal + if next_match >= 2 { + break; + } + } + } + + assert!(next >= 0); + + let a = indices[next as usize * 3 + 0]; + let b = indices[next as usize * 3 + 1]; + let c = indices[next as usize * 3 + 2]; + + // shift triangles before the next one forward so that we always keep an ordered partition + // note: this could have swapped triangles [i] and [next] but that distorts the order and may skew the output sequence + let count = (next as usize - i) * 3; + indices.copy_within((i * 3)..(i * 3 + count), (i + 1) * 3); + + indices[i * 3 + 0] = a; + indices[i * 3 + 1] = b; + indices[i * 3 + 2] = c; + + // cache timestamp is the same between all vertices of each triangle to reduce overflow + cache_last += 1; + cache[a as usize] = cache_last; + cache[b as usize] = cache_last; + cache[c as usize] = cache_last; + } + + // reorder meshlet vertices for access locality assuming index buffer is scanned sequentially + let mut order = [0u32; MESHLET_MAX_VERTICES]; + + let mut remap = [UNUSED; MESHLET_MAX_VERTICES]; + + let mut vertex_offset = 0; + + for i in 0..triangle_count * 3 { + let r = &mut remap[indices[i] as usize]; + + if *r == UNUSED { + *r = vertex_offset as u8; + order[vertex_offset] = vertices[indices[i] as usize]; + vertex_offset += 1; + } + + indices[i] = *r; + } + + assert!(vertex_offset <= vertex_count); + vertices.copy_from_slice(&order[0..vertex_offset]); +} + #[cfg(test)] mod test { use super::*; diff --git a/src/overdraw.rs b/src/overdraw.rs index d7e438f..56f3474 100644 --- a/src/overdraw.rs +++ b/src/overdraw.rs @@ -52,8 +52,9 @@ fn rasterize(buffer: &mut OverdrawBuffer, mut v1: Vector3, mut v2: Vector3, mut // flip backfacing triangles to simplify rasterization logic if sign { - // flipping v2 & v3 preserves depth gradients since they're based on v1 - std::mem::swap(&mut v2, &mut v3); + // flipping v2 & v3 preserves depth gradients since they're based on v1; only v1z is used below + std::mem::swap(&mut v2.x, &mut v3.x); + std::mem::swap(&mut v2.y, &mut v3.y); // flip depth since we rasterize backfacing triangles to second buffer with reverse Z; only v1z is used below v1.z = VIEWPORT as f32 - v1.z; @@ -148,7 +149,7 @@ where let mut result = OverdrawStatistics::default(); - let (minv, extent) = calc_pos_extents(vertices); + let (minv, extent) = calc_pos_extents(vertices.iter().map(|v| v.pos())); let scale = VIEWPORT as f32 / extent; diff --git a/src/simplify.rs b/src/simplify.rs index 5755ffe..d2ba1d7 100644 --- a/src/simplify.rs +++ b/src/simplify.rs @@ -39,10 +39,10 @@ fn prepare_edge_adjacency(adjacency: &mut EdgeAdjacency, index_count: usize, ver adjacency.data = vec![Edge::default(); index_count]; } -fn update_edge_adjacency(adjacency: &mut EdgeAdjacency, indices: &[u32], remap: Option<&[u32]>) { +fn update_edge_adjacency(adjacency: &mut EdgeAdjacency, indices: &[u32], vertex_count: usize, remap: Option<&[u32]>) { let face_count = indices.len() / 3; - let offsets = &mut adjacency.offsets[1..]; + let offsets = &mut adjacency.offsets[1..vertex_count + 1]; // fill edge counts offsets.fill(0); @@ -53,6 +53,7 @@ fn update_edge_adjacency(adjacency: &mut EdgeAdjacency, indices: &[u32], remap: } else { *index as usize }; + assert!(v < vertex_count); offsets[v] += 1; } @@ -135,18 +136,38 @@ mod hash { } impl Eq for VertexPosition {} + + #[derive(PartialEq, Eq, Debug)] + pub struct RemapId(pub u32); + + impl Hash for RemapId { + fn hash(&self, state: &mut H) { + state.write_u32(self.0.wrapping_mul(0x5bd1e995)); + } + } } -fn build_position_remap(remap: &mut [u32], wedge: &mut [u32], vertices: &[V]) -where +fn build_position_remap( + remap: &mut [u32], + wedge: &mut [u32], + vertices: &[V], + vertex_count: usize, + sparse_remap: Option<&[u32]>, +) where V: Vertex, { let mut table = HashMap::with_capacity_and_hasher(vertices.len(), BuildNoopHasher::default()); // build forward remap: for each vertex, which other (canonical) vertex does it map to? // we use position equivalence for this, and remap vertices to other existing vertices - for (index, vertex) in vertices.iter().enumerate() { - remap[index] = match table.entry(hash::VertexPosition(vertex.pos())) { + for (index, vertex) in vertices[0..vertex_count].iter().enumerate() { + let vp = if let Some(remap) = sparse_remap { + vertices[remap[index] as usize].pos() + } else { + vertex.pos() + }; + + remap[index] = match table.entry(hash::VertexPosition(vp)) { Entry::Occupied(entry) => *entry.get(), Entry::Vacant(entry) => { entry.insert(index as u32); @@ -173,6 +194,45 @@ where } } +fn build_sparse_remap(indices: &mut [u32], vertices: &[V]) -> Vec +where + V: Vertex, +{ + // use a bit set to compute the precise number of unique vertices + let mut filter = vec![0u8; vertices.len().div_ceil(8)]; + + let mut unique = 0; + for index in indices.iter().map(|i| *i as usize) { + assert!(index < vertices.len()); + + unique += ((filter[index / 8] & (1 << (index % 8))) == 0) as usize; + filter[index / 8] |= 1 << (index % 8); + } + + let mut remap = vec![0u32; unique]; + let mut offset = 0; + + // temporary map dense => sparse + let mut revremap = HashMap::with_capacity_and_hasher(unique, BuildNoopHasher::default()); + + // fill remap, using revremap as a helper, and rewrite indices in the same pass + for index in indices { + *index = match revremap.entry(hash::RemapId(*index)) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + remap[offset] = *index; + entry.insert(offset as u32); + offset += 1; + offset as u32 - 1 + } + }; + } + + assert_eq!(offset, unique); + + remap +} + #[derive(Clone, Copy, PartialEq)] enum VertexKind { Manifold, // not on an attribute seam, not on any boundary @@ -239,6 +299,8 @@ fn classify_vertices( adjacency: &EdgeAdjacency, remap: &[u32], wedge: &[u32], + vertex_lock: Option<&[bool]>, + sparse_remap: Option<&[u32]>, options: SimplificationOptions, ) { // incoming & outgoing open edges: `INVALID_INDEX` if no open edges, i if there are more than 1 @@ -284,7 +346,12 @@ fn classify_vertices( for i in 0..vertex_count { if remap[i] == i as u32 { - if wedge[i] == i as u32 { + if let Some(vertex_lock) = vertex_lock + && vertex_lock[sparse_remap.map(|r| r[i] as usize).unwrap_or(i)] + { + // vertex is explicitly locked + result[i] = VertexKind::Locked; + } else if wedge[i] == i as u32 { // no attribute seam, need to check if it's manifold let openi = openinc[i]; let openo = openout[i]; @@ -371,21 +438,39 @@ fn classify_vertices( ); } -fn rescale_positions(result: &mut [Vector3], vertices: &[V]) -> f32 +fn rescale_positions( + result: &mut [Vector3], + vertices: &[V], + sparse_remap: Option<&[u32]>, +) -> f32 where V: Vertex, { - let (minv, extent) = calc_pos_extents(vertices); + let (minv, extent) = if let Some(remap) = sparse_remap { + for (v, r) in remap + .iter() + .map(|ri| vertices[*ri as usize].pos()) + .zip(result.iter_mut()) + { + *r = Vector3 { + x: v[0], + y: v[1], + z: v[2], + }; + } - for (i, vertex) in vertices.iter().enumerate() { - let v = vertex.pos(); + calc_pos_extents(remap.iter().map(|ri| vertices[*ri as usize].pos())) + } else { + for (v, r) in vertices.iter().map(|v| v.pos()).zip(result.iter_mut()) { + *r = Vector3 { + x: v[0], + y: v[1], + z: v[2], + }; + } - result[i] = Vector3 { - x: v[0], - y: v[1], - z: v[2], - }; - } + calc_pos_extents(vertices.iter().map(|v| v.pos())) + }; let scale = zero_inverse(extent); @@ -400,19 +485,30 @@ where fn rescale_attributes( vertices: &[V], + vertex_count: usize, attribute_weights: &[f32; ATTR_COUNT], + sparse_remap: Option<&[u32]>, ) -> Vec<[f32; ATTR_COUNT]> where V: Vertex, { - let mut vertex_weighted_attrs = vec![[0f32; ATTR_COUNT]; vertices.len()]; + let mut vertex_weighted_attrs = vec![[0f32; ATTR_COUNT]; vertex_count]; - for (weighted_attrs, vertex) in vertex_weighted_attrs.iter_mut().zip(vertices.iter()) { - for (weighted_attr, (attr, attr_weight)) in weighted_attrs - .iter_mut() - .zip(vertex.attrs().iter().zip(attribute_weights.iter())) - { - *weighted_attr = attr * attr_weight; + for (weighted_attrs, (i, vertex)) in vertex_weighted_attrs.iter_mut().zip(vertices.iter().enumerate()) { + if let Some(remap) = sparse_remap { + for (weighted_attr, (attr, attr_weight)) in weighted_attrs + .iter_mut() + .zip(vertices[remap[i] as usize].attrs().iter().zip(attribute_weights.iter())) + { + *weighted_attr = attr * attr_weight; + } + } else { + for (weighted_attr, (attr, attr_weight)) in weighted_attrs + .iter_mut() + .zip(vertex.attrs().iter().zip(attribute_weights.iter())) + { + *weighted_attr = attr * attr_weight; + } } } @@ -870,7 +966,13 @@ fn has_triangle_flip(a: &Vector3, b: &Vector3, c: &Vector3, d: &Vector3) -> bool eb.x * ed.y - eb.y * ed.x, ); - nbc.x * nbd.x + nbc.y * nbd.y + nbc.z * nbd.z <= 0.0 + let ndp = nbc.x * nbd.x + nbc.y * nbd.y + nbc.z * nbd.z; + let abc = nbc.x * nbc.x + nbc.y * nbc.y + nbc.z * nbc.z; + let abd = nbd.x * nbd.x + nbd.y * nbd.y + nbd.z * nbd.z; + + // scale is cos(angle); somewhat arbitrarily set to ~75 degrees + // note that the "pure" check is ndp <= 0 (90 degree cutoff) but that allows flipping through a series of close-to-90 collapses + ndp <= 0.25 * (abc * abd).sqrt() } fn has_triangle_flips( @@ -1097,6 +1199,7 @@ fn perform_edge_collapses( attribute_quadrics: &mut [Quadric], attribute_gradients: &mut [[QuadricGrad; ATTR_COUNT]], collapses: &[Collapse], + collapse_count: usize, collapse_order: &[u32], remap: &[u32], wedge: &[u32], @@ -1114,7 +1217,7 @@ fn perform_edge_collapses( // note that edge_collapse_goal is an estimate; triangle_collapse_goal will be used to actually limit collapses let mut edge_collapse_goal = triangle_collapse_goal / 2; - for order in collapse_order { + for order in collapse_order.iter().take(collapse_count) { let c = collapses[*order as usize].clone(); let error = unsafe { c.u.error }; @@ -1519,9 +1622,16 @@ mod experimental { } bitflags! { + #[derive(Clone, Copy)] pub struct SimplificationOptions: u32 { /// Do not move vertices that are located on the topological border (vertices on triangle edges that don't have a paired triangle). Useful for simplifying portions of the larger mesh. const SimplifyLockBorder = 1 << 0; + + /// Improve simplification performance assuming input indices are a sparse subset of the mesh. Note that error becomes relative to subset extents. + const SimplifySparse = 1 << 1; + + /// Treat error limit and resulting error as absolute instead of relative to mesh extents. + const SimplifyErrorAbsolute = 1 << 2; } } @@ -1556,6 +1666,7 @@ where indices, vertices, &[], + None, target_index_count, target_error, options, @@ -1572,6 +1683,7 @@ where /// /// * `vertex_attributes`: should have attribute_count floats for each vertex /// * `attribute_weights`: should have attribute_count floats in total; the weights determine relative priority of attributes between each other and wrt position. The recommended weight range is [1e-3..1e-1], assuming attribute data is in [0..1] range. +/// * `vertex_lock`: when `Some`, it defines for each vertex if they can't be moved (`true`) or free to be simplified (`false`). /// /// TODO `target_error`/`result_error` currently use combined distance+attribute error; this may change in the future #[cfg(feature = "experimental")] @@ -1581,6 +1693,7 @@ pub fn simplify_with_attributes( indices: &[u32], vertices: &[V], attribute_weights: &[f32; ATTR_COUNT], + vertex_lock: Option<&[bool]>, target_index_count: usize, target_error: f32, options: SimplificationOptions, @@ -1594,6 +1707,7 @@ where indices, vertices, attribute_weights, + vertex_lock, target_index_count, target_error, options, @@ -1607,6 +1721,7 @@ fn simplify_edge( indices: &[u32], vertices: &[V], attribute_weights: &[f32; ATTR_COUNT], + vertex_lock: Option<&[bool]>, target_index_count: usize, target_error: f32, options: SimplificationOptions, @@ -1623,46 +1738,59 @@ where assert!(target_index_count <= indices.len()); let result = &mut destination[0..indices.len()]; + result.copy_from_slice(indices); + + // build an index remap and update indices/vertex_count to minimize the subsequent work + // note: as a consequence, errors will be computed relative to the subset extent + let (vertex_count, sparse_remap) = if options.contains(SimplificationOptions::SimplifySparse) { + let remap = build_sparse_remap(result, vertices); + (remap.len(), Some(remap)) + } else { + (vertices.len(), None) + }; + let sparse_remap = sparse_remap.as_deref(); // build adjacency information let mut adjacency = EdgeAdjacency::default(); - prepare_edge_adjacency(&mut adjacency, indices.len(), vertices.len()); - update_edge_adjacency(&mut adjacency, indices, None); + prepare_edge_adjacency(&mut adjacency, indices.len(), vertex_count); + update_edge_adjacency(&mut adjacency, result, vertex_count, None); // build position remap that maps each vertex to the one with identical position - let mut remap = vec![0u32; vertices.len()]; - let mut wedge = vec![0u32; vertices.len()]; - build_position_remap(&mut remap, &mut wedge, vertices); + let mut remap = vec![0u32; vertex_count]; + let mut wedge = vec![0u32; vertex_count]; + build_position_remap(&mut remap, &mut wedge, vertices, vertex_count, sparse_remap); // classify vertices; vertex kind determines collapse rules, see `CAN_COLLAPSE` - let mut vertex_kind = vec![VertexKind::Manifold; vertices.len()]; - let mut loop_ = vec![INVALID_INDEX; vertices.len()]; - let mut loopback = vec![INVALID_INDEX; vertices.len()]; + let mut vertex_kind = vec![VertexKind::Manifold; vertex_count]; + let mut loop_ = vec![INVALID_INDEX; vertex_count]; + let mut loopback = vec![INVALID_INDEX; vertex_count]; classify_vertices( &mut vertex_kind, &mut loop_, &mut loopback, - vertices.len(), + vertex_count, &adjacency, &remap, &wedge, + vertex_lock, + sparse_remap, options, ); #[cfg(feature = "trace")] { let mut unique_positions = 0; - for (i, remapped) in remap.iter().enumerate().take(vertices.len()) { + for (i, remapped) in remap.iter().enumerate().take(vertex_count) { unique_positions += (*remapped as usize == i) as u32; } println!( "position remap: {} vertices => {unique_positions} positions", - vertices.len(), + vertex_count, ); let mut kinds = [0_u32; KIND_COUNT]; - for i in 0..vertices.len() { + for i in 0..vertex_count { kinds[vertex_kind[i].index()] += (remap[i] as usize == i) as u32; } @@ -1676,30 +1804,30 @@ where ); } - let mut vertex_positions = vec![Vector3::default(); vertices.len()]; // TODO: spare init? - rescale_positions(&mut vertex_positions, vertices); + let mut vertex_positions = vec![Vector3::default(); vertex_count]; // TODO: spare init? + let vertex_scale = rescale_positions(&mut vertex_positions, vertices, sparse_remap); let vertex_attributes = if ATTR_COUNT > 0 { - rescale_attributes(vertices, attribute_weights) + rescale_attributes(vertices, vertex_count, attribute_weights, sparse_remap) } else { Vec::new() }; - let mut vertex_quadrics = vec![Quadric::default(); vertices.len()]; + let mut vertex_quadrics = vec![Quadric::default(); vertex_count]; let (mut attribute_quadrics, mut attribute_gradients) = if ATTR_COUNT > 0 { ( - vec![Quadric::default(); vertices.len()], - vec![[QuadricGrad::default(); ATTR_COUNT]; vertices.len()], + vec![Quadric::default(); vertex_count], + vec![[QuadricGrad::default(); ATTR_COUNT]; vertex_count], ) } else { (Vec::new(), Vec::new()) }; - fill_face_quadrics(&mut vertex_quadrics, indices, &vertex_positions, &remap); + fill_face_quadrics(&mut vertex_quadrics, result, &vertex_positions, &remap); fill_edge_quadrics( &mut vertex_quadrics, - indices, + result, &vertex_positions, &remap, &vertex_kind, @@ -1711,34 +1839,37 @@ where fill_attribute_quadrics::( &mut attribute_quadrics, &mut attribute_gradients, - indices, + result, &vertex_positions, &vertex_attributes, &remap, ); } - result.copy_from_slice(indices); - let collapse_capacity = bound_edge_collapses(&adjacency, indices.len(), &vertex_kind); // TODO: skip init? let mut edge_collapses = vec![Collapse::default(); collapse_capacity]; let mut collapse_order = vec![0u32; collapse_capacity]; - let mut collapse_remap = vec![0u32; vertices.len()]; - let mut collapse_locked = vec![false; vertices.len()]; + let mut collapse_remap = vec![0u32; vertex_count]; + let mut collapse_locked = vec![false; vertex_count]; let mut result_count = indices.len(); let mut result_error_max = 0.0; // `target_error` input is linear; we need to adjust it to match `Quadric::error` units - let error_limit = target_error * target_error; + let error_scale = if options.contains(SimplificationOptions::SimplifyErrorAbsolute) { + vertex_scale + } else { + 1.0 + }; + let error_limit = (target_error * target_error) / (error_scale * error_scale); #[cfg(feature = "trace")] let mut pass_count = 0; while result_count > target_index_count { // note: throughout the simplification process adjacency structure reflects welded topology for result-in-progress - update_edge_adjacency(&mut adjacency, &result[0..result_count], Some(&remap)); + update_edge_adjacency(&mut adjacency, &result[0..result_count], vertex_count, Some(&remap)); let edge_collapse_count = pick_edge_collapses( &mut edge_collapses, @@ -1788,6 +1919,7 @@ where &mut attribute_quadrics, &mut attribute_gradients, &edge_collapses, + edge_collapse_count, &collapse_order, &remap, &wedge, @@ -1808,20 +1940,28 @@ where remap_edge_loops(&mut loopback, &collapse_remap); let new_count = remap_index_buffer(&mut result[0..result_count], &collapse_remap); - assert!(new_count < result_count); + assert!(new_count < result_count, "{new_count} < {result_count}"); result_count = new_count; } #[cfg(feature = "trace")] println!( - "result: {result_count} triangles, error: {:e}; total {pass_count} passes", + "result: {} triangles, error: {:e}; total {pass_count} passes", + result_count / 3, result_error_max.sqrt() ); + // convert resulting indices back into the dense space of the larger mesh + if let Some(remap) = sparse_remap { + for result in result { + *result = remap[*result as usize]; + } + } + // result_error is quadratic; we need to remap it back to linear if let Some(result_error) = result_error { - *result_error = result_error_max.sqrt(); + *result_error = result_error_max.sqrt() * error_scale; } result_count @@ -1861,7 +2001,7 @@ where let target_cell_count = target_index_count / 6; let mut vertex_positions = vec![Vector3::default(); vertices.len()]; - rescale_positions(&mut vertex_positions, vertices); + rescale_positions(&mut vertex_positions, vertices, None); // find the optimal grid size using guided binary search #[cfg(feature = "trace")] @@ -2035,7 +2175,7 @@ where } let mut vertex_positions = vec![Vector3::default(); vertices.len()]; - rescale_positions(&mut vertex_positions, vertices); + rescale_positions(&mut vertex_positions, vertices, None); // find the optimal grid size using guided binary search #[cfg(feature = "trace")] @@ -2179,7 +2319,7 @@ pub fn simplify_scale(vertices: &[V]) -> f32 where V: Vertex, { - let (_minv, extent) = calc_pos_extents(vertices); + let (_minv, extent) = calc_pos_extents(vertices.iter().map(|v| v.pos())); extent } @@ -2557,6 +2697,7 @@ mod test { &ib, &vb, &attr_weights, + None, 6 * 3, 1e-2, SimplificationOptions::empty(), @@ -2566,4 +2707,232 @@ mod test { ); assert!(actual.iter().zip(expected.iter().flatten()).all(|(a, b)| a == b)); } + + #[test] + #[cfg(feature = "experimental")] + fn test_simplify_lock_flags() { + #[rustfmt::skip] + let vb = vb_from_slice(&[ + 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.0, 2.0, 0.0, + 1.0, 0.0, 0.0, + 1.0, 1.0, 0.0, + 1.0, 2.0, 0.0, + 2.0, 0.0, 0.0, + 2.0, 1.0, 0.0, + 2.0, 2.0, 0.0, + ]); + + #[rustfmt::skip] + let lock = [ + true, true, true, + true, false, true, + true, true, true, + ]; + + // 0 1 2 + // 3 4 5 + // 6 7 8 + + #[rustfmt::skip] + let ib = [ + 0, 1, 3, + 3, 1, 4, + 1, 2, 4, + 4, 2, 5, + 3, 4, 6, + 6, 4, 7, + 4, 5, 7, + 7, 5, 8, + ]; + + #[rustfmt::skip] + let expected = [ + 0, 1, 3, + 1, 2, 3, + 3, 2, 5, + 6, 3, 7, + 3, 5, 7, + 7, 5, 8, + ]; + + let mut actual = vec![0u32; ib.len()]; + + assert_eq!( + simplify_with_attributes::<_, 0>( + &mut actual, + &ib, + &vb, + &[], + Some(&lock), + 3, + 1e-3, + SimplificationOptions::empty(), + None + ), + 18 + ); + assert_eq!(&actual[0..expected.len()], expected); + } + + #[test] + #[cfg(feature = "experimental")] + fn test_simplify_sparse() { + #[derive(Default, Clone, Copy)] + struct TestVertexWithAttributes(([f32; 3], f32)); + + impl Vertex<1> for TestVertexWithAttributes { + fn pos(&self) -> [f32; 3] { + self.0.0 + } + + fn attrs(&self) -> [f32; 1] { + [self.0.1] + } + } + + #[rustfmt::skip] + let vb = vb_from_slice(&[ + 0.0, 0.0, 100.0, + 0.0, 1.0, 0.0, + 0.0, 2.0, 100.0, + 1.0, 0.0, 0.1, + 1.0, 1.0, 0.1, + 1.0, 2.0, 0.1, + 2.0, 0.0, 100.0, + 2.0, 1.0, 0.0, + 2.0, 2.0, 100.0, + ]); + + #[rustfmt::skip] + let vba: Vec = vb.iter().zip([ + 100.0, + 0.5, + 100.0, + 0.5, + 0.5, + 0.0, + 100.0, + 0.5, + 100.0 + ].iter()).map(|(p, a)| TestVertexWithAttributes((p.pos(), *a))).collect(); + + let aw = [0.2]; + + #[rustfmt::skip] + let lock = [ + true, true, true, + true, false, true, + true, true, true, + ]; + + // 1 + // 3 4 5 + // 7 + + #[rustfmt::skip] + let ib = [ + 3, 1, 4, + 1, 5, 4, + 3, 4, 7, + 4, 5, 7, + ]; + + // vertices 3-4-5 are slightly elevated along Z which guides the collapses when only using geometry + #[rustfmt::skip] + let expected = [ + 1, 5, 3, + 3, 5, 7, + ]; + + let mut actual = vec![0u32; ib.len()]; + + assert_eq!( + simplify::<_>( + &mut actual, + &ib, + &vb, + 6, + 1e-3, + SimplificationOptions::SimplifySparse, + None + ), + 6 + ); + assert_eq!(&actual[0..expected.len()], expected); + + // vertices 1-4-7 have a crease in the attribute value which guides the collapses the opposite way when weighing attributes sufficiently + #[rustfmt::skip] + let expecteda = [ + 3, 1, 7, + 1, 5, 7, + ]; + + assert_eq!( + simplify_with_attributes::<_, 1>( + &mut actual, + &ib, + &vba, + &aw, + Some(&lock), + 6, + 1e-1, + SimplificationOptions::SimplifySparse, + None + ), + 6 + ); + assert_eq!(&actual[0..expecteda.len()], expecteda); + } + + #[test] + #[cfg(feature = "experimental")] + fn test_simplify_error_absolute() { + #[rustfmt::skip] + let vb = vb_from_slice(&[ + 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + 0.0, 2.0, 0.0, + 1.0, 0.0, 0.0, + 1.0, 1.0, 1.0, + 1.0, 2.0, 0.0, + 2.0, 0.0, 0.0, + 2.0, 1.0, 0.0, + 2.0, 2.0, 0.0, + ]); + + // 0 1 2 + // 3 4 5 + // 6 7 8 + + #[rustfmt::skip] + let ib = [ + 0, 1, 3, + 3, 1, 4, + 1, 2, 4, + 4, 2, 5, + 3, 4, 6, + 6, 4, 7, + 4, 5, 7, + 7, 5, 8, + ]; + + let mut error = 0.0; + let mut actual = vec![0u32; ib.len()]; + + assert_eq!( + simplify::<_>( + &mut actual, + &ib, + &vb, + 18, + 2.0, + SimplificationOptions::SimplifyLockBorder | SimplificationOptions::SimplifyErrorAbsolute, + Some(&mut error) + ), + 18 + ); + assert!((error - 0.85).abs() < 0.01); + } } diff --git a/src/spatial_order.rs b/src/spatial_order.rs index 8b9670e..8e76a12 100644 --- a/src/spatial_order.rs +++ b/src/spatial_order.rs @@ -20,7 +20,7 @@ fn compute_order(result: &mut [u32], vertices: &[V]) where V: Vertex, { - let (minv, extent) = calc_pos_extents(vertices); + let (minv, extent) = calc_pos_extents(vertices.iter().map(|v| v.pos())); let scale = zero_inverse(extent); diff --git a/src/vertex/cache.rs b/src/vertex/cache.rs index 45a651e..8b8aec6 100644 --- a/src/vertex/cache.rs +++ b/src/vertex/cache.rs @@ -228,9 +228,6 @@ fn optimize_vertex_cache_table( let mut adjacency = TriangleAdjacency::default(); build_triangle_adjacency(&mut adjacency, indices, vertex_count); - // live triangle counts - let mut live_triangles = adjacency.counts.clone(); - // emitted flags let mut emitted_flags = vec![false; face_count]; @@ -238,7 +235,7 @@ fn optimize_vertex_cache_table( let mut vertex_scores = vec![0.0; vertex_count]; for (i, s) in vertex_scores.iter_mut().enumerate() { - *s = vertex_score(table, -1, live_triangles[i] as usize); + *s = vertex_score(table, -1, adjacency.counts[i] as usize); } // compute triangle scores @@ -290,13 +287,9 @@ fn optimize_vertex_cache_table( std::mem::swap(&mut cache, &mut cache_new); cache_count = cache_write.min(cache_size); - // update live triangle counts - for e in abc { - live_triangles[*e as usize] -= 1; - } - // remove emitted triangle from adjacency data // this makes sure that we spend less time traversing these lists on subsequent iterations + // live triangle counts (=adjacency.counts) are updated as a byproduct of these adjustments for index in abc { let index = *index as usize; @@ -327,7 +320,7 @@ fn optimize_vertex_cache_table( let cache_position = if i >= cache_size { -1 } else { i as i32 }; // update vertex score - let score = vertex_score(table, cache_position, live_triangles[index] as usize); + let score = vertex_score(table, cache_position, adjacency.counts[index] as usize); let score_diff = score - vertex_scores[index]; vertex_scores[index] = score; diff --git a/src/vertex/mod.rs b/src/vertex/mod.rs index 5f410ce..3118df0 100644 --- a/src/vertex/mod.rs +++ b/src/vertex/mod.rs @@ -48,16 +48,11 @@ impl Vertex for [f32; 3] { } } -pub(crate) fn calc_pos_extents(vertices: &[V]) -> ([f32; 3], f32) -where - V: Vertex, -{ +pub(crate) fn calc_pos_extents(vertices: impl Iterator) -> ([f32; 3], f32) { let mut minv = [f32::MAX; 3]; let mut maxv = [-f32::MAX; 3]; - for vertex in vertices { - let v = vertex.pos(); - + for v in vertices { for j in 0..3 { minv[j] = minv[j].min(v[j]); maxv[j] = maxv[j].max(v[j]);