Android OpenGL仿自如APP裸眼3D效果详解

目录
  • 原理简介 & OpenGL 的优势
  • 具体实现
    • 1. 绘制静态图片
    • 2. 让图片动起来
    • 3. 几个反直觉的细节
    • 4. 帕金森综合征?
  • 源码

原理简介 & OpenGL 的优势

裸眼 3D 效果的本质是——将整个图片结构分为 3 层:上层、中层、以及底层。在手机左右上下旋转时,上层和底层的图片呈相反的方向进行移动,中层则不动,在视觉上给人一种 3D 的感觉:

也就是说效果是由以下三张图构成的:

接下来,如何感应手机的旋转状态,并将三层图片进行对应的移动呢?当然是使用设备自身提供各种各样优秀的传感器了,通过传感器不断回调获取设备的旋转状态,对 UI 进行对应地渲染即可。

笔者最终选择了 Android 平台上的 OpenGL API 进行渲染,直接的原因是,无需将社区内已有的实现方案重复照搬。

另一个重要的原因是,GPU 更适合图形、图像的处理,裸眼3D效果中有大量的缩放和位移操作,都可在 java 层通过一个 矩阵 对几何变换进行描述,通过 shader 小程序中交给 GPU 处理 ——因此,理论上 OpenGL 的渲染性能比其它几个方案更好一些。

本文重点是描述 OpenGL 绘制时的思路描述,因此下文仅展示部分核心代码。

具体实现

1. 绘制静态图片

首先需要将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 参数对应不同的几何变换。

现在我们完成了图像静态的绘制,效果如下:

接下来我们需要接入传感器,并定义不同层级图片各自的几何变换,让图片动起来。

2. 让图片动起来

首先我们需要对 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. 几个反直觉的细节

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的效果。

4. 帕金森综合征?

还差一点就大功告成了,最后还需要处理下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的资料请关注我们其它相关文章!

(0)

