A-Frame · Three.js · WebXR

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

AFRAME.registerComponent THREE.Raycaster tick() loop MathUtils.clamp

01 — The Problem

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.


02 — Configuration

The Schema

The schema defines the configurable properties — the "knobs" you can adjust to change how collision behaves.

schema definition
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
},
PropertyTypeDefaultDescription
distancenumber0.8Minimum distance in metres between camera and wall before a collision triggers
xLimitnumber14.4Camera cannot go past ±14.4 on the X axis (left / right)
zLimitnumber19.4Camera 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).


03 — Setup

The init() Function

init() runs once when the component attaches to the camera. It prepares everything before the main loop begins.

1

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
2

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
3

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

4

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.


04 — Every Frame

The tick() Function

tick() runs on every frame (60+ times per second). This is where real-time collision checking happens.

Get position & rotation Calculate 4 directions Fire 4 raycasts Collision? Snap back / Save Clamp to boundary
1

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

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;
    }
}
3

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
}
4

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.


05 — Interactive Demo

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.

Top-down raycaster simulation Live
Move your cursor (or tap on mobile) to control camera position

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.


06 — How to Use It

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:

index.html
<!-- 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:

custom settings
<!-- 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