[WIP] start critical data structures, NEED TESTS*

- Add missing primitives file
- Begin bounding volume hierarchy code
- Add some untested base functions for octree and morton encoding
- Add interesection tests for box and triangle
This commit is contained in:
Alex Selimov 2025-03-05 09:14:58 -05:00
parent b97c6055a1
commit 209f483992
10 changed files with 236 additions and 3 deletions

View File

@ -5,3 +5,5 @@ edition = "2021"
[dependencies]
anyhow = "*"
vecmath = "*"
itertools = "*"

76
src/bvh.rs Normal file
View File

@ -0,0 +1,76 @@
use vecmath::vec3_add;
use crate::{primitives::Box3, stl::parser::StlSolid};
/// A node representing a node single in the bounding volume hierarchy
#[derive(Default)]
struct BvhNode {
aabb: Box3,
left_child: usize,
right_child: usize,
prim_idx: usize,
prim_count: usize,
}
/// Struct representing the total bounding volume hierarchy
pub struct Bvh {
nodes: Vec<BvhNode>,
}
/// Return type for get_subdivision_position_and_axis
struct SplitPlane {
axis: usize,
pos: f64,
}
impl Bvh {
/// Create a new Bounding volume hierarchy from an STL solid
pub fn new(solid: &StlSolid) -> Self {
// First we need to get all of the triangle centroids
let centroids: Vec<[f32; 3]> = solid
.triangles
.iter()
.map(|tri| {
tri.iter()
.fold([0.0; 3], |acc, idx| vec3_add(solid.vertices[*idx], acc))
})
.collect();
// Initialize the root node
let mut nodes = vec![BvhNode {
prim_idx: 0,
prim_count: solid.triangles.len(),
..Default::default()
}];
nodes[0]
.aabb
.shrink_wrap_primitives(&solid.vertices, &solid.triangles);
let mut bvh = Bvh { nodes };
bvh.subdivide(0);
bvh
}
/// Subdivide a node, subdivision doesn't occur if the node has only 2 primitives
pub fn subdivide(&mut self, node_idx: usize) {
let split_plane = self.get_split_plane(node_idx);
}
/// Calculate the optimal split plane axis and position.
/// This function should use the surface area heuristic to select the optimal split
fn get_split_plane(&self, node_idx: usize) -> SplitPlane {
// Initial approach just splits halfway along the longest axis
// TODO: Improve this to use the surface area heuristic
let axis = (0..3).fold(0, |acc, a| {
if self.nodes[node_idx].aabb.extents[acc] < self.nodes[node_idx].aabb.extents[a] {
a
} else {
acc
}
});
let pos = self.nodes[node_idx].aabb.extents[axis] / 2.0;
SplitPlane { axis, pos }
}
}

View File

@ -1,2 +1,5 @@
pub mod bvh;
pub mod octree;
pub mod primitives;
pub mod stl;
pub mod utilities;

9
src/octree/base.rs Normal file
View File

@ -0,0 +1,9 @@
pub struct Octree {
_octants: Vec<Octant>,
_min_size: f64,
}
pub struct Octant {
_morton_id: usize,
_level: usize,
}

2
src/octree/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod base;
pub mod morton_ids;

17
src/octree/morton_ids.rs Normal file
View File

@ -0,0 +1,17 @@
/// Calculate the morton id in 3d space based on integer position
pub fn encode(mut x: usize, mut y: usize, mut z: usize) {
let mut id = 0;
let mut level = 0;
while x > 0 || y > 0 || z > 0 {
let xbit = x % 2;
let ybit = x % 2;
let zbit = x % 2;
let curr_code = zbit + 2 * ybit + 4 * xbit;
id += curr_code << (3 * level);
x /= 2;
y /= 2;
z /= 2;
level += 1;
}
}

73
src/primitives.rs Normal file
View File

