使用 ThreeJS 在浏览器中展示全景图
在浏览器中实现全景浏览,听起来是很玄的事情. 但如果你清楚它的原理,这事就简单多了。
前言
在之前的一段时间,朋友圈中出现了一批使用全景图浏览技术的 H5 页面. 比如探班吴亦凡系列,或者是探访京东总部大楼找优惠券页面. 在当时,这几个页面取得了不错的宣传效果. 那么,这种新奇的全景效果到底是怎样实现的呢?
基础
现在的智能手机一般都自带全景图拍摄功能. 就算没有,通过安装一些第三方软件也可以拥有这个功能,但通过这些软件拍出来的只是一个非常宽的照片,还无法达到 360 随意转动观看的效果. 要制作像上面那样的 360 全景观看页面,我们需要从最基础的开始. 首先,什么是全景图?
360 度全景图也称为三维全景图、全景环视图。360 度全景技术是一种运用数码相机对现有场景进行多角度环视拍摄之后,再利用计算机进行后期缝合,并加载播放程序来完成的一种三维虚拟展示技术。 -- 360 度全景图_百度百科
也就是说,我们可以使用拍到的全景图, 使用计算机进行后期缝合 , 并加载播放程序来完成三维显示. 具体到使用 ThreeJS 实现全景图这个场景,我们需要做什么呢?
其实,粗略一想也可以想到,如果我们将拍摄到的全景图贴在一个圆柱的侧面上,我们站在圆柱中心朝四周看的话,应该就有全景观察的效果. 不过这样做也有坏处,也就是我们的头顶跟脚底都是无法看到的区域. 我们需要使用其他的方式来实现. 在这之前,我们需要了解一下 ThreeJS 中的相机。
ThreeJS 世界中的相机
在 ThreeJS 中,相机还分为 CubeCamera(立方体相机), PerspectiveCamera(透视相机) 以及 OrthographicCamera(正交相机). 其中, CubeCamera 是创建动态贴图用的, OrthographicCamera 创建的照相机不具有透视效果. 在这里,我们用到的是 PerspectiveCamera.
定义一个透视相机只需要一句话:
var camera = new THREE.PerspectiveCamera(
fov,
aspect,
near,
far
);
在这一段代码中, fov 代表相机的视角,即视野上平面与下平面的角度, aspect 是相机的宽高比, near 是视野近平面的距离, far 是远平面的距离. 然而,在 3D 的世界中,仅凭上面这几个参数,我们只能确定一个照相机的自身基本属性,却无法确定这台照相机究竟位于什么位置,是什么样的角度. ThreeJS 中, camera.position 属性是一个三维向量,我们可以用这个属性定义相机相对于原点的位置. camera.lookAt(Vector3) 函数可以定义照相机的观察方向,参数同样是一个三维向量. 对于照相机而言,还有一个参数显得非常重要,这就是相机的上方向. 同样的空间位置,朝向同一个方向,照相机还可以是横着,也可以竖着,最后看到的效果也不会一样. 所以 camera 还有一个 up 属性,定义照相机的上方向. 如上图蓝色空心箭头所示。
动手做全景展示 1
要让人产生全景的视觉效果,很关键的一点是,要让人看见他当前姿态所应当看见的景观. 如果将人眼比作一台照相机,我们很容易想到,我们如果将全景图贴在一个球形的内表面,那么人眼这台照相机所看到的景象就是上下左右 360 无死角的全景。
想想总是美如画的. 我们不妨实践一下. 首先,去 google 搜索关键字'全景图 360', 随意下载一个全景图. 接下来,我们需要将这个全景图贴到球形的表面. 这一步,我们再一次用到了 Blender.
首先,新建一个工程,然后往场景中添加一个经纬球。
我们希望使用之前下载到的全景图作为这个球体的贴图. 所以,我们需要首先对这个球体做 uv 展开. 对这个球使用球面投射,方向选择对齐到物体,选上缩放至边界框,接着我们看到 UV 展开图是这样的:
这个 UV 展开图非常不规则,就算有了全景图,我们也没法往上贴. 这是因为上下两个顶点处汇集了所有的经线,使得 Blender 也无法准确得知我们想要怎样的贴图. 这种时候,我们需要将球体的南北两个极点删除. 这样的话,球体的上下两个顶点就成为了两个空心的圆圈. 然而,模型空了两个洞不太好. 接下来,使用 Extude 工具推挤出新的纬圈,酌情缩小一些。
重新进行 UV 展开,会看到这次的 UV 展开非常平整。
将新的纬圈设定 scale 为 0, 再删除重叠的节点,南北极就可以重新汇聚到一个点了. 接下来我们把全景图贴上,一个全景球就这样诞生了。
我们将这个全景球通过 io_three 插件导出为 json. 新建一个页面,引入 three.js. 核心代码大概是这样子:
objloader = new THREE.ObjectLoader();
objloader.load(
'js/360.json',
function (obj) {
scene = obj;
//scene.add(new THREE.AmbientLight(0xffffff));
//material = scene.children[0].material;
//material.side = THREE.BackSide;
//material.emissive = 0x000000;
animate();
}
);
这里需要注意,如果将注释部分的代码删去,我们将会发现视野中一片黑. 这是因为 Blender 导出的模型默认使用的是遵守 Phong 光照模型的材质,这种材质在没有配置自发光,又没有外界光照的情况下就是一坨黑色. 所以我们还需要手动配置一下. blender 导出的 json 是 scene 本身. scene 是一个树状的结构,在它的 children 属性中有所有的对象信息. 在这里,我们需要配置一下贴图的方向以及自发光,接下来就可以看到效果了。
动手做全景展示 2
上面这样的实现其实也有一个弊端. 球状模型的顶点与面的数量十分逆天. 这些元素的数量越多,耗费的浏览器资源就会越多. 那么有没有更加节能环保的方法呢?
答案是肯定的. 既然我们的人眼可以被类比为照相机,那么如果摆多几台照相机,将拍到的照片无重叠地拼在一起,一样可以获得全景视觉。
这里,我们使用 6 台 90 度视角,纵宽比 1 的照相机,从球体中心分别朝向立方体六个面的方向。
将 6 个渲染图分别保存下来。
接下来新建页面,将这六张图片分别贴到立方体的六个面就大功告成了. 核心代码:
loader = new THREE.TextureLoader();
/*
虽然使用 THREE.ImageUtils.loadTexture 也没问题
不过估计是为了适应 Javascript 的异步式编程
ThreeJS 也逐步将一些会阻塞的 api 转换为异步回调的模式
原有的老 api 会被标记为 deprecated
*/
gardenMaterials = [
'garden/px.png',
'garden/nx.png',
'garden/py.png',
'garden/ny.png',
'garden/pz.png',
'garden/nz.png'
];
Promise.all(gardenMaterials.map(function (val) {
//加载图片,新建材质,传给下一个步骤.
return new Promise(function (resolve, reject) {
loader.load(val, function (texture) {
resolve(new THREE.MeshBasicMaterial({
map: texture,
side: THREE.BackSide
}));
});
});
})).then(function (materials) {
//将材质贴到正方体的 6 个面.
geometry = new THREE.BoxGeometry(60, 60, 60);
cube = new THREE.Mesh(
geometry,
new THREE.MeshFaceMaterial(materials)
);
scene.add(cube);
animate();
});
渲染出来的效果,其实是完全一样的。
小结
使用这两种方法做出来的全景展示其实还会有一些小问题,比如展示空间的底部与顶部会有聚焦在一点的现象:
这种情况单靠一个全景图是无法解决的,只能通过对底部与顶部多拍一个照片来补救. 目前,全景展示的技术已经有许多应用,比如谷歌地图百度地图的街景展示,或者是上面提到的几个 H5 页面中也有用到. 作为一名前端工程师,懂得其中的原理并付诸实践,这是非常重要的。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

上一篇: 精致化页面重构
下一篇: Web Workers 实践实践
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论