本文探索在Web前端实现AR导航效果的前沿技术和难点。

1. AR简介

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

一般在web中实现AR效果的主要步骤如下:

  1. 获取视频源
  2. 识别marker
  3. 叠加虚拟物体
  4. 显示最终画面

以上参考:如何通过 Web 技术实现一个简单但有趣的 AR 效果

AR导航比较特殊的地方是,它并非通过识别marker来确定虚拟物体的叠加位置,而是通过定位将虚拟和现实联系在一起,主要步骤如下:

  1. 获取视频源
  2. 坐标系转换:
    1. 获取设备和路径的绝对定位
    2. 计算路径中各标记点与设备间的相对定位
    3. 在设备坐标系中绘制标记点
  3. 3D图像与视频叠加
  4. 更新定位和设备方向,控制Three.js中的相机移动

2. 技术难点

如上文所述AR导航的主要步骤,其中难点在于:

  1. 兼容性问题
  2. WebGL三维作图
  3. 定位的精确度和轨迹优化
  4. 虚拟和现实单位尺度的映射

2.1 兼容性问题:

不同设备不同操作系统以及不同浏览器带来的兼容性问题主要体现在对获取视频流和获取设备陀螺仪信息的支持上。

2.1.1 获取视频流

  1. Navigator API兼容处理

    navigator.getUserMedia()已不推荐使用,目前新标准采用navigator.mediaDevices.getUserMedia()。可是不同浏览器对新方法的支持程度不同,需要进行判断和处理。同时,如果采用旧方法,在不同浏览器中方法名称也不尽相同,比如webkitGetUserMedia

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //不支持mediaDevices属性
    if (navigator.mediaDevices === undefined) {
    navigator.mediaDevices = {};
    }

    //不支持mediaDevices.getUserMedia
    if (navigator.mediaDevices.getUserMedia === undefined) {
    navigator.mediaDevices.getUserMedia = function(constraints) {
    var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

    if(!getUserMedia) {
    return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
    }

    return new Promise(function(resolve, reject) {
    getUserMedia.call(navigator, constraints, resolve, reject);
    });
    }
    }
  2. 参数兼容处理

    getUserMedia接收一个MediaStreamConstraints类型的参数,该参数包含两个成员videoaudio

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var constraints = {
    audio: true,
    video: {
    width: {
    min: 1024,
    ideal: 1280,
    max: 1920
    },
    height: 720,
    frameRate: {
    ideal: 10,
    max: 15
    },
    facingMode: "user" // user/environment,设置前后摄像头
    }
    }

    在使用WebAR导航时,需要调取后置摄像头,然而facingMode参数目前只有Firefox和Chrome部分支持,对于其他浏览器(微信、手Q、QQ浏览器)需要另一个参数optional.sourceId,传入设备媒体源的id。经测试,该方法在不同设备不同版本号的微信和手Q上表现有差异。

    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
    if(MediaStreamTrack.getSources) {
    MediaStreamTrack.getSources(function (sourceInfos) {
    for (var i = 0; i != sourceInfos.length; ++i) {
    var sourceInfo = sourceInfos[i];
    //这里会遍历audio,video,所以要加以区分
    if (sourceInfo.kind === 'video') {
    exArray.push(sourceInfo.id);
    }
    }
    constraints = {
    video: {
    optional: [{
    sourceId: exArray[1] //0为前置摄像头,1为后置
    }]
    }
    };
    });
    } else {
    constraints = {
    video: {
    facingMode: {
    exact: 'environment'
    }
    }
    });
    }
  3. 操作系统的兼容性问题

    由于苹果的安全机制问题,iOS设备任何浏览器都不支持getUserMedia()。所以无法在iOS系统上实现WebAR导航。

  4. 协议

    出于安全考虑,Chrome47之后只支持HTTPS页面获取视频源。

2.1.2 获取设备转动角度

