#[allow(dead_code)]
pub struct SmoothNormalsConfig {
pub crease_angle_deg: f32,
pub area_weighted: bool,
}
#[allow(dead_code)]
pub struct SmoothNormalsResult {
pub normals: Vec<[f32; 3]>,
pub hard_edge_count: usize,
}
#[allow(dead_code)]
pub fn normalize_vec3(v: [f32; 3]) -> [f32; 3] {
let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
if len < 1e-12 {
return [0.0, 0.0, 1.0];
}
[v[0] / len, v[1] / len, v[2] / len]
}
#[allow(dead_code)]
pub fn face_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] {
let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
[
ab[1] * ac[2] - ab[2] * ac[1],
ab[2] * ac[0] - ab[0] * ac[2],
ab[0] * ac[1] - ab[1] * ac[0],
]
}
#[allow(dead_code)]
pub fn angle_between_normals_deg(a: [f32; 3], b: [f32; 3]) -> f32 {
let na = normalize_vec3(a);
let nb = normalize_vec3(b);
let dot = (na[0] * nb[0] + na[1] * nb[1] + na[2] * nb[2]).clamp(-1.0, 1.0);
dot.acos().to_degrees()
}
fn vec3_len_sq(v: [f32; 3]) -> f32 {
v[0] * v[0] + v[1] * v[1] + v[2] * v[2]
}
fn add3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[allow(dead_code)]
fn scale3(v: [f32; 3], s: f32) -> [f32; 3] {
[v[0] * s, v[1] * s, v[2] * s]
}
#[allow(dead_code)]
pub fn default_smooth_normals_config() -> SmoothNormalsConfig {
SmoothNormalsConfig {
crease_angle_deg: 30.0,
area_weighted: true,
}
}
#[allow(dead_code)]
pub fn compute_smooth_normals(
verts: &[[f32; 3]],
faces: &[[u32; 3]],
cfg: &SmoothNormalsConfig,
) -> SmoothNormalsResult {
let n = verts.len();
if n == 0 || faces.is_empty() {
return SmoothNormalsResult {
normals: vec![[0.0, 0.0, 1.0]; n],
hard_edge_count: 0,
};
}
let face_normals: Vec<[f32; 3]> = faces
.iter()
.map(|f| {
let a = verts[f[0] as usize];
let b = verts[f[1] as usize];
let c = verts[f[2] as usize];
let raw = face_normal(a, b, c);
if cfg.area_weighted {
raw
} else {
normalize_vec3(raw)
}
})
.collect();
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
for (fi, f) in faces.iter().enumerate() {
for &vi in f.iter() {
adj[vi as usize].push(fi);
}
}
let _crease_sq = cfg.crease_angle_deg * cfg.crease_angle_deg;
let mut hard_edge_count = 0usize;
let mut normals = vec![[0.0f32; 3]; n];
for (vi, adj_faces) in adj.iter().enumerate() {
if adj_faces.is_empty() {
normals[vi] = [0.0, 0.0, 1.0];
continue;
}
let ref_n = normalize_vec3(face_normals[adj_faces[0]]);
let mut accum = face_normals[adj_faces[0]];
let mut included = 1usize;
for &fi in adj_faces.iter().skip(1) {
let fn_i = normalize_vec3(face_normals[fi]);
let angle = angle_between_normals_deg(ref_n, fn_i);
if angle <= cfg.crease_angle_deg {
accum = add3(accum, face_normals[fi]);
included += 1;
} else {
hard_edge_count += 1;
}
}
let _ = included;
normals[vi] = if vec3_len_sq(accum) < 1e-24 {
[0.0, 0.0, 1.0]
} else {
normalize_vec3(accum)
};
}
SmoothNormalsResult {
normals,
hard_edge_count,
}
}
#[allow(dead_code)]
pub fn smooth_normals_vertex_count(result: &SmoothNormalsResult) -> usize {
result.normals.len()
}
#[allow(dead_code)]
pub fn smooth_normals_are_unit(result: &SmoothNormalsResult) -> bool {
result.normals.iter().all(|&n| {
let len_sq = n[0] * n[0] + n[1] * n[1] + n[2] * n[2];
(len_sq - 1.0).abs() < 1e-4
})
}
#[allow(dead_code)]
pub fn flat_normals(verts: &[[f32; 3]], faces: &[[u32; 3]]) -> Vec<[f32; 3]> {
let n = verts.len();
let mut normals = vec![[0.0f32, 0.0, 1.0]; n];
for f in faces {
let a = verts[f[0] as usize];
let b = verts[f[1] as usize];
let c = verts[f[2] as usize];
let fn_ = normalize_vec3(face_normal(a, b, c));
normals[f[0] as usize] = fn_;
normals[f[1] as usize] = fn_;
normals[f[2] as usize] = fn_;
}
normals
}
#[cfg(test)]
mod tests {
use super::*;
fn single_tri_verts() -> Vec<[f32; 3]> {
vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
}
fn single_tri_faces() -> Vec<[u32; 3]> {
vec![[0, 1, 2]]
}
#[test]
fn test_normalize_vec3_unit() {
let v = normalize_vec3([3.0, 0.0, 0.0]);
assert!((v[0] - 1.0).abs() < 1e-6);
assert!(v[1].abs() < 1e-6);
assert!(v[2].abs() < 1e-6);
}
#[test]
fn test_normalize_vec3_zero_returns_default() {
let v = normalize_vec3([0.0, 0.0, 0.0]);
assert_eq!(v, [0.0, 0.0, 1.0]);
}
#[test]
fn test_face_normal_z_up() {
let n = face_normal([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
let nu = normalize_vec3(n);
assert!((nu[2] - 1.0).abs() < 1e-5, "nu={:?}", nu);
}
#[test]
fn test_angle_between_same_normals_is_zero() {
let angle = angle_between_normals_deg([0.0, 1.0, 0.0], [0.0, 1.0, 0.0]);
assert!(angle.abs() < 1e-4, "angle={}", angle);
}
#[test]
fn test_angle_between_perpendicular_is_90() {
let angle = angle_between_normals_deg([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
assert!((angle - 90.0).abs() < 1e-3, "angle={}", angle);
}
#[test]
fn test_default_config_crease_angle() {
let cfg = default_smooth_normals_config();
assert!((cfg.crease_angle_deg - 30.0).abs() < 1e-6);
assert!(cfg.area_weighted);
}
#[test]
fn test_smooth_normals_single_tri_count() {
let verts = single_tri_verts();
let faces = single_tri_faces();
let cfg = default_smooth_normals_config();
let result = compute_smooth_normals(&verts, &faces, &cfg);
assert_eq!(smooth_normals_vertex_count(&result), 3);
}
#[test]
fn test_smooth_normals_are_unit_single_tri() {
let verts = single_tri_verts();
let faces = single_tri_faces();
let cfg = default_smooth_normals_config();
let result = compute_smooth_normals(&verts, &faces, &cfg);
assert!(smooth_normals_are_unit(&result));
}
#[test]
fn test_smooth_normals_direction_z_up() {
let verts = single_tri_verts();
let faces = single_tri_faces();
let cfg = default_smooth_normals_config();
let result = compute_smooth_normals(&verts, &faces, &cfg);
for n in &result.normals {
assert!(n[2] > 0.9, "n={:?}", n);
}
}
#[test]
fn test_smooth_normals_empty_mesh() {
let cfg = default_smooth_normals_config();
let result = compute_smooth_normals(&[], &[], &cfg);
assert_eq!(result.normals.len(), 0);
}
#[test]
fn test_flat_normals_single_tri() {
let verts = single_tri_verts();
let faces = single_tri_faces();
let normals = flat_normals(&verts, &faces);
assert_eq!(normals.len(), 3);
for n in &normals {
assert!(n[2] > 0.9, "n={:?}", n);
}
}
#[test]
fn test_smooth_normals_two_coplanar_tris_no_crease() {
let verts = vec![
[0.0f32, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
];
let faces = vec![[0u32, 1, 2], [0, 2, 3]];
let cfg = default_smooth_normals_config();
let result = compute_smooth_normals(&verts, &faces, &cfg);
assert!(smooth_normals_are_unit(&result));
assert_eq!(result.hard_edge_count, 0);
}
}