15. Detecting 3d Collisions
With our transition to 3D complete, we now need to adapt collision detection that works with spheres and arbitrarily oriented boxes in 3D space.
We'll design a procedure to check if a sphere and bounding box are colliding in 3D:
check_sphere_vs_obb3(sphere, obb) -> bool
The Fractal Code Cycle Applied to 3D Collision Detection
Recall our fundamental pattern:
Input → Processing → Output
For 3D collision detection:
- Input: Sphere position and radius, OBB3 position, size, and rotation
- Processing: Transform sphere to OBB's local space, check against axis-aligned box
- Output: Boolean collision result
The Implementation
We can use the same algorithm as in 2D. First we transform the sphere to local space, and construct the local AABB. Then, we use the procedure supplied by raylib.
check_sphere_vs_obb3 :: #force_inline proc(sphere_center: Vec3, sphere_radius: f32, obb: OBB3) -> bool {
rotation_matrix := rl.MatrixRotateXYZ(obb.rotation)
inverse_matrix := rl.MatrixTranspose(rotation_matrix)
local_sphere := sphere_center - obb.center
local_sphere = rl.Vector3Transform(local_sphere, inverse_matrix)
local_aabb := rl.BoundingBox{
min = -obb.size / 2,
max = obb.size / 2,
}
return rl.CheckCollisionBoxSphere(local_aabb, local_sphere, sphere_radius)
}
What's Different in 3D
The core algorithm is the same, but we use 3D-specific procedures:
3D Rotation Matrix: rl.MatrixRotateXYZ handles rotation around all three axes, rather than just one.
3D Vector Transformation: rl.Vector3Transform applies the matrix transformations to 3D vectors, rather than our manual 2D matrix multiplication.
BoundingBox: rl.BoundingBox is raylib's representation of AABB, uses min, max rather than pos, size.
Visual Feedback
In the video, I show how we can visualise each collision on 3 axes, however the simplest form of feedback can be achieved by simply changing the colour that the OBB3 is rendered as:
for obb in gs.physics.static_geometry {
color := rl.GRAY
rotation_matrix := rl.MatrixRotateXYZ(obb.rotation)
inverse_matrix := rl.MatrixTranspose(rotation_matrix)
sphere_center := player.position
sphere_radius := player.collider_radius
local_sphere := sphere_center - obb.center
local_sphere = rl.Vector3Transform(local_sphere, inverse_matrix)
local_aabb := rl.BoundingBox{
min = -obb.size / 2,
max = obb.size / 2,
}
if rl.CheckCollisionBoxSphere(local_aabb, local_sphere, sphere_radius) {
color = rl.YELLOW
// ... More visualisation code
}
draw_obb3(obb, color)
}
Introducing Quaternions
Quaternions are 4D numbers that we can use for rotations.
For our purposes, we can think of them as a black box that holds a rotation.
We can construct one from a matrix, or from an axis + angle.
They can be combined by multiplying them.
We can then extract a matrix or axis + angle to visualise the rotation stored inside.
We could probably write less code and achieve a similar result by extracting values from the matrices, but this method is easier to use and it's just for visualisation code so performance isn't a priority.
// Create Quaternion from our Rotation Matrix
rotation_quat := rl.QuaternionFromMatrix(rotation_matrix)
// XY
// Create local plane
cross_section_xy := Rect{
-obb.size.x / 2,
-obb.size.y / 2,
obb.size.x,
obb.size.y,
}
// That's it - extract the Axis and Angle
xy_axis, xy_angle := rl.QuaternionToAxisAngle(rotation_quat)
// The "other" axis (XY_z_) is our "up" when rotated
xy_up_offset := local_sphere.z
draw_rect_3d(cross_section_xy, obb.center, xy_up_offset, rotation_matrix, {64, 255, 64, 80})
// This procedure requires an Axis and Angle in degrees
rl.DrawCircle3D(sphere_center, sphere_radius, xy_axis, math.to_degrees_f32(xy_angle), rl.GREEN)
// // XZ
cross_section_xz := Rect{
-obb.size.x / 2,
-obb.size.z / 2,
obb.size.x,
obb.size.z,
}
// Rotate around the X axis to get a flat XZ plane
// Imagine standing in front of a whiteboard and pushing the top/pulling the bottom until
// it's a level surface.
xz_quat := rl.QuaternionFromAxisAngle({1, 0, 0}, math.to_radians_f32(-90))
// Combine the rotations
xz_quat_final := rotation_quat * xz_quat
// Extract the Matrix for the plane drawing
xz_rotation_matrix := rl.QuaternionToMatrix(xz_quat_final)
// Extract the Axis and Angle for the circle drawing
xz_axis, xz_angle := rl.QuaternionToAxisAngle(xz_quat_final)
// The "other" axis
xz_up_offset := local_sphere.y
draw_rect_3d(cross_section_xz, obb.center, xz_up_offset, xz_rotation_matrix, {255, 64, 64, 80})
rl.DrawCircle3D(sphere_center, sphere_radius, xz_axis, math.to_degrees_f32(xz_angle), rl.RED)
// ZY
cross_section_zy := Rect{
-obb.size.z / 2,
-obb.size.y / 2,
obb.size.z,
obb.size.y,
}
zy_quat := rl.QuaternionFromAxisAngle({0, 1, 0}, math.to_radians_f32(90))
zy_quat_final := rotation_quat * zy_quat
zy_rotation_matrix := rl.QuaternionToMatrix(zy_quat_final)
zy_axis, zy_angle := rl.QuaternionToAxisAngle(zy_quat_final)
// The "other" axis
zy_up_offset := local_sphere.x
draw_rect_3d(cross_section_zy, obb.center, zy_up_offset, zy_rotation_matrix, {64, 64, 255, 80})
rl.DrawCircle3D(sphere_center, sphere_radius, zy_axis, math.to_degrees_f32(zy_angle), rl.BLUE)
The Big Idea
3D collision detection doesn't require any fundamentally new concepts. Just as before, we build on our 2D principles and transform our problem into something that we know how to solve.