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

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

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

原理簡(jiǎn)介 & OpenGL 的優(yōu)勢(shì)

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

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

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

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

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

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

具體實(shí)現(xiàn)

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

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

首先看一下頂點(diǎn)和片元著色器的 shader 代碼,其定義了圖像紋理是如何在GPU中處理渲染的:

// 頂點(diǎn)著色器代碼
// 頂點(diǎn)坐標(biāo)
attribute vec4 av_Position;
// 紋理坐標(biāo)
attribute vec2 af_Position;
uniform mat4 u_Matrix;
varying vec2 v_texPo;

void main() {
    v_texPo = af_Position;
    gl_Position =  u_Matrix * av_Position;
}
// 頂點(diǎn)著色器代碼
// 頂點(diǎn)坐標(biāo)
attribute vec4 av_Position;
// 紋理坐標(biāo)
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)建時(shí),初始化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);
  }
}

接下來是定義視口的大小,因?yàn)槭?code>2D圖像變換,且切圖和手機(jī)屏幕的寬高比基本一致,因此簡(jiǎn)單定義一個(gè)單位矩陣的正交投影即可:

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);
        // 圖像和屏幕寬高比基本一致,簡(jiǎn)化處理,使用一個(gè)單位矩陣
        Matrix.setIdentityM(mProjectionMatrix, 0);
    }
}

最后就是繪制,讀者需要理解,對(duì)于前、中、后三層圖像的渲染,其邏輯是基本一致的,差異僅僅有2點(diǎn):圖像本身不同 以及 圖像的幾何變換不同。

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ù)對(duì)應(yīng)不同圖像,matrix 參數(shù)對(duì)應(yīng)不同的幾何變換。

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

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

2. 讓圖片動(dòng)起來

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

// 2.1 注冊(cè)傳感器
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)角度,進(jìn)行矩陣變換
        updateMatrix(degreeX, degreeY);
    }
};

注意,因?yàn)槲覀冎恍杩刂茍D像的左右和上下移動(dòng),因此,我們只需關(guān)注設(shè)備本身 x 軸和 y 軸的偏轉(zhuǎn)角度:

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

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

也就是說,我們一開始進(jìn)入時(shí),看到的肯定只是圖片的部分區(qū)域。給每一個(gè)圖層設(shè)置 scale,將圖片進(jìn)行放大。顯示窗口是固定的,那么一開始只能看到圖片的正中位置。(中層可以不用,因?yàn)橹袑颖旧硎遣灰苿?dòng)的,所以也不必放大)

明白了這一點(diǎn),我們就能理解,裸眼3D的效果實(shí)際上就是對(duì) 不同層級(jí)的圖像 進(jìn)行縮放和位移的變換,下面是分別獲取幾何變換的代碼:

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),更新各個(gè)層級(jí)的變換矩陣.
     *
     * @param degreeX x軸旋轉(zhuǎn)角度,圖片應(yīng)該上下移動(dòng)
     * @param degreeY y軸旋轉(zhuǎn)角度,圖片應(yīng)該左右移動(dò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.正交投影
    }
}

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

3. 幾個(gè)反直覺的細(xì)節(jié)

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

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

// 設(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 默認(rèn)旋轉(zhuǎn)角度 ≠ 0°

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

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

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

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°)

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

4. 帕金森綜合征?

還差一點(diǎn)就大功告成了,最后還需要處理下3D效果抖動(dòng)的問題:

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

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

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

因此有人建立了 這個(gè)倉(cāng)庫(kù) , 通過對(duì) Android 傳感器追加低通濾波 ,過濾掉小的噪聲信號(hào),達(dá)到較為平穩(wěn)的效果:

private final SensorEventListener mSensorEventListener = new SensorEventListener() {
    @Override
    public void onSensorChanged(SensorEvent event) {
        // 對(duì)傳感器的數(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)角度,進(jìn)行矩陣變換
        updateMatrix(degreeX, degreeY);
    }
};

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

源碼

源碼地址

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

相關(guān)文章

最新評(píng)論