[关闭]
@zyl06 2017-11-10T19:15:50.000000Z 字数 13395 阅读 2548

Android ARCore demo 简析

Android AR


1. 简介

ARCore 是 google 官方出的一款 AR SDK,其基本原理为:

ARCore 使用手机摄像头来辨识特征点,并跟踪这些特征点的移动轨迹。结合特征点的移动轨迹和手机的惯性传感器,ARCore 就可以在手机移动时判定它的位置、角度等信息。识别出特征点,就能在特征点的基础上,侦测平面,如地板、桌子等。另外能 ARCore 也支持估测周围的平均光照强度。有了手机自身的位置角度信息和周围的光照强度信息,ARCore 就可以构建周边世界的模型。

  1. 运动跟踪

    它利用 IMU 传感器和设备的相机来发现空间的特征点,由此确定 Android 设备的位置和方向。此外,使用 VPS,可以让 AR 物体每次看起来似乎都在同一位置。

  2. 环境感知

    虚拟物体一般都是放置于平坦平面上的,用 ARCore 可以检测物体的水平表面,建立环境认知感,以保证虚拟的对象可以准确放置,然后让您看到放置在这些表面上的 AR 物体。

  3. 光线预测

    ARCore 根据环境的光强度,使开发人员可以与周围环境相匹配的方式点亮虚拟对象。此外,最近的一个实验发现,虚拟阴影在真实环境光照下的调整功能也是如此,这样就可以使 AR 物体的外观更为逼真。

取自 ARCore ——移动AR的浪潮

2. Android Studio 工程配置

  1. 安装 Android Studio 2.3 及以上,使用 Android SDK Platform 版本 7.0
  2. 一台支持的 Android 设备 (暂时仅支持 Google Pixel 和 Samsung Galaxy S8 的2款设备)
  3. 获取 ARCore SDK

鉴于支持 ARCore 的 Android 设备太少,为此可通过修改 ARCore 的设备支持接口,修改方式如下:

  1. Open a command line interface
  2. Unzip the AAR to a temporary directory: unzip arcore_client-original.aar -d aar-tmp
  3. Enter the temporary aar directory: cd aar-tmp
  4. Unzip classes.jar to a temporary directory: unzip classes.jar -d classes-tmp
  5. Enter the temporary classes directory: cd classes-tmp
  6. Enter the directory containing the SupportedDevices class: cd com/google/atap/tangoservice
  7. Decompile the SupportedDevices class: java -jar /path/to/cfr.jar SupportedDevices.class > SupportedDevices.java
  8. Open a text editor and delete return false from the end of isSupported()
  9. Compile the modified SupportedDevice class: javac -cp /path/to/sdk/platform/android.jar -source 1.7 -target 1.7 SupportedDevices.java
  10. Delete the Java source: rm SupportedDevices.java
  11. Change directory back to aar-tmp: cd ../../../../../
  12. Create a JAR from the modified classes directory: jar cvf classes.jar -C classes-tmp .
  13. Change directory back to repo root: cd ..
  14. Create an AAR from the modified aar directory: jar cvf arcore_client.aar -C aar-tmp .

    取自 arcore-for-all


