use crate::compiled::real::law_max_f32;
use crate::compiled::real::LawMinMax;
use crate::eval::{eval, eval_material, eval_normal};
use crate::mesh::dual_contouring::{dual_contouring, DualContouringConfig};
use crate::mesh::{sdf_to_mesh, MarchingCubesConfig, Mesh, Triangle, Vertex};
use crate::tight_aabb::compute_tight_aabb;
use crate::types::SdfNode;
use glam::Vec3;
pub const CLUSTER_MAX_TRIANGLES: usize = 128;
pub const CLUSTER_MAX_VERTICES: usize = 256;
#[derive(Debug, Clone, Copy)]
pub struct ClusterBounds {
pub center: Vec3,
pub radius: f32,
pub aabb_min: Vec3,
pub aabb_max: Vec3,
}
impl ClusterBounds {
pub fn from_vertices(vertices: &[Vec3]) -> Self {
if vertices.is_empty() {
return Self {
center: Vec3::ZERO,
radius: 0.0,
aabb_min: Vec3::ZERO,
aabb_max: Vec3::ZERO,
};
}
let mut aabb_min = Vec3::splat(f32::INFINITY);
let mut aabb_max = Vec3::splat(f32::NEG_INFINITY);
for &v in vertices {
aabb_min = aabb_min.min(v);
aabb_max = aabb_max.max(v);
}
let center = (aabb_min + aabb_max) * 0.5;
let radius = vertices
.iter()
.map(|&v| (v - center).length())
.fold(0.0f32, law_max_f32);
Self {
center,
radius,
aabb_min,
aabb_max,
}
}
#[inline]
pub fn is_visible(&self, view_pos: Vec3, view_dir: Vec3, fov_cos: f32) -> bool {
let to_center = self.center - view_pos;
let dist = to_center.length();
if dist <= self.radius {
return true; }
let dir = to_center / dist;
let sin_a = self.radius / dist;
let cos_a = (dist * dist + (-(self.radius * self.radius))).sqrt() / dist;
let fov_cos = fov_cos.clamp(-1.0, 1.0);
let fov_sin = (fov_cos * -fov_cos + 1.0).law_max(0.0).sqrt();
if fov_cos <= 0.0 && sin_a >= fov_sin {
return true;
}
dir.dot(view_dir) >= (fov_cos * cos_a + (-(fov_sin * sin_a)))
}
#[inline]
pub fn screen_error(&self, view_pos: Vec3, geometric_error: f32, screen_height: f32) -> f32 {
let dist = (self.center - view_pos).length().law_max(self.radius);
(geometric_error / dist) * screen_height
}
}
#[derive(Debug, Clone, Copy)]
pub struct NormalCone {
pub axis: Vec3,
pub cutoff_cos: f32,
pub apex: Option<Vec3>,
}
impl NormalCone {
#[must_use]
pub fn from_normals(normals: &[Vec3]) -> Self {
if normals.is_empty() {
return Self::unbounded();
}
let mut sum = Vec3::ZERO;
for &n in normals {
sum += n;
}
let sum_len = sum.length();
if sum_len < 1e-6 {
return Self::unbounded();
}
let axis = sum / sum_len;
let mut min_dot = 1.0_f32;
for &n in normals {
let d = axis.dot(n);
if d < min_dot {
min_dot = d;
}
}
Self {
axis,
cutoff_cos: min_dot,
apex: None,
}
}
#[must_use]
pub fn from_normals_and_positions(normals: &[Vec3], face_centers: &[Vec3]) -> Self {
let basic = Self::from_normals(normals);
if basic.cutoff_cos <= -1.0 + 1e-6 || normals.len() != face_centers.len() {
return basic;
}
let mut center_sum = Vec3::ZERO;
for &c in face_centers {
center_sum += c;
}
let cluster_center = center_sum / (face_centers.len() as f32);
let mut max_t = 0.0_f32;
for i in 0..normals.len() {
let n = normals[i];
let c = face_centers[i];
let denom = basic.axis.dot(n);
if denom <= 1e-6 {
continue; }
let numer = (cluster_center - c).dot(n);
let t = numer / denom;
if t > max_t {
max_t = t;
}
}
let apex = cluster_center - basic.axis * max_t;
Self {
axis: basic.axis,
cutoff_cos: basic.cutoff_cos,
apex: Some(apex),
}
}
#[must_use]
#[inline]
pub const fn unbounded() -> Self {
Self {
axis: Vec3::Y,
cutoff_cos: -1.0,
apex: None,
}
}
#[must_use]
#[inline]
pub fn is_backface_culled(&self, view_dir: Vec3) -> bool {
if self.cutoff_cos < 0.0 {
return false; }
let c = self.cutoff_cos.law_min(1.0);
let sin_beta = (c * -c + 1.0).law_max(0.0).sqrt();
self.axis.dot(view_dir) >= sin_beta
}
}
#[derive(Debug, Clone, Copy)]
pub struct LodLevel {
pub level: u32,
pub resolution: u32,
pub max_error: f32,
pub triangle_count: u32,
}
#[derive(Debug, Clone)]
pub struct NaniteCluster {
pub id: u32,
pub lod_level: u32,
pub vertices: Vec<Vertex>,
pub triangles: Vec<Triangle>,
pub bounds: ClusterBounds,
pub parent_ids: Vec<u32>,
pub child_ids: Vec<u32>,
pub geometric_error: f32,
pub lod_bounds: ClusterBounds,
pub parent_error: f32,
pub parent_lod_bounds: ClusterBounds,
pub material_id: u32,
}
impl NaniteCluster {
#[inline]
pub fn triangle_count(&self) -> usize {
self.triangles.len()
}
#[inline]
pub fn vertex_count(&self) -> usize {
self.vertices.len()
}
pub fn should_render(&self, view_pos: Vec3, error_threshold: f32) -> bool {
let fine = self.child_ids.is_empty()
|| projected_error(self.geometric_error, &self.lod_bounds, view_pos) <= error_threshold;
let parent_fine = self.parent_error != f32::INFINITY
&& projected_error(self.parent_error, &self.parent_lod_bounds, view_pos)
<= error_threshold;
fine && !parent_fine
}
}
#[inline]
fn projected_error(error: f32, bounds: &ClusterBounds, view_pos: Vec3) -> f32 {
if error <= 0.0 {
return 0.0;
}
let d = (bounds.center - view_pos).length() - bounds.radius;
if d <= 0.0 {
f32::INFINITY
} else {
error / d
}
}
#[derive(Debug, Clone)]
pub struct ClusterGroup {
pub id: u32,
pub lod_level: u32,
pub cluster_ids: Vec<u32>,
pub bounds: ClusterBounds,
pub max_error: f32,
}
#[derive(Debug, Clone)]
pub struct NaniteConfig {
pub lod_levels: u32,
pub base_resolution: u32,
pub lod_factor: f32,
pub max_triangles_per_cluster: usize,
pub cluster_overlap: f32,
pub compute_normals: bool,
pub use_dual_contouring: bool,
pub use_tight_aabb: bool,
pub curvature_adaptive: bool,
}
impl Default for NaniteConfig {
fn default() -> Self {
Self {
lod_levels: 6,
base_resolution: 128,
lod_factor: 0.5,
max_triangles_per_cluster: CLUSTER_MAX_TRIANGLES,
cluster_overlap: 0.1,
compute_normals: true,
use_dual_contouring: false,
use_tight_aabb: true,
curvature_adaptive: false,
}
}
}
impl NaniteConfig {
pub fn high_detail() -> Self {
Self {
lod_levels: 8,
base_resolution: 256,
lod_factor: 0.5,
..Default::default()
}
}
pub fn medium_detail() -> Self {
Self {
lod_levels: 5,
base_resolution: 64,
lod_factor: 0.5,
..Default::default()
}
}
pub fn preview() -> Self {
Self {
lod_levels: 3,
base_resolution: 32,
lod_factor: 0.5,
..Default::default()
}
}
}
#[derive(Debug)]
pub struct NaniteMesh {
pub clusters: Vec<NaniteCluster>,
pub groups: Vec<ClusterGroup>,
pub lod_levels: Vec<LodLevel>,
pub bounds: ClusterBounds,
pub total_triangles: usize,
}
impl NaniteMesh {
pub fn clusters_at_lod(&self, level: u32) -> Vec<&NaniteCluster> {
self.clusters
.iter()
.filter(|c| c.lod_level == level)
.collect()
}
pub fn get_cluster(&self, id: u32) -> Option<&NaniteCluster> {
self.clusters.iter().find(|c| c.id == id)
}
pub fn select_clusters(&self, view_pos: Vec3, error_threshold: f32) -> Vec<u32> {
use std::collections::HashMap;
let mut group_of: HashMap<u32, usize> = HashMap::with_capacity(self.clusters.len());
for (gi, g) in self.groups.iter().enumerate() {
for &id in &g.cluster_ids {
group_of.insert(id, gi);
}
}
let first_cluster =
|g: &ClusterGroup| g.cluster_ids.first().and_then(|&id| self.get_cluster(id));
let fine_enough = |g: &ClusterGroup| {
let leaf = first_cluster(g).is_none_or(|c| c.child_ids.is_empty());
leaf || projected_error(g.max_error, &g.bounds, view_pos) <= error_threshold
};
let mut selected = vec![false; self.groups.len()];
for (gi, g) in self.groups.iter().enumerate() {
if !fine_enough(g) {
continue;
}
let parent = first_cluster(g)
.and_then(|c| c.parent_ids.first())
.and_then(|pid| group_of.get(pid))
.map(|&pi| &self.groups[pi]);
if parent.is_none_or(|p| !fine_enough(p)) {
selected[gi] = true;
}
}
self.clusters
.iter()
.filter(|c| group_of.get(&c.id).is_some_and(|&gi| selected[gi]))
.map(|c| c.id)
.collect()
}
pub fn total_vertices(&self) -> usize {
self.clusters.iter().map(|c| c.vertex_count()).sum()
}
pub fn to_mesh(&self, lod_level: u32) -> Mesh {
let clusters: Vec<_> = self.clusters_at_lod(lod_level);
let mut mesh = Mesh::new();
let mut vertex_offset = 0u32;
for cluster in clusters {
mesh.vertices.extend_from_slice(&cluster.vertices);
for tri in &cluster.triangles {
mesh.indices.push(tri.a + vertex_offset);
mesh.indices.push(tri.b + vertex_offset);
mesh.indices.push(tri.c + vertex_offset);
}
vertex_offset += cluster.vertices.len() as u32;
}
mesh
}
}
const REGION_VOXELS: u32 = 8;
fn region_depth(resolution: u32, coarser_limit: u32) -> u32 {
let d = alice_det_math::log2(resolution.max(1) as f32 / REGION_VOXELS as f32)
.round()
.clamp(0.0, 16.0) as u32;
d.min(coarser_limit)
}
fn triangle_cell(a: Vec3, b: Vec3, c: Vec3, origin: Vec3, side: f32, depth: u32) -> [u32; 3] {
let n = 1u32 << depth;
let scale = n as f32 / side;
let rel = ((a + b + c) / 3.0 - origin) * scale;
let idx = |x: f32| (x.floor().law_max(0.0) as u32).min(n - 1);
[idx(rel.x), idx(rel.y), idx(rel.z)]
}
pub fn generate_nanite_mesh(
sdf: &SdfNode,
min_bounds: Vec3,
max_bounds: Vec3,
config: &NaniteConfig,
) -> NaniteMesh {
let bounds_center = (min_bounds + max_bounds) * 0.5;
let (min_bounds, max_bounds) = if config.use_tight_aabb {
let tight = compute_tight_aabb(sdf);
let tight_min = tight.min.max(min_bounds);
let tight_max = tight.max.min(max_bounds);
if tight_min.x < tight_max.x && tight_min.y < tight_max.y && tight_min.z < tight_max.z {
(tight_min, tight_max)
} else {
(min_bounds, max_bounds)
}
} else {
(min_bounds, max_bounds)
};
let origin = min_bounds;
let side = (max_bounds - min_bounds)
.max_element()
.law_max(f32::MIN_POSITIVE);
let max_tris = config.max_triangles_per_cluster.max(1);
let mut all_clusters: Vec<NaniteCluster> = Vec::new();
let mut regions: Vec<Region> = Vec::new();
let mut lod_infos = Vec::new();
let mut cluster_id = 0u32;
let mut depth_limit = u32::MAX;
for lod in 0..config.lod_levels {
let resolution = (config.base_resolution as f32
* alice_det_math::powi(config.lod_factor, lod as i32)) as u32;
let resolution = resolution.max(4);
let mesh = if config.use_dual_contouring {
let dc_config = DualContouringConfig {
resolution: resolution as usize,
compute_normals: config.compute_normals,
..Default::default()
};
dual_contouring(sdf, min_bounds, max_bounds, &dc_config)
} else {
let mc_config = MarchingCubesConfig {
resolution: resolution as usize,
iso_level: 0.0,
compute_normals: config.compute_normals,
..Default::default()
};
sdf_to_mesh(sdf, min_bounds, max_bounds, &mc_config)
};
if mesh.triangle_count() == 0 {
continue;
}
let depth = region_depth(resolution, depth_limit);
depth_limit = depth;
let mut cells: std::collections::BTreeMap<[u32; 3], Vec<usize>> =
std::collections::BTreeMap::new();
for t in 0..mesh.triangle_count() {
let [a, b, c] = triangle_positions(&mesh, t);
cells
.entry(triangle_cell(a, b, c, origin, side, depth))
.or_default()
.push(t);
}
let tri_errors = sampled_triangle_errors(sdf, &mesh);
let mut level_max = 0.0f32;
let mut level_tris = 0u32;
for (cell, tris) in cells {
let mut chunks = pack_clusters(&mesh, &tris, max_tris);
if config.curvature_adaptive && lod == 0 {
chunks = split_high_curvature(chunks, &mesh, sdf, max_tris);
}
let mut ids = Vec::with_capacity(chunks.len());
let mut region_error = 0.0f32;
for chunk in chunks {
let (vertices, triangles) = extract_cluster_geometry(&mesh, &chunk);
let positions: Vec<Vec3> = vertices.iter().map(|v| v.position).collect();
let bounds = ClusterBounds::from_vertices(&positions);
let error = cluster_surface_error(sdf, &mesh, &chunk, &tri_errors);
region_error = region_error.law_max(error);
level_tris += triangles.len() as u32;
ids.push(cluster_id);
all_clusters.push(NaniteCluster {
id: cluster_id,
lod_level: lod,
vertices,
triangles,
bounds,
parent_ids: Vec::new(),
child_ids: Vec::new(),
geometric_error: error,
lod_bounds: bounds,
parent_error: f32::INFINITY,
parent_lod_bounds: bounds,
material_id: eval_material(sdf, bounds.center),
});
cluster_id += 1;
}
level_max = level_max.law_max(region_error);
regions.push(Region {
lod,
depth,
cell,
cluster_ids: ids,
error: region_error,
parent: None,
});
}
lod_infos.push(LodLevel {
level: lod,
resolution,
max_error: level_max,
triangle_count: level_tris,
});
}
let groups = build_region_tree(&mut regions, &mut all_clusters);
for info in &mut lod_infos {
info.max_error = groups
.iter()
.filter(|g| g.lod_level == info.level)
.map(|g| g.max_error)
.fold(0.0f32, law_max_f32);
}
let global_bounds = if !all_clusters.is_empty() {
let all_vertices: Vec<Vec3> = all_clusters
.iter()
.flat_map(|c| c.vertices.iter().map(|v| v.position))
.collect();
ClusterBounds::from_vertices(&all_vertices)
} else {
ClusterBounds::from_vertices(&[bounds_center])
};
let total_triangles = all_clusters
.iter()
.filter(|c| c.lod_level == 0)
.map(|c| c.triangle_count())
.sum();
NaniteMesh {
clusters: all_clusters,
groups,
lod_levels: lod_infos,
bounds: global_bounds,
total_triangles,
}
}
struct Region {
lod: u32,
depth: u32,
cell: [u32; 3],
cluster_ids: Vec<u32>,
error: f32,
parent: Option<usize>,
}
fn build_region_tree(regions: &mut [Region], clusters: &mut [NaniteCluster]) -> Vec<ClusterGroup> {
use std::collections::HashMap;
let mut by_key: HashMap<(u32, [u32; 3]), usize> = HashMap::with_capacity(regions.len());
for (i, r) in regions.iter().enumerate() {
by_key.insert((r.lod, r.cell), i);
}
let mut levels: Vec<(u32, u32)> = regions.iter().map(|r| (r.lod, r.depth)).collect();
levels.dedup();
for r in regions.iter_mut() {
let (lod, depth, cell) = (r.lod, r.depth, r.cell);
r.parent = levels
.iter()
.filter(|&&(l, _)| l > lod)
.find_map(|&(l, d)| {
let shift = depth - d;
by_key.get(&(l, cell.map(|x| x >> shift))).copied()
});
}
let mut bounds: Vec<ClusterBounds> = regions
.iter()
.map(|r| {
let pts: Vec<Vec3> = r
.cluster_ids
.iter()
.flat_map(|&id| clusters[id as usize].vertices.iter().map(|v| v.position))
.collect();
ClusterBounds::from_vertices(&pts)
})
.collect();
let mut errors: Vec<f32> = regions.iter().map(|r| r.error).collect();
for i in 0..regions.len() {
if let Some(p) = regions[i].parent {
errors[p] = errors[p].law_max(errors[i]);
let reach = (bounds[i].center - bounds[p].center).length() + bounds[i].radius;
bounds[p].radius = bounds[p].radius.law_max(reach);
}
}
let mut children: Vec<Vec<u32>> = vec![Vec::new(); regions.len()];
for r in regions.iter() {
if let Some(p) = r.parent {
children[p].extend_from_slice(&r.cluster_ids);
}
}
for (i, r) in regions.iter().enumerate() {
let parents: Vec<u32> = r
.parent
.map(|p| regions[p].cluster_ids.clone())
.unwrap_or_default();
for &id in &r.cluster_ids {
let c = &mut clusters[id as usize];
c.geometric_error = errors[i];
c.lod_bounds = bounds[i];
(c.parent_error, c.parent_lod_bounds) = match r.parent {
Some(p) => (errors[p], bounds[p]),
None => (f32::INFINITY, bounds[i]),
};
c.parent_ids.clone_from(&parents);
c.child_ids.clone_from(&children[i]);
}
}
regions
.iter()
.enumerate()
.map(|(i, r)| ClusterGroup {
id: i as u32,
lod_level: r.lod,
cluster_ids: r.cluster_ids.clone(),
bounds: bounds[i],
max_error: errors[i],
})
.collect()
}
#[inline]
fn triangle_positions(mesh: &Mesh, t: usize) -> [Vec3; 3] {
let i = &mesh.indices[t * 3..t * 3 + 3];
[
mesh.vertices[i[0] as usize].position,
mesh.vertices[i[1] as usize].position,
mesh.vertices[i[2] as usize].position,
]
}
fn pack_clusters(mesh: &Mesh, tris: &[usize], max_tris: usize) -> Vec<Vec<usize>> {
use std::collections::HashSet;
let mut chunks = Vec::new();
let mut current: Vec<usize> = Vec::new();
let mut verts: HashSet<u32> = HashSet::new();
for &t in tris {
let idx = &mesh.indices[t * 3..t * 3 + 3];
let new = idx
.iter()
.enumerate()
.filter(|&(k, i)| !verts.contains(i) && !idx[..k].contains(i))
.count();
if !current.is_empty()
&& (current.len() >= max_tris || verts.len() + new > CLUSTER_MAX_VERTICES)
{
chunks.push(std::mem::take(&mut current));
verts.clear();
}
verts.extend(idx.iter().copied());
current.push(t);
}
if !current.is_empty() {
chunks.push(current);
}
chunks
}
fn split_high_curvature(
chunks: Vec<Vec<usize>>,
mesh: &Mesh,
sdf: &SdfNode,
max_tris: usize,
) -> Vec<Vec<usize>> {
let mut out = Vec::with_capacity(chunks.len());
for chunk in chunks {
if chunk.len() <= max_tris / 2 || chunk.len() < 2 {
out.push(chunk);
continue;
}
let step = (chunk.len() / 16).max(1);
let normals: Vec<Vec3> = chunk
.iter()
.step_by(step)
.map(|&t| eval_normal(sdf, mesh.vertices[mesh.indices[t * 3] as usize].position))
.collect();
let mean = normals.iter().copied().sum::<Vec3>() / normals.len() as f32;
let variance = normals
.iter()
.map(|n| (*n - mean).length_squared())
.sum::<f32>()
/ normals.len() as f32;
if variance > 0.1 {
let (a, b) = chunk.split_at(chunk.len() / 2);
out.push(a.to_vec());
out.push(b.to_vec());
} else {
out.push(chunk);
}
}
out
}
const TRI_SAMPLES: u32 = 4;
fn sample_triangle(sdf: &SdfNode, [a, b, c]: [Vec3; 3]) -> (f32, f32, f32) {
let n = TRI_SAMPLES as f32;
let mut best = (f32::NEG_INFINITY, 0.0, 0.0);
for i in 0..=TRI_SAMPLES {
for j in 0..=(TRI_SAMPLES - i) {
let (u, v) = (i as f32 / n, j as f32 / n);
let e = eval(sdf, a + (b - a) * u + (c - a) * v).abs();
if e > best.0 {
best = (e, u, v);
}
}
}
best
}
fn refine_triangle(sdf: &SdfNode, tri: [Vec3; 3]) -> f32 {
let [a, b, c] = tri;
let (mut best, mut u, mut v) = sample_triangle(sdf, tri);
let mut h = 0.5 / TRI_SAMPLES as f32;
let dirs = [
(1.0, 0.0),
(-1.0, 0.0),
(0.0, 1.0),
(0.0, -1.0),
(1.0, -1.0),
(-1.0, 1.0),
];
let mut halvings = 0;
while halvings < 11 {
let mut moved = false;
for (du, dv) in dirs {
let (nu, nv) = (u + du * h, v + dv * h);
if nu < 0.0 || nv < 0.0 || nu + nv > 1.0 {
continue;
}
let e = eval(sdf, a + (b - a) * nu + (c - a) * nv).abs();
if e > best {
(best, u, v, moved) = (e, nu, nv, true);
}
}
if !moved {
h *= 0.5;
halvings += 1;
}
}
best
}
pub(crate) fn sampled_triangle_errors(sdf: &SdfNode, mesh: &Mesh) -> Vec<f32> {
use rayon::prelude::*;
(0..mesh.triangle_count())
.into_par_iter()
.map(|t| sample_triangle(sdf, triangle_positions(mesh, t)).0)
.collect()
}
pub(crate) fn cluster_surface_error(
sdf: &SdfNode,
mesh: &Mesh,
tris: &[usize],
sampled: &[f32],
) -> f32 {
use rayon::prelude::*;
let coarse = tris.iter().map(|&t| sampled[t]).fold(0.0f32, law_max_f32);
let found = tris
.par_iter()
.filter(|&&t| sampled[t] >= 0.5 * coarse)
.map(|&t| refine_triangle(sdf, triangle_positions(mesh, t)))
.reduce(|| coarse, law_max_f32);
if found == 0.0 {
return 0.0;
}
let scale = tris
.iter()
.flat_map(|&t| triangle_positions(mesh, t))
.map(|p| p.abs().max_element())
.fold(1.0f32, law_max_f32);
4.0f32 * (f32::EPSILON * scale) + found
}
fn extract_cluster_geometry(mesh: &Mesh, tri_indices: &[usize]) -> (Vec<Vertex>, Vec<Triangle>) {
use std::collections::HashMap;
let mut vertex_map: HashMap<u32, u32> = HashMap::new();
let mut vertices = Vec::new();
let mut triangles = Vec::new();
for &tri_idx in tri_indices {
let base = tri_idx * 3;
let mut new_indices = [0u32; 3];
#[allow(clippy::needless_range_loop)]
for i in 0..3 {
let old_idx = mesh.indices[base + i];
let new_idx = *vertex_map.entry(old_idx).or_insert_with(|| {
let idx = vertices.len() as u32;
vertices.push(mesh.vertices[old_idx as usize]);
idx
});
new_indices[i] = new_idx;
}
triangles.push(Triangle::new(
new_indices[0],
new_indices[1],
new_indices[2],
));
}
(vertices, triangles)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cluster_bounds() {
let vertices = vec![
Vec3::new(-1.0, -1.0, -1.0),
Vec3::new(1.0, 1.0, 1.0),
Vec3::new(0.0, 0.0, 0.0),
];
let bounds = ClusterBounds::from_vertices(&vertices);
assert_eq!(bounds.aabb_min, Vec3::new(-1.0, -1.0, -1.0));
assert_eq!(bounds.aabb_max, Vec3::new(1.0, 1.0, 1.0));
assert!((bounds.center - Vec3::ZERO).length() < 0.01);
}
#[test]
fn test_generate_nanite_mesh() {
let sphere = SdfNode::sphere(1.0);
let config = NaniteConfig::preview();
let nanite = generate_nanite_mesh(&sphere, Vec3::splat(-2.0), Vec3::splat(2.0), &config);
assert!(!nanite.clusters.is_empty());
assert!(!nanite.lod_levels.is_empty());
assert!(nanite.total_triangles > 0);
}
#[test]
fn test_cluster_selection() {
let sphere = SdfNode::sphere(1.0);
let config = NaniteConfig::preview();
let nanite = generate_nanite_mesh(&sphere, Vec3::splat(-2.0), Vec3::splat(2.0), &config);
let close_clusters = nanite.select_clusters(Vec3::new(0.0, 0.0, 3.0), 0.01);
let far_clusters = nanite.select_clusters(Vec3::new(0.0, 0.0, 100.0), 0.01);
assert!(far_clusters.len() <= close_clusters.len() + nanite.clusters.len());
}
#[test]
fn test_to_mesh() {
let sphere = SdfNode::sphere(1.0);
let config = NaniteConfig::preview();
let nanite = generate_nanite_mesh(&sphere, Vec3::splat(-2.0), Vec3::splat(2.0), &config);
let mesh = nanite.to_mesh(0);
assert!(mesh.vertex_count() > 0);
assert!(mesh.triangle_count() > 0);
}
#[test]
fn test_nanite_dual_contouring() {
let sphere = SdfNode::sphere(1.0);
let config = NaniteConfig {
use_dual_contouring: true,
lod_levels: 2,
base_resolution: 16,
..NaniteConfig::preview()
};
let nanite = generate_nanite_mesh(&sphere, Vec3::splat(-2.0), Vec3::splat(2.0), &config);
assert!(!nanite.clusters.is_empty());
assert!(nanite.total_triangles > 0);
}
#[test]
fn test_nanite_tight_aabb() {
let sphere = SdfNode::sphere(0.5);
let config = NaniteConfig {
use_tight_aabb: true,
..NaniteConfig::preview()
};
let nanite = generate_nanite_mesh(&sphere, Vec3::splat(-10.0), Vec3::splat(10.0), &config);
assert!(!nanite.clusters.is_empty());
}
#[test]
fn test_nanite_material_id() {
let sphere = SdfNode::sphere(1.0);
let config = NaniteConfig::preview();
let nanite = generate_nanite_mesh(&sphere, Vec3::splat(-2.0), Vec3::splat(2.0), &config);
for cluster in &nanite.clusters {
assert_eq!(cluster.material_id, 0);
}
}
#[test]
fn test_normal_cone_empty() {
let cone = NormalCone::from_normals(&[]);
assert!((cone.cutoff_cos + 1.0).abs() < 1e-6);
assert!(!cone.is_backface_culled(Vec3::Z));
}
#[test]
fn test_normal_cone_all_same_normal() {
let normals = vec![Vec3::Y; 5];
let cone = NormalCone::from_normals(&normals);
assert!((cone.axis.y - 1.0).abs() < 1e-4);
assert!((cone.cutoff_cos - 1.0).abs() < 1e-4);
}
#[test]
fn test_normal_cone_scattered() {
let normals = vec![
Vec3::X,
Vec3::NEG_X,
Vec3::Y,
Vec3::NEG_Y,
Vec3::Z,
Vec3::NEG_Z,
];
let cone = NormalCone::from_normals(&normals);
assert!(cone.cutoff_cos <= -1.0 + 1e-6);
}
#[test]
fn test_normal_cone_hemispherical() {
let normals = vec![
Vec3::Y,
Vec3::new(1.0, 1.0, 0.0).normalize(),
Vec3::new(-1.0, 1.0, 0.0).normalize(),
Vec3::new(0.0, 1.0, 1.0).normalize(),
Vec3::new(0.0, 1.0, -1.0).normalize(),
];
let cone = NormalCone::from_normals(&normals);
assert!(cone.axis.y > 0.9);
assert!(cone.cutoff_cos > 0.0);
}
#[test]
fn test_normal_cone_backface_culled() {
let normals = vec![Vec3::Y; 3];
let cone = NormalCone::from_normals(&normals);
assert!(cone.is_backface_culled(Vec3::Y));
assert!(!cone.is_backface_culled(Vec3::NEG_Y));
}
#[test]
fn test_normal_cone_wide_no_cull() {
let normals = vec![
Vec3::Y,
Vec3::new(0.866, 0.5, 0.0), Vec3::new(-0.866, 0.5, 0.0),
];
let cone = NormalCone::from_normals(&normals);
assert!(!cone.is_backface_culled(Vec3::X));
}
#[test]
fn test_normal_cone_unbounded_never_culls() {
let cone = NormalCone::unbounded();
assert!(!cone.is_backface_culled(Vec3::X));
assert!(!cone.is_backface_culled(Vec3::NEG_X));
assert!(!cone.is_backface_culled(Vec3::Y));
assert!(!cone.is_backface_culled(Vec3::NEG_Y));
}
#[test]
fn test_normal_cone_apex_default_none() {
let normals = vec![Vec3::Y; 3];
let cone = NormalCone::from_normals(&normals);
assert!(cone.apex.is_none());
}
#[test]
fn test_normal_cone_from_normals_and_positions_populates_apex() {
let normals = vec![Vec3::Y; 3];
let face_centers = vec![
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
];
let cone = NormalCone::from_normals_and_positions(&normals, &face_centers);
assert!(cone.apex.is_some());
let apex = cone.apex.unwrap();
assert!(apex.y <= 0.0, "apex.y should be <= 0, got {}", apex.y);
}
#[test]
fn test_normal_cone_apex_unbounded_returns_none() {
let normals = vec![Vec3::X, Vec3::NEG_X, Vec3::Y, Vec3::NEG_Y];
let centers = vec![Vec3::ZERO; 4];
let cone = NormalCone::from_normals_and_positions(&normals, ¢ers);
assert!(cone.cutoff_cos <= -1.0 + 1e-6);
assert!(cone.apex.is_none());
}
#[test]
fn test_normal_cone_apex_length_mismatch_fallback() {
let normals = vec![Vec3::Y; 3];
let centers = vec![Vec3::ZERO; 2]; let cone = NormalCone::from_normals_and_positions(&normals, ¢ers);
assert!(cone.apex.is_none());
assert!((cone.axis.y - 1.0).abs() < 1e-4);
}
#[test]
fn test_normal_cone_apex_finite() {
let normals = vec![
Vec3::Y,
Vec3::new(0.5, 0.866, 0.0).normalize(),
Vec3::new(-0.5, 0.866, 0.0).normalize(),
Vec3::new(0.0, 0.866, 0.5).normalize(),
];
let face_centers = vec![
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(0.5, 0.866, 0.0),
Vec3::new(-0.5, 0.866, 0.0),
Vec3::new(0.0, 0.866, 0.5),
];
let cone = NormalCone::from_normals_and_positions(&normals, &face_centers);
let apex = cone.apex.expect("apex should be Some");
assert!(
apex.x.is_finite() && apex.y.is_finite() && apex.z.is_finite(),
"apex must be finite, got {:?}",
apex
);
}
}