use crate::mesh::Mesh;
use glam::Vec3;
const OVERDRAW_CACHE_SIZE: usize = 32;
struct Cluster {
start_tri: usize,
end_tri: usize, centroid: Vec3,
}
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
pub fn optimize_overdraw(mesh: &mut Mesh, threshold: f32) {
let tri_count = mesh.indices.len() / 3;
if tri_count == 0 {
return;
}
let vertex_count = mesh.vertices.len();
if mesh.indices.iter().any(|&i| i as usize >= vertex_count) {
return; }
let hard = hard_boundaries(&mesh.indices, vertex_count, MESHOPT_OVERDRAW_CACHE_SIZE);
let soft = soft_boundaries(
&mesh.indices,
vertex_count,
&hard,
MESHOPT_OVERDRAW_CACHE_SIZE,
threshold,
);
if soft.len() <= 1 {
return;
}
let keys = cluster_sort_keys(mesh, &soft);
let mut order: Vec<usize> = (0..soft.len()).collect();
order.sort_by(|&a, &b| keys[b].total_cmp(&keys[a]));
let mut new_indices = Vec::with_capacity(mesh.indices.len());
for &ci in &order {
let start = soft[ci];
let end = soft.get(ci + 1).copied().unwrap_or(tri_count);
new_indices.extend_from_slice(&mesh.indices[start * 3..end * 3]);
}
mesh.indices = new_indices;
}
const MESHOPT_OVERDRAW_CACHE_SIZE: u32 = 16;
fn update_fifo(tri: &[u32], cache_size: u32, stamps: &mut [u32], timestamp: &mut u32) -> u32 {
let mut misses = 0;
for &v in tri {
let s = &mut stamps[v as usize];
if timestamp.wrapping_sub(*s) > cache_size {
*s = *timestamp;
*timestamp = timestamp.wrapping_add(1);
misses += 1;
}
}
misses
}
fn hard_boundaries(indices: &[u32], vertex_count: usize, cache_size: u32) -> Vec<usize> {
let mut stamps = vec![0u32; vertex_count];
let mut timestamp = cache_size + 1;
let mut out = Vec::new();
for (t, tri) in indices.chunks_exact(3).enumerate() {
let m = update_fifo(tri, cache_size, &mut stamps, &mut timestamp);
if t == 0 || m == 3 {
out.push(t);
}
}
out
}
#[allow(clippy::cast_precision_loss)]
fn soft_boundaries(
indices: &[u32],
vertex_count: usize,
hard: &[usize],
cache_size: u32,
threshold: f32,
) -> Vec<usize> {
let tri_count = indices.len() / 3;
let mut stamps = vec![0u32; vertex_count];
let mut timestamp = 0u32;
let mut out = Vec::with_capacity(hard.len());
for (h, &start) in hard.iter().enumerate() {
let end = hard.get(h + 1).copied().unwrap_or(tri_count);
timestamp = timestamp.wrapping_add(cache_size + 1);
let mut cluster_misses = 0u32;
for t in start..end {
cluster_misses += update_fifo(
&indices[t * 3..t * 3 + 3],
cache_size,
&mut stamps,
&mut timestamp,
);
}
let cluster_threshold = threshold * (cluster_misses as f32 / (end - start) as f32);
out.push(start);
timestamp = timestamp.wrapping_add(cache_size + 1);
let mut running_misses = 0u32;
let mut running_faces = 0u32;
for t in start..end {
running_misses += update_fifo(
&indices[t * 3..t * 3 + 3],
cache_size,
&mut stamps,
&mut timestamp,
);
running_faces += 1;
if running_misses as f32 / running_faces as f32 <= cluster_threshold {
out.push(t + 1);
timestamp = timestamp.wrapping_add(cache_size + 1);
running_misses = 0;
running_faces = 0;
}
}
if out.last() != Some(&start) {
out.pop();
}
}
out
}
fn cluster_sort_keys(mesh: &Mesh, clusters: &[usize]) -> Vec<f32> {
let tri_count = mesh.indices.len() / 3;
let pos = |i: u32| mesh.vertices[i as usize].position;
let mut mesh_centroid = Vec3::ZERO;
for &i in &mesh.indices {
mesh_centroid += pos(i);
}
#[allow(clippy::cast_precision_loss)]
let mesh_centroid = mesh_centroid / mesh.indices.len() as f32;
clusters
.iter()
.enumerate()
.map(|(ci, &start)| {
let end = clusters.get(ci + 1).copied().unwrap_or(tri_count);
let mut area_sum = 0.0f32;
let mut centroid = Vec3::ZERO;
let mut normal = Vec3::ZERO;
for tri in mesh.indices[start * 3..end * 3].chunks_exact(3) {
let (p0, p1, p2) = (pos(tri[0]), pos(tri[1]), pos(tri[2]));
let n = (p1 - p0).cross(p2 - p0);
let area = n.length();
centroid += (p0 + p1 + p2) * (area / 3.0);
normal += n;
area_sum += area;
}
let centroid = if area_sum == 0.0 {
Vec3::ZERO
} else {
centroid / area_sum
};
let normal = normal.normalize_or_zero();
normal.dot(centroid - mesh_centroid)
})
.collect()
}
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
pub fn optimize_overdraw_with_views(mesh: &mut Mesh, _threshold: f32, view_directions: &[Vec3]) {
let tri_count = mesh.indices.len() / 3;
if tri_count == 0 || view_directions.is_empty() {
return;
}
let clusters = compute_clusters(mesh, OVERDRAW_CACHE_SIZE);
if clusters.len() <= 1 {
return; }
let mut rank_sum = vec![0.0_f32; clusters.len()];
for &view in view_directions {
let mut view_indexed: Vec<(usize, f32)> = clusters
.iter()
.enumerate()
.map(|(i, c)| (i, c.centroid.dot(view)))
.collect();
view_indexed.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(core::cmp::Ordering::Equal));
for (rank, &(cluster_idx, _)) in view_indexed.iter().enumerate() {
rank_sum[cluster_idx] += rank as f32;
}
}
let mut cluster_order: Vec<usize> = (0..clusters.len()).collect();
cluster_order.sort_by(|&a, &b| {
rank_sum[a]
.partial_cmp(&rank_sum[b])
.unwrap_or(core::cmp::Ordering::Equal)
});
let mut new_indices = Vec::with_capacity(mesh.indices.len());
for &ci in &cluster_order {
let c = &clusters[ci];
for t in c.start_tri..c.end_tri {
let base = t * 3;
new_indices.push(mesh.indices[base]);
new_indices.push(mesh.indices[base + 1]);
new_indices.push(mesh.indices[base + 2]);
}
}
mesh.indices = new_indices;
}
#[must_use]
pub const fn default_view_directions() -> [Vec3; 6] {
[
Vec3::X,
Vec3::NEG_X,
Vec3::Y,
Vec3::NEG_Y,
Vec3::Z,
Vec3::NEG_Z,
]
}
#[allow(clippy::cast_precision_loss)]
fn compute_clusters(mesh: &Mesh, cache_size: usize) -> Vec<Cluster> {
let tri_count = mesh.indices.len() / 3;
if tri_count == 0 {
return Vec::new();
}
let mut clusters: Vec<Cluster> = Vec::new();
let mut cache: Vec<u32> = Vec::with_capacity(cache_size);
let mut miss_since_last_boundary = 0usize;
let mut cluster_start = 0usize;
let mut centroid_accum = Vec3::ZERO;
let mut centroid_count = 0usize;
let boundary_miss_count = 3usize;
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 misses = 0;
for &v in &[ia, ib, ic] {
if cache.contains(&v) {
} else {
misses += 1;
cache.push(v);
if cache.len() > cache_size {
cache.remove(0);
}
}
}
miss_since_last_boundary += misses;
if (ia as usize) < mesh.vertices.len() {
centroid_accum += mesh.vertices[ia as usize].position;
centroid_count += 1;
}
if (ib as usize) < mesh.vertices.len() {
centroid_accum += mesh.vertices[ib as usize].position;
centroid_count += 1;
}
if (ic as usize) < mesh.vertices.len() {
centroid_accum += mesh.vertices[ic as usize].position;
centroid_count += 1;
}
if misses >= boundary_miss_count && miss_since_last_boundary >= cache_size {
let centroid = if centroid_count > 0 {
centroid_accum / (centroid_count as f32)
} else {
Vec3::ZERO
};
clusters.push(Cluster {
start_tri: cluster_start,
end_tri: t + 1,
centroid,
});
cluster_start = t + 1;
miss_since_last_boundary = 0;
centroid_accum = Vec3::ZERO;
centroid_count = 0;
}
}
if cluster_start < tri_count {
let centroid = if centroid_count > 0 {
centroid_accum / (centroid_count as f32)
} else {
Vec3::ZERO
};
clusters.push(Cluster {
start_tri: cluster_start,
end_tri: tri_count,
centroid,
});
}
clusters
}
#[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_overdraw_empty_mesh() {
let mut mesh = Mesh::new();
optimize_overdraw(&mut mesh, 1.0);
assert!(mesh.indices.is_empty());
}
#[test]
fn test_overdraw_preserves_triangle_count() {
let mut mesh = make_sphere_mesh(16);
let tri_before = mesh.triangle_count();
let vert_before = mesh.vertices.len();
optimize_overdraw(&mut mesh, 1.0);
assert_eq!(mesh.triangle_count(), tri_before);
assert_eq!(mesh.vertices.len(), vert_before);
}
#[test]
fn test_overdraw_preserves_triangle_set() {
let mut mesh = make_sphere_mesh(8);
let mut tris_before: Vec<[u32; 3]> = (0..mesh.triangle_count())
.map(|t| {
let mut tri = [
mesh.indices[t * 3],
mesh.indices[t * 3 + 1],
mesh.indices[t * 3 + 2],
];
tri.sort_unstable();
tri
})
.collect();
tris_before.sort_unstable();
optimize_overdraw(&mut mesh, 1.0);
let mut tris_after: Vec<[u32; 3]> = (0..mesh.triangle_count())
.map(|t| {
let mut tri = [
mesh.indices[t * 3],
mesh.indices[t * 3 + 1],
mesh.indices[t * 3 + 2],
];
tri.sort_unstable();
tri
})
.collect();
tris_after.sort_unstable();
assert_eq!(
tris_before, tris_after,
"三角形集合が保存されていない (index 追加/削除された?)"
);
}
#[test]
fn test_overdraw_with_custom_views() {
let mut mesh = make_sphere_mesh(8);
let tri_before = mesh.triangle_count();
let views = [Vec3::Y];
optimize_overdraw_with_views(&mut mesh, 1.0, &views);
assert_eq!(mesh.triangle_count(), tri_before);
}
#[test]
fn test_overdraw_single_cluster_noop() {
let v = |x: f32, z: f32| Vertex::new(Vec3::new(x, 0.0, z), Vec3::Y);
let mut mesh = Mesh {
vertices: vec![v(0.0, 0.0), v(1.0, 0.0), v(0.0, 1.0), v(1.0, 1.0)],
indices: vec![0, 1, 2, 1, 3, 2],
};
let indices_before = mesh.indices.clone();
optimize_overdraw(&mut mesh, 1.0);
assert_eq!(mesh.indices, indices_before);
}
#[test]
fn test_default_view_directions() {
let views = default_view_directions();
assert_eq!(views.len(), 6);
for v in &views {
assert!((v.length() - 1.0).abs() < 1e-6);
}
assert!(views.contains(&Vec3::X) && views.contains(&Vec3::NEG_X));
assert!(views.contains(&Vec3::Y) && views.contains(&Vec3::NEG_Y));
assert!(views.contains(&Vec3::Z) && views.contains(&Vec3::NEG_Z));
}
}