0. 背景

AR引擎的运行的基本流程是由终端摄像头采集像素帧,传给识别跟踪算法处理,拿到姿态矩阵,然后由渲染引擎渲染出模型,并将姿态矩阵设置给模型,以让模型的位置紧贴在Marker图或者某个平面上。

这个过程中有比较完备的平台比如Unity,可以完整处理从采集帧、处理帧、渲染模型的整个流程,甚至还有强大的跨全平台能力,一套代码全平台运行。但是当AR引擎直接运行在终端上时,跨平台的问题就需要各平台开发者自己解决了。

其中渲染的问题,要么用opengl直接绘制3D模型,要么选择第三方的渲染引擎(大多也是基于opengl的封装),要么自研渲染引擎。其中opengl提供的能力太过于原始,写代码工作量太大;自研渲染引擎代价更大,不做考虑;选择第三方的渲染引擎,则面临着模型类型支持不全,库体积控制,跨平台等问题。所以考虑在终端启动一个webview来做渲染的工作,首先最大的优势是跨平台几乎成本为0,用three.js来做3D渲染压缩后体积在400多K,模型格式的话得益于强大的前端社区,各种格式模型loader的支持都比较全面,参见https://threejs.org/examples/#webgl_loader_3ds
于是尝试了一下这个方案,在这里总结一下实践过程的基本流程和注意点。

1.摄像头帧渲染方案选择

现有终端native版本AR引擎(以android为例)做背景帧渲染的方式是,由终端取帧,opengl渲染背景和模型。要修改成web版本的。

1.1 webrtc取帧,webgl渲染

webrtc目前浏览器内核支持不好,很多机型自带内核都无法使用,即使能取到视频帧,从webview把视频帧传到终端再传到算法,这过程中的各种格式转换代价太大,耗时太长,会导致帧率低不可接受。

1.2 终端取帧,webgl渲染

这种方式也有问题

首先android终端取到的帧是yuv格式的而不是rgb,这一点倒可以和终端用shader在opengl渲染一样,写一个shader将其渲染到webgl上。问题还是后面会传输的问题,我这边没有找到一个比较简单的能快速(10ms以内)的将一张1920*1080大图yuv二进制数据传到webview中的方法,如果哪位同学知道,可以提点我一下~

考虑到背景帧采集和渲染在终端做的代价并不是特别大,所以在背景渲染方面,还是保留原来的方案,web化的思路主要考虑到模型渲染的方向。下面所有的实践均为android平台下的。

2. 模型加载与渲染

用three.js创建一个3D场景的基本流程:

1
2
3
4
5
6
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(45, screen.width / screen.height, 0.1, 1000);
renderer = new THREE.WebGLRenderer();
var model = loadModel();
scene.add(model);
renderer.render(scene, camera);

AR引擎要做的事情,就是给camera设置一个根据物理摄像头内参计算的一个投影矩阵,然后给model设置一个姿态矩阵,让model看起来像是紧贴在marker图或者平面上,达到AR的效果。

其中loadModel这块不多讲,可以看看https://threejs.org/examples/#webgl_loader_3ds这里的Demo

3.终端view与webview的叠加

3.1 在view内打开网页

在activity中创建一个view

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
WebView webView=new WebView(this);
WebSettings wSet = webView.getSettings();
wSet.setJavaScriptEnabled(true);
webView.loadUrl("file://xxxxxx");
```
这样会在默认浏览器中打开指定路径的网页文件,加上下面的处理
```java
webView.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest req) {
// TODO Auto-generated method stub
//返回值是true的时候控制去WebView打开,为false调用系统浏览器或第三方浏览器
view.setLayerType(View.LAYER_TYPE_SOFTWARE,null);
return true;
}
});

这样就可以在view中打开网页了

3.2 webview透明和叠加设置

首先是前端创建render的时候,需要启用透明设置,并将背景色设置为透明

1
2
3
4
5
6
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
preserveDrawingBuffer: true
});
renderer.setClearColor(0x000000, 0);

body背景色设置为透明

1
2
3
body {
background-color:rgba(0,0,0,0);
}

终端的WebView背景色设置为透明

1
webView.setBackgroundColor(Color.TRANSPARENT);

在activity的layout中先后添加background的view和webview

1
2
3
4
FrameLayout mainLayout = new FrameLayout(this);
mainLayout.addView(backgroundView);
mainLayout.addView(webView);
setContentView(mainLayout);

至此,就达到了一个webView盖住backgroundView,但是webView背景透明的目的。

3.3 终端与webview的通信

终端与webview通信的方式有很多种,考虑到背景帧采集和渲染完全交给终端,这里实际上只需要终端单向的向webview发消息,所以可以采用下面这种方式:

1
2
3
4
5
String projectionMatrixStr = getMatrixString(projectionMatrix);
String poseMatrixStr = getMatrixString(poseMatrix);
String url = "javascript:onRenderFrame("+visible+"," + poseMatrixStr + ","+ projectionMatrixStr +");void(0);";
Log.i(TAG,url);
webView.loadUrl(url);

