以下內容為筆者閱讀ARCore Sample的筆記,僅供個人學習、記錄、參考使用,如有紕漏,還請留言指正。
tags: ARCore
入口:HelloArActivity
HelloArActivity
是示例應用的入口。這個入口簡單演示了ARCore
的使用方法。這裡主要做了以下四件事:
- 配置ARCore SDK
- 配置繪製環境
- 往畫面繪製資訊,如攝像頭資料、點雲、菱形平面、Android小機器人
- 點選互動
可以看到,ARCore還是比較簡單易用的。SDK以儘可能簡單的方式封裝了一系列API。連平時最讓人頭疼的攝像頭API使用也不需要我們操心了。
ARCore 最簡使用指南
既然是ARCore的示例工程,那麼最核心的當然是ARCore的使用了。
SDK暴露在外的主要介面類為Session
類。ARCore
的功能通過這個類提供。開發者通過這個類和ARCore
進行互動。
Session
類的使用很簡單:
- 構造一個和當前Activity繫結的Session
- 對這個
Session
進行配置 - 將
onPause
、onResume
生命週期事件通知給這個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
控制元件mSurfaceView
。GLSurfaceView
會為我們準備好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);
}複製程式碼
onSurfaceCreated(GL10 gl, EGLConfig config)
這個方法在可繪製表面建立或重新建立的時候被呼叫。在這個回撥裡,可以做一些初始化的事情。注意,此方法執行在OpenGL執行緒中,具有OpenGL上下文,因此這裡可以進行執行OpenGL呼叫。onSurfaceChanged(GL10 gl, int width, int height)
這個方法在可繪製表面發生變化的時候被呼叫。此時外部可能改變了控制元件的大小,因此我們需要在這個呼叫裡更新我們的視口資訊,以便繪製的時候能準確繪製到螢幕中來。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識別出來的平面,用mVirtualObject
、mVirtualObjectShadow
繪製虛擬物體和它的陰影。
可以看到,繪製相關的方法都是draw
或drawXXX
。正是這些呼叫,使得介面上有東西顯示出來。具體的邏輯,都封裝在了對應的類裡,有興趣的同學可以深入研究下。
同樣的,可以看到,在繪製之前,這些負責繪製的物件都需要我們提供一些資訊:
- 繪製的物體的位置
- 繪製的物體檢視(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例項工程程式碼的簡單分析。如有紕漏,還請評論指出,謝謝!