Android OpenGL如何实现APP裸眼3D效果,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。
裸眼 3D 效果的本质是——将整个图片结构分为 3 层:上层、中层、以及底层。在手机左右上下旋转时,上层和底层的图片呈相反的方向进行移动,中层则不动,在视觉上给人一种 3D 的感觉:
也就是说效果是由以下三张图构成的:
接下来,如何感应手机的旋转状态,并将三层图片进行对应的移动呢?当然是使用设备自身提供各种各样优秀的传感器了,通过传感器不断回调获取设备的旋转状态,对 UI 进行对应地渲染即可。
笔者最终选择了 Android 平台上的 OpenGL API 进行渲染,直接的原因是,无需将社区内已有的实现方案重复照搬。
另一个重要的原因是,GPU 更适合图形、图像的处理,裸眼3D效果中有大量的缩放和位移操作,都可在 java 层通过一个 矩阵 对几何变换进行描述,通过 shader 小程序中交给 GPU 处理 ——因此,理论上 OpenGL 的渲染性能比其它几个方案更好一些。
本文重点是描述 OpenGL
绘制时的思路描述,因此下文仅展示部分核心代码。
首先需要将3张图片依次进行静态绘制,这里涉及大量 OpenGL API
的使用,不熟悉的读可略读本小节,以捋清思路为主。
首先看一下顶点和片元着色器的 shader
代码,其定义了图像纹理是如何在GPU
中处理渲染的:
// 顶点着色器代码 // 顶点坐标 attribute vec4 av_Position; // 纹理坐标 attribute vec2 af_Position; uniform mat4 u_Matrix; varying vec2 v_texPo; void main() { v_texPo = af_Position; gl_Position = u_Matrix * av_Position; }
// 顶点着色器代码 // 顶点坐标 attribute vec4 av_Position; // 纹理坐标 attribute vec2 af_Position; uniform mat4 u_Matrix; varying vec2 v_texPo; void main() { v_texPo = af_Position; gl_Position = u_Matrix * av_Position; }
定义好了 Shader
,接下来在 GLSurfaceView
(可以理解为 OpenGL
中的画布) 创建时,初始化Shader
小程序,并将图像纹理依次加载到GPU
中:
public class My3DRenderer implements GLSurfaceView.Renderer { @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { // 1.加载shader小程序 mProgram = loadShaderWithResource( mContext, R.raw.projection_vertex_shader, R.raw.projection_fragment_shader ); // ... // 2. 依次将3张切图纹理传入GPU this.texImageInner(R.drawable.bg_3d_back, mBackTextureId); this.texImageInner(R.drawable.bg_3d_mid, mMidTextureId); this.texImageInner(R.drawable.bg_3d_fore, mFrontTextureId); } }
接下来是定义视口的大小,因为是2D
图像变换,且切图和手机屏幕的宽高比基本一致,因此简单定义一个单位矩阵的正交投影即可:
public class My3DRenderer implements GLSurfaceView.Renderer { // 投影矩阵 private float[] mProjectionMatrix = new float[16]; @Override public void onSurfaceChanged(GL10 gl, int width, int height) { // 设置视口大小,这里设置全屏 GLES20.glViewport(0, 0, width, height); // 图像和屏幕宽高比基本一致,简化处理,使用一个单位矩阵 Matrix.setIdentityM(mProjectionMatrix, 0); } }
最后就是绘制,读者需要理解,对于前、中、后三层图像的渲染,其逻辑是基本一致的,差异仅仅有2点:图像本身不同 以及 图像的几何变换不同。
public class My3DRenderer implements GLSurfaceView.Renderer { private float[] mBackMatrix = new float[16]; private float[] mMidMatrix = new float[16]; private float[] mFrontMatrix = new float[16]; @Override public void onDrawFrame(GL10 gl) { GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); GLES20.glUseProgram(mProgram); // 依次绘制背景、中景、前景 this.drawLayerInner(mBackTextureId, mTextureBuffer, mBackMatrix); this.drawLayerInner(mMidTextureId, mTextureBuffer, mMidMatrix); this.drawLayerInner(mFrontTextureId, mTextureBuffer, mFrontMatrix); } private void drawLayerInner(int textureId, FloatBuffer textureBuffer, float[] matrix) { // 1.绑定图像纹理 GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); // 2.矩阵变换 GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, matrix, 0); // ... // 3.执行绘制 GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); } }
参考 drawLayerInner
的代码,其用于绘制单层的图像,其中 textureId
参数对应不同图像,matrix
参数对应不同的几何变换。
现在我们完成了图像静态的绘制,效果如下:
接下来我们需要接入传感器,并定义不同层级图片各自的几何变换,让图片动起来。
首先我们需要对 Android 平台上的传感器进行注册,监听手机的旋转状态,并拿到手机 xy 轴的旋转角度。
// 2.1 注册传感器 mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); mAcceleSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); mMagneticSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); mSensorManager.registerListener(mSensorEventListener, mAcceleSensor, SensorManager.SENSOR_DELAY_GAME); mSensorManager.registerListener(mSensorEventListener, mMagneticSensor, SensorManager.SENSOR_DELAY_GAME); // 2.2 不断接受旋转状态 private final SensorEventListener mSensorEventListener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { // ... 省略具体代码 float[] values = new float[3]; float[] R = new float[9]; SensorManager.getRotationMatrix(R, null, mAcceleValues, mMageneticValues); SensorManager.getOrientation(R, values); // x轴的偏转角度 float degreeX = (float) Math.toDegrees(values[1]); // y轴的偏转角度 float degreeY = (float) Math.toDegrees(values[2]); // z轴的偏转角度 float degreeZ = (float) Math.toDegrees(values[0]); // 拿到 xy 轴的旋转角度,进行矩阵变换 updateMatrix(degreeX, degreeY); } };
注意,因为我们只需控制图像的左右和上下移动,因此,我们只需关注设备本身 x
轴和 y
轴的偏转角度:
拿到了 x
轴和 y
轴的偏转角度后,接下来开始定义图像的位移了。
但如果将图片直接进行位移操作,将会因为位移后图像的另一侧没有纹理数据,导致渲染结果有黑边现象,为了避免这个问题,我们需要将图像默认从中心点进行放大,保证图像移动的过程中,不会超出自身的边界。
也就是说,我们一开始进入时,看到的肯定只是图片的部分区域。给每一个图层设置 scale
,将图片进行放大。显示窗口是固定的,那么一开始只能看到图片的正中位置。(中层可以不用,因为中层本身是不移动的,所以也不必放大)
明白了这一点,我们就能理解,裸眼3D的效果实际上就是对 不同层级的图像 进行缩放和位移的变换,下面是分别获取几何变换的代码:
public class My3DRenderer implements GLSurfaceView.Renderer { private float[] mBackMatrix = new float[16]; private float[] mMidMatrix = new float[16]; private float[] mFrontMatrix = new float[16]; /** * 陀螺仪数据回调,更新各个层级的变换矩阵. * * @param degreeX x轴旋转角度,图片应该上下移动 * @param degreeY y轴旋转角度,图片应该左右移动 */ private void updateMatrix(@FloatRange(from = -180.0f, to = 180.0f) float degreeX, @FloatRange(from = -180.0f, to = 180.0f) float degreeY) { // ... 其它处理 // 背景变换 // 1.最大位移量 float maxTransXY = MAX_VISIBLE_SIDE_BACKGROUND - 1f; // 2.本次的位移量 float transX = ((maxTransXY) / MAX_TRANS_DEGREE_Y) * -degreeY; float transY = ((maxTransXY) / MAX_TRANS_DEGREE_X) * -degreeX; float[] backMatrix = new float[16]; Matrix.setIdentityM(backMatrix, 0); Matrix.translateM(backMatrix, 0, transX, transY, 0f); // 2.平移 Matrix.scaleM(backMatrix, 0, SCALE_BACK_GROUND, SCALE_BACK_GROUND, 1f); // 1.缩放 Matrix.multiplyMM(mBackMatrix, 0, mProjectionMatrix, 0, backMatrix, 0); // 3.正交投影 // 中景变换 Matrix.setIdentityM(mMidMatrix, 0); // 前景变换 // 1.最大位移量 maxTransXY = MAX_VISIBLE_SIDE_FOREGROUND - 1f; // 2.本次的位移量 transX = ((maxTransXY) / MAX_TRANS_DEGREE_Y) * -degreeY; transY = ((maxTransXY) / MAX_TRANS_DEGREE_X) * -degreeX; float[] frontMatrix = new float[16]; Matrix.setIdentityM(frontMatrix, 0); Matrix.translateM(frontMatrix, 0, -transX, -transY - 0.10f, 0f); // 2.平移 Matrix.scaleM(frontMatrix, 0, SCALE_FORE_GROUND, SCALE_FORE_GROUND, 1f); // 1.缩放 Matrix.multiplyMM(mFrontMatrix, 0, mProjectionMatrix, 0, frontMatrix, 0); // 3.正交投影 } }
这段代码中还有几点细节需要处理。
3.1 旋转方向 ≠ 位移方向
首先,设备旋转方向和图片的位移方向是相反的,举例来说,当设备沿 X 轴旋转,对于用户而言,对应前后景的图片应该上下移动,反过来,设备沿 Y 轴旋转,图片应该左右移动(没太明白的同学可参考上文中陀螺仪的图片加深理解):
// 设备旋转方向和图片的位移方向是相反的 float transX = ((maxTransXY) / MAX_TRANS_DEGREE_Y) * -degreeY; float transY = ((maxTransXY) / MAX_TRANS_DEGREE_X) * -degreeX; // ... Matrix.translateM(backMatrix, 0, transX, transY, 0f);
3.2 默认旋转角度 ≠ 0°
其次,在定义最大旋转角度的时候,不能主观认为旋转角度 = 0°是默认值。什么意思呢?Y 轴旋转角度为0°,即 degreeY = 0 时,默认设备左右的高度差是 0,这个符合用户的使用习惯,相对易于理解,因此,我们可以定义左右的最大旋转角度,比如 Y ∈ (-45°,45°),超过这两个旋转角度,图片也就移动到边缘了。
但当 X 轴旋转角度为0°,即 degreeX = 0 时,意味着设备上下的高度差是 0,你可以理解为设备是放在水平的桌面上的,这个绝不符合大多数用户的使用习惯,相比之下,设备屏幕平行于人的面部 才更适用大多数场景(degreeX = -90):
因此,代码上需对 X、Y
轴的最大旋转角度区间进行分开定义:
private static final float USER_X_AXIS_STANDARD = -45f; private static final float MAX_TRANS_DEGREE_X = 25f; // X轴最大旋转角度 ∈ (-20°,-70°) private static final float USER_Y_AXIS_STANDARD = 0f; private static final float MAX_TRANS_DEGREE_Y = 45f; // Y轴最大旋转角度 ∈ (-45°,45°)
解决了这些 反直觉 的细节问题,我们基本完成了裸眼3D的效果。
还差一点就大功告成了,最后还需要处理下3D
效果抖动的问题:
如图,由于传感器过于灵敏,即使平稳的握住设备,XYZ 三个方向上微弱的变化都会影响到用户的实际体验,会给用户带来 帕金森综合征 的自我怀疑。
解决这个问题,传统的 OpenGL 以及 Android API 似乎都无能为力,好在 GitHub 上有人提供了另外一个思路。
熟悉信号处理的同学比较了解,为了通过剔除短期波动、保留长期发展趋势提供了信号的平滑形式,可以使用 低通滤波器,保证低于截止频率的信号可以通过,高于截止频率的信号不能通过。
因此有人建立了 这个仓库 , 通过对 Android 传感器追加低通滤波 ,过滤掉小的噪声信号,达到较为平稳的效果:
private final SensorEventListener mSensorEventListener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { // 对传感器的数据追加低通滤波 if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { mAcceleValues = lowPass(event.values.clone(), mAcceleValues); } if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) { mMageneticValues = lowPass(event.values.clone(), mMageneticValues); } // ... 省略具体代码 // x轴的偏转角度 float degreeX = (float) Math.toDegrees(values[1]); // y轴的偏转角度 float degreeY = (float) Math.toDegrees(values[2]); // z轴的偏转角度 float degreeZ = (float) Math.toDegrees(values[0]); // 拿到 xy 轴的旋转角度,进行矩阵变换 updateMatrix(degreeX, degreeY); } };
大功告成,最终我们实现了预期的效果:
Android是一种基于Linux内核的自由及开放源代码的操作系统,主要使用于移动设备,如智能手机和平板电脑,由美国Google公司和开放手机联盟领导及开发。
看完上述内容,你们掌握Android OpenGL如何实现APP裸眼3D效果的方法了吗?如果还想学到更多技能或想了解更多相关内容,欢迎关注亿速云行业资讯频道,感谢各位的阅读!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。