@zyl06
2017-11-10T19:15:50.000000Z
字数 13395
阅读 2548
Android
AR
ARCore 是 google 官方出的一款 AR SDK,其基本原理为:
ARCore 使用手机摄像头来辨识特征点,并跟踪这些特征点的移动轨迹。结合特征点的移动轨迹和手机的惯性传感器,ARCore 就可以在手机移动时判定它的位置、角度等信息。识别出特征点,就能在特征点的基础上,侦测平面,如地板、桌子等。另外能 ARCore 也支持估测周围的平均光照强度。有了手机自身的位置角度信息和周围的光照强度信息,ARCore 就可以构建周边世界的模型。
运动跟踪
它利用 IMU 传感器和设备的相机来发现空间的特征点,由此确定 Android 设备的位置和方向。此外,使用 VPS,可以让 AR 物体每次看起来似乎都在同一位置。
环境感知
虚拟物体一般都是放置于平坦平面上的,用 ARCore 可以检测物体的水平表面,建立环境认知感,以保证虚拟的对象可以准确放置,然后让您看到放置在这些表面上的 AR 物体。
光线预测
ARCore 根据环境的光强度,使开发人员可以与周围环境相匹配的方式点亮虚拟对象。此外,最近的一个实验发现,虚拟阴影在真实环境光照下的调整功能也是如此,这样就可以使 AR 物体的外观更为逼真。
鉴于支持 ARCore 的 Android 设备太少,为此可通过修改 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 | √ | × | × |
配置 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 文件为模型数据
布局
<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
相机视频流数据显示至纹理
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,设置各个通道的大小,设置渲染监听实现,设置为主动渲染
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 异常
@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);
}
@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);
}
@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);
}
}
OpenGL 相关,和 AR 无关
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);
}
片元显示器
设置纹理几何的顶点坐标
加载顶点显示器
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
varying vec2 v_TexCoord;
void main() {
gl_Position = a_Position;
v_TexCoord = a_TexCoord;
}
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
其他内容显示类似,不再赘述
由上其实可以看到场景的显示等,其实并不是 ARCore 关心的,ARCore 提供给我们的数据或帮我们完成的功能有:
相比其他第三方库,如 EasyAR 收费版本,支持估计周围环境光照强度,slam 算法更加稳定强大,效果更好;相比 Vuforia 支持平面监测,但当前可支持机型还比较少。而非支持机型,通过改 ARR 代码强制支持,效果也不太理想,如 Nexus 6P。