深入淺出 ARCore

weixin_34148456發表於2018-01-18

前言

其實關注 ARCore也蠻久了,但一直沒有騰出時間來寫個總結。正好應朋友之約,我們今天就來好好聊一聊 ARCore.

ARCore的歷史以及與蘋果ARKit的競爭我就不多講了,在網上可以搜到一堆資訊。但網上深入講解ARCore的確實不多。

本文主要有兩個目的,一是向大家介紹一下ARCore的基本概念,瞭解這些概念對於大家後續深入的學習 ARCore具有關鍵的作用。二是深入剖析一下 ARCore的工作機理,這樣可以讓大家更容易理解 ARCore。

另外,ARCore與ARKit的基本概念很接近,只要瞭解了其中的一個,基本上也就掌握了另一個。

由於本文篇幅有此長,而且很多新概念,所以大家在閱讀時要做好心理準備。:)

ARCore的基本概念

ARCore工作時要做兩件事兒,首先跟蹤手機的運動軌跡,然後構建出它對現實世界的理解。

ARCore的運動跟蹤技術是通過 Camera 標識出特徵點,並隨著時間的推移跟蹤這些特徵點是如何移動的。通過這些特徵點的運動資料及從手機慣性感測器讀到的資訊,ARCore計算出手機移動的位置和方向,並稱其為姿態。

除了識別出這些特徵點外,ARCore還能檢測出像地板、桌面等平面資訊以及在某個地方的光線強度。這些資訊使得ARCore能夠構建出自己理解的真實世界。構建出這樣一個模型後,可以在上面放置一些虛擬內容了。

ARCore是如何做到的呢?它使用三項關鍵技術將虛擬內容與真實世界整合到一起,這三種技術分別是:

  • 運動跟蹤
  • 環境理解
  • 光線評估

運動跟蹤

5956443-070fa1ebd86bb770.jpg
運動跟蹤

ARCore 可以在手機移動的過程中知道,相對於真實世界手機所在的位置和方向(姿勢)。

當手機在真實世界移動時,ARCore使用稱為併發測距和對映的過程來了解手機與周圍世界的相對位置。

ARCore能檢測到Camera捕獲的影像在視覺上的不同特徵,稱為特徵點。它使用這些點計算其位置變化。隨著時間的推移,通過視覺資訊與來自IMU裝置的慣性測量,ARCore就可以估算出Camera相對於真實世界的姿態(位置和方向)。

通過將渲染的3D虛擬內容與物理Camera的姿勢對齊,開發人員就可以從正確的角度渲染虛擬內容。 再通過將虛擬物品的影像渲染到從Camera獲得的影像之上,這樣看起來就好像虛擬內容是真實世界的一部分似的。

環境理解

5956443-1994182d706d4afd.jpg
環境理解

ARCore可以讓手機檢測出一塊水平面的位置和大小。如地面、桌子、書架等等。這樣就可以將虛擬物體放置到檢測出的水平面上了。

它是如何做到的呢?ARCore通過檢測特徵點和平面不斷改善對現實世界環境的理解。

ARCore會查詢常見水平表面(如桌面)上的特徵點叢集,除此之外,ARCore還可以確定每個平面的邊界,並將以上資訊提供給您的應用程式。 這樣,開發人員就可以使用這些資訊,並將虛擬物體放置在平坦的表面上了。

由於ARCore使用特徵點檢測平面,因此可能無法正確檢測到沒有紋理的平坦表面(如白色桌面)。

光線評估

5956443-a7871f2b82c835c3.jpg
光線估計

ARCore 可以讓手機估算出當前環境的光線強度,這樣可以讓虛擬物理顯示在真實環境中更加逼真。

使用者互動

ARCore使用 hit testing(命中測試) 獲取與手機螢幕相對應的(x,y)座標(如通過點選螢幕等互動方式),將其投射到 Camera 的3D座標系中,並返回與命中點射線相交的所有平面和特徵點,以及在世界座標系中該交叉點的姿態。這樣就能實現使用者與ARCore環境中的物件互動了。

錨點與跟蹤

ARCore可以改變對自身位置和環境的理解來調整姿態。如我們要在ARCore環境中放置一個虛擬物件,首先要確定一個錨點,以確保ARCore能隨著時間的推移不斷跟蹤物件的位置。通常情況下,會根據命中測試返回的姿勢建立一個錨點。

