Camera Zoom To Fit Object

Yep. Zoom changes effective fov internally:

getEffectiveFOV: function () {
	return _Math.RAD2DEG * 2 * Math.atan(
		Math.tan( _Math.DEG2RAD * 0.5 * this.fov ) / this.zoom );
},

The above formula changes the fov that with zoom = 2 the size that is fit into view ( 2 * b on my image above) will be 2 times smaller.

This works in my case so I found it much easier to use than moving camera (I’m not trying automatically fit objects in camera’s view).

I would say, you should calculate the size of an object and change the fov of the camera based on the size… Just fiddle around with values.

This distorts how objects look, I don’t think it is the desired effect.

I didn’t look at all the math above, but it seems you can just find the height in the camera frustum that matches the height of your object (or width, whichever is bigger). Keep track of the distance that this part of the frustum is from the near plane. Then, you know the objects world transform, so just move the camera there, but displace it back by the distance to the part of the frustum that you found.

Maybe I’m too late, but I have to share the research.

Three.js actually uses a vertical fov.
To correctly calculate the distance from the camera to the object, you need to determine which side of the bounding box we are interested in.

If it is vertical, then we use formula

cameraZ = verticalSize / 2 / Math.tan (fov / 2);

But if it is horizontal, then it is necessary to take into account the aspect ratio of camera

cameraZ = horizontalSize / 2 / Math.tan (fov * camera.aspect / 2);

Hope this will help who will search similar question.

4 Likes

In trying to get this code found up the thread here to work I get 3 errors:

[Warning] THREE.Box3: .getCenter() target is now required (three.js, line 4936)
[Warning] THREE.Box3: .getSize() target is now required (three.js, line 4949)
[Error] TypeError: controls.saveState is not a function. (In ‘controls.saveState()’, ‘controls.saveState’ is undefined)

This is a stretch for me but it would be great if I could find a way to keep my animated model within the camera view.

  function fitCameraToObject( camera, object, offset, controls ) {

	offset = offset || 1.25;

	const boundingBox = new THREE.Box3();

	// get bounding box of object - this will be used to setup controls and camera
	boundingBox.setFromObject( object );
        
            //ERRORS HERE
	const center = boundingBox.getCenter();
	const size = boundingBox.getSize();

	// get the max side of the bounding box (fits to width OR height as needed )
	const maxDim = Math.max( size.x, size.y, size.z );
	const fov = camera.fov * ( Math.PI / 180 );
	cameraZ = Math.abs( maxDim / 2 * Math.tan( fov * 2 ) ); //Applied fifonik correction

	cameraZ *= offset; // zoom out a little so that objects don't fill the screen

	// <--- NEW CODE
	//Method 1 to get object's world position
	scene.updateMatrixWorld(); //Update world positions
	var objectWorldPosition = new THREE.Vector3();
	objectWorldPosition.setFromMatrixPosition( object.matrixWorld );
	
	//Method 2 to get object's world position
	//objectWorldPosition = object.getWorldPosition();

	const directionVector = camera.position.sub(objectWorldPosition); 	//Get vector from camera to object
	const unitDirectionVector = directionVector.normalize(); // Convert to unit vector
	camera.position = unitDirectionVector.multiplyScalar(cameraZ); //Multiply unit vector times cameraZ distance
	camera.lookAt(objectWorldPosition); //Look at object
	// --->

	const minZ = boundingBox.min.z;
	const cameraToFarEdge = ( minZ < 0 ) ? -minZ + cameraZ : cameraZ - minZ;

	camera.far = cameraToFarEdge * 3;
	camera.updateProjectionMatrix();

	if ( controls ) {

	  // set camera to rotate around center of loaded object
	  controls.target = center;

	  // prevent camera from zooming out far enough to create far plane cutoff
	  controls.maxDistance = cameraToFarEdge * 2;
             // ERROR HERE	
	  controls.saveState();

	} else {

		camera.lookAt( center )

   }
}

im far from an expert, so double check but sounds like you need to create the vector first to pass in as the target…

const center = new THREE.Vector3()
boundingBox.getCenter(center)

I believe theres some performance guidelines for constantly creating new vectors so watch out for that.

as for the other error sounds just like the function doesnt exist??have you included it in your code?

1 Like

Could you tell me where “offset” parameter is from?

This worked beautifully for me. I moved my camera to the object’s centroid, and used your vertical formula to move backwards.

1 Like

