Web前端AR技術探索-導航中的應用

多多洛愛學習發表於2018-12-28

本文探索在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

    //不支援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

    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上表現有差異。

    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物件。

{
    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等介面方法用於在場景中新增/刪除物體或更新渲染等。

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方法:

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方法獲取到的定位資料包含如下資訊:

{
	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技術探索-導航中的應用

相關文章