姿勢改變這項技術特別關鍵,只有得到姿勢,ARCore才可以隨著時間的推移不斷更新環境物件(像飛機和特徵點)的位置。ARCore將平面和點認為是可跟蹤的特殊型別的物件。您可以將虛擬物件錨定到這些可追蹤的物件上,以確保在裝置移動時,虛擬物件和可跟蹤物件之間保持穩定的關係。這就好像您在桌面上放置一個虛擬的花瓶,如果ARCore稍後調整與桌面相關的姿勢,那麼花瓶仍然會保持在桌面上。

ARCore 核心類介紹

Session

com.google.ar.core.Session類,Session管理AR系統狀態並處理Session生命週期。 該類是ARCore API的主要入口點。 該類允許使用者建立Session,配置Session,啟動/停止Session,最重要的是接收視訊幀,以允許訪問Camera影像和裝置姿勢。

Config

com.google.ar.core.Config類,用於儲存Session的設定。

Frame

com.google.ar.core.Frame類,該類通過呼叫update()方法,獲取狀態資訊並更新AR系統。

HitResult

com.google.ar.core.HitResult類,該類定義了命中點射線與估算的真實幾何世界之間的交集。

Point

com.google.ar.core.Point類,它代表ARCore正在跟蹤的空間點。 它是建立錨點(呼叫createAnchor方法)時,或者進行命中檢測(呼叫hitTest方法)時,返回的結果。

PointCloud

5956443-d063b5bfff3f28e4.png
點雲

com.google.ar.core.PointCloud類,它包含一組觀察到的3D點和信心值。

Plane

5956443-fe05bd29c94f5dbf.png
平面

com.google.ar.core.Plane類,描述了現實世界平面表面的最新資訊。

Anchor

com.google.ar.core.Anchor類,描述了現實世界中的固定位置和方向。 為了保持物理空間的固定位置,這個位置的數字描述資訊將隨著ARCore對空間的理解的不斷改進而更新。

Pose

com.google.ar.core.Pose類, 姿勢表示從一個座標空間到另一個座標空間位置不變的轉換。 在所有的ARCore API裡,姿勢總是描述從物件本地座標空間到世界座標空間的轉換。

隨著ARCore對環境的瞭解不斷變化,它將調整座標系模式以便與真實世界保持一致。 這時,Camera和錨點的位置(座標)可能會發生明顯的變化,以便它們所代表的物體處理恰當的位置。

這意味著,每一幀影像都應被認為是在一個完全獨立的世界座標空間中。錨點和Camera的座標不應該在渲染幀之外的地方使用,如果需考慮到某個位置超出單個渲染框架的範圍,則應該建立一個錨點或者應該使用相對於附近現有錨點的位置。

ImageMetadata

com.google.ar.core.ImageMetadata類,提供了對Camera影像捕捉結果的後設資料的訪問。

LightEstimate

com.google.ar.core.LightEstimate儲存關於真實場景光照的估計資訊。 通過 getLightEstimate()得到。

Trackable

com.google.ar.core.Pose介面類,它是ARCore可以跟蹤的,並能與錨點繫結在一起的東西。

Camera

android.graphics.Camera類,它提供用於捕獲影像的Camera的資訊。 Camera是一個長期存活的物件,每次呼叫Session.update() 都會更新Camera的屬性。

例項分析

Google釋出的 ARCore SDK 中包括了一些例子程式,有了上面的基本知識後,我們就很容易理解他所寫的 Demo 程式的流程了。

建立 Session 和 Conig

在 Activity中的 onCreate 方法中建立 Session 和 Config是個不錯的地方。

mSession = new Session(/*context=*/this);

mDefaultConfig = Config.createDefaultConfig();
if (!mSession.isSupported(mDefaultConfig)) {
    Toast.makeText(this, "This device does not support AR", Toast.LENGTH_LONG).show();
    finish();
    return;
}
  • Session: 是ARCore的管理類,它非常重要。ARCore的開啟,關閉,視訊幀的獲取等都是通過它來管理的。
  • Config:存放一些配置資訊,如平面的查詢模式,光照模式等資訊都是記錄在該類中。目前該類還比較簡單,裡邊沒存多少東西。
  • isSupported:該方法主要是對 SDK的版本及機型做控制。目前官方只支援幾款Google和三星的機子做測試。其它機型還都不支援ARCore,當然有一些機型通過破解後的SDK是可以使用 ARCore的。該方法中的 Config 引數沒有用到。

