Raycaster Collision Detection

Description

While raycasting is almost always used for mouse picking objects in the 3D scene, it can also be used for simple collision detection.

In this example, I detect whether the orbit controls will penetrate another object and adjust the cameras position so that it stays outside.

Essentially, I am creating a ray from the camera target to the camera position. If there is an intersected object between, then the camera position is adjusted to the intersect point. This prevents the camera from going behind a wall, or inside a box, or floor, or any object which is part of the objects array being tested for an intersect.

Code

./src/server/server.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import express from "express"
import path from "path"
import http from "http"

const port: number = 3000

class App {
    private server: http.Server
    private port: number

    constructor(port: number) {
        this.port = port
        const app = express()
        app.use(express.static(path.join(__dirname, '../client')))
        app.use('/build/three.module.js', express.static(path.join(__dirname, '../../node_modules/three/build/three.module.js')))
        app.use('/jsm/controls/OrbitControls', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/controls/OrbitControls.js')))
        app.use('/jsm/libs/stats.module', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/libs/stats.module.js')))

        this.server = new http.Server(app);
    }

    public Start() {
        this.server.listen(this.port, () => {
            console.log( `Server listening on port ${this.port}.` )
        })
    }
}

new App(port).Start()

./src/client/client.ts

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
import * as THREE from '/build/three.module.js'
import { OrbitControls } from '/jsm/controls/OrbitControls'
import Stats from '/jsm/libs/stats.module'

const scene: THREE.Scene = new THREE.Scene()

const camera: THREE.PerspectiveCamera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.z = 2

const renderer: THREE.WebGLRenderer = new THREE.WebGLRenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)

const raycaster = new THREE.Raycaster();
const sceneMeshes = new Array()
let dir = new THREE.Vector3();
let intersects = new Array()

const controls = new OrbitControls(camera, renderer.domElement)
controls.addEventListener('change', function () {

    xLine.position.copy(controls.target)
    yLine.position.copy(controls.target)
    zLine.position.copy(controls.target)

    dir.subVectors(camera.position, controls.target).normalize();

    raycaster.set(controls.target, dir.subVectors(camera.position, controls.target).normalize())
    intersects = raycaster.intersectObjects(sceneMeshes, false);
    if (intersects.length > 0) {
        if (intersects[0].distance < controls.target.distanceTo(camera.position)) {
            camera.position.copy(intersects[0].point)
        }
    }
})


const floor = new THREE.Mesh(new THREE.PlaneBufferGeometry(10, 10), new THREE.MeshNormalMaterial({ side: THREE.DoubleSide }))
floor.rotateX(-Math.PI / 2)
floor.position.y = -1
scene.add(floor)
sceneMeshes.push(floor)

const wall1 = new THREE.Mesh(new THREE.PlaneBufferGeometry(2, 2), new THREE.MeshNormalMaterial({ side: THREE.DoubleSide }))
wall1.position.x = 4
wall1.rotateY(-Math.PI / 2)
scene.add(wall1)
sceneMeshes.push(wall1)

const wall2 = new THREE.Mesh(new THREE.PlaneBufferGeometry(2, 2), new THREE.MeshNormalMaterial({ side: THREE.DoubleSide }))
wall2.position.z = -3
scene.add(wall2)
sceneMeshes.push(wall2)

const cube: THREE.Mesh = new THREE.Mesh(new THREE.BoxBufferGeometry(), new THREE.MeshNormalMaterial())
cube.position.set(-3, 0, 0)
scene.add(cube)
sceneMeshes.push(cube)

const ceiling = new THREE.Mesh(new THREE.PlaneBufferGeometry(10, 10), new THREE.MeshNormalMaterial({ side: THREE.DoubleSide }))
ceiling.rotateX(Math.PI / 2)
ceiling.position.y = 3
scene.add(ceiling)
sceneMeshes.push(ceiling)


//crosshair
const lineMaterial = new THREE.LineBasicMaterial({
    color: 0x0000ff
});
var points = new Array();
points[0] = new THREE.Vector3(-.1, 0, 0);
points[1] = new THREE.Vector3(.1, 0, 0);
let lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
const xLine = new THREE.Line(lineGeometry, lineMaterial);
scene.add(xLine);
points[0] = new THREE.Vector3(0, -.1, 0);
points[1] = new THREE.Vector3(0, .1, 0);
lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
const yLine = new THREE.Line(lineGeometry, lineMaterial);
scene.add(yLine);
points[0] = new THREE.Vector3(0, 0, -.1);
points[1] = new THREE.Vector3(0, 0, .1);
lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
const zLine = new THREE.Line(lineGeometry, lineMaterial);
scene.add(zLine);

window.addEventListener('resize', onWindowResize, false)
function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight
    camera.updateProjectionMatrix()
    renderer.setSize(window.innerWidth, window.innerHeight)
    render()
}

const stats = Stats()
document.body.appendChild(stats.dom)

var animate = function () {
    requestAnimationFrame(animate)

    render()

    stats.update()
};

function render() {
    renderer.render(scene, camera)
}
animate();

Raycaster