Web AR 预研二三事

  AR:增强现实(Augmented Reality,简称AR):是一种实时地计算摄影机影像的位置及角度并加上相应图像、视频、3D模型的技术,这种技术的目标是在屏幕上把虚拟世界套在现实世界并进行互动。

  本文将探究Web AR的可行性与实现。


实现分析

既然是手机AR,那么意味着有两个前提

  • H5 意味着在手机而非 PC 上实现
  • AR 意味着需要获取实时的视频流目前 Web 中实现 AR

都需要开启摄像头(手机具有先天优势),而获取实时视频流。然后再将虚拟物体以某种方式结合到画面中。
综合考虑,可以得出实现一个Web AR大概需要经历以下步骤:

  1. 获取视频源
  2. 将虚拟物体叠加在视频源上
  3. 将最终画面显示在屏幕上
  4. 用户与设备进行交互

技术分析

获取视频

浏览器可以用过navigator.getUserMedia()这一API获取用户的摄像头数据流与麦克风数据流(Stream)。在使用该API时,我们发现该API已被废弃。通过MDN查询该API资料可得:
Image text
其新的标准是 接口来调用 方法,该方法通过提示用户,让用户选择是否打开系统上的相机或麦克风等,并提供包含音视频的 MediaStream。但经过尝试,仍未解决在安卓webkit内核浏览器中调用后置摄像头问题,使用一实验性方法mediaDevices.enumerateDevices(),此API仅能在支持ES6语法的手机上运行。

1
2
3
4
5
navigator.mediaDevices.getUserMedia(constraints).then(function(stream) {
// 对媒体流进行处理
}).catch(function(err) {
// 对错误进行处理
});

注:IOS不支持调用摄像头,webkit内核浏览器调用摄像头需通https。

constraints需要请求的媒体类型对象,如{audio: true, video:true} { audio: true, video: { facingMode: { exact: “environment” } } }
在实际操作过程中,我们发现即便是传入的参数强制指定为后置摄像头,在安卓手机QQ等一些 或浏览器中并不能生效。因此我们需要通过强制指定请求媒体的设备id来实现打开后置摄像头。方法如下
核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
navigator.mediaDevices.enumerateDevices() // 调设备信息
.then(function(devices) {
devices.forEach(function(device) {
if (device.label.indexOf('back') > 0 || device.kind == 'videoinput') {
_this.videoId = device.deviceId // 获取后置摄像头 id
}
})
})
.then(function() {
var constraints = {
"video": {
optional: [{
sourceId: _this.videoId
}]
},
"audio": false
};
try {
navigator.mediaDevices.getUserMedia(constraints) // 调起后置设置摄像头
.then(_this.successFunc)
.catch(_this.oldGetUserMedia)
} catch (error) {
// 开启全景背景图
_this.initBg();
}
})

通过 mediaDevices.enumerateDevices()方法来获得设备上所有的媒体输入和输出设备信息,比如摄像头,麦克风等。通过枚举这些设备,判断设备标签(device.label)中是否有 back 关键词来获取后置摄像头的设备id device.deviceIddevice.kind == ‘videoinput’ 是对 firefox 的兼容处理,因为 firefox 中的 device.deviceId 为空。
获取设备id信息后,再构造 constraints 对象使用,即通过 optional 指定 sourceId 为指定的设备id。

注意:mediaDevices.enumerateDevices() 目前还处于试验性标准阶段,实现该方法的浏览器目前还不多。对于不支持此方法的,我们直接不做调起后置摄像头的尝试,转而使用3D全景图。


其中使用了ES6所具有的方法与语法,相关方法支持度如图黑色框:
Image text

可以看出,IOS9以及安卓5.1以上的版本支持友好。对于不支持ES6的设备,调用3D全景图进行替换。

3D全景图

对于不支持调用摄像头的设备,我们输出3D全景图对其进行兼容。考虑到以后模型等使用three.js进行建立,所以3D全景我们也使用three.js来制作。通过three.js构造一个球体,将全景图片作为材质对其进行贴合。
全景图示例:
Image text
创建球形3D全景核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
texture = THREE.ImageUtils.loadTexture(_this.Img_id.src, {}, function () {
var geometry = new THREE.SphereGeometry(r, 100, 100); //构造球体
var material = new THREE.MeshLambertMaterial({ //传入贴图材质
map: texture,
side: THREE.DoubleSide
});
var mesh = new THREE.Mesh(geometry, material); //构建三位网格模型
_this.Scene.add(mesh); //添加到场景之中
mesh.position.set(0, 0, 0);
var light = new THREE.AmbientLight(0xffffff); //全局光
_this.Scene.add(light);
});

考虑到用户进入页面时候的手机角度有各种情况,可能是竖着手机进入,然后平放手机等待加载完成查看页面……并且为了降低交互难度以及固定让3D模型出现在舞台上(背景比较好看的情况下),需要的是在不同朝向(东南西北)打开3D全景,都是看向一个固定方向的。强制其朝向同一个方向。

1
_this.Camera.lookAt({ x: 200, y: 0, z: 0 });

陀螺仪绑定

无论是AR亦或是VR,其核心交互少不了根据所持设备方向变换而展示不同场景,这其中的核心便是根据陀螺仪改变场景。DeviceOrientationControls.js– 这是一个 three.js 插件,它帮助我们完成之前提到过的监视设备朝向,并且代码量非常少,通过它我们可以省去将陀螺仪alpha、beta、gamma值计算为我们所需值的步骤。
代码如下:

1
2
3
4
initDevices:function(){
var _this = this;
_this.Devices = new THREE.DeviceOrientationControls(_this.Camera);
}

用户与设备交互

目前实现了两种交互方式

1.  用户通过移动手机,使手机中心点与我们设立的目标事物某一点对齐,完成交互。
实现思路是通过监听陀螺仪数据与目标事物当前的位置通过一定的计算进行对比。当到达某一临界值,判断其对齐成功。

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
rx=Math.round(camera.getWorldDirection().x*1000); //当前位置信息
ry=Math.round(camera.getWorldDirection().y*1000);
rz=Math.round(camera.getWorldDirection().z*1000);
dx=Math.round(Devices.deviceOrientation.alpha); //陀螺仪信息
dy=Math.round(Devices.deviceOrientation.beta);
dz=Math.round(Devices.deviceOrientation.gamma);
renderer.render(scene, camera);
if(rx/dx<0 && rx/dx>-0.5 && ry/dy>-0.5 && ry/dy<0.5){ //符合条件调用success方法
succes();
}

  1. 用户通过点击屏幕进行交互
    在页面中插入3D模型,不光只是为了看,更重要的是实现交互,比如点击,拖拽等。在 Three.js 模型中实现点击操作,可以使用 Three.js 自带的射线检测( Raycaster )。

其原理是:在用户点击时,获取点击点的屏幕坐标;根据场景中照相机,将屏幕坐标转换成场景中的坐标;新建一条从照相机位置,向转换后的场景坐标位置射出一条射线(向量),检测这条射线穿过的场景中的物体。
关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 实例化射线检测
_this.raycaster = new THREE.Raycaster();
rayCheck: function() {
var _this = this;
// 传入touch坐标 和 照相机
_this.raycaster.setFromCamera(_this.tempTouch, _this.camera);
// 射线检测是否穿过指定物体
// _this.scene.selectable 是一个场景需要检测的物体的数组,在往_this.scene添加物体时push进去的
// intersects 射线与被检测物体相交的物体对象
var intersects = _this.raycaster.intersectObjects(_this.scene.selectable, true);
if (intersects.length > 0) {
alert("中奖啦");
_this.isTouched = true;
// todo other
}
},
initEvent: function() {
var _this = this;
document.addEventListener('touchend', function(event) {
// touch结束时,touch 横坐标到屏幕左侧的距离与屏幕宽度的一半的比值,绝对值不超过1
_this.tempTouch.x = (event.changedTouches[0].clientX / window.innerWidth) * 2 - 1;
// touch结束时,touch 纵坐标到屏幕顶部的距离与屏幕高度的一半的比值,绝对值不超过1
_this.tempTouch.y = -(event.changedTouches[0].clientY / window.innerHeight) * 2 + 1;
}, false);
},

这是里涉及到Three.js里的两个坐标系,世界坐标系和屏幕坐标系。屏幕坐标系是原点基于窗口左上角的坐标系。Threejs的世界坐标系与openGL的世界坐标系相同,以屏幕中心为原点(0,0,0),并且始终不变。面对屏幕,右边为x轴的正轴,上面为y轴的正轴,屏幕向上的地方为z轴的正轴。也称为右手坐标系。
Image text

结语

新技术的预研一路上会遇到很多坑,当解决了这个问题之后又会发现出现了新问题。不过踩的坑越多,才能知道越多