欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

 更新時間:2022年01月23日 08:52:00   作者:卻把清梅嗅  
之前自如團隊發(fā)布了一個自如客APP裸眼3D效果的實現(xiàn)。本文將通過Android OpenGL實現(xiàn)這一裸眼3D效果,文中的示例代碼講解詳細,感興趣的可以學習一下

原理簡介 & OpenGL 的優(yōu)勢

裸眼 3D 效果的本質(zhì)是——將整個圖片結(jié)構(gòu)分為 3 層:上層、中層、以及底層。在手機左右上下旋轉(zhuǎn)時,上層和底層的圖片呈相反的方向進行移動,中層則不動,在視覺上給人一種 3D 的感覺:

也就是說效果是由以下三張圖構(gòu)成的:

接下來,如何感應(yīng)手機的旋轉(zhuǎn)狀態(tài),并將三層圖片進行對應(yīng)的移動呢?當然是使用設(shè)備自身提供各種各樣優(yōu)秀的傳感器了,通過傳感器不斷回調(diào)獲取設(shè)備的旋轉(zhuǎn)狀態(tài),對 UI 進行對應(yīng)地渲染即可。

筆者最終選擇了 Android 平臺上的 OpenGL API 進行渲染,直接的原因是,無需將社區(qū)內(nèi)已有的實現(xiàn)方案重復照搬。

另一個重要的原因是,GPU 更適合圖形、圖像的處理,裸眼3D效果中有大量的縮放和位移操作,都可在 java 層通過一個 矩陣 對幾何變換進行描述,通過 shader 小程序中交給 GPU 處理 ——因此,理論上 OpenGL 的渲染性能比其它幾個方案更好一些。

本文重點是描述 OpenGL 繪制時的思路描述,因此下文僅展示部分核心代碼。

具體實現(xiàn)

1. 繪制靜態(tài)圖片

首先需要將3張圖片依次進行靜態(tài)繪制,這里涉及大量 OpenGL API 的使用,不熟悉的讀可略讀本小節(jié),以捋清思路為主。

首先看一下頂點和片元著色器的 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 中的畫布) 創(chuàng)建時,初始化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) {
        // 設(shè)置視口大小,這里設(shè)置全屏
        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.執(zhí)行繪制
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
    }
}

參考 drawLayerInner 的代碼,其用于繪制單層的圖像,其中 textureId 參數(shù)對應(yīng)不同圖像,matrix 參數(shù)對應(yīng)不同的幾何變換。

現(xiàn)在我們完成了圖像靜態(tài)的繪制,效果如下:

接下來我們需要接入傳感器,并定義不同層級圖片各自的幾何變換,讓圖片動起來。

2. 讓圖片動起來

首先我們需要對 Android 平臺上的傳感器進行注冊,監(jiān)聽手機的旋轉(zhuǎn)狀態(tài),并拿到手機 xy 軸的旋轉(zhuǎn)角度。

// 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 不斷接受旋轉(zhuǎn)狀態(tài)
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軸的偏轉(zhuǎn)角度
        float degreeX = (float) Math.toDegrees(values[1]);
        // y軸的偏轉(zhuǎn)角度
        float degreeY = (float) Math.toDegrees(values[2]);
        // z軸的偏轉(zhuǎn)角度
        float degreeZ = (float) Math.toDegrees(values[0]);
        
        // 拿到 xy 軸的旋轉(zhuǎn)角度,進行矩陣變換
        updateMatrix(degreeX, degreeY);
    }
};

注意,因為我們只需控制圖像的左右和上下移動,因此,我們只需關(guān)注設(shè)備本身 x 軸和 y 軸的偏轉(zhuǎn)角度:

拿到了 x 軸和 y 軸的偏轉(zhuǎn)角度后,接下來開始定義圖像的位移了。

但如果將圖片直接進行位移操作,將會因為位移后圖像的另一側(cè)沒有紋理數(shù)據(jù),導致渲染結(jié)果有黑邊現(xiàn)象,為了避免這個問題,我們需要將圖像默認從中心點進行放大,保證圖像移動的過程中,不會超出自身的邊界。

也就是說,我們一開始進入時,看到的肯定只是圖片的部分區(qū)域。給每一個圖層設(shè)置 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];

    /**
     * 陀螺儀數(shù)據(jù)回調(diào),更新各個層級的變換矩陣.
     *
     * @param degreeX x軸旋轉(zhuǎn)角度,圖片應(yīng)該上下移動
     * @param degreeY y軸旋轉(zhuǎn)角度,圖片應(yīng)該左右移動
     */
    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.正交投影
    }
}

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

