目錄
- 寫著色器代碼
- 通過GLSurfaceview加載Shader并運行
- 遇到的問題
- 參考
- 收獲
我們前兩篇介紹了OpenGL ES 基本概念和GLSL及Shader的渲染流程,這篇我們開始實戰(zhàn),通過GLSurfaceView加載著色器,來繪制三角形、正方形和直線這些平面圖形。在實踐過程中遇到的問題有時候讓人沒有頭緒,檢查了一遍又一遍代碼,發(fā)現(xiàn)流程沒有問題,但屏幕就是一片漆黑。。通過近一個小時的排查,發(fā)現(xiàn)問題出在了這里。。。下面開始我們今天的學習時間之旅,希望對你也有幫助。
GLSL著色器的編寫
如果對OpenGL的基本概念以及渲染流程不清晰的,建議看下前兩篇文章,這些基本概念和流程要了解或者理解,否則后面實踐之旅就是跳坑之旅。
我們先通過下面重要的二張圖,快速回顧下


工欲善其事,必先利其器,如何方便的編寫GLSL代碼吶? AndoridStudio提供了“Support for GLSL”插件。VS Code也有比較強大的插件比如“Shader Toy
”和“Shader languages support for VS Code”。但是都不足之處,就是沒有自動提示和補全的功能。所以編寫GLSL代碼是要細心。
1.1 著色器代碼的編寫
首先我們來編寫下頂點著色器和片元著色器
//vertex_shader.glsl 頂點著色器
attribute vec4 a_Position;
attribute vec4 a_Color;
varying vec4 v_Color;
void main() {
v_Color = a_Color;
gl_Position = a_Position;
}
上述代碼簡單語法回顧
attribute是修飾符 只能用于頂點著色器,用于修飾可變的參數(shù)
vec4: 浮點型向量
gl_Position:內(nèi)置變量
varying:也是一個修飾符,用于頂點著色器和片元著色器的值傳遞。
注意:必須要在頂點著色器和片元著色器都定義同名同類型同varying修飾符的變量,才能正常傳遞。
//fragment_shader.glsl 片元著色器
precision mediump float;
varying vec4 v_Color;
void main() {
gl_FragColor = v_Color;
}
這里對精度precision mediump 做下說明
用于修飾浮點型和整形,有三個等級 highp\mediump、lowp

