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

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

接下來(lái),如何感應(yīng)手機(jī)的旋轉(zhuǎn)狀態(tài),并將三層圖片進(jìn)行對(duì)應(yīng)的移動(dòng)呢?當(dāng)然是使用設(shè)備自身提供各種各樣優(yōu)秀的傳感器了,通過(guò)傳感器不斷回調(diào)獲取設(shè)備的旋轉(zhuǎn)狀態(tài),對(duì) UI 進(jìn)行對(duì)應(yīng)地渲染即可。
筆者最終選擇了 Android 平臺(tái)上的 OpenGL API 進(jìn)行渲染,直接的原因是,無(wú)需將社區(qū)內(nèi)已有的實(shí)現(xiàn)方案重復(fù)照搬。
另一個(gè)重要的原因是,GPU 更適合圖形、圖像的處理,裸眼3D效果中有大量的縮放和位移操作,都可在 java 層通過(guò)一個(gè) 矩陣 對(duì)幾何變換進(jìn)行描述,通過(guò) 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 ,接下來(lái)在 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);
}
}接下來(lái)是定義視口的大小,因?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)的繪制,效果如下:

接下來(lái)我們需要接入傳感器,并定義不同層級(jí)圖片各自的幾何變換,讓圖片動(dòng)起來(lái)。
2. 讓圖片動(dòng)起來(lái)
首先我們需要對(duì) Android 平臺(tái)上的傳感器進(jìn)行注冊(cè),監(jiān)聽(tīng)手機(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)角度后,接下來(lái)開(kāi)始定義圖像的位移了。
但如果將圖片直接進(jìn)行位移操作,將會(huì)因?yàn)槲灰坪髨D像的另一側(cè)沒(méi)有紋理數(shù)據(jù),導(dǎo)致渲染結(jié)果有黑邊現(xiàn)象,為了避免這個(gè)問(wèn)題,我們需要將圖像默認(rèn)從中心點(diǎn)進(jìn)行放大,保證圖像移動(dòng)的過(guò)程中,不會(huì)超出自身的邊界。
也就是說(shuō),我們一開(kāi)始進(jìn)入時(shí),看到的肯定只是圖片的部分區(qū)域。給每一個(gè)圖層設(shè)置 scale,將圖片進(jìn)行放大。顯示窗口是固定的,那么一開(kāi)始只能看到圖片的正中位置。(中層可以不用,因?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è)反直覺(jué)的細(xì)節(jié)
3.1 旋轉(zhuǎn)方向 ≠ 位移方向
首先,設(shè)備旋轉(zhuǎn)方向和圖片的位移方向是相反的,舉例來(lái)說(shuō),當(dāng)設(shè)備沿 X 軸旋轉(zhuǎn),對(duì)于用戶而言,對(duì)應(yīng)前后景的圖片應(yīng)該上下移動(dòng),反過(guò)來(lái),設(shè)備沿 Y 軸旋轉(zhuǎn),圖片應(yīng)該左右移動(dòng)(沒(méi)太明白的同學(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°),超過(guò)這兩個(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)行分開(kāi)定義:
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°)
解決了這些 反直覺(jué) 的細(xì)節(jié)問(wèn)題,我們基本完成了裸眼3D的效果。
4. 帕金森綜合征?
還差一點(diǎn)就大功告成了,最后還需要處理下3D效果抖動(dòng)的問(wèn)題:

如圖,由于傳感器過(guò)于靈敏,即使平穩(wěn)的握住設(shè)備,XYZ 三個(gè)方向上微弱的變化都會(huì)影響到用戶的實(shí)際體驗(yàn),會(huì)給用戶帶來(lái) 帕金森綜合征 的自我懷疑。
解決這個(gè)問(wèn)題,傳統(tǒng)的 OpenGL 以及 Android API 似乎都無(wú)能為力,好在 GitHub 上有人提供了另外一個(gè)思路。
熟悉信號(hào)處理的同學(xué)比較了解,為了通過(guò)剔除短期波動(dòng)、保留長(zhǎng)期發(fā)展趨勢(shì)提供了信號(hào)的平滑形式,可以使用 低通濾波器,保證低于截止頻率的信號(hào)可以通過(guò),高于截止頻率的信號(hào)不能通過(guò)。
因此有人建立了 這個(gè)倉(cāng)庫(kù) , 通過(guò)對(duì) Android 傳感器追加低通濾波 ,過(guò)濾掉小的噪聲信號(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)文章
Android 浮動(dòng)編輯框的具體實(shí)現(xiàn)代碼
本篇文章主要介紹了Android 浮動(dòng)編輯框的具體實(shí)現(xiàn)代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10
RecyclerView實(shí)現(xiàn)橫向滾動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了RecyclerView實(shí)現(xiàn)橫向滾動(dòng)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-01-01
Android實(shí)現(xiàn)類似execel的表格 能回顯并能修改表格內(nèi)容的方法
今天小編就為大家分享一篇Android實(shí)現(xiàn)類似execel的表格 能回顯并能修改表格內(nèi)容的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-08-08
Android UI設(shè)計(jì)之AlertDialog彈窗控件
這篇文章主要為大家詳細(xì)介紹了Android UI設(shè)計(jì)之AlertDialog彈窗控件的使用方法,感興趣的小伙伴們可以參考一下2016-08-08
如何利用Flutter實(shí)現(xiàn)酷狗流暢Tabbar效果
這篇文章主要給大家介紹了關(guān)于如何利用Flutter實(shí)現(xiàn)酷狗流暢Tabbar效果的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-02-02
Android訪問(wèn)php取回json數(shù)據(jù)實(shí)例
Android訪問(wèn)php取回json數(shù)據(jù),實(shí)現(xiàn)代碼如下,遇到訪問(wèn)網(wǎng)絡(luò)的權(quán)限不足在AndroidManifest.xml中,需要進(jìn)行如下配置2013-06-06
Android實(shí)現(xiàn)動(dòng)態(tài)高斯模糊效果示例代碼
這篇文章主要介紹了Android快速實(shí)現(xiàn)動(dòng)態(tài)模糊效果示例代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-01-01
Android提高Service優(yōu)先級(jí)的方法分析
這篇文章主要介紹了Android提高Service優(yōu)先級(jí)的方法,簡(jiǎn)單講述了Service優(yōu)先級(jí)的功能,并對(duì)比分析了1.5與1.0設(shè)置Service的技巧,需要的朋友可以參考下2016-06-06

