ARCore學習之旅:ARCore Sample 導讀

Geedio發表於2019-03-04

以下內容為筆者閱讀ARCore Sample的筆記,僅供個人學習、記錄、參考使用,如有紕漏,還請留言指正。

tags: ARCore

入口:HelloArActivity

HelloArActivity是示例應用的入口。這個入口簡單演示了ARCore的使用方法。這裡主要做了以下四件事:

  1. 配置ARCore SDK
  2. 配置繪製環境
  3. 往畫面繪製資訊,如攝像頭資料、點雲、菱形平面、Android小機器人
  4. 點選互動

可以看到,ARCore還是比較簡單易用的。SDK以儘可能簡單的方式封裝了一系列API。連平時最讓人頭疼的攝像頭API使用也不需要我們操心了。

ARCore 最簡使用指南

既然是ARCore的示例工程,那麼最核心的當然是ARCore的使用了。

SDK暴露在外的主要介面類為Session類。ARCore的功能通過這個類提供。開發者通過這個類和ARCore進行互動。

Session類的使用很簡單:

  • 構造一個和當前Activity繫結的Session
  • 對這個Session進行配置
  • onPauseonResume生命週期事件通知給這個Session

從Sample裡看,這是使用ARCore最核心的幾步配置了。但僅僅只有這樣還不夠。這幾步僅僅是讓ARCore跑起來了。但沒有顯示到介面上,怎麼能確定ARCore真的有在好好工作呢。這個問題先按下不表。後面深入學習的時候再嘗試解答。

注意:由於ARCore是基於攝像頭工作的,因此還需要確保應用被授予了攝像頭的使用許可權。

ARCore Sample 圖形繪製

接下來來看看,Sample裡是怎麼進行圖形繪製的。這也是AR應用開發過程中開發者最關心的部分。

繪製邏輯

和繪製相關的幾個物件有:

BackgroundRenderer mBackgroundRenderer ...;
ObjectRenderer mVirtualObject ...;
ObjectRenderer mVirtualObjectShadow ...;
PlaneRenderer mPlaneRenderer ...;
PointCloudRenderer mPointCloud ...;複製程式碼

其中:

  • mBackgroundRenderer用於繪製攝像頭採集到的資料。
  • mVirtualObject用於繪製Android小機器人。
  • mVirtualObjectShadow用於給Android機器人繪製陰影。
  • mPlaneRenderer用於繪製SDK識別出來的平面。
  • mPointCloud用於繪製SDK識別出來的點雲。

負責繪製的物件就是以上這幾位仁兄了。但具體在哪裡進行繪製?應該怎麼進行繪製呢?

繪製到螢幕上的配置

在Android上開發過OpenGL相關應用的同學們知道,要在Android上進行繪製,需要準備一個GLSurfaceView作為繪製的目標。Sample裡也不例外。

首先,佈局檔案裡準備了一個GLSurfaceView控制元件mSurfaceViewGLSurfaceView會為我們準備好OpenGL的繪製環境,並在合適的時候回撥給我們。

首先,需要配置GLSurfaceView

相關程式碼如下:

// Set up renderer.
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的配置中規中矩:

  • 在pause狀態下,保留EGL上下文
  • OpenGL ES 版本選擇 2.0 版本
  • 繪製表面選擇RGBA分別為8位,深度16位,模板0位的配置
  • 設定自身為渲染器,即處理邏輯在這個類裡實現
  • 渲染模式設為持續渲染,即一幀渲染完,馬上開始下一幀的渲染

更深入的學習,可以參考官網的OpenGL相關的教程文件。

繪製的實現邏輯

設定完GLSurfaceView的配置之後,接下來需要我們實現我們的繪製邏輯了。要實現在GLSurfaceView上繪製內容,需要實現GLSurfaceView.Renderer介面。這個介面的定義如下:

public interface Renderer {
    void onSurfaceCreated(GL10 gl, EGLConfig config);
    void onSurfaceChanged(GL10 gl, int width, int height);
    void onDrawFrame(GL10 gl);
}複製程式碼
  1. onSurfaceCreated(GL10 gl, EGLConfig config)
    這個方法在可繪製表面建立或重新建立的時候被呼叫。在這個回撥裡,可以做一些初始化的事情。注意,此方法執行在OpenGL執行緒中,具有OpenGL上下文,因此這裡可以進行執行OpenGL呼叫。
  2. onSurfaceChanged(GL10 gl, int width, int height)
    這個方法在可繪製表面發生變化的時候被呼叫。此時外部可能改變了控制元件的大小,因此我們需要在這個呼叫裡更新我們的視口資訊,以便繪製的時候能準確繪製到螢幕中來。
  3. void onDrawFrame(GL10 gl)
    這個方法在繪製的時候呼叫。每繪製一次,就會呼叫一次,即每一幀觸發一次。這裡是主要的繪製邏輯。

因此,想知道Sample裡是怎麼進行繪製內容,就需要重點查閱這三個方法。

繪製邏輯

