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