【问题标题】:How to Fit Camera to Object如何使相机适合对象
【发布时间】:2013-01-14 20:58:13
【问题描述】:

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

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

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

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

【问题讨论】:

    标签: javascript three.js


    【解决方案1】:

    假设如果边界球适合,则该对象适合屏幕,我们将任务简化为将球体适合相机视图。

    在给定的示例中,我们保持PerspectiveCamera.fov 不变,同时更改相机旋转以获得对象的最佳视角。缩放效果是通过沿 .lookAt 方向矢量移动相机来实现的。

    在图片上您可以看到问题定义: 给定边界球体和 camera.fov,求 L,使边界球体接触相机的平截头体平面。

    以下是计算球体到相机所需距离的方法:

    完整解决方案: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();
    

    【讨论】:

    • 我发现这非常有用,所以感谢小提琴和详细的答案。如果目标对象未定位在 0,0,0 开始,或者如果您不希望轨道控件以网格几何体的中心为目标,则需要进行一些调整。我还通过网格的高度增加了 z 偏移量,这将相机按比例地按对象的几何形状向后拉恰到好处,至少对于我的目的而言。
    【解决方案2】:

    我有同样的问题,但我希望对象(整体由 Box3 表示)可以在我的手机上旋转,如果整体比我的屏幕宽,所以我可以通过放大查看它尽可能。

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

    我用我的手机在两个不同的方向(横向和纵向)上测试了Box3 的两个不同方面,效果很好。

    参考文献

    【讨论】:

      【解决方案3】:

      试试这个 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);
      

      【讨论】:

        【解决方案4】:

        根据 user151496 关于使用纵横比的建议,这似乎可行,尽管我只测试了几个不同的参数集。

        var maxDim = Math.max(w, h);
        var aspectRatio = w / h;        
        var distance = maxDim/ 2 /  aspectRatio / Math.tan(Math.PI * fov / 360);
        

        【讨论】:

          【解决方案5】:

          要计算将相机放置多远以使对象适合屏幕,您可以使用以下公式(在 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 ) );
          

          其中objectSize 是对象的高度或宽度。对于立方体/球体对象,您可以使用高度或宽度。对于非立方体/非球体对象,长度或宽度较大,使用var objectSize = Math.max( width, height )获取较大值。

          请注意,如果您的对象位置不在0, 0, 0,您需要调整相机位置以包含偏移量。

          Here's a CodePen 展示了这一点。相关线路:

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

          您可以看到,如果您抓住窗口句柄并调整其大小,球体仍然占据屏幕高度的 100%。此外,对象以正弦波方式 (0.6 + ( 0.5 * Math.sin( Date.now() * 0.001 ) )) 放大和缩小,以显示考虑到对象比例的相机位置。

          【讨论】:

          • 有什么相反的想法吗?我有类似第一人称射击游戏的控件,我想在相机所面对的当前视图中放置一个新对象...
          • 三角函数必须是tan 而不是sin 像这样:Math.abs( objectSize / Math.tan( fov / 2 ) );
          【解决方案6】:

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

          您可以设置相机的位置和/或视野。

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

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

          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
          

          three.js r.69

          【讨论】:

          • “如果相机居中并正面观察立方体” - 这假设我们已经将相机移动到朝向边界框?如果您还不知道相机的外观将在哪里,如何为一组面向相机的点生成边界框?
          • three.js 中的边界框是轴对齐的——它是一个 AABB——并且与相机位置或方向无关。
          • @WestLangley :我尝试了您的解决方案,并且在某种程度上可以正常工作。在我的例子中,3D 对象的宽度和长度可以根据用户输入而改变。这是一个类似房子的结构,当用户输入 6m 时,对象的宽度变为 12。现在它可以正常工作,直到宽度和长度达到 25。之后,当我旋转对象时,相机进入 3D 对象内部。它是一个移动应用程序,所以我们已经阻止了手动放大和缩小的捏和膨胀。所以用户卡住了。我试着做一个小提琴,请参阅:jsfiddle.net/0z3z6y7w/86。但是这个问题并没有发生在小提琴中。
          • @WestLangley 您如何获得dist? “立方体的最近面”是什么意思?
          【解决方案7】:

          根据 WestLangleys 的回答,这里是您如何使用固定相机视野计算距离:

          dist = height / 2 / Math.tan(Math.PI * fov / 360);
          

          【讨论】:

          • 我不确定哪里错了,但它是错的。它离立方体太近了
          • 感谢您的示例。我仔细检查了计算,它们应该是正确的,但由于某种原因它离立方体太近了,我不知道为什么:(
          • 也许它与纵横比有关?它不存在于等式中
          • 这个公式确实不正确,是上面公式的不正确解
          • 您能否分享您的解决方案并告诉我们出了什么问题?
          猜你喜欢
          • 2014-10-11
          • 1970-01-01
          • 2022-11-08
          • 2015-11-05
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-01-26
          相关资源
          最近更新 更多