简单一招搞定 three.js 屏幕适配

这篇文章只讨论 PerspectiveCamera 的适配方法javascript

作过手机 H5 的同窗可能会以为屏幕适配挺麻烦。缘由是设计师提供的设计稿尺寸比固定,可是前端开发者却要适配不一样大小、长宽比的目标设备。适配的终极目标无非是最大程度把主体内容优雅地呈现给用户。开发和设计若是没有协调好的话可能会妥协比较丑陋的方案,例如因为设计比例问题,为了照顾主体内容不被裁剪,只好设备两边,或者上下留黑边这种。前端

不过在 3D 的世界里,咱们不用担忧会有黑边的问题,由于 3D 场景是无限延伸的,总能填满任何比例的屏幕。java

先看看 PerspectiveCamera 官方 API 说明以下:web

PerspectiveCamera( fov, aspect, near, far )

fov — Camera frustum vertical field of view.
aspect — Camera frustum aspect ratio.
near — Camera frustum near plane.
far — Camera frustum far plane.

上面四个参数都会影响成像结果,fovaspect 设置 XY 平面的范围,也就是广度。 nearfar 影响的是纵深 Z 轴的范围,也就是深度。纵深只要保证物体离相机距离在这个范围就能够了,这是为了性能而设置的参数,由用户设置,只渲染必要的东西。实际上真实的相机这两个值对应的是 0 到 无限远。ide

这些参数设置好以后,成像就相应肯定了。最后 three.js 把相机拍摄到的矩形区域对应好四个顶点渲染到屏幕上。一样比例的屏幕看到的图像是一致的,与屏幕大小无关</span>。函数

clipboard.png

下面我用一个简单的场景来看一下这些参数对成像的影响。工具

场景元素

  • 相机 (PerspectiveCamera)性能

  • 一个边长为 100 的平面(主体内容范围),放在世界坐标中心。ui

var camera = new THREE.PerspectiveCamera(53, 500 / 500, 0.1, 1000);

var planeGemo = new THREE.PlaneGeometry( 100, 100, 10, 10 )
var meshMaterial = new THREE.MeshLambertMaterial();
meshMaterial.color = new THREE.Color(0x2dcaf1);
meshMaterial.side = THREE.DoubleSide;

var wireFrameMat = new THREE.MeshBasicMaterial();
wireFrameMat.color = new THREE.Color(0xdddddd);
wireFrameMat.wireframe = true;

var plane = THREE.SceneUtils.createMultiMaterialObject(planeGemo, [meshMaterial, wireFrameMat]);

scene.add(plane);

目标

在任何屏幕下,都能最大程度地显示完整的立方体。最大程度,就是最少多余空间的意思。下面是要达到效果this

clipboard.png

设置 fov 参数

能够直接想到的一种适配方法是——改变 camera 到目标物体的距离以控制成像的内容,可是这样作计算成本比较高,并且还有可能影响其余一些数值,而后须要相应一块儿计算修改。
我想到改变视角也能够达到控制成像内容多少的目的,因而我想可不能够只经过改变 fov 一个数值,达到我要的效果。

fov 官网的定义翻译过来是垂直方向的视角大小。咱们先规定好相机到平面的距离为 100,而后试试看能不能经过计算设置 fov 值,恰好让平面填满一个宽高比为 1:1 的屏幕。

plane.position.set(0,0,0);
camera.position.set(0,0,100);
camera.lookAt(new THREE.Vector3);

clipboard.png

观察上面的图,能够很容易求出 fov 的值, fov = arctan((100/2)/100) * 2; fov 为 0.9272952180016122,约等于 53 度。

camera.fov = Math.atan((100/2)/100) * 2 * (180 / Math.PI);
camera.updateProjectionMatrix();

设置完刚刚求出的 fov 值,将场景渲染到 宽高比为 1:1 的画布上。

clipboard.png

渲染结果和预想的同样,平面恰好填满了 1:1 的画布。

fov 和宽高比例的关系

下面在固定的 fov 下,使用 dat.gui 工具调整宽高比,观察渲染区域的变化。

clipboard.png

由于fov设置的是垂直方向的视角范围,能够看到不管咱们怎么改变宽高比例,垂直方向的渲染范围,都是一致的。水平方向则是以裁剪的方式显示。也就是说当咱们设置好视角让垂直方向范围恰好等于主体内容的范围,只要宽高比大于1,咱们获得的渲染结果,已是最佳的了。问题就只剩下当宽高比小于1的状况了。

宽高比小于1的时候,垂直方向显示的高度恰好是等于主体内容的高度。为了能让水平方向完整显示主体内容,咱们只有将垂直方向范围增大,也就是将 fov 设置一个更大的值,此时水平方向的范围也会随之增大。当将 fov调整到 水平方向恰好能显示主体内容时,垂直方向此时显示的范围是超过主体内容垂直方向的范围的。其中的关系,其实能够用很简单的函数求出来。

已知 照相机到主题内容的距离为 d
正方形主体内容的边长为 w

设宽高比为 r,求照相机垂直方向的视角 f

当 r >= 1 时,照相机拍摄到的垂直方向范围等于 w

当 r > 1
d * tan(f/2) * 2 = w

当 r < 1 时,照相机拍摄到的水平方向范围等于 w,垂直方向范围应该是 w/r
d * tan(f/2) * 2 = w/r

这样,任意宽高比例的屏幕应该对应多大的垂直视角就肯定了。

最终代码与效果

var controls = new function () {

    camera.position.z = CAMERA_TO_MAIN_DIS;

    this.width = 500;
    this.height = 500;
    this.planeRY = 0;

    /**
    * 计算相机 fov 的函数
    * @param d : 在相机前方 d 距离
    * @param w : 想要看到最大正方形区域边长为 w
    * @param r : 屏幕宽高比
    */
    function calcFov(d, w, r) {
        var f;
        var vertical = w;
        if (r < 1) {
            vertical = vertical/r;
        }
        f = Math.atan(vertical/d/2)*2 * (180 / Math.PI);
        return f;
    }

    this.redraw = ()=>{
        webGLRenderer.setSize(this.width, this.height);
        plane.rotation.y = this.planeRY;
        camera.fov = calcFov(CAMERA_TO_MAIN_DIS, MAIN_CONTENT_WIDTH, this.width / this.height);
        camera.aspect = this.width / this.height;
        camera.updateProjectionMatrix();
    }
}

效果:

demo 的完整代码:http://codepen.io/JasonTurbo/pen/ZLwJMo
原文连接:http://gnauhca.com/blog/2016/11/24/threejs/THREEJS%E5%B1%8F%E5%B9%95%E9%80%82%E9%85%8D/

相关文章
相关标签/搜索