阅读:1618回复:0
ARCore Java Sample 导读入口:HelloARActivity HelloArActivity是示例应用的入口。这个入口简单演示了ARCore的使用方法。这里主要做了以下四件事:
可以看到,ARCore还是比较简单易用的。SDK以尽可能简单的方式封装了一系列API。连平时最让人头疼的摄像头API使用也不需要我们操心了。 ARCore 最简使用指南 既然是ARCore的示例工程,那么最核心的当然是ARCore的使用了。 SDK暴露在外的主要接口类为Session类。ARCore的功能通过这个类提供。开发者通过这个类和ARCore进行交互。 Session类的使用很简单:
从Sample里看,这是使用ARCore最核心的几步配置了。但仅仅只有这样还不够。这几步仅仅是让ARCore跑起来了。但没有显示到界面上,怎么能确定ARCore真的有在好好工作呢。这个问题先按下不表。后面深入学习的时候再尝试解答。 注意:由于ARCore是基于摄像头工作的,因此还需要确保应用被授予了摄像头的使用权限。 ARCore Sample 图形绘制 接下来来看看,Sample里是怎么进行图形绘制的。这也是AR应用开发过程中开发者最关心的部分。 绘制逻辑 和绘制相关的几个对象有: BackgroundRenderer mBackgroundRenderer ...; ObjectRenderer mVirtualObject ...; ObjectRenderer mVirtualObjectShadow ...; PlaneRenderer mPlaneRenderer ...; PointCloudRenderer mPointCloud ...; 其中:
负责绘制的对象就是以上这几位仁兄了。但具体在哪里进行绘制?应该怎么进行绘制呢? 绘制到屏幕上的配置 在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); }
因此,想知道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中取得这些数据。 从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实例工程代码的简单分析。如有纰漏,还请评论指出,谢谢! 作者:天使燚飞翔-骄傲的放弃治疗 链接:https://juejin.im/post/59ac1f2bf265da249517ac72 |
|
|