Camera Zoom To Fit Object

Hi there!
I want to create a function zoomToFit(camera, targetObject), which makes the camera move and zoom to fit the target object.

I do not want to modify the fov of the camera, instead I want move the camera, in order to create the zoom

To do this I will move the camera in the direction of camera.lookAt(Object) vector; however, I cannot figure out the rest of the math… and I have researched a lot, without success…

Another constraint: the target object to zoom will be in a shape closest to a rectangular box. Imagine a long rectangular box laying flat on a table, and filming the box as you walk towards the table until the box fits the whole camera view.). So I don’t want to use a bounding sphere as many tutorials suggest. I would like to use a bounding box, so to have a better fit. So the math would be to fit the box inside the camera frustum. (Another possibility would be to use a bounding sphere but to fit in the diameter of the sphere in the X direction… which would be similar to fitting a bounding box)

Could anyone help?
Thanks in advance!
Rod

8 Likes

I am in the same situation as this guy: https://stackoverflow.com/questions/16462848/three-js-zoom-to-fit-width-of-objects-ignoring-height

…however I don’t really understand his solution which he vaguely explained in words. And trust me, I have researched a lot.

Summary: I want to zoom perspective camera to fit target object width, without changing fov.

Thanks in advance!

1 Like

I use this function. It sets the camera z position and the far place to include the object, plus an offset to prevent the object filling exactly to the screen edge.

You can also pass in OrbitControls if you are using that and it will set the controls up too. Otherwise it will just point the camera at the object.

Note that there are a couple of expectations - the object should be “in front” of the camera, in particular.

const fitCameraToObject = function ( 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 );

    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 );
    let cameraZ = Math.abs( maxDim / 4 * Math.tan( fov * 2 ) );

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

    camera.position.z = cameraZ;

    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;

      controls.saveState();

    } else {

        camera.lookAt( center )

   }

5 Likes

Could you explain this line please:
let cameraZ = Math.abs( maxDim / 4 * Math.tan( fov * 2 ) );

I’d write something like this:
let cameraZ = maxDim / 2 / Math.tan( fov / 2 );
and then
camera.position.z = center.z + cameraZ;
Or I missed something?

P.S. I used the following logic:
tan(x) = a / b. a – opposite size. Where x = fov/2, a = size/2 and b = z that we’d like to calcz.

3 Likes

@fifonik I did originally use something like your cameraZ calculation and ran into issues so recalculated and arrived at my version.

I honestly don’t remember why now though - I wrote that function about 6 months ago and I’d rather not go over the maths again :laughing:

But try both versions and see which works for you.

BTW yes, it should be camera.position.z = center.z + cameraZ if the object is not positioned at z=0. Thanks.

Hi guys!
Thanks so much for the replies!!!

Let’s see if with your code I can get to the desired result.
I would like to remove the restriction of having the camera in front of the object.

These are my changes. Not tested since I am on a really old laptop right now, but in theory should work. Now we should be able to put the camera on ANY position and still make zoom work:

const fitCameraToObject = function ( 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 );

	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 );
	let 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;

	  controls.saveState();

	} else {

		camera.lookAt( center )

   }
}

Could you guys test it and let me know what you think?
Thanks!!!

This was not my correction :slight_smile:

P.S. I still believe that tan(x * 2) is incorrect as tan becomes infinite for 180 angles (that is 90 * 2).
So if you have pov = 90 (that is real life value for some setups) the routine gives infinite distance.
Also ,“something” should be divided on tan, not multiplied as tan is bigger for bigger angles (from 0 to 180). The bigger the angle – the smaller should be the distance from the camera for the same object, not the bigger.

Fifonik, could you please post your complete code?
I tested looeee’s code alone and it is not working well for me.

I also tested my code. It doesn’t work.
I might be able to fix my code but first I need a starting working example.

Thanks both Looeee and Fifonik!!!

Sorry, I do not have complete code.
I just read looeee’s message and noticed that based on my trigonometry knowledge something looks strange.

P.S. In my project I’m using camera.zoom instead and I’m not trying to fit object in the camera view automatically.

Hi Fifonik!
Thanks for the reply.

Can you tell me more about camera.zoom?
How does it work? There is not much info on the documentation.

Thanks!

You can play with this example:
https://threejs.org/examples/?q=camera#canvas_camera_orthographic2

Thanks! That was helpful! From what I can see; however, .zoom property changes fov and not z. Is This correct?

So it is zooming with fov and not by moving the camera like we were doing in our previous code…

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.

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?