专栏首页字节流动这交互炸了,Android 仿自如APP裸眼 3D 效果 OpenGL 版

这交互炸了,Android 仿自如APP裸眼 3D 效果 OpenGL 版

本文作者

作者:却把清梅嗅

链接:

https://juejin.cn/post/7035645207278256165

本文由作者授权发布。

之前自如系列各个版本:

自如App裸眼3D效果最近火爆了,各个版本齐了~

1概述

之前看到 自如团队 发布的 自如客APP裸眼3D效果的实现 ,非常有趣,不久后,社区内 Android 的开发者们陆续提供了 Flutter、 Android 原生 、Android Jetpack Compose 等不同的实现版本。

自如客APP裸眼3D效果的实现

https://juejin.cn/post/6989227733410644005

Flutter 版本:

https://juejin.cn/post/6991409083765129229

Android 原生

https://juejin.cn/post/6991840263362576421

Android Jetpack Compose

https://juejin.cn/post/6992169168938205191

很快我看到了一个好玩的评论:

既然客户端都卷成这样了,干脆破罐破摔,把 Android OpenGL 的实现版本也补齐,毕竟 图形学或许会迟到,但绝不会缺席 。

实现效果如下(图片来源),这一波属实参与到社区内裸眼3D的 客户端大满贯 了 :

https://juejin.cn/post/6991409083765129229

2原理简介 & OpenGL 的优势

裸眼 3D 原理其它文章都拆解非常清晰了,本着不重复造轮子的原则,这里引用 Nayuta 付十一 文章中的部分内容,再次感谢。 https://juejin.cn/post/6991409083765129229 https://juejin.cn/post/6992169168938205191

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

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

前景
中景(文字是白色的)
背景

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

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

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

本文重点是描述 OpenGL 绘制时的思路描述,因此下文仅展示部分核心代码,对具体实现感兴趣的读者可参考文末的链接。

3具体实现

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,将图片进行放大。显示窗口是固定的,那么一开始只能看到图片的正中位置。(中层可以不用,因为中层本身是不移动的,所以也不必放大)

这里的处理参考自 Nayuta 的 这篇文章,内部已经将思路阐述的非常清晰,强烈建议读者进行阅读。 https://juejin.cn/post/6991409083765129229#heading-4

明白了这一点,我们就能理解,裸眼 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 传感器追加低通滤波 ,过滤掉小的噪声信号,达到较为平稳的效果:

https://github.com/Bhide/Low-Pass-Filter-To-Android-Sensors

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);
    }
};

大功告成,最终我们实现了预期的效果:

源码地址

本文所有源码,请查看 这里

https://github.com/qingmei2/OpenGL-demo/blob/master/app/src/main/java/com/github/qingmei2/opengl_demo/c_image_process/processor/C06Image3DProcessor.java

参考

最后是本文中提到的相关资料,再次感谢先驱者的付出实践。

自如客APP裸眼3D效果的实现 @自如大前端团队

https://juejin.cn/post/6989227733410644005

拿去吧你!Flutter 仿自如 App 裸眼 3D 效果 @Nayuta

https://juejin.cn/post/6991409083765129229

Compose版来啦!仿自如裸眼3D效果 @付十一

https://juejin.cn/post/6992169168938205191

GitHub: Low-Pass-Filter-To-Android-Sensors

https://github.com/Bhide/Low-Pass-Filter-To-Android-Sensors

文章分享自微信公众号:
字节流动

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!

