This guide explains how the 3D collision detection system works, using a Bounding Volume Hierarchy (BVH) for efficient spatial partitioning and physics-based collision resolution.
The collision system consists of several key components working together:
The AABB is the fundamental collision primitive - a rectangular box aligned with world axes.
#[derive(Debug, Clone)]
pub struct AABB {
pub min: [f32; 3],
pub max: [f32; 3],
}
impl AABB {
pub fn new(min: [f32; 3], max: [f32; 3]) -> Self {
Self { min, max }
}
pub fn from_wall_face(p1: [f32; 3], p2: [f32; 3], p3: [f32; 3], p4: [f32; 3]) -> Self {
let min_x = p1[0].min(p2[0]).min(p3[0]).min(p4[0]);
let min_y = p1[1].min(p2[1]).min(p3[1]).min(p4[1]);
let min_z = p1[2].min(p2[2]).min(p3[2]).min(p4[2]);
let max_x = p1[0].max(p2[0]).max(p3[0]).max(p4[0]);
let max_y = p1[1].max(p2[1]).max(p3[1]).max(p4[1]);
let max_z = p1[2].max(p2[2]).max(p3[2]).max(p4[2]);
Self::new([min_x, min_y, min_z], [max_x, max_y, max_z])
}
pub fn intersects(&self, other: &AABB) -> bool {
for i in 0..3 {
if self.max[i] < other.min[i] || self.min[i] > other.max[i] {
return false;
}
}
true
}
pub fn center(&self) -> [f32; 3] {
[
(self.min[0] + self.max[0]) * 0.5,
(self.min[1] + self.max[1]) * 0.5,
(self.min[2] + self.max[2]) * 0.5,
]
}
}
Key Functions:
intersects()
- Fast AABB-AABB collision test using separating axis theoremfrom_wall_face()
- Creates bounding box from four corner pointscenter()
- Used for spatial sorting and distance calculationsWall faces are quadrilateral surfaces with orientation information.
#[derive(Debug, Clone)]
pub struct WallFace {
pub corners: [[f32; 3]; 4],
pub normal: [f32; 3],
pub aabb: AABB,
}
impl WallFace {
pub fn new(corners: [[f32; 3]; 4]) -> Self {
let normal = Self::calculate_normal(&corners);
let aabb = AABB::from_wall_face(corners[0], corners[1], corners[2], corners[3]);
Self {
corners,
normal,
aabb,
}
}
fn calculate_normal(corners: &[[f32; 3]; 4]) -> [f32; 3] {
let u = [
corners[1][0] - corners[0][0],
corners[1][1] - corners[0][1],
corners[1][2] - corners[0][2],
];
let v = [
corners[2][0] - corners[0][0],
corners[2][1] - corners[0][1],
corners[2][2] - corners[0][2],
];
let normal = [
v[1] * u[2] - v[2] * u[1],
v[2] * u[0] - v[0] * u[2],
v[0] * u[1] - v[1] * u[0],
];
let len = (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
[normal[0] / len, normal[1] / len, normal[2] / len]
}
}
Key Features:
The BVH is a binary tree that spatially partitions wall faces for efficient collision queries.
#[derive(Debug, Clone)]
pub enum BVHNode {
Leaf {
aabb: AABB,
faces: Vec<WallFace>,
},
Internal {
aabb: AABB,
left: Box<BVHNode>,
right: Box<BVHNode>,
},
}
#[derive(Debug, Default, Clone)]
pub struct BVH {
pub root: Option<BVHNode>,
}
impl BVH {
pub fn build(&mut self, faces: Vec<WallFace>) {
if faces.is_empty() {
self.root = None;
return;
}
self.root = Some(Self::build_recursive(faces));
}
fn build_recursive(mut faces: Vec<WallFace>) -> BVHNode {
if faces.len() <= 4 {
let mut aabb = faces[0].aabb.clone();
for face in faces.iter().skip(1) {
aabb.expand(&face.aabb);
}
return BVHNode::Leaf { aabb, faces };
}
let (split_axis, _split_pos) = Self::find_best_split(&faces);
faces.sort_by(|a, b| {
let a_center = a.aabb.center()[split_axis];
let b_center = b.aabb.center()[split_axis];
a_center.partial_cmp(&b_center).unwrap()
});
let mid = faces.len() / 2;
let left_faces = faces.drain(..mid).collect();
let right_faces = faces;
let left_child = Self::build_recursive(left_faces);
let right_child = Self::build_recursive(right_faces);
let mut aabb = left_child.aabb().clone();
aabb.expand(right_child.aabb());
BVHNode::Internal {
aabb,
left: Box::new(left_child),
right: Box::new(right_child),
}
}
}
BVH Construction Process:
The BVH enables fast collision queries by pruning branches that can’t contain collisions.
impl BVH {
pub fn query_collisions(&self, player_aabb: &AABB) -> Vec<&WallFace> {
let mut results = Vec::new();
if let Some(ref root) = self.root {
Self::query_recursive(root, player_aabb, &mut results);
}
results
}
fn query_recursive<'a>(node: &'a BVHNode, query_aabb: &AABB, results: &mut Vec<&'a WallFace>) {
if !node.aabb().intersects(query_aabb) {
return;
}
match node {
BVHNode::Leaf { faces, .. } => {
for face in faces {
if face.aabb.intersects(query_aabb) {
results.push(face);
}
}
}
BVHNode::Internal { left, right, .. } => {
Self::query_recursive(left, query_aabb, results);
Self::query_recursive(right, query_aabb, results);
}
}
}
}
Query Algorithm:
The high-level collision system manages the entire process.
#[derive(Debug, Default, Clone)]
pub struct CollisionSystem {
pub bvh: BVH,
pub player_radius: f32,
pub player_height: f32,
}
impl CollisionSystem {
pub fn new(player_radius: f32, player_height: f32) -> Self {
Self {
bvh: BVH::new(),
player_radius,
player_height,
}
}
pub fn build_from_maze(&mut self, maze_grid: &[Vec<bool>]) {
let wall_faces = self.extract_wall_faces_from_maze(maze_grid);
self.bvh.build(wall_faces);
}
pub fn check_and_resolve_collision(
&self,
current_pos: [f32; 3],
desired_pos: [f32; 3],
) -> [f32; 3] {
let player_aabb = AABB::new(
[
desired_pos[0] - self.player_radius,
desired_pos[1],
desired_pos[2] - self.player_radius,
],
[
desired_pos[0] + self.player_radius,
desired_pos[1] + self.player_height,
desired_pos[2] + self.player_radius,
],
);
let potential_collisions = self.bvh.query_collisions(&player_aabb);
if potential_collisions.is_empty() {
return desired_pos;
}
let mut resolved_pos = desired_pos;
let max_iterations = 5;
for _ in 0..max_iterations {
let player_aabb = AABB::new(
[
resolved_pos[0] - self.player_radius,
resolved_pos[1],
resolved_pos[2] - self.player_radius,
],
[
resolved_pos[0] + self.player_radius,
resolved_pos[1] + self.player_height,
resolved_pos[2] + self.player_radius,
],
);
let potential_collisions = self.bvh.query_collisions(&player_aabb);
if potential_collisions.is_empty() {
break;
}
let mut closest_face = &potential_collisions[0];
let mut closest_distance = f32::MAX;
for face in &potential_collisions {
let face_center = face.aabb.center();
let distance = ((face_center[0] - resolved_pos[0]).powi(2)
+ (face_center[1] - resolved_pos[1]).powi(2)
+ (face_center[2] - resolved_pos[2]).powi(2))
.sqrt();
if distance < closest_distance {
closest_distance = distance;
closest_face = face;
}
}
let movement = [
resolved_pos[0] - current_pos[0],
resolved_pos[1] - current_pos[1],
resolved_pos[2] - current_pos[2],
];
resolved_pos =
self.resolve_wall_collision(current_pos, resolved_pos, movement, closest_face);
let epsilon = 0.0001;
let position_changed = (resolved_pos[0] - current_pos[0]).abs() > epsilon
|| (resolved_pos[1] - current_pos[1]).abs() > epsilon
|| (resolved_pos[2] - current_pos[2]).abs() > epsilon;
if !position_changed {
break;
}
}
resolved_pos
}
}
The collision resolution uses vector projection to enable realistic wall sliding.
Wall Sliding Algorithm:
The player movement system integrates with collision detection.
impl Player {
pub fn move_with_collision(
&mut self,
collision_system: &CollisionSystem,
delta_time: f32,
forward: bool,
backward: bool,
left: bool,
right: bool,
) {
let current_pos = self.position;
let mut desired_pos = current_pos;
if forward {
let forward_x = self.yaw.to_radians().sin();
let forward_z = self.yaw.to_radians().cos();
desired_pos[0] -= forward_x * self.speed * delta_time;
desired_pos[2] -= forward_z * self.speed * delta_time;
}
if backward {
let forward_x = self.yaw.to_radians().sin();
let forward_z = self.yaw.to_radians().cos();
desired_pos[0] += forward_x * self.speed * delta_time;
desired_pos[2] += forward_z * self.speed * delta_time;
}
if left {
let right_x = self.yaw.to_radians().cos();
let right_z = self.yaw.to_radians().sin();
desired_pos[0] -= right_x * self.speed * delta_time;
desired_pos[2] += right_z * self.speed * delta_time;
}
if right {
let right_x = self.yaw.to_radians().cos();
let right_z = self.yaw.to_radians().sin();
desired_pos[0] += right_x * self.speed * delta_time;
desired_pos[2] -= right_z * self.speed * delta_time;
}
self.position = collision_system.check_and_resolve_collision(current_pos, desired_pos);
}
}
This system provides efficient, realistic collision detection with smooth wall sliding for 3D maze navigation.