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 farStep 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();