Step-by-Step Implementation

Below are three proven methods for making objects follow each other in Three.js, from simple to advanced. Each builds on the previous concept.

1

Simple Lerp Following

Linear interpolation (lerp) is the most common approach. It smoothly moves the follower a percentage of the remaining distance each frame.

// followSpeed: 0 = no movement, 1 = instant snap
const followSpeed = 0.05;

function animate() {
    // Each frame, move 5% of remaining distance toward target
    follower.position.lerp(target.position, followSpeed);
    renderer.render(scene, camera);
    requestAnimationFrame(animate);
}

Key insight: Lower values (0.02-0.05) create a smooth trailing effect. Higher values (0.1-0.3) feel more responsive but less cinematic.

2

Distance-Based Following

Maintains a fixed distance between follower and target. Perfect for third-person cameras or orbiting objects.

const desiredDistance = 8;
const followSpeed = 0.02;

function animate() {
    // Calculate direction FROM target TO follower
    const direction = new THREE.Vector3();
    direction.subVectors(follower.position, target.position);
    direction.normalize();

    // Place desired position at fixed distance behind target
    const desiredPosition = new THREE.Vector3();
    desiredPosition.copy(target.position);
    desiredPosition.addScaledVector(direction, desiredDistance);

    // Smoothly move toward desired position
    follower.position.lerp(desiredPosition, followSpeed);
    renderer.render(scene, camera);
    requestAnimationFrame(animate);
}

Key insight: The follower orbits at a fixed radius. Change desiredDistance to zoom in/out. This is how most third-person cameras work.

3

Velocity-Based Following

Uses acceleration and damping for physics-like movement. The follower speeds up when far away and decelerates when close — feels natural and responsive.

const maxSpeed = 0.1;
const acceleration = 0.02;
const damping = 0.9;
let velocity = new THREE.Vector3();

function animate() {
    const direction = new THREE.Vector3();
    direction.subVectors(target.position, follower.position);

    if (direction.length() > 0.1) {
        // Accelerate toward target
        direction.normalize();
        velocity.addScaledVector(direction, acceleration);

        // Cap at max speed
        if (velocity.length() > maxSpeed) {
            velocity.normalize().multiplyScalar(maxSpeed);
        }
    } else {
        // Close enough — apply damping to slow down
        velocity.multiplyScalar(damping);
    }

    follower.position.add(velocity);
    renderer.render(scene, camera);
    requestAnimationFrame(animate);
}

Key insight: This is ideal for enemy AI, homing missiles, or any object that needs momentum. Adjust damping (0.8-0.95) to control how quickly it stops.

4

Adding Look-At Rotation

Combine any following method with lookAt() to make the follower face the target. Essential for cameras, turrets, and characters.

function animate() {
    // Any following method...
    follower.position.lerp(target.position, 0.05);

    // Make follower face the target
    follower.lookAt(target.position);

    renderer.render(scene, camera);
    requestAnimationFrame(animate);
}

Code Breakdown

Scene Setup: We create a dark scene with fog, a grid ground plane, and directional lighting with shadows. This gives depth and makes the 3D objects readable.

Target Object (Red Cube): Controlled with WASD keys. This is what the followers chase. In a real game, this would be your player character.

Green Sphere (Lerp): Uses simple position.lerp() — always moves 2% of remaining distance per frame. Creates a smooth, elastic trailing effect.

Blue Cone (Distance): Maintains an 8-unit orbit around the target. Calculates a desired position at fixed distance, then lerps toward it. This is how third-person cameras work.

Yellow Octahedron (Velocity): Accelerates toward the target with a speed cap and damping. Feels like a homing missile — builds momentum and decelerates on approach.

Camera System: The camera itself uses lerp following with an offset vector, creating a smooth cinematic feel as you move the target around.

Performance Tip: Reuse Vector3 objects instead of creating new ones each frame. In the demo we create them inside the loop for clarity, but in production you'd declare them once outside.

Real-World Applications

COMPLETE SOURCE CODE

Copy this code into an HTML file and open it in your browser. Use WASD to move the red cube — watch the three followers react differently.

HTML / Three.js
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>Object Following Demo - Three.js</title>
    <style>body { margin: 0; overflow: hidden; }</style>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/three@latest/build/three.min.js"></script>