设备的转动角度代表了用户的视角,也是连接虚拟和现实的重要参数。HTML5提供DeviceOrientation API可以实时获取设备的旋转角度参数。通过监听deviceorientation事件,返回DeviceOrientationEvent对象。

1
2
3
4
5
6
{
absolute: [boolean] 是否为绝对转动值
alpha: [0-360]
beta: [-180-180]
gamma: [-90-90]
}

其中alpha、beta、gamma是我们想要获取的角度,它们各自的意义可以参照下图和参考文章:
陀螺仪
陀螺仪的基本知识

然而iOS系统的webkit内核浏览器中,该对象还包括webkitCompassHeading成员,其值为设备与正北方向的偏离角度。同时iOS系统的浏览器中,alpha并非绝对角度,而是以开始监听事件时的角度为零点。

Android系统中,我们可以使用-alpha得到设备与正北方的角度,但是就目前的测试情况看来,该值并不稳定。所以在测试Demo中加入了手动校正alpha值的过程,在导航开始前将设备朝向正北方来获取绝对0度,虽然不严谨但效果还不错。

手动校正alpha

2.2 WebGL三维作图

WebGL是在浏览器中实现三维效果的一套规范,AR导航需要绘制出不同距离不同角度的标记点,就需要三维效果以适应真实场景视频流。然而WebGL原生的接口非常复杂,Three.js是一个基于WebGL的库,它对一些原生的方法进行了简化封装,使我们能够更方便地进行编程。

Three.js中有三个主要概念:

  1. 场景(scene):物体的容器,我们要绘制标记点就是在场景中添加指定坐标和大小的球体
  2. 相机(camera):模拟人的眼睛,决定了呈现哪个角度哪个部分的场景,在AR导航中,我们主要通过相机的移动和转动来模拟设备的移动和转动
  3. 渲染器(renderer):设置画布,将相机拍摄的场景呈现在web页面上

在AR导航的代码中,我对Three.js的创建过程进行了封装,只需传入DOM元素(一般为<div>,作为容器)和参数,自动创建三大组件,并提供了Three.addObjectThree.renderThree等接口方法用于在场景中添加/删除物体或更新渲染等。

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
28
29
30
31
32
function Three(cSelector, options) {
var container = document.querySelector(cSelector);
// 创建场景
var scene = new THREE.Scene();
// 创建相机
var camera = new THREE.PerspectiveCamera(options.camera.fov, options.camera.aspect, options.camera.near, options.camera.far);
// 创建渲染器
var renderer = new THREE.WebGLRenderer({
alpha: true
});
// 设置相机转动控制器
var oriControls = new THREE.DeviceOrientationControls(camera);
// 设置场景大小,并添加到页面中
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setClearColor(0xFFFFFF, 0.0);
container.appendChild(renderer.domElement);

// 暴露在外的成员
this.main = {
scene: scene,
camera: camera,
renderer: renderer,
oriControls: oriControls,
}
this.objects = [];
this.options = options;
}
Three.prototype.addObject = function(type, options) {...} // 向场景中添加物体,type支持sphere/cube/cone
Three.prototype.popObject = function() {...} // 删除场景中的物体
Three.prototype.setCameraPos = function(position) {...} // 设置相机位置
Three.prototype.renderThree = function(render) {...} // 渲染更新,render为回调函数
Three.prototype.setAlphaOffset = function(offset) {..} // 设置校正alpha的偏离角度

在控制相机的转动上,我使用了DeviceOrientationControls,它是Three.js官方提供的相机跟随设备转动的控制器,实现对deviceorientation的侦听和对DeviceOrientationEvent的欧拉角处理,并控制相机的转动角度。只需在渲染更新时调用一下update方法:

1
2
3
4
5
6
7
8
three.renderThree(function(objects, main) {
animate();
function animate() {
window.requestAnimationFrame(animate);
main.oriControls.update();
main.renderer.render(main.scene, main.camera);
}
});

2.3 定位的精确度和轨迹优化

