use cumulus3d_core::io::PointCloud;
use rayon::prelude::*;
use std::collections::HashMap;
#[derive(Clone, Debug, PartialEq)]
pub struct SmoothParams {
pub iterations: usize,
pub radius_gsd: f64,
pub sigma_range_gsd: f64,
pub normal_deg: f64,
}
impl Default for SmoothParams {
fn default() -> Self {
Self { iterations: 0, radius_gsd: 1.0, sigma_range_gsd: 0.25, normal_deg: 30.0 }
}
}
impl SmoothParams {
pub fn enabled(&self) -> bool {
self.iterations > 0 && self.radius_gsd > 0.0 && self.sigma_range_gsd > 0.0
}
}
#[derive(Default, Clone, Copy)]
struct KeyHasher(u64);
impl std::hash::Hasher for KeyHasher {
fn finish(&self) -> u64 {
self.0
}
fn write(&mut self, bytes: &[u8]) {
for &b in bytes {
self.0 = (self.0 << 8) ^ b as u64;
}
}
fn write_u64(&mut self, v: u64) {
self.0 = v;
}
}
type KeyMap = HashMap<u64, (u32, u32), std::hash::BuildHasherDefault<KeyHasher>>;
fn cell_key(c: [i32; 3]) -> u64 {
let k = ((c[0] as u32 as u64 & 0x1f_ffff) << 42) | ((c[1] as u32 as u64 & 0x1f_ffff) << 21) | (c[2] as u32 as u64 & 0x1f_ffff);
crate::math::mix64(k)
}
struct Grid {
order: Vec<u32>,
pos: Vec<[f64; 3]>,
nrm: Vec<[f64; 3]>,
runs: Vec<([i32; 3], u32, u32)>,
map: KeyMap,
}
impl Grid {
fn new(pos: &[[f32; 3]], nrm: &[[f64; 3]], cell: f64) -> Self {
let cell_of = |x: &[f32; 3]| x.map(|a| (a as f64 / cell).floor() as i32);
let mut keyed: Vec<([i32; 3], u32)> = pos.par_iter().enumerate().map(|(i, x)| (cell_of(x), i as u32)).collect();
keyed.par_sort_unstable();
let mut runs = Vec::new();
let mut s = 0usize;
while s < keyed.len() {
let k = keyed[s].0;
let mut e = s + 1;
while e < keyed.len() && keyed[e].0 == k {
e += 1;
}
runs.push((k, s as u32, e as u32));
s = e;
}
let mut map = KeyMap::with_capacity_and_hasher(runs.len(), Default::default());
for &(c, s, e) in &runs {
map.insert(cell_key(c), (s, e));
}
let order: Vec<u32> = keyed.into_iter().map(|(_, i)| i).collect();
let spos = order.par_iter().map(|&i| pos[i as usize].map(|a| a as f64)).collect();
let snrm = order.par_iter().map(|&i| nrm[i as usize]).collect();
Self { order, pos: spos, nrm: snrm, runs, map }
}
}
pub fn smooth_along_normals(cloud: &mut PointCloud, gsd: f64, p: &SmoothParams) -> f64 {
let n = cloud.positions.len();
if !p.enabled() || gsd <= 0.0 || cloud.normals.len() != n || n == 0 {
return 0.0;
}
assert!(n < u32::MAX as usize, "smoothing: too many points for u32 indices");
let r = p.radius_gsd * gsd;
let r2 = r * r;
let inv_2ss = 1.0 / (2.0 * (0.5 * r).powi(2));
let inv_2sr = 1.0 / (2.0 * (p.sigma_range_gsd * gsd).powi(2));
let cos_n = p.normal_deg.to_radians().cos();
let normals: Vec<[f64; 3]> = cloud
.normals
.par_iter()
.map(|m| {
let m = [m[0] as f64, m[1] as f64, m[2] as f64];
let l = (m[0] * m[0] + m[1] * m[1] + m[2] * m[2]).sqrt();
if l > 1e-12 {
m.map(|a| a / l)
} else {
[0.0; 3]
}
})
.collect();
let mut last_disp = 0.0;
for _ in 0..p.iterations {
let g = Grid::new(&cloud.positions, &normals, r);
let shifts: Vec<Vec<f64>> = g
.runs
.par_iter()
.map(|&(c, s, e)| {
let mut nb: Vec<(u32, u32)> = Vec::with_capacity(27);
for dz in -1..=1 {
for dy in -1..=1 {
for dx in -1..=1 {
if let Some(&rg) = g.map.get(&cell_key([c[0] + dx, c[1] + dy, c[2] + dz])) {
nb.push(rg);
}
}
}
}
(s..e)
.map(|i| {
let i = i as usize;
let (pi, ni) = (g.pos[i], g.nrm[i]);
if ni == [0.0; 3] {
return 0.0;
}
let (mut sw, mut swd) = (1.0f64, 0.0f64);
for &(a, b) in &nb {
for j in a as usize..b as usize {
let q = g.pos[j];
let dv = [q[0] - pi[0], q[1] - pi[1], q[2] - pi[2]];
let d2 = dv[0] * dv[0] + dv[1] * dv[1] + dv[2] * dv[2];
if d2 > r2 || j == i {
continue;
}
let nj = g.nrm[j];
if ni[0] * nj[0] + ni[1] * nj[1] + ni[2] * nj[2] < cos_n {
continue;
}
let dn = ni[0] * dv[0] + ni[1] * dv[1] + ni[2] * dv[2];
let w = (-d2 * inv_2ss - dn * dn * inv_2sr).exp();
sw += w;
swd += w * dn;
}
}
swd / sw
})
.collect()
})
.collect();
let mut disp: Vec<f64> = Vec::with_capacity(n);
for (k, t) in shifts.into_iter().flatten().enumerate() {
let (pi, ni) = (g.pos[k], g.nrm[k]);
cloud.positions[g.order[k] as usize] = [(pi[0] + t * ni[0]) as f32, (pi[1] + t * ni[1]) as f32, (pi[2] + t * ni[2]) as f32];
disp.push(t.abs());
}
last_disp = crate::math::median_in_place(&mut disp);
}
last_disp
}
#[cfg(test)]
mod tests {
use super::*;
fn noisy_plane(n: usize, noise: f64) -> PointCloud {
let mut pc = PointCloud::default();
let mut s = 0x1234_5678u64;
let mut rnd = || {
s = crate::math::mix64(s.wrapping_add(0x9e37_79b9_7f4a_7c15));
(s >> 11) as f64 / (1u64 << 53) as f64
};
for _ in 0..n {
let (x, y) = (rnd() * 4.0, rnd() * 4.0);
let z = (rnd() - 0.5) * 2.0 * noise;
pc.positions.push([x as f32, y as f32, z as f32]);
pc.normals.push([0.0, 0.0, 1.0]);
pc.colors.push([0, 0, 0]);
}
pc
}
fn rms_z(pc: &PointCloud) -> f64 {
(pc.positions.iter().map(|p| (p[2] as f64).powi(2)).sum::<f64>() / pc.len() as f64).sqrt()
}
#[test]
fn thins_a_noisy_plane_without_moving_tangentially() {
let mut pc = noisy_plane(20_000, 0.01);
let before = pc.clone();
let p = SmoothParams { iterations: 1, radius_gsd: 4.0, sigma_range_gsd: 1.0, normal_deg: 30.0 };
let d = smooth_along_normals(&mut pc, 0.05, &p);
assert!(d > 0.0);
assert!(rms_z(&pc) < 0.5 * rms_z(&before), "{} vs {}", rms_z(&pc), rms_z(&before));
for (a, b) in pc.positions.iter().zip(&before.positions) {
assert_eq!((a[0], a[1]), (b[0], b[1]));
}
}
#[test]
fn keeps_a_step_and_respects_normals() {
let mut pc = noisy_plane(10_000, 0.0);
let mut upper = noisy_plane(10_000, 0.0);
for q in upper.positions.iter_mut() {
q[2] = 0.5;
}
pc.positions.extend(upper.positions);
pc.normals.extend(upper.normals);
pc.colors.extend(upper.colors);
let p = SmoothParams { iterations: 2, radius_gsd: 12.0, sigma_range_gsd: 0.5, normal_deg: 30.0 };
smooth_along_normals(&mut pc, 0.05, &p);
assert!(pc.positions[..10_000].iter().all(|q| q[2].abs() < 1e-4));
assert!(pc.positions[10_000..].iter().all(|q| (q[2] - 0.5).abs() < 1e-4));
let mut c = noisy_plane(100, 0.01);
let b = c.clone();
assert_eq!(smooth_along_normals(&mut c, 0.05, &SmoothParams::default()), 0.0);
assert_eq!(c.positions, b.positions);
}
}