3. 幾個反直覺的細節(jié)

3.1 旋轉(zhuǎn)方向 ≠ 位移方向

首先,設(shè)備旋轉(zhuǎn)方向和圖片的位移方向是相反的,舉例來說,當設(shè)備沿 X 軸旋轉(zhuǎn),對于用戶而言,對應(yīng)前后景的圖片應(yīng)該上下移動,反過來,設(shè)備沿 Y 軸旋轉(zhuǎn),圖片應(yīng)該左右移動(沒太明白的同學可參考上文中陀螺儀的圖片加深理解):

// 設(shè)備旋轉(zhuǎn)方向和圖片的位移方向是相反的
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 默認旋轉(zhuǎn)角度 ≠ 0°

其次,在定義最大旋轉(zhuǎn)角度的時候,不能主觀認為旋轉(zhuǎn)角度 = 0°是默認值。什么意思呢?Y 軸旋轉(zhuǎn)角度為0°,即 degreeY = 0 時,默認設(shè)備左右的高度差是 0,這個符合用戶的使用習慣,相對易于理解,因此,我們可以定義左右的最大旋轉(zhuǎn)角度,比如 Y ∈ (-45°,45°),超過這兩個旋轉(zhuǎn)角度,圖片也就移動到邊緣了。

但當 X 軸旋轉(zhuǎn)角度為0°,即 degreeX = 0 時,意味著設(shè)備上下的高度差是 0,你可以理解為設(shè)備是放在水平的桌面上的,這個絕不符合大多數(shù)用戶的使用習慣,相比之下,設(shè)備屏幕平行于人的面部 才更適用大多數(shù)場景(degreeX = -90):

因此,代碼上需對 X、Y 軸的最大旋轉(zhuǎn)角度區(qū)間進行分開定義:

private static final float USER_X_AXIS_STANDARD = -45f;
private static final float MAX_TRANS_DEGREE_X = 25f;   // X軸最大旋轉(zhuǎn)角度 ∈ (-20°,-70°)

private static final float USER_Y_AXIS_STANDARD = 0f;
private static final float MAX_TRANS_DEGREE_Y = 45f;   // Y軸最大旋轉(zhuǎn)角度 ∈ (-45°,45°)

解決了這些 反直覺 的細節(jié)問題,我們基本完成了裸眼3D的效果。

4. 帕金森綜合征?

還差一點就大功告成了,最后還需要處理下3D效果抖動的問題:

如圖,由于傳感器過于靈敏,即使平穩(wěn)的握住設(shè)備,XYZ 三個方向上微弱的變化都會影響到用戶的實際體驗,會給用戶帶來 帕金森綜合征 的自我懷疑。

解決這個問題,傳統(tǒng)的 OpenGL 以及 Android API 似乎都無能為力,好在 GitHub 上有人提供了另外一個思路。

熟悉信號處理的同學比較了解,為了通過剔除短期波動、保留長期發(fā)展趨勢提供了信號的平滑形式,可以使用 低通濾波器,保證低于截止頻率的信號可以通過,高于截止頻率的信號不能通過。

因此有人建立了 這個倉庫 , 通過對 Android 傳感器追加低通濾波 ,過濾掉小的噪聲信號,達到較為平穩(wěn)的效果:

private final SensorEventListener mSensorEventListener = new SensorEventListener() {
    @Override
    public void onSensorChanged(SensorEvent event) {
        // 對傳感器的數(shù)據(jù)追加低通濾波
        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軸的偏轉(zhuǎn)角度
        float degreeX = (float) Math.toDegrees(values[1]);
        // y軸的偏轉(zhuǎn)角度
        float degreeY = (float) Math.toDegrees(values[2]);
        // z軸的偏轉(zhuǎn)角度
        float degreeZ = (float) Math.toDegrees(values[0]);
        
        // 拿到 xy 軸的旋轉(zhuǎn)角度,進行矩陣變換
        updateMatrix(degreeX, degreeY);
    }
};

大功告成,最終我們實現(xiàn)了預期的效果:

源碼

源碼地址

以上就是Android OpenGL仿自如APP裸眼3D效果詳解的詳細內(nèi)容,更多關(guān)于Android OpenGL裸眼3D的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論