ARKit:增强现实技术在美团到餐业务的实践
前言
增強現實(Augmented Reality)是一種在視覺上呈現虛擬物體與現實場景結合的技術。Apple 公司在 2017 年 6 月正式推出了 ARKit,iOS 開發者可以在這個平臺上使用簡單便捷的 API 來開發 AR 應用程序。
本文將結合美團到餐業務場景,介紹一種基于位置服務(LBS)的 AR 應用。使用 AR 的方式展現商家相對用戶的位置,這會給用戶帶來身臨其境的沉浸式體驗。下面是實現效果:
項目實現
iOS 平臺的 AR 應用通常由 ARKit 和渲染引擎兩部分構成:
ARKit 是連接真實世界與虛擬世界的橋梁,而渲染引擎是把虛擬世界的內容渲染到屏幕上。本部分會圍繞這兩個方面展開介紹。
ARKit
ARKit 的 ARSession 負責管理每一幀的信息。ARSession 做了兩件事:拍攝圖像并獲取傳感器數據;對數據進行分析處理后逐幀輸出。如下圖:
設備追蹤
設備追蹤確保了虛擬物體的位置不受設備移動的影響。在啟動 ARSession 時需要傳入一個 ARSessionConfiguration 的子類對象,以區別三種追蹤模式:
- ARFaceTrackingConfiguration
- ARWorldTrackingConfiguration
- AROrientationTrackingConfiguration
其中 ARFaceTrackingConfiguration 可以識別人臉的位置、方向以及獲取拓撲結構。此外,還可以探測到預設的 52 種豐富的面部動作,如眨眼、微笑、皺眉等等。ARFaceTrackingConfiguration 需要調用支持 TrueDepth 的前置攝像頭進行追蹤,顯然不能滿足我們的需求,這里就不做過多的介紹。下面只針對使用后置攝像頭的另外兩種類型進行對比。
ARWorldTrackingConfiguration
ARWorldTrackingConfiguration 提供 6DoF(Six Degree of Freedom)的設備追蹤。包括三個姿態角 Yaw(偏航角)、Pitch(俯仰角)和 Roll(翻滾角),以及沿笛卡爾坐標系中 X、Y 和 Z 三軸的偏移量:
不僅如此,ARKit 還使用了 VIO(Visual-Inertial Odometry)來提高設備運動追蹤的精度。在使用慣性測量單元(IMU)檢測運動軌跡的同時,對運動過程中攝像頭拍攝到的圖片進行圖像處理。將圖像中的一些特征點的變化軌跡與傳感器的結果進行比對后,輸出最終的高精度結果。
從追蹤的維度和準確度來看,ARWorldTrackingConfiguration 非常強悍。但如官方文檔所言,它也有兩個致命的缺點:
- 受環境光線質量影響
- 受劇烈運動影響
由于在追蹤過程中要通過采集圖像來提取特征點,所以圖像的質量會影響追蹤的結果。在光線較差的環境下(比如夜晚或者強光),拍攝的圖像無法提供正確的參考,追蹤的質量也會隨之下降。
追蹤過程中會逐幀比對圖像與傳感器結果,如果設備在短時間內劇烈的移動,會很大程度上干擾追蹤結果。追蹤的結果與真實的運動軌跡有偏差,那么用戶看到的商家位置就不準確。
AROrientationTrackingConfiguration
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 屬性決定了三個坐標軸的方向,該屬性有三個枚舉值:
- ARWorldAlignmentCamera
- ARWorldAlignmentGravity
- ARWorldAlignmentGravityAndHeading
三種枚舉值對應的坐標軸如下圖所示:
對于 ARWorldAlignmentCamera 來說,設備的姿態決定了三個坐標軸的方向。這種坐標設定適用于以設備作為參考系的坐標計算,與真實地理環境無關,比如用 AR 技術丈量真實世界物體的尺寸。
對于 ARWorldAlignmentGravity 來說,Y 軸方向始終與重力方向平行,而其 X、Z 軸方向仍然由設備的姿態確定。這種坐標設定適用于計算擁有重力屬性的物體坐標,比如放置一排氫氣球,或者執行一段籃球下落的動畫。
對于 ARWorldAlignmentGravityAndHeading 來說,X、Y、Z 三軸固定朝向正東、正上、正南。在這種模式下 ARKit 內部會根據設備偏航角的朝向與地磁真北(非地磁北)方向的夾角不斷地做出調整,以確保 ARKit 坐標系中 -Z 方向與我們真實世界的正北方向吻合。有了這個前提條件,真實世界位置坐標才能夠正確地映射到虛擬世界中。顯然,ARWorldAlignmentGravityAndHeading 才是我們需要的。
商家坐標
商家坐標的確定,包含水平坐標和垂直坐標兩部分:
水平坐標
商家的水平位置只是一組經緯度值,那么如何將它對應到 ARKit 當中呢?我們通過下圖來說明:
借助 CLLocation 中的 distanceFromLocation:location 方法,可以計算出兩個經緯度坐標之間的距離,返回值單位是米。我們可以以用戶的經度 lng1、商家的緯度 lat2 作一個輔助點(lng1, lat2),然后分別計算出輔助點距離商家的距離 x、輔助點距離用戶的距離 z。ARKit 坐標系同樣以米為單位,因而可以直接確定商家的水平坐標(x, -z)。
垂直坐標
對商家地址進行中文分詞可以提取出商戶所在樓層數,再乘以一層樓大概的高度,以此確定商家的垂直坐標 y 值:
卡片渲染
通常我們想展示的信息,都是通過 UIView 及其子類來實現。但是 ARKit 只負責建立真實世界與虛擬世界的橋梁,渲染的部分還是要交給渲染引擎來處理。Apple 給我們提供了三種可選的引擎:
- Metal
- SpriteKit
- SceneKit
強大的 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 的子類,它做了三件事:
- 將設備攝像頭捕捉的每一幀的圖像信息作為 3D 場景的背景。
- 將設備攝像頭的位置作為 3D 場景的攝像頭(觀察點)位置。
- 將 ARKit 追蹤的真實世界坐標軸與 3D 場景坐標軸重合。
卡片信息
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。
卡片位置
對于距離用戶過近的商家卡片,會出現兩個問題:
- 由于 ARKit 自動將卡片展現得近大遠小,身邊的卡片會大到遮住了視野
- 前文提到的 ARSession 使用 AROrientationTrackingConfiguration 追蹤模式,由于沒有追蹤設備的水平位移,當用戶走向商家時,并不會發覺商家卡片越來越近
這里我們將距離用戶過近的卡片映射到稍遠的位置。如下圖所示,距離用戶的距離小于 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.widthheight: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:
- 來自 ARSCNView 的 hitTest:types: 方法:查找點擊的位置所對應的真實世界中的物體或位置
- 來自 SCNSceneRenderer 協議的 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。
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的ARKit:增强现实技术在美团到餐业务的实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring Boot引起的“堆外内存泄
- 下一篇: 【重版】朴素贝叶斯与拣鱼的故事