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(); |