相关推荐

  • Android开发 OpenGL ES绘制3D 图形实例详解

    OpenGL ES是 OpenGL三维图形API 的子集,针对手机.PDA和游戏主机等嵌入式设备而设计. Ophone目前支持OpenGL ES 1.0 ,OpenGL ES 1.0 是以 OpenGL 1.3 规范为基础的,OpenGL ES 1.1 是以 OpenGL 1.5 规范为基础的.本文主要介绍利用OpenGL ES绘制图形方面的基本步骤. 本文内容由三部分构成.首先通过EGL获得OpenGL ES的编程接口;其次介绍构建3D程序的基本概念;最后是一个应用程序示例. OpenGL E

  • Android实现3D翻转动画效果

    Android中并没有提供直接做3D翻转的动画,所以关于3D翻转的动画效果需要我们自己实现,那么我们首先来分析一下Animation 和 Transformation. Animation动画的主要接口,其中主要定义了动画的一些属性比如开始时间,持续时间,是否重复播放等等.而Transformation中则包含一个矩阵和alpha值,矩阵是用来做平移,旋转和缩放动画的,而alpha值是用来做alpha动画的,要实现3D旋转动画我们需要继承自Animation类来实现,我们需要重载getTrans

  • 详解Android 裸眼3D效果View控件

    描述:这是一个裸眼3D效果的控件View. Tips:本项目代码部分逻辑参考于其他文章(自如的3D裸眼实现),众人拾柴火焰高,希望大家能多多补充. 项目代码:https://gitee.com/jiugeishere/uidesign 控件效果如下: 实现功能: 实现三层图片叠加效果(裸眼3D效果) 可设置每层图片移动速率 可设置每层图片移动的限制度数 可直接设置图片或引入图片 设计核心: 主要的设计核心是依赖于传感器对手机晃动的监听(重力感应监听器),对每层图片进行不同的移动,实现仿3D效果.

  • Android编程实现3D立体旋转效果的实例代码

    说明:之前在网上到处搜寻类似的旋转效果 但搜到的结果都不是十分满意 原因不多追述(如果有人找到过相关 比较好的效果 可以发一下连接 一起共同进步) 一 效果展示 : 如非您所需要的效果 也希望能给些微帮助 具体操作以及实现 效果 请看项目例子 二 使用方式 此空间继承与FrameLayout 子空间直接添加如同framelayout 相同 如要如图效果 唯一要求子空间必须位于父控件中心且宽高等大小 为了方便扩展而做 如有其他需求可自行更改 (注 所有子控件 最好添加上背景 由于绘制机制和动画原因

  • Android酷炫动画效果之3D星体旋转效果

    在Android中,如果想要实现3D动画效果一般有两种选择:一是使用Open GL ES,二是使用Camera.Open GL ES使用起来太过复杂,一般是用于比较高级的3D特效或游戏,并且这个也不是开源的,像比较简单的一些3D效果,使用Camera就足够了. 一些熟知的Android 3D动画如对某个View进行旋转或翻转的 Rotate3dAnimation类,还有使用Gallery( Gallery目前已过时,现在都推荐使用 HorizontalScrollView或 RecyclerVi

  • Android OpenGL仿自如APP裸眼3D效果详解

    目录 原理简介 & OpenGL 的优势 具体实现 1. 绘制静态图片 2. 让图片动起来 3. 几个反直觉的细节 4. 帕金森综合征? 源码 原理简介 & OpenGL 的优势 裸眼 3D 效果的本质是——将整个图片结构分为 3 层:上层.中层.以及底层.在手机左右上下旋转时,上层和底层的图片呈相反的方向进行移动,中层则不动,在视觉上给人一种 3D 的感觉: 也就是说效果是由以下三张图构成的: 接下来,如何感应手机的旋转状态,并将三层图片进行对应的移动呢?当然是使用设备自身提供各种各样优

  • Android通过自定义view实现刮刮乐效果详解

    前言 已经有两个月没有更新博客了,其实这篇文章我早在两个月前就写好了,一直保存在草稿箱里没有发布出来.原因是有一些原理性的东西还没了解清楚,最近抽时间研究了一下混合模式,终于也理解了刮刮乐是怎么实现的,所以想继续分享一下自己的一些心得,先上效果图. 效果图: 实现原理 其实刮刮乐实现原理也不算很复杂,最关键的还是需要了解Paint的混合模式.因为刮刮乐是由两个bitmap组成的,一个是源图另一个是目标图,我们需要把目标图的颜色改成灰色,在源图上面盖上了一张灰色的目标图.当手指滑动屏幕时paint

  • Android高仿微信表情输入与键盘输入详解

    最近公司在项目上要使用到表情与键盘的切换输入,自己实现了一个,还是存在些缺陷,比如说键盘与表情切换时出现跳闪问题,这个相当困扰我,不过所幸在Github(其中一个不错的开源项目,其代码整体结构很不错)并且在论坛上找些解决方案,再加上我也是研究了好多个开源项目的代码,最后才苦逼地整合出比较不错的实现效果,可以说跟微信基本一样(嘿嘿,只能说目前还没发现大Bug,若发现大家一起日后慢慢完善,这里我也只是给出了实现方案,拓展其他表情我并没有实现哈,不过代码中我实现了一个可拓展的fragment模板以便大

  • Android UI仿QQ好友列表分组悬浮效果

    本文实例为大家分享了Android UI仿QQ好友列表分组悬浮效果的具体代码,供大家参考,具体内容如下 楼主是在平板上測试的.图片略微有点大,大家看看效果就好 接下来贴源代码: PinnedHeaderExpandableListView.java 要注意的是 在 onGroupClick方法中parent.setSelectedGroup(groupPosition)这句代码的作用是点击分组置顶, 我这边不须要这个效果.QQ也没实用到,所以给凝视了.大家假设须要能够解开凝视 package c

  • Android实现仿微软系统加载动画效果

    效果图: 实现步骤: 初始化五个圆球分别设置中心点,方便画圆 利用ValueAnimator的值变化来获取旋转角度 onDraw来分别画每个圆 具体代码实现: 1.创建Circle对象 package com.sjl.keeplive.track; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PointF; public class Circle { private

  • Android自定义View实现APP启动页倒计时效果

    Android自定义View实现APP启动页倒计时效果,供大家参考,具体内容如下 之前也是做过APP启动页的倒计时效果,但是只有文字变化,没有动画效果,这次通过使用自定义View控件来制作一个带有动画效果的倒计时. 倒计时效果的基本思路如下: Canvas提供了绘制弧形的方法,drawArc(),使用这个方法通过定时刷新计算当前弧形的角度,就可以模拟出倒计时的动画效果,同时借助drawText()方法可以实现倒计时文字.(1)继承View(2)使用canvas的drawArc()来绘制弧形,模拟

  • Android屏幕锁屏弹窗的正确姿势DEMO详解

    在上篇文章给大家介绍了Android程序开发仿新版QQ锁屏下弹窗功能.今天通过本文给大家分享android锁屏弹窗的正确姿势. 最近在做一个关于屏幕锁屏悬浮窗的功能,于是在网上搜索了很多安卓屏幕锁屏的相关资料,鉴于网上的资料比较零碎,所以我在这里进行整理总结.本文将从以下两点对屏幕锁屏进行解析: 1. 如何监听系统屏幕锁屏 2. 如何在锁屏界面弹出悬浮窗 如何监听系统屏幕锁屏 经过总结,监听系统的锁屏可以通过以下两种方式: 1) 代码直接判定 2) 接收广播 1) 代码直接判定 代码判断方式,也

  • Android 实现夜间模式的快速简单方法实例详解

    ChangeMode 项目地址:ChangeMode Implementation of night mode for Android. 用最简单的方式实现夜间模式,支持ListView.RecyclerView. Preview Usage xml android:background="?attr/zzbackground" app:backgroundAttr="zzbackground"//如果当前页面要立即刷新,这里传入属性名称 比如 R.attr.zzb

  • Android ksoap调用webservice批量上传多张图片详解

    Android ksoap调用webservice批量上传多张图片详解 这几天一直在开发app,哎呀,什么都是第一接触,想想自己自学Java,然后自学Android,一直没有放弃,曾想放弃的,但是想到爸妈供我上学,不能在宿舍里面玩游戏,加入学校实验室,一天没课就来着里学习,当然这里也有志同道合的人,一起努力一起进步!虽然大学这几年都在努力的学习技术,也没有参加什么活动的,更别说找个女伴了!还是老老实实的敲代码,成功给我带来巨大的潜能,新技术总是吸引着我.自己做项目,哎呀!好像说偏题了,言归正传吧

随机推荐