I tried to make it work but somehow the object stays small most of the time (it is centred though). I reproduced the problem in the JSfiddle: link

In that fiddle it puts a cube on a random spot. When you press the button in the top left corner it should center en zoom to that object. But as you can see it doesn’t do that properly. What am I doing wrong?

Great post. Thanks :+1:

Great post !

It didn’t work for me at first though, so I changed it a bit.
Also make sure that the offset is at the right scale for you… If your object is 500 wide, you will have no great result with an offset of 1.5. Try 50.

function fitCameraToObject( camera, object, offset ) {

offset = offset || 1.5;

const boundingBox = new THREE.Box3();

boundingBox.setFromObject( object );

const center = boundingBox.getCenter( new THREE.Vector3() );
const size = boundingBox.getSize( new THREE.Vector3() );

const startDistance = center.distanceTo(camera.position);
// here we must check if the screen is horizontal or vertical, because camera.fov is
// based on the vertical direction.
const endDistance = camera.aspect > 1 ?
					((size.y/2)+offset) / Math.abs(Math.tan(camera.fov/2)) :
					((size.y/2)+offset) / Math.abs(Math.tan(camera.fov/2)) / camera.aspect ;


camera.position.set(
	camera.position.x * endDistance / startDistance,
	camera.position.y * endDistance / startDistance,
	camera.position.z * endDistance / startDistance,
	);
camera.lookAt(center);

};

I wrote an updated version of this function that takes an array of objects and fits the camera to them. Should fit well to any amount of objects of any size. Aside from that, this maintains the direction of the camera and controls.

Codepen


function fitCameraToSelection( camera, controls, selection, fitOffset = 1.2 ) {
  
  const box = new THREE.Box3();
  
  for( const object of selection ) box.expandByObject( object );
  
  const size = box.getSize( new THREE.Vector3() );
  const center = box.getCenter( new THREE.Vector3() );
  
  const maxSize = Math.max( size.x, size.y, size.z );
  const fitHeightDistance = maxSize / ( 2 * Math.atan( Math.PI * camera.fov / 360 ) );
  const fitWidthDistance = fitHeightDistance / camera.aspect;
  const distance = fitOffset * Math.max( fitHeightDistance, fitWidthDistance );
  
  const direction = controls.target.clone()
    .sub( camera.position )
    .normalize()
    .multiplyScalar( distance );

  controls.maxDistance = distance * 10;
  controls.target.copy( center );
  
  camera.near = distance / 100;
  camera.far = distance * 100;
  camera.updateProjectionMatrix();

  camera.position.copy( controls.target ).sub(direction);
  
  controls.update();
  
}

13 Likes

Related:

1 Like

This works fantastically on PerspectiveCamera, but not on OrthographicCamera since OrthographicCamera doesn’t have fov. I tried calculating an fov using what is described at http://forums.cgsociety.org/archive/index.php/t-725538.html , but failed miserably. haha

Any tips for adapting this to the Ortho camera?

Works great, thank you!

In my case I’m using a CSS2DRenderer too to render labels on top of some objects; the camera is correctly facing the objects, but somehow the labels seem not to be updating their positions. I add a label like this (extracted from one of the threejs examples), where points is an array of Three.Points that are correctly rendered.-

var earthDiv = document.createElement( 'div' );
earthDiv.className = 'label';
earthDiv.textContent = 'Nube de puntos';
earthDiv.style.marginTop = '-1em';
var earthLabel = new CSS2DObject( earthDiv );
earthLabel.position.set( 0, 1, 0 );
points[0].add( earthLabel );

Any idea of how to fix this?

OrthoCamera is easier, because the width of the frustum is constant along the length of the frustum. Just set the width/height of the ortho cam to match the width/height of the object you want to fit, and done!

If I was straight on, sure, but at an isometric angle, with multiple objects, it isn’t that clear cut. I will put together a demo in the morning.

Yeah, when not looking at the object straight on, you could use object’s bounding sphere (not perfect, might have empty space around the edges of the view), or do some trig on the object to get the diagonal size that you need based on the bounding box (a little better), or something more complicated depending on the shape of the geometry (but this one applies for PerspectiveCamera too if you want it to fit perfectly).

I second that, we know the canvas size so is there a way to calculate it? In my project by try and error I ended up using 6.50. no idea why this works. magic number?

Maybe this helps: Here are some functions to fit objects within a view using a few lines of trigonometry math: