Table of Contents
Rectangular Prisms
Y-Axis Detection
Z-Axis Detection
X-Axis Detection
Conclusion
Christopher Lis
Christopher Lis
Last updated

Senior Frontend Engineer

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:

Box and center position

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:

  1. Top
  2. Bottom
  3. Front
  4. Back
  5. Left
  6. 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:

The top of a box

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:

Bottom of box two
Hey look... box2! I brightened the scene since box2 was hard to see initially.

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).

First collision
Definitely overlapping, therefore, `boxCollision` will return `true`

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.

No overlap, yet collision exists
No overlap, but `boxCollision` returns `true` still... what gives?

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:

z shifted yellow box
`boxCollision` returns `true` in this case, because although the two aren't touching, our code only takes the y-axis into account

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.

z shifted yellow box
`boxCollision` returns `false` now, that is correct, yay!

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:

Registers as colliding when they actually aren't
Registers as colliding since the y and z axis overlap

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:

Finally, full 3D collision detection
Finally, full 3D collision detection

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

Want to participate?

Create a free Chris Courses account to begin

chris posted 7 months ago
0

Providing the lift to launch your development career

© 2024 Chris Courses. All rights reserved.