Manufacturer Device Model GPU 64-bit? Official Support? Functional?
Google Pixel All Adreno 530
Google Pixel XL All Adreno 530
Samsung Galaxy S6 G920 Mali-T760MP8 × ××
Samsung Galaxy S7 G930F Mali-T880 MP12 × ×
Samsung Galaxy S7 Edge G9350 (Hong Kong) Adreno 530 × ?
Samsung Galaxy S7 Edge G935FD, G935F, G935W8 Mali-T880 MP12 × ×
Samsung Galaxy S8 USA & China Adreno 540
Samsung Galaxy S8 EMEA Mali-G71 MP20 ?
Samsung Galaxy S8+ USA & China Adreno 540 ×
Samsung Galaxy S8+ G955F (EMEA) Mali-G71 MP20 ×
HTC HTC 10 All Adreno 530 × ×
Huawei Nexus 6P All Adreno 430 ×
Huawei P9 Lite All Mali-T830MP2 × ×
Huawei P10 All Mali-G71 MP8 × ×
LG G2 All Adreno 330 × × ×
LG V20 US996 Adreno 530 × ×
LG Nexus 5 All Adreno 330 × ×
LG Nexus 5X All Adreno 418 × ×
OnePlus 3 All Adreno 530 × ×
OnePlus 3T All Adreno 530 × ×
OnePlus X All Adreno 330 × × ×
OnePlus 5 All Adreno 540 ×
Nvidia Shield K1 All ULP GeForce Kepler × ×
Xiaomi Redmi Note 4 All Adreno 506 × ×
Xiaomi Mi 5s capricorn Adreno 530 × ×
Xiaomi Mi Mix All Adreno 530 × ×
Motorola Moto G4 All Adreno 405 × ×
Motorola Nexus 6 All Adreno 420 × × ×
ZTE Axon 7 A2017 Adreno 530 × ×
Sony Xperia XZs All Adreno 530 × ×

取自 arcore-for-all Device Research

3. demo 简析

3.0 demo 效果

3.1 配置工程

  1. 配置 sdk 版本信息

    1. compileSdkVersion 25
    2. buildToolsVersion "25.0.0"
    3. defaultConfig {
    4. applicationId "com.google.ar.core.examples.java.helloar"
    5. minSdkVersion 19
    6. targetSdkVersion 25
    7. versionCode 1
    8. versionName "1.0"
    9. }

    其中配置的 minSdkVersion 最小为 19

  2. 引入需要的第三方库

    1. dependencies {
    2. compile (name: 'arcore_client', ext: 'aar')
    3. compile (name: 'obj-0.2.1', ext: 'jar')
    4. ...
    5. }

    其中 obj-0.2.1.jar 包用于加载解析 obj 文件为模型数据

3.2 显示 Activity 布局和准备对象

  1. 布局

    1. <android.opengl.GLSurfaceView
    2. android:id="@+id/surfaceview"
    3. android:layout_width="match_parent"
    4. android:layout_height="match_parent"
    5. android:layout_gravity="top"/>
  2. 渲染封装对象

    • ObjectRenderer mVirtualObject

      google 机器人模型

    • ObjectRenderer mVirtualObjectShadow

      google 机器人阴影模型

    • PointCloudRenderer mPointCloud

      平面监测特征点

    • PlaneRenderer mPlaneRenderer

      平面识别成功之后的网格模型

    • BackgroundRenderer mBackgroundRenderer

      相机视频流数据显示至纹理

3.3 onCreate 初始化

  1. setContentView(R.layout.activity_main);
  2. mSurfaceView = (GLSurfaceView) findViewById(R.id.surfaceview);
  3. // 1.
  4. mSession = new Session(/*context=*/this);
  5. // 2.
  6. // Create default config, check is supported, create session from that config.
  7. mDefaultConfig = Config.createDefaultConfig();
  8. if (!mSession.isSupported(mDefaultConfig)) {
  9. Toast.makeText(this, "This device does not support AR", Toast.LENGTH_LONG).show();
  10. finish();
  11. return;
  12. }
  13. // 3.
  14. // 创建并设置 SurfaceView Tap 事件
  15. ...
  16. // 4.
  17. // Set up renderer.
  18. mSurfaceView.setPreserveEGLContextOnPause(true);
  19. mSurfaceView.setEGLContextClientVersion(2);
  20. mSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0); // Alpha used for plane blending.
  21. mSurfaceView.setRenderer(this);
  22. mSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
  1. 创建 Session 对象

    Session 用于处理 ARCore 状态,处理当前 AR 的生命周期(resume,pause),绑定背景相机图像纹理,设置视口显示大小,从视图中获取 frame 数据(可得到估计光照强度、投影矩阵、模型变换矩阵、图像特征点数据和模型矩阵、监测平面数据等)

  2. 创建 mDefaultConfig 并判断当前机型是否支持 ARCore

    暂时支持的机型,见 Supported Devices

  3. 创建并设置 SurfaceView Tap 事件

    记录用户在 Surface Tap 的位置信息,用于创建 Google 机器人

  4. SurfaceView 相关设置

    设置在 Pause 时保留 GL 上下文环境,设置 EGL 版本为 2.0,设置各个通道的大小,设置渲染监听实现,设置为主动渲染

