完整代碼在https://github.com/andev009/AndroidShaderDemo
本文是基礎(chǔ)講解,后面的文章給了一些圖像特效原理和相機(jī)特效分析。
Camera濾鏡本質(zhì)上是通過OpenGL SE shader對(duì)圖像進(jìn)行處理,這里為了說明重點(diǎn),忽略Camera部分,只看shader對(duì)圖像進(jìn)行處理部分,分成三個(gè)小Demo,一步步實(shí)現(xiàn)簡單濾鏡。相機(jī)部分在后面的文章里說。
一、單色四邊形(查看SimpleRender.java)
Android 平臺(tái)用GLSurfaceView當(dāng)做畫布,里面封裝了和OpenGL的交互,在布局文件里直接放置GLSurfaceView就行了:
<android.opengl.GLSurfaceView
android:id="@+id/glsurfaceView"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center"
/>
然后需要給GLSurfaceView設(shè)置一個(gè)Render,Render有三個(gè)回調(diào)方法,當(dāng)設(shè)置完Render后,GLSurfaceView內(nèi)部會(huì)開啟一個(gè)GL線程GLThread,具體查看GLSurfaceView的源碼。
public class SimpleRender implements GLSurfaceView.Renderer {
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);//設(shè)置清屏顏色
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
glViewport(0, 0, width, height);//設(shè)置視口尺寸
}
@Override
public void onDrawFrame(GL10 gl) {
glClear(GL_COLOR_BUFFER_BIT);//清屏指令
}
}
glSurfaceView.setRenderer(simpleRender);//設(shè)置Render
我們要畫四邊形,就需要先定義一個(gè)四邊形,四邊形的屬性有頂點(diǎn)坐標(biāo)和頂點(diǎn)顏色兩部分組成,OpenGL的坐標(biāo)范圍在[-1,1]之間,這里不考慮三維頂點(diǎn)坐標(biāo)變換,直接用二維空間定義,每個(gè)頂點(diǎn)的顏色都是紅色(1f, 0f, 0f):
public static final float CUBE[] = {
-1.0f, -1.0f, 1f, 0f, 0f,//左下角
1.0f, -1.0f, 1f, 0f, 0f,//右下角
-1.0f, 1.0f, 1f, 0f, 0f,//左上角
1.0f, 1.0f, 1f, 0f, 0f,//右上角
};
定義好了頂點(diǎn)后,就需要將頂點(diǎn)數(shù)據(jù)傳給OpenGL。這里需要說明的是java代碼運(yùn)行環(huán)境和OpenGL運(yùn)行環(huán)境是不一樣的,java代碼不直接在硬件里運(yùn)行,而是在虛擬機(jī)里運(yùn)行。java還有垃圾回收機(jī)制。而OpenGL在硬件里運(yùn)行,沒有垃圾回收機(jī)制。java可以通過JNI機(jī)制來訪問底層代碼(c或c++),我們之后調(diào)用的android.opengl.GLES20包下的方法,本質(zhì)上就是通過JNI來調(diào)用對(duì)應(yīng)的c或c++代碼來和OpenGL交互。
將頂點(diǎn)數(shù)據(jù)傳給OpenGL通過ByteBuffer類的方法實(shí)現(xiàn),這里為了使用方便封裝成了一個(gè)類VertexArray:
public class VertexArray {
private final FloatBuffer floatBuffer;
public VertexArray(float[] vertexData) {
floatBuffer = ByteBuffer
.allocateDirect(vertexData.length * BYTES_PER_FLOAT)//BYTES_PER_FLOAT = 4,float占4個(gè)字節(jié)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()//分配完所需要的內(nèi)存空間,轉(zhuǎn)化成FloatBuffer
.put(vertexData);//將vertexData 從虛擬機(jī)內(nèi)存復(fù)制到native memory
}
}
現(xiàn)在native memory有了頂點(diǎn)數(shù)據(jù),下面就要告訴OpenGL怎么去畫圖形了。這里就涉及到OpenGL渲染管線的概念。我們只關(guān)注兩點(diǎn),一個(gè)是頂點(diǎn)處理(頂點(diǎn)坐標(biāo)變換等),一個(gè)是片元處理(每像素最終顯示什么顏色),這兩部分的工作分別由vertex shader(頂點(diǎn)著色器)和fragment shader(片元著色器)處理。
這里的四邊形頂點(diǎn)坐標(biāo)和顏色數(shù)據(jù)都有,而且不需要做額外處理,直接傳給fragment shader去處理就行了,所以vertex shader很簡單,就是賦值:
//simple_vertex_shader.glsl
attribute vec4 a_Position;//attribute變量,表示只能在vertex shader中使用
attribute vec4 a_Color;
varying vec4 v_Color;//varying 變量,表示要傳給fragment shader的數(shù)據(jù)
void main()
{
v_Color = a_Color;//之前cube定義的頂點(diǎn)顏色,傳給fragment shader
gl_Position = a_Position;//之前cube定義的頂點(diǎn)坐標(biāo),最終顯示的坐標(biāo)給gl_Position,OpenGL用gl_Position做為最終的位置值
}
再定義fragment shader,在fragment shader里,只需要把
vertex shader傳過來的顏色值顯示出來:
//simple_fragment_shader.glsl
precision mediump float; //設(shè)置精度,vertex shader默認(rèn)是highp
varying vec4 v_Color;
void main()
{
gl_FragColor = v_Color; // gl_FragColor就是最終的顏色值
}
定義好了vertex shader和fragment shader,使用前要經(jīng)過加載,編譯,鏈接三個(gè)階段,可以看到shader語言和c語言很像,運(yùn)行c語言也要經(jīng)過這幾個(gè)階段。
1.加載
加載代碼在TextResourceReader.java,就是把shader文件讀出來存在String里。
2.編譯
編譯代碼在ShaderHelper.java的compileShader方法里:
//創(chuàng)建shader對(duì)象
final int shaderObjectId = glCreateShader(type);//type分別是GL_VERTEX_SHADER或GL_FRAGMENT_SHADER
glShaderSource(shaderObjectId, shaderCode);//shaderCode就是加載的shader字符串,OpenGL會(huì)讀shader并和shaderObjectId聯(lián)系在一起
glCompileShader(shaderObjectId);//OpenGL編譯讀入的shader
3.鏈接
鏈接代碼在ShaderHelper.java的linkProgram方法里:
//創(chuàng)建著色器程序?qū)ο?final int programObjectId = glCreateProgram();
// vertexShader關(guān)聯(lián)到 program.
glAttachShader(programObjectId, vertexShaderId);
//fragmentShader關(guān)聯(lián)到 program.
glAttachShader(programObjectId, fragmentShaderId);
// Link the two shaders together into a program.
glLinkProgram(programObjectId);
上面步驟處理完,就得到了programObjectId這個(gè)著色器程序?qū)ο?,在繪制時(shí),怎么處理頂點(diǎn),顏色,都是programObjectId負(fù)責(zé)了。
準(zhǔn)備工作做了那么多,下面開始真正畫四邊形了,在前面定義的Render回調(diào)方法onDrawFrame里畫了。
@Override
public void onDrawFrame(GL10 gl) {
glClear(GL_COLOR_BUFFER_BIT);
colorProgram.useProgram();
vertexArray.setVertexAttribPointer(
0,
colorProgram.getPositionAttributeLocation(),
POSITION_COMPONENT_COUNT,
STRIDE);//綁定vertex shader的a_Color
vertexArray.setVertexAttribPointer(
POSITION_COMPONENT_COUNT,
colorProgram.getColorAttributeLocation(),
COLOR_COMPONENT_COUNT,
STRIDE);//綁定vertex shader的a_Position
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
}
colorProgram就是個(gè)封裝的著色器程序?qū)ο?,useProgram內(nèi)部調(diào)用的OpenGL方法glUseProgram(program);program就是之前鏈接后生成的programObjectId。
前面vertex shader里有兩個(gè)attribute變量a_Position和a_Color,vertexArray里的頂點(diǎn)數(shù)據(jù)要分別傳給這兩個(gè)變量,vertexArray.setVertexAttribPointer這兩個(gè)方法就是做這件事的,具體查看代碼,內(nèi)部調(diào)用了glVertexAttribPointer。
OpenGL現(xiàn)在有了所有數(shù)據(jù)和繪制方法,最后一步讓OpenGL按照GL_TRIANGLE_STRIP規(guī)則繪制頂點(diǎn):
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
運(yùn)行后,顯示:

