上一篇文章具體參考上文:
Android上Java程序和Opengl通信方式和opengl es着色器
着色器原理:
我們之前多次介紹過OpenGL裏面圖形都是通過頂點着色器和片段着色器共同完成的,頂點着色器計算每個頂點在屏幕上的最終位置,OpenGL把這些頂點組裝成點,直線,三角形並且分解成片段,會詢問片段着色器每個片段的最終顏色,如果沒有頂點着色器OpenGL就不知道在哪繪製圖形,如果沒有片段着色器就不知道要怎麼繪製組成圖形的點,直線,三角形的片段,所以他們總是一起工作的,最終一起合成屏幕上的一幅圖像。
上文中我們已經編寫了兩個着色器,simple_vertex_shader.glsl和simple_fragment_shader.glsl,本文將具體講解如何編譯和使用。
加載着色器:
打開TextResourceReader類代碼如下:
/***
* Excerpted from "OpenGL ES for Android",
* published by The Pragmatic Bookshelf.
* Copyrights apply to this code. It may not be used to create training material,
* courses, books, articles, and the like. Contact us if you are in doubt.
* We make no guarantees that this code is fit for any purpose.
* Visit http://www.pragmaticprogrammer.com/titles/kbogla for more book information.
***/
package opengl.timothy.net.openglesproject_lesson2.util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import android.content.Context;
import android.content.res.Resources;
public class TextResourceReader {
/**
* Reads in text from a resource file and returns a String containing the
* text.
*/
public static String readTextFileFromResource(Context context,
int resourceId) {
StringBuilder body = new StringBuilder();
try {
InputStream inputStream =
context.getResources().openRawResource(resourceId);
InputStreamReader inputStreamReader =
new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String nextLine;
while ((nextLine = bufferedReader.readLine()) != null) {
body.append(nextLine);
body.append('\n');
}
} catch (IOException e) {
throw new RuntimeException(
"Could not open resource: " + resourceId, e);
} catch (Resources.NotFoundException nfe) {
throw new RuntimeException("Resource not found: " + resourceId, nfe);
}
return body.toString();
}
}
readTextFileFromResource方法需要傳入資源id,方法返回值爲String類型,通過該方法就加載了raw裏面定義的兩種着色器代碼。
編譯着色器:
目的是通過加載着色器代碼,返回一個代表着色器的對象。
在ShaderHelper類中
/**
* Compiles a shader, returning the OpenGL object ID.
*/
private static int compileShader(int type, String shaderCode) {
// Create a new shader object.
final int shaderObjectId = glCreateShader(type);
if (shaderObjectId == 0) {
if (LoggerConfig.ON) {
Log.w(TAG, "Could not create new shader.");
}
return 0;
}
// Pass in the shader source.
glShaderSource(shaderObjectId, shaderCode);
// Compile the shader.
glCompileShader(shaderObjectId);
// Get the compilation status.
final int[] compileStatus = new int[1];
glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0);
if (LoggerConfig.ON) {
// Print the shader info log to the Android log output.
Log.v(TAG, "Results of compiling source:" + "\n" + shaderCode + "\n:"
+ glGetShaderInfoLog(shaderObjectId));
}
// Verify the compile status.
if (compileStatus[0] == 0) {
// If it failed, delete the shader object.
glDeleteShader(shaderObjectId);
if (LoggerConfig.ON) {
Log.w(TAG, "Compilation of shader failed.");
}
return 0;
}
// Return the shader object ID.
return shaderObjectId;
}
第一個參數如果是頂點着色器則傳入android.opengl.GLES20.GL_VERTEX_SHADER;片段着色器則是android.opengl.GLES20.GL_FRAGMENT_SHADER;第二個參數是之前的加載着色器返回的String。具體過程:
1.創建着色器對象GLES20.glCreateShader(type);
2. 傳入着色器代碼 GLES20.glShaderSource(shaderObjectId, shaderCode);
3. 編譯着色器glCompileShader(shaderObjectId);
4. 獲取編譯狀態 // Get the compilation status.
final int[] compileStatus = new int[1];
glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0);
可以通過 glGetShaderInfoLog(shaderObjectId)打印日誌信息
同時if (compileStatus[0] == 0) 則表示編譯失敗,則刪除着色器並且返回0,即:
if (compileStatus[0] == 0) {
// If it failed, delete the shader object.
glDeleteShader(shaderObjectId);
if (LoggerConfig.ON) {
Log.w(TAG, "Compilation of shader failed.");
}
return 0;
}
成功則返回當前着色器的id即shaderObjectId.
在AirHockeyRenderer裏面的onSurfaceCreated裏面可以看到有對以上過程調用,最終得到
int vertexShader = ShaderHelper.compileVertexShader(vertexShaderSource);
int fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderSource);
頂點着色器和片段着色器兩個對象vertexShader 和fragmentShader 。
鏈接程序:
program = ShaderHelper.linkProgram(vertexShader, fragmentShader);
我們已經知道頂點着色器和片段着色器是需要一起使用的,使用需要連接成一個對象,具體ShaderHelper.linkProgram代碼如下:
/**
* Links a vertex shader and a fragment shader together into an OpenGL
* program. Returns the OpenGL program object ID, or 0 if linking failed.
*/
public static int linkProgram(int vertexShaderId, int fragmentShaderId) {
// Create a new program object.
final int programObjectId = glCreateProgram();
if (programObjectId == 0) {
if (LoggerConfig.ON) {
Log.w(TAG, "Could not create new program");
}
return 0;
}
// Attach the vertex shader to the program.
glAttachShader(programObjectId, vertexShaderId);
// Attach the fragment shader to the program.
glAttachShader(programObjectId, fragmentShaderId);
// Link the two shaders together into a program.
glLinkProgram(programObjectId);
// Get the link status.
final int[] linkStatus = new int[1];
glGetProgramiv(programObjectId, GL_LINK_STATUS, linkStatus, 0);
if (LoggerConfig.ON) {
// Print the program info log to the Android log output.
Log.v(TAG, "Results of linking program:\n"
+ glGetProgramInfoLog(programObjectId));
}
// Verify the link status.
if (linkStatus[0] == 0) {
// If it failed, delete the program object.
glDeleteProgram(programObjectId);
if (LoggerConfig.ON) {
Log.w(TAG, "Linking of program failed.");
}
return 0;
}
// Return the program object ID.
return programObjectId;
}
首先創建程序對象 GLES20.glCreateProgram(),GLES20.glAttachShader方法將頂點着色器和片段着色器添加到程序對象上,最後 GLES20.glLinkProgram(programObjectId)連接對象,如果成功則返回programObjectId否則刪除 GLES20.glDeleteProgram(programObjectId).
回答AirHockeyRenderer的onSurfaceCreated方法,在linkProgram之後 ShaderHelper.validateProgram(program);這個是來驗證程序對象的,是否存在一些問題,打印日誌。
最後, GLES20.glUseProgram(program);是告訴opengl任何時候繪製東西到屏幕都需要使用這裏的程序對象。
取值:
在simple_fragment_shader.glsl和simple_vertex_shader.glsl中我們定義了類型爲attribute(屬性)的a_Position和類型爲uniform的u_Color,通過 uColorLocation = GLES20.glGetUniformLocation(program, U_COLOR);
aPositionLocation = GLES20.glGetAttribLocation(program, A_POSITION);
這兩個方法我們就可以告訴opengl在哪裏讀取這兩個屬性的值,相當於是告訴opengl這兩個屬性的值的位置,並且將位置信息存入uColorLocation和aPositionLocation。
關聯屬性和緩衝區:
vertexData.position(0);
glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GL_FLOAT,
false, 0, vertexData);
這個方法是告訴opengl從之前定義的頂點緩衝區vertexData裏找到a_Position對應的數據,同時由於每個緩衝區是本地一塊內存,爲了保證是從內存起始位置讀取,所以調用之前需要先將緩衝區指針移動到最初位置即vertexData.position(0)。glVertexAttribPointer參數含義如下表:
glVertexAttribPointer方法各個參數含義
glVertexAttribPointer參數不正確的話可能導致一些問題。同時有了該方法,opengl就知道去哪裏讀取屬性a_Position的值了。最後調用 glEnableVertexAttribArray(aPositionLocation);使這一切有效。
截止目前,我們已經完成了頂點着色器的功能,找到了繪製點,直線,三角形的位置信息即頂點緩衝區的值。接下來具體怎麼繪製?就是片段着色器的事了。
片段着色器繪製過程:
在AirHockeyRenderer的onDrawFrame方法裏面, glUniform4f(uColorLocation, 1.0f, 1.0f, 1.0f, 1.0f); 方法是用來更新着色器裏面的u_Color的值,同時uniform沒有默認值,如果uniform是vec4類型的則需要提供所有4個分量的值即紅,綠,藍和透明度,本文指定的4個分量值爲1.0f, 1.0f, 1.0f, 1.0f即白色不透明。
glDrawArrays(GL_TRIANGLES, 0, 6);就是具體繪製了,第一個參數GL_TRIANGLES指的是繪製三角形,第二個參數告訴opengl從浮點數組tableVerticesWithTriangles的開頭位置讀取頂點,第三個參數是讀取6個頂點,一個三角形3頂點,讀取6個的話就是2個三角形即一個長方形桌子。當我們在之前調用glVertexAttribPointer的時候第二個參數表示每個頂點的分量,比如是2維座標就是2,三維座標就是3等等,本例子POSITION_COMPONENT_COUNT值爲2則一共是讀取浮點數組tableVerticesWithTriangles的前12個值,即
// Triangle 1
0f, 0f,
9f, 14f,
0f, 14f,
// Triangle 2
0f, 0f,
9f, 0f,
9f, 14f
接下來就是 // Draw the center dividing line.
glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
glDrawArrays(GL_LINES, 6, 2); 就是花了一條紅色的線,是從第6個頂點後在讀入兩個頂點畫成一條線GL_LINES,即
// Line 1
0f, 7f,
9f, 7f,
同理後面代碼是繪製出兩個木槌:
// Draw the first mallet blue.
glUniform4f(uColorLocation, 0.0f, 0.0f, 1.0f, 1.0f);
glDrawArrays(GL_POINTS, 8, 1);
// Draw the second mallet red.
glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
glDrawArrays(GL_POINTS, 9, 1);
即座標 tableVerticesWithTriangles裏面座標值
// Mallets
4.5f, 2f,
4.5f, 12f。
在simple_vertex_shader.glsl裏面有gl_PointSize = 10.0;其實就是給點(木槌)的繪製設置大小。下一講我們繼續深入顏色和着色。
本文代碼地址:
https://github.com/pangrui201/OpenGlesProject/tree/master/OpenGlesProject_lesson2