[关闭]
@SR1s 2017-09-03T15:34:06.000000Z 字数 7912 阅读 1560

ARCore Sample 导读

ARCore


以下内容为笔者阅读ARCore Sample的笔记,仅供个人学习、记录、参考使用,如有纰漏,还请留言指正。

入口: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应用开发过程中开发者最关心的部分。

绘制逻辑

和绘制相关的几个对象有:

  1. BackgroundRenderer mBackgroundRenderer ...;
  2. ObjectRenderer mVirtualObject ...;
  3. ObjectRenderer mVirtualObjectShadow ...;
  4. PlaneRenderer mPlaneRenderer ...;
  5. PointCloudRenderer mPointCloud ...;

其中:

负责绘制的对象就是以上这几位仁兄了。但具体在哪里进行绘制?应该怎么进行绘制呢?

绘制到屏幕上的配置

在Android上开发过OpenGL相关应用的同学们知道,要在Android上进行绘制,需要准备一个GLSurfaceView作为绘制的目标。Sample里也不例外。

首先,布局文件里准备了一个GLSurfaceView控件mSurfaceViewGLSurfaceView会为我们准备好OpenGL的绘制环境,并在合适的时候回调给我们。

首先,需要配置GLSurfaceView

相关代码如下:

  1. // Set up renderer.
  2. mSurfaceView.setPreserveEGLContextOnPause(true);
  3. mSurfaceView.setEGLContextClientVersion(2);
  4. mSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0); // Alpha used for plane blending.
  5. mSurfaceView.setRenderer(this);
  6. mSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);

这里对GLSurfaceView的配置中规中矩:
- 在pause状态下,保留EGL上下文
- OpenGL ES 版本选择 2.0 版本
- 绘制表面选择RGBA分别为8位,深度16位,模板0位的配置
- 设置自身为渲染器,即处理逻辑在这个类里实现
- 渲染模式设为持续渲染,即一帧渲染完,马上开始下一帧的渲染

更深入的学习,可以参考官网的OpenGL相关的教程文档。

绘制的实现逻辑

设置完GLSurfaceView的配置之后,接下来需要我们实现我们的绘制逻辑了。要实现在GLSurfaceView上绘制内容,需要实现GLSurfaceView.Renderer接口。这个接口的定义如下:

  1. public interface Renderer {
  2. void onSurfaceCreated(GL10 gl, EGLConfig config);
  3. void onSurfaceChanged(GL10 gl, int width, int height);
  4. void onDrawFrame(GL10 gl);
  5. }
  1. onSurfaceCreated(GL10 gl, EGLConfig config)
    这个方法在可绘制表面创建或重新创建的时候被调用。在这个回调里,可以做一些初始化的事情。注意,此方法运行在OpenGL线程中,具有OpenGL上下文,因此这里可以进行执行OpenGL调用。
  2. onSurfaceChanged(GL10 gl, int width, int height)
    这个方法在可绘制表面发生变化的时候被调用。此时外部可能改变了控件的大小,因此我们需要在这个调用里更新我们的视口信息,以便绘制的时候能准确绘制到屏幕中来。
  3. void onDrawFrame(GL10 gl)
    这个方法在绘制的时候调用。每绘制一次,就会调用一次,即每一帧触发一次。这里是主要的绘制逻辑。

因此,想知道Sample里是怎么进行绘制内容,就需要重点查阅这三个方法。

绘制逻辑

首先,看下如何初始化:

  1. @Override
  2. public void onSurfaceCreated(GL10 gl, EGLConfig config) {
  3. // 设置清除屏幕的时候颜色
  4. GLES20.glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
  5. // 初始化背景绘制器(即摄像头的数据)
  6. // 入参类型为Context,因为内部需要Context来读取资源
  7. mBackgroundRenderer.createOnGlThread(this);
  8. // 设置摄像头纹理句柄,ARCore会将摄像头数据更新到这个纹理上
  9. mSession.setCameraTextureName(mBackgroundRenderer.getTextureId());
  10. // 配置其他的渲染物体
  11. try {
  12. // 虚拟物体,android小绿机器人
  13. mVirtualObject.createOnGlThread(/*context=*/this, "andy.obj", "andy.png");
  14. // 材质信息配置
  15. mVirtualObject.setMaterialProperties(0.0f, 3.5f, 1.0f, 6.0f);
  16. // 阴影配置
  17. mVirtualObjectShadow.createOnGlThread(/*context=*/this,
  18. "andy_shadow.obj", "andy_shadow.png");
  19. // 混合模式设置
  20. mVirtualObjectShadow.setBlendMode(BlendMode.Shadow);
  21. // 材质信息配置
  22. mVirtualObjectShadow.setMaterialProperties(1.0f, 0.0f, 0.0f, 1.0f);
  23. } catch (IOException e) {
  24. Log.e(TAG, "Failed to read obj file");
  25. }
  26. try {
  27. // 平面
  28. mPlaneRenderer.createOnGlThread(/*context=*/this, "trigrid.png");
  29. } catch (IOException e) {
  30. Log.e(TAG, "Failed to read plane texture");
  31. }
  32. // 点云配置
  33. mPointCloud.createOnGlThread(/*context=*/this);
  34. }