在webview网页里边定义一个window下的onRenderFrame函数,来接收投影矩阵和姿态矩阵并渲染,ar引擎每次触发帧回调的时候,调用这个函数,即可实现终端到webview的通信:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//大致逻辑,省去了部分细节
window.onRenderFrame = function(visible,poseMatrix,projectionMatrix){
if(!visible){
if(!model.visible) return;
model.visible = false;
renderer.render(scene, camera);
return;
}
model.visible = true;
transformMatrix.set.apply(transformMatrix,row2column(poseMatrix));
model.position.setFromMatrixPosition(transformMatrix);
model.setRotationFromMatrix(transformMatrix);
renderer.render(scene, camera);
}

3.4 webview内xmlhttprequest访问模型文件

在webview中通过three.js load模型文件的时候,loader默认会用一个xmlhttprequest从一个uri来load模型数据,此时如果模型是放在本地应用缓存目录下的,就会报一个跨域的错误,本地文件的file://协议是不允许跨域的,这里可以对webview设置

1
2
3
WebSettings wSet = webView.getSettings();
wSet.setAllowFileAccessFromFileURLs(true);
wSet.setAllowUniversalAccessFromFileURLs(true);

来允许网页中的脚本访问本地文件。当然这样做是十分危险的,一旦打开的网页被替换,就会有安全隐患,调试阶段可以用此方法,生产环境最好不要这样。

生产环境可以采用的方案:一是将模型转换为three.js支持的json格式,然后在其文件中加上一个可调用的js函数,在前端代码中用jsonp的方式去拿到json数据,然后用loader的parse函数来处理json,得到模型的mesh和动画; 二是将模型放到远程服务器上下载。

用这种方式还有一个要注意的地方,上面的截图是在手机上从本地加载一个3M的模型并渲染出来的全部时间,整个过程总耗时800多ms,所以AR场景启动前需要预加载好网页和模型,不然进入AR场景后模型会有一点延迟才出现。

4.设置投影矩阵和姿态矩阵

4.1 three.js Matrix4类型的行列优先

这里不得不提一下three.js的神奇代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
THREE.Matrix4.prototype = {
constructor: THREE.Matrix4,
set: function ( n11, n12, n13, n14, n21, n22, n23, n24, n31, n32, n33, n34, n41, n42, n43, n44 ) {
var te = this.elements;
te[ 0 ] = n11; te[ 4 ] = n12; te[ 8 ] = n13; te[ 12 ] = n14;
te[ 1 ] = n21; te[ 5 ] = n22; te[ 9 ] = n23; te[ 13 ] = n24;
te[ 2 ] = n31; te[ 6 ] = n32; te[ 10 ] = n33; te[ 14 ] = n34;
te[ 3 ] = n41; te[ 7 ] = n42; te[ 11 ] = n43; te[ 15 ] = n44;
return this;
}
}

一眼看上去,index是按列增加的,所以three.js的矩阵是列优先矩阵,而识别跟踪算法给出来的投影矩阵和姿态矩阵也是列优先矩阵,所以拿到投影矩阵data数组之后直接就

1
projectionMatrix.set.apply(projectionMatrix,data);

设置上去了。

结果如上图,彩色方块是在终端用opengl绘制的模型,跳舞的伊泽瑞尔是webview中渲染的模型,可以很明显的观察到,方块是紧贴在marker上的,而伊泽瑞尔则显得有点飘。

最开始我是把终端的投影矩阵和姿态矩阵都打印出来,然后在PC的chrome里边测试,设置了投影矩阵和姿态矩阵后,发现模型不能显示,于是也没多想就对姿态矩阵做了个行列转换,模型可以显示了。然而真正把网页放到webview里边后,就发现了上面的现象。

为啥marker中心已经到了屏幕边缘,人物模型还是在屏幕右侧离边缘还有一段距离的位置呢?

相机视锥体向右移动,marker图片位置不变的时候,可以看到原本渲染在屏幕中心的方块,理论位置应该跑到屏幕边缘而且只能看到一个角,那当什么时候,方块会渲染在屏幕右侧离边缘有一定距离的位置呢?看图中相机右移之后新的视锥体,若将方块z坐标绝对值减小(即方块向着摄像机移动),则方块完全不可见;若将方块z坐标绝对值增大,则原本只能看到一个角的方块,可见范围会越来越大,位置越来越靠近屏幕中心,理论上z无限大的时候,方块会固定渲染在屏幕中心。同样的,若x坐标和y坐标发生偏移,那么将直接影响方块渲染的位置

那么是姿态矩阵的xyz坐标出了问题吗? 观察现象是移动和旋转都能正常体现在模型上,打印出模型的xyz坐标也能和终端保持一致,所以这里就能推论到,世界坐标系中的模型位置是准确的,那么一定是相机投影的过程出了问题,3D物体由相机的视锥体投影到规则观察体中的时候,投影坐标计算不准确,导致本应该渲染到边缘外的模型渲染到了边缘内。

回过头来再去观察相机投影矩阵的设置过程,发现three.js的矩阵的set函数

