Reputation: 1809
Using three.js I have the following.
Given an object being viewed and the camera position they have chosen how do I compute the final camera position to "best fit" the object on screen?
If the camera positions are used "as is" on some screens the objects bleed over the edge of my viewport whilst others they appear smaller. I believe it is possible to fit the object to the camera frustum but haven't been able to find anything suitable.
Upvotes: 47
Views: 47748
Reputation: 2334
Assuming that object fits into screen if it's bounding sphere fits, we reduce the task to fitting sphere into camera view.
In given example we keep PerspectiveCamera.fov constant while changing camera rotation to achieve best point of view for the object. Zoom effect is achieved by moving camera along .lookAt direction vector.
On the picture you can see problem definition: given bounding sphere and camera.fov, find L, so that bounding sphere touches camera's frustum planes.
Here's how you calculate desired distance from sphere to camera:
Complete solution: https://jsfiddle.net/mmalex/h7wzvbkt/
var renderer;
var camera;
var scene;
var orbit;
var object1;
function zoomExtents() {
let vFoV = camera.getEffectiveFOV();
let hFoV = camera.fov * camera.aspect;
let FoV = Math.min(vFoV, hFoV);
let FoV2 = FoV / 2;
let dir = new THREE.Vector3();
camera.getWorldDirection(dir);
let bb = object1.geometry.boundingBox;
let bs = object1.geometry.boundingSphere;
let bsWorld = bs.center.clone();
object1.localToWorld(bsWorld);
let th = FoV2 * Math.PI / 180.0;
let sina = Math.sin(th);
let R = bs.radius;
let FL = R / sina;
let cameraDir = new THREE.Vector3();
camera.getWorldDirection(cameraDir);
let cameraOffs = cameraDir.clone();
cameraOffs.multiplyScalar(-FL);
let newCameraPos = bsWorld.clone().add(cameraOffs);
camera.position.copy(newCameraPos);
camera.lookAt(bsWorld);
orbit.target.copy(bsWorld);
orbit.update();
}
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(54, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.x = 15;
camera.position.y = 15;
camera.position.z = 15;
camera.lookAt(0, 0, 0);
renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(new THREE.Color(0xfefefe));
document.body.appendChild(renderer.domElement);
orbit = new THREE.OrbitControls(camera, renderer.domElement);
// create light
{
var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(0, 100, 50);
spotLight.castShadow = true;
spotLight.shadow.mapSize.width = 1024;
spotLight.shadow.mapSize.height = 1024;
spotLight.shadow.camera.near = 500;
spotLight.shadow.camera.far = 4000;
spotLight.shadow.camera.fov = 30;
scene.add(spotLight);
}
var root = new THREE.Object3D();
scene.add(root);
function CustomSinCurve(scale) {
THREE.Curve.call(this);
this.scale = (scale === undefined) ? 1 : scale;
}
CustomSinCurve.prototype = Object.create(THREE.Curve.prototype);
CustomSinCurve.prototype.constructor = CustomSinCurve;
CustomSinCurve.prototype.getPoint = function(t) {
var tx = t * 3 - 1.5;
var ty = Math.sin(2 * Math.PI * t);
var tz = 0;
return new THREE.Vector3(tx, ty, tz).multiplyScalar(this.scale);
};
var path = new CustomSinCurve(10);
var geometry = new THREE.TubeGeometry(path, 20, 2, 8, false);
var material = new THREE.MeshPhongMaterial({
color: 0x20f910,
transparent: true,
opacity: 0.75
});
object1 = new THREE.Mesh(geometry, material);
object1.geometry.computeBoundingBox();
object1.position.x = 22.3;
object1.position.y = 0.2;
object1.position.z = -1.1;
object1.rotation.x = Math.PI / 3;
object1.rotation.z = Math.PI / 4;
root.add(object1);
object1.geometry.computeBoundingSphere();
var geometry = new THREE.SphereGeometry(object1.geometry.boundingSphere.radius, 32, 32);
var material = new THREE.MeshBasicMaterial({
color: 0xffff00
});
material.transparent = true;
material.opacity = 0.35;
var sphere = new THREE.Mesh(geometry, material);
object1.add(sphere);
var size = 10;
var divisions = 10;
var gridHelper = new THREE.GridHelper(size, divisions);
scene.add(gridHelper);
var animate = function() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
};
animate();
Upvotes: 11
Reputation: 712
I had the same question but I expected the object(s) (represented by a Box3
as a whole) could rotate on my phone if the whole was wider than my screen so I could view it by zooming in as near as possible.
const objectSizes = bboxMap.getSize();
console.log('centerPoint', centerPoint, bboxMap, objectSizes, tileMap);
//setupIsometricOrthographicCamera(bboxMap);
//https://gamedev.stackexchange.com/questions/43588/how-to-rotate-camera-centered-around-the-cameras-position
//https://threejs.org/docs/#api/en/cameras/PerspectiveCamera
//https://stackoverflow.com/questions/14614252/how-to-fit-camera-to-object
// Top
// +--------+
// Left | Camera | Right
// +--------+
// Bottom
// canvas.height/2 / disance = tan(fov); canvas.width/2 / disance = tan(fovLR);
// => canvas.width / canvas.height = tan(fovLR)/tan(fov);
// => tan(fovLR) = tan(fov) * aspectRatio;
//If rotating the camera around z-axis in local space by 90 degrees.
// Left
// +---+
// Bottom | | Top
// | |
// +---+
// Right
// => tan(fovLR) = tan(fov) / aspectRatio;
const padding = 0, fov = 50;
let aspectRatio = canvas.width / canvas.height;
let tanFOV = Math.tan(Math.PI * fov / 360);
let viewWidth = padding + objectSizes.x, viewHeight = padding + objectSizes.y;
//The distances are proportional to the view's with or height
let distanceH = viewWidth / 2 / (tanFOV * aspectRatio);
let distanceV = viewHeight / 2 / tanFOV;
const camera = this.camera = new THREE.PerspectiveCamera(fov, aspectRatio, 0.1, 10000); //VIEW_ANGLE, ASPECT, NEAR, FAR
if (aspectRatio > 1 != viewWidth > viewHeight) {
console.log('screen is more narrow than the objects to be viewed');
// viewWidth / canvas.width => viewHeight / canvas.width
// viewHeight / canvas.height => viewWidth / canvas.height;
distanceH *= viewHeight / viewWidth;
distanceV *= viewWidth / viewHeight;
camera.rotateZ(Math.PI / 2);
}
camera.position.z = Math.max(distanceH, distanceV) + bboxMap.max.z;
//camera.lookAt(tileMap.position);
I had tested two different aspect of Box3
on tow different orientations (landscape and portrait) using my phone, it worked well.
References
target
— the result will be copied into thisVector3
. Returns the width, height and depth of this box.
rad
- the angle to rotate in radians. Rotates the object around z axis in local space.
Upvotes: 1
Reputation: 2290
try this for OrbitControls
let padding = 48;
let w = Math.max(objectLength, objectWidth) + padding;
let h = objectHeight + padding;
let fovX = camera.fov * (aspectX / aspectY);
let fovY = camera.fov;
let distanceX = (w / 2) / Math.tan(Math.PI * fovX / 360) + (w / 2);
let distanceY = (h / 2) / Math.tan(Math.PI * fovY / 360) + (w / 2);
let distance = Math.max(distanceX, distanceY);
Upvotes: 6
Reputation: 11
From user151496's suggestion about using the aspect ratio, this seems to work, although I've only tested with a few different parameter sets.
var maxDim = Math.max(w, h);
var aspectRatio = w / h;
var distance = maxDim/ 2 / aspectRatio / Math.tan(Math.PI * fov / 360);
Upvotes: 1
Reputation: 32056
To calculate how far away to place your camera to fit an object to the screen, you can use this formula (in Javascript):
// Convert camera fov degrees to radians
var fov = camera.fov * ( Math.PI / 180 );
// Calculate the camera distance
var distance = Math.abs( objectSize / Math.sin( fov / 2 ) );
Where objectSize
is the height or width of the object. For cube/sphere objects you can use either the height or width. For a non-cube/non-sphere object, where length or width is greater, use var objectSize = Math.max( width, height )
to get the larger value.
Note that if your object position isn't at 0, 0, 0
, you need to adjust your camera position to include the offset.
Here's a CodePen showing this in action. The relevant lines:
var fov = cameraFov * ( Math.PI / 180 );
var objectSize = 0.6 + ( 0.5 * Math.sin( Date.now() * 0.001 ) );
var cameraPosition = new THREE.Vector3(
0,
sphereMesh.position.y + Math.abs( objectSize / Math.sin( fov / 2 ) ),
0
);
You can see that if you grab the window handle and resize it, the sphere still takes up 100% of the screen height. Additionally, the object is scaling up and down in a sine wave fashion (0.6 + ( 0.5 * Math.sin( Date.now() * 0.001 ) )
), to show the camera position takes into account scale of the object.
Upvotes: 13
Reputation: 104763
I am assuming you are using a perspective camera.
You can set the camera's position, field-of-view, or both.
The following calculation is exact for an object that is a cube, so think in terms of the object's bounding box, aligned to face the camera.
If the camera is centered and viewing the cube head-on, define
dist = distance from the camera to the _closest face_ of the cube
and
height = height of the cube.
If you set the camera field-of-view as follows
fov = 2 * Math.atan( height / ( 2 * dist ) ) * ( 180 / Math.PI ); // in degrees
then the cube height will match the visible height.
At this point, you can back the camera up a bit, or increase the field-of-view a bit.
If the field-of-view is fixed, then use the above equation to solve for the distance.
EDIT: If you want the cube width
to match the visible width, let aspect
be the aspect ratio of the canvas ( canvas width divided by canvas height ), and set the camera field-of-view like so
fov = 2 * Math.atan( ( width / aspect ) / ( 2 * dist ) ) * ( 180 / Math.PI ); // in degrees
three.js r.69
Upvotes: 70
Reputation: 479
Based on WestLangleys answer here is how you calculate the distance with a fixed camera field-of-view:
dist = height / 2 / Math.tan(Math.PI * fov / 360);
Upvotes: 19