ARCore学习之旅:ARCore Sample 导读

以下内容为笔者阅读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实例工程代码的简单分析。如有纰漏,还请评论指出,谢谢!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值