您的位置:首页 > 移动开发 > Android开发

OpenGL.ES在Android上的简单实践:2-曲棍球(编译着色器及屏幕上绘图)

2018-01-17 15:59 417 查看

OpenGL.ES在Android上的简单实践:2-曲棍球(编译着色器及屏幕上绘图)

本文会继续上一篇开始的工作。我们首先加载并编译前面定义的着色器,然后把它们链接在一起放在OpenGL的一个程序里。我们接下来就可以用着色器程序在屏幕上绘制曲棍球桌子了。

1、加载着色器。

既然我们已经准备好了顶点着色器和片段着色器,我们就需要把它们从res/raw/shader.glsl 加载到内存。
我们在工程目录下添加一个utils的工具包类目录,并添加 TextResourceReader 的新类。在类中加入如下代码:
/**
* 从原生文件读入glsl
* @param context
* @param resourceId
* @return
*/
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 (Exception e) {
e.printStackTrace();
}
return body.toString();
}我们在HockeyRenderer添加一个构造函数,并在构造中读入着色器的代码。
public HockeyRenderer(Context context) {

String vertexShaderSource = TextResourceReader.readTextFileFromResource(context, R.raw.simple_vertex_shader);
String fragmentShaderSource = TextResourceReader.readTextFileFromResource(context, R.raw.simple_fragment_shader);
... ...
}
现在我们已经把着色器源代码从文件中读出来了,下一步就是编译每个着色器了。我们要创建一个新的辅助类,它可以创建新的OpenGL着色器对象、编译着色器代码并且返回代表那段着色器代码的着色器对象。一旦写出样板代码,在未来的项目中就可以重用了。

2、编译着色器

作为开始,创建一个名为ShaderHelper的新类,并在类中添加如下代码:
public class ShaderHelper {
private static final String TAG = "ShaderHelper";

public static int compileVertexShader(String shaderCode){
return compileShader(GL_VERTEX_SHADER, shaderCode);
}

public static int compileFragmentShader(String shaderCode){
return compileShader(GL_FRAGMENT_SHADER, shaderCode);
}

private static int compileShader(int type, String shaderCode){
final int shaderObjectId = glCreateShader(type);
if (shaderObjectId == 0){
if(LoggerConfig.ON){
Log.w(TAG,"Warning! Could not create new shader, glGetError:"+glGetError());
}
return 0;
}
glShaderSource(shaderObjectId, shaderCode);

glCompileShader(shaderObjectId);

final int[] compileStatus = new int[1];
glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0);

if(LoggerConfig.ON){
Log.i(TAG, "Result of compiling source:"+"\n"+shaderCode+"\n"
+ glGetShaderInfoLog(shaderObjectId));
}

if(compileStatus[0] == 0){
glDeleteShader(shaderObjectId);
if(LoggerConfig.ON){
Log.w(TAG,"Warning! Compilation of shader failed, glGetError:"+glGetError());
}
return 0;
}
return shaderObjectId;
}

... ...
}我们来分析一下这部分模板代码。
        首先我们调用glCreateShader()创建了一个新的着色器对象,并把这个对象的ID存入变量shaderObjectId。这个type可以是代表顶点着色器的GL_VERTEX_SHADER,或者是代表片段着色器的GL_FRAGMENT_SHADER。剩下代码也同样的方式。
        记住我们是如何创建对象并检查它是否有效的;这个模式将在OpenGL里广泛使用:
        1、首先使用一个如glCreateShader()一样的调用创建一个对象,这个调用会返回一个整型值,代表这个对象。
        2、这个整型值就是OpenGL对象的引用。无论后面什么时候想要引用这个对象,就要把这个整型值传回OpenGL。
        3、返回值0表示这个对象创建失败,它类似于Java代码中返回null值。
        如果对象创建失败,就给调用代码返回0。为什么返回0而不是抛出一个异常呢?这是因为OpenGL内部实际不会抛出任何异常;相反,我们会得到返回值0,并且OpenGL通过glGetError()告诉我们这个错误,这个方法可以让我们询问OpenGL是不是某个API调用导致了错误。这个惯例会一直遵从贯彻整个Android工程。
        然后我们上传和编译着色器源代码,一旦有了有效的着色器对象,就可以调用glShaderSource(shaderObjectId, shaderCode); 上传源代码了。这个调用告诉OpenGL读入字符串shaderCode定义的源代码,并把它与shaderObjectId所引用的着色器对象关联在一起。然后,可以调用 glCompileShader(shaderObjectId) 编译这个着色器。
        接着取出编译状态及着色器信息日志,为了检查编译是失败还是成功,我们可以调用glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0)。当我们获得编译状态的时候,OpenGL只给出一个简单的是或否的回答。难道没兴趣知道发生了什么错误以及哪里出问题了吗?事实证明,我们可以通过调用glGetShaderInfoLog(shaderObjectId) 获得一个可读的信息。
        验证编译状态并返回着色器对象ID,既然我们已经记录了着色器信息日志,就可以查看一下编译是否成功了。我们所需要做的就是检查上面那步的返回值,它是不是0。如果是0,编译就失败了。这种情况下,我们就不再需要着色器对象了,因此告诉OpenGL把它删除并返回0;如果编译成功,着色器对象就是有效了,我们就可以返回给调用代码使用。
        至此,我们完成了编译着色器。

