Web 前端也能做的 AR 互动

3,667
原文链接: tgideas.qq.com

  一、项目体验       以往的AR,都是要在某个APP内才可以体验到的,例如pokemon go和QQ AR火炬传递活动。

       我们团队在做技术储备的时候,发现Android设备下微信、手Q支持getUserMedia()网页拉起摄像头,并通过createObjectURL把数据流传给video在页面展示,以营造出实时视频的效果。据最新统计数据显示,安卓操作系统占有全球移动智能手机系统86.2%的市场份额。也就是说,我们可以基于微信和手Q来做网页的AR互动啦!!!(http://digi.tech.qq.com/a/20160822/034526.htm?t=1471877263208) 于是我们基于实际需求发起并开发了一个WebAR的H5项目,玩家进来页面可以在不安装额外APP的情况下,体验到AR带来的乐趣;针对不支持实时视频的浏览器,就提供3D全景体验版。进入页面根据提示操作即可完成WebAR小游戏,跳转落地页。

   

  

    二、技术实现   2.1 WebRTC——WebAR的基础        无论是APPAR还是WebAR的一个最基础要实现的功能就是实时视频效果。WebRTC(网页实时通信,Web Real-Time Communication)是一个支持网页浏览器进行实时语音对话或视频对话的API。getUserMedia()是WebRTC的其中一个API,就是支持网页拉起摄像头的API,摄像头获取到的数据流会以<video>标签作为载体呈现在页面上,这就给了我们一个很好的信息,可以在video上叠加任何我们需要的内容和操作,从而营造出WebAR的效果。   关键代码:
function getMedia() { 
 if (navigator.getUserMedia) { 
 navigator.getUserMedia({ 
 'video': { 
 'optional': [{ 
 }] 
 }
 }, successFunc, errorFunc); 
 } 
 else { 
 alert('Native device media streaming (getUserMedia) not supported in this browser.'); 
 }
} 
var localStream;
function successFunc(stream) { 
 document.getElementById('video').src = window.URL && window.URL.createObjectURL(stream) || stream;
 localStream = stream; 
} 
function errorFunc(e) { 
 alert('Error!'+e); 
} 
function closeMedia() { 
 localStream.stop();
 document.getElementById('video').src = '';
}

 (http://blog.csdn.net/journey191/article/details/40744015)

  2.2 3D模型——WebGL、three.js、3DMAX       这次做的WebAR非常酷炫的一个体验是他的3D模型展示。要在网页展示3D模型,需要先把模型放到3DMAX里进行预处理(过程会遇到很多bug和困难),导出成js文件,再借助three.js在页面里建立模型、调整动画等(过程也会遇到很多bug和困难)。这块工作主要是四姑娘(signhuang)来负责的,让我们期待四姑娘后面更详细心酸的分享吧。   2.3 3D全景——three.js、球体全景       3D全景的制作有很多种方法,CSS3、Flash、Krpano等,因为3D模型动画我们是借助three.js在页面建模的,所以3D全景我们也考虑用three.js来制作。     三、兼容情况   3.1 getUserMedia()        由于苹果的安全机制问题,iOS设备任何浏览器都不支持getUserMedia()。      最终数据展示,Android设备下,有99.45%的设备在微信是支持getUserMedia()的,98.05%的设备在手Q是支持getUserMedia()的。而我们之前测试机型里面,本机浏览器、QQ浏览器对getUserMedia()都有不同程度的支持。      2015年底前,也就是chrome47版本前,chrome是支持http页面拉起摄像头的,出于安全问题考虑,chrome47版本后只支持https页面拉起摄像头。

(http://caniuse.mojijs.com/Home/Html/item/key/stream/index.html)   3.2 3D模型&3D全景        WebGL是一项利用JavaScript API渲染交互式3D电脑图形和2D图形的技术,可兼容任何的网页浏览器,无需加装插件。WebGL在现代浏览器中已经被广泛支持。3D模型在移动设备浏览器上的兼容情况还是很好的,已测试机型里面,92%的设备是支持浏览器3D建模和动画的。

(http://caniuse.mojijs.com/Home/Html/item/key/webgl/index.html)     四、遇到的问题   4.1 getusermedia()   4.1.1 前后摄像头 遇到问题:       使用getUserMedia()拉起摄像头时,默认是拉起前置摄像头的,但是要营造WebAR的效果,肯定是需要拉起后置摄像头的。   解决方法:       因为在获取设备源ID的时候,前置摄像头会排在后置摄像头前,不单独设置的话,就会使用第一个获取到的设备源ID,也就是前置摄像头的ID。稍微增加点预处理即可。   关键代码:
var exArray = []; //存储设备源ID 
if (navigator.getUserMedia) { 
 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); 
 } 
 }
 navigator.getUserMedia({ 
 'video': { 
 'optional': [{ 
 'sourceId': exArray[1] //0为前置摄像头,1为后置 
 }] 
 },
 'audio':false 
 }, successFunc, errorFunc); 
 });
}

 

