上一篇文章具体参考上文:
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