3、链接着色器到OpenGL的程序

        既然我们已经加载并编译了 一个顶点着色器 和一个片段着色器,下一步就是把他们绑定到程序(program)里。简单来说,一个OpenGL程序(program)就是把一个顶点着色器和片段着色器链接在一起变成单个对象。顶点着色器和片段着色器总是一起工作的,没有片段着色器,OpenGL就不知道怎么绘制那些组成每个点、直线、三角形的片段;没有顶点着色器,OpenGL就不知道在哪里绘制这些片段。
        虽然顶点着色器和片段着色器总是要一起工作,但并不意味着它们必须是一对一匹配的,它们是可以共用的;同一顶点的着色器可以和不同的片段着色器工作,效果也不尽相同。
        让我们在ShaderHelper增加如下代码,并比较以上的模板代码,逐一分析之。
public static int linkProgram(int vertexShaderId, int fragmentShaderId){
final int programObjectId = glCreateProgram();
if (programObjectId == 0){
if(LoggerConfig.ON){
Log.w(TAG," Warning! Could not create new program, glGetError:"+glGetError());
}
return 0;
}

glAttachShader(programObjectId, vertexShaderId);
glAttachShader(programObjectId, fragmentShaderId);

glLinkProgram(programObjectId);

final int[] linkStatus = new int[1];
glGetProgramiv(programObjectId, GL_LINK_STATUS, linkStatus, 0);

if(LoggerConfig.ON){
Log.i(TAG, "Result of linking program:"
+ glGetProgramInfoLog(programObjectId));
}

if(linkStatus[0] == 0){
glDeleteProgram(programObjectId);
if(LoggerConfig.ON){
Log.w(TAG," Warning! Linking of program failed, glGetError:"+glGetError());
}
return 0;
}

return programObjectId;
}参照之前的模板代码,我想这段代码不能理解了。
        首先我们就是调用glCreateProgram() 新建程序对象,用programObjectId记录这个程序对象的ID。
        下一步就是使用glAttachShader附上顶点着色器 和 片段着色器。
        紧接着调用glLinkProgram把两个着色器链接起来,以及调用glGetProgramiv检查程序的信息日志。
        最后验证链接状态并返回程序对象ID到调用者。至此,着色器程序已经链接顶点和片段着色器了。

到最后,我们再写一个buildProgram 把以上模板代码组织起来。
public static int buildProgram(String vertexShaderSource, String fragmentShaderSource){
int programObjectId;

int vertexShader = compileVertexShader(vertexShaderSource);
int fragmentShader = compileFragmentShader(fragmentShaderSource);

programObjectId = linkProgram(vertexShader,fragmentShader);

if(LoggerConfig.ON){
validateProgram(programObjectId);
}

return programObjectId;
}两个String参数vertexShaderSource、fragmentShaderSource就是使用TextResourceReader.readTextFileFromResource从res/raw读取的shader源代码。   
上面还有一个validateProgram没有讲解,在开始使用OpenGL的程序之前,我们应该验证一下它,看看这个程序对于当前的OpenGL状态是不是有效的。