建立 GLSurfaceView 用於AR展示

在 Google 提供的Demo中,AR的展示部分使用的是 GLSurfaceView。做視訊開發的同學都清楚,Android 可以使用三種View進行視訊渲染。分別是:

  • SurfaceView
  • GLSurfaceView
  • TextureView

其中,SurfaceView最靈活,效率也最高,但使用起來比較煩鎖。而GLSurfaceView相對 SurfaceView就是簡單很多,只需要實現它的 Render 介面即可。而 TextureView使用最簡單,很多工作都由 Android 的視窗管理器幫你做了,但靈活性相對較差。

更為詳細的資訊請參考我的另一篇文章

為了渲染的高效,Google在Demo中大量使用了OpenGL技術。由於OpenGL是影像處理非常大的一個領域,無法通過一兩篇文章講解清楚,同時也不是我們本文的重點,所以我們這裡不對它做詳細介紹,有興趣的同學可以到網上自行學習。

mSurfaceView = (GLSurfaceView) findViewById(R.id.surfaceview);
...
mSurfaceView.setPreserveEGLContextOnPause(true);
mSurfaceView.setEGLContextClientVersion(2);
mSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0); // Alpha used for plane blending.
mSurfaceView.setRenderer(this);     mSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);

該段程式碼首先通過資原始檔建立一個GLSurfaceView物件,然後將 GLSurfaceView 與 EGL 上下文關聯。並將Activity作為GLSurfaceView的回撥物件(也就是說該Activity要實現 GLSurfaceView.Renderer中定義的介面,如onSurfaceCreated、onSurfaceChanged、onDrawFrame等),最後設定 mSurfaceView 的渲染模式為 GLSurfaceView.RENDERMODE_CONTINUOUSLY,即對 GLSurfaceView 持續不斷的渲染。

建立各種執行緒

要理解本節內容,首先大家要知道AR的詳細工作原理是怎樣的。我在這裡再向大家做個簡要的說明。

背景展示

用過AR的人都知道,AR是將一些虛擬物品放到真實的場景中。那麼這個真實的場景從哪裡來呢?當然是從手機的 Camera上獲取。

我們把從 Camera中獲取的視訊當作 AR的背景。其實,AR 就是將虛擬物品放到視訊上,只不過不是簡單的放置,而是需要經過大量的計算,找到視訊中的平面位置再放置。

而Android中視訊的採集相對比較簡單,像直播系統,照像機都要使用該技術。

平臺檢測

上面我們已經說了,AR就是實時視訊+虛擬物品。但虛擬物不能簡單的放到視訊上,而是先對視訊中的每一幀進行檢測,找到視訊中的平面,確定好位置後,再將虛擬物品放置上去。這樣才算是AR呀:)

點雲

上面我們知道了,AR=實時視訊+平面+虛擬物品。除此之外,它還應該能對虛擬物品進行跟蹤,也就是可以在不同的角度觀察同一個物品,並得出不同的姿態,所以就有了“點雲” 技術。那什麼是點雲呢?顧名思義,形象的說就是一堆點,這些的形狀有點像雲。點雲中的每個點都是一個特徵點,它是通過Camera獲得的。

放置虛擬物品

找到了平面,有了跟蹤手段,我們就可以將準備好的虛擬物品放置到平臺上,現在才是真正的AR哈。

好,知道了這些基本原理後,我們來看看Google Demo是如何做的呢?

建立執行緒

對於上面的每一點,Demo都啟動了一個執行緒,程式碼如下:

...

// Create the texture and pass it to ARCore session to be filled during update().
mBackgroundRenderer.createOnGlThread(/*context=*/this);
mSession.setCameraTextureName(mBackgroundRenderer.getTextureId());

// Prepare the other rendering objects.
try {
    mVirtualObject.createOnGlThread(/*context=*/this, "andy.obj", "andy.png");
    mVirtualObject.setMaterialProperties(0.0f, 3.5f, 1.0f, 6.0f);
    ...
} catch (IOException e) {
    Log.e(TAG, "Failed to read obj file");
}
try {
    mPlaneRenderer.createOnGlThread(/*context=*/this, "trigrid.png");
} catch (IOException e) {
    Log.e(TAG, "Failed to read plane texture");
}
mPointCloud.createOnGlThread(/*context=*/this);

...

上面的程式碼中首先建立了一個背景執行緒,用來將從Camera中獲取的視訊渲染到螢幕上當背景。資料是從哪裡來的呢?就是通過 Session.update 獲取 Camera 資料,再通過紋理交給背景執行緒。

對紋理沒有概念的同學可以把它想像成一塊記憶體空間。

然後啟動虛擬物品執行緒,用於繪製虛擬物品,及發生角度變化時,更新虛擬物別的姿勢。緊接著建立平面執行緒來繪製平面。最後啟動點雲執行緒繪製特徵點。

到此,各種執行緒就建立完畢了。下面我們來說一下如何渲染。

命中檢測與渲染

命中檢測

當我們要向背景繪製虛擬物品時,首先要進行命中檢測。程式碼如下:

MotionEvent tap = mQueuedSingleTaps.poll();
if (tap != null && frame.getTrackingState() == TrackingState.TRACKING) {
    for (HitResult hit : frame.hitTest(tap)) {
        // Check if any plane was hit, and if it was hit inside the plane polygon.
        if (hit instanceof PlaneHitResult && ((PlaneHitResult) hit).isHitInPolygon()) {
            // Cap the number of objects created. This avoids overloading both the
            // rendering system and ARCore.
            if (mTouches.size() >= 16) {
                mSession.removeAnchors(Arrays.asList(mTouches.get(0).getAnchor()));
                mTouches.remove(0);
            }
            // Adding an Anchor tells ARCore that it should track this position in
            // space. This anchor will be used in PlaneAttachment to place the 3d model
            // in the correct position relative both to the world and to the plane.
            mTouches.add(new PlaneAttachment(
                ((PlaneHitResult) hit).getPlane(),
                mSession.addAnchor(hit.getHitPose())));

            // Hits are sorted by depth. Consider only closest hit on a plane.
            break;
        }
    }
}

在例子中,它檢視是否有點選事件,且影像處理於跟蹤狀態?如果是,就對其進行命中檢測,看是否可以找到一個平面,如果找到就建立一個錨點並將其與該平臺繫結起來。

渲染背景

// Draw background.
mBackgroundRenderer.draw(frame);

通過上面的程式碼就可以將紋理中的內容推給 EGL,上面建立的渲染執行緒從 EGL 上下文中獲取資料,最終將視訊渲染到螢幕上。

繪製點雲

mPointCloud.update(frame.getPointCloud());
mPointCloud.draw(frame.getPointCloudPose(), viewmtx, projmtx);

同理,通過上面的程式碼,就可以將資料傳給點雲執行緒進行點雲的繪製。

繪製平面

// Visualize planes.
mPlaneRenderer.drawPlanes(mSession.getAllPlanes(), frame.getPose(), projmtx);

通過上面程式碼將資料傳給平面執行緒進行平面的繪製。

繪製虛擬物品

for (PlaneAttachment planeAttachment : mTouches) {
    if (!planeAttachment.isTracking()) {
        continue;
    }
    // Get the current combined pose of an Anchor and Plane in world space. The Anchor
    // and Plane poses are updated during calls to session.update() as ARCore refines
    // its estimate of the world.
    planeAttachment.getPose().toMatrix(mAnchorMatrix, 0);

    // Update and draw the model and its shadow.
    mVirtualObject.updateModelMatrix(mAnchorMatrix, scaleFactor);
    mVirtualObjectShadow.updateModelMatrix(mAnchorMatrix, scaleFactor);
}

最後,遍歷所有的錨點,在每個錨點上繪製虛擬物品。

至此,我們對ARCore的分析就告一段落了。

小結

ARCore相對於初學者來說還是有不少難度的。因為裡面有很多新概念需要大家消化吸收。

另一方面,ARCore目前只有幾款機型可能做測試,而這幾款機型在國內用的人不多,所以對於大多數人來說沒法做實驗,這也增加了學習的難度。

除了以上兩點外,ARCore中大量使用了 OpenGL的相關知識。而OpenGL又是一門很深的學問,所以學習的難度更加陡峭了。

通過以上三點,可以說目前學習ARCore的門檻相較於蘋果的ARKit要難不少。

希望本文能對您有所幫助,並請多多關注我,謝謝!

參考

ARCore github