3.4 处理生命周期

  1. onResume

    1. @Override
    2. protected void onResume() {
    3. super.onResume();
    4. if (CameraPermissionHelper.hasCameraPermission(this)) {
    5. showLoadingMessage();
    6. // Note that order matters - see the note in onPause(), the reverse applies here.
    7. mSession.resume(mDefaultConfig);
    8. mSurfaceView.onResume();
    9. } else {
    10. CameraPermissionHelper.requestCameraPermission(this);
    11. }
    12. }

    在页面 onResume 时调用 mSession.resume(mDefaultConfig)

  2. onPause

    1. @Override
    2. public void onPause() {
    3. super.onPause();
    4. mSurfaceView.onPause();
    5. mSession.pause();
    6. }

    在页面 onPause 时调用 mSession.pause(),停止页面查询 Session

    注意:mSession.pause() 必须在 mSurfaceView.onPause() 后面执行,否则 可能发生在 mSession.pause() 之后继续调用 mSession.update(),进而发生 SessionPausedException 异常

3.5 SurfaceView 显示准备

  1. @Override
  2. public void onSurfaceCreated(GL10 gl, EGLConfig config) {
  3. // 1.
  4. GLES20.glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
  5. // 2.
  6. mBackgroundRenderer.createOnGlThread(/*context=*/this);
  7. mSession.setCameraTextureName(mBackgroundRenderer.getTextureId());
  8. try {
  9. // 3.
  10. mVirtualObject.createOnGlThread(/*context=*/this, "andy.obj", "andy.png");
  11. mVirtualObject.setMaterialProperties(0.0f, 3.5f, 1.0f, 6.0f);
  12. // 4.
  13. mVirtualObjectShadow.createOnGlThread(/*context=*/this,
  14. "andy_shadow.obj", "andy_shadow.png");
  15. mVirtualObjectShadow.setBlendMode(BlendMode.Shadow);
  16. mVirtualObjectShadow.setMaterialProperties(1.0f, 0.0f, 0.0f, 1.0f);
  17. } catch (IOException e) {
  18. Log.e(TAG, "Failed to read obj file");
  19. }
  20. try {
  21. // 5.
  22. mPlaneRenderer.createOnGlThread(/*context=*/this, "trigrid.png");
  23. } catch (IOException e) {
  24. Log.e(TAG, "Failed to read plane texture");
  25. }
  26. // 6.
  27. mPointCloud.createOnGlThread(/*context=*/this);
  28. }
  1. 设置 opengl 帧缓存清空颜色为灰白色
  2. 初始化背景纹理对象(后续详细介绍),并将创建的纹理 id 绑定给 mSession 的相机纹理对象,用于显示相机产生的视频内容
  3. 初始化 Android 机器人显示对象,并设置材料反射光照(环境光无, 漫反射光 3.5, 镜面光 1.0, 光照聚焦 6.0)
  4. 初始化 Android 机器人阴影显示对象,设置显示混合模式(开启透明度,GLES20.GL_ONE_MINUS_SRC_ALPHA),设置显示材料反射光照信息(1.0f, 0.0f, 0.0f, 1.0f)
  5. 初始化平面监测结果显示对象
  6. 初始化特征点云显示对象

3.6 SurfaceView 大小改变

  1. @Override
  2. public void onSurfaceChanged(GL10 gl, int width, int height) {
  3. // 1. 设置 opengl 的视口大小
  4. GLES20.glViewport(0, 0, width, height);
  5. // 2. 设置 mSession 中的显示视口大小(和后续计算 frame 有关)
  6. mSession.setDisplayGeometry(width, height);
  7. }

