如何用Three.js快速实现全景图

3,335 阅读6分钟
原文链接: zhuanlan.zhihu.com

封面图 by Thư Anh on Unsplash

去年全景图在微博上很是火爆了一阵,正好我也做过一点全景相关的项目,这些天抽空写下这一篇用Three.js来实现全景图的文章,和大家一起探讨。真的是抛砖引玉,还请包涵。



1. 开胃菜:用纯css实现一个伪全景图


先跟大家分享一个比较简单的例子作为开胃小菜,利用css3里的animation,我们可以让一张比较长的画卷,以匀速滚动的形式从左往右展现,代码如下:

.panorama {
 width: 300px;
 height: 300px;
 background-image: url(./img.jpeg);
 background-size: auto 100%;
 animation: panorama 8s linear infinite;
}

@keyframes panorama {
 to {
 background-position: 100% 0;
    }
}


2. 正餐:用Three.js实现全景图


尝了开胃菜,大家大概也能猜到全景图的原理是怎样的了,下面我们就来说说这道正餐。
Three.js是一个强大的开源项目,这里就不多做介绍了,我们主要会用到它的这几个功能:

  • Camera(相机)、
  • SphereBufferGeometry(球体)
  • BoxGeometry(正方体)
  • MeshBasicMaterial(材质)
  • Mesh/Scene(场景)
  • WebGLRenderer(渲染)


引入Three.js文件,压缩版的文件有140kb,只能说勉强能接受吧。

<script src="https://threejs.org/build/three.min.js"></script>



先说一下全景图的实现原理:通过创造一个球体/正方体,并在其表面贴上专门的背景图,然后将相机放在球体/正方体的中心,监听手指拖动/陀螺仪移动来改变相机的面向,从而实现全景图。


相机:首先,我们要创造一个相机,并指明相机的面向。

// 相机
camera = new THREE.PerspectiveCamera(opt.fov, opt.width / opt.height, 1, 10000);
camera.target = new THREE.Vector3(0, 0, 0);


相机需要传的四个参数分别是fov(相机摄像角度,可用于放大和缩小)、width/height(宽高比)、neer(近距离界限)、far(远距离界限)。


生成球体:生成一个球体,并让相机位于它的中心。

// 球体
var geometry = new THREE.SphereBufferGeometry(opt.radius, 60, 60);
geometry.scale(-1, 1, 1);

添加材质: 把准备好的背景图添加到材质上。

// 材质
var material = new THREE.MeshBasicMaterial({
    map: new THREE.TextureLoader().load(opt.url)
});


创建场景:场景背景我设成了灰色,方便调试,如果不设的话默认是黑色。

// 场景
var mesh = new THREE.Mesh(geometry, material);
scene = new THREE.Scene();
scene.background = new THREE.Color( 0xf0f0f0 );
scene.add(mesh);


渲染:设置好dpr、画布宽高,Three.js就会生成一个canvas。

// 渲染
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(opt.width, opt.height);
canvas = renderer.domElement;
opt.container.appendChild(canvas);


监听滑动:通过监听touchstart、touchmove、touchend,来判断手指划过的距离,并以此计算出相机应该在x轴和y轴方向上各移动多少角度(一般不考虑z轴)。


// 监听
window.addEventListener('touchstart', start);
window.addEventListener('touchmove', move);
window.addEventListener('touchend', end);

// 开始触摸
function start(event) {
 var evt = event.changedTouches[0];
    startX = evt.clientX;
    lastX = evt.clientX;
    startY = evt.clientY;
    lastY = evt.clientY;
}