1.2 著色器代碼的讀取
著色器代碼通常放在assets目錄下或者raw目錄下,當然也見到過直接寫在代碼里。我們?yōu)榱朔奖?,采用比較通用的方式:把glsl代碼文件放在了assets下,再加載前需要先把他們讀到內(nèi)存中,常規(guī)的文件讀寫
public static String loadAsset(Resources res, String path) {
StringBuilder stringBuilder = new StringBuilder();
try {
InputStream is = res.getAssets().open(path);
byte[] buffer = new byte[1024];
int count;
while (-1 != (count = is.read(buffer))) {
stringBuilder.append(new String(buffer, 0, count));
}
String result = stringBuilder.toString().replaceAll("\\r\\n", "\n");
return result;
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
1.3 著色器的創(chuàng)建、設(shè)置源碼、編譯

private static int loadShader(int type, String codeStr) {
//1. 根據(jù)類型(頂點著色器、片元著色器)創(chuàng)建著色器,拿到著色器句柄
int shader = GLES20.glCreateShader(type);
Log.i(TAG, "compileShaderCode: type=" + type + " shaderId=" + shader);
if (shader > 0) {
//2. 設(shè)置著色器代碼 ,shader句柄和code進行綁定
GLES20.glShaderSource(shader, codeStr);
//3. 編譯著色器,
GLES20.glCompileShader(shader);
//4. 查詢編譯狀態(tài)
int[] status = new int[1];
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, status, 0);
Log.i(TAG, "loadShader: status[0]=" + status[0]);
//如果失敗,釋放資源
if (status[0] == 0) {
GLES20.glDeleteShader(shader);
return 0;
}
}
return shader;
}
1.4 程序的創(chuàng)建、attach著色器、鏈接、使用
public static int loadProgram(String verCode, String fragmentCode) {
//1. 創(chuàng)建Shader程序,獲取到program句柄
int programId = GLES20.glCreateProgram();
if(programId == 0){
Log.e(TAG, "loadProgram: glCreateProgram error" );
return 0;
}
//2. 根據(jù)著色器語言類型和代碼,attach著色器
GLES20.glAttachShader(programId, loadShader(GLES20.GL_VERTEX_SHADER, verCode));
GLES20.glAttachShader(programId, loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentCode));
//3. 鏈接
GLES20.glLinkProgram(programId);
//4. 使用
GLES20.glUseProgram(programId);
return programId;
}
1.5 狀態(tài)查詢
著色器Shader和Program創(chuàng)建后會拿到對應(yīng)的句柄,通過檢查是否大于0驗證是否可用
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, status, 0);著色器編譯后檢查編譯的狀態(tài)是否大于0判斷可用性。
glGetProgramiv(programObjectId, GL_VALIDATE_STATUS,
validateStatus, 0);//程序鏈接后,檢查程序的可用性
1.6 輸入與輸出
輸入:給著色器賦值
輸出:在屏幕上顯示
前面5個步驟把準備工作都做好了,那邊現(xiàn)在面臨兩個問題.
1. 我們看到頂點著色器中有兩個attribute修飾的變量,如何給它們賦值?
2. 如何把著色器再屏幕上繪制出來?
這個環(huán)節(jié)我們就來解決這兩個問題
首先定義好頂點坐標和顏色的位數(shù)和著色器的數(shù)據(jù)
//每個頂點坐標的個數(shù)
private final static int COORDS_PER_VERTEX = 2;
//每個頂點顏色的個數(shù)
private final static int COLOR_PER_VERTEX = 3;
// 浮點類型占用的字節(jié)數(shù)
private final static int BYTES_PER_FLOAT = 4;
//下面兩個字符串常量就是GLSL頂點著色器的輸入
private static final String A_POSITION = "a_Position";
private static final String A_COLOR = "a_Color";
//STRIDE是一個頂點的字節(jié)偏移(頂點坐標xy+顏色rgb)
private final int STRIDE = (COORDS_PER_VERTEX+ COLOR_PER_VERTEX )* BYTES_PER_FLOAT;
private FloatBuffer mVertexData;
public MyRender2() {
//頂點數(shù)組
float[] TRIANGLE_COORDS = {
0.5f, 0.5f,1f, 0.5f,0.5f,
-0.5f, -0.5f,0.5f, 1f,0.5f,
0.5f, -0.5f,0.5f, 0.5f,1f
};
//通過nio ByteBuffer把設(shè)置的頂點數(shù)據(jù)加載到內(nèi)存
mVertexData = ByteBuffer
.allocateDirect(TRIANGLE_COORDS.length * BYTES_PER_FLOAT) //需要多少字節(jié)內(nèi)存
.order(ByteOrder.nativeOrder())//大小端排序
.asFloatBuffer()
.put(TRIANGLE_COORDS);//設(shè)置數(shù)據(jù)
}
然后給頂點著色器的變量賦值
String vertexCode = ShaderHelper.loadAsset(MyApplication.getContext().getResources(), "vertex_shader.glsl");
String fragmentCode = ShaderHelper.loadAsset(MyApplication.getContext().getResources(), "fragment_shader.glsl");
//創(chuàng)建著色器程序
programId = ShaderHelper.loadProgram(vertexCode, fragmentCode);
int aPosition = GLES20.glGetAttribLocation(programId, A_POSITION);
Log.i(TAG, "drawFrame: aposition="+aPosition);
mVertexData.position(0);
GLES20.glVertexAttribPointer(aPosition,
COORDS_PER_VERTEX,//用幾個偏移描述一個頂點
GLES20.GL_FLOAT,//頂點數(shù)據(jù)類型
false,
STRIDE,//一個頂點需要多少個字節(jié)偏移
mVertexData//分配的buffer
);
//開啟頂點著色器的attribute
GLES20.glEnableVertexAttribArray(aPosition);
int aColor = GLES20.glGetAttribLocation(programId, A_COLOR);
mVertexData.position(COORDS_PER_VERTEX);
GLES20.glVertexAttribPointer(aColor,COLOR_PER_VERTEX,GL_FLOAT,false,STRIDE,mVertexData);
GLES20.glEnableVertexAttribArray(aColor);
關(guān)鍵API說明
獲取著色器attribute一個屬性:int aPosition = GLES20.glGetAttribLocation(programId, A_POSITION);_
數(shù)據(jù)的偏移:mVertexData.position(0); 因為有坐標和顏色兩個變量的值,數(shù)據(jù)又是根據(jù)頂點一一設(shè)定的。
給attribute賦值:GLES20. glVertexAttribPointer(
int indx,//attribute的句柄
int size,//在數(shù)組中占用的位數(shù)
int type,//數(shù)據(jù)的類型
boolean normalized,
int stride,//步幅 單位字節(jié)數(shù)
java.nio.Buffer ptr // 元數(shù)據(jù)
)使能對應(yīng)的attribute屬性:GLES20.glEnableVertexAttribArray(aPosition);
通過上面幾個環(huán)節(jié)我們可以看到,我們可以看到,是如何給著色器語言中的變量賦值的。下面我們看來下如何渲染。
我們采用GlSurfaceView,通過Render來實現(xiàn),具體如下:
//1. 設(shè)置OpenGL ES的版本
glSView.setEGLContextClientVersion(3);
//2. 給glSurfaceView設(shè)置render
glSView.setRenderer(new MyRender2());
public class MyRender2 implements GLSurfaceView.Renderer {
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//著色器的加載、賦值
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
//清屏
GLES20.glClear(GL_COLOR_BUFFER_BIT);
//繪制
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN,0,3);
}
}
是的,在onDrawFrame中進行不斷的 glDrawArrays來繪制刷新
public static native void glDrawArrays(
int mode, //點、線、三角形
int first,//頂點的第一個數(shù)據(jù)的index
int count//頂點的總數(shù)
);
二、實踐:用GLSurfaceView加載GLSL繪制屏幕圖形
2.1 三角形
上面的代碼中定義的就是三角形,對應(yīng)頂點數(shù)據(jù)如下
float[] TRIANGLE_COORDS = {0.5f, 0.5f,1f, 0.5f,0.5f,
-0.5f, -0.5f,0.5f, 1f,0.5f,
0.5f, -0.5f,0.5f, 0.5f,1f
};
兩個坐標位x&y,和三個顏色位rgb
效果如下