4.1.2 摄像头全屏 遇到问题:     摄像头拍摄的内容不能完美的全屏,上下总有留白,不完美。尝试了多种原生设置来设置<video>大小,希望能完美铺满全屏,然而都不成功。   解决方法:      上图是用CSS设置了<video>为红色背景,并且设置了宽高铺满全屏。<video>标签确实如期待的展现,但摄像头拍摄到的内容却不会被合理铺满全屏。可以理解为就像我们平常拍摄的视频,是有固定宽高比的,在浏览器宽高比不同又要求视频全部显示时,就会出现上下留白或者左右留白的情况。测试发现,<video>标签不需要另外设置宽高,会默认为铺满全屏并且溢出,那我们在外层增加一个div并且设置为浏览器宽高,再增加 overflow:hidden就可以模拟全屏的效果了。

    

  关键代码:
<style>
#videoBox{position: absolute;left: 50%;top: 50%;z-index: 1;-webkit-transform: translate(-50%,-50%);-webkit-transform-origin: 50% 50%;}
#videoWrap{position: relative;left: 0;top: 0;background: #4CAABE;overflow: hidden;}
</style>
<div id="videoWrap">
 <video autoplay="autoplay" id="videoBox"></video>
</div>
<script>
var videoWrap = document.getElementById('videoWrap');
videoWrap.style.width = window.innerWidth + 'px';
videoWrap.style.height = window.innerHeight + 'px';
</script>
  4.1.3 页面无法针对模型点击 遇到问题:     本来页面设置的是让用户点击3D模型来进行交互的,可发现没办法单独点击到模型,整个交互就不能进行下去了。   解决方法:       修改了页面的点击交互方式,改用坐标瞄准判定的方式。页面中心设置一个瞄准区域,用户移动手机让3D模型处在瞄准区域内,即判定为成功,进行下一步。这样就避免了页面需要点击的情况。      思路是:3D模型和camera都有自己的三维坐标,new THREE.Vector3获取到两者的坐标,a.angleTo(b)求他们的夹角,夹角小于设定范围,则判断为已瞄准。       由于最开始的需求是要做一个tips,提醒用户向左转或者向右转,用的是new THREE.Vector2获取到两者在Y面的坐标,再用a.angle()-b.angle()求得两者在Y面的夹角来判断当前camera在模型的右侧还是左侧。新需求相当于要增加上下的判定,用new THREE.Vector2获取到两者在X面的坐标,再用a.angle()-b.angle()求得两者在X面的夹角。Y面夹角和X面夹角都小于设定范围时,则判断为已瞄准。  

  关键代码:
/***瞄准判定***/
var v = camera.getWorldDirection();
//左右判断
var a1 = new THREE.Vector2( a.position.x, a.position.z );
var b1 = new THREE.Vector2( v.x, v.z );
dt1 = a1.angle()-b1.angle();
//上下判断
var a2 = new THREE.Vector2( a.position.y, a.position.z );
var b2 = new THREE.Vector2( v.y, v.z );
dt2 = a2.angle()-b2.angle();
if(Math.abs(dt1)<0.1 && Math.abs(dt2)<0.1){
 //居中啦!
}

 

4.2 3D全景   4.2.1 iOS和Android初始化3D全景的朝向不一致 遇到问题:     iOS设备在任何朝向(东南西北)打开3D全景,都是看向一个固定方向的。

      Android设备在不同朝向(东南西北)打开3D全景,看向的是不同方向。

      这个页面为了降低交互难度以及固定让3D模型出现在舞台上(背景比较好看),需要的是在不同朝向(东南西北)打开3D全景,都是看向一个固定方向的。   解决方法:       页面加载完毕初始化camera,获取到camera看向的矢量(不会根据看向角度不同而不同),lookAt(target)旋转3D全景朝向那个位置,这样每次打开页面都会朝向一个固定的方向,再rotate各个方向进行最终调整。   关键代码:
var v = camera.getWorldDirection();
var geometry = new THREE.SphereGeometry( 1000, 16, 8 );
 geometry.scale( -1, 1, 1 );
var material = new THREE.MeshBasicMaterial( {
 map: new THREE.TextureLoader().load( 'bg.jpg' )
} );
var stage = new THREE.Mesh( geometry, material );
stage.lookAt(new THREE.Vector3( v.x, 0, v.z ));
stage.rotateY((85 * Math.PI)/180);
stage.rotateZ((-4 * Math.PI)/180);
scene.add( stage );

 

4.2.2 初始化定位 遇到问题:     用户进入页面时候的手机角度有各种情况,可能是竖着手机扫描二维码,然后平放手机等待加载完成查看页面。页面初始化方向和最终查看方向差太多的话,渲染的全景、3D模型等的位置可能会偏移很大。   解决方法:     进入页面捕获camera看向的矢量,加载完成捕获当前camera看向的矢量,有偏移的话重新初始化全景、3D模型等的位置和角度。   关键代码:
if(!hasMoved && typeof(defaultCameraDirection)!='undefined'){
 var v = camera.getWorldDirection();
 if( v.x != defaultCameraDirection.x
 || v.y != defaultCameraDirection.y
 || v.z != defaultCameraDirection.z
 ){
 hasMoved = true;
 resetLocationAndRotation();//初始化
 }
}

 

4.3 不支持情况排除   4.3.1 getUserMedia()、three.js、陀螺仪   ①判断是否支持getUserMedia()
function getMedia() { 
 if (navigator.getUserMedia) { 
 }, successFunc, errorFunc);//errorFunc是设备支持getUserMedia但是用户不同意调用摄像头的情况
 } 
 else { 
 //设备不支持getUserMedia
 }
} 
function errorFunc(e) { 
 alert('Error!'+e); 
}

 

②用Detector.js判断是否支持WebGL
<script src="https://github.com/mrdoob/three.js/blob/master/examples/js/Detector.js"></script>
<script>
if(!Detector.webgl) {
 //不支持WebGL
}else{
 //支持WebGL
}
</script>

 

③使用three.js有报错时
console.error = (function(origin){ 
 return function(errorlog) {
 if(/THREE/.test(errorlog)) {
 //Three报错
 }
 } 
})(console.error);
console.warn = (function(origin){ 
 return function(errorlog) {
 if(/^THREE.WebGLRenderer:$/.test(errorlog)) {
 //Three渲染时报错
 }
 } 
})(console.warn);

 

④设备不支持陀螺仪
if(window.DeviceOrientationEvent) {
 //支持陀螺仪
}else{
 //不支持陀螺仪
}

 

4.3.2 iOS8未知错误 遇到问题:       有位远方的同事是iPhone 5S,测试发现加载到100%了,加载界面一直不消失,页面卡住。   解决方法:       本身页面的设置是:为了防止加载界面隐藏后,3D全景和3D模型没渲染完成显示一片空白,于是加了个判定,建模渲染完成才隐藏加载界面。找身边的同事借了iPhone 5S测试,能顺利进入3D全景AR界面,所以不是设备问题。问了下微信版本也是最新的,系统版本是iOS8,而没有问题的iPhone 5S的系统版本是iOS9,所以考虑是系统版本导致的问题。但是设备不在身边没办法一步步排除可能性,查了下iOS8的占比只有2.8%,该游戏用户群体iOS设备占比少于30%,所以决定放弃这部分用户的页面体验,直接跳转落地页。   关键代码:
var agent = navigator.userAgent.toLowerCase() ;
var version;
if(agent.indexOf("like mac os x") > 0){
 //ios
 var regStr_saf = /os [\d._]*/gi ;
 var verinfo = agent.match(regStr_saf) ;
 version = (verinfo+"").replace(/[^0-9|_.]/ig,"").replace(/_/ig,".");
}
var version_str = version+"";
if(version_str != "undefined" && version_str.length >0){
 version=version.substring(0,1);
 if(version>5 && version <9){
 //版本为iOS8以下直接跳转落地页
 }
}

 

  五、结语

 

做新技术研究和实践的过程中会遇到没有先例的坑,去查API文档,曲线救国的方式解决问题(有时候觉得重构/前端是一个需要带着小聪明的工种XD)。很感谢游戏公众号的同事们给予的大力支持让我们的页面能顺利上线并被推送给玩家,看着数据蹭蹭蹭的涨,终于觉得努力没有白费了。

最终页面的数据展示出,部分用户设备是支持拉起摄像头的,但可能出于安全问题的考虑,他们拒绝拉起。这是我们后续工作要考虑的一个问题,如何保护用户隐私以及让用户信任我们。

总的来说,这是一次很有趣充实的新技术研究和实践,学习的过程是很让人幸福的。