然后,是配置绘制表面的大小,把绘制表面的size信息通知给ARCore。

  1. @Override
  2. public void onSurfaceChanged(GL10 gl, int width, int height) {
  3. GLES20.glViewport(0, 0, width, height);
  4. // 通知ARCore 显示区域大小改变了,以便ARCore内部调整透视矩阵,以及调整视频背景
  5. mSession.setDisplayGeometry(width, height);
  6. }

最后,就是核心的绘制部分void onDrawFrame(GL10 gl),这部分很长,仅保留绘制到界面的核心部分:

  1. // 清屏
  2. GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
  3. try {
  4. // ... 省略信息处理过程相关代码
  5. // 绘制背景,即摄像头捕获的图像数据
  6. mBackgroundRenderer.draw(frame);
  7. // 如果没出于运动追踪状态,那就不绘制其他东西了
  8. if (frame.getTrackingState() == TrackingState.NOT_TRACKING) {
  9. return;
  10. }
  11. // 绘制ARCore的点云,即ARCore识别到的特征点
  12. mPointCloud.update(frame.getPointCloud());
  13. mPointCloud.draw(frame.getPointCloudPose(), viewmtx, projmtx);
  14. // 绘制ARCore识别出来到的平面
  15. mPlaneRenderer.drawPlanes(mSession.getAllPlanes(), frame.getPose(), projmtx);
  16. for (PlaneAttachment planeAttachment : mTouches) {
  17. if (!planeAttachment.isTracking()) {
  18. continue;
  19. }
  20. planeAttachment.getPose().toMatrix(mAnchorMatrix, 0);
  21. // 绘制防止的虚拟物体和它的阴影
  22. mVirtualObject.updateModelMatrix(mAnchorMatrix, scaleFactor);
  23. mVirtualObjectShadow.updateModelMatrix(mAnchorMatrix, scaleFactor);
  24. mVirtualObject.draw(viewmtx, projmtx, lightIntensity);
  25. mVirtualObjectShadow.draw(viewmtx, projmtx, lightIntensity);
  26. }
  27. } catch (Throwable t) {
  28. // Avoid crashing the application due to unhandled exceptions.
  29. Log.e(TAG, "Exception on the OpenGL thread", t);
  30. }

这里用mBackgroundRenderer绘制了摄像头拍到的内容,用mPointCloud绘制了ARCore识别出来的特征点云,用mPlaneRenderer绘制ARCore识别出来的平面,用mVirtualObjectmVirtualObjectShadow绘制虚拟物体和它的阴影。

可以看到,绘制相关的方法都是drawdrawXXX。正是这些调用,使得界面上有东西显示出来。具体的逻辑,都封装在了对应的类里,有兴趣的同学可以深入研究下。

同样的,可以看到,在绘制之前,这些负责绘制的对象都需要我们提供一些信息:

这些信息怎么来的呢?基本都是通过ARCore来取得的。下面我们来看怎么从ARCore中取得这些数据。

从ARCore中获取绘制相关信息