首先,看下如何初始化:

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    // 設定清除螢幕的時候顏色
    GLES20.glClearColor(0.1f, 0.1f, 0.1f, 1.0f);

    // 初始化背景繪製器(即攝像頭的資料)
    // 入參型別為Context,因為內部需要Context來讀取資源
    mBackgroundRenderer.createOnGlThread(this);
    // 設定攝像頭紋理控制程式碼,ARCore會將攝像頭資料更新到這個紋理上
    mSession.setCameraTextureName(mBackgroundRenderer.getTextureId());

    // 配置其他的渲染物體
    try {
        // 虛擬物體,android小綠機器人
        mVirtualObject.createOnGlThread(/*context=*/this, "andy.obj", "andy.png");
        // 材質資訊配置
        mVirtualObject.setMaterialProperties(0.0f, 3.5f, 1.0f, 6.0f);

        // 陰影配置
        mVirtualObjectShadow.createOnGlThread(/*context=*/this,
            "andy_shadow.obj", "andy_shadow.png");
        // 混合模式設定
        mVirtualObjectShadow.setBlendMode(BlendMode.Shadow);
        // 材質資訊配置
        mVirtualObjectShadow.setMaterialProperties(1.0f, 0.0f, 0.0f, 1.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);
}複製程式碼

然後,是配置繪製表面的大小,把繪製表面的size資訊通知給ARCore。

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
    GLES20.glViewport(0, 0, width, height);
    // 通知ARCore 顯示區域大小改變了,以便ARCore內部調整透視矩陣,以及調整視訊背景
    mSession.setDisplayGeometry(width, height);
}複製程式碼

最後,就是核心的繪製部分void onDrawFrame(GL10 gl),這部分很長,僅保留繪製到介面的核心部分:

// 清屏
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

try {

    // ... 省略資訊處理過程相關程式碼

    // 繪製背景,即攝像頭捕獲的影像資料
    mBackgroundRenderer.draw(frame);

    // 如果沒出於運動追蹤狀態,那就不繪製其他東西了
    if (frame.getTrackingState() == TrackingState.NOT_TRACKING) {
        return;
    }

    // 繪製ARCore的點雲,即ARCore識別到的特徵點
    mPointCloud.update(frame.getPointCloud());
    mPointCloud.draw(frame.getPointCloudPose(), viewmtx, projmtx);

    // 繪製ARCore識別出來到的平面
    mPlaneRenderer.drawPlanes(mSession.getAllPlanes(), frame.getPose(), projmtx);


    for (PlaneAttachment planeAttachment : mTouches) {
        if (!planeAttachment.isTracking()) {
            continue;
        }

        planeAttachment.getPose().toMatrix(mAnchorMatrix, 0);

        // 繪製防止的虛擬物體和它的陰影
        mVirtualObject.updateModelMatrix(mAnchorMatrix, scaleFactor);
        mVirtualObjectShadow.updateModelMatrix(mAnchorMatrix, scaleFactor);
        mVirtualObject.draw(viewmtx, projmtx, lightIntensity);
        mVirtualObjectShadow.draw(viewmtx, projmtx, lightIntensity);
    }

} catch (Throwable t) {
    // Avoid crashing the application due to unhandled exceptions.
    Log.e(TAG, "Exception on the OpenGL thread", t);
}複製程式碼

這裡用mBackgroundRenderer繪製了攝像頭拍到的內容,用mPointCloud繪製了ARCore識別出來的特徵點雲,用mPlaneRenderer繪製ARCore識別出來的平面,用mVirtualObjectmVirtualObjectShadow繪製虛擬物體和它的陰影。

可以看到,繪製相關的方法都是drawdrawXXX。正是這些呼叫,使得介面上有東西顯示出來。具體的邏輯,都封裝在了對應的類裡,有興趣的同學可以深入研究下。

同樣的,可以看到,在繪製之前,這些負責繪製的物件都需要我們提供一些資訊:

  • 繪製的物體的位置
  • 繪製的物體檢視(View)矩陣,投影(Project)矩陣
  • 物體的姿態(位置和朝向)
  • 物體的光照資訊

這些資訊怎麼來的呢?基本都是通過ARCore來取得的。下面我們來看怎麼從ARCore中取得這些資料。

從ARCore中獲取繪製相關資訊

還記得上文提到的Session類嗎?是的,和AR相關的資訊,依舊通過Session來取得。因為這些資訊主要是用於繪製使用,因此,獲取資料的程式碼在渲染器的void onDrawFrame(GL10 gl)裡。


