加強現實(Augmented Reality)是一種在視覺上呈現虛擬物體與現實場景結合的技術。Apple 公司在 2017 年 6 月正式推出了 ARKit,iOS 開發者能夠在這個平臺上使用簡單便捷的 API 來開發 AR 應用程序。node
本文將結合美團到餐業務場景,介紹一種基於位置服務(LBS)的 AR 應用。使用 AR 的方式展示商家相對用戶的位置,這會給用戶帶來身臨其境的沉浸式體驗。下面是實現效果:算法
iOS 平臺的 AR 應用一般由 ARKit 和渲染引擎兩部分構成:數組
ARKit 是鏈接真實世界與虛擬世界的橋樑,而渲染引擎是把虛擬世界的內容渲染到屏幕上。本部分會圍繞這兩個方面展開介紹。網絡
ARKit 的 ARSession 負責管理每一幀的信息。ARSession 作了兩件事:拍攝圖像並獲取傳感器數據;對數據進行分析處理後逐幀輸出。以下圖:app
設備追蹤確保了虛擬物體的位置不受設備移動的影響。在啓動 ARSession 時須要傳入一個 ARSessionConfiguration 的子類對象,以區別三種追蹤模式:dom
其中 ARFaceTrackingConfiguration 能夠識別人臉的位置、方向以及獲取拓撲結構。此外,還能夠探測到預設的 52 種豐富的面部動做,如眨眼、微笑、皺眉等等。ARFaceTrackingConfiguration 須要調用支持 TrueDepth 的前置攝像頭進行追蹤,顯然不能知足咱們的需求,這裏就不作過多的介紹。下面只針對使用後置攝像頭的另外兩種類型進行對比。工具
ARWorldTrackingConfiguration 提供 6DoF(Six Degree of Freedom)的設備追蹤。包括三個姿態角 Yaw(偏航角)、Pitch(俯仰角)和 Roll(翻滾角),以及沿笛卡爾座標系中 X、Y 和 Z 三軸的偏移量:優化
不只如此,ARKit 還使用了 VIO(Visual-Inertial Odometry)來提升設備運動追蹤的精度。在使用慣性測量單元(IMU)檢測運動軌跡的同時,對運動過程當中攝像頭拍攝到的圖片進行圖像處理。將圖像中的一些特徵點的變化軌跡與傳感器的結果進行比對後,輸出最終的高精度結果。動畫
從追蹤的維度和準確度來看,ARWorldTrackingConfiguration 很是強悍。但如官方文檔所言,它也有兩個致命的缺點:ui
因爲在追蹤過程當中要經過採集圖像來提取特徵點,因此圖像的質量會影響追蹤的結果。在光線較差的環境下(好比夜晚或者強光),拍攝的圖像沒法提供正確的參考,追蹤的質量也會隨之降低。
追蹤過程當中會逐幀比對圖像與傳感器結果,若是設備在短期內劇烈的移動,會很大程度上干擾追蹤結果。追蹤的結果與真實的運動軌跡有誤差,那麼用戶看到的商家位置就不許確。
AROrientationTrackingConfiguration 只提供對三個姿態角的追蹤(3DoF),而且不會開啓 VIO。
Because 3DOF tracking creates limited AR experiences, you should generally not use the AROrientationTrackingConfiguration class directly. Instead, use the subclass ARWorldTrackingConfiguration for tracking with six degrees of freedom (6DOF), plane detection, and hit testing. Use 3DOF tracking only as a fallback in situations where 6DOF tracking is temporarily unavailable.
一般來說,由於 AROrientationTrackingConfiguration 的追蹤能力受限,官方文檔不推薦直接使用。可是鑑於:
最終咱們決定使用 AROrientationTrackingConfiguration。這樣的話,即使是在夜晚,甚至遮住攝像頭,商家的位置也可以正確的進行展示。並且劇烈晃動帶來的影響很小,商家位置雖然會出現短暫的角度誤差,可是在傳感器數值穩定下來後就會獲得校準。
ARKit 使用笛卡爾座標系度量真實世界。ARSession 開啓時的設備位置便是座標軸的原點。而 ARSessionConfiguration 的 worldAlignment 屬性決定了三個座標軸的方向,該屬性有三個枚舉值:
三種枚舉值對應的座標軸以下圖所示:
對於 ARWorldAlignmentGravity 來講,Y 軸方向始終與重力方向平行,而其 X、Z 軸方向仍然由設備的姿態肯定。這種座標設定適用於計算擁有重力屬性的物體座標,好比放置一排氫氣球,或者執行一段籃球下落的動畫。
對於 ARWorldAlignmentGravityAndHeading 來講,X、Y、Z 三軸固定朝向正東、正上、正南。在這種模式下 ARKit 內部會根據設備偏航角的朝向與地磁真北(非地磁北)方向的夾角不斷地作出調整,以確保 ARKit 座標系中 -Z 方向與咱們真實世界的正北方向吻合。有了這個前提條件,真實世界位置座標纔可以正確地映射到虛擬世界中。顯然,ARWorldAlignmentGravityAndHeading 纔是咱們須要的。
商家座標的肯定,包含水平座標和垂直座標兩部分:
商家的水平位置只是一組經緯度值,那麼如何將它對應到 ARKit 當中呢?咱們經過下圖來講明:
藉助 CLLocation 中的 distanceFromLocation:location
方法,能夠計算出兩個經緯度座標之間的距離,返回值單位是米。咱們能夠以用戶的經度 lng一、商家的緯度 lat2 做一個輔助點(lng1, lat2),而後分別計算出輔助點距離商家的距離 x、輔助點距離用戶的距離 z。ARKit 座標系一樣以米爲單位,於是能夠直接肯定商家的水平座標(x, -z)。
對商家地址進行中文分詞能夠提取出商戶所在樓層數,再乘以一層樓大概的高度,以此肯定商家的垂直座標 y 值:
一般咱們想展現的信息,都是經過 UIView 及其子類來實現。可是 ARKit 只負責創建真實世界與虛擬世界的橋樑,渲染的部分仍是要交給渲染引擎來處理。Apple 給咱們提供了三種可選的引擎:
強大的 Metal 引擎包含了 MetalKit、Metal 着色器以及標準庫等等工具,能夠更高效地利用 GPU,適用於高度定製化的渲染要求。不過 Metal 對於當前需求來講,有些大材小用。
SpriteKit 是 2D 渲染引擎,它提供了動畫、事件處理、物理碰撞等接口,一般用於製做 2D 遊戲。SceneKit 是 3D 渲染引擎,它創建在 OpenGL 之上,支持多通道渲染。除了能夠處理 3D 物體的物理碰撞和動畫,還能夠呈現逼真的紋理和粒子特效。SceneKit 能夠用於製做 3D 遊戲,或者在 App 中加入 3D 內容。
雖然咱們能夠用 SpriteKit 把 2D 的卡片放置到 3D 的 AR 世界中,可是考慮到擴展性,方便以後爲 AR 頁面添加新的功能,這裏咱們選用 3D 渲染引擎 SceneKit。
咱們能夠直接經過建立 ARSCNView 來使用 SceneKit。ARSCNView 是 SCNView 的子類,它作了三件事:
SceneKit 中使用 SCNNode 來管理 3D 物體。設置 SCNNode 的 geometry 屬性能夠改變物體的外觀。系統已經給咱們提供了例如 SCNBox、SCNPlane、SCNSphere 等等一些常見的形狀,其中 SCNPlane 正是咱們所須要的卡片形狀。藉助 UIGraphics 中的一些方法能夠將繪製好的 UIView 渲染成一個 UIImage 對象。根據這張圖片建立 SCNPlane,以做爲 SCNNode 的外觀。
ARKit 中的物體都是近大遠小。只要固定好 SCNPlane 的寬高,ARKit 會自動根據距離的遠近設置 SCNPlane 的大小。這裏列出一個在屏幕上具體的像素數與距離的粗略計算公式,爲筆者在開發過程當中摸索的經驗值:
也就是說,假如 SCNPlane 的寬度爲 30,距離用戶 100 米,那麼在屏幕上看到這個 SCNPlane 的寬度大約爲 \(530 / 100 \times 30 = 159\) pt。
對於距離用戶過近的商家卡片,會出現兩個問題:
這裏咱們將距離用戶過近的卡片映射到稍遠的位置。以下圖所示,距離用戶的距離小於 d 的卡片,會被映射到 d-k ~ d 的區間內。
假設某商家距離用戶的真實距離爲 x,映射後的距離爲 y,映射關係以下:
這樣既解決了距離過近的問題,又能夠保持卡片之間的遠近關係。用戶位置發生位移到達必定閾值後,會觸發一次新的網絡請求,根據新的用戶位置來從新計算商家的位置。這樣隨着用戶的移動,卡片的位置也會持續地更新。
SceneKit 會在渲染每一幀以前,根據 SCNNode 的約束自動調整卡片的各類行爲,好比碰撞、位置、速度、朝向等等。SCNConstraint 的子類中 SCNLookAtConstraint 和 SCNBillboardConstraint 能夠約束卡片的朝向。
SCNLookAtConstraint 可讓卡片始終朝向空間中某一個點。這樣相鄰的卡片會出現交叉現象,用戶看到的卡片信息極可能是不完整的。使用 SCNBillboardConstraint 能夠解決這個問題,讓卡片的朝向始終與攝像頭的朝向平行。
下面是建立卡片的示例代碼:
// 位置
SCNVector nodePosition = SCNVectorMake(-200, 5, -80);
// 外觀
SCNPlane *plane = [SCNPlane planeWithWidth:image.size.width
height:image.size.height];
plane.firstMaterial.diffuse.contents = image;
// 約束
SCNBillboardConstraint *constraint = [SCNBillboardConstraint billboardConstraint];
constraint.freeAxes = SCNBillboardAxisY;
SCNNode *node = [SCNNode nodeWithGeometry:plane];
node.position = nodePosition;
node.constraints = @[constraint];
複製代碼
若是同一個方向的商家數量有不少,那麼卡片會出現互相重疊的現象,這會致使用戶只能看到離本身近的卡片。這是個比較棘手的問題,若是在屏幕上平鋪卡片的話,既犧牲了對商家高度的感知,又沒法體現商家距離用戶的遠近關係。
通過漫長的討論,咱們最終決定採起點擊重疊區域後,卡片向四周分散的交互方式來解決重疊問題,效果以下:
下面圍繞點擊和投射兩個部分,介紹該效果的實現原理。
熟悉 Cocoa Touch 的朋友都瞭解,UIView 的層級結構是經過 hit-testing 來判斷哪一個視圖響應事件的,在 ARKit 中也不例外。
ARSCNView 可使用兩種 hit-testing:
hitTest:types:
方法:查找點擊的位置所對應的真實世界中的物體或位置hitTest:options:
方法:查找點擊位置所對應的虛擬世界中的內容。顯然,hitTest:options:
纔是咱們須要的。在 3D 世界中的 hit-testing 就像一束激光同樣,向點擊位置的方向發射,hitTest:options:
的返回值就是被激光穿透的全部卡片的數組。這樣就能夠檢測到用戶點擊的位置有哪些卡片發生了重疊。
這裏簡單介紹一下散開的實現原理。SCNSceneRenderer 協議有兩個方法用來投射座標:
projectPoint:
:將三維座標系中點的座標,投射到屏幕座標系中unprojectPoint:
:將屏幕座標系中的點的座標,投射到三維座標系中其中屏幕座標系中的點也是個 SCNVector3,其 z 座標表明着深度,從 0.0(近裁面)到 1.0(遠裁面)。散開的總體過程以下:
對於排布比較密集的商家,卡片的重疊現象會很嚴重。點擊散開的卡片數量太多對用戶不是很友好。後臺在返回用戶附近的商家數據時,按照商家的經緯度座標,使用 K-Means 聚類算法進行二維聚類,將距離很近的商家聚合爲一個卡片。因爲這些商家的位置大致相同,能夠採用一個帶有數字的卡片來表明幾個商家的位置:
實測中發現,距離較近的卡片在重疊區域會發生閃爍的現象:
這裏要引入一個 3D 渲染引擎廣泛要面對的問題——可見性問題。簡單來講就是屏幕上哪些物體應該被展現,哪些物體應該被遮擋。GPU 最終應該在屏幕上渲染出全部應該被展現的像素。
可見性問題的一個典型的解決方案就是畫家算法,它像一個頭腦簡單的畫家同樣,先繪製最遠的物體,而後一層層的繪製到最近的物體。可想而知,畫家算法的效率很低,繪製較精細場景會很消耗資源。
深度緩衝彌補了畫家算法的缺陷,它使用一個二維數組來存儲當前屏幕中每一個像素的深度。以下圖所示,某個像素點渲染了深度爲 0.5 的像素,並儲存該像素的深度:
下一幀時,當另一個物體的某個像素也在這個像素點渲染時,GPU 會對該像素的深度與緩衝區中的深度進行比較,深度小者被保留並被存入緩衝區,深度大者不被渲染。以下圖所示,該像素點下一幀要渲染的像素深度爲 0.2,比緩衝區存儲的 0.5 小,其深度被存儲,而且該像素被渲染在屏幕上:
顯然,深度緩衝技術相比畫家算法,能夠極大地提高渲染效率。可是它也會帶來深度衝突的問題。
深度緩衝技術在處理具備相同深度的像素點時,會出現深度衝突(Z-fighting)現象。這些具備相同深度的像素點在競爭中只有一個「勝出」,顯示在屏幕上。以下圖所示:
若是這兩個像素點交替「勝出」,就會出現咱們視覺上的閃爍效果。因爲每一個卡片都被設置了 SCNBillboardConstraint 約束,始終朝向攝像頭方向。攝像頭輕微的角度變化,都會引發卡片之間出現部分重合。與有厚度的物體不一樣,卡片之間的深度關係變化很快,很容易出現多個卡片在屏幕同一個位置渲染的狀況。因此常常會出現閃爍的現象:
爲了解決這 Bug 般的體驗,最終決定犧牲深度緩衝帶來的渲染效率。SceneKit 爲咱們暴露了深度是否寫入、讀取緩衝區的接口,咱們將其禁用便可:
plane.firstMaterial.writesToDepthBuffer = NO;
plane.firstMaterial.readsFromDepthBuffer = NO;
複製代碼
因爲卡片內容內容相對簡單,禁用緩衝區對幀率幾乎沒什麼影響。
在到餐業務場景中,以 AR+LBS 的方式展示商家信息,能夠給用戶帶來沉浸式的體驗。本文介紹了 ARKit 的一些使用細節,總結了在開發過程當中遇到的問題以及解決方案,但願能夠給其餘開發者帶來一點參考價值。
曹宇,美團 iOS 開發工程師。2017年加入美團到店餐飲事業羣,參與美團客戶端美食頻道開發工做。
到店餐飲技術部,負責美團和點評兩個平臺的美食頻道相關業務,服務於數以億計用戶,經過更好的榜單、真實的評價和完善的信息爲用戶提供更好的決策支持,致力於提高用戶體驗。咱們同時承載全部餐飲商戶端線上流量,爲餐飲商戶提供多種營銷工具,提高餐飲商戶營銷效率,最終達到讓國人「Eat Better、Live Better」的美好願景!咱們的團隊須要經驗豐富的FE方向高級/資深工程師和技術專家,歡迎有興趣的同窗投遞簡歷至wangying49#meituan.com。