在Three.JS画布的初始加载中,如何将3D物体(Collada文件)适应于画布大小?

6
我有一个盒子(collada文件),在three.js画布中加载。我可以像预期的那样与它交互。然而,盒子的大小会随着用户更改大小而发生变化。
当我将其载入500px x 500px的画布中时,如果盒子很大,用户必须放大才能看到它,如果它很小,它就很小,用户需要放大。大小取决于传递的变量。
我该如何让对象(collada文件)在加载时适应画布,然后让用户缩放?这是点击加载在three.js画布中显示3D对象的代码:
$scope.generate3D = function () {

        // 3D OBJECT - Variables
        var texture0 = baseBlobURL + 'Texture_0.png';
        var boxDAE = baseBlobURL + 'Box.dae';
        var scene;
        var camera;
        var renderer;
        var box;
        var controls;
        var newtexture;

        // Update texture
        newtexture = THREE.ImageUtils.loadTexture(texture0);

        // Initial call to render scene, from this point, Orbit Controls render the scene per the event listener
        THREE.DefaultLoadingManager.onProgress = function (item, loaded, total) {
            // console.log( item, loaded, total ); // debug
            if (loaded === total) render();
        };

        //Instantiate a Collada loader
        var loader = new THREE.ColladaLoader();
        loader.options.convertUpAxis = true;
        loader.load(boxDAE, function (collada) {
            box = collada.scene;
            box.traverse(function (child) {
                if (child instanceof THREE.SkinnedMesh) {
                    var animation = new THREE.Animation(child, child.geometry.animation);
                    animation.play();
                }
            });
            box.scale.x = box.scale.y = box.scale.z = .2;
            box.updateMatrix();
            init();
        });

        function init() {
            scene = new THREE.Scene();
            camera = new THREE.PerspectiveCamera(100, window.innerWidth / window.innerHeight, 0.1, 1000);
            renderer = new THREE.WebGLRenderer();
            renderer.setClearColor(0xdddddd);

            //renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setSize(500, 500);

            // Load the box file
            scene.add(box);

            // Lighting
            var light = new THREE.AmbientLight();
            scene.add(light);

            // Camera
            camera.position.x = 40;
            camera.position.y = 40;
            camera.position.z = 40;

            camera.lookAt(scene.position);

            // Rotation Controls
            controls = new THREE.OrbitControls(camera, renderer.domElement);
            controls.addEventListener('change', render);
            controls.rotateSpeed = 5.0;
            controls.zoomSpeed = 5;
            controls.noZoom = false;
            controls.noPan = false;

            // Add 3D rendering to HTML5 DOM element
            var myEl = angular.element(document.querySelector('#webGL-container'));
            myEl.append(renderer.domElement);

        }

        // Render scene
        function render() {
            renderer.render(scene, camera);
            console.log('loaded');
        }
    }

    // Initial 3D Preview Load
    $scope.generate3D();

更新:我已经评估了在如何适应对象的相机中提出的解决方案,但是不确定如何为我的collada文件定义距离,因为它可能因用户输入的尺寸而异。 collada文件是由用户向第三方供应商发送变量生成的,该供应商返回一个collada文件,随后加载到three.js中。

更新2:感谢@Blindman67,我更接近理解这个交互作用。当我手动提高camera.position的x,y,z值时,对象就在屏幕上了。我面临的挑战是如何确定每个盒子的正确x,y,z值,因为每次都会动态更改,我实际上有超过2.8亿种变化。我知道@Blindman67已经在逻辑上给了我答案,但我只需要最后推动一下以发现如何获取每次变化的正确位置,以便设置正确的x,y,z。


1
如何将相机适配到物体上?谷歌可以帮助你。如何计算相机所需的缩放比例以适应屏幕高度中的物体? - 2pha
仍需要额外的帮助,因为我正在加载Collada文件而不是创建几何形状,所以我不确定如何将其定位到需要缩放的位置。 - Kode
1
不用担心,所有的加载器都会创建一个Object3D对象,该对象将具有一个geometry属性。 - 2pha
由于我只需要允许用户与对象交互,那么我应该使用不同的相机吗?透视投影和正交投影有什么优势? - Kode
这与你最初的问题有什么关系呢?为什么你需要一个不同的相机?在谷歌上搜索透视和正交之间的区别。 - 2pha
显示剩余3条评论
3个回答

