use glam::Vec3;
use std::collections::HashMap;
use crate::mesh::Mesh;
fn avg4(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
[
(a[0] + b[0]) / 2.0,
(a[1] + b[1]) / 2.0,
(a[2] + b[2]) / 2.0,
(a[3] + b[3]) / 2.0,
]
}
fn blend_skin(ja: [u16; 4], wa: [f32; 4], jb: [u16; 4], wb: [f32; 4]) -> ([u16; 4], [f32; 4]) {
let mut acc: Vec<(u16, f32)> = Vec::with_capacity(8);
for (j, w) in ja.iter().zip(wa).chain(jb.iter().zip(wb)) {
if w <= 0.0 {
continue;
}
match acc.iter_mut().find(|(jj, _)| jj == j) {
Some((_, ww)) => *ww += w / 2.0,
None => acc.push((*j, w / 2.0)),
}
}
acc.sort_by(|x, y| y.1.partial_cmp(&x.1).unwrap().then(x.0.cmp(&y.0)));
acc.truncate(4);
let sum: f32 = acc.iter().map(|(_, w)| w).sum();
let mut joints = [0u16; 4];
let mut weights = [0f32; 4];
for (i, (j, w)) in acc.iter().enumerate() {
joints[i] = *j;
weights[i] = w / sum.max(1e-9);
}
(joints, weights)
}
pub fn subdivide(src: &Mesh, smooth: bool) -> Mesh {
let mut m = Mesh {
positions: src.positions.clone(),
normals: src.normals.clone(),
colors: src.colors.clone(),
indices: Vec::with_capacity(src.indices.len() * 4),
joints: src.joints.clone(),
weights: src.weights.clone(),
uvs: src.uvs.clone(),
tangents: src.tangents.clone(),
morphs: Vec::new(),
};
let mut morphs = src.morphs.clone();
let mut midpoint_cache: HashMap<(u32, u32), u32> = HashMap::new();
let mut edge_of: HashMap<(u32, u32), Vec<u32>> = HashMap::new(); for t in src.indices.chunks_exact(3) {
for k in 0..3 {
let (a, b, c) = (t[k], t[(k + 1) % 3], t[(k + 2) % 3]);
let key = (a.min(b), a.max(b));
edge_of.entry(key).or_default().push(c);
}
}
let mut mid =
|a: u32, b: u32, m: &mut Mesh, morphs: &mut Vec<crate::mesh::MorphTarget>| -> u32 {
let key = (a.min(b), a.max(b));
if let Some(&i) = midpoint_cache.get(&key) {
return i;
}
let (ia, ib) = (a as usize, b as usize);
let opp = edge_of.get(&key).map(|v| v.as_slice()).unwrap_or(&[]);
let pos = if smooth && opp.len() == 2 {
(m.positions[ia] + m.positions[ib]) * 0.375
+ (m.positions[opp[0] as usize] + m.positions[opp[1] as usize]) * 0.125
} else {
(m.positions[ia] + m.positions[ib]) / 2.0
};
m.positions.push(pos);
m.normals
.push(((m.normals[ia] + m.normals[ib]) / 2.0).normalize_or(Vec3::Y));
m.colors.push((m.colors[ia] + m.colors[ib]) / 2.0);
if !m.joints.is_empty() {
let (j, w) = blend_skin(m.joints[ia], m.weights[ia], m.joints[ib], m.weights[ib]);
m.joints.push(j);
m.weights.push(w);
}
if !m.uvs.is_empty() {
m.uvs.push((m.uvs[ia] + m.uvs[ib]) / 2.0);
m.tangents
.push(avg4(m.tangents[ia].into(), m.tangents[ib].into()).into());
}
for mt in morphs.iter_mut() {
let d = (mt.deltas[ia] + mt.deltas[ib]) / 2.0;
mt.deltas.push(d);
}
let i = (m.positions.len() - 1) as u32;
midpoint_cache.insert(key, i);
i
};
for t in src.indices.chunks_exact(3) {
let (a, b, c) = (t[0], t[1], t[2]);
let ab = mid(a, b, &mut m, &mut morphs);
let bc = mid(b, c, &mut m, &mut morphs);
let ca = mid(c, a, &mut m, &mut morphs);
m.indices
.extend_from_slice(&[a, ab, ca, b, bc, ab, c, ca, bc, ab, bc, ca]);
}
if smooth {
let n_orig = src.positions.len();
let mut neighbors: Vec<Vec<u32>> = vec![Vec::new(); n_orig];
let mut boundary = vec![false; n_orig];
for (&(a, b), opp) in &edge_of {
neighbors[a as usize].push(b);
neighbors[b as usize].push(a);
if opp.len() != 2 {
boundary[a as usize] = true;
boundary[b as usize] = true;
}
}
let mut new_pos = m.positions.clone();
for (v, nbrs) in neighbors.iter().enumerate() {
if boundary[v] || nbrs.len() < 3 {
continue;
}
let k = nbrs.len() as f32;
let beta =
(5.0 / 8.0 - (0.375 + 0.25 * (core::f32::consts::TAU / k).cos()).powi(2)) / k;
let sum: Vec3 = nbrs.iter().map(|&i| src.positions[i as usize]).sum();
new_pos[v] = src.positions[v] * (1.0 - k * beta) + sum * beta;
}
m.positions = new_pos;
m.recompute_smooth_normals();
}
m.morphs = morphs;
m
}
pub fn subdivide_n(src: &Mesh, n: u32, smooth: bool) -> Mesh {
let mut m = src.clone();
for _ in 0..n.min(4) {
m = subdivide(&m, smooth);
}
m
}
pub fn decimate(src: &Mesh, ratio: f32) -> Mesh {
let target = ((src.triangle_count() as f32) * ratio.clamp(0.02, 1.0)) as usize;
let (lo, hi) = src.bounds();
let diag = (hi - lo).length().max(1e-6);
let mut cell = diag / 96.0;
for _ in 0..24 {
let m = cluster(src, lo, cell);
if m.triangle_count() <= target.max(4) {
return m;
}
cell *= 1.35;
}
cluster(src, lo, diag / 2.0)
}
fn cluster(src: &Mesh, lo: Vec3, cell: f32) -> Mesh {
use std::collections::HashMap;
let key = |p: Vec3| -> (i32, i32, i32) {
(
((p.x - lo.x) / cell).floor() as i32,
((p.y - lo.y) / cell).floor() as i32,
((p.z - lo.z) / cell).floor() as i32,
)
};
let mut ids: HashMap<(i32, i32, i32), u32> = HashMap::new();
let mut remap = Vec::with_capacity(src.positions.len());
let mut count: Vec<u32> = Vec::new();
let mut m = Mesh::new();
for (vi, &p) in src.positions.iter().enumerate() {
let k = key(p);
let id = *ids.entry(k).or_insert_with(|| {
m.positions.push(Vec3::ZERO);
m.normals.push(Vec3::ZERO);
m.colors.push(Vec3::ZERO);
if src.is_skinned() {
m.joints.push(src.joints[vi]);
m.weights.push(src.weights[vi]);
}
if src.has_uvs() {
m.uvs.push(src.uvs[vi]);
m.tangents.push(src.tangents[vi]);
}
count.push(0);
(m.positions.len() - 1) as u32
});
m.positions[id as usize] += p;
m.normals[id as usize] += src.normals[vi];
m.colors[id as usize] += src.colors[vi];
count[id as usize] += 1;
remap.push(id);
}
for (i, c) in count.iter().enumerate() {
let inv = 1.0 / *c as f32;
m.positions[i] *= inv;
m.colors[i] *= inv;
m.normals[i] = m.normals[i].normalize_or(Vec3::Y);
}
for t in src.indices.chunks_exact(3) {
let (a, b, c) = (
remap[t[0] as usize],
remap[t[1] as usize],
remap[t[2] as usize],
);
if a != b && b != c && a != c {
m.indices.extend_from_slice(&[a, b, c]);
}
}
m
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mesh::{cuboid, icosphere};
#[test]
fn quadruples_triangles() {
let m = icosphere(1.0, 1, Vec3::ONE);
let s = subdivide(&m, false);
s.validate().unwrap();
assert_eq!(s.triangle_count(), m.triangle_count() * 4);
}
#[test]
fn smoothing_shrinks_slightly_and_stays_valid() {
let m = cuboid(Vec3::ZERO, Vec3::ONE, Vec3::ONE);
let s = subdivide_n(&m, 2, true);
s.validate().unwrap();
let (lo, hi) = s.bounds();
assert!(hi.x <= 1.0 + 1e-5 && lo.x >= -1.0 - 1e-5);
assert!(hi.x - lo.x > 1.2, "should not collapse: {:?}", hi - lo);
}
#[test]
fn decimate_reduces_and_stays_valid() {
let m = icosphere(1.0, 4, Vec3::ONE); let d = decimate(&m, 0.2);
d.validate().unwrap();
assert!(d.triangle_count() <= m.triangle_count() / 4);
assert!(d.triangle_count() > 16);
let d2 = decimate(&m, 0.2);
assert_eq!(d.positions, d2.positions);
assert_eq!(d.indices, d2.indices);
}
#[test]
fn carries_skin_weights() {
let mut m = icosphere(1.0, 1, Vec3::ONE);
m.bind_all_to_joint(3);
let s = subdivide(&m, true);
assert_eq!(s.joints.len(), s.positions.len());
assert!(s.joints.iter().all(|j| j[0] == 3));
assert!(
s.weights
.iter()
.all(|w| (w.iter().sum::<f32>() - 1.0).abs() < 1e-4)
);
}
}