@ -0,0 +1,73 @@
use std::collections::BTreeSet;
use itertools::Itertools;
use vecmath::{vec3_add, vec3_scale};
#[derive(Default)]
pub struct Box3 {
pub center: [f64; 3],
pub extents: [f64; 3],
}
impl Box3 {
/// Create a new box from the bounding values
pub fn new_from_bounds(min_bound: [f32; 3], max_bound: [f32; 3]) -> Self {
let mut center = [0.0; 3];
let mut extents = [0.0; 3];
for (i, (min_val, max_val)) in min_bound.iter().zip(max_bound).enumerate() {
center[i] = ((min_val + max_val) / 2.0) as f64;
extents[i] = (max_val - min_val) as f64;
}
Box3 { center, extents }
}
/// This function returns the 8 vertices of an axis-aligned bounding box.
pub fn get_vertices(&self) -> Vec<[f64; 3]> {
let half_extent = vec3_scale(self.extents, 0.5);
// Define all eight corners relative to the center.
[-half_extent[0], half_extent[0]]
.into_iter()
.cartesian_product([-half_extent[1], half_extent[1]])
.cartesian_product([-half_extent[2], half_extent[2]])
.map(|((x, y), z)| vec3_add(self.center, [x, y, z]))
.collect()
}
/// Expand a box to fit all primitive inside of it.
/// NOTE: vertices does not need to include only vertices being considered. Instead only
/// vertices included as indices in the primitives list are considered
pub fn shrink_wrap_primitives<T>(&mut self, vertices: &[[f32; 3]], primitives: &[T])
where
for<'a> &'a T: IntoIterator<Item = &'a usize>,
{
// If the current primitive list is empty, zero bounding box and exit
if primitives.is_empty() {
self.center = [0.0; 3];
self.extents = [0.0; 3]
}
// Get all of the unique vertex indices so we don't have to check them multiple times
let unique_vertex_idxs: BTreeSet<&usize> = BTreeSet::from_iter(primitives.iter().flatten());
// Now expand the box
let mut min_bd = [f32::INFINITY; 3];
let mut max_bd = [f32::NEG_INFINITY; 3];
for idx in unique_vertex_idxs.into_iter() {
for ((vertex_pos, min_pos), max_pos) in vertices[*idx]
.iter()
.zip(min_bd.iter_mut())
.zip(max_bd.iter_mut())
{
*min_pos = vertex_pos.min(*min_pos);
*max_pos = vertex_pos.max(*max_pos);
}
}
}
}
pub struct Triangle {
pub v1: [f64; 3],
pub v2: [f64; 3],
pub v3: [f64; 3],
}

View File

@ -10,9 +10,9 @@ use std::path::Path;
/// [f32; 3]. Triangles are represented as a [usize; 3] where each entry is an index in the
/// vertices Vec
pub struct StlSolid {
vertices: Vec<[f32; 3]>,
triangles: Vec<[usize; 3]>,
normals: Vec<[f32; 3]>,
pub vertices: Vec<[f32; 3]>,
pub triangles: Vec<[usize; 3]>,
pub normals: Vec<[f32; 3]>,
}
pub fn parse_ascii_stl(file_path: &impl AsRef<Path>) -> Result<StlSolid> {

View File

@ -0,0 +1,50 @@
use core::f64;
use vecmath::{vec3_cross, vec3_dot, vec3_sub};
use crate::primitives::{Box3, Triangle};
/// Determine whether two shapes interesect using the separating axis theorem
pub fn tri_aabb_intersect(bx: &Box3, tri: &Triangle) -> bool {
// Get edge vectors for triangle
let e1 = vec3_sub(tri.v2, tri.v1);
let e2 = vec3_sub(tri.v3, tri.v2);
let e3 = vec3_sub(tri.v1, tri.v3);
// Get face and edge normals
let tri_face = vec3_cross(e1, e2);
let n1 = vec3_cross(e1, e1);
let n2 = vec3_cross(e2, e1);
let n3 = vec3_cross(e3, e1);
// Face normals AABB
let u0 = [1.0, 0.0, 0.0];
let u1 = [0.0, 1.0, 0.0];
let u2 = [0.0, 0.0, 1.0];
// Now check all of the axes
let box_vertices = bx.get_vertices();
for axis in [u0, u1, u2, tri_face, n1, n2, n3] {
let box_projection = project_pnts(&box_vertices, axis);
let tri_projection = project_pnts(&[tri.v1, tri.v2, tri.v3], axis);
let start = f64::max(box_projection.0, tri_projection.0);
let end = f64::min(box_projection.1, tri_projection.1);
if start < end {
return false;
}
}
return true;
}
/// Project points unto an Axis and return the max and min
fn project_pnts(pnts: &[[f64; 3]], axis: [f64; 3]) -> (f64, f64) {
let mut min_proj = f64::INFINITY;
let mut max_proj = f64::NEG_INFINITY;
for pnt in pnts {
let projection = vec3_dot(axis, *pnt);
min_proj = min_proj.min(projection);
max_proj = max_proj.max(projection);
}
(min_proj, max_proj)
}

View File

@ -1,2 +1,3 @@
pub mod intersection_checks;
#[cfg(test)]
pub mod test_macros;