44

使用 three.js 我有以下内容。

  • 包含多个 Object3D 实例的场景
  • 几个预定义的相机 Vector3 位置
  • 如果屏幕调整大小,画布的动态宽度/高度
  • 用户可以选择一个对象(从上方)
  • 用户可以选择相机位置(从上方)

给定正在查看的对象和他们选择的相机位置,我如何计算最终的相机位置以“最适合”屏幕上的对象?

如果在某些屏幕上“按原样”使用相机位置,则对象会在我的视口边缘溢出,而其他对象则显得更小。我相信可以将物体安装到相机平截头体上,但找不到合适的东西。

4

7 回答 7

66

我假设您使用的是透视相机。

您可以设置相机的位置、视野或两者。

以下计算对于立方体对象是精确的,因此请考虑对象的边界框,对齐以面向相机。

如果相机居中并正面观察立方体,则定义

dist = distance from the camera to the _closest face_ of the cube

height = height of the cube.

如果您将相机视野设置如下

fov = 2 * Math.atan( height / ( 2 * dist ) ) * ( 180 / Math.PI ); // in degrees

那么立方体高度将匹配可见高度。

在这一点上,您可以将相机稍微倒退一点,或者将视野扩大一点。

如果视野是固定的,那么使用上面的方程来求解距离。


编辑:如果你想让立方体width匹配可见宽度,让aspect画布的纵横比(画布宽度除以画布高度),并像这样设置相机视野

fov = 2 * Math.atan( ( width / aspect ) / ( 2 * dist ) ) * ( 180 / Math.PI ); // in degrees

三.js r.69

于 2013-01-30T22:06:48.777 回答
18

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);
于 2014-04-29T09:46:10.097 回答
13

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.

于 2016-01-06T00:34:08.170 回答
6

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.

three.js zoom extents demo

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.

three.js zoom all camera

Here's how you calculate desired distance from sphere to camera:

three.js zoom all 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();
于 2020-10-28T11:23:41.463 回答
3

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);
于 2019-05-03T19:34:15.553 回答
1

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);
于 2016-01-17T20:51:20.803 回答
1

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

于 2020-03-08T08:06:49.660 回答