Android OpenGL仿自如APP裸眼3D效果詳解
原理簡介 & OpenGL 的優(yōu)勢
裸眼 3D 效果的本質是——將整個圖片結構分為 3 層:上層、中層、以及底層。在手機左右上下旋轉時,上層和底層的圖片呈相反的方向進行移動,中層則不動,在視覺上給人一種 3D 的感覺:

也就是說效果是由以下三張圖構成的:

接下來,如何感應手機的旋轉狀態(tài),并將三層圖片進行對應的移動呢?當然是使用設備自身提供各種各樣優(yōu)秀的傳感器了,通過傳感器不斷回調獲取設備的旋轉狀態(tài),對 UI 進行對應地渲染即可。
筆者最終選擇了 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) {
// 設置視口大小,這里設置全屏
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ù)對應不同圖像,matrix 參數(shù)對應不同的幾何變換。
現(xiàn)在我們完成了圖像靜態(tài)的繪制,效果如下:

接下來我們需要接入傳感器,并定義不同層級圖片各自的幾何變換,讓圖片動起來。
2. 讓圖片動起來
首先我們需要對 Android 平臺上的傳感器進行注冊,監(jiān)聽手機的旋轉狀態(tài),并拿到手機 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 不斷接受旋轉狀態(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軸的偏轉角度
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 軸的偏轉角度后,接下來開始定義圖像的位移了。
但如果將圖片直接進行位移操作,將會因為位移后圖像的另一側沒有紋理數(shù)據(jù),導致渲染結果有黑邊現(xiàn)象,為了避免這個問題,我們需要將圖像默認從中心點進行放大,保證圖像移動的過程中,不會超出自身的邊界。
也就是說,我們一開始進入時,看到的肯定只是圖片的部分區(qū)域。給每一個圖層設置 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ù)回調,更新各個層級的變換矩陣.
*
* @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.正交投影
}
}這段代碼中還有幾點細節(jié)需要處理。
3. 幾個反直覺的細節(jié)
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,你可以理解為設備是放在水平的桌面上的,這個絕不符合大多數(shù)用戶的使用習慣,相比之下,設備屏幕平行于人的面部 才更適用大多數(shù)場景(degreeX = -90):

因此,代碼上需對 X、Y 軸的最大旋轉角度區(qū)間進行分開定義:
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°)
解決了這些 反直覺 的細節(jié)問題,我們基本完成了裸眼3D的效果。
4. 帕金森綜合征?
還差一點就大功告成了,最后還需要處理下3D效果抖動的問題:

如圖,由于傳感器過于靈敏,即使平穩(wěn)的握住設備,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軸的偏轉角度
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);
}
};大功告成,最終我們實現(xiàn)了預期的效果:

源碼
以上就是Android OpenGL仿自如APP裸眼3D效果詳解的詳細內(nèi)容,更多關于Android OpenGL裸眼3D的資料請關注腳本之家其它相關文章!
相關文章
Android App中實現(xiàn)圖片異步加載的實例分享
這篇文章主要介紹了Android App中實現(xiàn)圖片異步加載的實例分享,這樣GridView在加載大量圖片時便可以延時分布顯示,需要的朋友可以參考下2016-04-04
Android開發(fā)自定義雙向SeekBar拖動條控件
這篇文章主要為大家介紹了Android開發(fā)自定義雙向SeekBar拖動條控件使用實例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-06-06
Android自定義ViewGroup(側滑菜單)詳解及簡單實例
這篇文章主要介紹了Android自定義ViewGroup(側滑菜單)詳解及簡單實例的相關資料,需要的朋友可以參考下2017-02-02
Android使用Notification實現(xiàn)普通通知欄(一)
這篇文章主要為大家詳細介紹了Android使用Notification實現(xiàn)普通通知欄,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-12-12
Android studio報: java.lang.ExceptionInInitializerError 錯誤
本篇文章主要介紹了Android studio報: java.lang.ExceptionInInitializerError錯誤的解決方法,具有很好的參考價值。下面跟著小編一起來看下吧2017-03-03
Android Handler,Message,MessageQueue,Loper源碼解析詳解
這篇文章主要介紹了Android Handler,Message,MessageQueue,Loper源碼解析詳解,本篇文章通過簡要的案例,講解了該項技術的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下2021-09-09
導入takephoto庫編譯失敗與glide庫沖突應排除依賴
今天小編就為大家分享一篇關于導入takephoto庫編譯失敗與glide庫沖突應排除依賴的文章,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2018-10-10

