4.为着色器程序添加类
我们会为纹理着色器创建一个类,并颜色器程序创建另一个类:我们会用纹理着色器绘制桌子,用颜色着色器绘制木槌。我们也会创建一个基类作为它们的公共函数。我们不用再担心那条直线,因为它是纹理的一部分。
我们开始给ShaderHelper加入一个辅助函数,打开博文第三篇的类,在其尾部加入如下方法:
public static int buildProgram(String vertexShaderSource,String fragmentShaderSource){ int program; int vertexShader=compileVertexShader(vertexShaderSource); int fragmentShader=compileFragmentShader(fragmentShaderSource); program=linkProgram(vertexShader,fragmentShader); validateProgram(program); return program; }
这个辅助函数会编译vertexShaderSource和fragmentShaderSource定义的着色器,并把它们链接在一起成为一个程序。我们会使用这个辅助函数组成我们的基类。
创建一个名为programs的包,并在包中创建一个名为ShaderProgram的新类,加入如下代码:
protected static final String U_MATRIX="u_Matrix"; protected static final StringU_TEXTURE_UNIT="u_TextureUnit"; protected static final StringA_POSITION="a_Position"; protected static final StringA_COLOR="a_Color"; protected static final StringA_TEXTURE_COORDINATES="a_TextureCoordinates"; protected final int program; protected ShaderProgram(Context context,int vertexShaderResourceId,int fragmentShaderReourceId){ this.program=ShaderHelper.buildProgram( TextResourceReader.readTextFileFromResource(context,vertexShaderResourceId), TextResourceReader.readTextFileFromResource(context,fragmentShaderReourceId)); } public void useProgram(){ GLES20.glUseProgram(); }
我们通过定义一些公用的常量作为这个类的开始,在构造函数中,我们调用刚刚定义过的辅助函数,其使用是指定的着色器构建了一个OpenGL着色器程序。我们用useProgram()作为结束,其调用glUseProgram()告诉OpenGL接下来的渲染要使用这个程序。
加入纹理着色器程序
我们现在将定义一个类来建立和表示纹理着色器程序。
创建一个名为TextureShaderProgram的新类,其继承自ShaderProgram,并在该类内部加入如下代码:
private final int uMatrixLocation; private final int uTextureUnitLocation; private final int aPositionLocation; private final int aTextureCoordinatesLocation;
我们加入了四个整型用来保存那些uniform和属性的位置。
下一步是初始化着色器程序,创建用于初始化着色器程序的构造函数,代码如下:
public TextureShaderProgram(Context context){ super(context,R.raw.texture_vertex_shader,R.raw.texture_fragment_shader); this.uMatrixLocation=GLES20.glGetUniformLocation(program,U_MATRIX); this.uTextureUnitLocation=GLES20.glGetUniformLocation(program,U_TEXTURE_UNIT); this.aPositionLocation=GLES20.glGetAttribLocation(program,A_POSITION); this.aTextureCoordinatesLocation=GLES20.glGetAttribLocation(program,A_TEXTURE_COORDINATES); }
这个构造函数会用我们选择的资源调用其父类的构造函数,其父类会构造着色器程序。我们读入并保存那些uniform和属性的位置。
设置uniform并返回属性的位置
传递矩阵和纹理给它们的uniform。加入如下代码:
public void setUniforms(float[] matrix,int textureId){ GLES20.glUniformMatrix4fv(this.uMatrixLocation,1,false,matrix,0); GLES20.glActiveTexture(GLES20.GL_TEXTURE_2D); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,textureId); GLES20.glUniformli(this.uTextureUnitLocation,0); }
第一步是传递矩阵给它的uniform,这足够简单明了。下一部分就是需要更多的解释了。当我们在OpenGL里使用纹理进行绘制时,我们不需要直接给着色器传递纹理。相反,我们使用纹理单元保存那个纹理。之所以这样做,是因为一个GPU只能同时绘制数量有限的纹理。它使用这些纹理表示当前正在被绘制的活动的纹理。
如果需要切换纹理,我们可以在纹理单元中来回切换纹理,但是,如果我们切换得太频繁,可能会渲染的速度。也可以同时用几个纹理单元绘制多个纹理。
通过调用glActiveTexture()把活动的纹理单元设置成为纹理单元0,我们以此开始,然后通过调用glBindTexture()把这个纹理绑定到这个单元,接着,通过调用glUniformli()把被选定的纹理单元传递给片段着色器中的u_TextureUnit。
我们几乎已经完成了这个纹理器类;只需要一种方法来获取属性的位置,以便可以把它们绑定到正确的顶点数组数据矩阵。加入如下代码完成这个类:
public int getPositionLocation(){ return this.aPositionLocation; } public int getTextureLocation(){ return this.aTextureCoordinatesLocation; }
加入颜色着色器程序
在同一个包中创建另一个类,命名为ColorShaderProgram。这个类应该也继承自ShaderProgram,它也遵循与TextureShaderProgram一样的模式:有一个构造函数,一个设置uniform的方法和获取属性位置的方法。在此类内部加入如下代码:
private final int uMatrixLocation; private final int aPositionLocation; private final int aColorLocation; public ColorShaderProgram(Context context){ super(context,R.raw.simple_vertex_shader,R.raw.simple_fragment_shader); this.uMatrixLocation=GLES20.glGetUniformLocation(program,U_MATRIX); this.aPositionLocation=GLES20.glGetAttribLocation(program,A_POSITION); this.aColorLocation=GLES20.glGetAttribLocation(program,A_COLOR); } public void setUniform(float[] matrix){ GLES20.glUniformMatrix4fv(this.uMatrixLocation,1,false,matrix,0); } public int getPositionLocation(){ return this.aPositionLocation; } public int getColorLocation(){ return this.aColorLocation; }
我们会使用这个项目绘制木槌。
通过把这些着色器程序与这些程序要绘制的数据进行解耦,就很容易重用这些代码了。比如,我们可以通过这个颜色着色器程序用一种颜色属性绘制任何物体,而不仅仅是木槌。
5.绘制纹理
既然我们已经把顶点数据和着色器程序分别放于不同的类中了,现在就可以更新渲染类,使用纹理进行绘制了。打开LYJRenderer,删掉所有第三篇该类下面的代码,只保留onSurfaceChanged(),这是我们唯一不会改变的。加入如下成员变量和构造函数:
private final Context context; private final float[] projectionMatrix=new float[16]; private final float[] modelMatrix=new float[16]; private Table table; private Mallet mallet; private TextureShaderProgram textureProgram; private ColorShaderProgram colorProgram; private int texture; public LYJRenderer(Context context){ this.context=context }
我们只保留上下文和矩阵的变量,并添加了顶点数组,着色器程序和纹理的变量。这个构造函数被简化为只保存一个Android上下文的引用。
初始化变量
在onSurfaceCreated()加入初始化这些变量:
GLES20.glClearColor(0.0f,0.f,0.0f,0.0f); this.table=new Table(); this.mallet=new Mallet(); this.textureProgram=new TextureShaderProgram(context); this.colorProgram=new ColorShaderProgram (context); this.texture=TextureHelper.loadTexture(Context,R.drawable.air_hockey_surface);
我们把清屏颜色设置为黑色,初始化顶点数组和着色器程序。并用本篇博文的第一个小标题的函数加载纹理。
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); this.textureProgram.useProgram(); this.textureProgram.setUniforms(this.projectionMatrix,this.texture); this.table.bindData(this.textureProgram); this.table.draw(); this.colorProgram.useProgram(); this.colorProgram.setUniforms(this.projectionMatrix); this.mallet.bindData(this.colorProgram); this.mallet.draw();
使用纹理进行绘制
不再赘述onSurfaceChanged(),因为它保持不变的,加入如下代码到onDrawFrame()绘制桌子和木槌:
我们清空了渲染表面,接下来,我们做的第一件事是绘制桌子。我们首先调用this.textureProgram.useProgram();告诉OpenGL使用这个程序,然后通过调用this.textureProgram.setUniforms(this.projectionMatrix,this.texture);把那些uniform传递进来。下一步是通过调用this.table.bindData(this.textureProgram);把顶点数组数据和着色器程序定起来,最后调用this.table.draw();绘制桌子。
我们重复同样的调用顺序,用颜色着色器程序绘制了木槌。
源代码地址:http://download.csdn.net/detail/liyuanjinglyj/8848105
程序运行后的效果图如下图所示: