Collision
Detection
in A-Frame
How Stopping the camera from walking through walls — using raycasters, snap-back logic, and boundary clamping.
For the following Art Gallery Immersive Website
What is Collision?
In 3D virtual environments, cameras can pass through walls by default. Collision detection prevents this — it checks if the camera is too close to a surface and stops it from continuing.
The Wall Problem
A-Frame's camera has no physical body. Without extra code, you can walk straight through any wall in the scene.
Raycasting Solution
Invisible "rays" fire outward in 4 directions. If a ray hits a wall too close, a collision is detected.
Snap-Back Logic
On collision, the camera snaps back to the last safe position — where it was before the collision occurred.
Hard Boundaries
A secondary clamp keeps the camera inside a defined rectangle — a safety net in case raycasting misses anything.
The component is registered as collision-detector and attaches directly to <a-camera>. Every frame it checks whether the camera is too close to a wall and takes corrective action.
The Schema
The schema defines the configurable properties — the "knobs" you can adjust to change how collision behaves.
schema: { distance: { type: 'number', default: 0.8 }, // min metres from wall xLimit: { type: 'number', default: 14.4 }, // hard X boundary zLimit: { type: 'number', default: 19.4 } // hard Z boundary },
| Property | Type | Default | Description |
|---|---|---|---|
| distance | number | 0.8 | Minimum distance in metres between camera and wall before a collision triggers |
| xLimit | number | 14.4 | Camera cannot go past ±14.4 on the X axis (left / right) |
| zLimit | number | 19.4 | Camera cannot go past ±19.4 on the Z axis (forward / backward) |
Why X and Z, not Y? In A-Frame and Three.js, Y is the vertical axis (up/down). Since we walk on a flat floor, only the horizontal axes need constraining — X (left/right) and Z (forward/backward).
The init() Function
init() runs once when the component attaches to the camera. It prepares everything before the main loop begins.
Save the Starting Position
A copy of the camera's position is stored as lastPosition — the safe point to snap back to on collision.
const position = this.el.object3D.position; this.lastPosition = position.clone(); // a copy, not a reference
Create Helper Vectors
Two reusable THREE.Vector3 objects: origin (where rays start) and yAxis (used to rotate direction vectors based on camera facing).
this.origin = new THREE.Vector3(); this.yAxis = new THREE.Vector3(0, 1, 0); // straight up
Set Up 4 Raycasters
One raycaster per direction. Each can fire a ray and report what it hits and how far away it is.
this.raycasters = { forward: new THREE.Raycaster(), backward: new THREE.Raycaster(), left: new THREE.Raycaster(), right: new THREE.Raycaster() };
Forward
Checks for a wall ahead
Backward
Checks for a wall behind
Left
Checks for a wall to the left
Right
Checks for a wall to the right
Collect Wall Objects
querySelectorAll('.wall') finds every element with the CSS class wall. Raycasters only test these objects — ignoring everything else for performance.
const collectWalls = () => { this.wallObjects = Array.from( document.querySelectorAll('.wall') ).map(w => w.object3D); }; if (this.el.sceneEl.hasLoaded) { collectWalls(); } else { this.el.sceneEl.addEventListener('loaded', collectWalls); }
Every wall in your scene must have class="wall". Without it, the raycaster ignores the element and the camera will pass straight through.
The tick() Function
tick() runs on every frame (60+ times per second). This is where real-time collision checking happens.
Calculate Direction Vectors
applyAxisAngle() rotates the base forward vector (0,0,−1) by the camera's current Y-rotation, so "forward" always matches where the camera is facing.
const rotation = this.el.object3D.rotation; const forward = new THREE.Vector3(0,0,-1).applyAxisAngle(this.yAxis, rotation.y); const right = new THREE.Vector3(1,0,0).applyAxisAngle(this.yAxis, rotation.y);
Fire Rays in 4 Directions
Each direction is tested. If any ray hits a wall object closer than this.data.distance, the collision flag is set and the loop stops early.
const directions = [ forward, forward.clone().multiplyScalar(-1), right.clone().multiplyScalar(-1), right ]; let collision = false; for (let dir of directions) { const rc = new THREE.Raycaster(); rc.set(this.origin, dir); const hits = rc.intersectObjects(wallObjects, true); if (hits.length > 0 && hits[0].distance < this.data.distance) { collision = true; break; } }
Snap Back or Save Position
Collision detected → snap back to lastPosition. No collision → save current position as the new safe one.
if (collision) { position.copy(this.lastPosition); // snap back } else { this.lastPosition.copy(position); // update safe position }
Hard Boundary Clamp
As a final fallback, X and Z are clamped so the camera can never leave the defined scene limits.
position.x = THREE.MathUtils.clamp(position.x, -this.data.xLimit, +this.data.xLimit); position.z = THREE.MathUtils.clamp(position.z, -this.data.zLimit, +this.data.zLimit);
Defence in depth. Raycasting is the primary check. Clamping is the backup. Both together make the system robust — fast movement and thin walls won't break the experience.
See it in Action
Move your cursor inside the canvas. The dot is the camera; rays fire outward in 4 directions. Watch collision trigger when the camera gets too close to a wall.
Blue Dot
The camera. Move the cursor to control it.
Blue Beams
The 4 raycasters. They detect wall intersections.
Red State
Camera turns red on collision — snapping back to safety.
Amber Border
The hard clamp boundary — the camera cannot leave this area.
Putting it all Together
Include the JS file, attach the component to <a-camera>, and add class="wall" to every wall you want detected.
📄 HTML — Attaching the Component
Add collision-detector directly to the camera element:
<!-- Camera with collision detection --> <a-camera id="rig" position="0 1.6 15" collision-detector></a-camera> <!-- Every wall must have class="wall" --> <a-box class="wall" position="0 2 -10" width="30" height="4" depth="0.3"></a-box>
⚙️ Customising the Properties
Override defaults directly in the HTML attribute string:
<!-- Stop earlier (1m), for a smaller scene --> <a-camera collision-detector="distance: 1.0; xLimit: 8; zLimit: 10"></a-camera>
✅ Checklist
☑ Include the component JS before the scene
☑ Add collision-detector to <a-camera>
☑ Add class="wall" to every wall
☑ Adjust xLimit / zLimit to match your scene
⚠️ Common Mistakes
✗ Forgetting class="wall" on wall elements
✗ xLimit / zLimit too small (instant collision at start)
✗ distance too large (stops too far from wall surface)
✗ Nested wall objects without the true flag