11

将3D对象适配到视图中

有几种方法可以将3D对象适配到相机视图中。

  • 向后或向前移动相机。
  • 增加或减少焦距(这也会影响视野)。
  • 更改世界空间比例或对象的本地空间比例,使对象变大或变小。

您可以忽略最后一种选项,因为在大多数情况下它是不切实际的。(尽管我注意到您正在代码中执行此操作,但在此答案中提供了足够的信息来计算如何缩放对象以适应。但我不建议您这样做)

因此,您只能将相机向内或向外移动,或者保持静止并进行缩放。这与真实相机相同,您可以进行缩放或靠近。

这两种方法都有优缺点。

移动相机(轨迹)对于大多数情况来说是最好的,因为它保持了透视的一致性(线汇聚到消失点的速度),因此不会扭曲视图中的对象。在3D中,这种方法存在3个问题。

  • 视锥体(场景显示的体积)有一个背面平面(物体将被显示的最大距离)和前面平面(物体能够显示的最近距离)。将相机移动以适应非常大或非常小的对象可能会导致对象超出前面或后面的平面并部分或完全被裁剪。
  • 移动平面以适应对象也可能会产生不良结果。同时移动前面和后面的平面来容纳对象可能会导致场景中更近或更远的对象被裁剪掉。
  • 扩大前后平面之间的总距离也可能会导致Z缓冲区别名伪影。但这些问题仅适用于非常大或非常小的对象和场景。

缩放

缩放涉及更改相机的焦距。在使用的库中,这是通过调整FOV(视场)来完成的,它是视图左右两侧之间的角度,以度数表示。减小FOV实际上增加了焦距并进行缩放(3D图形没有像相机一样的焦距)。增加FOV则进行缩小。但此方法存在问题。

  • 随着视场角的减小,透视(视差)减小,使场景看起来越来越不立体。随着视场角的增加,透视增加,扭曲物体并使远处的物体非常小。
  • 由于摄像机不移动,因此前后平面保持不变,但在靠近前后平面的物体上缩放或缩放可能仍会导致z缓冲区别名伪影。

使用哪种方法取决于您,您可以使用其中一种或两种组合。

如何完成

对于这个问题,我们需要知道对象在场景中的大小,并使用这些信息来更改相机设置以实现所需的效果(即将对象适配到显示器上)。

Diagram displaying the camera and the various properties requiered to contruct a view 图1. 相机,对象和视图。

因此,需要一些值。请参见图1以获得可视化解释。

  • 视场角。我会将其转换为弧度。
  • 物体距离相机的距离
  • 物体的包围球半径。
  • 前平面
  • 后平面
  • 屏幕像素尺寸

您需要计算物体的包围球。或者使用另一个近似包围球的值。这取决于您。

代码

var oL,cL; // for the math to make it readable
var FOV = 45 * (Math.PI / 180); // convert to radians
var objectLocation = oL = {x : 0, y : 0, z : 400};
var objectRadius = 50;
var cameraLocation = cL = {x : 0, y : 0, z : 0};
var farPlane = 1000;
var nearPlane = 200;
var displayWidth = 1600;
var displayHeight = 1000;

计算边界球在视图中的大小很简单,只需要用三角函数即可。

// Get the distance from camera to object
var distToObject = Math.sqrt(Math.pow(oL.x - cL.x, 2) + Math.pow(oL.y - cL.y, 2) + Math.pow(oL.z - cL.z, 2));Figure 1

由于我们使用了直角三角形(见图1),所以将结果乘以2得到总角度大小。

// trig inverse tan of opposite over adjacent.
var objectAngularSize = Math.atan( (objectRadius) / distToObject ) * 2;

获取对象占据的视野分数。
var objectView = objectAngularSize / FOV;

最后,您可以获得对象的像素大小。

