目录原理简介 & OpenGL 的优势具体实现1. 绘制静态图片2. 让图片动起来3. 几个反直觉的细节4. 帕金森综合征?源码原理简介 & OpenGL 的优势 裸
裸眼 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];
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 OpenGL仿自如APP裸眼3D效果详解的详细内容,更多关于Android OpenGL裸眼3D的资料请关注编程网其它相关文章!
--结束END--
本文标题: AndroidOpenGL仿自如APP裸眼3D效果详解
本文链接: https://lsjlt.com/news/163348.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-01-21
2023-10-28
2023-10-28
2023-10-27
2023-10-27
2023-10-27
2023-10-27
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0