目錄
- 寫著色器代碼
- 通過(guò)GLSurfaceview加載Shader并運(yùn)行
- 遇到的問(wèn)題
- 參考
- 收獲
我們前兩篇介紹了OpenGL ES 基本概念和GLSL及Shader的渲染流程瑞眼,這篇我們開始實(shí)戰(zhàn),通過(guò)GLSurfaceView加載著色器帆啃,來(lái)繪制三角形勋又、正方形和直線這些平面圖形生蚁。在實(shí)踐過(guò)程中遇到的問(wèn)題有時(shí)候讓人沒(méi)有頭緒肋拔,檢查了一遍又一遍代碼氛魁,發(fā)現(xiàn)流程沒(méi)有問(wèn)題,但屏幕就是一片漆黑乖篷。响驴。通過(guò)近一個(gè)小時(shí)的排查,發(fā)現(xiàn)問(wèn)題出在了這里撕蔼。豁鲤。秽誊。下面開始我們今天的學(xué)習(xí)時(shí)間之旅,希望對(duì)你也有幫助琳骡。
GLSL著色器的編寫
如果對(duì)OpenGL的基本概念以及渲染流程不清晰的锅论,建議看下前兩篇文章,這些基本概念和流程要了解或者理解楣号,否則后面實(shí)踐之旅就是跳坑之旅最易。
我們先通過(guò)下面重要的二張圖,快速回顧下
工欲善其事炫狱,必先利其器耘纱,如何方便的編寫GLSL代碼吶? AndoridStudio提供了“Support for GLSL”插件毕荐。VS Code也有比較強(qiáng)大的插件比如“Shader Toy
”和“Shader languages support for VS Code”。但是都不足之處艳馒,就是沒(méi)有自動(dòng)提示和補(bǔ)全的功能憎亚。所以編寫GLSL代碼是要細(xì)心。
1.1 著色器代碼的編寫
首先我們來(lái)編寫下頂點(diǎn)著色器和片元著色器
//vertex_shader.glsl 頂點(diǎn)著色器
attribute vec4 a_Position;
attribute vec4 a_Color;
varying vec4 v_Color;
void main() {
v_Color = a_Color;
gl_Position = a_Position;
}
上述代碼簡(jiǎn)單語(yǔ)法回顧
attribute是修飾符 只能用于頂點(diǎn)著色器弄慰,用于修飾可變的參數(shù)
vec4: 浮點(diǎn)型向量
gl_Position:內(nèi)置變量
varying:也是一個(gè)修飾符第美,用于頂點(diǎn)著色器和片元著色器的值傳遞。
注意:必須要在頂點(diǎn)著色器和片元著色器都定義同名同類型同varying修飾符的變量陆爽,才能正常傳遞什往。
//fragment_shader.glsl 片元著色器
precision mediump float;
varying vec4 v_Color;
void main() {
gl_FragColor = v_Color;
}
這里對(duì)精度precision mediump 做下說(shuō)明
用于修飾浮點(diǎn)型和整形,有三個(gè)等級(jí) highp\mediump慌闭、lowp
1.2 著色器代碼的讀取
著色器代碼通常放在assets目錄下或者raw目錄下别威,當(dāng)然也見(jiàn)到過(guò)直接寫在代碼里。我們?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ù)類型(頂點(diǎn)著色器、片元著色器)創(chuàng)建著色器布讹,拿到著色器句柄
int shader = GLES20.glCreateShader(type);
Log.i(TAG, "compileShaderCode: type=" + type + " shaderId=" + shader);
if (shader > 0) {
//2. 設(shè)置著色器代碼 琳拭,shader句柄和code進(jìn)行綁定
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ù)著色器語(yǔ)言類型和代碼姑躲,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)建后會(huì)拿到對(duì)應(yīng)的句柄,通過(guò)檢查是否大于0驗(yàn)證是否可用
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, status, 0);著色器編譯后檢查編譯的狀態(tài)是否大于0判斷可用性盟蚣。
glGetProgramiv(programObjectId, GL_VALIDATE_STATUS,
validateStatus, 0);//程序鏈接后黍析,檢查程序的可用性
1.6 輸入與輸出
輸入:給著色器賦值
輸出:在屏幕上顯示
前面5個(gè)步驟把準(zhǔn)備工作都做好了,那邊現(xiàn)在面臨兩個(gè)問(wèn)題.
1. 我們看到頂點(diǎn)著色器中有兩個(gè)attribute修飾的變量屎开,如何給它們賦值?
2. 如何把著色器再屏幕上繪制出來(lái)阐枣?
這個(gè)環(huán)節(jié)我們就來(lái)解決這兩個(gè)問(wèn)題
首先定義好頂點(diǎn)坐標(biāo)和顏色的位數(shù)和著色器的數(shù)據(jù)
//每個(gè)頂點(diǎn)坐標(biāo)的個(gè)數(shù)
private final static int COORDS_PER_VERTEX = 2;
//每個(gè)頂點(diǎn)顏色的個(gè)數(shù)
private final static int COLOR_PER_VERTEX = 3;
// 浮點(diǎn)類型占用的字節(jié)數(shù)
private final static int BYTES_PER_FLOAT = 4;
//下面兩個(gè)字符串常量就是GLSL頂點(diǎn)著色器的輸入
private static final String A_POSITION = "a_Position";
private static final String A_COLOR = "a_Color";
//STRIDE是一個(gè)頂點(diǎn)的字節(jié)偏移(頂點(diǎn)坐標(biāo)xy+顏色rgb)
private final int STRIDE = (COORDS_PER_VERTEX+ COLOR_PER_VERTEX )* BYTES_PER_FLOAT;
private FloatBuffer mVertexData;
public MyRender2() {
//頂點(diǎn)數(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
};
//通過(guò)nio ByteBuffer把設(shè)置的頂點(diǎn)數(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ù)
}
然后給頂點(diǎn)著色器的變量賦值
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,//用幾個(gè)偏移描述一個(gè)頂點(diǎn)
GLES20.GL_FLOAT,//頂點(diǎn)數(shù)據(jù)類型
false,
STRIDE,//一個(gè)頂點(diǎn)需要多少個(gè)字節(jié)偏移
mVertexData//分配的buffer
);
//開啟頂點(diǎn)著色器的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說(shuō)明
獲取著色器attribute一個(gè)屬性:int aPosition = GLES20.glGetAttribLocation(programId, A_POSITION);_
數(shù)據(jù)的偏移:mVertexData.position(0); 因?yàn)橛凶鴺?biāo)和顏色兩個(gè)變量的值,數(shù)據(jù)又是根據(jù)頂點(diǎn)一一設(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ù)
)使能對(duì)應(yīng)的attribute屬性:GLES20.glEnableVertexAttribArray(aPosition);
通過(guò)上面幾個(gè)環(huán)節(jié)我們可以看到蔼两,我們可以看到,是如何給著色器語(yǔ)言中的變量賦值的逞度。下面我們看來(lái)下如何渲染额划。
我們采用GlSurfaceView,通過(guò)Render來(lái)實(shí)現(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中進(jìn)行不斷的 glDrawArrays來(lái)繪制刷新
public static native void glDrawArrays(
int mode, //點(diǎn)馆匿、線抑胎、三角形
int first,//頂點(diǎn)的第一個(gè)數(shù)據(jù)的index
int count//頂點(diǎn)的總數(shù)
);
二、實(shí)踐:用GLSurfaceView加載GLSL繪制屏幕圖形
2.1 三角形
上面的代碼中定義的就是三角形渐北,對(duì)應(yīng)頂點(diǎn)數(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
};
兩個(gè)坐標(biāo)位x&y,和三個(gè)顏色位rgb
效果如下
2.2 正方形
只需要修改 頂點(diǎn)數(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);
}
通過(guò)log分析阿逃,我們發(fā)現(xiàn)GLSurfaceView.Renderer是運(yùn)行中一個(gè)叫GLThread的線程中,它的作用和意義是什么赃蛛,下一篇我們通過(guò)對(duì)GLSurfaceView的源碼分析來(lái)理解EGL和GLThread恃锉,了解完整的流程,然后不使用Render焊虏,采用自建管理EGL和創(chuàng)建GLThread淡喜,通過(guò)TextureView實(shí)現(xiàn)圖形的繪制。做到知其然诵闭,也知其所以然炼团。
三、遇到的問(wè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()時(shí)出現(xiàn)的上述崩潰
原因是因?yàn)闆](méi)有g(shù)lSView.setEGLContextClientVersion(3);
2. GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, status, 0)
Shader編譯后獲取狀態(tài)為0(失敗了)
原因是因?yàn)間lsl語(yǔ)言注釋不對(duì)用成了 # 而不是 //
3. 無(wú)法正常的看到期望的圖片
這個(gè)就是開頭說(shuō)的說(shuō)的那個(gè)折騰了一個(gè)小時(shí)的問(wèn)題疏尿,一個(gè)“逗號(hào)”引發(fā)的問(wèn)題
具體如下:
float[] TRIANGLE_COORDS = {0.5f, 0.5f,1f, 0.5f,0.5f ///注意這里沒(méi)有加逗號(hào)瘟芝,就是這個(gè)導(dǎo)致的,glsl認(rèn)為到這里就結(jié)束了褥琐。锌俱。但是代碼上寫的是3個(gè)頂點(diǎn),找不到就無(wú)法正常繪制敌呈。多么痛的領(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ā)實(shí)踐指南》
《音視頻開發(fā)進(jìn)階指南》
[搭建OpenGL ES環(huán)境的兩種方式]
[Android OpenGL ES(一)-開始描繪一個(gè)平面三角形]
[Android OpenGL ES(三)-平面圖形]
[EGL 環(huán)境搭建流程]
五、收獲
- 通過(guò)代碼實(shí)踐加深了對(duì)GLSL語(yǔ)法吭练,OpenGL基本概念和繪制流程的熟悉诫龙。
- glsl程序編寫androidstudio等IDE插件的了解
- 理解實(shí)現(xiàn)了如何給著色器輸入數(shù)據(jù),又如何在屏幕上繪制鲫咽。
- 繪制三角形签赃、正方形、直線等平面圖形
- 遇到的問(wèn)題分析解決分尸,彌補(bǔ)認(rèn)知不足锦聊。
下一篇我們來(lái)解析GLSurfaceView源碼&自己實(shí)現(xiàn)EGL管理與GLThread。歡迎關(guān)注公眾號(hào)“音視頻開發(fā)之旅”箩绍,一起學(xué)習(xí)成長(zhǎng)孔庭。
歡迎交流