本章介紹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繪製。