use crate::mesh::nanite::{ClusterBounds, NormalCone};
use crate::mesh::Mesh;
use glam::Vec3;
#[derive(Debug, Clone)]
pub struct Meshlet {
pub vertices: Vec<u32>,
pub triangles: Vec<u8>,
pub bounds: ClusterBounds,
pub normal_cone: NormalCone,
}
impl Meshlet {
#[must_use]
pub fn triangle_count(&self) -> usize {
self.triangles.len() / 3
}
#[must_use]
pub fn vertex_count(&self) -> usize {
self.vertices.len()
}
}
#[derive(Debug, Clone, Copy)]
pub struct MeshletConfig {
pub max_vertices: usize,
pub max_triangles: usize,
pub cone_weight: f32,
pub adjacency_grow: bool,
}
impl Default for MeshletConfig {
fn default() -> Self {
Self {
max_vertices: 64,
max_triangles: 124,
cone_weight: 0.0,
adjacency_grow: false,
}
}
}
impl MeshletConfig {
#[must_use]
pub const fn quality() -> Self {
Self {
max_vertices: 64,
max_triangles: 124,
cone_weight: 0.25,
adjacency_grow: true,
}
}
}
#[must_use]
pub fn build_meshlets(mesh: &Mesh, config: &MeshletConfig) -> Vec<Meshlet> {
if config.adjacency_grow {
build_meshlets_adjacency(mesh, config)
} else {
build_meshlets_scan(mesh, config)
}
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub fn build_meshlets_scan(mesh: &Mesh, config: &MeshletConfig) -> Vec<Meshlet> {
let tri_count = mesh.indices.len() / 3;
if tri_count == 0
|| config.max_vertices < 3
|| config.max_triangles == 0
|| config.max_vertices > 255
{
return Vec::new();
}
let mut meshlets: Vec<Meshlet> = Vec::new();
let mut current_vertices: Vec<u32> = Vec::with_capacity(config.max_vertices);
let mut current_triangles: Vec<u8> = Vec::with_capacity(config.max_triangles * 3);
for t in 0..tri_count {
let base = t * 3;
let ia = mesh.indices[base];
let ib = mesh.indices[base + 1];
let ic = mesh.indices[base + 2];
let mut new_verts_needed = 0;
for &v in &[ia, ib, ic] {
if !current_vertices.contains(&v) {
new_verts_needed += 1;
}
}
let would_exceed_verts = current_vertices.len() + new_verts_needed > config.max_vertices;
let would_exceed_tris = current_triangles.len() / 3 + 1 > config.max_triangles;
if would_exceed_verts || would_exceed_tris {
if !current_triangles.is_empty() {
meshlets.push(finalize_meshlet(
¤t_vertices,
¤t_triangles,
mesh,
));
current_vertices.clear();
current_triangles.clear();
}
}
for &v in &[ia, ib, ic] {
let local_idx = if let Some(pos) = current_vertices.iter().position(|&x| x == v) {
pos
} else {
current_vertices.push(v);
current_vertices.len() - 1
};
current_triangles.push(local_idx as u8);
}
}
if !current_triangles.is_empty() {
meshlets.push(finalize_meshlet(
¤t_vertices,
¤t_triangles,
mesh,
));
}
meshlets
}
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::too_many_lines)]
pub fn build_meshlets_adjacency(mesh: &Mesh, config: &MeshletConfig) -> Vec<Meshlet> {
let tri_count = mesh.indices.len() / 3;
if tri_count == 0
|| config.max_vertices < 3
|| config.max_triangles == 0
|| config.max_vertices > 255
{
return Vec::new();
}
let face_normals = precompute_face_normals(mesh);
let vertex_to_tris = precompute_vertex_to_tris(mesh);
let mut meshlets: Vec<Meshlet> = Vec::new();
let mut processed = vec![false; tri_count];
let mut next_seed = 0usize;
loop {
while next_seed < tri_count && processed[next_seed] {
next_seed += 1;
}
if next_seed >= tri_count {
break;
}
let seed = next_seed;
processed[seed] = true;
let mut current_vertices: Vec<u32> = Vec::with_capacity(config.max_vertices);
let mut current_triangles: Vec<u8> = Vec::with_capacity(config.max_triangles * 3);
let mut normal_sum = Vec3::ZERO;
add_triangle_to_meshlet(&mut current_vertices, &mut current_triangles, mesh, seed);
if let Some(&n) = face_normals.get(seed) {
normal_sum += n;
}
loop {
let mut best_score: f32 = f32::NEG_INFINITY;
let mut best_tri: Option<usize> = None;
let mut best_reuse: usize = 0;
let avg_normal = if normal_sum.length_squared() > 1e-12 {
normal_sum.normalize()
} else {
Vec3::ZERO
};
for &vi in ¤t_vertices {
let adj_list = match vertex_to_tris.get(vi as usize) {
Some(list) => list,
None => continue,
};
for &cand_t in adj_list {
let cand_t_usize = cand_t as usize;
if processed[cand_t_usize] {
continue;
}
let base = cand_t_usize * 3;
let ia = mesh.indices[base];
let ib = mesh.indices[base + 1];
let ic = mesh.indices[base + 2];
let mut reuse = 0;
for &v in &[ia, ib, ic] {
if current_vertices.contains(&v) {
reuse += 1;
}
}
let new_verts = 3 - reuse;
if current_vertices.len() + new_verts > config.max_vertices {
continue;
}
if current_triangles.len() / 3 + 1 > config.max_triangles {
continue;
}
let cand_normal = face_normals.get(cand_t_usize).copied().unwrap_or(Vec3::Y);
let cone_term = if config.cone_weight > 0.0 && avg_normal != Vec3::ZERO {
config.cone_weight * cand_normal.dot(avg_normal)
} else {
0.0
};
let score = reuse as f32 + cone_term;
if score > best_score {
best_score = score;
best_tri = Some(cand_t_usize);
best_reuse = reuse;
}
}
}
let _ = best_reuse;
match best_tri {
Some(t) => {
processed[t] = true;
add_triangle_to_meshlet(&mut current_vertices, &mut current_triangles, mesh, t);
if let Some(&n) = face_normals.get(t) {
normal_sum += n;
}
}
None => break, }
}
if !current_triangles.is_empty() {
meshlets.push(finalize_meshlet(
¤t_vertices,
¤t_triangles,
mesh,
));
}
}
meshlets
}
#[allow(clippy::cast_possible_truncation)]
fn add_triangle_to_meshlet(
current_vertices: &mut Vec<u32>,
current_triangles: &mut Vec<u8>,
mesh: &Mesh,
t: usize,
) {
let base = t * 3;
for k in 0..3 {
let v = mesh.indices[base + k];
let local_idx = if let Some(pos) = current_vertices.iter().position(|&x| x == v) {
pos
} else {
current_vertices.push(v);
current_vertices.len() - 1
};
current_triangles.push(local_idx as u8);
}
}
fn precompute_face_normals(mesh: &Mesh) -> Vec<Vec3> {
let tri_count = mesh.indices.len() / 3;
let mut normals = Vec::with_capacity(tri_count);
for t in 0..tri_count {
let base = t * 3;
let ia = mesh.indices[base] as usize;
let ib = mesh.indices[base + 1] as usize;
let ic = mesh.indices[base + 2] as usize;
if ia >= mesh.vertices.len() || ib >= mesh.vertices.len() || ic >= mesh.vertices.len() {
normals.push(Vec3::ZERO);
continue;
}
let a = mesh.vertices[ia].position;
let b = mesh.vertices[ib].position;
let c = mesh.vertices[ic].position;
let n = (b - a).cross(c - a);
if n.length_squared() > 1e-12 {
normals.push(n.normalize());
} else {
normals.push(Vec3::ZERO);
}
}
normals
}
fn precompute_vertex_to_tris(mesh: &Mesh) -> Vec<Vec<u32>> {
let vert_count = mesh.vertices.len();
let tri_count = mesh.indices.len() / 3;
let mut vtot: Vec<Vec<u32>> = vec![Vec::new(); vert_count];
for t in 0..tri_count {
let base = t * 3;
for k in 0..3 {
let v = mesh.indices[base + k] as usize;
if v < vert_count {
vtot[v].push(t as u32);
}
}
}
vtot
}
fn finalize_meshlet(vertices: &[u32], triangles: &[u8], mesh: &Mesh) -> Meshlet {
let positions: Vec<Vec3> = vertices
.iter()
.filter_map(|&i| mesh.vertices.get(i as usize).map(|v| v.position))
.collect();
let tri_count = triangles.len() / 3;
let mut face_normals: Vec<Vec3> = Vec::with_capacity(tri_count);
for t in 0..tri_count {
let base = t * 3;
let a_local = triangles[base] as usize;
let b_local = triangles[base + 1] as usize;
let c_local = triangles[base + 2] as usize;
if a_local >= positions.len() || b_local >= positions.len() || c_local >= positions.len() {
continue;
}
let a = positions[a_local];
let b = positions[b_local];
let c = positions[c_local];
let n = (b - a).cross(c - a);
if n.length_squared() > 1e-12 {
face_normals.push(n.normalize());
}
}
let bounds = ClusterBounds::from_vertices(&positions);
let normal_cone = NormalCone::from_normals(&face_normals);
Meshlet {
vertices: vertices.to_vec(),
triangles: triangles.to_vec(),
bounds,
normal_cone,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mesh::{sdf_to_mesh, MarchingCubesConfig, Vertex};
use crate::types::SdfNode;
fn make_sphere_mesh(resolution: usize) -> Mesh {
let sphere = SdfNode::sphere(1.0);
sdf_to_mesh(
&sphere,
Vec3::splat(-2.0),
Vec3::splat(2.0),
&MarchingCubesConfig {
resolution,
iso_level: 0.0,
compute_normals: true,
..Default::default()
},
)
}
#[test]
fn test_empty_mesh_returns_empty() {
let mesh = Mesh::new();
let meshlets = build_meshlets(&mesh, &MeshletConfig::default());
assert!(meshlets.is_empty());
}
#[test]
fn test_small_mesh_single_meshlet() {
let mesh = Mesh {
vertices: vec![
Vertex::new(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
Vertex::new(Vec3::new(1.0, 0.0, 0.0), Vec3::Y),
Vertex::new(Vec3::new(0.0, 0.0, 1.0), Vec3::Y),
],
indices: vec![0, 1, 2],
};
let meshlets = build_meshlets(&mesh, &MeshletConfig::default());
assert_eq!(meshlets.len(), 1);
assert_eq!(meshlets[0].vertex_count(), 3);
assert_eq!(meshlets[0].triangle_count(), 1);
assert_eq!(meshlets[0].triangles, vec![0, 1, 2]);
}
#[test]
fn test_sphere_mesh_multiple_meshlets() {
let mesh = make_sphere_mesh(16);
let config = MeshletConfig::default();
let meshlets = build_meshlets(&mesh, &config);
assert!(
!meshlets.is_empty(),
"expected at least 1 meshlet, got 0 (mesh has {} tris)",
mesh.triangle_count()
);
for m in &meshlets {
assert!(
m.vertex_count() <= config.max_vertices,
"meshlet exceeded max_vertices: {} > {}",
m.vertex_count(),
config.max_vertices
);
assert!(
m.triangle_count() <= config.max_triangles,
"meshlet exceeded max_triangles: {} > {}",
m.triangle_count(),
config.max_triangles
);
}
}
#[test]
fn test_local_indices_within_range() {
let mesh = make_sphere_mesh(8);
let meshlets = build_meshlets(&mesh, &MeshletConfig::default());
for m in &meshlets {
let n = m.vertex_count() as u8;
for &idx in &m.triangles {
assert!(
idx < n,
"local index {} out of range (vertex_count={})",
idx,
n
);
}
}
}
#[test]
fn test_triangle_count_conservation() {
let mesh = make_sphere_mesh(16);
let orig_tris = mesh.triangle_count();
let meshlets = build_meshlets(&mesh, &MeshletConfig::default());
let total: usize = meshlets.iter().map(|m| m.triangle_count()).sum();
assert_eq!(
total, orig_tris,
"meshlet triangle count sum mismatch: {} vs {}",
total, orig_tris
);
}
#[test]
fn test_smaller_max_increases_meshlet_count() {
let mesh = make_sphere_mesh(16);
let default_meshlets = build_meshlets(&mesh, &MeshletConfig::default());
let small_config = MeshletConfig {
max_vertices: 32,
max_triangles: 60,
cone_weight: 0.0,
adjacency_grow: false,
};
let small_meshlets = build_meshlets(&mesh, &small_config);
assert!(
small_meshlets.len() > default_meshlets.len(),
"smaller config should produce more meshlets: {} vs {}",
small_meshlets.len(),
default_meshlets.len()
);
}
#[test]
fn test_bounds_and_normal_cone_populated() {
let mesh = make_sphere_mesh(8);
let meshlets = build_meshlets(&mesh, &MeshletConfig::default());
for m in &meshlets {
assert!(m.bounds.radius >= 0.0);
assert!((m.normal_cone.axis.length() - 1.0).abs() < 1e-4);
assert!(m.normal_cone.cutoff_cos >= -1.0 && m.normal_cone.cutoff_cos <= 1.0);
}
}
#[test]
fn test_invalid_config_returns_empty() {
let mesh = make_sphere_mesh(4);
let m0 = build_meshlets(
&mesh,
&MeshletConfig {
max_vertices: 0,
max_triangles: 64,
cone_weight: 0.0,
adjacency_grow: false,
},
);
assert!(m0.is_empty());
let m1 = build_meshlets(
&mesh,
&MeshletConfig {
max_vertices: 64,
max_triangles: 0,
cone_weight: 0.0,
adjacency_grow: false,
},
);
assert!(m1.is_empty());
let m2 = build_meshlets(
&mesh,
&MeshletConfig {
max_vertices: 256,
max_triangles: 64,
cone_weight: 0.0,
adjacency_grow: false,
},
);
assert!(m2.is_empty());
}
#[test]
fn test_default_config_values() {
let c = MeshletConfig::default();
assert_eq!(c.max_vertices, 64);
assert_eq!(c.max_triangles, 124);
assert!((c.cone_weight - 0.0).abs() < 1e-6);
assert!(!c.adjacency_grow);
}
#[test]
fn test_quality_config_is_v2() {
let c = MeshletConfig::quality();
assert!(c.adjacency_grow);
assert!(c.cone_weight > 0.0);
}
#[test]
fn test_v2_empty_mesh_returns_empty() {
let mesh = Mesh::new();
let meshlets = build_meshlets(&mesh, &MeshletConfig::quality());
assert!(meshlets.is_empty());
}
#[test]
fn test_v2_single_triangle_single_meshlet() {
let mesh = Mesh {
vertices: vec![
Vertex::new(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
Vertex::new(Vec3::new(1.0, 0.0, 0.0), Vec3::Y),
Vertex::new(Vec3::new(0.0, 0.0, 1.0), Vec3::Y),
],
indices: vec![0, 1, 2],
};
let meshlets = build_meshlets(&mesh, &MeshletConfig::quality());
assert_eq!(meshlets.len(), 1);
assert_eq!(meshlets[0].vertex_count(), 3);
assert_eq!(meshlets[0].triangle_count(), 1);
}
#[test]
fn test_v2_sphere_conservation() {
let mesh = make_sphere_mesh(16);
let orig_tris = mesh.triangle_count();
let meshlets = build_meshlets(&mesh, &MeshletConfig::quality());
let total: usize = meshlets.iter().map(|m| m.triangle_count()).sum();
assert_eq!(
total, orig_tris,
"V2 triangle count mismatch: {} vs {}",
total, orig_tris
);
}
#[test]
fn test_v2_constraints_respected() {
let mesh = make_sphere_mesh(16);
let config = MeshletConfig::quality();
let meshlets = build_meshlets(&mesh, &config);
for m in &meshlets {
assert!(m.vertex_count() <= config.max_vertices);
assert!(m.triangle_count() <= config.max_triangles);
let n = m.vertex_count() as u8;
for &idx in &m.triangles {
assert!(idx < n);
}
}
}
#[test]
fn test_v2_normal_cone_tighter_with_cone_weight() {
let mesh = make_sphere_mesh(16);
let v1_config = MeshletConfig {
adjacency_grow: false,
..MeshletConfig::default()
};
let v2_config = MeshletConfig {
adjacency_grow: true,
cone_weight: 0.5,
..MeshletConfig::default()
};
let v1_meshlets = build_meshlets(&mesh, &v1_config);
let v2_meshlets = build_meshlets(&mesh, &v2_config);
let avg_cutoff = |ms: &[Meshlet]| -> f32 {
if ms.is_empty() {
return 0.0;
}
let sum: f32 = ms.iter().map(|m| m.normal_cone.cutoff_cos).sum();
sum / (ms.len() as f32)
};
let v1_avg = avg_cutoff(&v1_meshlets);
let v2_avg = avg_cutoff(&v2_meshlets);
assert!(
v2_avg >= v1_avg - 0.15,
"V2 avg cutoff_cos should be >= V1 (with tolerance): V1={}, V2={}",
v1_avg,
v2_avg
);
}
#[test]
fn test_v2_bounds_populated() {
let mesh = make_sphere_mesh(8);
let meshlets = build_meshlets(&mesh, &MeshletConfig::quality());
for m in &meshlets {
assert!(m.bounds.radius >= 0.0);
assert!((m.normal_cone.axis.length() - 1.0).abs() < 1e-4);
}
}
#[test]
fn test_v2_invalid_config_returns_empty() {
let mesh = make_sphere_mesh(4);
let m = build_meshlets(
&mesh,
&MeshletConfig {
max_vertices: 0,
max_triangles: 64,
cone_weight: 0.5,
adjacency_grow: true,
},
);
assert!(m.is_empty());
}
#[test]
fn test_v2_vs_v1_both_produce_valid_meshlets() {
let mesh = make_sphere_mesh(12);
let v1 = build_meshlets(&mesh, &MeshletConfig::default());
let v2 = build_meshlets(&mesh, &MeshletConfig::quality());
assert!(!v1.is_empty());
assert!(!v2.is_empty());
let v1_total: usize = v1.iter().map(|m| m.triangle_count()).sum();
let v2_total: usize = v2.iter().map(|m| m.triangle_count()).sum();
assert_eq!(v1_total, mesh.triangle_count());
assert_eq!(v2_total, mesh.triangle_count());
}
#[test]
fn test_v2_scan_helper_direct() {
let mesh = make_sphere_mesh(8);
let config = MeshletConfig::default();
let scan_result = build_meshlets_scan(&mesh, &config);
let adj_config = MeshletConfig {
adjacency_grow: true,
..config
};
let adj_result = build_meshlets_adjacency(&mesh, &adj_config);
assert!(!scan_result.is_empty());
assert!(!adj_result.is_empty());
}
}