Cracking the three.js object fitting (to camera) nut
Tl;dr
This article explains both backstory and solution for fitting an object
within a Perspective camera in three.js library. In other words, how far
to pull back a camera on z
axis to get an object displayed as big as possible
without clipping.
Problem statement
Initially I’ve been using viewstl to display the 3D (STL) models on this blog.
But I wasn’t happy with the auto_resize
functionality. So much that I filed
an issue for viewstl. But the
solution offered (to use manual zoom
) also doesn’t fully work for me^{1}.
And because during my research of this problem I stumbled upon Anthony Biondo’s Simple STL Viewer post, I decided to also roll out my own – simpler – STL viewer based on his^{2}. After all, three.js has all the components needed. So all I needed was a bit of a glue.
When further researching the “fit camera to object” problem, I found some discussion on three.js that offered some solutions. But none of the solutions I’ve found really worked well for me.
Alas, I think I have finally cracked the nut. :)
Solution
There are essentially two problems with most of the algorithms I’ve found:
 They ignore the discrepancy between vertical FOV and horizontal FOV with
cameras that have
aspect
ratio different than1.0
 They take max of the three dimensions of the object and compute based on that
Since my simplified STL viewer has a single model always centered on (0, 0, 0)
,
I can basically take a shortcut that allows me to compute optimal fitting:
The z
distance of camera is half of z
size of the bounding box, and then the bigger of:
 distance based on
horizontal FOV
andx
size of bounding box  distance based on
vertical FOV
andy
size of bounding box
Why does that work? Because I need to have the entire front size of the bounding
box (BB) visible. So the half of z
size gets me from center to the front of the
BB, and then a bit of trigonometry gets me to the correct distance given aspect ratio
and size of the side.
It’s sort of hard to explain but much easier to draw:
I also tried documenting it in the code:
const fitCameraToCenteredObject = function (camera, object, offset, orbitControls ) {
const boundingBox = new THREE.Box3();
boundingBox.setFromObject( object );
var middle = new THREE.Vector3();
var size = new THREE.Vector3();
boundingBox.getSize(size);
// figure out how to fit the box in the view:
// 1. figure out horizontal FOV (on non1.0 aspects)
// 2. figure out distance from the object in X and Y planes
// 3. select the max distance (to fit both sides in)
//
// The reason is as follows:
//
// Imagine a bounding box (BB) is centered at (0,0,0).
// Camera has vertical FOV (camera.fov) and horizontal FOV
// (camera.fov scaled by aspect, see fovh below)
//
// Therefore if you want to put the entire object into the field of view,
// you have to compute the distance as: z/2 (half of Z size of the BB
// protruding towards us) plus for both X and Y size of BB you have to
// figure out the distance created by the appropriate FOV.
//
// The FOV is always a triangle:
//
// (size/2)
// ++
//  /
//  /
//  /
//  F° /
//  /
//  /
//  /
// /
//
// F° is half of respective FOV, so to compute the distance (the length
// of the straight line) one has to: `size/2 / Math.tan(F)`.
//
// FTR, from https://threejs.org/docs/#api/en/cameras/PerspectiveCamera
// the camera.fov is the vertical FOV.
const fov = camera.fov * ( Math.PI / 180 );
const fovh = 2*Math.atan(Math.tan(fov/2) * camera.aspect);
let dx = size.z / 2 + Math.abs( size.x / 2 / Math.tan( fovh / 2 ) );
let dy = size.z / 2 + Math.abs( size.y / 2 / Math.tan( fov / 2 ) );
let cameraZ = Math.max(dx, dy);
// offset the camera, if desired (to avoid filling the whole canvas)
if( offset !== undefined && offset !== 0 ) cameraZ *= offset;
camera.position.set( 0, 0, cameraZ );
// set the far plane of the camera so that it easily encompasses the whole object
const minZ = boundingBox.min.z;
const cameraToFarEdge = ( minZ < 0 ) ? minZ + cameraZ : cameraZ  minZ;
camera.far = cameraToFarEdge * 3;
camera.updateProjectionMatrix();
if ( orbitControls !== undefined ) {
// set camera to rotate around the center
orbitControls.target = new THREE.Vector3(0, 0, 0);
// prevent camera from zooming out far enough to create far plane cutoff
orbitControls.maxDistance = cameraToFarEdge * 2;
}
};
With that, the autosizing works well (to my eyes anyway).
If you want to get inspired by the full stlviewer, check out the stlviewerdatatag.js.
Demo
Check out the autopositioning of the same model in 4 different rotations (with bounding box drawn around the object)^{3}:
First:
Second:
Third:
Fourth:

Because mobile version of this site uses different aspect ratio, which always throws the static sizing off on one of them. ↩

And partially on the viewstl lib, because there are some great ideas in the library. Like binding the light to the camera. ↩

I have to admit that the bounding box in the third example looks off, but I’m not enough of a three.js whiz to poke it with a stick to figure out what the problem is. ↩