1. 简介
ARCore 是 google 官方出的一款 AR SDK,其基本原理为:
ARCore 使用手机摄像头来辨识特征点,并跟踪这些特征点的移动轨迹。结合特征点的移动轨迹和手机的惯性传感器,ARCore 就可以在手机移动时判定它的位置、角度等信息。识别出特征点,就能在特征点的基础上,侦测平面,如地板、桌子等。另外能 ARCore 也支持估测周围的平均光照强度。有了手机自身的位置角度信息和周围的光照强度信息,ARCore 就可以构建周边世界的模型。
-
运动跟踪
它利用 IMU 传感器和设备的相机来发现空间的特征点,由此确定 Android 设备的位置和方向。此外,使用 VPS,可以让 AR 物体每次看起来似乎都在同一位置。
-
环境感知
虚拟物体一般都是放置于平坦平面上的,用 ARCore 可以检测物体的水平表面,建立环境认知感,以保证虚拟的对象可以准确放置,然后让您看到放置在这些表面上的 AR 物体。
-
光线预测
ARCore 根据环境的光强度,使开发人员可以与周围环境相匹配的方式点亮虚拟对象。此外,最近的一个实验发现,虚拟阴影在真实环境光照下的调整功能也是如此,这样就可以使 AR 物体的外观更为逼真。
2. Android Studio 工程配置
- 安装 Android Studio 2.3 及以上,使用 Android SDK Platform 版本 7.0
- 一台支持的 Android 设备 (暂时仅支持 Google Pixel 和 Samsung Galaxy S8 的2款设备)
- 获取 ARCore SDK
鉴于支持 ARCore 的 Android 设备太少,为此可通过修改 ARCore 的设备支持接口,修改方式如下:
- Open a command line interface
- Unzip the AAR to a temporary directory: unzip arcore_client-original.aar -d aar-tmp
- Enter the temporary aar directory: cd aar-tmp
- Unzip classes.jar to a temporary directory: unzip classes.jar -d classes-tmp
- Enter the temporary classes directory: cd classes-tmp
- Enter the directory containing the SupportedDevices class: cd com/google/atap/tangoservice
- Decompile the SupportedDevices class: java -jar /path/to/cfr.jar SupportedDevices.class > SupportedDevices.java
- Open a text editor and delete return false from the end of isSupported()
- Compile the modified SupportedDevice class: javac -cp /path/to/sdk/platform/android.jar -source 1.7 -target 1.7 SupportedDevices.java
- Delete the Java source: rm SupportedDevices.java
- Change directory back to aar-tmp: cd ../../../../../
- Create a JAR from the modified classes directory: jar cvf classes.jar -C classes-tmp .
- Change directory back to repo root: cd ..
- Create an AAR from the modified aar directory: jar cvf arcore_client.aar -C aar-tmp .
强行修改 ARCore 的支持函数判断后,各机型的支持程度如下:
Manufacturer | Device | Model | GPU | 64-bit? | Official Support? | Functional? |
---|---|---|---|---|---|---|
Pixel | All | Adreno 530 | √ | √ | √ | |
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 | √ | × | × |
实际使用 Nexus 6P 运行 arcore-for-all sample.apk 效果并不如意,平面监测效果较差,较难形成平面
3. demo 简析
3.0 demo 效果
3.1 配置工程
-
配置 sdk 版本信息
compileSdkVersion 25 buildToolsVersion "25.0.0" defaultConfig { applicationId "com.google.ar.core.examples.java.helloar" minSdkVersion 19 targetSdkVersion 25 versionCode 1 versionName "1.0" }
其中配置的
minSdkVersion
最小为 19 -
引入需要的第三方库
dependencies { compile (name: 'arcore_client', ext: 'aar') compile (name: 'obj-0.2.1', ext: 'jar') ... }
其中 obj-0.2.1.jar 包用于加载解析 obj 文件为模型数据
3.2 显示 Activity 布局和准备对象
-
布局
<android.opengl.GLSurfaceView android:id="@+id/surfaceview" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="top"/>
-
渲染封装对象
-
ObjectRenderer mVirtualObject
google 机器人模型
-
ObjectRenderer mVirtualObjectShadow
google 机器人阴影模型
-
PointCloudRenderer mPointCloud
平面监测特征点
-
PlaneRenderer mPlaneRenderer
平面识别成功之后的网格模型
-
BackgroundRenderer mBackgroundRenderer
相机视频流数据显示至纹理
-
3.3 onCreate 初始化
setContentView(R.layout.activity_main);
mSurfaceView = (GLSurfaceView) findViewById(R.id.surfaceview);
// 1.
mSession = new Session(/*context=*/this);
// 2.
// Create default config, check is supported, create session from that config.
mDefaultConfig = Config.createDefaultConfig();
if (!mSession.isSupported(mDefaultConfig)) {
Toast.makeText(this, "This device does not support AR", Toast.LENGTH_LONG).show();
finish();
return;
}
// 3.
// 创建并设置 SurfaceView Tap 事件
...
// 4.
// 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);
-
创建 Session 对象
Session
用于处理 ARCore 状态,处理当前 AR 的生命周期(resume,pause),绑定背景相机图像纹理,设置视口显示大小,从视图中获取 frame 数据(可得到估计光照强度、投影矩阵、模型变换矩阵、图像特征点数据和模型矩阵、监测平面数据等) -
创建
mDefaultConfig
并判断当前机型是否支持 ARCore暂时支持的机型,见 Supported Devices
-
创建并设置 SurfaceView Tap 事件
记录用户在 Surface Tap 的位置信息,用于创建 Google 机器人
-
SurfaceView 相关设置
设置在 Pause 时保留 GL 上下文环境,设置 EGL 版本为 2.0,设置各个通道的大小,设置渲染监听实现,设置为主动渲染
3.4 处理生命周期
-
onResume
@Override protected void onResume() { super.onResume(); if (CameraPermissionHelper.hasCameraPermission(this)) { showLoadingMessage(); // Note that order matters - see the note in onPause(), the reverse applies here. mSession.resume(mDefaultConfig); mSurfaceView.onResume(); } else { CameraPermissionHelper.requestCameraPermission(this); } }
在页面 onResume 时调用
mSession.resume(mDefaultConfig)
-
onPause
@Override public void onPause() { super.onPause(); mSurfaceView.onPause(); mSession.pause(); }
在页面 onPause 时调用
mSession.pause()
,停止页面查询 Session注意:mSession.pause() 必须在 mSurfaceView.onPause() 后面执行,否则 可能发生在 mSession.pause() 之后继续调用 mSession.update(),进而发生 SessionPausedException 异常
3.5 SurfaceView 显示准备
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 1.
GLES20.glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
// 2.
mBackgroundRenderer.createOnGlThread(/*context=*/this);
mSession.setCameraTextureName(mBackgroundRenderer.getTextureId());
try {
// 3.
mVirtualObject.createOnGlThread(/*context=*/this, "andy.obj", "andy.png");
mVirtualObject.setMaterialProperties(0.0f, 3.5f, 1.0f, 6.0f);
// 4.
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 {
// 5.
mPlaneRenderer.createOnGlThread(/*context=*/this, "trigrid.png");
} catch (IOException e) {
Log.e(TAG, "Failed to read plane texture");
}
// 6.
mPointCloud.createOnGlThread(/*context=*/this);
}
- 设置 opengl 帧缓存清空颜色为灰白色
- 初始化背景纹理对象(后续详细介绍),并将创建的纹理 id 绑定给 mSession 的相机纹理对象,用于显示相机产生的视频内容
- 初始化 Android 机器人显示对象,并设置材料反射光照(环境光无, 漫反射光 3.5, 镜面光 1.0, 光照聚焦 6.0)
- 初始化 Android 机器人阴影显示对象,设置显示混合模式(开启透明度,GLES20.GL_ONE_MINUS_SRC_ALPHA),设置显示材料反射光照信息(1.0f, 0.0f, 0.0f, 1.0f)
- 初始化平面监测结果显示对象
- 初始化特征点云显示对象
3.6 SurfaceView 大小改变
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// 1. 设置 opengl 的视口大小
GLES20.glViewport(0, 0, width, height);
// 2. 设置 mSession 中的显示视口大小(和后续计算 frame 有关)
mSession.setDisplayGeometry(width, height);
}
3.7 SurfaceView 绘制
@Override
public void onDrawFrame(GL10 gl) {
// 1.
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
try {
// 2.
Frame frame = mSession.update();
// 3.
MotionEvent tap = mQueuedSingleTaps.poll();
if (tap != null && frame.getTrackingState() == TrackingState.TRACKING) {
for (HitResult hit : frame.hitTest(tap)) {
// Check if any plane was hit, and if it was hit inside the plane polygon.
if (hit instanceof PlaneHitResult && ((PlaneHitResult) hit).isHitInPolygon()) {
// Cap the number of objects created. This avoids overloading both the
// rendering system and ARCore.
if (mTouches.size() >= 16) {
mSession.removeAnchors(Arrays.asList(mTouches.get(0).getAnchor()));
mTouches.remove(0);
}
// Adding an Anchor tells ARCore that it should track this position in
// space. This anchor will be used in PlaneAttachment to place the 3d model
// in the correct position relative both to the world and to the plane.
mTouches.add(new PlaneAttachment(
((PlaneHitResult) hit).getPlane(),
mSession.addAnchor(hit.getHitPose())));
// Hits are sorted by depth. Consider only closest hit on a plane.
break;
}
}
}
// 4.
mBackgroundRenderer.draw(frame);
// 5.
if (frame.getTrackingState() == TrackingState.NOT_TRACKING) {
return;
}
// 6.
float[] projmtx = new float[16];
mSession.getProjectionMatrix(projmtx, 0, 0.1f, 100.0f);
// 7.
float[] viewmtx = new float[16];
frame.getViewMatrix(viewmtx, 0);
// 8.
// Compute lighting from average intensity of the image.
final float lightIntensity = frame.getLightEstimate().getPixelIntensity();
// 9.
// Visualize tracked points.
mPointCloud.update(frame.getPointCloud());
mPointCloud.draw(frame.getPointCloudPose(), viewmtx, projmtx);
// 10.
// Check if we detected at least one plane. If so, hide the loading message.
...
// 11. Visualize planes.
mPlaneRenderer.drawPlanes(mSession.getAllPlanes(), frame.getPose(), projmtx);
// 12. Visualize anchors created by touch.
float scaleFactor = 1.0f;
for (PlaneAttachment planeAttachment : mTouches) {
if (!planeAttachment.isTracking()) {
continue;
}
// Get the current combined pose of an Anchor and Plane in world space. The Anchor
// and Plane poses are updated during calls to session.update() as ARCore refines
// its estimate of the world.
planeAttachment.getPose().toMatrix(mAnchorMatrix, 0);
// Update and draw the model and its shadow.
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);
}
}
- 清除颜色和深度缓冲区
- 从 mSession 得到最新的 frame
- 遍历前面 tap 操作得到的点列表(可以理解以此为起点,垂直向下的一条射线),计算射线是否和 frame 中的平面有交点,且交点是否处理平面监测得到的区域里面。若是,则保存平面信息和 pose 信息(可得到模型的位置和转向信息)
- 绘制相机背景视图
- 判断是否已经检测到平面,没有的话,不在显示后续内容
- 从 frame 中得到投影矩阵
- 从 frame 中得到模型变换矩阵
- 得到预计的环境光照强度
- 更新检测的特征点云的坐标并绘制
- 取消显示正在检测中的 toast(和 AR 无关)
- 绘制检测到的平面(显示为网格状)
- 遍历第 3 步记录的 PlaneAttachment 列表,得到投影矩阵和模型变换矩阵,并结合第 8 步得到的光照强度,绘制 Android 机器人和阴影
3.8 显示相机捕捉的视频内容
OpenGL 相关,和 AR 无关
3.8.1 初始化背景纹理对象
public void createOnGlThread(Context context) {
// 1.
int textures[] = new int[1];
GLES20.glGenTextures(1, textures, 0);
mTextureId = textures[0];
GLES20.glBindTexture(mTextureTarget, mTextureId);
GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
int numVertices = 4;
if (numVertices != QUAD_COORDS.length / COORDS_PER_VERTEX) {
throw new RuntimeException("Unexpected number of vertices in BackgroundRenderer.");
}
// 2.
ByteBuffer bbVertices = ByteBuffer.allocateDirect(QUAD_COORDS.length * FLOAT_SIZE);
bbVertices.order(ByteOrder.nativeOrder());
mQuadVertices = bbVertices.asFloatBuffer();
mQuadVertices.put(QUAD_COORDS);
mQuadVertices.position(0);
// 3.
ByteBuffer bbTexCoords = ByteBuffer.allocateDirect(
numVertices * TEXCOORDS_PER_VERTEX * FLOAT_SIZE);
bbTexCoords.order(ByteOrder.nativeOrder());
mQuadTexCoord = bbTexCoords.asFloatBuffer();
mQuadTexCoord.put(QUAD_TEXCOORDS);
mQuadTexCoord.position(0);
// 4.
ByteBuffer bbTexCoordsTransformed = ByteBuffer.allocateDirect(
numVertices * TEXCOORDS_PER_VERTEX * FLOAT_SIZE);
bbTexCoordsTransformed.order(ByteOrder.nativeOrder());
mQuadTexCoordTransformed = bbTexCoordsTransformed.asFloatBuffer();
// 5.
int vertexShader = ShaderUtil.loadGLShader(TAG, context,
GLES20.GL_VERTEX_SHADER, R.raw.screenquad_vertex);
// 6.
int fragmentShader = ShaderUtil.loadGLShader(TAG, context,
GLES20.GL_FRAGMENT_SHADER, R.raw.screenquad_fragment_oes);
// 7.
mQuadProgram = GLES20.glCreateProgram();
GLES20.glAttachShader(mQuadProgram, vertexShader);
GLES20.glAttachShader(mQuadProgram, fragmentShader);
GLES20.glLinkProgram(mQuadProgram);
GLES20.glUseProgram(mQuadProgram);
// 8.
ShaderUtil.checkGLError(TAG, "Program creation");
// 9.
mQuadPositionParam = GLES20.glGetAttribLocation(mQuadProgram, "a_Position");
mQuadTexCoordParam = GLES20.glGetAttribLocation(mQuadProgram, "a_TexCoord");
ShaderUtil.checkGLError(TAG, "Program parameters");
}
-
创建纹理对象id,并绑定到
GL_TEXTURE_EXTERNAL_OES
,设置纹理贴图的效果和缩放效果绑定的纹理不是
GL_TEXTURE_2D
,而是GL_TEXTURE_EXTERNAL_OES
,是因为Camera
使用的输出texture
是一种特殊的格式。同样的,在 shader 中也必须使用SamperExternalOES
的变量类型来访问该纹理#extension GL_OES_EGL_image_external : require precision mediump float; varying vec2 v_TexCoord; uniform samplerExternalOES sTexture; void main() { gl_FragColor = texture2D(sTexture, v_TexCoord); }
片元显示器
设置纹理几何的顶点坐标
设置纹理的初始贴图坐标
设置纹理最终的贴图坐标,可从 Frame 中计算得到
-
加载顶点显示器
attribute vec4 a_Position; attribute vec2 a_TexCoord; varying vec2 v_TexCoord; void main() { gl_Position = a_Position; v_TexCoord = a_TexCoord; }
加载片元显示器
opengl 程序连接编译
检查 opengl 错误
初始化背景矩形的顶点坐标对象,和纹理坐标对象
3.8.2 绘制相机捕获的视频帧至纹理
3.7 SurfaceView 绘制
的第 4 步调用了 mBackgroundRenderer.draw(frame)
其内容如下:
public class BackgroundRenderer {
...
public void draw(Frame frame) {
// 1.
if (frame.isDisplayRotationChanged()) {
frame.transformDisplayUvCoords(mQuadTexCoord, mQuadTexCoordTransformed);
}
// 2.
GLES20.glDisable(GLES20.GL_DEPTH_TEST);
GLES20.glDepthMask(false);
// 3.
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId);
// 4.
GLES20.glUseProgram(mQuadProgram);
// 5.
GLES20.glVertexAttribPointer(
mQuadPositionParam, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, mQuadVertices);
GLES20.glVertexAttribPointer(mQuadTexCoordParam, TEXCOORDS_PER_VERTEX,
GLES20.GL_FLOAT, false, 0, mQuadTexCoordTransformed);
GLES20.glEnableVertexAttribArray(mQuadPositionParam);
GLES20.glEnableVertexAttribArray(mQuadTexCoordParam);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
GLES20.glDisableVertexAttribArray(mQuadPositionParam);
GLES20.glDisableVertexAttribArray(mQuadTexCoordParam);
// 6.
GLES20.glDepthMask(true);
GLES20.glEnable(GLES20.GL_DEPTH_TEST);
// 7.
ShaderUtil.checkGLError(TAG, "Draw");
}
}
-
当显示角度发生变化或者 SurfaceView 大小发生变化,重新计算背景的纹理 uv 坐标
是否发生变化,如何计算,均有 frame 接口提供
-
关闭深度测试和深度缓冲区的可读性为不可读
相机视频帧数据是在所有模型的后面
绑定 mTextureId 至 GLES11Ext.GL_TEXTURE_EXTERNAL_OES
设置使用绘制背景的 shader 程序
将背景四边形顶点数据和纹理贴图的 uv 数据设置给绑定的 shader 变量,设置数据在着色器端可见,绘制矩形内容,关闭数据在着色器端可见
重新打开深度测试和深度缓冲区的可读性
检查错误
其他内容显示类似,不再赘述
4. 总结
由上其实可以看到场景的显示等,其实并不是 ARCore 关心的,ARCore 提供给我们的数据或帮我们完成的功能有:
- 判断当前机型是否支持 ARCore
- 提供接口判断当前角度是否发生变化,转换相机帧纹理的 uv 坐标
- 提供接口提取可用的 frame,得到投影矩阵和模型变换矩阵,得到特征点云,监测出来的平面,用于后续业务开发显示场景
- 提供接口获取估计光照强度
- 提供相机帧中的特征点和检测得到的平面数据
- 提供接口计算点击操作和场景是否相交,和交点信息(包括交点垂直平面的位置和方向)
- 暂时未见如果识别特定图片,显示模型的 demo
相比其他第三方库,如 EasyAR 收费版本,支持估计周围环境光照强度,slam 算法更加稳定强大,效果更好;相比 Vuforia 支持平面监测,但当前可支持机型还比较少。而非支持机型,通过改 ARR 代码强制支持,效果也不太理想,如 Nexus 6P。