fn normalize3(v: [f32; 3]) -> [f32; 3] {
let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
if len < 1e-12 {
[0.0, 1.0, 0.0]
} else {
[v[0] / len, v[1] / len, v[2] / len]
}
}
fn cross3(a: [f32; 3], b: [f32; 3]) -> [f32; 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 add3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct OffsetSurfaceConfig {
pub offset_distance: f32,
pub inward: bool,
pub normalize_normals: bool,
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct OffsetSurfaceResult {
pub vertices: Vec<[f32; 3]>,
pub max_displacement: f32,
}
#[allow(dead_code)]
pub fn default_offset_surface_config() -> OffsetSurfaceConfig {
OffsetSurfaceConfig {
offset_distance: 0.01,
inward: false,
normalize_normals: true,
}
}
#[allow(dead_code)]
pub fn offset_surface(
verts: &[[f32; 3]],
normals: &[[f32; 3]],
cfg: &OffsetSurfaceConfig,
) -> OffsetSurfaceResult {
let sign = if cfg.inward { -1.0f32 } else { 1.0f32 };
let mut out_verts: Vec<[f32; 3]> = Vec::with_capacity(verts.len());
let mut max_disp = 0.0f32;
for (v, n) in verts.iter().zip(normals.iter()) {
let nhat = if cfg.normalize_normals {
normalize3(*n)
} else {
*n
};
let disp = cfg.offset_distance * sign;
let new_v = [
v[0] + nhat[0] * disp,
v[1] + nhat[1] * disp,
v[2] + nhat[2] * disp,
];
let mag = disp.abs();
if mag > max_disp {
max_disp = mag;
}
out_verts.push(new_v);
}
OffsetSurfaceResult {
vertices: out_verts,
max_displacement: max_disp,
}
}
#[allow(dead_code)]
pub fn offset_surface_vertex_count(result: &OffsetSurfaceResult) -> usize {
result.vertices.len()
}
#[allow(dead_code)]
pub fn offset_surface_max_displacement(result: &OffsetSurfaceResult) -> f32 {
result.max_displacement
}
#[allow(dead_code)]
pub fn compute_vertex_normals_offset(verts: &[[f32; 3]], faces: &[[u32; 3]]) -> Vec<[f32; 3]> {
let n = verts.len();
let mut accum = vec![[0.0f32; 3]; 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 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]];
let fn_ = cross3(ab, ac);
for &i in f.iter() {
accum[i as usize] = add3(accum[i as usize], fn_);
}
}
accum.iter().map(|n| normalize3(*n)).collect()
}
#[allow(dead_code)]
pub fn offset_inward(verts: &[[f32; 3]], normals: &[[f32; 3]], amount: f32) -> Vec<[f32; 3]> {
verts
.iter()
.zip(normals.iter())
.map(|(v, n)| {
let nhat = normalize3(*n);
[
v[0] - nhat[0] * amount,
v[1] - nhat[1] * amount,
v[2] - nhat[2] * amount,
]
})
.collect()
}
#[allow(dead_code)]
pub fn offset_outward(verts: &[[f32; 3]], normals: &[[f32; 3]], amount: f32) -> Vec<[f32; 3]> {
verts
.iter()
.zip(normals.iter())
.map(|(v, n)| {
let nhat = normalize3(*n);
[
v[0] + nhat[0] * amount,
v[1] + nhat[1] * amount,
v[2] + nhat[2] * amount,
]
})
.collect()
}
#[allow(dead_code)]
pub fn shell_mesh(
verts: &[[f32; 3]],
faces: &[[u32; 3]],
thickness: f32,
) -> (Vec<[f32; 3]>, Vec<[u32; 3]>) {
let normals = compute_vertex_normals_offset(verts, faces);
let outer = offset_outward(verts, &normals, thickness * 0.5);
let inner = offset_inward(verts, &normals, thickness * 0.5);
let nv = verts.len() as u32;
let mut shell_verts: Vec<[f32; 3]> = outer;
shell_verts.extend(inner);
let mut shell_faces: Vec<[u32; 3]> = Vec::new();
shell_faces.extend(faces.iter().copied());
for f in faces {
shell_faces.push([f[0] + nv, f[2] + nv, f[1] + nv]);
}
for f in faces {
let edges = [(f[0], f[1]), (f[1], f[2]), (f[2], f[0])];
for (a, b) in edges {
let oa = a;
let ob = b;
let ib = b + nv;
let ia = a + nv;
shell_faces.push([oa, ob, ib]);
shell_faces.push([oa, ib, ia]);
}
}
(shell_verts, shell_faces)
}
#[cfg(test)]
mod tests {
use super::*;
fn triangle_verts() -> Vec<[f32; 3]> {
vec![[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
}
fn flat_normals() -> Vec<[f32; 3]> {
vec![[0.0f32, 0.0, 1.0]; 3]
}
#[test]
fn test_default_config() {
let cfg = default_offset_surface_config();
assert!(cfg.offset_distance > 0.0);
assert!(!cfg.inward);
assert!(cfg.normalize_normals);
}
#[test]
fn test_offset_surface_vertex_count() {
let v = triangle_verts();
let n = flat_normals();
let cfg = default_offset_surface_config();
let result = offset_surface(&v, &n, &cfg);
assert_eq!(offset_surface_vertex_count(&result), 3);
}
#[test]
fn test_offset_surface_max_displacement() {
let v = triangle_verts();
let n = flat_normals();
let mut cfg = default_offset_surface_config();
cfg.offset_distance = 0.5;
let result = offset_surface(&v, &n, &cfg);
assert!((offset_surface_max_displacement(&result) - 0.5).abs() < 1e-5);
}
#[test]
fn test_offset_outward_moves_along_normal() {
let v = triangle_verts();
let n = flat_normals();
let out = offset_outward(&v, &n, 1.0);
for p in &out {
assert!((p[2] - 1.0).abs() < 1e-5, "z={}", p[2]);
}
}
#[test]
fn test_offset_inward_moves_opposite_normal() {
let v = triangle_verts();
let n = flat_normals();
let inn = offset_inward(&v, &n, 1.0);
for p in &inn {
assert!((p[2] + 1.0).abs() < 1e-5, "z={}", p[2]);
}
}
#[test]
fn test_compute_vertex_normals_offset_unit_length() {
let v = triangle_verts();
let f = vec![[0u32, 1, 2]];
let normals = compute_vertex_normals_offset(&v, &f);
assert_eq!(normals.len(), 3);
for n in &normals {
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
assert!((len - 1.0).abs() < 1e-5, "len={len}");
}
}
#[test]
fn test_shell_mesh_vertex_count() {
let v = triangle_verts();
let f = vec![[0u32, 1, 2]];
let (sv, _sf) = shell_mesh(&v, &f, 0.1);
assert_eq!(sv.len(), 2 * v.len());
}
#[test]
fn test_shell_mesh_valid_indices() {
let v = triangle_verts();
let f = vec![[0u32, 1, 2]];
let (sv, sf) = shell_mesh(&v, &f, 0.1);
let nv = sv.len() as u32;
for face in &sf {
assert!(face[0] < nv, "idx {} out of range {}", face[0], nv);
assert!(face[1] < nv, "idx {} out of range {}", face[1], nv);
assert!(face[2] < nv, "idx {} out of range {}", face[2], nv);
}
}
#[test]
fn test_offset_surface_inward() {
let v = triangle_verts();
let n = flat_normals();
let mut cfg = default_offset_surface_config();
cfg.inward = true;
cfg.offset_distance = 0.5;
let result = offset_surface(&v, &n, &cfg);
for p in &result.vertices {
assert!((p[2] + 0.5).abs() < 1e-5, "z={}", p[2]);
}
}
}