var objectPixelSize = objectView * displayWidth;

这是您需要知道的全部内容,以完成您的要求。如果您使用以上代码和数学知识尝试重新排列计算,使物体通过移动相机或调整FOV来占据所需像素大小,将有助于您的理解。复制代码并不能教会您很多东西,将上述信息应用到实践中才能牢记,并且将使未来您需要进行的3D过程更加容易。
话虽如此,复制代码是一个快速解决方案,这就是框架和库的全部。没有必要知道,您有更好的东西要学习。
自适应缩放。
这是最简单的方法,它获取对象的角度大小并调整FOV以适合它。(注意:我使用弧度,Three.js 使用 FOV 的度数,您需要进行转换)
var requieredObjectPixelSize = 900;
var distToObject = Math.sqrt(Math.pow(oL.x - cL.x, 2) + Math.pow(oL.y - cL.y, 2) + Math.pow(oL.z - cL.z, 2));
var objectAngularSize = Math.atan( (objectRadius) / distToObject ) * 2;
// get the amount the FOV must be expanded by
var scaling = displayWidth / requieredObjectPixelSize;
// change the FOV to set the objects size 
FOV =  objectAngularSize * scaling;

将FOV转换为度数并使用它来创建相机。
适应性转换:
将相机移动以适应物体。这需要更多的操作,但是是更好的方法。
// Approx size in pixels you want the object to occupy
var requieredObjectPixelSize = 900;

// camera distance to object
var distToObject = Math.sqrt(Math.pow(oL.x - cL.x, 2) + Math.pow(oL.y - cL.y, 2) + Math.pow(oL.z - cL.z, 2));

// get the object's angular size.
var objectAngularSize = Math.atan( (objectRadius) / distToObject ) * 2;

// get the fraction of the FOV the object must occupy to be 900 pixels
var scaling = requieredObjectPixelSize / displayWidth;

// get the angular size the object has to be
var objectAngularSize = FOV * scaling;

// use half the angular size to get the distance the camera must be from the object
distToObject = objectRadius / Math.tan(objectAngularSize / 2);

现在需要移动相机。必须沿着物体和相机之间的向量进行移动。
// Get the vector from the object to the camera
var toCam = {
    x : cL.x - oL.x,
    y : cL.y - oL.y,
    z : cL.z - oL.z,
}

将向量标准化。这意味着使向量的长度等于1,方法是通过将每个分量(x、y、z)除以向量的长度来完成。

// First length
var len = Math.sqrt(Math.pow(toCam.x, 2) + Math.pow(toCam.y, 2) + Math.pow(toCam.z, 2));
// Then divide to normalise (you may want to test for divide by zero)
toCam.x /= len;
toCam.y /= len;
toCam.z /= len;

现在,您可以缩放向量,使其等于相机与物体之间的距离。
toCam.x *= distToObject;
toCam.y *= distToObject;
toCam.z *= distToObject;

然后只需要将向量添加到对象的位置,并将其放置在相机位置即可。
cL.x = oL.x + toCam.x;
cL.y = oL.y + toCam.y;
cL.z = oL.z + toCam.z;

cl现在保存着相机的位置。

最后一件事情,你需要检查物体是否在视野范围内。

if (distToObject - objectRadius < nearPlane) {
    nearPlane = (distToObject - objectRadius) * 0.8; // move the near plane towards the camera 
                                                 // by 20% of the distance between the front of the object and the camera
}

if (distToObject + objectRadius > farPlane) {
    farPlane = distToObject + objectRadius * 1.2; // move the far plane away from the camera 
                                              // by 1.2 time the object radius
}

还有一个问题。如果物体非常小,可能会非常接近以至于物体的前面在摄像机后面。如果发生这种情况,您将需要使用缩放方法并将相机向后移动。这只适用于非常特殊的情况,在大多数情况下可以忽略。
我没有提供如何将其与Three.js集成的信息,但这是旨在适用于所有3D软件包的通用答案。您将不得不查阅three.js文档以了解如何更改各种相机设置。它很简单,并适用于透视相机。
好的,这是一个很长的回答,我需要暂时忘记它,因为我没有休息看不到错别字和错误。我会在今天稍后返回并进行修正。
希望对你有所帮助。