public static boolean validateProgram(int programObjectId){
glValidateProgram(programObjectId);

final int[] validateStatus = new int[1];
glGetProgramiv(programObjectId, GL_VALIDATE_STATUS, validateStatus, 0);

if(LoggerConfig.ON){
Log.i(TAG, "Result of validating program:"+validateStatus[0]
+"\nLog:"+ glGetProgramInfoLog(programObjectId));
}

return validateStatus[0]!= GLES20.GL_FALSE ;
}我们调用glValidateProgram来验证这个程序,然后用GL_VALIDATE_STATUS作为参赛调用glGetProgramiv方法检查其结果。如果OpenGL有什么有用的信息要透露,这些信息会在程序日志里,因此我们使用glGetProgramInfoLog把日志信息打印出来。

4、在GLSurfaceview上使用

以上的代码我们要怎样组织起来呢?初学者可以秉持着一个原则,GLES20.*的接口必须只能在GLSurfaceView.Renderer的接口上调用。所以我们在 onSurfaceCreated 接口添加如下代码:
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
int programId = ShaderHelper.buildProgram(vertexShaderSource, fragmentShaderSource);
GLES20.glUseProgram(programId);
}调用glUseProgram告诉OpenGL在绘制任何东西到屏幕上的时候要使用这里定义的程序对象。
好了,我们来总结一下:
        1、我们现在已经把顶点数据加载到本地内存了。
        2、我们也建立OpenGL管道了。
下一步我们要通过管道操作着色器链接数据了。

5、操作着色器链接数据

下一步是获得我们早前在着色器定义的attribute和uniform等属性。当OpenGL把着色器链接成一个程序的时候,它实际上为每个自定义的属性附加了一个位置编号。这些位置编号用来给着色器发送数据的时候使用的。现在我们回来看看之前定义的两个着色器:
// 顶点着色器
attribute vec4 a_Position;
void main()
{
gl_Position = a_Position;
}

// 片段着色器
precision mediump float;
uniform vec4 u_Color;
void main()
{
    gl_FragColor = u_Color;
}
我们需要获取a_Position的位置来传送顶点数据,获取u_Color的位置传送颜色值。怎么获取呢?看看以下模板代码:(记住GLES20.* 必须在Renderer的接口里面调用)
private static final String U_COLOR = "u_Color";
private int uColorLocation;
private static final String A_POSITION = "a_Position";
private int aPositionLocation;

@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
int programId = ShaderHelper.buildProgram(vertexShaderSource, fragmentShaderSource);
GLES20.glUseProgram(programId);
uColorLocation = GLES20.glGetUniformLocation(programId, U_COLOR);
aPositionLocation = GLES20.glGetAttribLocation(programId, A_POSITION);
}
我们为自定义attribute/uniform的属性名字创建一个常量和一个用来保存位置编号的变量。attribute/uniform的位置并不是事先指定的,因此,一旦程序链接成功了,我们就要查询这个位置。一个attribute/uniform的位置在一个程序对象中是唯一的,即使在两个不同的程序中使用了相同的attribute/uniform名字,也不意味着它们使用相同的位置。我们调用GLES20.glGetAttribLocation / GLES20.glGetUniformLocation 分别获取attribute/uniform的位置,并把这个位置存入位置变量aPositionLocation / uColorLocation,接下来我们就可以使用它了。

还记得我们之前已经加载本地内存的桌子顶点数据吗?下一步是要告诉OpenGL到哪里找到属性a_Position对应的数据了。(注意这句话的描述,不是传数据到a_Position,是让OpenGL找。) 我们在onSurfaceCreated中添加如下代码:
vertexData.position(0);
GLES20.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT,
GLES20.GL_FLOAT, false, 0, vertexData);
我们调用GLES20.glVertexAttribPointer告诉OpenGL,它可以在缓冲区vertexData中找到a_Position对应的数据。这是一个很重要的函数,因此这里有必要仔细看一下每个参数的意义。