二、單個(gè)紋理的四邊形(查看SimpleTextureRender.java)
這次繪制的四邊形帶紋理,定義四邊形不需要頂點(diǎn)顏色值了,但是需要紋理坐標(biāo)。一般紋理圖片的坐標(biāo)原點(diǎn)在左上角,而OpenGL要求的紋理坐標(biāo)原點(diǎn)在左下角,因此定義四邊形紋理坐標(biāo)時(shí)將y值1-翻轉(zhuǎn)。
public static final float CUBE[] = {//翻轉(zhuǎn)頂點(diǎn)信息中的紋理坐標(biāo),統(tǒng)一用1去減
-1.0f, -1.0f, 0f, 1f - 0f,
1.0f, -1.0f, 1f, 1f -0f,
-1.0f, 1.0f, 0f, 1f -1f,
1.0f, 1.0f, 1f, 1f -1f,
};
紋理的加載被封裝成TextureHelper,由下面代碼加載紋理:
final int[] textureObjectIds = new int[1];
glGenTextures(1, textureObjectIds, 0);//生成紋理對(duì)象
final Bitmap bitmap = BitmapFactory.decodeResource(
context.getResources(), resourceId, options);//讀入drawable文件下的紋理
圖片文件生成bitmap
glBindTexture(GL_TEXTURE_2D, textureObjectIds[0]);//綁定紋理對(duì)象,意味著
以下操作都是對(duì)此紋理操作
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
GL_LINEAR_MIPMAP_LINEAR);//過濾器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);//過濾器
texImage2D(GL_TEXTURE_2D, 0, bitmap, 0);//讓OpenGL讀入bitmap數(shù)據(jù),并將數(shù)據(jù)copy到當(dāng)前綁定的紋理對(duì)象去,當(dāng)前紋理對(duì)象在這里是textureObjectIds。
bitmap.recycle();//bitmap使用完,釋放
glBindTexture(GL_TEXTURE_2D, 0);//解綁當(dāng)前紋理對(duì)象,不然以后操作還是這個(gè)紋理
SimpleTextureRender里的成員變量texture就是這次加載的紋理標(biāo)識(shí):
texture = TextureHelper.loadTexture(context, R.drawable.face);
因?yàn)槭褂昧思y理坐標(biāo),前面的shader也要重寫,參考simple_texture_vertex_shader和simple_texture_fragment_shader。在vertex shader里把之前的頂點(diǎn)顏色a_Color換成現(xiàn)在的紋理坐標(biāo)a_TextureCoordinates,頂點(diǎn)坐標(biāo)變量不變。fragment shader之前的著色器程序直接使用cube里的顏色,現(xiàn)在要使用紋理,重寫的fragment shader代碼如下:
precision mediump float;
uniform sampler2D u_TextureUnit;//代表紋理圖片
varying vec2 v_TextureCoordinates;//紋理坐標(biāo)
void main()
{
gl_FragColor = texture2D(u_TextureUnit, v_TextureCoordinates);//獲取紋理坐標(biāo)對(duì)應(yīng)的rgb顏色值
}
這里多了個(gè)uniform標(biāo)識(shí)的變量u_TextureUnit,uniform變量也是外部傳給vertex,fragment shader的,它類似c語言的常量,不能被修改。
texture2D函數(shù)的作用就是根據(jù)紋理坐標(biāo)在紋理圖上找對(duì)應(yīng)的rgb顏色值,找到的顏色值最終賦值給gl_FragColor。
頂點(diǎn)定義好了,shader 準(zhǔn)備好了,下面準(zhǔn)備著色器程序了,這里封裝了SimpleTextureShaderProgram,shader的加載,編譯,鏈接和之前一樣。多了個(gè)設(shè)置shader 里Uniform變量的方法:
public void setUniforms(int textureId) {
// 設(shè)置當(dāng)前紋理單元texture unit 0.
glActiveTexture(GL_TEXTURE0);
// 將紋理對(duì)象綁定上面的紋理單元.
glBindTexture(GL_TEXTURE_2D, textureId);
// 在shader里,讓紋理采樣器用texture unit 0這個(gè)單元的紋理
glUniform1i(uTextureUnitLocation, 0);
}
下面看SimpleTextureRender完整的繪圖方法:
@Override
public void onDrawFrame(GL10 gl) {
glClear(GL_COLOR_BUFFER_BIT);
simpleTextureShaderProgram.useProgram();//使用著色器程序
simpleTextureShaderProgram.setUniforms(texture);
vertexArray.setVertexAttribPointer(
0,
simpleTextureShaderProgram.getPositionAttributeLocation(),
POSITION_COMPONENT_COUNT,
STRIDE);//綁定頂點(diǎn)坐標(biāo)值
vertexArray.setVertexAttribPointer(
POSITION_COMPONENT_COUNT,
simpleTextureShaderProgram.getTextureCoordinatesAttributeLocation(),
TEXTURE_COORDINATES_COMPONENT_COUNT,
STRIDE);//綁定紋理坐標(biāo)值
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
}
可以看到和之前繪制單色四邊形幾乎一樣,區(qū)別就是給shader設(shè)置一個(gè)紋理圖片,把之前的顏色值換成了紋理坐標(biāo)值。
運(yùn)行程序后,顯示:

