Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
name: continuous-integration

on: [push, pull_request]
on:
push:
branches:
- 'master'
pull_request:

jobs:
build_and_test:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
105 changes: 102 additions & 3 deletions examples/demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 };

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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);
Expand All @@ -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<dyn std::error::Error>> {
Expand Down
96 changes: 95 additions & 1 deletion src/cluster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<V>(
Expand Down Expand Up @@ -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::*;
Expand Down
7 changes: 4 additions & 3 deletions src/overdraw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down
Loading