use std::collections::HashMap;
const CREASE_COS: f64 = std::f64::consts::FRAC_1_SQRT_2;
const INV_EPS: f64 = 1.0e6;
pub fn smooth_vertex_normals(
vertices: &[[f64; 3]],
indices: &[[u32; 3]],
smooth: bool,
) -> Vec<[f32; 3]> {
let mut welded: HashMap<[i64; 3], usize> = HashMap::new();
let mut rep = vec![0usize; vertices.len()];
for (i, p) in vertices.iter().enumerate() {
let key = [
(p[0] * INV_EPS).round() as i64,
(p[1] * INV_EPS).round() as i64,
(p[2] * INV_EPS).round() as i64,
];
let next = welded.len();
rep[i] = *welded.entry(key).or_insert(next);
}
let mut face_n: Vec<[f64; 3]> = Vec::with_capacity(indices.len());
let mut incident: Vec<Vec<usize>> = vec![Vec::new(); welded.len()];
for (fi, t) in indices.iter().enumerate() {
let a = vertices[t[0] as usize];
let b = vertices[t[1] as usize];
let c = vertices[t[2] as usize];
face_n.push(cross(sub(b, a), sub(c, a)));
for &corner in t {
incident[rep[corner as usize]].push(fi);
}
}
let mut out = vec![[0.0f32; 3]; vertices.len()];
for (fi, t) in indices.iter().enumerate() {
let nf = normalize_or(face_n[fi], [0.0, 0.0, 1.0]);
for &corner in t {
let mut acc = [0.0f64; 3];
for &gf in &incident[rep[corner as usize]] {
if smooth || dot(nf, normalize_or(face_n[gf], nf)) >= CREASE_COS {
acc = add(acc, face_n[gf]);
}
}
let n = normalize_or(acc, nf);
out[corner as usize] = [n[0] as f32, n[1] as f32, n[2] as f32];
}
}
out
}
fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn normalize_or(v: [f64; 3], fallback: [f64; 3]) -> [f64; 3] {
let len = dot(v, v).sqrt();
if len > 1.0e-12 {
[v[0] / len, v[1] / len, v[2] / len]
} else {
fallback
}
}
#[cfg(test)]
mod tests {
use super::smooth_vertex_normals;
fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn flatten(tris: &[[[f64; 3]; 3]]) -> (Vec<[f64; 3]>, Vec<[u32; 3]>) {
let mut verts = Vec::new();
let mut idx = Vec::new();
for t in tris {
let base = verts.len() as u32;
verts.extend_from_slice(t);
idx.push([base, base + 1, base + 2]);
}
(verts, idx)
}
fn hinge(beta: f64) -> (Vec<[f64; 3]>, Vec<[u32; 3]>) {
let e0 = [0.0, 0.0, 0.0];
let e1 = [1.0, 0.0, 0.0];
let w1 = [0.5, 1.0, 0.0]; let w2 = [0.5, -beta.cos(), beta.sin()]; flatten(&[[e0, e1, w1], [e1, e0, w2]])
}
#[test]
fn sharp_edge_stays_creased() {
let (v, i) = hinge(std::f64::consts::FRAC_PI_2);
let n = smooth_vertex_normals(&v, &i, false);
assert_eq!(n.len(), v.len());
assert!(dot(n[0], [0.0, 0.0, 1.0]) > 0.999, "{:?}", n[0]);
assert!(dot(n[4], [0.0, 1.0, 0.0]) > 0.999, "{:?}", n[4]);
}
#[test]
fn shallow_edge_gets_smoothed() {
let beta = 20.0_f64.to_radians();
let (v, i) = hinge(beta);
let n = smooth_vertex_normals(&v, &i, false);
let onto_z = dot(n[0], [0.0, 0.0, 1.0]);
let expected = (beta / 2.0).cos() as f32;
assert!((onto_z - expected).abs() < 1.0e-3, "{onto_z} vs {expected}");
assert!(onto_z < 0.999, "should be blended, not flat: {onto_z}");
assert!(dot(n[0], n[4]) > 0.999, "{:?} vs {:?}", n[0], n[4]);
}
#[test]
fn smoothnormal_forces_blend_across_sharp_edge() {
let (v, i) = hinge(std::f64::consts::FRAC_PI_2);
let n = smooth_vertex_normals(&v, &i, true);
assert!(dot(n[0], n[4]) > 0.999, "{:?} vs {:?}", n[0], n[4]);
assert!(
dot(n[0], [0.0, 0.0, 1.0]) < 0.999,
"should blend: {:?}",
n[0]
);
}
}