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)
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 )
}
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!!!
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.
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 )
}
}
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?