1. 程式人生 > >Android平臺Camera實時濾鏡實現方法探討(二)--Android-GPUImage探討

Android平臺Camera實時濾鏡實現方法探討(二)--Android-GPUImage探討

本章介紹android-gpuimage實現方式,即通過在C++層實現YUV-RGB轉換,通過OpenGL繪製,通過片段著色器執行Shader指令碼實現影象處理,雖然將濾鏡的一些處理交給GPU來執行,極大的減少了速度,但YUV-RGB過程卻拖了後腿。本章將從YUV、GLSL與OpenGL開始,逐步探討方案5。其中YUV-RGB過程上一章已有粗略探討,本章不再贅述。

“OpenGL著色語言(OpenGL Shading Language)是用來在OpenGL中著色程式設計的語言,也即開發人員寫的短小的自定義程式,他們是在圖形卡的GPU (Graphic Processor Unit圖形處理單元)上執行的,代替了固定的

渲染管線的一部分,使渲染管線中不同層次具有可程式設計型。比如:檢視轉換、投影轉換等。GLSL(GL Shading Language)的著色器程式碼分成2個部分:Vertex Shader(頂點著色器)和Fragment(片斷著色器),有時還會有Geometry Shader(幾何著色器)。負責執行頂點著色的是頂點著色器。它可以得到當前OpenGL 中的狀態,GLSL內建變數進行傳遞。GLSL其使用C語言作為基礎高階著色語言,避免了使用組合語言或硬體規格語言的複雜性。”

頂點著色器是一個可程式設計單元,執行頂點變換、紋理座標變換、光照、材質等頂點的相關操作,每頂點執行一次。頂點著色器定義了在 2D 或者 3D 場景中幾何圖形是如何處理的。一個頂點指的是 2D 或者 3D 空間中的一個點。在影象處理中,有 4 個頂點:每一個頂點代表影象的一個角。頂點著色器設定頂點的位置,並且把位置和紋理座標這樣的引數傳送到片段著色器。下面是GPUImage中一個頂點著色器:

attribute vec4 position;  
attribute vec4 inputTextureCoordinate;

varying vec2 textureCoordinate;

void main()  
{
    gl_position = position;
    textureCoordinate = inputTextureCoordinate.xy;
}

attribute是隻能在頂點著色器中使用的變數,來表示一些頂點的資料,如:頂點座標,法線,紋理座標,頂點顏色等。

varying變數是vertex和fragment shader之間做資料傳遞用的。一般vertex shader修改varying變數的值,然後fragment shader使用該varying變數的值。因此varying變數在vertex和fragment shader二者之間的宣告必須是一致的。

attribute vec4 position;

position變數是我們在程式中傳給Shader的頂點資料的位置,是一個矩陣,規定了影象4個點的位置,並且可以在shader中經過矩陣進行平移、旋轉等再次變換。在GPUImage中,我們根據GLSurfaceView的大小、PreviewSize的大小實現計算出矩陣,通過glGetAttribLocation獲取id,再通過glVertexAttribPointer將矩陣傳入。新的頂點位置通過在頂點著色器中寫入gl_Position傳遞到渲染管線的後繼階段繼續處理。結合後面繪製過程中的glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);,首先選取第三個點,與前兩個點繪製成一個三角形,再選取最後一個點,與第二、第三個點繪製成三角形,最終繪製成多邊形區域。

attribute vec2 inputTextureCoordinate;

inputTextureCoordinate是紋理座標,紋理座標定義了影象的哪一部分將被對映到多邊形。如圖所示,下圖是OpenGL紋理座標系統,左下角為原點,

     

傳入此座標,代表輸出影象不會經過變換,在GPUImage中,因為輸出影象與應用方向關係,需要將影象旋轉90度,即座標為

<span style="font-size:10px;"> public static final float TEXTURE_ROTATED_90[] = {
            1.0f, 1.0f,
            1.0f, 0.0f,
            0.0f, 1.0f,
            0.0f, 0.0f,
 };</span>

varying vec2 textureCoordinate

因為頂點著色器負責和片段著色器交流,所以我們需要建立一個變數和它共享相關的資訊。在影象處理中,片段著色器需要的唯一相關資訊就是頂點著色器現在正在處理哪個畫素。

gl_Position = position;

gl_Position是用來傳輸投影座標系內頂點座標的內建變數,GPUImage在Java層已經變換過,在這裡不需要經過任何變換。

textureCoordinate = inputTextureCoordinate.xy;

取出這個頂點中紋理座標的 X 和 Y 的位置(僅需要這兩個屬性),然後賦值給一個將要和片段著色器通訊的變數。到此,頂點著色器建立完畢。

片段著色器:

varying highp vec2 textureCoordinate;

uniform sampler2D inputImageTexture;

void main()  
{
    gl_FragColor = texture2D(inputImageTexture, textureCoordinate);
}

片段著色器和頂點著色器會成對出現。片段著色器扮演著顯示的角色。我們的濾鏡處理大部分都在片段著色器中進行。上段程式碼是一個無濾鏡效果的片段著色器。

varying highp vec2 textureCoordinate;

對應頂點著色器中變數名相同的變數,片段著色器作用在每一個畫素上,我們需要一個方法來確定我們當前在分析哪一個畫素/片段。它需要儲存畫素的 X 和 Y 座標。我們接收到的是當前在頂點著色器被設定好的紋理座標。

uniform sampler2D inputImageTexture;