调用了glVertexAttribPointer之后,OpenGL就知道在哪里读取属性a_Position的数据了。但是!尽管我们已经把数据属性链接起来了,在开始绘制之前,我们还需要调用glEnableVertexAttribArray使能顶点数据。完整代码如下:
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
int programId = ShaderHelper.buildProgram(vertexShaderSource, fragmentShaderSource);
GLES20.glUseProgram(programId);
uColorLocation = GLES20.glGetUniformLocation(programId, U_COLOR);
aPositionLocation = GLES20.glGetAttribLocation(programId, A_POSITION);

vertexData.position(0);
GLES20.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT,
GLES20.GL_FLOAT, false, 0, vertexData);
GLES20.glEnableVertexAttribArray(aPositionLocation);
}
好了,现在来小结
        1、我们现在已经把顶点数据加载到本地内存了。
        2、我们也建立OpenGL管道了。(建立程序,链接 顶点/片段着色器)
        3、通过管道,操作着色器,告诉对应数据在哪里。
剩下最后一步了,在屏幕上绘制(光栅化图元)

6、在屏幕上绘制

随着完成以上最后的连接,我们现在算是准备好开始在屏幕上绘制了!我们先回顾之前定义的结构 点数据 和 示意图:
float[] tableVerticesWithTriangles = {
// 第一个三角形
0f, 0f,
9f, 14f,
0f, 14f,
// 第二个三角形
0f, 0f,
9f, 0f,
9f, 14f
// 中间的分界线
0f, 7f,
9f, 7f,
// 两个摇杆的质点位置
4.5f, 2f,
4.5f, 12f
};



好了,我们在onDrawFrame处,添加如下代码:
@Override
public void onDrawFrame(GL10 gl10) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

GLES20.glUniform4f(uColorLocation, 1.0f,1.0f,1.0f,1.0f);
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6);
... ...
}
我们首先通过调用glUniform4f更新着色器代码中的u_Color的值,与attribute属性不同,uniform的分量没有默认值,因此,如果一个uniform在着色器中被定义vec4类型,我们需要提供所有四个分量的值。我们想要一张白色桌子作为开始,所有把红、绿、蓝的色值设置为代表完全亮度的1.0f值,透明度阿尔法无关紧要,但是还是要指定值的。
一旦指定了颜色,接下来就可以用 GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6);   第一个参数告诉OpenGL我们要画三角形; 第二个参数0,告诉OpenGL从顶点数组(tableVerticesWithTriangles )的开头处开始读顶点;   第三个参数6,告诉OpenGL读入六个顶点。   因为每个三角形有三个顶点,所以调用最终会画出两个三角形。
之前我们调用glVertexAttribPointer方法,记得第二个参数告诉OpenGL每个顶点的位置包含2个浮点数分量。glDrawArrays调用让OpenGL使用前六个顶点绘制三角形, 第一个被绘制的三角形由点(0,0)(9,14)(0,14)围成,而第二个由(0,0)(9,0)(9,14)围成。
下一步是绘制跨越桌子中间的中心分割线。继续添加代码:
... ...
GLES20.glUniform4f(uColorLocation, 1.0f,0.0f,0.0f,1.0f);
GLES20.glDrawArrays(GLES20.GL_LINES, 6, 2);
这次我们定义u_Color为红色,这次画的是线GLES20.GL_LINES,从顶点数据的第6组开始,每组也是2个元素。因此,这条线是从(0,7)到(9,7)画一条线。

接着就是画木槌位置的两个点了。添加代码:
... ...
GLES20.glUniform4f(uColorLocation, 0.0f,0.0f,1.0f, 1.0f);
GLES20.glDrawArrays(GLES20.GL_POINTS, 8, 1);
GLES20.glUniform4f(uColorLocation, 0.0f,1.0f,0.0f, 1.0f);
GLES20.glDrawArrays(GLES20.GL_POINTS, 9, 1);
通过传递GL_POINTS给glDrawArrays方法,让OpenGL绘制点。对于第一个木槌,我们设置其颜色为蓝色,从偏移位置8开始,并用一个顶点绘制一个点,对于第二个木槌,设置其颜色为绿色,从偏移位置9开始,并用一个顶点绘制一个点。

好了,试着把程序跑起来吧。发生什么奇怪的问题不要怪我哦。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: