Fix OrthographicCamera Zoom with OrbitControls in Three.js

Using an OrthographicCamera alongside OrbitControls in Three.js often results in broken or unresponsive zooming because orthographic projection does not rely on camera distance to scale objects. This article explains why this limitation occurs and provides a direct, step-by-step guide to configuring your camera bounds, zoom limits, and control settings to achieve smooth, predictable orthographic zooming.

The Core Problem: Distance vs. Zoom

In perspective projection, moving the camera physically closer to an object makes it appear larger. OrbitControls exploits this by changing the camera’s position along its look-at vector when you scroll.

However, in parallel (orthographic) projection, moving the camera closer or further away has zero effect on the perceived size of the objects. To zoom with an OrthographicCamera, you must scale the camera’s frustum width and height, or adjust its .zoom property and call .updateProjectionMatrix().

Fortunately, OrbitControls natively supports orthographic zooming, but it requires specific configuration to work correctly.

Step 1: Correctly Initialize the OrthographicCamera

When setting up your camera, you must define the frustum boundaries (left, right, top, bottom) relative to the canvas aspect ratio.

const aspect = window.innerWidth / window.innerHeight;
const frustumSize = 10;

const camera = new THREE.OrthographicCamera(
    (frustumSize * aspect) / -2, // left
    (frustumSize * aspect) / 2,  // right
    frustumSize / 2,             // top
    frustumSize / -2,            // bottom
    0.1,                         // near
    1000                         // far
);

// Position the camera back so it can view the scene
camera.position.set(10, 10, 10);
camera.lookAt(0, 0, 0);

Step 2: Configure OrbitControls for Orthographic Zoom

When you pass an OrthographicCamera to OrbitControls, the controls automatically switch to adjusting the camera’s .zoom property instead of its position.

To limit how far a user can zoom in or out, you must set minZoom and maxZoom on the camera object itself, not the controls. Setting minDistance or maxDistance on the controls will have no effect.

import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // Optional, for smooth movement

// Set zoom limits directly on the camera
camera.minZoom = 0.5; // Prevent zooming out too far
camera.maxZoom = 5;   // Prevent zooming in too far

Step 3: Handle Window Resizing Properly

Because orthographic cameras rely on manual frustum boundaries, resizing the browser window will distort your scene unless you update the camera’s left, right, top, and bottom planes, followed by a projection matrix update.

window.addEventListener('resize', () => {
    const aspect = window.innerWidth / window.innerHeight;

    camera.left = (frustumSize * aspect) / -2;
    camera.right = (frustumSize * aspect) / 2;
    camera.top = frustumSize / 2;
    camera.bottom = frustumSize / -2;

    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
});

Step 4: Update the Controls in the Animation Loop

If you have enabled damping (enableDamping = true) or need the controls to smoothly interpolate camera adjustments, you must call controls.update() in your requestAnimationFrame loop.

function animate() {
    requestAnimationFrame(animate);

    // Required if controls.enableDamping or controls.autoRotate are set to true
    controls.update(); 

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