uniforms變數(一致變數)用來將資料值從應用程其序傳遞到頂點著色器或者片元著色器。該變數有點類似C語言中的常量(const),即該變數的值不能被shader程式修改。sampler2D對應2D紋理,在GPUImage中,與onPreviewFrame中經過變換過的RGB資料繫結。GPU將從該紋理中取出點進行處理。

gl_FragColor = texture2D(inputImageTexture, textureCoordinate);

這是我們碰到的第一個 GLSL 特有的方法:texture2D,顧名思義,建立一個 2D 的紋理。它採用我們之前宣告過的屬性作為引數來決定被處理的畫素的顏色。這個顏色然後被設定給另外一個內建變數,gl_FragColor。因為片段著色器的唯一目的就是確定一個畫素的顏色,gl_FragColor 本質上就是我們片段著色器的返回語句。一旦這個片段的顏色被設定,接下來片段著色器就不需要再做其他任何事情了,所以你在這之後寫任何的語句,都不會被執行。

到此為止,我們的Shader就寫完了。

在實際程式例如GPUImage中,操作順序如下

 1.建立shader

1)編寫Vertex Shader和Fragment Shader原始碼。      

2)建立兩個shader 例項:GLuint   glCreateShader(GLenum type);        

3)給Shader例項指定原始碼。 glShaderSource        

4)線上編譯shaer原始碼 void   glCompileShader(GLuint shader)

    public static int loadShader(final String strSource, final int iType) {
        int[] compiled = new int[1];
        int iShader = GLES20.glCreateShader(iType);
        GLES20.glShaderSource(iShader, strSource);
        GLES20.glCompileShader(iShader);
        GLES20.glGetShaderiv(iShader, GLES20.GL_COMPILE_STATUS, compiled, 0);
        if (compiled[0] == 0) {
            Log.d("Load Shader Failed", "Compilation\n" + GLES20.glGetShaderInfoLog(iShader));
            return 0;
        }
        return iShader;
    }

2.建立program

在OpenGL ES中,每個program物件有且僅有一個Vertex Shader物件和一個Fragment Shader物件連線到它。Shader類似於C編譯器。Program類似於C連結器。glLinkProgram操作產生最後的可執行程式,它包含最後可以在硬體上執行的硬體指令。

  1)建立program : GLuint   glCreateProgram(void)       

2)繫結shader到program : void   glAttachShader(GLuint program, GLuint shader)。每個program必須繫結一個Vertex Shader 和一個Fragment Shader。       

3)連結program : void   glLinkProgram(GLuint program)        

4)使用porgram : void   glUseProgram(GLuint program)

    public static int loadProgram(final String strVSource, final String strFSource) {
        int iVShader;
        int iFShader;
        int iProgId;
        int[] link = new int[1];
        iVShader = loadShader(strVSource, GLES20.GL_VERTEX_SHADER);
        if (iVShader == 0) {
            Log.d("Load Program", "Vertex Shader Failed");
            return 0;
        }
        iFShader = loadShader(strFSource, GLES20.GL_FRAGMENT_SHADER);
        if (iFShader == 0) {
            Log.d("Load Program", "Fragment Shader Failed");
            return 0;
        }

        iProgId = GLES20.glCreateProgram();

        GLES20.glAttachShader(iProgId, iVShader);
        GLES20.glAttachShader(iProgId, iFShader);

        GLES20.glLinkProgram(iProgId);

        GLES20.glGetProgramiv(iProgId, GLES20.GL_LINK_STATUS, link, 0);
        if (link[0] <= 0) {
            Log.d("Load Program", "Linking Failed");
            return 0;
        }
        GLES20.glDeleteShader(iVShader);
        GLES20.glDeleteShader(iFShader);
        return iProgId;
    }

3.獲取紋理座標、頂點座標、紋理等對應id

通過glGetAttribLocation和glGetUniformLocation獲取對應的id

 mGLAttribPosition = GLES20.glGetAttribLocation(mGLProgId, "position");
 mGLUniformTexture = GLES20.glGetUniformLocation(mGLProgId, "inputImageTexture");
 mGLAttribTextureCoordinate = GLES20.glGetAttribLocation(mGLProgId,
                "inputTextureCoordinate");

4.繪製

1)首先設定背景顏色和繪製建立繪製區域、清理當前緩衝區

2)使用program(glUseProgram),傳遞兩個矩陣

3)通過glGenTextures(GLsizei n, GLuint *textures)產生你要操作的紋理物件的id,然後通過glBindTexture繫結並獲取紋理id,告訴OpenGL下面對紋理的任何操作都是對它所繫結的紋理物件的,比如glBindTexture(GL_TEXTURE_2D,1)告訴OpenGL下面程式碼中對2D紋理的任何設定都是針對索引為1的紋理的。通過glTexParameteri設定一些屬性。最後通過glTexImage2D根據指定引數,包括RGB資料,生成2D紋理。當第二幀繪製的時候,則不需要重新繫結紋理,使用glTexSubImage2D更新現有紋理即可。

public static int loadTexture(final IntBuffer data, final Size size, final int usedTexId) {
        int textures[] = new int[1];
        if (usedTexId == NO_TEXTURE) {
            GLES20.glGenTextures(1, textures, 0);
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
            GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
                    GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
            GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
                    GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
            GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
                    GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
            GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
                    GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
            GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, size.width, size.height,
                    0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, data);
        } else {
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, usedTexId);
            GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, size.width,
                    size.height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, data);
            textures[0] = usedTexId;
        }
        return textures[0];
    }
4)然後使用函式glActiveTexture()來指定要對其進行設定的紋理單元,這裡為GL_TEXTURE0,使用glBindTexture再次繫結,通過glUniform1i複製,最後glDrawArrays繪製。