Camera Zoom To Fit Object

camera-controls
camera

#1

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


#2

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!


#3

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 )

   }


#4

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.


#5

@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.


#6

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!!!


#7

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.


#8

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!!!


#9

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.


#10

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!


#11

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


#12

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…


#13

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).


#14

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.


#15

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.


#16

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.