您好,登錄后才能下訂單哦!
Android OpenGL如何實現APP裸眼3D效果,相信很多沒有經驗的人對此束手無策,為此本文總結了問題出現的原因和解決方法,通過這篇文章希望你能解決這個問題。
裸眼 3D 效果的本質是——將整個圖片結構分為 3 層:上層、中層、以及底層。在手機左右上下旋轉時,上層和底層的圖片呈相反的方向進行移動,中層則不動,在視覺上給人一種 3D 的感覺:
也就是說效果是由以下三張圖構成的:
接下來,如何感應手機的旋轉狀態,并將三層圖片進行對應的移動呢?當然是使用設備自身提供各種各樣優秀的傳感器了,通過傳感器不斷回調獲取設備的旋轉狀態,對 UI 進行對應地渲染即可。
筆者最終選擇了 Android 平臺上的 OpenGL API 進行渲染,直接的原因是,無需將社區內已有的實現方案重復照搬。
另一個重要的原因是,GPU 更適合圖形、圖像的處理,裸眼3D效果中有大量的縮放和位移操作,都可在 java 層通過一個 矩陣 對幾何變換進行描述,通過 shader 小程序中交給 GPU 處理 ——因此,理論上 OpenGL 的渲染性能比其它幾個方案更好一些。
本文重點是描述 OpenGL
繪制時的思路描述,因此下文僅展示部分核心代碼。
首先需要將3張圖片依次進行靜態繪制,這里涉及大量 OpenGL API
的使用,不熟悉的讀可略讀本小節,以捋清思路為主。
首先看一下頂點和片元著色器的 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
中的畫布) 創建時,初始化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.執行繪制 GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); } }
參考 drawLayerInner
的代碼,其用于繪制單層的圖像,其中 textureId
參數對應不同圖像,matrix
參數對應不同的幾何變換。
現在我們完成了圖像靜態的繪制,效果如下:
接下來我們需要接入傳感器,并定義不同層級圖片各自的幾何變換,讓圖片動起來。
首先我們需要對 Android 平臺上的傳感器進行注冊,監聽手機的旋轉狀態,并拿到手機 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 不斷接受旋轉狀態 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
軸的偏轉角度后,接下來開始定義圖像的位移了。
但如果將圖片直接進行位移操作,將會因為位移后圖像的另一側沒有紋理數據,導致渲染結果有黑邊現象,為了避免這個問題,我們需要將圖像默認從中心點進行放大,保證圖像移動的過程中,不會超出自身的邊界。
也就是說,我們一開始進入時,看到的肯定只是圖片的部分區域。給每一個圖層設置 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]; /** * 陀螺儀數據回調,更新各個層級的變換矩陣. * * @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.正交投影 } }
這段代碼中還有幾點細節需要處理。
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,你可以理解為設備是放在水平的桌面上的,這個絕不符合大多數用戶的使用習慣,相比之下,設備屏幕平行于人的面部 才更適用大多數場景(degreeX = -90):
因此,代碼上需對 X、Y
軸的最大旋轉角度區間進行分開定義:
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°)
解決了這些 反直覺 的細節問題,我們基本完成了裸眼3D的效果。
還差一點就大功告成了,最后還需要處理下3D
效果抖動的問題:
如圖,由于傳感器過于靈敏,即使平穩的握住設備,XYZ 三個方向上微弱的變化都會影響到用戶的實際體驗,會給用戶帶來 帕金森綜合征 的自我懷疑。
解決這個問題,傳統的 OpenGL 以及 Android API 似乎都無能為力,好在 GitHub 上有人提供了另外一個思路。
熟悉信號處理的同學比較了解,為了通過剔除短期波動、保留長期發展趨勢提供了信號的平滑形式,可以使用 低通濾波器,保證低于截止頻率的信號可以通過,高于截止頻率的信號不能通過。
因此有人建立了 這個倉庫 , 通過對 Android 傳感器追加低通濾波 ,過濾掉小的噪聲信號,達到較為平穩的效果:
private final SensorEventListener mSensorEventListener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { // 對傳感器的數據追加低通濾波 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); } };
大功告成,最終我們實現了預期的效果:
Android是一種基于Linux內核的自由及開放源代碼的操作系統,主要使用于移動設備,如智能手機和平板電腦,由美國Google公司和開放手機聯盟領導及開發。
看完上述內容,你們掌握Android OpenGL如何實現APP裸眼3D效果的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。