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

2022-02-01 網際網路程式設計師

轉自 | 卻把清梅嗅的博客

原文地址:

https://juejin.cn/post/7035645207278256165

/   概述   /

之前看到自如團隊發布的自如客APP裸眼3D效果的實現 ,非常有趣,不久後,社區內Android的開發者們陸續提供了Flutter、 Android原生 、Android Jetpack Compose等不同的實現版本。

很快我看到了一個好玩的評論:

既然客戶端都捲成這樣了,乾脆破罐破摔,把Android OpenGL的實現版本也補齊,畢竟圖形學或許會遲到,但絕不會缺席。

實現效果如下,這一波屬實參與到社區內裸眼3D的客戶端大滿貫了:

/   原理簡介&OpenGL的優勢   /

裸眼3D原理其它文章都拆解非常清晰了,本著不重複造輪子的原則,這裡引用Nayuta和付十一文章中的部分內容,再次感謝。

裸眼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;
}

定義好了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,將圖片進行放大。顯示窗口是固定的,那麼一開始只能看到圖片的正中位置。(中層可以不用,因為中層本身是不移動的,所以也不必放大)

這裡的處理參考自Nayuta的這篇文章,內部已經將思路闡述的非常清晰,強烈建議讀者進行閱讀。

https://github.com/DylanCaiCoding/ActivityResultLauncher

明白了這一點,我們就能理解,裸眼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.正交投影
    }
}

這段代碼中還有幾點細節需要處理。

幾個反直覺的細節旋轉方向 ≠ 位移方向

首先,設備旋轉方向和圖片的位移方向是相反的,舉例來說,當設備沿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); 

默認旋轉角度 ≠ 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效果抖動的問題:

如圖,由於傳感器過於靈敏,即使平穩的握住設備,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);
    }
};

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

大功告成,最終我們實現了預期的效果:

/   參考   /

最後是本文中提到的相關資料,再次感謝先驅者的付出實踐。

https://juejin.cn/post/6989227733410644005

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

https://juejin.cn/post/6991409083765129229

https://juejin.cn/post/6992169168938205191

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

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

https://github.com/qingmei2/OpenGL-demo

版權申明:內容來源網絡,版權歸原創者所有。除非無法確認,都會標明作者及出處,如有侵權,煩請告知,我們會立即刪除並致歉!

相關焦點

  • 這交互炸了,Android 仿自如APP裸眼 3D 效果 OpenGL 版
    之前自如系列各個版本:自如App裸眼3D效果最近火爆了,各個版本齊了~之前看到 自如團隊 發布的 自如客APP裸眼3D效果的實現 ,非常有趣,不久後,社區內 Android 的開發者們陸續提供了 Flutter、 Android 原生 、Android Jetpack Compose 等不同的實現版本。
  • Android OpenGL仿自如APP裸眼3D效果
    /   作者簡介   /本篇文章轉自卻把清梅嗅的博客,文章主要分享了他用OpenGL模仿自如APP的裸眼3D效果,相信會對大家有所幫助!原文地址:https://juejin.cn/post/7035645207278256165/   概述   /之前看到自如團隊發布的自如客APP裸眼3D效果的實現 ,非常有趣,不久後,社區內Android的開發者們陸續提供了Flutter、 Android原生 、Android Jetpack Compose等不同的實現版本。
  • Android OpenGL 仿自如 APP 裸眼 3D 效果
    作者:卻把清梅嗅連結:https://juejin.cn/post/7035645207278256165概述 之前看到 自如團隊 發布的 自如客APP裸眼3D效果的實現 ,非常有趣,不久後,社區內 Android 的開發者們陸續提供了 Flutter、 Android 原生 、Android Jetpack Compose
  • Android APP Banner ,用這一個就夠了
    但是為了使用方便,這個庫不僅僅只有仿魅族效果的BannerView 來使用,還可以當作普通的BannerView 來使用,還可以當作一個ViewPager 來使用。使用很方便,具體使用方法和API 請看後面的示例。
  • Android仿全歷史——全沉浸時間軸實現
    今天良心發現,來更一個比較有意思的東西,如題——仿全歷史APP的全沉浸時間軸實現。這張圖呢,就是全歷史App中全古古蹟功能的界面圖。顯然,它通過沉浸狀態欄、透明背景、recyclerView的自定義Item等,實現了一個很優秀的界面效果。今天,我要做的就是猜測這效果背後的實現原理,並仿製一個類似的界面正文解構
  • Android之AppBarLayout實現懸停吸附伸縮效果
    前幾天看到這樣一個UI效果,然後自己也仿照實現了下:開眼app個人中心看著挺酷的,也有很多App都用到了這個UI效果,比如開眼App和滬江開心詞場就用到了.所以下面就來簡單實現一下這個UI效果吧.特別說明:三劍客配合使用,可以做出一些很炫酷的UI效果.但是前提必須滿足:AppbarLayout 要作為CoordinatorLayout 的直接子View,而CollapsingToolbarLayout 要作為AppbarLayout 的直接子View ,否則,上面展示的效果將實現不了.
  • 開啟裸眼3D視界 觀3D V5手機全面評測
    但是短短幾年後,康得新將這一效果移植到了手機屏幕上,裸眼3D再也不需要用戶佩戴笨重的眼鏡,可謂在如今功能單一同質化嚴重的手機市場開闢了一條新的道路,今天評測的就是有著裸眼3D功能的智慧型手機「觀3D V5手機(以下簡稱V5)」,型號K3DX-V5G,由中興製造康得新提供裸眼3D解決方案。
  • 用Compose 打造裸眼 3D 效果!
    作者:付十一連結:https://juejin.cn/post/6992169168938205191前段時間自如團隊發了自如客APP裸眼3D效果的文章讓人眼前一亮,還沒看過的,可以看看之前發的文章:Android自如客APP裸眼3D效果的實現之後 Nayuta
  • Android 兩種方式實現類似水波擴散效果
    兩種方式實現類似水波擴散效果,先上圖為敬自定義view實現動畫實現自定義view實現思路分析:通過canvas畫圓,每次改變圓半徑和透明度,當半徑達到一定程度,再次從中心開始繪圓,達到不同層級的效果,通過不斷繪製達到view擴散效果private Paint centerPaint; private int
  • 基於機智雲的Android開源app修改教程
    然後按照圖片提示,將密匙填入到相應位置,具體參考我上一篇博客,點擊下載,將app工程下載到本地然後可以從Android studio裡面打開修改好的機智雲開源app,等待一段時間打開後如下圖所示,下面在這個位置修改app名稱
  • 最新 21 款Android 自定義View及炫酷動畫開源框架,總有一款適合你!
    6.OriSim3D-Androidopengl 實現了各種摺紙效果,模擬了從一張紙摺疊成一條船的整個過程項目地址: https://github.com/RemiKoutcherawy13.BezierDemo仿qq消息氣泡拖拽消失的效果。項目地址:https://github.com/chenupt/BezierDemo
  • Android Flutter 混合開發高仿大廠 App
    https://github.com/persilee/android_ctrip以下博文會分為4個部分概述:項目完成的功能預覽首先,我們還是通過一個視頻來快速預覽下項目完成的功能和運行效果,如下https://www.bilibili.com/video/BV1W54y1B72U
  • 記一次APP的java層算法逆向(六)
    .app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:288) at android.view.View.performClick(View.java:4438) at android.view.View$PerformClick.run(View.java
  • Android10.0(Q) Launcher3 去掉抽屜效果!
    app 自動添加圖標到 Workspace6、替換 Workspace 圖標長按刪除選項為取消上代碼1、Settings 中增加欄位 launcher3.is_full_apppublic static final String LAUNCHER3_IS_FULL_APP = "launcher3.is_full_app";
  • 是時候讓 Android Tools 屬性拯救你了
    如果存在像 TextView 或者 ImageView 這種基礎控制項,你是不是還在通過諸如 android:text="xxx" 和 android:src="@drawable/xxx" 的方式來測試和預覽UI效果?
  • 【Android 原創】用PHP幫你刷關---記一次分析遊戲並寫出掛機軟體的過程
    d=MsgId%3d27%26Sid%3d%26Uid%3d114364%26St%3d%26actionId%3d1007%26MobileType%3d1%26Token%3d461bd5efee****52914e5b73ebcfd504%26DeviceID%3dandroid-bd19f5ede3358bdff8ff0eadc425d524%26RetailID%3d0000%26sign
  • Android OpenGL ES 實現 3D 阿凡達效果
    偶然間,看到技術交流群裡的一位同學在做類似於上圖所示的 3D 效果壁紙,乍一看效果確實挺驚豔的。當時看到素材之後,馬上就萌生了一個想法:利用 OpenGL 做一個能與之媲美的 3D 效果。毫無疑問,這種 3D 效果選擇使用 OpenGL 實現是再合適不過了,當然 Vulkan 也挺香的。通過觀察上圖 3D 壁紙的效果,羅列一下我們可能要用到的技術點:
  • 裸眼3D大揭秘!這些圖片你能看出神奇的3D效果嗎?
    去年11月的時候,深圳的林先生參加了高交會,第一次接觸到了所謂的「裸眼3D」產品。那是一張薄薄的裸視三維智慧膜,貼在手機上,就能將普通的2D手機一秒升級為3D手機,無需佩戴任何輔助設備,裸眼即可觀看3D電影,讓他大受震撼。
  • 【吐血推薦】Android 開源項目列表,趕緊收藏吧!
    : https://github.com/mikepenz/wallsplash-androidandroid client for the awesome unsplash.comFastAccess: https://github.com/k0shk0sh/FastAccess仿三星桌面的浮動工具
  • Python+android+appium App自動化測試環境搭建