ARCore 相关
1、概述
ARCore 是 Google 的增强现实体验构建平台。 ARCore 利用不同的 API 让手机能够感知其环境、了解现实世界并与信息进行交互。 在一些 Android 和 iOS 上提供的 API 支持共享 AR 体验。在讨论ARCore之前先说一下AR、VR等概念。其实AR、VR可以统称为沉浸式计算用于描述为用户提供身临其境体验的应用程序,以增强或虚拟现实体验的形式出现。为了更好地理解沉浸式计算的范围,来看一下图:
沉浸式计算谱从上面可以看到,当你看到的事物都是真实世界中的东西时候,那就是传统的应用程序。当我们看到的事物全部由计算生成的虚拟世界,那种应用程序就是我们所说的VR(虚拟现实),而处于两者之间的应用程序就是所谓的AR(增强现实)。而这个文集集中就是要讨论使用ARCore这个框架来构建AR程序。
2、支持的设备
ARCore是一个在Android上构建增强现实应用程序的平台,旨在用于运行Nougat(7.0)及更高版本的各种合格Android手机。在中国地区目前只有小米和华为两家公司支持ARCore,其支持型号如下:
支持ARCore型号从上表可以看出,两家公司的最新几款机型都已经支持ARCore了。可以预见在未来,ARCore模块会渐渐的被所有后续机型所支持。
3、基本概念
ARCore起源于Tango,但它是一种更先进的AR工具包,使用内置于设备中的特殊传感器。为了使AR更易于访问和主流化,Google开发了ARCore作为AR工具包,专为未配备任何特殊传感器的Android设备而设计。在Tango依赖于特殊传感器的地方,ARCore使用软件来尝试完成相同的核心增强。对于ARCore,官方确定了使用此工具包解决的三个核心领域。
3.1 运动跟踪
跟踪用户的运动并最终在2D和3D空间中的确定其位置是任何AR应用的基础。ARCore可以通过识别和跟踪相机设备图像中的可视特征点来跟踪位置变化。
运动追踪在上图中,可以看到如何跟踪用户的位置与真实沙发上识别的特征点的关系。以前,为了成功跟踪运动位),需要预先注册或预先训练特征点。现在,ARCore无需任何训练即可实时自动完成所有这些工作。
它使用手机摄像头来检测进入现实世界的不同特征点。当摄像机移动时,ARCore将跟踪屏幕上所有这些特征点的位置变化。与远离相机的点相比,靠近相机的点将在屏幕上移动得更少。根据这些数据,ARCore来计算特征点与摄像机的相对距离。ARCore还利用手机内置传感器来检测手机的方向。
3.2 环境理解
AR应用程序了解用户的现实或周围环境越好,沉浸感就越成功。ARCore寻找看似位于常见水平或垂直表面(如表格或墙壁)上的特征点群集,并使这些曲面可用作平面。ARCore还可以确定每个平面的边界,并将这些信息提供给应用程序。可以使用此信息将虚拟对象放置在平面上。ARCore使用一种称为网格划分的技术来做到这一点。
环境理解由上图所示,平面上的三角形分布(由ARCore标识)是检测到的平面,可以在其中放置虚拟对象。
3.3 光估计
ARCore使用手机摄像头检测场景的平均光照条件。它提供有关给定场景的平均光强度和颜色校正信息的信息。然后,开发者可以使用此照明信息来照亮虚拟AR对象。它可以检测有关其环境照明的信息,并为开发者提供给定摄像机图像的平均光强度和颜色校正。
光估计4、使用ARCore
ARCore框架可以通过OpenGL来进行使用,但是其代码相对来说较为复杂在今年的IO大会上谷歌推出了Sceneform框架来简化ARCore的使用操作。想看如何用OpenGL进行ARCore的使用。可以查看如下官方样例:hello_ar。这个例子展示了如何使用ARCore API,来创建增强现实(AR)应用程序。应用程序将显示任何检测到的平面,并允许用户点击平面来放置Android机器人的3D模型。其核心代码如下:
public class HelloArActivity extends AppCompatActivity implements GLSurfaceView.Renderer {
private static final String TAG = HelloArActivity.class.getSimpleName();
// 渲染器在创建GLSurfaceView 的时候创建和初始化,
private GLSurfaceView surfaceView;
private boolean installRequested;
private Session session;
private final SnackbarHelper messageSnackbarHelper = new SnackbarHelper();
private DisplayRotationHelper displayRotationHelper;
private TapHelper tapHelper;
private final BackgroundRenderer backgroundRenderer = new BackgroundRenderer();
private final ObjectRenderer virtualObject = new ObjectRenderer();
private final ObjectRenderer virtualObjectShadow = new ObjectRenderer();
private final PlaneRenderer planeRenderer = new PlaneRenderer();
private final PointCloudRenderer pointCloudRenderer = new PointCloudRenderer();
// 临时矩阵在这里直接分配空间,而不是对每一帧进行重新分配。这样可以减少每一帧渲染的压力。
private final float[] anchorMatrix = new float[16];
private static final float[] DEFAULT_COLOR = new float[] {0f, 0f, 0f, 0f};
// 这个数据结构用来描述Anchor及及该锚点颜色。Anchor指用来描述现实世界中的固定位置和方向的点。
private static class ColoredAnchor {
public final Anchor anchor;
public final float[] color;
public ColoredAnchor(Anchor a, float[] color4f) {
this.anchor = a;
this.color = color4f;
}
}
private final ArrayList<ColoredAnchor> anchors = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
surfaceView = findViewById(R.id.surfaceview);
displayRotationHelper = new DisplayRotationHelper(/*context=*/ this);
// 设置点击监听器
tapHelper = new TapHelper(/*context=*/ this);
surfaceView.setOnTouchListener(tapHelper);
// 设置渲染器
surfaceView.setPreserveEGLContextOnPause(true);
surfaceView.setEGLContextClientVersion(2);
surfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0); // Alpha used for plane blending.
surfaceView.setRenderer(this);
surfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
installRequested = false;
}
@Override
protected void onResume() {
super.onResume();
if (session == null) {
Exception exception = null;
String message = null;
try {
switch (ArCoreApk.getInstance().requestInstall(this, !installRequested)) {
case INSTALL_REQUESTED:
installRequested = true;
return;
case INSTALLED:
break;
}
//ARCore需要相机权限才能运行。这里获取相机权限
if (!CameraPermissionHelper.hasCameraPermission(this)) {
CameraPermissionHelper.requestCameraPermission(this);
return;
}
// 创建 session.session类用来管理AR系统状态并处理session自己的生命周期。
session = new Session(/* context= */ this);
} catch (UnavailableArcoreNotInstalledException
| UnavailableUserDeclinedInstallationException e) {
message = "Please install ARCore";
exception = e;
} catch (UnavailableApkTooOldException e) {
message = "Please update ARCore";
exception = e;
} catch (UnavailableSdkTooOldException e) {
message = "Please update this app";
exception = e;
} catch (UnavailableDeviceNotCompatibleException e) {
message = "This device does not support AR";
exception = e;
} catch (Exception e) {
message = "Failed to create AR session";
exception = e;
}
if (message != null) {
messageSnackbarHelper.showError(this, message);
Log.e(TAG, "Exception creating session", exception);
return;
}
}
// 调用顺序很重要 - 请参阅onPause()中的注释,在onResume()中顺序与onPause()中的相反。
try {
session.resume();
} catch (CameraNotAvailableException e) {
//在某些情况下(例如另一个相机应用程序启动),相机可能会被提供给q其他应用程序。
// 通过显示错误提示并在下一次迭代中重新创建session 来正确解决此问题。
messageSnackbarHelper.showError(this, "Camera not available. Please restart the app.");
session = null;
return;
}
surfaceView.onResume();
displayRotationHelper.onResume();
messageSnackbarHelper.showMessage(this, "Searching for surfaces...");
}
@Override
public void onPause() {
super.onPause();
if (session != null) {
//请注意,调用顺序 - 首先暂停GLSurfaceView,以便它不会尝试查询Session。 如果在GLSurfaceView之前暂停Session,则GLSurfaceView仍可调用session.update()从而可能导致抛出SessionPausedException。
displayRotationHelper.onPause();
surfaceView.onPause();
session.pause();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) {
if (!CameraPermissionHelper.hasCameraPermission(this)) {
Toast.makeText(this, "Camera permission is needed to run this application", Toast.LENGTH_LONG)
.show();
if (!CameraPermissionHelper.shouldShowRequestPermissionRationale(this)) {
// Permission denied with checking "Do not ask again".
CameraPermissionHelper.launchPermissionSettings(this);
}
finish();
}
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
FullScreenHelper.setFullScreenOnWindowFocusChanged(this, hasFocus);
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
GLES20.glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
// 准备渲染对象(rendering objects). 这里会涉及到读取着色器(shaders),所以可能会抛出 IOException.
try {
// 创建纹理(texture)并将其传递给ARCore的session 以在update()期间来进行填充。
backgroundRenderer.createOnGlThread(/*context=*/ this);
planeRenderer.createOnGlThread(/*context=*/ this, "models/trigrid.png");
pointCloudRenderer.createOnGlThread(/*context=*/ this);
virtualObject.createOnGlThread(/*context=*/ this, "models/andy.obj", "models/andy.png");
virtualObject.setMaterialProperties(0.0f, 2.0f, 0.5f, 6.0f);
virtualObjectShadow.createOnGlThread(
/*context=*/ this, "models/andy_shadow.obj", "models/andy_shadow.png");
virtualObjectShadow.setBlendMode(BlendMode.Shadow);
virtualObjectShadow.setMaterialProperties(1.0f, 0.0f, 0.0f, 1.0f);
} catch (IOException e) {
Log.e(TAG, "Failed to read an asset file", e);
}
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
displayRotationHelper.onSurfaceChanged(width, height);
GLES20.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
// 清除屏幕以通知驱动程序它不应加载前一帧的任何像素。
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
if (session == null) {
return;
}
// 通知ARCore session 视图大小已更改,以便可以正确调整透视矩阵和视频背景。
displayRotationHelper.updateSessionIfNeeded(session);
try {
session.setCameraTextureName(backgroundRenderer.getTextureId());
// 从AR session获取当前帧,可以由此改变AR系统的状态。 当配置设置为UpdateMode.BLOCKING(默认情况下)时,这将限制渲染到相机帧的速率。
Frame frame = session.update();
Camera camera = frame.getCamera();
// 每帧处理一个Tap 。该函数在下面有具体实现。
// 每次获取当前帧时处理一次,与帧速率相比,handleTap频率相对较低。
// 可以理解为其不会对于每一帧都进行获取当前帧操作,因为只有获取当前帧操作后才会进行handleTap 处理。
// 所以处理handleTap 的频率一般是要低于实际帧率的。
handleTap(frame, camera);
// 对该帧绘制AR 的背景
backgroundRenderer.draw(frame);
// 如果相机不是出于跟踪状态下, 不要绘制3D对象。
if (camera.getTrackingState() == TrackingState.PAUSED) {
return;
}
// 获取投影矩阵。
float[] projmtx = new float[16];
camera.getProjectionMatrix(projmtx, 0, 0.1f, 100.0f);
// 获取相机矩阵并绘制。
float[] viewmtx = new float[16];
camera.getViewMatrix(viewmtx, 0);
// 根据图像的平均强度计算光照。
// 前三个分量是颜色缩放因子。最后一个是伽马空间中的平均像素强度。
final float[] colorCorrectionRgba = new float[4];
frame.getLightEstimate().getColorCorrection(colorCorrectionRgba, 0);
// 可视化跟踪点。PointCloud 指 一组观察到的3D点和置信度值。
PointCloud pointCloud = frame.acquirePointCloud();
pointCloudRenderer.update(pointCloud);
pointCloudRenderer.draw(viewmtx, projmtx);
// 应用程序负责在使用后释放PointCloud 资源。
pointCloud.release();
// 检查我们是否检测到至少一个平面。 如果是,请隐藏加载消息。
if (messageSnackbarHelper.isShowing()) {
for (Plane plane : session.getAllTrackables(Plane.class)) {
if (plane.getTrackingState() == TrackingState.TRACKING) {
messageSnackbarHelper.hide(this);
break;
}
}
}
// 可视化平面。
planeRenderer.drawPlanes(
session.getAllTrackables(Plane.class), camera.getDisplayOrientedPose(), projmtx);
// 可视化触摸创建的锚点。
float scaleFactor = 1.0f;
for (ColoredAnchor coloredAnchor : anchors) {
if (coloredAnchor.anchor.getTrackingState() != TrackingState.TRACKING) {
continue;
}
// 在世界空间中获取锚点(Anchor)的当前姿势(可以理解为点的位置和方向)。
// 随着ARCore现实世界的评估,会通过调用session.update()来更新锚点(Anchor)姿势。
coloredAnchor.anchor.getPose().toMatrix(anchorMatrix, 0);
// 更新并绘制模型(在这个程序里就是Android小人)及其阴影。
virtualObject.updateModelMatrix(anchorMatrix, scaleFactor);
virtualObjectShadow.updateModelMatrix(anchorMatrix, scaleFactor);
virtualObject.draw(viewmtx, projmtx, colorCorrectionRgba, coloredAnchor.color);
virtualObjectShadow.draw(viewmtx, projmtx, colorCorrectionRgba, coloredAnchor.color);
}
} catch (Throwable t) {
// Avoid crashing the application due to unhandled exceptions.
Log.e(TAG, "Exception on the OpenGL thread", t);
}
}
// 每次获取当前帧时处理一次,与帧速率相比,handleTap频率相对较低。
// 可以理解为其不会对于每一帧都进行获取当前帧操作,因为只有获取当前帧操作后才会进行handleTap 处理。
// 所以处理handleTap 的频率一般是要低于实际帧率的。
private void handleTap(Frame frame, Camera camera) {
MotionEvent tap = tapHelper.poll();
if (tap != null && camera.getTrackingState() == TrackingState.TRACKING) {
for (HitResult hit : frame.hitTest(tap)) {
// 检查是否有任何平面被击中,以及是否在平面多边形内部被击中
Trackable trackable = hit.getTrackable();
// 如果击中了平面或定向点,则创建锚点。
if ((trackable instanceof Plane
&& ((Plane) trackable).isPoseInPolygon(hit.getHitPose())
&& (PlaneRenderer.calculateDistanceToPlane(hit.getHitPose(), camera.getPose()) > 0))
|| (trackable instanceof Point
&& ((Point) trackable).getOrientationMode()
== OrientationMode.ESTIMATED_SURFACE_NORMAL)) {
// 命中按深度排序。 考虑仅在平面或定向点上最接近的击中。
// 限制创建的对象数量。 这避免了渲染系统和ARCore的重复渲染。
if (anchors.size() >= 20) {
anchors.get(0).anchor.detach();
anchors.remove(0);
}
// 根据此锚点附加到的可跟踪类型,为对象指定颜色以进行渲染。 对于AR_TRACKABLE_POINT,它是蓝色,对于AR_TRACKABLE_PLANE,它是绿色。
float[] objColor;
if (trackable instanceof Point) {
objColor = new float[] {66.0f, 133.0f, 244.0f, 255.0f};
} else if (trackable instanceof Plane) {
objColor = new float[] {139.0f, 195.0f, 74.0f, 255.0f};
} else {
objColor = DEFAULT_COLOR;
}
// 添加锚点告诉ARCore它应该在空间中跟踪这个位置。 在平面上创建此锚点,以将3D模型放置在相对于世界和平面的正确位置。
anchors.add(new ColoredAnchor(hit.createAnchor(), objColor));
break;
}
}
}
}
}
上面的代码流程可以归纳为如下过程:
① 初始化渲染器。
② 检查是否已安装ARCore, 处理是否需要更新ARCore,安装ARCore和其他一些异常。
③ 检查相机相关权限。
④ 如果以上所有内容都通过,调用 SurfaceView 的 resume()。
⑤ 重写GLSurfaceView.Renderer,主要重写如下三个方法onSurfaceCreated,onSurfaceChanged和onDrawFrame。
⑥ 在onSurfaceCreated()中使用着色器和材质属性初始化所有3D模型。
⑦ 在onDrawFrame()方法中设置AR框架,让ARCore识别hitTest框架。
⑧ 如果hit成功命中就来创建平面。
⑨ 让ARCore通过添加锚点来进行跟踪这些特征点。
⑩ 创建PointCloud ,并通过用户触摸屏幕中的PointCloud 来将渲染对象放在平面上。
从上面可以看到,通过OpenGL来使用ARCore会非常麻烦,这些还只是核心代码。一些渲染3D的代码还不在此范围内。对于开发者非常不友好,尤其是OpenGL其实本身还有一套语言系统,需要通过字符串的拼接的形式内置进去代码。谷歌官方也意识到了这个问题,就如前面所述,在今年他们推出了Sceneform 框架来简化ARCore。至于Sceneform 的内容会在之后的文章进行讨论。