How to Code 3D Collision Detection
If you've ever coded 2d collision detection, you may have felt the utmost urge to bash your head against the wall. Don't worry, we've all dealt with it, but if you had trouble with that, just wait 'til you struggle with this!
Presenting: 3D Collision Detection
Rectangular Prisms
Thankfully, coding collision detection with rectangular prisms is much easier than doing so with other 3D geometries.
Let's say we have a box:
In most 3D environments, including Three.js, the default position of a geometry is directly in its center. To start coding collision detection between two boxes, we need not the center of the boxes, but rather, the position of each boxes' face.
A box has six faces:
- Top
- Bottom
- Front
- Back
- Left
- Right
We need to get the position of each of these relative to whatever axis they're on.
Y-Axis Detection
Take the top face for instance, it's facing up on the y-axis, therefore, we need to find the y position of where this face is in 3D space:
We can get this position by taking the center of our box and adding 1/2 of the box's height:
box1.top = box1.position.y + box1.height / 2
Note, the
.height
property is not immediately available from Three.js'Mesh
object. If you want to use this directly like I'm doing here, you need to declare the height property manually. I have a really good video tutorial that shows you how to do this here (free btw).
With the top face taken care of, next we need to determine box2
's bottom position:
This can be done by grabbing the same y-position associated with the box's center, but instead of adding one-half the box's height, we're going to subtract it:
box2.bottom = box2.position.y - box2.height / 2
With the bottom position of the second box and the top position of the first box, we can compare the two values to see if there's an intersection:
box1.top = box1.position.y + box1.height / 2
box2.bottom = box2.position.y - box2.height / 2
function boxCollision({ box1, box2 }) {
return box1.top >= box2.bottom
}
If box1.top
is greater than box2.bottom
(meaning the top side of box1
is higher than the bottom side of box2
), we know that the two boxes collide on the y-axis (at least from the sides that we're currently monitoring for).
This only accounts for one side of each box however. If we were to keep pushing box2
downwards, eventually there would be no overlap, yet our collision detection code would still register as true
.
To fix this, we need to take into account the top side of the yellow box (box2
) and the bottom side of the red box (box1
):
box1.top = box1.position.y + box1.height / 2
box1.bottom = box1.position.y - box1.height / 2
box2.top = box2.position.y + box2.height / 2
box2.bottom = box2.position.y - box2.height / 2
function boxCollision({ box1, box2 }) {
return box1.top >= box2.bottom && box1.bottom <= box2.top
}
Z-Axis Detection
Now, boxCollision
will only return true
if the two are indeed overlapping on the y-axis (yay!). However, we have an issue... boxCollision
still returns true
when a box is shifted over on the x or z axis:
In this case, we need to expand our collision detection function to ensure we're also accounting for the front and back sides of both boxes. First, let's get the front and back positions:
box1.front = box1.position.z + box1.depth / 2
box1.back = box1.position.z - box1.depth / 2
box2.front = box2.position.z + box2.depth / 2
box2.back = box2.position.z - box2.depth / 2
Here, the only differences from the top and bottom positions are that we're utilizing each meshs' z-position rather than y, and we're also referencing depth
instead of height
.
Like
height
,depth
is only available if we set it manually
Now we can integrate this into boxCollision
by expanding our conditional:
// ...
box1.front = box1.position.z + box1.depth / 2
box1.back = box1.position.z - box1.depth / 2
box2.front = box2.position.z + box2.depth / 2
box2.back = box2.position.z - box2.depth / 2
function boxCollision({ box1, box2 }) {
const yCollision = box1.top >= box2.bottom && box1.bottom <= box2.top
const zCollision = box1.front >= box2.back && box1.back <= box2.front
return yCollision && zCollision
}
You'll see I assigned each condition to a constant to better describe what we're testing for in each case. Now we're testing if the two boxes overlap on both the y and z axes, so with the current position of our cubes, boxCollision
will return false
since the two are not overlapping on either of those.
X-Axis Detection
But what if we move our box along the x-axis? Even with our fancy new boxCollision
function, we'd get an incorrect value that states the two boxes are colliding even when they aren't:
As a result, we need to take our third and final dimension into account: the x-axis. As long as we're monitoring for collision between the left and right sides of all boxes, we'll have a trifecta of collision detection statements that give us a robust and working function.
First, let's set our boxes' left and right properties:
box1.left = box1.position.x - box1.width / 2
box1.right = box1.position.x + box1.width / 2
box2.left = box2.position.x - box2.width / 2
box2.right = box2.position.x + box2.width / 2
Note, I'm referencing our position's x value and our boxes' widths. Although our width, height, and depth are all the same, it's important we reference width
here in case we elongate one of our boxes' dimensions.
Now, within our boxCollision
function, we can integrate a collision conditional that tests for whether box1
's right side overlaps with box2
's left side and vice versa:
// ...
box1.left = box1.position.x - box1.width / 2
box1.right = box1.position.x + box1.width / 2
box2.left = box2.position.x - box2.width / 2
box2.right = box2.position.x + box2.width / 2
function boxCollision({ box1, box2 }) {
const xCollision = box1.right >= box2.left && box1.left <= box2.right
const yCollision = box1.top >= box2.bottom && box1.bottom <= box2.top
const zCollision = box1.front >= box2.back && box1.back <= box2.front
return xCollision && yCollision && zCollision
}
This'll fill in the missing piece and allow us detect for collisions in all three dimensions without error:
To use this, we can take our new function, throw it in an animation loop, and test every frame for collisions (then react accordingly):
function animate() {
window.requestAnimationFrame(animate)
if (boxCollision({ box1, box2 })) {
// collision occurred, do something!
}
}
And that's all we need for rectangular prism detection. For a quick reference of all of this article's code, here's a comprehensive code block you can copy and paste as needed:
// Make sure you declare height, width, and depth before assigning these!
box1.top = box1.position.y + box1.height / 2
box1.bottom = box1.position.y - box1.height / 2
box1.front = box1.position.z + box1.depth / 2
box1.back = box1.position.z - box1.depth / 2
box1.left = box1.position.x - box1.width / 2
box1.right = box1.position.x + box1.width / 2
box2.top = box2.position.y + box2.height / 2
box2.bottom = box2.position.y - box2.height / 2
box2.front = box2.position.z + box2.depth / 2
box2.back = box2.position.z - box2.depth / 2
box2.left = box2.position.x - box2.width / 2
box2.right = box2.position.x + box2.width / 2
function boxCollision({ box1, box2 }) {
const xCollision = box1.right >= box2.left && box1.left <= box2.right
const yCollision = box1.top >= box2.bottom && box1.bottom <= box2.top
const zCollision = box1.front >= box2.back && box1.back <= box2.front
return xCollision && yCollision && zCollision
}
function animate() {
window.requestAnimationFrame(animate)
if (boxCollision({ box1, box2 })) {
// collision occurred, do something!
}
}
Conclusion
As I learn more about Three.js and physics, I plan to update this post with more geometries like spheres and complex objects. But that'll be for another time. If you enjoyed this post and want to learn more about Three.js, check out my beginner course where you'll learn how to make a geometric 3D portfolio site from scratch. Otherwise, thanks for reading, and see you in the next one.
Comments work on blog posts now, cool!