use std::boxed::Box;
use itertools::Itertools;
use ordered_float::NotNan;
use rayon::prelude::*;
mod geo;
mod grid;
mod point;
pub use grid::{Grid, SnapResult};
pub use point::Point;
pub enum Topology<'a, I>
where
I: Into<u32>,
{
TriangleList(Option<&'a [I]>),
TriangleStrip(Option<&'a [I]>),
}
impl<'a, I> Topology<'a, I>
where
I: Into<u32>,
{
fn get_triangles<V>(
vertices: &[V],
indices: &'a Topology<I>,
) -> Box<dyn Iterator<Item = (usize, usize, usize)> + 'a>
where
V: Point,
I: Copy + Into<u32> + Sync + Send,
{
match indices {
Topology::TriangleList(Some(indices)) => {
Box::new(indices.iter().map(|x| (*x).into() as usize).tuples())
}
Topology::TriangleList(None) => Box::new((0..vertices.len()).tuples()),
Topology::TriangleStrip(Some(indices)) => {
Box::new(indices.iter().map(|x| (*x).into() as usize).tuple_windows())
}
Topology::TriangleStrip(None) => Box::new((0..vertices.len()).tuple_windows()),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum SignMethod {
#[default]
Raycast,
Normal,
}
fn compare_distances(a: f32, b: f32) -> std::cmp::Ordering {
if float_cmp::approx_eq!(f32, a.abs(), b.abs(), ulps = 2, epsilon = 1e-6) {
match (a.is_sign_negative(), b.is_sign_negative()) {
(true, false) => std::cmp::Ordering::Greater,
(false, true) => std::cmp::Ordering::Less,
_ => a.abs().partial_cmp(&b.abs()).unwrap(),
}
} else {
a.abs().partial_cmp(&b.abs()).expect("NaN distance")
}
}
pub fn generate_sdf<V, I>(
vertices: &[V],
indices: Topology<I>,
query_points: &[V],
sign_method: SignMethod,
) -> Vec<f32>
where
V: Point,
I: Copy + Into<u32> + Sync + Send,
{
query_points
.par_iter()
.map(|query| {
Topology::get_triangles(vertices, &indices)
.map(|(i, j, k)| (&vertices[i], &vertices[j], &vertices[k]))
.map(|(a, b, c)| match sign_method {
SignMethod::Raycast => (
geo::point_triangle_distance(query, a, b, c),
geo::ray_triangle_intersection_aligned(query, [a, b, c], geo::GridAlign::X)
.is_some(),
),
SignMethod::Normal => {
(geo::point_triangle_signed_distance(query, a, b, c), false)
}
})
.fold(
(f32::MAX, 0),
|(min_distance, intersection_count), (distance, ray_intersection)| {
match sign_method {
SignMethod::Raycast => (
min_distance.min(distance),
intersection_count + ray_intersection as u32,
),
SignMethod::Normal => (
match compare_distances(distance, min_distance) {
std::cmp::Ordering::Less => distance,
_ => min_distance,
},
intersection_count,
),
}
},
)
})
.map(|(distance, intersection_count)| {
if intersection_count % 2 == 0 {
distance
} else {
-distance
}
})
.collect()
}
#[derive(Copy, Clone, Eq, PartialEq)]
struct State {
distance: NotNan<f32>,
cell: [usize; 3],
triangle: (usize, usize, usize),
}
impl Ord for State {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
compare_distances(other.distance.into_inner(), self.distance.into_inner())
.then_with(|| self.cell.cmp(&other.cell))
.then_with(|| self.triangle.cmp(&other.triangle))
}
}
impl PartialOrd for State {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
pub fn generate_grid_sdf<V, I>(
vertices: &[V],
indices: Topology<I>,
grid: &Grid<V>,
sign_method: SignMethod,
) -> Vec<f32>
where
V: Point,
I: Copy + Into<u32> + Sync + Send,
{
let mut distances = vec![f32::MAX; grid.get_total_cell_count()];
let mut heap = std::collections::BinaryHeap::new();
let mut steps = 0;
Topology::get_triangles(vertices, &indices).for_each(|triangle| {
let a = &vertices[triangle.0];
let b = &vertices[triangle.1];
let c = &vertices[triangle.2];
let bounding_box = geo::triangle_bounding_box(a, b, c);
let min_cell = match grid.snap_point_to_grid(&bounding_box.0) {
SnapResult::Inside(cell) | SnapResult::Outside(cell) => cell,
};
let max_cell = match grid.snap_point_to_grid(&bounding_box.1) {
SnapResult::Inside(cell) | SnapResult::Outside(cell) => cell,
};
let min_cell = [
if min_cell[0] == 0 { 0 } else { min_cell[0] - 1 },
if min_cell[1] == 0 { 0 } else { min_cell[1] - 1 },
if min_cell[2] == 0 { 0 } else { min_cell[2] - 1 },
];
let max_cell = [
(max_cell[0] + 1).min(grid.get_cell_count()[0] - 1),
(max_cell[1] + 1).min(grid.get_cell_count()[1] - 1),
(max_cell[2] + 1).min(grid.get_cell_count()[2] - 1),
];
for cell in itertools::iproduct!(
min_cell[0]..=max_cell[0],
min_cell[1]..=max_cell[1],
min_cell[2]..=max_cell[2]
) {
let cell = [cell.0, cell.1, cell.2];
let cell_idx = grid.get_cell_idx(&cell);
let cell_pos = grid.get_cell_center(&cell);
let distance = match sign_method {
SignMethod::Raycast => geo::point_triangle_distance(&cell_pos, a, b, c),
SignMethod::Normal => geo::point_triangle_signed_distance(&cell_pos, a, b, c),
};
if compare_distances(distance, distances[cell_idx]).is_lt() {
steps += 1;
distances[cell_idx] = distance;
let state = State {
distance: NotNan::new(distance).unwrap(), triangle,
cell,
};
heap.push(state);
}
}
});
log::info!("[generate_grid_sdf] init steps: {}", steps);
steps = 0;
while let Some(State { triangle, cell, .. }) = heap.pop() {
let a = &vertices[triangle.0];
let b = &vertices[triangle.1];
let c = &vertices[triangle.2];
let neighbours = itertools::iproduct!(-1..=1, -1..=1, -1..=1)
.map(|v| {
(
cell[0] as isize + v.0,
cell[1] as isize + v.1,
cell[2] as isize + v.2,
)
})
.filter(|&(x, y, z)| {
x >= 0
&& y >= 0
&& z >= 0
&& x < grid.get_cell_count()[0] as isize
&& y < grid.get_cell_count()[1] as isize
&& z < grid.get_cell_count()[2] as isize
})
.map(|(x, y, z)| [x as usize, y as usize, z as usize]);
for neighbour_cell in neighbours {
let neighbour_cell_pos = grid.get_cell_center(&neighbour_cell);
let neighbour_cell_idx = grid.get_cell_idx(&neighbour_cell);
let distance = match sign_method {
SignMethod::Raycast => geo::point_triangle_distance(&neighbour_cell_pos, a, b, c),
SignMethod::Normal => {
geo::point_triangle_signed_distance(&neighbour_cell_pos, a, b, c)
}
};
if compare_distances(distance, distances[neighbour_cell_idx]).is_lt() {
steps += 1;
distances[neighbour_cell_idx] = distance;
let state = State {
distance: NotNan::new(distance).unwrap(), triangle,
cell: neighbour_cell,
};
heap.push(state);
}
}
}
log::info!("[generate_grid_sdf] propagation steps: {}", steps);
if sign_method == SignMethod::Raycast {
let mut intersections = vec![[0, 0, 0]; grid.get_total_cell_count()];
let mut raycasts_done = 0;
for triangle in Topology::get_triangles(vertices, &indices) {
let a = &vertices[triangle.0];
let b = &vertices[triangle.1];
let c = &vertices[triangle.2];
let bounding_box = geo::triangle_bounding_box(a, b, c);
let min_cell = match grid.snap_point_to_grid(&bounding_box.0) {
SnapResult::Inside(cell) | SnapResult::Outside(cell) => cell,
};
let max_cell = match grid.snap_point_to_grid(&bounding_box.1) {
SnapResult::Inside(cell) | SnapResult::Outside(cell) => cell,
};
for y in min_cell[1]..=max_cell[1] {
for z in min_cell[2]..=max_cell[2] {
let cell = [0, y, z];
let cell_pos = grid.get_cell_center(&cell);
raycasts_done += 1;
if let Some(distance) = geo::ray_triangle_intersection_aligned(
&cell_pos,
[a, b, c],
geo::GridAlign::X,
) {
let cell_count = distance / grid.get_cell_size().x();
let cell_count = cell_count.floor() as usize;
for x in 0..=cell_count {
let cell = [x, y, z];
let cell_idx = grid.get_cell_idx(&cell);
intersections[cell_idx][0] += 1;
}
}
}
}
for x in min_cell[0]..=max_cell[0] {
for z in min_cell[2]..=max_cell[2] {
let cell = [x, 0, z];
let cell_pos = grid.get_cell_center(&cell);
raycasts_done += 1;
if let Some(distance) = geo::ray_triangle_intersection_aligned(
&cell_pos,
[a, b, c],
geo::GridAlign::Y,
) {
let cell_count = distance / grid.get_cell_size().y();
let cell_count = cell_count.floor() as usize;
for y in 0..=cell_count {
let cell = [x, y, z];
let cell_idx = grid.get_cell_idx(&cell);
intersections[cell_idx][1] += 1;
}
}
}
}
for x in min_cell[0]..=max_cell[0] {
for y in min_cell[1]..=max_cell[1] {
let cell = [x, y, 0];
let cell_pos = grid.get_cell_center(&cell);
raycasts_done += 1;
if let Some(distance) = geo::ray_triangle_intersection_aligned(
&cell_pos,
[a, b, c],
geo::GridAlign::Z,
) {
let cell_count = distance / grid.get_cell_size().z();
let cell_count = cell_count.floor() as usize;
for z in 0..=cell_count {
let cell = [x, y, z];
let cell_idx = grid.get_cell_idx(&cell);
intersections[cell_idx][2] += 1;
}
}
}
}
}
for (i, distance) in distances.iter_mut().enumerate() {
let inter = intersections[i];
match (inter[0] % 2, inter[1] % 2, inter[2] % 2) {
(1, 1, _) | (1, _, 1) | (_, 1, 1) => *distance = -*distance,
_ => {}
}
}
log::info!("[generate_grid_sdf] raycasts done: {}", raycasts_done);
}
distances
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate() {
let model = &easy_gltf::load("assets/suzanne.glb").unwrap()[0].models[0];
let vertices = model.vertices().iter().map(|v| v.position).collect_vec();
let indices = model.indices().unwrap();
let query_points = [
cgmath::Vector3::new(0.0, 0.0, 0.0),
cgmath::Vector3::new(1.0, 1.0, 1.0),
cgmath::Vector3::new(0.1, 0.2, 0.2),
];
let sdf = generate_sdf(
&vertices,
crate::Topology::TriangleList(Some(indices)),
&query_points,
SignMethod::Normal,
);
let baseline = [-0.42, 0.69, -0.46];
for (sdf, baseline) in sdf.iter().zip(baseline.iter()) {
assert!((sdf - baseline).abs() < 0.1);
}
}
#[test]
fn test_generate_grid() {
let vertices: Vec<[f32; 3]> = vec![[0., 1., 0.], [1., 2., 3.], [1., 3., 4.], [2., 0., 0.]];
let indices: Vec<u32> = vec![0, 1, 2, 1, 2, 3];
let grid = Grid::from_bounding_box(&[0., 0., 0.], &[5., 5., 5.], [5, 5, 5]);
let mut query_points = Vec::new();
for x in 0..grid.get_cell_count()[0] {
for y in 0..grid.get_cell_count()[1] {
for z in 0..grid.get_cell_count()[2] {
query_points.push(grid.get_cell_center(&[x, y, z]));
}
}
}
let sdf = generate_sdf(
&vertices,
crate::Topology::TriangleList(Some(&indices)),
&query_points,
SignMethod::Raycast,
);
let grid_sdf = generate_grid_sdf(
&vertices,
crate::Topology::TriangleList(Some(&indices)),
&grid,
SignMethod::Raycast,
);
for (i, (sdf, grid_sdf)) in sdf.iter().zip(grid_sdf.iter()).enumerate() {
assert_eq!(sdf, grid_sdf, "i: {}", i);
}
}
#[test]
fn test_grid_continuity() {
let model = &easy_gltf::load("assets/ferris3d.glb").unwrap()[0].models[0];
let vertices = model.vertices().iter().map(|v| v.position).collect_vec();
let indices = model.indices().unwrap();
let bbox_min = vertices.iter().fold(
cgmath::Vector3::new(f32::MAX, f32::MAX, f32::MAX),
|acc, v| cgmath::Vector3 {
x: acc.x.min(v.x),
y: acc.y.min(v.y),
z: acc.z.min(v.z),
},
);
let bbox_max = vertices.iter().fold(
cgmath::Vector3::new(-f32::MAX, -f32::MAX, -f32::MAX),
|acc, v| cgmath::Vector3 {
x: acc.x.max(v.x),
y: acc.y.max(v.y),
z: acc.z.max(v.z),
},
);
let extend = 0.2 * (bbox_max - bbox_min);
let bbox_min = bbox_min - extend;
let bbox_max = bbox_max + extend;
let grid = Grid::from_bounding_box(&bbox_min, &bbox_max, [32, 32, 32]);
let sdf = generate_grid_sdf(
&vertices,
crate::Topology::TriangleList(Some(&indices)),
&grid,
SignMethod::Raycast,
);
let test_continuity = |distance: f32, neigh_distance: f32, size: f32| {
let valid_unsigned = (distance.abs() - neigh_distance.abs()).abs() <= size;
let valid_signed = match (distance * neigh_distance).signum() < 0.0 {
true => distance.abs() <= size && neigh_distance.abs() <= size,
false => true,
};
assert!(
valid_unsigned && valid_signed,
"({} {}) <= {}",
distance,
neigh_distance,
size
);
};
let cell_size = grid.get_cell_size();
for x in 0..grid.get_cell_count()[0] - 1 {
for y in 0..grid.get_cell_count()[1] - 1 {
for z in 0..grid.get_cell_count()[2] - 1 {
let index = grid.get_cell_idx(&[x, y, z]);
let distance = sdf[index];
let neigh = grid.get_cell_idx(&[x + 1, y, z]);
let neigh_distance = sdf[neigh];
test_continuity(distance, neigh_distance, cell_size.x());
let neigh = grid.get_cell_idx(&[x, y + 1, z]);
let neigh_distance = sdf[neigh];
test_continuity(distance, neigh_distance, cell_size.y());
let neigh = grid.get_cell_idx(&[x, y, z + 1]);
let neigh_distance = sdf[neigh];
test_continuity(distance, neigh_distance, cell_size.z());
}
}
}
}
#[test]
fn test_topology() {
let grid = Grid::from_bounding_box(&[0., 0., 0.], &[5., 5., 5.], [25, 25, 25]);
let v0 = [0., 1., 0.];
let v1 = [1., 2., 3.];
let v2 = [1., 3., 4.];
let v3 = [2., 0., 0.];
let triangle_list_indices = {
let vertices: Vec<[f32; 3]> = vec![v0, v1, v2, v3];
let indices: Vec<u32> = vec![0, 1, 2, 1, 2, 3, 2, 3, 0];
generate_grid_sdf(
&vertices,
crate::Topology::TriangleList(Some(&indices)),
&grid,
SignMethod::Normal,
)
};
let triangle_list_none = {
let vertices: Vec<[f32; 3]> = vec![v0, v1, v2, v1, v2, v3, v2, v3, v0];
generate_grid_sdf(
&vertices,
Topology::TriangleList::<u32>(None),
&grid,
SignMethod::Normal,
)
};
let triangle_strip_indices = {
let vertices: Vec<[f32; 3]> = vec![v0, v1, v2, v3];
let indices: Vec<u32> = vec![0, 1, 2, 3, 0];
generate_grid_sdf(
&vertices,
Topology::TriangleStrip(Some(&indices)),
&grid,
SignMethod::Normal,
)
};
let triangle_strip_none = {
let vertices: Vec<[f32; 3]> = vec![v0, v1, v2, v3, v0];
generate_grid_sdf(
&vertices,
Topology::TriangleStrip::<u32>(None),
&grid,
SignMethod::Normal,
)
};
let cell_count = grid.get_total_cell_count();
for i in 0..cell_count {
assert_eq!(triangle_list_indices[i], triangle_list_none[i]);
assert_eq!(triangle_list_indices[i], triangle_strip_indices[i]);
assert_eq!(triangle_list_indices[i], triangle_strip_none[i]);
}
}
}