如下內容爲筆者閱讀ARCore Sample的筆記,僅供我的學習、記錄、參考使用,若有紕漏,還請留言指正。android
tags: ARCorebash
HelloArActivity
是示例應用的入口。這個入口簡單演示了ARCore
的使用方法。這裏主要作了如下四件事:app
能夠看到,ARCore仍是比較簡單易用的。SDK以儘量簡單的方式封裝了一系列API。連平時最讓人頭疼的攝像頭API使用也不須要咱們操心了。框架
既然是ARCore的示例工程,那麼最核心的固然是ARCore的使用了。ide
SDK暴露在外的主要接口類爲Session
類。ARCore
的功能經過這個類提供。開發者經過這個類和ARCore
進行交互。佈局
Session
類的使用很簡單:學習
Session
進行配置onPause
、onResume
生命週期事件通知給這個Session
從Sample裏看,這是使用ARCore最核心的幾步配置了。但僅僅只有這樣還不夠。這幾步僅僅是讓ARCore跑起來了。但沒有顯示到界面上,怎麼能肯定ARCore真的有在好好工做呢。這個問題先按下不表。後面深刻學習的時候再嘗試解答。測試
注意:因爲ARCore是基於攝像頭工做的,所以還須要確保應用被授予了攝像頭的使用權限。優化
接下來來看看,Sample裏是怎麼進行圖形繪製的。這也是AR應用開發過程當中開發者最關心的部分。ui
和繪製相關的幾個對象有:
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
的配置中規中矩:
更深刻的學習,能夠參考官網的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)
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
。正是這些調用,使得界面上有東西顯示出來。具體的邏輯,都封裝在了對應的類裏,有興趣的同窗能夠深刻研究下。
一樣的,能夠看到,在繪製以前,這些負責繪製的對象都須要咱們提供一些信息:
這些信息怎麼來的呢?基本都是經過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實例工程代碼的簡單分析。若有紕漏,還請評論指出,謝謝!