3.7 SurfaceView 绘制

  1. @Override
  2. public void onDrawFrame(GL10 gl) {
  3. // 1.
  4. GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
  5. try {
  6. // 2.
  7. Frame frame = mSession.update();
  8. // 3.
  9. MotionEvent tap = mQueuedSingleTaps.poll();
  10. if (tap != null && frame.getTrackingState() == TrackingState.TRACKING) {
  11. for (HitResult hit : frame.hitTest(tap)) {
  12. // Check if any plane was hit, and if it was hit inside the plane polygon.
  13. if (hit instanceof PlaneHitResult && ((PlaneHitResult) hit).isHitInPolygon()) {
  14. // Cap the number of objects created. This avoids overloading both the
  15. // rendering system and ARCore.
  16. if (mTouches.size() >= 16) {
  17. mSession.removeAnchors(Arrays.asList(mTouches.get(0).getAnchor()));
  18. mTouches.remove(0);
  19. }
  20. // Adding an Anchor tells ARCore that it should track this position in
  21. // space. This anchor will be used in PlaneAttachment to place the 3d model
  22. // in the correct position relative both to the world and to the plane.
  23. mTouches.add(new PlaneAttachment(
  24. ((PlaneHitResult) hit).getPlane(),
  25. mSession.addAnchor(hit.getHitPose())));
  26. // Hits are sorted by depth. Consider only closest hit on a plane.
  27. break;
  28. }
  29. }
  30. }
  31. // 4.
  32. mBackgroundRenderer.draw(frame);
  33. // 5.
  34. if (frame.getTrackingState() == TrackingState.NOT_TRACKING) {
  35. return;
  36. }
  37. // 6.
  38. float[] projmtx = new float[16];
  39. mSession.getProjectionMatrix(projmtx, 0, 0.1f, 100.0f);
  40. // 7.
  41. float[] viewmtx = new float[16];
  42. frame.getViewMatrix(viewmtx, 0);
  43. // 8.
  44. // Compute lighting from average intensity of the image.
  45. final float lightIntensity = frame.getLightEstimate().getPixelIntensity();
  46. // 9.
  47. // Visualize tracked points.
  48. mPointCloud.update(frame.getPointCloud());
  49. mPointCloud.draw(frame.getPointCloudPose(), viewmtx, projmtx);
  50. // 10.
  51. // Check if we detected at least one plane. If so, hide the loading message.
  52. ...
  53. // 11. Visualize planes.
  54. mPlaneRenderer.drawPlanes(mSession.getAllPlanes(), frame.getPose(), projmtx);
  55. // 12. Visualize anchors created by touch.
  56. float scaleFactor = 1.0f;
  57. for (PlaneAttachment planeAttachment : mTouches) {
  58. if (!planeAttachment.isTracking()) {
  59. continue;
  60. }
  61. // Get the current combined pose of an Anchor and Plane in world space. The Anchor
  62. // and Plane poses are updated during calls to session.update() as ARCore refines
  63. // its estimate of the world.
  64. planeAttachment.getPose().toMatrix(mAnchorMatrix, 0);
  65. // Update and draw the model and its shadow.
  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. }
  75. }
  1. 清除颜色和深度缓冲区
  2. 从 mSession 得到最新的 frame
  3. 遍历前面 tap 操作得到的点列表(可以理解以此为起点,垂直向下的一条射线),计算射线是否和 frame 中的平面有交点,且交点是否处理平面监测得到的区域里面。若是,则保存平面信息和 pose 信息(可得到模型的位置和转向信息)
  4. 绘制相机背景视图
  5. 判断是否已经检测到平面,没有的话,不在显示后续内容
  6. 从 frame 中得到投影矩阵
  7. 从 frame 中得到模型变换矩阵
  8. 得到预计的环境光照强度
  9. 更新检测的特征点云的坐标并绘制
  10. 取消显示正在检测中的 toast(和 AR 无关)
  11. 绘制检测到的平面(显示为网格状)
  12. 遍历第 3 步记录的 PlaneAttachment 列表,得到投影矩阵和模型变换矩阵,并结合第 8 步得到的光照强度,绘制 Android 机器人和阴影

3.8 显示相机捕捉的视频内容

OpenGL 相关,和 AR 无关

3.8.1 初始化背景纹理对象
  1. public void createOnGlThread(Context context) {
  2. // 1.
  3. int textures[] = new int[1];
  4. GLES20.glGenTextures(1, textures, 0);
  5. mTextureId = textures[0];
  6. GLES20.glBindTexture(mTextureTarget, mTextureId);
  7. GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
  8. GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
  9. GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
  10. GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
  11. int numVertices = 4;
  12. if (numVertices != QUAD_COORDS.length / COORDS_PER_VERTEX) {
  13. throw new RuntimeException("Unexpected number of vertices in BackgroundRenderer.");
  14. }
  15. // 2.
  16. ByteBuffer bbVertices = ByteBuffer.allocateDirect(QUAD_COORDS.length * FLOAT_SIZE);
  17. bbVertices.order(ByteOrder.nativeOrder());
  18. mQuadVertices = bbVertices.asFloatBuffer();
  19. mQuadVertices.put(QUAD_COORDS);
  20. mQuadVertices.position(0);
  21. // 3.
  22. ByteBuffer bbTexCoords = ByteBuffer.allocateDirect(
  23. numVertices * TEXCOORDS_PER_VERTEX * FLOAT_SIZE);
  24. bbTexCoords.order(ByteOrder.nativeOrder());
  25. mQuadTexCoord = bbTexCoords.asFloatBuffer();
  26. mQuadTexCoord.put(QUAD_TEXCOORDS);
  27. mQuadTexCoord.position(0);
  28. // 4.
  29. ByteBuffer bbTexCoordsTransformed = ByteBuffer.allocateDirect(
  30. numVertices * TEXCOORDS_PER_VERTEX * FLOAT_SIZE);
  31. bbTexCoordsTransformed.order(ByteOrder.nativeOrder());
  32. mQuadTexCoordTransformed = bbTexCoordsTransformed.asFloatBuffer();
  33. // 5.
  34. int vertexShader = ShaderUtil.loadGLShader(TAG, context,
  35. GLES20.GL_VERTEX_SHADER, R.raw.screenquad_vertex);
  36. // 6.
  37. int fragmentShader = ShaderUtil.loadGLShader(TAG, context,
  38. GLES20.GL_FRAGMENT_SHADER, R.raw.screenquad_fragment_oes);
  39. // 7.
  40. mQuadProgram = GLES20.glCreateProgram();
  41. GLES20.glAttachShader(mQuadProgram, vertexShader);
  42. GLES20.glAttachShader(mQuadProgram, fragmentShader);
  43. GLES20.glLinkProgram(mQuadProgram);
  44. GLES20.glUseProgram(mQuadProgram);
  45. // 8.
  46. ShaderUtil.checkGLError(TAG, "Program creation");
  47. // 9.
  48. mQuadPositionParam = GLES20.glGetAttribLocation(mQuadProgram, "a_Position");
  49. mQuadTexCoordParam = GLES20.glGetAttribLocation(mQuadProgram, "a_TexCoord");
  50. ShaderUtil.checkGLError(TAG, "Program parameters");
  51. }
  1. 创建纹理对象id,并绑定到 GL_TEXTURE_EXTERNAL_OES,设置纹理贴图的效果和缩放效果

    绑定的纹理不是 GL_TEXTURE_2D,而是 GL_TEXTURE_EXTERNAL_OES,是因为 Camera 使用的输出 texture 是一种特殊的格式。同样的,在 shader 中也必须使用 SamperExternalOES 的变量类型来访问该纹理

    1. #extension GL_OES_EGL_image_external : require
    2. precision mediump float;
    3. varying vec2 v_TexCoord;
    4. uniform samplerExternalOES sTexture;
    5. void main() {
    6. gl_FragColor = texture2D(sTexture, v_TexCoord);
    7. }

    片元显示器

  2. 设置纹理几何的顶点坐标

  3. 设置纹理的初始贴图坐标
  4. 设置纹理最终的贴图坐标,可从 Frame 中计算得到
  5. 加载顶点显示器

    1. attribute vec4 a_Position;
    2. attribute vec2 a_TexCoord;
    3. varying vec2 v_TexCoord;
    4. void main() {
    5. gl_Position = a_Position;
    6. v_TexCoord = a_TexCoord;
    7. }
  6. 加载片元显示器
  7. opengl 程序连接编译
  8. 检查 opengl 错误
  9. 初始化背景矩形的顶点坐标对象,和纹理坐标对象
3.8.2 绘制相机捕获的视频帧至纹理

3.7 SurfaceView 绘制 的第 4 步调用了 mBackgroundRenderer.draw(frame) 其内容如下:

  1. public class BackgroundRenderer {
  2. ...
  3. public void draw(Frame frame) {
  4. // 1.
  5. if (frame.isDisplayRotationChanged()) {
  6. frame.transformDisplayUvCoords(mQuadTexCoord, mQuadTexCoordTransformed);
  7. }
  8. // 2.
  9. GLES20.glDisable(GLES20.GL_DEPTH_TEST);
  10. GLES20.glDepthMask(false);
  11. // 3.
  12. GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId);
  13. // 4.
  14. GLES20.glUseProgram(mQuadProgram);
  15. // 5.
  16. GLES20.glVertexAttribPointer(
  17. mQuadPositionParam, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, mQuadVertices);
  18. GLES20.glVertexAttribPointer(mQuadTexCoordParam, TEXCOORDS_PER_VERTEX,
  19. GLES20.GL_FLOAT, false, 0, mQuadTexCoordTransformed);
  20. GLES20.glEnableVertexAttribArray(mQuadPositionParam);
  21. GLES20.glEnableVertexAttribArray(mQuadTexCoordParam);
  22. GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
  23. GLES20.glDisableVertexAttribArray(mQuadPositionParam);
  24. GLES20.glDisableVertexAttribArray(mQuadTexCoordParam);
  25. // 6.
  26. GLES20.glDepthMask(true);
  27. GLES20.glEnable(GLES20.GL_DEPTH_TEST);
  28. // 7.
  29. ShaderUtil.checkGLError(TAG, "Draw");
  30. }
  31. }
  1. 当显示角度发生变化或者 SurfaceView 大小发生变化,重新计算背景的纹理 uv 坐标

    是否发生变化,如何计算,均有 frame 接口提供

  2. 关闭深度测试和深度缓冲区的可读性为不可读

    相机视频帧数据是在所有模型的后面

  3. 绑定 mTextureId 至 GLES11Ext.GL_TEXTURE_EXTERNAL_OES

  4. 设置使用绘制背景的 shader 程序
  5. 将背景四边形顶点数据和纹理贴图的 uv 数据设置给绑定的 shader 变量,设置数据在着色器端可见,绘制矩形内容,关闭数据在着色器端可见
  6. 重新打开深度测试和深度缓冲区的可读性
  7. 检查错误

其他内容显示类似,不再赘述

4. 总结

由上其实可以看到场景的显示等,其实并不是 ARCore 关心的,ARCore 提供给我们的数据或帮我们完成的功能有:

  1. 判断当前机型是否支持 ARCore
  2. 提供接口判断当前角度是否发生变化,转换相机帧纹理的 uv 坐标
  3. 提供接口提取可用的 frame,得到投影矩阵和模型变换矩阵,得到特征点云,监测出来的平面,用于后续业务开发显示场景
  4. 提供接口获取估计光照强度
  5. 提供相机帧中的特征点和检测得到的平面数据
  6. 提供接口计算点击操作和场景是否相交,和交点信息(包括交点垂直平面的位置和方向)
  7. 暂时未见如果识别特定图片,显示模型的 demo

相比其他第三方库,如 EasyAR 收费版本,支持估计周围环境光照强度,slam 算法更加稳定强大,效果更好;相比 Vuforia 支持平面监测,但当前可支持机型还比较少。而非支持机型,通过改 ARR 代码强制支持,效果也不太理想,如 Nexus 6P。

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