二、帶多個(gè)紋理的四邊形,濾鏡效果(查看MultiTextureRender.java)
Camera濾鏡特效本質(zhì)是將Camera的每一幀圖像經(jīng)過shader處理后再顯示出來。shader處理圖像時(shí)有各種圖像處理算法,有的只是縮放原始圖像的rgb值,有的是多圖像顏色混合。這里給shader傳入多張圖片,看看混合后的效果。
首先四邊形頂點(diǎn)數(shù)據(jù)不需要重新定義,之前的頂點(diǎn)坐標(biāo)和紋理坐標(biāo)就夠了。
然后vertex shader也不用改,只接收頂點(diǎn)坐標(biāo)和紋理坐標(biāo)。
需要改動(dòng)的是fragment shader,參考multi_texture_fragment_shader
這里傳入了6張圖片,當(dāng)然shader里混合顏色時(shí)可以只使用部分幾張。
precision mediump float;
uniform sampler2D u_TextureUnit0;//orgin
uniform sampler2D u_TextureUnit1;//edge
uniform sampler2D u_TextureUnit2;//hefemap
uniform sampler2D u_TextureUnit3;//hefemetal
uniform sampler2D u_TextureUnit4;//hefesoftlight
uniform sampler2D u_TextureUnit5;//hefegradientmap
varying vec2 v_TextureCoordinates;
void main()
{
// gl_FragColor = texture2D(u_TextureUnit0, v_TextureCoordinates);
vec4 originColor = texture2D(u_TextureUnit0, v_TextureCoordinates);
vec3 texel = texture2D(u_TextureUnit0, v_TextureCoordinates).rgb;
vec3 edge = texture2D(u_TextureUnit1, v_TextureCoordinates).rgb;
vec3 hefemap = texture2D(u_TextureUnit2, v_TextureCoordinates).rgb;
texel = texel * edge;
//texel = vec3(dot(vec3(0.3, 0.6, 0.1), texel));
gl_FragColor = vec4(texel, 1.0f);
}
u_TextureUnit0代表上面單個(gè)紋理Demo運(yùn)行顯示的原始圖像,一個(gè)美女?,F(xiàn)在要在這個(gè)圖像四周加漸變邊框,邊框是下面這個(gè)圖,在shader中被u_TextureUnit1代表