2.2 正方形
只需要修改 頂點數(shù)組和glDrawArrays的count參數(shù)即可
float[] TRIANGLE_COORDS = {
0.5f, 0.5f,1f, 0.5f,0.5f,
-0.5f, -0.5f,0.5f, 1f,0.5f,
0.5f, -0.5f,0.5f, 0.5f,1f,
0.5f, 0.5f,1f, 0.5f,0.5f,
-0.5f, 0.5f,0.5f, 0.5f,1f,
-0.5f, -0.5f,0.5f, 1f,0.5f,
};
public void onDrawFrame(GL10 gl) {
GLES20.glClear(GL_COLOR_BUFFER_BIT);
GLES20.glDrawArrays(GLES20.GL_TRIANGLES,0,6);
}

2.3 直線
float[] TRIANGLE_COORDS = {
-0.5f, 0f, 1f, 0f, 0f,
0.5f, 0f, 0f, 0f, 1f,
};
public void onDrawFrame(GL10 gl) {
GLES20.glClear(GL_COLOR_BUFFER_BIT);
GLES20.glDrawArrays(GLES20.GL_LINES,0,2);
}

通過log分析,我們發(fā)現(xiàn)GLSurfaceView.Renderer是運行中一個叫GLThread的線程中,它的作用和意義是什么,下一篇我們通過對GLSurfaceView的源碼分析來理解EGL和GLThread,了解完整的流程,然后不使用Render,采用自建管理EGL和創(chuàng)建GLThread,通過TextureView實現(xiàn)圖形的繪制。做到知其然,也知其所以然。
三、遇到的問題
1. A/libc: Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 in tid 9940 (GLThread 6703)_
發(fā)現(xiàn)是GLES20.glCreateProgram()時出現(xiàn)的上述崩潰
原因是因為沒有g(shù)lSView.setEGLContextClientVersion(3);
2. GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, status, 0)
Shader編譯后獲取狀態(tài)為0(失敗了)
原因是因為glsl語言注釋不對用成了 # 而不是 //
3. 無法正常的看到期望的圖片
這個就是開頭說的說的那個折騰了一個小時的問題,一個“逗號”引發(fā)的問題
具體如下:
float[] TRIANGLE_COORDS = {0.5f, 0.5f,1f, 0.5f,0.5f ///注意這里沒有加逗號,就是這個導(dǎo)致的,glsl認為到這里就結(jié)束了。。但是代碼上寫的是3個頂點,找不到就無法正常繪制。多么痛的領(lǐng)悟。
-0.5f, -0.5f,0.5f, 1f,0.5f,
0.5f, -0.5f,0.5f, 0.5f,1f
};
四、參考
《OpenGL ES應(yīng)用開發(fā)實踐指南》
《音視頻開發(fā)進階指南》
[搭建OpenGL ES環(huán)境的兩種方式]
[Android OpenGL ES(一)-開始描繪一個平面三角形]
[Android OpenGL ES(三)-平面圖形]
[EGL 環(huán)境搭建流程]
五、收獲
- 通過代碼實踐加深了對GLSL語法,OpenGL基本概念和繪制流程的熟悉。
- glsl程序編寫androidstudio等IDE插件的了解
- 理解實現(xiàn)了如何給著色器輸入數(shù)據(jù),又如何在屏幕上繪制。
- 繪制三角形、正方形、直線等平面圖形
- 遇到的問題分析解決,彌補認知不足。
下一篇我們來解析GLSurfaceView源碼&自己實現(xiàn)EGL管理與GLThread。歡迎關(guān)注公眾號“音視頻開發(fā)之旅”,一起學習成長。
歡迎交流