我们的调研中目前有三种获取定位的方案:原生navigator.geolocation接口,腾讯前端定位组件,微信JS-SDK地理位置接口:

  1. 原生接口

    navigator.geolocation接口提供了getCurrentPositionwatchPosition两个方法用于获取当前定位和监听位置改变。经过测试,Android系统中watchPosition更新频率低,而iOS中更新频率高,但抖动严重。

  2. 前端定位组件

    使用前端定位组件需要引入JS模块(https://3gimg.qq.com/lightmap/components/geolocation/geolocation.min.js),通过
    qq.maps.Geolocation(key, referer)构造对象,也提供getLocationwatchPosition两个方法。经过测试,在X5内核的浏览器(包括微信、手Q)中,定位组件比原生接口定位更加准确,更新频率较高。

  3. 微信JS-SDK地理位置接口

    使用微信JS-SDK接口,我们可以调用室内定位达到更高的精度,但是需要绑定公众号,只能在微信中使用,仅提供getLocation方法,暂时不考虑。

    综上所述,我们主要考虑在X5内核浏览器中的实现,所以选用腾讯前端定位组件获取定位。但是在测试中仍然暴露出了定位不准确的问题:

    1. 定位不准导致虚拟物体与现实无法准确叠加
    2. 定位的抖动导致虚拟标记点跟随抖动,移动视觉效果不够平稳

针对该问题,我设计了优化轨迹的方法,进行定位去噪、确定初始中心点、根据路径吸附等操作,以实现移动时的变化效果更加平稳且准确。

2.3.1 定位去噪

我们通过getLocationwatchPosition方法获取到的定位数据包含如下信息:

1
2
3
4
5
6
{
accuracy: 65,
lat: 39.98333,
lng: 116.30133
...
}

其中accuracy表示定位精度,该值越低表示定位越精确。假设定位精度在固定的设备上服从正态分布(准确来说应该是正偏态分布),统计整条轨迹点定位精度的均值mean和标准差stdev,将轨迹中定位精度大于mean + (1~2) * stdev的点过滤掉。或者采用箱型图的方法去除噪声点。

2.3.2 初始点确定

初始点非常重要,若初始点偏离,则路线不准确、虚拟现实无法重叠、无法获取到正确的移动路线。测试中我发现定位开始时获得的定位点大多不太准确,所以需要一段时间来确定初始点。

定位开始,设置N秒用以获取初始定位。N秒钟获取到的定位去噪之后形成一个序列track_denoise = [ loc0, loc1, loc2...],对该序列中的每一个点计算其到其他点的距离之和,并加上自身的定位精度,得到一个中心衡量值,然后取衡量值最小的点为起始点。

初始点纠偏

2.3.3 基于路线的定位校正

基于设备始终跟随规划路线进行移动的假设,可以将定位点吸附到规划路线上以防止3D图像的抖动。

如下图所示,以定位点到线段的映射点作为校正点。路线线段的选择依据如下:

  1. 初始状态:以起始点与第二路线点之间的线段为当前线段,cur = 0; P_cur = P[cur];
  2. 在第N条线段上移动时,若映射长度(映射点与线段起点的距离)为负,校正点取当前线段的起点,线路回退至上一线段,cur = N - 1; P_cur = P[cur];;若映射长度大于线段长度,则校正点取当前线段的终点,线路前进至下一线段,cur = N + 1; P_cur = P[cur];
  3. 若当前线段与下一线段的有效范围有重叠区域(如下图绿色阴影区),则需判断定位点到两条线段的距离,以较短的为准,确定校正点和线路选择。

基于路线的定位纠偏

2.4 虚拟和现实的单位长度映射

WebGL中的单位长度与现实世界的单位长度并没有确定的映射关系,暂时还无法准确进行映射。通过测试,暂且选择1(米):15(WebGL单位长度)。

3. demo演示

演示视频:WebAR技术探索-导航中的应用