音視頻開發之旅(41)-天空盒

目錄

  1. 天空盒的實現原理
  2. 具體代碼實現
  3. 資料
  4. 收穫

效果如下


今天我們學習實踐天空盒,天空盒的技術本身比較簡單,但是卻可以做出來很多比較天空、大山、大海、以及VR看房等效果。可以作爲背景動態移動,也可以跟隨手勢或者傳感器等進行移動變換。

一、立方體貼圖和天空盒

所謂的天空盒其實就是將一個立方體展開,然後在六個面上貼上相應的貼圖

天空盒的效果正如開篇動畫中展示的效果一樣,從一個視點,旋轉視角看天空,呈現出來不同畫面。我們可以想象成我們自己就位於一個三維空間的內部中心點,四周是一個大的立方體,包含上下、左右、前後 六個平面,我們旋轉我們的視角就會看到不同的畫面。

因此我們可以採用上面的原理,在一個立方體進行立方體貼圖

在實際的渲染中,將這個立方體始終罩在攝像機的周圍,讓攝像機始終處於這個立方體的中心位置,然後根據視線與立方體的交點的座標,來確定究竟要在哪一個面上進行紋理採樣。具體的映射方法爲:設視線與立方體的交點爲(x,y,z)(x,y,z),在x、y、zx、y、z中取絕對值最大的那個分量,根據它的符號來判定在哪個面上採樣。

然後讓其他兩個分量都除以最大分量的絕對值,這樣就讓另外兩個分量都映射到了[0,1]內,然後就可以直接在對應的紋理上做紋理映射就行了,這個方法就是所謂的Cube Map,是天空盒方法的核心

立方體貼圖是和2D紋理創建流程一樣

      GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
        GLES20.glBindTexture(GLES20.GL_TEXTURE_CUBE_MAP, skyBoxTexture)
        GLES20.glUniform1i(uTextureLoc, 0)

立方體紋理貼圖的加載如下

 /**
     * 加載立方體紋理貼圖
     * @param context
     * @param cubeResources
     * @return
     */
    public static int loadCubeMap(Context context, int[] cubeResources) {
        final int[] textureObjectIds = new int[1];
        glGenTextures(1, textureObjectIds, 0);

        if (textureObjectIds[0] == 0) {
            Log.w(TAG, "Could not generate a new OpenGL texture object.");
            return 0;
        }
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inScaled = false;
        final Bitmap[] cubeBitmaps = new Bitmap[6];
        for (int i = 0; i < 6; i++) {
            cubeBitmaps[i] =
                    BitmapFactory.decodeResource(context.getResources(),
                            cubeResources[i], options);

            if (cubeBitmaps[i] == null) {
                Log.w(TAG, "Resource ID " + cubeResources[i]
                        + " could not be decoded.");
                glDeleteTextures(1, textureObjectIds, 0);
                return 0;
            }
        }
        // Linear filtering for minification and magnification
        //注意這裏不是GL_TEXTURE_2D,而是GL_TEXTURE_CUBE_MAP,使用六張紋理組合成一個立方體紋理
        glBindTexture(GL_TEXTURE_CUBE_MAP, textureObjectIds[0]);

        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        //左右、下上、前後---》注意 使用的是左手座標系
        texImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, cubeBitmaps[0], 0);
        texImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, cubeBitmaps[1], 0);

        texImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, cubeBitmaps[2], 0);
        texImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, cubeBitmaps[3], 0);

        texImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, cubeBitmaps[4], 0);
        texImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, cubeBitmaps[5], 0);

        glBindTexture(GL_TEXTURE_CUBE_MAP, 0);

        //把紋理複製到GPU後就可以回收原理的bitmap了
        for (Bitmap bitmap : cubeBitmaps) {
            bitmap.recycle();
        }

        return textureObjectIds[0];
    }

OpenGL給我們提供了6個特殊的紋理目標,專門對應立方體貼圖的一個面。

GL_TEXTURE_CUBE_MAP_POSITIVE_X 右
GL_TEXTURE_CUBE_MAP_NEGATIVE_X 左
GL_TEXTURE_CUBE_MAP_POSITIVE_Y 上
GL_TEXTURE_CUBE_MAP_NEGATIVE_Y 下
GL_TEXTURE_CUBE_MAP_POSITIVE_Z 後
GL_TEXTURE_CUBE_MAP_NEGATIVE_Z 前

另外在着色器上使用立方體紋理

//使用立方體紋理
uniform samplerCube uTexture;
varying vec3 vPosition;

void main() {
    gl_FragColor = textureCube(uTexture,vPosition);
}

二、具體代碼實現

通過上面小節,我們瞭解到天空盒的實現原理比較簡單,下面我們開始具體的代碼實現。

首先,寫着色器代碼

uniform mat4 uMatrix;
attribute vec3 aPosition;
varying vec3 vPosition;

void main() {
    vPosition = aPosition;
    gl_Position = uMatrix*vec4(aPosition, 1.0);
    //注意這裏
    gl_Position = gl_Position.xyww;
}

z = w
在投影變換之後,會做一步透視除法,即讓四元向量的所有分量都除以它的W分量,從而使視錐體內的區域的x、y映射到[−1,1][−1,1],z映射到[0,1][0,1],從而根據透視除法之後的x、y、zx、y、z的範圍直接剔除掉那些不可見的頂點,如果令z=wz=w,就表示透視除法後的z=1z=1,也就是讓天空盒始終處於遠平面的位置

//使用立方體紋理
uniform samplerCube uTexture;
varying vec3 vPosition;

void main() {
    gl_FragColor = textureCube(uTexture,vPosition);
}

接着我們重點來看下Render的實現

package com.av.mediajourney.skybox

import android.content.Context
import android.opengl.GLES20
import android.opengl.GLSurfaceView
import android.opengl.Matrix
import com.av.mediajourney.R
import com.av.mediajourney.opengl.ShaderHelper
import com.av.mediajourney.particles.android.util.TextureHelper
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10

class SkyBoxRender(var context: Context) : GLSurfaceView.Renderer {

    lateinit var skyBox: SkyBox;
    var mProgram = -1

    private val projectionMatrix = FloatArray(16)
    private val viewMatrix = FloatArray(16)
    private val viewProjectionMatrix = FloatArray(16)

    private var aPositionLoc = -1;
    private var uMatrixLoc = -1;
    private var uTextureLoc = -1;
    private var skyBoxTexture = -1;


    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        GLES20.glClearColor(0f, 0f, 0f, 1f)
        skyBox = SkyBox()
        val vertexStr = ShaderHelper.loadAsset(context.resources, "sky_box_vertex.glsl")
        val fragStr = ShaderHelper.loadAsset(context.resources, "sky_box_fragment.glsl")

        mProgram = ShaderHelper.loadProgram(vertexStr, fragStr)

        aPositionLoc = GLES20.glGetAttribLocation(mProgram, "aPosition")

        uMatrixLoc = GLES20.glGetUniformLocation(mProgram, "uMatrix")

        uTextureLoc = GLES20.glGetUniformLocation(mProgram, "uTexture")

        skyBoxTexture = TextureHelper.loadCubeMap(context, intArrayOf(R.drawable.left2, R.drawable.right2,
                R.drawable.bottom2, R.drawable.top2,
                R.drawable.front2, R.drawable.back2))

    }


    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        GLES20.glViewport(0, 0, width, height)
        val whRadio = width / (height * 1.0f)
        Matrix.setIdentityM(projectionMatrix, 0)
        Matrix.perspectiveM(projectionMatrix, 0, 105f, whRadio, 1f, 10f)
    }

    var frameIndex: Int = 0

    override fun onDrawFrame(gl: GL10?) {

        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
        GLES20.glClearColor(0f, 0f, 0f, 1f)

        //自動旋轉的
        val xRotationAuto = frameIndex / 8f

        //整體旋轉的值 = 自旋轉+滑動觸摸觸發的旋轉值
        val xRotationT = xRotationAuto +xRotation

        frameIndex++

        Matrix.setIdentityM(viewMatrix, 0)


        //採用移動的方式,可以看到立方體的6個面上的紋理圖片
//        Matrix.translateM(viewMatrix,0, xRotation,0f,0f)

        //採用旋轉的方式,只能採用旋轉的方式,進行實現視角變換,達到移動的效果
        Matrix.rotateM(viewMatrix, 0, xRotationT, 0f, 1f, 0f)
//        Matrix.rotateM(viewMatrix, 0, yRotation, 1f, 0f, 0f)


        Matrix.multiplyMM(viewProjectionMatrix, 0, projectionMatrix, 0, viewMatrix, 0)

        GLES20.glUseProgram(mProgram)

        //傳mvp矩陣數據
        GLES20.glUniformMatrix4fv(uMatrixLoc, 1, false, viewProjectionMatrix, 0)
        //傳紋理數據
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
        GLES20.glBindTexture(GLES20.GL_TEXTURE_CUBE_MAP, skyBoxTexture)
        GLES20.glUniform1i(uTextureLoc, 0)


        GLES20.glEnableVertexAttribArray(aPositionLoc)
        skyBox.vertexArrayBuffer.position(0);
        GLES20.glVertexAttribPointer(aPositionLoc, SkyBox.POSITION_COMPONENT_COUNT, GLES20.GL_FLOAT, false, 0, skyBox.vertexArrayBuffer)

        GLES20.glDrawElements(GLES20.GL_TRIANGLES, 36, GLES20.GL_UNSIGNED_BYTE, skyBox.indexArrayBuffer)
    }


    private var xRotation = 0f
    private var yRotation = 0f

    fun handleTouchMove(deltaX: Float, deltaY: Float) {
        xRotation += deltaX / 16f
        yRotation += deltaY / 16f

        if (yRotation < -90f) {
            yRotation = -90f
        } else if (yRotation > 90) {
            yRotation = 90f
        }
    }

}

具體的流程和邏輯詳見代碼註釋。
這裏說明下爲什麼採用旋轉的方式,而不是位移的方式進行視角的切換,因爲我們不是在一個平面中,而是位於一個立方體的中央,沿着某個方向(比如Y軸)進行選擇,即可實現天空移動的效果,如果採用位移的方式看到的是立方體的移動。
對比效果如下:


另外關於移動,可以自動旋轉,也可以加入觸碰旋轉的實現,通過glSurfaceView.queueEvent給render刷新旋轉的大小,即可相應跟隨手勢旋轉的效果

        glSurfaceView.setOnTouchListener(object : OnTouchListener {
            var lastX = 0f;
            var lastY = 0f;
            override fun onTouch(v: View?, event: MotionEvent?): Boolean {

                if (event == null) {
                    return false
                }
                if (MotionEvent.ACTION_DOWN == event.action) {
                    lastX = event.x;
                    lastY = event.y;
                } else if (MotionEvent.ACTION_MOVE == event.action) {
                    val deltaX = event.x - lastX
                    val deltaY = event.y - lastY

                    lastX = event.x
                    lastY = event.y

                    glSurfaceView.queueEvent {
                        skyBoxRender.handleTouchMove(deltaX, deltaY)
                    }
                }
                return true
            }
        })

詳細代碼請查看 github https://github.com/ayyb1988/mediajourney

三、資料

  1. 天空盒(SkyBox)的實現原理與細節
  2. NDK OpenGL ES 3.0 開發(十五):立方體貼圖(天空盒)
  3. 立方體貼圖
  4. OpenGL 圖形庫的使用(二十六)—— 高級OpenGL之立方體貼圖Cubemaps
  5. opengl渲染管線 不能再詳細了

四、收穫

  1. 瞭解天空盒的原理
  2. 立方體貼圖的實現
  3. 具體代碼實現

感謝你的閱讀
要讓渲染的內容更加逼真,反射、折射等的應用必不可少
下一篇我們進入光照部分的學習實踐,歡迎關注公衆號“音視頻開發之旅”,一起學習成長。
歡迎交流

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章