shader中兩張圖片顏色混合的部分在這里:
texel = texel * edge;
edge是黑白圖,白色是1,黑色是0,漸變是[0,1]之間的值,這樣相乘后原始圖x1的部分保持原來不變,x0的部分就變黑了,中間值就有漸變效果。
和之前一樣,頂點(diǎn)數(shù)據(jù),紋理坐標(biāo)有了,shader也有了,該寫著色器程序了,著色器程序這里封裝成了MultiTextureShaderProgram,同樣在構(gòu)造方法里走了shader的加載,編譯,鏈接。來看下和單張紋理圖有什么不同。之前用的一個(gè)int變量持有單張紋理,現(xiàn)在有多張就使用一個(gè)數(shù)組:
private final int []uTextureUnitLocation;
同樣setUniforms方法不像之前傳一個(gè)紋理給shader,這里要傳多個(gè)紋理:
public void setUniforms(int [] textureIDs, float strength) {
for(int i = 0; i < textureIDs.length; i++){
glActiveTexture(GL_TEXTURE0 + i);
glBindTexture(GL_TEXTURE_2D, textureIDs[i]);
glUniform1i(uTextureUnitLocation[i], i);
}
}
最后寫render,這里封裝的是MultiTextureRender,可以看到幾乎和之前單個(gè)紋理的SimpleTextureShaderProgram一樣,只是多了幾個(gè)加載紋理的調(diào)用:
multiTextureShaderProgram = new MultiTextureShaderProgram(context);
originTexture = TextureHelper.loadTexture(context, R.drawable.face);
edgeTexture = TextureHelper.loadTexture(context, R.drawable.edgeburn);
hefeMapTexture = TextureHelper.loadTexture(context, R.drawable.hefemap);
hefemetalTexture = TextureHelper.loadTexture(context, R.drawable.hefemetal);
hefesoftlightTexture = TextureHelper.loadTexture(context, R.drawable.hefesoftlight);
hefegradientmapTexture = TextureHelper.loadTexture(context, R.drawable.hefegradientmap);
textureIDs = new int[]{originTexture, edgeTexture, hefeMapTexture,
hefemetalTexture, hefesoftlightTexture,hefegradientmapTexture};
運(yùn)行后,顯示

可以看到圖片周圍混合了黑白漸變色。
還可以在fragment shader里加上注釋的那行代碼,
texel = vec3(dot(vec3(0.3, 0.6, 0.1), texel));
混合后圖片可以變黑白色了。至于其他濾鏡效果,就要寫不同的算法混合了。