@SR1s
2017-09-03T23:34:06.000000Z
字数 7912
阅读 1635
ARCore
以下内容为笔者阅读ARCore Sample的笔记,仅供个人学习、记录、参考使用,如有纰漏,还请留言指正。
HelloArActivity
是示例应用的入口。这个入口简单演示了ARCore
的使用方法。这里主要做了以下四件事:
1. 配置ARCore SDK
2. 配置绘制环境
3. 往画面绘制信息,如摄像头数据、点云、菱形平面、Android小机器人
4. 点击交互
可以看到,ARCore还是比较简单易用的。SDK以尽可能简单的方式封装了一系列API。连平时最让人头疼的摄像头API使用也不需要我们操心了。
既然是ARCore的示例工程,那么最核心的当然是ARCore的使用了。
SDK暴露在外的主要接口类为Session
类。ARCore
的功能通过这个类提供。开发者通过这个类和ARCore
进行交互。
Session
类的使用很简单:
- 构造一个和当前Activity绑定的Session
- 对这个Session
进行配置
- 将onPause
、onResume
生命周期事件通知给这个Session
从Sample里看,这是使用ARCore最核心的几步配置了。但仅仅只有这样还不够。这几步仅仅是让ARCore跑起来了。但没有显示到界面上,怎么能确定ARCore真的有在好好工作呢。这个问题先按下不表。后面深入学习的时候再尝试解答。
注意:由于ARCore是基于摄像头工作的,因此还需要确保应用被授予了摄像头的使用权限。
接下来来看看,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)
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实例工程代码的简单分析。如有纰漏,还请评论指出,谢谢!