最近在做和WebVR有关的项目,在这里简单地分析一下总体的脉络。整个实现的方式是通过js来控制css 3d变换来实现的,我们将围绕360度全景VR来做例子。

首先,我们要实现一个360度全景,立方体算是最简单的,点击看demo。这里面只要设置好6个面的3D变换就可以围成一个立方体。

关键在于以下的变换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.face1 {
background: #3498db;
transform: rotateY(180deg) translateZ(100px);/* 后 */
}
.face2 {
background: #f1c40f;
transform: rotateY(90deg) translateZ(100px);/* 右 */
}
.face3 {
background: #7f8c8d;
transform: rotateY(0deg) translateZ(100px);/* 前 */
}
.face4 {
background: #7f8c8d;
transform: rotateY(-90deg) translateZ(100px);/* 左 */
}
.face5 {
background: #34495e;
transform: rotateX(90deg) translateZ(100px);/* 上 */
}
.face6 {
background: #9b59b6;
transform: rotateX(-90deg) translateZ(100px);/* 下 */
}

rotateY/totateX比较好算,translateZ的计算需要结合三角函数比较好理解(快速理解链接),它在空间中的位移遵循右手直角坐标系。

1
2
3
4
5
6
7
function calTranslateZ(opts) {
return Math.round(opts.width / (2 * Math.tan(Math.PI / opts.number)))
}
calTranlateZ({
width: 200,
number: 4
}); // 100

但实际我们想要的并不是在外部看这个立方体外侧,而是站在立方体内部看立方体的内侧,这里的关键点在于perspective和translateZ之间的关系。当面的translateZ的绝对值大于或等于perspective的值,这个面在转动的过程中就可以经过人眼睛的背后,这时候你就看不到它了,我们就感觉是进入了这个立方体的内部。

那上面为什么说是translateZ的绝对值?因为我们进入了立方体看里面,我们就必须把面做一个翻转,面原本的正值变成负值就可以了,而它当然从原本位置变到对面去了。我们基于上面立方体的代码稍作修改,直接看demo

然后,我们需要给VR场景添加重力感应。要使用行动装置的陀螺仪,我們只要监听deviceorientation事件就可以得到三个值alpha、beta、gamma(数学物理中所讲的欧拉角)。这里的难点在于理解立方体rotateX、rotateY的角度和alpha、beta、gamma之间的关系。

alpha:行动装置水平放置時,绕Z轴旋转的角度,数值为0deg到360deg。

beta:行动装置水平放置時,绕X轴旋转的角度,数值为-180deg到180deg。

gamma:行动装置水平放置時,绕Y轴旋转的角度,数值为-90deg到90deg。

显然,立方体的rotateX和beta有关系,rotateY和alpha、gamma有关系,看demo

但非常遗憾的是,安卓和IOS对于alpha的值不尽相同,而且当手机的gamma值在90度时起手式会发生突变,这个叫做万向节死锁问题,需要进行兼容和临界点的重新运算。另外再加上手势事件后,alpha\beta\gamma值需要增量叠加不能再单纯使用实时监听的值,非常麻烦。还有一点,这个只是简单的demo,如果考虑到项目的复杂度、页面性能,这么做下去坑可以把自己埋了。但起码我们把最基本的原理搞清楚了,如果借助一些现成的东西,不需要太关心面的变换和起手架复杂的变动关系,我们可以更专注于项目业务层、逻辑层的事情,将大大提高效率和降低风险。

Github上这位哥shrek.wang贡献了一套构造CSS 3D全景的引擎css3d-engine和横竖屏重力感应的易用组件orienter,另外如果用得着还有动画库jstween和便于编写高效可维护代码的轻量框架bone,手势事件就需要自己去写了。

css3d-engine提供了一个C3D.skyBox的类,可以快速构建立方体全景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//创建场景
var s = new C3D.Stage();
s.size(window.innerWidth, window.innerHeight).material({
color: "#ccc"
}).update();
document.getElementById('main').appendChild(s.el);

//创建1个立方体放入场景
var c = new C3D.Skybox();
c.size(1024).position(0, 0, 0).material({
front: {image: "images/cube_FR.jpg"},
back: {image: "images/cube_BK.jpg"},
left: {image: "images/cube_LF.jpg"},
right: {image: "images/cube_RT.jpg"},
up: {image: "images/cube_UP.jpg"},
down: {image: "images/cube_DN.jpg"},

}).update();
s.addChild(c);

相信大家不用看API都能理解上面代码的意思,确实简单明了,完整代码请看官方例子,或者快速预览demo

如果场景是超过6个面的多面体,就改用C3D.create()来创建各个面,里面传参做好贴图定位,原理是一样的。

然后,我们再利用orienter.js给场景添加重力感应,看下面demo,但是万向节死锁问题并没有在orienter.js中得到解决,chrekshrek先生另外在具体的项目中再重新计算了相关的值,本人拙劣着实看不懂,所以只能先抄着代码用着了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var o = new Orienter();
var tip = document.getElementById('tip');
o.orient = function (obj) {
rotateX = obj.lat;
rotateY = obj.lon;
tip.innerHTML =
'alpha:' + obj.a +
'<br>' + 'beta:' + obj.b +
'<br>' + 'gamma:' + obj.g +
'<br>' + 'longitude:' + obj.lon +
'<br>' + 'latitude:' + obj.lat +
'<br>' + 'direction:' + obj.dir;
};
o.init();

学习参考请看shrekshrek的demo,里面还包含了封装的手势事件。另外,再推荐一个“使用”起来比较好上手的学习项目,由vivo音乐手机联合QQ音乐的这款H5

以下是个人项目相关:

兼容和性能

1、图片没得说,必须尽量压缩,装饰性小图能合并尽量合并,代码合并压缩;
2、图片\结构分级加载:预加载封面只加载场景;可见物件在场景渲染完成后加载插入;不可见物件以及事件在入场动画完成后再进行渲染和绑定;
3、通过加载时间判断网络环境和硬件条件的好坏,加载超时提供跳转降级版逻辑;Android4及以下直接跳降级版

踩坑

1、场景的需要,禁止了默认触屏事件,需要区分滑动操作和点击操作,避免在滑动过程中误触发点击:监听touchstart和touchend的坐标差值,0才视为点击,否则视为滑屏;
2、在VR中有层级问题,点击出现冒泡,常规方法无法解决,通过标记状态条件执行事件,延时再更改标记状态,保证事件在此瞬间只触发一次
3、诡异的死角,场景中某些角度,广告牌无法点击,需要另外再遮罩一个透明点击层,这个估计是使用了vivo h5中的点击区域算法有关

协同开发

1、初始阶段,个人gulp项目用于sass编译补全、图片压缩、导出压缩包;
2、后期接入开发,git托管webpack项目,方便迭代。

除了css 3d panorama实现WebVR,我们还有其他的方案,如ThreeJs、Aframe,学习成本会更高一些,根据走在前头的前辈反馈,也确实会比css 3d好使,特别建议我去换ThreeJS VR。

拓展阅读:
1、《CSS 3D Panorama - 淘宝造物节技术剖析
2、《WebVR如此近 - three.js的WebVR示例程序解析
3、《ThreeJS 轻松实现主视觉太阳系漫游
4、《A-Frame WebVR试玩报告