这绝对是我在StackOverflow上看过的最有启发性的帖子。无论是否需要弄清楚如何在threejs代码中使其工作,我都学到了很多,这比仅仅得到一些代码更有价值。可以说它给了我更大的“视角”…… - Kode
这确实增加了我的知识,但我需要知道如何在threejs中实现。有人愿意接手吗? - Kode
1
你需要获取盒子在三维空间中的大小和位置。使用 box.computeBoundingSphere();var boxRadius = box.boundingSphere; 获取大小,然后 box.position 就是盒子的 (x,y,z) 位置。将这些值用于答案示例,然后使用 camera.fov 设置视场或使用 camera.position 设置位置。除此之外,我不知道你卡在哪里了。 - Blindman67
1
大小是在3D空间中的尺寸,实际像素大小将由设备屏幕和所需像素大小var requieredObjectPixelSize = ?;应用适合过程后确定。 3D对象的坐标系独立于屏幕分辨率。 您可以缩放框以适应,但然后您还必须缩放场景中的所有内容。 - Blindman67
摄像机和盒子都有一个位置。在代码标题下的第一部分代码是我设置摄像机和盒子位置cameraLocationobjectLocation的地方。我使用快捷键clol,在翻译到适合的方法中,您将看到我如何沿着由这两个点创建的线移动相机。我从对象到相机获得向量,对其进行归一化,将该向量乘以相机必须远离对象的距离,然后添加对象位置并设置摄像机位置。答案中的每个步骤都有详细说明。 - Blindman67
显示剩余7条评论

2
这段代码将“自动居中”相机在给定对象周围。
对于用户的“缩放”,还应查看three.js examples/js/controls/TrackballControls.js,该控件会使相机位置变化(并更改lookat和up)。如果您选择使用它,我记得您需要在使用centerCam移动相机位置之前初始化控件。在这种情况下,centerCam可以让整个对象进入视野以准备用户交互,然后TrackballControls侦听器接管控制。
<body onload="initPage()">
  <canvas id="cadCanvas" width="400" height="300"></canvas>
  <button onclick="loadFile()">Load Collada File</button>
</body>

function initPage(){
  var domCanvas = document.getElementById('cadCanvas');
  scene = new THREE.Scene();
  var fovy = 75;
  camera = new THREE.PerspectiveCamera( fovy, domCanvas.width/domCanvas.height, 0.1, 1000 );
  renderer = new THREE.WebGLRenderer({canvas:domCanvas});
  renderer.setSize( domCanvas.width, domCanvas.height );
}

function loadFile(){
  new THREE.ColladaLoader().load( url, function(obj3D) {
    scene.add(obj3D);
    centerCam(obj3D);
    renderer.render(scene, camera);
  });
}

function centerCam(aroundObject3D){

  //calc cam pos from Bounding Box
  var BB = new THREE.Box3().setFromObject(aroundObject3D);
  var centerpoint = BB.center();
  var size = BB.size();
  var backup = (size.y / 2) / Math.sin( (camera.fov/2)*(Math.PI/180) );
  var camZpos = BB.max.z + backup + camera.near ;

  //move cam
  camera.position.set(centerpoint.x, centerpoint.y, camZpos);
  camera.far = camera.near + 10*size.z;
  camera.updateProjectionMatrix();

}

-1
感谢Blindmann67,我能够更好地理解所有这些复杂性并在threejs中提出解决方案。有一行特定的代码,其中盒子的比例尺被手动设置:
box.scale.x = box.scale.y = box.scale.z = .2;

由于我知道我的对象的宽度(动态的),以及画布的大小(500像素),因此我可以通过以下方式获得正确的比例尺:

var initialZoomScale = 500 / canvasWidthProdPixels; 
// canvasWidthProdPixels is my large object (ex. 25,000 px wide) dividing these gives me a small fraction such as .02 which scales the object perfectly)

box.scale.x = box.scale.y = box.scale.z = initialZoomScale;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接