// 滑动
function move(event) {
 var evt = event.changedTouches[0];
 switch (event.changedTouches.length) {
       case 1:
            lon += (evt.clientX - lastX) * factor;
            lat += (evt.clientY - lastY) * factor;

            lastX = evt.clientX;
            lastY = evt.clientY;
            break;
       case 2:
       // 如果要做放大缩小效果的话,可以在这里补充,我偷懒没写这种情况╮(╯▽╰)╭    }
}
// 结束
function end(event) {
   // 这里可以添加减速效果增加流畅性,没错我又偷懒了
}



至于为何不管z轴,大家看看相机xyz轴分别对应的转动方向就能明白了。相机默认是由+z至-z拍摄,z轴转动只会让视线变斜。


监听陀螺仪:陀螺仪也分为x轴(beta:-180至+180)、y轴(Gamma:-90至90)、z轴(Alpha:0至360),如图:

插图来自公众号“交互设计前端开发”
插图来自公众号“交互设计前端开发”
插图来自公众号“交互设计前端开发”


我们要做的就是监听陀螺仪事件,并把变化的角度告诉相机(事先通过window,orientation判断是否支持陀螺仪)。


// 监听
.addEventListener('deviceorientation', orient);

function orient(event) {
    orientLon = event.gamma;
    orientLat = event.beta - 70; // 减90°让默认状态是手机直立,但一般人手机都会向后仰一点,所以我少减了20°
}



实时渲染:Three.js的requestAnimationFrame(实时渲染),能让canvas实时更新,无论是否有变化,我们利用这个特点,进行相机视角变化后的渲染。

// 动态渲染
var render = function () {
    requestAnimationFrame( render );
    lat -= orientLat;
    lon -= orientLon;

    camera.rotation.x = lat * Math.PI / 180;
    camera.rotation.y = lon * Math.PI / 180;
    camera.rotation.z = 0;
  // camera.rotation.x = THREE.Math.degToRad(lat); 
  // Three.js自带有换算角度的方法,两种写法都可以 
  // camera.rotation.y = THREE.Math.degToRad(lon);
    renderer.render(scene, camera);
};
render();


我这里用的是通过改变相机的角度(rotation)来达到改变视角的目的,还有一种实现方式是通过告诉相机一个点,让它把视线对准这个点,来改变视角。 当画图中有别的元素,并且要把视角对准它时,这个方法就更实用了。

camera.lookAt(x, y, z);



抛砖引玉:其实我说的这些,都只是最基本的内容,如果想要实现一个完美的全景图,还有很多地方可以改进,比如:

  • 拖动结束时,可以加一个减速停止的效果,防止拖动出现抖动、不顺畅的情况;
  • 当角度大于90°或小于-90°时,让相机停止,不然画面会倒过来;
  • 实时渲染必定会让手机变成暖宝宝,有没有什么方法能优化性能呢?Three.js还提供了个方法cancelAnimationFrame,我们是不是能在视角未改变的时候停止渲染呢?
  • resize的情况也可以兼容一下,可以使用camera.updateProjectionMatrix( );
  • 当event.changedTouches.length为2时,表示用户在用两指滑动,这时我们可以处理放大缩小的情况,通过改变camera的fov来实现;
  • 全景图的背景都是很大的,我们应该想办法去对它进行优化,比如我们使用正方体时,如果背景比较单一,那我们完全可以六个面统一用一个面的背景图,这样图片大小会大大缩减;
  • 通过监听mousedown、mousemove、mouseup兼容pc端;

3. 甜点:利用svg添加标签

在利用Three.js实现全景图后,我们能否做一些别的提升呢?
我在查找资料的时候,发现有个网站通过使用svg实现了在画布上添加内容的效果,大家可以参考一下。


通过svg的polygon、polyline、circle,能在画布上添加各种标签,并监听拖动事件,改变元素的translate3d、points,来达到标签跟随画布一同滚动的效果。


4. 小结:
相信大家都已经明白全景图的原理和实现逻辑了,我也列了几条可以优化的点,有兴趣深究的同学可以亲自去实现一下。因为这个demo是我闲暇之余做的,可能会有许多瑕疵,性能上也还有很多有待提升的空间,希望大家多包涵,也请大家多多指点。


作者来自: