Android OpenGL ES視頻渲染(一)GLSurfaceView

Android中視頻渲染有幾種方式,之前的文章使用的是nativewindow(包括softwareRender)。今天介紹另一總視頻渲染的方式——OpenGL ES。
閱讀本文之前需要對OpenGL有一定的瞭解,可以參考https://www.jianshu.com/p/99daa25b4573

在Android中使用OpenGL的方法有兩種,一種是在native層使用EGL+OpenGL來實現,另一種則是GLSurfaceView。
本文將使用GLSurfaceView+MediaPlayer實現播放,並通過OpenGL進行簡單的濾鏡處理,以此來說明如何使用GLSurfaceView。

題外話:nativewindow和OpenGL渲染視頻的代碼,可以參考ijkplayer的實現。

OpenGL

OpenGL引擎渲染圖像的流程比較複雜,簡單來說是以下幾步。(引用自https://www.jianshu.com/p/99daa25b4573)
但我們最主要先了解頂點處理階段及片元處理階段。

階段一:指定幾何對象
所謂幾何對象,就是點,直線,三角形,這裏將根據具體執行的指令繪製幾何圖元。比如,OpenGL提供給開發者的繪製方法glDrawArrays,這個方法裏的第一個參數是mode,就是制定繪製方式,可選值有一下幾種。

GL_POINT:以點的形式進行繪製,通常用在繪製粒子效果的場景中。
GL_LINES:以線的形式進行繪製,通常用在繪製直線的場景中。
GL_TRIANGLE_STRIP:以三角形的形式進行繪製,所有二維圖像的渲染都會使用這種方式。

階段二:頂點處理
不論以上的幾何對象是如何指定的,所有的幾何數據都將會經過這個階段。這個階段所做的操作就是,根據模型視圖和投影矩陣進行變換來改變頂點的位置,根據紋理座標與紋理矩陣來改變紋理座標的位置,如果涉及三維的渲染,那麼這裏還要處理光照計算與法線變換。
一般輸出是以gl_Position來表示具體的頂點位置的,如果是以點來繪製幾何圖元,那麼還應該輸出gl_PointSize。

階段三:圖元組裝
在經過階段二的頂點處理操作之後,還是紋理座標都是已經確定好了的。在這個階段,頂點將會根據應用程序送往圖元的規則(如GL_POINT、GL_TRIANGLE_STRIP),將紋理組裝成圖元。

階段四:柵格化操作
由階段三傳遞過來的圖元數據,在此將會分解成更小的單元並對應於幀緩衝區的各個像素。這些單元稱爲片元,一個片元可能包含窗口顏色、紋理座標等屬性。片元的屬性是根據頂點座標利用插值來確定的,這就是柵格化操作,也就是確認好每一個片元是什麼。

階段五:片元處理
通過紋理座標取得紋理(texture)中相對應的片元像素值(texel),根據自己的業務處理(比如提亮、飽和度調節、對比度調節、高斯模糊)來變換這個片元的顏色。這裏的輸出是gl_FragColor,用於表示修改之後的像素的最終結果。

階段六:幀緩衝操作
該階段主要執行幀緩衝的寫入操作,這也是渲染管線的最後一步,負責將最終的像素值寫入到幀緩衝區中。

OpenGL ES提供了可編程的着色器來代替渲染管線的某個階段。
Vertex Shader(頂點着色器)用來替代頂點處理階段。
Fragment Shader(片元着色器,又稱爲像素着色器)用來替換片元處理階段。

簡單來講就是OpenGL會在頂點着色器確定頂點的位置,然後這些頂點連起來就是我們想要的圖形。接着在片元着色器裏面給這些圖形上色:

在這裏插入圖片描述

GLSurfaceView

GLSurfaceView看名字就是可以使用OpenGL的SurfaceView,也確實如此,它繼承自SurfaceView,具備SurfaceView的特性,並加入了EGL的管理,它自帶了一個GLThread繪製線程(EGLContext創建GL環境所在線程即爲GL線程),繪製的工作直接通過OpenGL在繪製線程進行,不會阻塞主線程,繪製的結果輸出到SurfaceView所提供的Surface上。
所以爲什麼我們不直接用surfaceView來進行播放呢?有以下兩個好處:

  1. 通過GLSurfaceView進行視頻渲染,可以使用GPU加速,相對於SurfaceView使用畫布進行繪製,OpenGL的繪製關聯到GPU,效率更高。
  2. 可以定製render(渲染器),從而可以實現定製效果。

使用流程:
創建一個GLSurfaceView用來承載視頻
->設置render(實現OpenGL着色器代碼)
->創建SurfaceTexture,綁定的外部Texture
->將SurfaceTexture的surface設置給MediaPlayer,啓動播放
->在render的onDrawFrame中更新Texture,繪製新畫面。

其中,render是最核心部分。

1、創建GLSurfaceView

    <android.opengl.GLSurfaceView
        android:id="@+id/surface_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
        glView = findViewById(R.id.surface_view);
        glView.setEGLContextClientVersion(2);

        MyGLRender glVideoRenderer = new MyGLRender();//創建renderer
        glView.setRenderer(glVideoRenderer);//設置renderer

創建GLSurfaceView後,設置其OpenGL版本爲2.0,然後設置render。下面介紹MyGLRender。

2、創建render

render需要實現GLSurfaceView.Renderer的三個接口:

    public interface Renderer {
        void onSurfaceCreated(GL10 var1, EGLConfig var2);

        void onSurfaceChanged(GL10 var1, int var2, int var3);

        void onDrawFrame(GL10 var1);
    }

onSurfaceCreated進行渲染程序的初始化,創建Surface,啓動MediaPlayer

    @Override
    public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
        initGLProgram();
        Surface surface = crateSurface();
        // mediaplayer play
        try {
            mPlayer.setSurface(surface);
            mPlayer.prepare();
            mPlayer.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

渲染程序的初始化

initGLProgram()中創建頂點着色器和片元着色器代碼,一步步看:

頂點着色器

    private final String VSH_CODE =  "uniform mat4 uSTMatrix;\n"+
                                        "attribute vec4 aPosition;\n"+
                                        "attribute vec4 aTexCoord;\n"+
                                        "varying vec2 vTexCoord;\n"+
                                        "void main(){\n"+
                                            "vTexCoord = (uSTMatrix*aTexCoord).xy;\n"+
                                            "gl_Position = aPosition;\n"+
                                        "}";

OpenGL會將每個頂點的座標傳遞給頂點着色器,我們可以在這裏改變頂點的位置。例如我們給每個頂點都加上一個偏移,就能實現整個圖形的移動。

aPosition爲頂點座標,賦值給gl_Position ,表示物體位置,構成圖元,可由外部傳入。
aTexCoord爲紋理座標,紋理座標描述紋理該如何在圖元上貼圖,可由外部傳入。
vTexCoord爲最終要傳遞給片元着色器的紋理座標,爲什麼要在aTexCoord的基礎上進行矩陣轉換呢?這是因爲計算機圖像座標與紋理座標的表示是不一致的。如下圖:

在這裏插入圖片描述
在這裏插入圖片描述
因爲我們使用的texture是從外部得到的,其對應的是計算機座標系,所以需要矩陣轉換,這個矩陣可通過SurfaceTexture.getTransformMatrix函數獲取到。

片元着色器

    private  final String FSH_CODE = "#extension GL_OES_EGL_image_external : require\n"+
                                        "precision mediump float;\n"+
                                        "varying vec2 vTexCoord;\n"+
                                        "uniform mat4 uColorMatrix;\n"+
                                        "uniform samplerExternalOES sTexture;\n"+
                                        "void main() {\n"+
                                            "gl_FragColor=uColorMatrix*texture2D(sTexture, vTexCoord).rgba;\n"+
                                            //"gl_FragColor = texture2D(sTexture, vTexCoord);\n"+
                                        "}";

片元着色器要注意的是#extension GL_OES_EGL_image_external : require,因爲使用的是外部紋理samplerExternalOES類型的紋理sTexture,所以需要加上。
vTexCoord是從頂點着色器傳過來的紋理座標。
texture2D函數可以從該座標獲取到對應的顏色,這裏我們加入了顏色轉換矩陣uColorMatrix,這樣就能進行一些效果處理。最後將顏色賦值給gl_FragColor。

顏色效果矩陣如下:

   private static float[] COLOR_MATRIX3 = {
        // 懷舊效果矩陣
            0.393f,0.349f, 0.272f,0.0f ,
            0.769f,0.686f,0.534f,0.0f,
            0.189f,0.168f,0.131f,0.0f,
            0.0f,0.0f,0.0f,1.0f
    };

填充頂點座標及紋理座標
完成頂點着色器及片元着色器後,創建渲染程序,接下來我們要填充頂點信息:
頂點着色器中,aPosition表示物體位置座標,座標系中x軸從左到右是從-1到1變化的,y軸從下到上是從-1到1變化的,物體的中心點恰好是(0,0)的位置。
在這裏插入圖片描述
aTexCoord描述紋理座標(如上圖OpenGL二維紋理座標),我們現在要把紋理按照,左下->右下->左上->右上的順序,貼到物體上。所以對應的頂點座標及紋理座標數據爲:

        //頂點着色器座標,z爲0
        float[] vers = {
                -1.0f, -1.0f, 0.0f,
                1.0f, -1.0f, 0.0f,
                -1.0f, 1.0f, 0.0f,
                1.0f, 1.0f, 0.0f,
        };
	   //紋理座標,texture座標ST,需要根據圖像進行轉換
        float[] txts = {
                0.0f, 0.0f,
                1.0f, 0.0f,
                0.0f, 1.0f,
                1.0f, 1.0f
        };

通過 GLES20.glEnableVertexAttribArray及GLES20.glVertexAttribPointer兩個函數,完成頂點信息設置。

設置顏色效果
通過glGetUniformLocation獲取到uColorMatrix矩陣的句柄,將顏色矩陣設賦值給它就行。這樣就會在片元着色器中生效。

        //設置顏色效果
        int colorMatrixHandle = GLES20.glGetUniformLocation(programId, "uColorMatrix");
        GLES20.glUniformMatrix4fv(colorMatrixHandle, 1, false, COLOR_MATRIX3, 0);

完整代碼:

private void initGLProgram(){
        int vertexShader = compileShader(GLES20.GL_VERTEX_SHADER, VSH_CODE);
        int fragmentShader = compileShader(GLES20.GL_FRAGMENT_SHADER, FSH_CODE);
        int programId = buildProgram(vertexShader, fragmentShader);
        if(programId == 0)
            return;

        GLES20.glUseProgram(programId);
        mSTMatrixHandle = GLES20.glGetUniformLocation(programId, "uSTMatrix");//轉換矩陣

        //頂點着色器座標
        float[] vers = {
                -1.0f, -1.0f, 0.0f,
                1.0f, -1.0f, 0.0f,
                -1.0f, 1.0f, 0.0f,
                1.0f, 1.0f, 0.0f,
        };
        FloatBuffer vertexBuffer = ByteBuffer.allocateDirect(vers.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer()
                .put(vers);
        vertexBuffer.position(0);

        //紋理座標,texture座標ST,需要根據圖像進行轉換
        float[] txts = {
                0.0f, 0.0f,
                1.0f, 0.0f,
                0.0f, 1.0f,
                1.0f, 1.0f
        };

        FloatBuffer textureVertexBuffer = ByteBuffer.allocateDirect(txts.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer()
                .put(txts);
        textureVertexBuffer.position(0);

        //設置頂點座標和紋理座標
        int apos = GLES20.glGetAttribLocation(programId, "aPosition");
        GLES20.glEnableVertexAttribArray(apos);
        GLES20.glVertexAttribPointer(apos, 3, GLES20.GL_FLOAT, false, 12, vertexBuffer);

        int atex = GLES20.glGetAttribLocation(programId, "aTexCoord");
        GLES20.glEnableVertexAttribArray(atex);
        GLES20.glVertexAttribPointer(atex, 2, GLES20.GL_FLOAT, false, 8, textureVertexBuffer);

        //設置顏色效果
        int colorMatrixHandle = GLES20.glGetUniformLocation(programId, "uColorMatrix");
        GLES20.glUniformMatrix4fv(colorMatrixHandle, 1, false, COLOR_MATRIX3, 0);
    }

3、創建SurfaceTexture,綁定外部紋理

glGenTextures創建Texture,我們使用的是外部紋理,所以只需要一個即可。
glBindTexture綁定紋理,要注意這裏需要設置GL_TEXTURE_EXTERNAL_OES標誌。
glTexParameterf設置一些屬性,這裏設置的是縮放的算法。
然後根據mTextureID創建SurfaceTexture,然後創建Surface,Surface就可以設置給MeidaPlayer。

完整代碼:

    private Surface crateSurface(){
        // Create SurfaceTexture that will feed this textureId and pass to MediaPlayer
        int[] textures = new int[1];//just one texures,use external mode
        GLES20.glGenTextures(1, textures, 0);
        mTextureID = textures[0];
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID);

        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);

        mSurfaceTexture = new SurfaceTexture(mTextureID);
        mSurfaceTexture.setOnFrameAvailableListener(this);
        Surface surface = new Surface(mSurfaceTexture);
        return surface;
    }

4、Surface設置給MediaPlayer,啓動播放

沒什麼可以說道的,就是把上面創建的surface設置給播放器,同步的prepare,加上start。

    // mediaplayer play
    try {
        mPlayer.setSurface(surface);
        mPlayer.prepare();
        mPlayer.start();
    } catch (IOException e) {
        e.printStackTrace();
    }

5、onDrawFrame中更新Texture,繪製新畫面

上面創SurfaceTexture時通過setOnFrameAvailableListener設置了監聽器,監聽紋理的更新,更新了,我們就設置isFrameUpdate爲true。
onDrawFrame是render進行繪製時會調用,當isFrameUpdate爲true,意味着我們可以進行繪製了。

先通過SurfaceTexture.updateTexImage()更新紋理,然後glViewport設置繪製的窗口大小。

OpenGL雖然是在Surface上繪製,但我們可以不鋪滿整個Surface,可以只在它的某部分繪製,例如我們可以用下面代碼只用TextureSurface的左下角的四分之一去顯示OpenGL的畫面:

//width、height是TextureView的寬高 
GLES20.glViewport(0, 0, width/2, height/2); 

在這裏插入圖片描述

我們這裏還是鋪滿整個View,寬高可以在onSurfaceChanged中獲取到。

繪製前先清除上一幀,

        //clear
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);

當然這裏還可以再清空片元着色器的外部紋理。

設置紋理變換矩陣,矩陣在SurfaceTexture.getTransformMatrix獲取到
激活綁定紋理,然後就可以繪製了。
繪製採用的三角形方式GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

完整代碼如下:

    @Override
    public void onSurfaceChanged(GL10 gl10, int width, int height) {
        screenWidth = width;
        screenHeight = height;
    }

    @Override
    public void onDrawFrame(GL10 gl10) {
        synchronized (this){
            if(isFrameUpdate){
                mSurfaceTexture.updateTexImage();
                mSurfaceTexture.getTransformMatrix(mSTMatrix);
                isFrameUpdate = false;
            }
        }
        //update width and height
        GLES20.glViewport(0, 0, screenWidth, screenHeight);

        //clear
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);

        //update st mat4
        GLES20.glUniformMatrix4fv(mSTMatrixHandle, 1, false, mSTMatrix, 0);

        //bind and active, juest one time
        {
            GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
            GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID);
        }
        //draw
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
    }

    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
        isFrameUpdate = true;
    }

總結

播放效果如下:
在這裏插入圖片描述
下一章會描述如何在native層使用EGL和OpenGL,這樣會對Android OpenGL ES視頻渲染有更深入的瞭解。

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