作者:却把清梅嗅
原始发表时间:2021-12-17
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • Android仿自如客APP裸眼3D效果

    前两天,偶然看到自如大前端开源了一个裸眼3D的Banner轮播图实现方案,觉得非常有意思,于是也打算研究一下。

    xiangzhihong
  • 裸眼 3D 是什么效果?

    作者:沙因,腾讯 IEG 前端开发工程师 介绍一种裸眼 3D 的实现方式,代码以 web 端为例。 平常我们都是戴着 3D 眼镜才能感受 3D 效果,那裸眼能直...

    腾讯技术工程官方号
  • 理论 | VR大潮来袭 ---前端开发能做些什么

    去年谷歌和火狐针对WebVR提出了WebVR API的标准,顾名思义,WebVR即web + VR的体验方式,我们可以戴着头显享受沉浸式的网页,新的API标准让...

    用户1097444
  • Android 10.0正在来的路上!

    目前,美国 Google公司的 AndroidP (安卓9.0),已经正式全面推出有几个多月了。众多手机品牌厂商也都在积极的进行更新适配 Android 9.0...

    刘盼
  • 【Unity 实用工具】✨| Unity 十款 浏览器相关插件 整理(web view / browser)

    有的是内嵌形式的,就是在Unity中显示浏览器的相关内容,有的则是会调用电脑本身的浏览器

    呆呆敲代码的小Y
  • 【元宇宙】iOS16将支持WebXR!一起来撸个WebVR华容道吧

    6月7日凌晨,苹果举行了2022年的WWDC全球开发者大会,在iOS16-Beta开发者预览版中,Safari已支持WebXR标准api。早在2018年,Chr...

    CS逍遥剑仙
  • 1.19 VR扫描:重磅:云视频会议服务商Zoom获1亿美元融资发力VR/AR

    VRPinea
  • 操控悬浮粒子,空中三维成像,能听能摸!Nature和Science报道,裸眼3D新可能

    在 1977 年上映的科幻经典《星球大战》中,莱娅公主向卢克天行者和欧比旺发出了三维版求救影像。

    大数据文摘
  • webgl图库研究(包括BabylonJS、Threejs、LayaboxJS、SceneJS、ThingJS等框架的特性、适用范围、支持格式、优缺点、相关网址)

    为实现企业80%以上的生产数据进行智能转化,在烟草、造纸、能源、电力、机床、化肥等行业,赢得领袖企业青睐,助力企业构建AI赋能中心,实现智能化转型升级。“远...

    acoolgiser
  • 响铃:人人争抢的观影和游戏,智能视频眼镜真能撬开大门?

    近日一则纳德光学发布新品GOOVIS-G1的消息进入响铃视野,按照官方的说法,这“将定义高品质观影新方式”。这不禁让人联想起如日中天的虚拟现实(VR),...

    曾响铃
  • 冬奥闭幕式黑科技再次引爆全网,AR中国结、折柳寄情……还有212项科技藏在冬奥里

    明敏 发自 凹非寺 量子位 | 公众号 QbitAI 冬奥会开幕式用一众黑科技炸翻全网,闭幕式当然也不甘示弱。 用AR (增强现实)合成的万千红丝带,在空中翻飞...

    量子位
  • 为什么说AR也救不了纸媒?从过往变革证明给你看

    近日,“传统媒体和新媒体之争”被再一次炒热。新媒体有人表示:“报纸除了倒闭没有别的出路、新旧媒体不可能融合、纸媒转型做独立APP的时机已经过去。”网上的争论更多...

    新智元
  • 神秘七年、融资23亿美元,Magic Leap终于发售首款产品,被吐槽full of shit

    三年前,Magic Leap是最神秘也是最火的高科技公司。通过多段演示视频,这家公司的产品被认为可以实现裸眼3D全息特效。

    量子位
  • 2021-2022 设计趋势ISUX报告·数字未来篇

    背景 回顾互联网发展历程,从桌面端拨号上网到高速5G的移动互联网,随时随地互联互通对现实生活的影响力也逐步提升,虚拟与现实的距离也逐渐缩小。未来数字世界在沉浸感...

    腾讯ISUX
  • WebGL简易教程(五):图形变换(模型、视图、投影变换)

    通过之前的教程,对WebGL中可编程渲染管线的流程有了一定的认识。但是只有前面的知识还不足以绘制真正的三维场景,可以发现之前我们绘制的点、三角形的坐标都是[-1...

    charlee44
  • Android开发丰富资源集锦

    程序员飞飞

扫码关注腾讯云开发者

领取腾讯云代金券