<script>
// Scene setup
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
scene.fog = new THREE.Fog(0x1a1a2e, 50, 200);

const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000);
camera.position.set(0, 2.5, 7.5);
const cameraOffset = new THREE.Vector3(0, 2.5, 7.5);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);

// Lighting
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
const sun = new THREE.DirectionalLight(0xffffff, 0.8);
sun.position.set(50, 50, 50);
sun.castShadow = true;
scene.add(sun);

// Ground + Grid
const ground = new THREE.Mesh(
    new THREE.PlaneGeometry(200, 200),
    new THREE.MeshStandardMaterial({ color: 0x2d3436 })
);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
scene.add(new THREE.GridHelper(200, 40, 0x444444, 0x222222));

// Target (red cube) - controlled with WASD
const target = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshStandardMaterial({ color: 0xff6b6b, metalness: 0.5 })
);
target.position.y = 0.5;
target.castShadow = true;
scene.add(target);

// Follower 1: Simple Lerp (green sphere)
const f1 = new THREE.Mesh(
    new THREE.SphereGeometry(0.5, 32, 32),
    new THREE.MeshStandardMaterial({ color: 0x00ff88, metalness: 0.7 })
);
f1.position.set(-5, 0.5, 0);
f1.castShadow = true;
scene.add(f1);

// Follower 2: Distance-Based (blue cone)
const f2 = new THREE.Mesh(
    new THREE.ConeGeometry(0.5, 1, 32),
    new THREE.MeshStandardMaterial({ color: 0x4ecdc4, metalness: 0.7 })
);
f2.position.set(5, 0.5, 0);
f2.castShadow = true;
scene.add(f2);

// Follower 3: Velocity-Based (yellow octahedron)
const f3 = new THREE.Mesh(
    new THREE.OctahedronGeometry(0.5),
    new THREE.MeshStandardMaterial({ color: 0xffd93d, metalness: 0.7 })
);
f3.position.set(0, 0.5, -5);
f3.castShadow = true;
scene.add(f3);
let f3Velocity = new THREE.Vector3();

// Keyboard input
const keys = {};
addEventListener('keydown', e => keys[e.key.toLowerCase()] = true);
addEventListener('keyup', e => keys[e.key.toLowerCase()] = false);

// Animation loop
function animate() {
    requestAnimationFrame(animate);

    // Move target with WASD
    if (keys['w']) target.position.z -= 0.1;
    if (keys['s']) target.position.z += 0.1;
    if (keys['a']) target.position.x -= 0.1;
    if (keys['d']) target.position.x += 0.1;

    // METHOD 1: Simple Lerp (green sphere)
    f1.position.lerp(target.position, 0.02);
    f1.position.y = 0.5; // Keep on ground

    // METHOD 2: Distance-Based (blue cone)
    const dir = new THREE.Vector3()
        .subVectors(f2.position, target.position).normalize();
    const desired = target.position.clone()
        .addScaledVector(dir, 8);
    f2.position.lerp(desired, 0.02);
    f2.position.y = 0.5;

    // METHOD 3: Velocity-Based (yellow octahedron)
    const toTarget = new THREE.Vector3()
        .subVectors(target.position, f3.position);
    if (toTarget.length() > 0.1) {
        toTarget.normalize();
        f3Velocity.addScaledVector(toTarget, 0.02);
        if (f3Velocity.length() > 0.1)
            f3Velocity.normalize().multiplyScalar(0.1);
    } else {
        f3Velocity.multiplyScalar(0.9);
    }
    f3.position.add(f3Velocity);
    f3.position.y = 0.5;

    // Spin followers for visual flair
    f1.rotation.x += 0.01; f1.rotation.y += 0.02;
    f2.rotation.x += 0.01; f2.rotation.y += 0.02;
    f3.rotation.x += 0.01; f3.rotation.y += 0.02;

    // Camera follows target smoothly
    const camTarget = target.position.clone().add(cameraOffset);
    camera.position.lerp(camTarget, 0.05);
    camera.lookAt(target.position);

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

// Handle window resize
addEventListener('resize', () => {
    camera.aspect = innerWidth / innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>