Dolly zoom

돌리 줌

Dollying the camera back while narrowing the field of view keeps the subject the same size on screen, but stretches the background around it. Named for its debut in Hitchcock's Vertigo.

Also known as: Vertigo effectZolly shotTrombone shot
···
js
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a12);
const FOV0 = 50;
const camera = new THREE.PerspectiveCamera(FOV0, 1, 0.1, 100);

scene.add(new THREE.AmbientLight(0x556080, 1.4));
const dLight = new THREE.DirectionalLight(0xffffff, 1.3);
dLight.position.set(2, 4, 3);
scene.add(dLight);

const subject = new THREE.Mesh(
  new THREE.CapsuleGeometry(0.5, 1.1, 8, 16),
  new THREE.MeshStandardMaterial({ color: 0xff6a4d, roughness: 0.5 })
);
subject.position.set(0, 0.85, 0);
scene.add(subject);

const corridor = new THREE.Group();
const N = 8;
for (let i = 1; i <= N; i++) {
  const z = -i * 2.4;
  const h = 2.2 + (i % 3) * 0.6;
  const mat = new THREE.MeshStandardMaterial({ color: new THREE.Color().setHSL(0.58, 0.35, 0.22 + i * 0.02), roughness: 0.8 });
  const left = new THREE.Mesh(new THREE.BoxGeometry(0.5, h, 0.5), mat);
  left.position.set(-2.4, h / 2 - 0.4, z);
  corridor.add(left);
  const right = new THREE.Mesh(new THREE.BoxGeometry(0.5, h, 0.5), mat.clone());
  right.position.set(2.4, h / 2 - 0.4, z);
  corridor.add(right);
}
scene.add(corridor);

const floor = new THREE.Mesh(
  new THREE.PlaneGeometry(6, N * 2.4 + 4),
  new THREE.MeshStandardMaterial({ color: 0x14141f, roughness: 1 })
);
floor.rotation.x = -Math.PI / 2;
floor.position.set(0, -0.4, -N * 1.2);
scene.add(floor);

const baseDist = 4.2;
const baseFovRad = THREE.MathUtils.degToRad(FOV0);
const k = baseDist * Math.tan(baseFovRad / 2);

function resize() { renderer.setSize(innerWidth, innerHeight); camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); }
addEventListener('resize', resize); resize();

renderer.setAnimationLoop((t) => {
  const time = t * 0.0006;
  const dist = baseDist + (Math.sin(time) * 0.5 + 0.5) * 6.5;
  const fovRad = 2 * Math.atan(k / dist);
  camera.fov = THREE.MathUtils.radToDeg(fovRad);
  camera.position.set(0, 0.85, dist);
  camera.lookAt(0, 0.85, 0);
  camera.updateProjectionMatrix();
  subject.rotation.y = time * 0.4;
  renderer.render(scene, camera);
});

Move the camera away from a subject and it shrinks on screen; pull the lens in (narrow the field of view) and it grows back. Match the two rates exactly and the subject's size holds steady while the background — scaled by that same narrowing FOV — appears to stretch or rush inward.

The physics: distance × tan(fov/2) needs to stay constant, so the two values move in opposite directions. In three.js that means moving camera.position.z each frame while recomputing camera.fov from that formula, then calling camera.updateProjectionMatrix().

It's used to convey dizziness, dread, or a sense of reality warping. On a real set it takes a physical dolly track to push and pull the camera while zooming — hence "dolly zoom," or "trombone shot" for the push-pull motion.

When to use

Reach for it at a moment of shock or dawning dread — overuse tips it into comedy, so save it for once per scene at most.