还记得上文提到的Session类吗?是的,和AR相关的信息,依旧通过Session来取得。因为这些信息主要是用于绘制使用,因此,获取数据的代码在渲染器的void onDrawFrame(GL10 gl)里。

  1. try {
  2. // 从ARSession获取当前帧的相关信息
  3. // 这个Frame是ARCore的核心API之一
  4. Frame frame = mSession.update();
  5. // 处理点击事件,Sample的代码设计里,一次只处理一个点击事件,以减轻绘制过程的工作量
  6. // 因为点击事件的频率相较于渲染帧率来说,低了很多,因此分多帧来处理点击事件,而感官上并没多大差异,但渲染帧率得到了提升
  7. // 这是一种优化技巧,可以在实践中进行使用
  8. MotionEvent tap = mQueuedSingleTaps.poll();
  9. if (tap != null && frame.getTrackingState() == TrackingState.TRACKING) {
  10. for (HitResult hit : frame.hitTest(tap)) {
  11. // 检查是否点击到了平面
  12. // hitTest是ARCore提供命中测试接口,用于检查点击操作命中了哪些目标
  13. if (hit instanceof PlaneHitResult && ((PlaneHitResult) hit).isHitInPolygon()) {
  14. // 这也是一个优化技巧,限制最多放置16个对象
  15. // 因为这些对象是需要ARCore内部保持跟踪的,ARCore跟踪越多,需要计算的量也越大
  16. if (mTouches.size() >= 16) {
  17. mSession.removeAnchors(Arrays.asList(mTouches.get(0).getAnchor()));
  18. mTouches.remove(0);
  19. }
  20. // 保存对象的信息到mTouches里
  21. // 注意:下面调用了mSession.addAnchor(hit.getHitPose())
  22. // 这句是很关键的,它告诉ARCore,这个对象需要持续跟踪
  23. mTouches.add(new PlaneAttachment(
  24. ((PlaneHitResult) hit).getPlane(),
  25. mSession.addAnchor(hit.getHitPose())));
  26. break;
  27. }
  28. }
  29. }
  30. // ...
  31. // 获取当前摄像头相对于世界坐标系的投影矩阵
  32. float[] projmtx = new float[16];
  33. mSession.getProjectionMatrix(projmtx, 0, 0.1f, 100.0f);
  34. // 获取视图矩阵
  35. // 这个矩阵和上面的矩阵一起,决定了虚拟世界里的哪些物体能够被看见
  36. float[] viewmtx = new float[16];
  37. frame.getViewMatrix(viewmtx, 0);
  38. // 计算光照强度
  39. final float lightIntensity = frame.getLightEstimate().getPixelIntensity();
  40. // 通过getPointCloud获取ARCore追踪的特征点云
  41. mPointCloud.update(frame.getPointCloud());
  42. // 通过getPointCloudPose获取特征点的姿态信息
  43. // 姿态决定这些点的朝向信息,视图和投影矩阵,决定了哪些点能够看到
  44. mPointCloud.draw(frame.getPointCloudPose(), viewmtx, projmtx);
  45. // Check if we detected at least one plane. If so, hide the loading message.
  46. if (mLoadingMessageSnackbar != null) {
  47. // getAllPlanes获取识别到的所有平面的位置信息
  48. for (Plane plane : mSession.getAllPlanes()) {
  49. if (plane.getType() == com.google.ar.core.Plane.Type.HORIZONTAL_UPWARD_FACING &&
  50. plane.getTrackingState() == Plane.TrackingState.TRACKING) {
  51. hideLoadingMessage();
  52. break;
  53. }
  54. }
  55. }
  56. // 通过所有平面的位置信息和姿态信息,结合投影矩阵,进行绘制
  57. mPlaneRenderer.drawPlanes(mSession.getAllPlanes(), frame.getPose(), projmtx);
  58. float scaleFactor = 1.0f;
  59. for (PlaneAttachment planeAttachment : mTouches) {
  60. if (!planeAttachment.isTracking()) {
  61. continue;
  62. }
  63. // 将姿态信息转成矩阵,包含姿态、位置信息
  64. planeAttachment.getPose().toMatrix(mAnchorMatrix, 0);
  65. // 用这些信息绘制小机器人和它的阴影
  66. mVirtualObject.updateModelMatrix(mAnchorMatrix, scaleFactor);
  67. mVirtualObjectShadow.updateModelMatrix(mAnchorMatrix, scaleFactor);
  68. mVirtualObject.draw(viewmtx, projmtx, lightIntensity);
  69. mVirtualObjectShadow.draw(viewmtx, projmtx, lightIntensity);
  70. }
  71. } catch (Throwable t) {
  72. // Avoid crashing the application due to unhandled exceptions.
  73. Log.e(TAG, "Exception on the OpenGL thread", t);
  74. }

这些信息就是ARCore提供能提供给我们的能力的体现了。有了这些信息,我们可以做很多很多的事情。而不仅仅局限于示例程序上绘制的小东西。

知道了如何获取这些信息,我们可以把绘制相关的代码都替换掉,比如用别的3D图形框架来进行绘制,只需要把这些信息给到对应的API即可。有兴趣的同学可以试一试,也就是把上文提到的绘制内容的部分替换掉罢了。

总结

至此,ARCore的示例程序也就解析完毕了。rendering包下的东西主要是为了绘制内容而服务的,和ARCore关系并不大,如前文所述,可以用更成熟更现代化的3D图形框架替换掉。

总的来说,ARCore的API设计还是很精简的,以尽可能少的暴露API的方式,提供了它最核心的功能。使用起来难度不大。但要用好ARCore,还需要开发者有一定的OpenGL基础,以及一丢丢游戏开发的基础知识,比如坐标系,投影透视矩阵,视图矩阵,纹理等基础概念。

笔者也会继续探索,如何将ARCore和其他3D图形框架结合使用,减少和底层OpenGL互操作的相关代码(这些东西虽然基础,但裸写OpenGL是在不是一件有趣的事情),但和OpenGL相关的基础知识,还是非常非常有必要了解的。

以上,是笔者对ARCore实例工程代码的简单分析。如有纰漏,还请评论指出,谢谢!

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注