1
2
3
4
5
6
7
8
9
10
11
12
function ( n11, n12, n13, n14, n21, n22, n23, n24, n31, n32, n33, n34, n41, n42, n43, n44 ) {
var te = this.elements;
te[ 0 ] = n11; te[ 4 ] = n12; te[ 8 ] = n13; te[ 12 ] = n14;
te[ 1 ] = n21; te[ 5 ] = n22; te[ 9 ] = n23; te[ 13 ] = n24;
te[ 2 ] = n31; te[ 6 ] = n32; te[ 10 ] = n33; te[ 14 ] = n34;
te[ 3 ] = n41; te[ 7 ] = n42; te[ 11 ] = n43; te[ 15 ] = n44;
return this;
}

确实是列优先矩阵没错,但是用来给它赋值的set函数,居然是按行来取值的!那么行和列搞反了会有什么后果了?将透视投影算法中投影矩阵做个行列转换看看(上边是原矩阵,下边是行列转换后的矩阵,公式中a≈-1,b≈-2):

可以很明显的得到两个结论:

1.行列转换后,投影后坐标随世界坐标系xyz变化的趋势与行列转换前相同

2.投影后xyz坐标都发生了变化,其中x和y的绝对值变为了原来的一半,z坐标以-3为界,偏大或偏小

这也就是为什么上面gif中看到的现象是,一眼看上去姿态是对的,但是上下左右移动的时候总有一点点偏移。设置投影矩阵时加上行列转换:

1
projectionMatrix.set.apply(projectionMatrix,row2column(data);

重新打包运行,可以看到,现在webview中的渲染和终端用opengl渲染方块的姿态完全一致了。

4.2 将变换设置给相机以方便调整模型姿态

一开始3D模型默认的姿态是面向我们的

要想让它“站在”marker图片上,需要将它旋转90°,如果提前旋转模型,每帧给模型设置姿态矩阵后,都会覆盖这个旋转,所以只有没帧设置完姿态矩阵后,再去旋转一次

1
2
3
model.position.setFromMatrixPosition(transformMatrix);
model.setRotationFromMatrix(transformMatrix);
model.rotateX(Math.PI/2)


这样模型就站在屏幕上了。

这样做每次都要多一个旋转的步骤,我们还可以选择把姿态矩阵设置给相机,这样模型就只需要在开始时调整好角度和位置就行了。相机与模型的位置是相对固定的,对模型做的变换,对象相机做逆变换,效果是一样的,下面是一个示意图过程。

所以我们只需要取姿态矩阵的逆矩阵设置给相机就好了,模型的x轴旋转在模型加载好之后设置一次就行了。

1
2
3
transformMatrix.getInverse(transformMatrix);
camera.position.setFromMatrixPosition(transformMatrix);
camera.setRotationFromMatrix(transformMatrix);

4.3 提高骨骼动画渲染帧率

一开始的想法是在终端每次调用onRenderFrame的时候,绘制一次,后来发现假如终端设备性能较差,算法识别帧率只能达到20甚至20以下的时候,模型骨骼动画会看起来十分卡顿,而单纯的webgl渲染一般都能达到40帧以上,所以把render的逻辑从onRenderFrame移出来,onRenderFrame中仅仅设置一下相机的姿态,网页中使用requestAnimationFrame来尽可能高帧率的渲染。

1
2
3
4
5
6
function animate() {
requestAnimationFrame(animate, renderer.domElement);
var delta = clock.getDelta();
model.update(delta);
renderer.render(scene, camera)
};

5.业界类似做法

以前看过webar的现状,发现现有web AR引擎的采集帧→识别跟踪算法→渲染流程中,采集帧依赖浏览器对webrtc的支持,某些地方可用但尚不能普及;识别跟踪算法在浏览器中运行性能不足,可用识别简单的黑白方块marker,但是无法识别复杂的任意图片marker;渲染则支持较好,毫无压力。

当时也有想过将前两个步骤交给终端来做,web端仅仅负责渲染,后来业界其实也出现了一些这样的做法,现在也算是自己实践了一遍。

5.1 TBS内核对AR能力的支持

https://x5.tencent.com/tbs/guide/tbsar.html

采用的方案是将识别跟踪算法交给内核来做,采集帧因为本身就是个浏览器内核,所以自身支持了webrtc,这块也可以交给前端来做,渲染则用webgl来渲染。

5.2 google arcore web版本

google最近推出的ar core也提到了web版本的ar https://developers.google.com/ar/develop/web/getting-started

他们的做法就是通过终端来提供ar的识别跟踪算法能力,然后用一个基于three.js做的渲染引擎来显示AR的3D模型https://github.com/google-ar/three.ar.jshttps://github.com/google-ar/three.ar.js“),这个就和我这边做的工作非常类似了,只不过我这边用的是部门自研的识别跟踪算法,ar core的web版用的是以后要集成在系统里边的ar core算法。

文中提到

AR is still cutting edge technology, and while there’s no standard for AR on the web today, developers can start experimenting with using web tools to create AR-enhanced web experiences, using a javascript library, three.ar.js, and prototype browsers for ARCore on Android and ARKit on iOS.

如果未来有一天AR能力会集成到所有主流系统平台或者浏览器内核,或许前端真的没有必要追求所有的流程都运行在浏览器中。

☞ 参与评论