try {
    // 從ARSession獲取當前幀的相關資訊
    // 這個Frame是ARCore的核心API之一
    Frame frame = mSession.update();

    // 處理點選事件,Sample的程式碼設計裡,一次只處理一個點選事件,以減輕繪製過程的工作量
    // 因為點選事件的頻率相較於渲染幀率來說,低了很多,因此分多幀來處理點選事件,而感官上並沒多大差異,但渲染幀率得到了提升
    // 這是一種優化技巧,可以在實踐中進行使用
    MotionEvent tap = mQueuedSingleTaps.poll();
    if (tap != null && frame.getTrackingState() == TrackingState.TRACKING) {
        for (HitResult hit : frame.hitTest(tap)) {
            // 檢查是否點選到了平面
            // hitTest是ARCore提供命中測試介面,用於檢查點選操作命中了哪些目標
            if (hit instanceof PlaneHitResult && ((PlaneHitResult) hit).isHitInPolygon()) {
                // 這也是一個優化技巧,限制最多放置16個物件
                // 因為這些物件是需要ARCore內部保持跟蹤的,ARCore跟蹤越多,需要計算的量也越大
                if (mTouches.size() >= 16) {
                    mSession.removeAnchors(Arrays.asList(mTouches.get(0).getAnchor()));
                    mTouches.remove(0);
                }
                // 儲存物件的資訊到mTouches裡
                // 注意:下面呼叫了mSession.addAnchor(hit.getHitPose())
                // 這句是很關鍵的,它告訴ARCore,這個物件需要持續跟蹤
                mTouches.add(new PlaneAttachment(
                    ((PlaneHitResult) hit).getPlane(),
                    mSession.addAnchor(hit.getHitPose())));

                break;
            }
        }
    }

    // ...

    // 獲取當前攝像頭相對於世界座標系的投影矩陣
    float[] projmtx = new float[16];
    mSession.getProjectionMatrix(projmtx, 0, 0.1f, 100.0f);

    // 獲取檢視矩陣
    // 這個矩陣和上面的矩陣一起,決定了虛擬世界裡的哪些物體能夠被看見
    float[] viewmtx = new float[16];
    frame.getViewMatrix(viewmtx, 0);

    // 計算光照強度
    final float lightIntensity = frame.getLightEstimate().getPixelIntensity();

    // 通過getPointCloud獲取ARCore追蹤的特徵點雲
    mPointCloud.update(frame.getPointCloud());
    // 通過getPointCloudPose獲取特徵點的姿態資訊
    // 姿態決定這些點的朝向資訊,檢視和投影矩陣,決定了哪些點能夠看到
    mPointCloud.draw(frame.getPointCloudPose(), viewmtx, projmtx);

    // Check if we detected at least one plane. If so, hide the loading message.
    if (mLoadingMessageSnackbar != null) {
        // getAllPlanes獲取識別到的所有平面的位置資訊
        for (Plane plane : mSession.getAllPlanes()) {
            if (plane.getType() == com.google.ar.core.Plane.Type.HORIZONTAL_UPWARD_FACING &&
                    plane.getTrackingState() == Plane.TrackingState.TRACKING) {
                hideLoadingMessage();
                break;
            }
        }
    }

    // 通過所有平面的位置資訊和姿態資訊,結合投影矩陣,進行繪製
    mPlaneRenderer.drawPlanes(mSession.getAllPlanes(), frame.getPose(), projmtx);

    float scaleFactor = 1.0f;
    for (PlaneAttachment planeAttachment : mTouches) {
        if (!planeAttachment.isTracking()) {
            continue;
        }

        // 將姿態資訊轉成矩陣,包含姿態、位置資訊
        planeAttachment.getPose().toMatrix(mAnchorMatrix, 0);

        // 用這些資訊繪製小機器人和它的陰影
        mVirtualObject.updateModelMatrix(mAnchorMatrix, scaleFactor);
        mVirtualObjectShadow.updateModelMatrix(mAnchorMatrix, scaleFactor);
        mVirtualObject.draw(viewmtx, projmtx, lightIntensity);
        mVirtualObjectShadow.draw(viewmtx, projmtx, lightIntensity);
    }

} catch (Throwable t) {
    // Avoid crashing the application due to unhandled exceptions.
    Log.e(TAG, "Exception on the OpenGL thread", t);
}複製程式碼

這些資訊就是ARCore提供能提供給我們的能力的體現了。有了這些資訊,我們可以做很多很多的事情。而不僅僅侷限於示例程式上繪製的小東西。

知道了如何獲取這些資訊,我們可以把繪製相關的程式碼都替換掉,比如用別的3D圖形框架來進行繪製,只需要把這些資訊給到對應的API即可。有興趣的同學可以試一試,也就是把上文提到的繪製內容的部分替換掉罷了。

總結

至此,ARCore的示例程式也就解析完畢了。rendering包下的東西主要是為了繪製內容而服務的,和ARCore關係並不大,如前文所述,可以用更成熟更現代化的3D圖形框架替換掉。

總的來說,ARCore的API設計還是很精簡的,以儘可能少的暴露API的方式,提供了它最核心的功能。使用起來難度不大。但要用好ARCore,還需要開發者有一定的OpenGL基礎,以及一丟丟遊戲開發的基礎知識,比如座標系,投影透視矩陣,檢視矩陣,紋理等基礎概念。

筆者也會繼續探索,如何將ARCore和其他3D圖形框架結合使用,減少和底層OpenGL互操作的相關程式碼(這些東西雖然基礎,但裸寫OpenGL是在不是一件有趣的事情),但和OpenGL相關的基礎知識,還是非常非常有必要了解的。

以上,是筆者對ARCore例項工程程式碼的簡單分析。如有紕漏,還請評論指出,謝謝!

相關文章