一.視圖組件 GLSurfaceView
Android上用于顯示OpenGL視圖,一般是使用GLSurfaceView,一個(gè)繼承自SurfaceView的組件。
它的渲染繪制在一個(gè)單獨(dú)的線程中,而非主線程。
GLSurfaceView一般是結(jié)合一個(gè)GLSurfaceView的內(nèi)部接口類Renderer來使用。Renderer類負(fù)責(zé)渲染圖形圖像,而GLSurfaceView負(fù)責(zé)觸摸事件等邏輯的處理。
Renderer接口
- onSurfaceCreated(GL10 gl, EGLConfig config):GLSurfaceView內(nèi)的Surface被創(chuàng)建時(shí)會(huì)被調(diào)用到
- onSurfaceChanged(GL10 gl, int width, int height):Surface尺寸改變時(shí)調(diào)用到
- onDrawFrame(GL10 gl):渲染繪制每一幀時(shí)調(diào)用到
所以一般情況下,首次創(chuàng)建GLSurfaceView時(shí),會(huì)按順序調(diào)用onSurfaceCreated、onSurfaceChanged、onDrawFrame這3個(gè)方法,然后每繪制一幀,都會(huì)不停地回調(diào)onDrawFrame方法。
GLSurfaceView常用方法
- setEGLContextClientVersion:設(shè)置OpenGL ES版本,2.0則設(shè)置2
- onPause:暫停渲染,最好是在Activity、Fragment的onPause方法內(nèi)調(diào)用,減少不必要的性能開銷,避免不必要的崩潰
- onResume:恢復(fù)渲染,用法類比onPause
- setRenderer:設(shè)置渲染器
- setRenderMode:設(shè)置渲染模式
- requestRender: 請求渲染,由于是請求異步線程進(jìn)行渲染,所以不是同步方法,調(diào)用后不會(huì)立刻就進(jìn)行渲染。渲染會(huì)回調(diào)到Renderer接口的onDrawFrame方法。
- queueEvent:插入一個(gè)Runnable任務(wù)到后臺渲染線程上執(zhí)行。相應(yīng)的,渲染線程中可以通過Activity的runOnUIThread的方法來傳遞事件給主線程去執(zhí)行
GLSurfaceView渲染模式
- RENDERMODE_CONTINUOUSLY:不停地渲染
- RENDERMODE_WHEN_DIRTY:只有調(diào)用了requestRender之后才會(huì)觸發(fā)渲染回調(diào)onDrawFrame方法
二.編程流程
- 編寫GLSL:重點(diǎn)學(xué)習(xí)
- 編譯GLSL,獲取OpenGL程序?qū)ο螅夯竟潭?,不需要死記,理解即可。后期?huì)進(jìn)行封裝,便于使用。
- 獲取GLSL中變量的引用:理解調(diào)用方式
- 通過內(nèi)存Buffer,將數(shù)據(jù)傳遞給變量引用,從而控制繪制圖形、顏色:重點(diǎn)學(xué)習(xí)
1. 簡單的GLSL
/**
* 頂點(diǎn)著色器
*/
private static final String VERTEX_SHADER = "" +
// vec4:4個(gè)分量的向量:x、y、z、w
"attribute vec4 a_Position;\n" +
"void main()\n" +
"{\n" +
// gl_Position:GL中默認(rèn)定義的輸出變量,決定了當(dāng)前頂點(diǎn)的最終位置
" gl_Position = a_Position;\n" +
// gl_PointSize:GL中默認(rèn)定義的輸出變量,決定了當(dāng)前頂點(diǎn)的大小
" gl_PointSize = 40.0;\n" +
"}";
/**
* 片段著色器
*/
private static final String FRAGMENT_SHADER = "" +
// 定義所有浮點(diǎn)數(shù)據(jù)類型的默認(rèn)精度;有l(wèi)owp、mediump、highp 三種,但只有部分硬件支持片段著色器使用highp。(頂點(diǎn)著色器默認(rèn)highp)
"precision mediump float;\n" +
"uniform mediump vec4 u_Color;\n" +
"void main()\n" +
"{\n" +
// gl_FragColor:GL中默認(rèn)定義的輸出變量,決定了當(dāng)前片段的最終顏色
" gl_FragColor = u_Color;\n" +
"}";
注意
在聲明vec向量的時(shí)候,一定要標(biāo)識其精度類型,否則會(huì)導(dǎo)致部分機(jī)型花屏,如紅米note2
2.1 編譯著色器
使用compileVertexShader、compileFragmentShader兩個(gè)方法分別調(diào)用上面定義的頂點(diǎn)著色器、片段著色器。
/**
* 編譯頂點(diǎn)著色器
*
* @param shaderCode 編譯代碼
* @return 著色器對象ID
*/
public static int compileVertexShader(String shaderCode) {
return compileShader(GLES20.GL_VERTEX_SHADER, shaderCode);
}
/**
* 編譯片段著色器
*
* @param shaderCode 編譯代碼
* @return 著色器對象ID
*/
public static int compileFragmentShader(String shaderCode) {
return compileShader(GLES20.GL_FRAGMENT_SHADER, shaderCode);
}
/**
* 編譯片段著色器
*
* @param type 著色器類型
* @param shaderCode 編譯代碼
* @return 著色器對象ID
*/
private static int compileShader(int type, String shaderCode) {
// 1.創(chuàng)建一個(gè)新的著色器對象
final int shaderObjectId = GLES20.glCreateShader(type);
// 2.獲取創(chuàng)建狀態(tài)
if (shaderObjectId == 0) {
// 在OpenGL中,都是通過整型值去作為OpenGL對象的引用。之后進(jìn)行操作的時(shí)候都是將這個(gè)整型值傳回給OpenGL進(jìn)行操作。
// 返回值0代表著創(chuàng)建對象失敗。
if (LoggerConfig.ON) {
Log.w(TAG, "Could not create new shader.");
}
return 0;
}
// 3.將著色器代碼上傳到著色器對象中
GLES20.glShaderSource(shaderObjectId, shaderCode);
// 4.編譯著色器對象
GLES20.glCompileShader(shaderObjectId);
// 5.獲取編譯狀態(tài):OpenGL將想要獲取的值放入長度為1的數(shù)組的首位
final int[] compileStatus = new int[1];
GLES20.glGetShaderiv(shaderObjectId, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
if (LoggerConfig.ON) {
// 打印編譯的著色器信息
Log.v(TAG, "Results of compiling source:" + "\n" + shaderCode + "\n:"
+ GLES20.glGetShaderInfoLog(shaderObjectId));
}
// 6.驗(yàn)證編譯狀態(tài)
if (compileStatus[0] == 0) {
// 如果編譯失敗,則刪除創(chuàng)建的著色器對象
GLES20.glDeleteShader(shaderObjectId);
if (LoggerConfig.ON) {
Log.w(TAG, "Compilation of shader failed.");
}
// 7.返回著色器對象:失敗,為0
return 0;
}
// 7.返回著色器對象:成功,非0
return shaderObjectId;
}
2.2 創(chuàng)建OpenGL程序?qū)ο?,鏈接頂點(diǎn)著色器、片段著色器
/**
* 創(chuàng)建OpenGL程序?qū)ο? *
* @param vertexShader 頂點(diǎn)著色器代碼
* @param fragmentShader 片段著色器代碼
*/
protected void makeProgram(String vertexShader, String fragmentShader) {
// 步驟1:編譯頂點(diǎn)著色器
int vertexShaderId = ShaderHelper.compileVertexShader(vertexShader);
// 步驟2:編譯片段著色器
int fragmentShaderId = ShaderHelper.compileFragmentShader(fragmentShader);
// 步驟3:將頂點(diǎn)著色器、片段著色器進(jìn)行鏈接,組裝成一個(gè)OpenGL程序
mProgram = ShaderHelper.linkProgram(vertexShaderId, fragmentShaderId);
if (LoggerConfig.ON) {
ShaderHelper.validateProgram(mProgram);
}
// 步驟4:通知OpenGL開始使用該程序
GLES20.glUseProgram(mProgram);
}
/**
* 創(chuàng)建OpenGL程序:通過鏈接頂點(diǎn)著色器、片段著色器
*
* @param vertexShaderId 頂點(diǎn)著色器ID
* @param fragmentShaderId 片段著色器ID
* @return OpenGL程序ID
*/
public static int linkProgram(int vertexShaderId, int fragmentShaderId) {
// 1.創(chuàng)建一個(gè)OpenGL程序?qū)ο? final int programObjectId = GLES20.glCreateProgram();
// 2.獲取創(chuàng)建狀態(tài)
if (programObjectId == 0) {
if (LoggerConfig.ON) {
Log.w(TAG, "Could not create new program");
}
return 0;
}
// 3.將頂點(diǎn)著色器依附到OpenGL程序?qū)ο? GLES20.glAttachShader(programObjectId, vertexShaderId);
// 3.將片段著色器依附到OpenGL程序?qū)ο? GLES20.glAttachShader(programObjectId, fragmentShaderId);
// 4.將兩個(gè)著色器鏈接到OpenGL程序?qū)ο? GLES20.glLinkProgram(programObjectId);
// 5.獲取鏈接狀態(tài):OpenGL將想要獲取的值放入長度為1的數(shù)組的首位
final int[] linkStatus = new int[1];
GLES20.glGetProgramiv(programObjectId, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (LoggerConfig.ON) {
// 打印鏈接信息
Log.v(TAG, "Results of linking program:\n"
+ GLES20.glGetProgramInfoLog(programObjectId));
}
// 6.驗(yàn)證鏈接狀態(tài)
if (linkStatus[0] == 0) {
// 鏈接失敗則刪除程序?qū)ο? GLES20.glDeleteProgram(programObjectId);
if (LoggerConfig.ON) {
Log.w(TAG, "Linking of program failed.");
}
// 7.返回程序?qū)ο螅菏?,?
return 0;
}
// 7.返回程序?qū)ο螅撼晒Γ?
return programObjectId;
}
/**
* 驗(yàn)證OpenGL程序?qū)ο鬆顟B(tài)
*
* @param programObjectId OpenGL程序ID
* @return 是否可用
*/
public static boolean validateProgram(int programObjectId) {
GLES20.glValidateProgram(programObjectId);
final int[] validateStatus = new int[1];
GLES20.glGetProgramiv(programObjectId, GLES20.GL_VALIDATE_STATUS, validateStatus, 0);
Log.v(TAG, "Results of validating program: " + validateStatus[0]
+ "\nLog:" + GLES20.glGetProgramInfoLog(programObjectId));
return validateStatus[0] != 0;
}
3. 獲取GLSL中的索引
根據(jù)索引的類型,調(diào)用不同的方法去獲取索引,索引的值類型都是int
// 獲取頂點(diǎn)坐標(biāo)屬性在OpenGL程序中的索引
aPositionLocation = GLES20.glGetAttribLocation(mProgram, A_POSITION);
// 獲取顏色Uniform在OpenGL程序中的索引
uColorLocation = GLES20.glGetUniformLocation(mProgram, U_COLOR);
4.1 將數(shù)據(jù)傳遞到Native層內(nèi)存緩沖中
/**
* Float類型占4Byte
*/
private static final int BYTES_PER_FLOAT = 4;
/**
* 創(chuàng)建一個(gè)FloatBuffer
*/
public static FloatBuffer createFloatBuffer(float[] array) {
FloatBuffer buffer = ByteBuffer
// 分配頂點(diǎn)坐標(biāo)分量個(gè)數(shù) * Float占的Byte位數(shù)
.allocateDirect(array.length * BYTES_PER_FLOAT)
// 按照本地字節(jié)序排序
.order(ByteOrder.nativeOrder())
// Byte類型轉(zhuǎn)Float類型
.asFloatBuffer();
// 將Java Dalvik的內(nèi)存數(shù)據(jù)復(fù)制到Native內(nèi)存中
buffer.put(array);
return buffer;
}
4.2 將內(nèi)存堆中的值傳遞給GLSL引用
接下來,我們把頂點(diǎn)信息傳遞給GLSL中的頂點(diǎn)位置引用
// 將緩沖區(qū)的指針移動(dòng)到頭部,保證數(shù)據(jù)是從最開始處讀取
mVertexData.position(0);
// 關(guān)聯(lián)頂點(diǎn)坐標(biāo)屬性和緩存數(shù)據(jù)
// 1. 位置索引;
// 2. 每個(gè)頂點(diǎn)屬性需要關(guān)聯(lián)的分量個(gè)數(shù)(必須為1、2、3或者4。初始值為4。);
// 3. 數(shù)據(jù)類型;
// 4. 指定當(dāng)被訪問時(shí),固定點(diǎn)數(shù)據(jù)值是否應(yīng)該被歸一化(GL_TRUE)或者直接轉(zhuǎn)換為固定點(diǎn)值(GL_FALSE)(只有使用整數(shù)數(shù)據(jù)時(shí))
// 5. 指定連續(xù)頂點(diǎn)屬性之間的偏移量。如果為0,那么頂點(diǎn)屬性會(huì)被理解為:它們是緊密排列在一起的。初始值為0。
// 6. 數(shù)據(jù)緩沖區(qū)
GLES20.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES20.GL_FLOAT,
false, 0, mVertexData);
// 通知GL程序使用指定的頂點(diǎn)屬性索引
GLES20.glEnableVertexAttribArray(aPositionLocation);
然后,我們給圖形上色
// 更新u_Color的值,即更新畫筆顏色
GLES20.glUniform4f(uColorLocation, 0.0f, 0.0f, 1.0f, 1.0f);
最后,再根據(jù)需求繪制不同的圖形。當(dāng)前案例中,我就只繪制一個(gè)點(diǎn)。
// 使用數(shù)組繪制圖形:1.繪制的圖形類型;2.從頂點(diǎn)數(shù)組讀取的起點(diǎn);3.從頂點(diǎn)數(shù)組讀取的數(shù)據(jù)長度
GLES20.glDrawArrays(GLES20.GL_POINTS, 0, 1);
注意:這里一定要先上色,再繪制圖形,否則會(huì)導(dǎo)致顏色在當(dāng)前這一幀使用失敗,要下一幀才能生效。
刷屏顏色
// 設(shè)置刷新屏幕時(shí)候使用的顏色值,順序是RGBA,值的范圍從0~1。這里不會(huì)立刻刷新,只有在GLES20.glClear調(diào)用時(shí)使用該顏色值才刷新。
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
// 使用glClearColor設(shè)置的顏色,刷新Surface
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
注意
Buffer數(shù)據(jù)在傳遞給GLSL之前,一定要調(diào)用position方法將指針移到正確的位置,當(dāng)前是0,之后會(huì)有課程講解到非0的情況。
// 將緩沖區(qū)的指針移動(dòng)到頭部,保證數(shù)據(jù)是從最開始處讀取
mVertexData.position(0);
將數(shù)組數(shù)據(jù)put進(jìn)buffer之后,指針并不是在首位,所以一定要position到0,至關(guān)重要!否則會(huì)有很多奇妙的錯(cuò)誤!如:
java.lang.ArrayIndexOutOfBoundsException: remaining() < count < needed
效果

參考
見Android OpenGL ES學(xué)習(xí)資料所列舉的博客、資料。
GitHub代碼工程
本系列課程所有相關(guān)代碼請參考我的GitHub項(xiàng)目GLStudio。
課程目錄
本系列課程目錄詳見 簡書 - Android OpenGL ES教程規(guī)劃