文章目录
本篇会继续上一篇的工作开始。我们首先加载并编译前面定义的着色器,然后把它们链接在一起放在OpenGL的一个程序里。我们接下来就可以用着色器程序在屏幕上绘制矩形图形了。
加载着色器
首先我们需要新建一个帮助类,用来读取我们在raw目录下写入的着色器文件,该类命名为TextResourceReader。
object TextResourceReader {
fun readTextFromResource(context:Context,resId:Int):String{
var bufferReader:BufferedReader? = null
try {
var body = StringBuilder()
var inputStream = context.resources.openRawResource(resId)
var inputStreamReader = InputStreamReader(inputStream)
bufferReader = BufferedReader(inputStreamReader)
var line:String? = bufferReader.readLine()
while (line != null) {
body.append(line)
body.append("\n")
line = bufferReader.readLine()
}
return body.toString()
}catch (e: IOException){
throw RuntimeException("Could not open resource: $resId",e)
}catch(nfe: Resources.NotFoundException){
throw RuntimeException("Resource not found: $resId",nfe)
}finally {
bufferReader?.close()
}
}
}
定义了一个名为readTextFileFromResource()的方法,用于从资源文件中读取文本。该方法需要传入Android上下文(context)和资源标识符(resource ID)来调用此方法。
代码中检测了两种常见异常情况,资源不存在或读取资源时发生错误,并在这些情况下捕捉错误并抛出异常。
读取着色器代码
由于我们读取着色器代码需要用到Context,所以第一篇中的FirstOpenGLDemoRenderer需要改造下,添加构造函数并且在创建时传入Context。
private var context: Context
constructor(context: Context){
this.context = context
}
之后在onSurfaceCreated()方法中,glClearColor()调用之后。添加读取着色器的代码,这段新代码将用于读取顶点着色器和片段着色器代码。
var vertexShaderSource = TextResourceReader.readTextFromResource(context,R.raw.simple_vertex_shader)
var fragShaderSource = TextResourceReader.readTextFromResource(context,R.raw.simple_fragment_shader)
日志添加
当代码变得复杂时,使用日志记录可以帮助我们了解错误发生的情况和追踪问题的源头。在Android中,可以使用Log类将信息记录到系统日志中。为了灵活控制日志记录的开启和关闭,创建了一个名为LoggerConfig的新类。
object LoggerConfig {
const val ON = true
}
后续代码中将通过LoggerConfig.ON常量的值为true或false来决定是否记录日志信息。
编译着色器
我们已经成功从文件中读取出着色器的源代码。接下来的步骤是编译每个着色器。为了简化创建和编译着色器的过程,需要创建一个新的辅助类ShaderHelper,它将创建新的OpenGL着色器对象,然后编译着色器代码并返回一个代表编译后着色器代码的着色器对象。
编写的样板代码可以在后续的实践中重用。
object ShaderHelper {
const val TAG = "ShaderHelper"
fun compileVertexShader(shaderSource:String):Int{
return ShaderHelper.compileShader(GLES20.GL_VERTEX_SHADER, shaderSource)
}
fun compileFragmentShader(shaderSource:String):Int{
return ShaderHelper.compileShader(GLES20.GL_FRAGMENT_SHADER, shaderSource)
}
private fun compileShader(type:Int, shaderSource:String):Int{
}
}
接下来我们将会逐步构建compileShader方法。
1.创建一个着色器对象
在编译着色器之前,我们应该做的第一件事就是创建一个新的着色器对象,并且检查这个创建是否成功。
在compileShader()中添加如下代码。
var shaderObjectId = GLES20.glCreateShader(type)
if(shaderObjectId == 0){
if(LoggerConfig.ON){
Log.w(com.zlgspcae.firstopengldrawdemo.utils.ShaderHelper.TAG,"Could not create new shader.")
}
return 0
}
使用glCreateShader()函数创建一个新的着色器对象,并将对象的ID存储在变量shaderObjectId中。type参数指定着色器的类型,可以是GL_VERTEX_SHADER(顶点着色器)或GL_FRAGMENT_SHADER(片段着色器)。
使用类似glCreateShader()的调用创建对象,它返回一个整数值,作为OpenGL对象的引用。这个整数值在后续需要引用该对象时传回给OpenGL。返回值0表示对象创建失败,类似于Java中的null。
如果对象创建失败,返回0而不是抛出异常。OpenGL不会抛出异常,而是通过glGetError()方法来指示错误。这个方法用来查询OpenGL是否因为某个API调用导致了错误。
2.上传并编译着色器源码
上传着色器源代码
GLES20.glShaderSource(shaderObjectId,shaderSource)
当我们成功拿到着色器对象之后,就可以使用glShaderSource()函数将着色器的源代码字符串shaderCode上传到着色器对象中,上传的代码将与shaderObjectId所引用的着色器对象关联起来。然后就可以编译着色器了。
GLES20.glCompileShader(shaderObjectId)
3.查询编译状态
添加如下代码来检测OpenGL是否成功编译着色器。
var compileStatus = IntArray(1)
GLES20.glGetShaderiv(shaderObjectId,GLES20.GL_COMPILE_STATUS,compileStatus,0)
创建了一个长度为1的整型数组compileStatus,用于存储编译状态。使用glGetShaderiv()函数查询着色器的编译状态,函数的第一个参数是着色器对象的ID shaderObjectId,第二个参数是GL_COMPILE_STATUS,第三个参数是存储编译状态的数组compileStatus,第四个参数是数组中存储状态的偏移量,这里是0。
通过compileStatus数组的第0个元素的值来判断着色器编译是否成功。
4.着色器信息日志
OpenGL提供了基本的编译成功或失败状态,但可能需要更详细的错误信息。通过调用glGetShaderInfoLog(shaderObjectId)函数,可以获得OpenGL记录的关于着色器的可读消息的有用内容。如果OpenGL有关于着色器的任何信息,它会将这些信息存储在着色器的信息日志中。
if(LoggerConfig.ON){
Log.v(TAG,"Results of compile source:\n $shaderSource \n:${GLES20.glGetShaderInfoLog(shaderObjectId)}")
}
5.确认着色器编译状态并返回编译结果
添加如下代码:
if(compileStatus[0] == 0){
GLES20.glDeleteShader(shaderObjectId)
if(LoggerConfig.ON){
Log.w(TAG,"Compilation of shader failed.")
}
return 0
}
return shaderObjectId
需要检查状态返回值是否为0。如果返回值为0,表示编译失败。编译失败时,不再需要着色器对象,应通知OpenGL删除该对象,并返回0给调用代码。如果编译成功,着色器对象是有效的,可以在代码中使用,则直接返回着色器对象索引。
6.调用模板代码编译着色器
现在是时候充分利用我们刚刚写的代码了,接着在Reanderer中的onSurfaceCreate()继续添加如下代码:
var vertexShader = ShaderHelper.compileVertexShader(vertexShaderSource)
var fragShader = ShaderHelper.compileFragmentShader(fragShaderSource)
如果一切顺利,那么顶点着色器和片段着色器的对象索引将会被分别保存在vertexShader和fragShader变量中。
通过OpenGL程序连接顶点着色器和片段着色器
既然我们已经成功加载并编译了一个顶点着色器和一个片段着色器。那么接下来的步骤是将编译后的顶点着色器和片段着色器绑定在一起,放入一个单独的OpenGL程序中。通过绑定,创建一个完整的着色器程序,使得OpenGL可以在渲染过程中使用这些着色器。
1.了解OpenGL程序
一个OpenGL程序是由顶点着色器和片段着色器链接在一起形成的单个对象。顶点着色器和片段着色器总是一起工作,顶点着色器计算屏幕上每个顶点的最终位置,而片段着色器确定组成几何形状的每个片段的最终颜色。
OpenGL使用顶点着色器确定顶点位置,然后组织顶点成点、直线和三角形,并分解成片段,接着片段着色器为每个片段确定颜色。虽然顶点着色器和片段着色器通常一起使用,但它们并不是一对一匹配的,同一个着色器可以在多个程序中使用。
让我们接着在ShaderHelper类中创建了一个名为linkProgram的方法,用于链接顶点着色器和片段着色器的ID,形成程序。
fun linkProgram(vertexShaderId:Int,frgShaderId:Int):Int{
}
与构建compileShader()一样,我们也将一步一步完善它。
2.新建程序并添加着色器
直接上代码:
var programObjectId = GLES20.glCreateProgram()
if(programObjectId == 0){
if(LoggerConfig.ON){
Log.w(TAG,"Could not create new program.")
}
return 0
}
使用glCreateProgram()创建一个新的OpenGL程序对象,并将对象的ID存储在programObjectId变量中。如果programObjectId的值为0,表示程序对象创建失败。这时,如果日志记录开启(LoggerConfig.ON为true),则记录警告日志,并返回0。
继续通过programObjectId添加着色器:
GLES20.glAttachShader(programObjectId,vertexShaderId)
GLES20.glAttachShader(programObjectId,frgShaderId)
使用glAttachShader()将顶点着色器和片段着色器添加到程序对象上。
3.链接程序
现在准备将这些着色器联合起来,我们将调用glLinkProgram(programObjectId)
GLES20.glLinkProgram(programObjectId)
还是与着色器编译一样,我们为了检查这个链接是否成功,还需要添加如下代码:
var linkStatus = IntArray(1)
GLES20.glGetProgramiv(programObjectId,GLES20.GL_LINK_STATUS,linkStatus,0)
创建一个整型数组linkStatus来存储链接操作的成功与否结果。使用glGetProgramiv(programObjectId, GL_LINK_STATUS, linkStatus, 0)来获取链接状态。
如果链接过程中出现问题或OpenGL有关于程序的有用信息,可以通过检查程序的信息日志来获取。这可以通过调用glGetProgramInfoLog(programObjectId)实现。
如果日志记录开启(LoggerConfig.ON为true),则将程序链接结果和信息日志输出到Android的日志系统。
if(LoggerConfig.ON){
Log.v(TAG,"Results of linking program: \n${GLES20.glGetProgramInfoLog(programObjectId)}")
}
4.验证链接状态并返回程序索引
if(linkStatus[0] == 0){
GLES20.glDeleteProgram(programObjectId)
if(LoggerConfig.ON){
Log.w(TAG,"Linking of program failed.")
}
return 0
}
return programObjectId
还是一样,如果链接状态结果返回为0,则链接失败,那么OpenGL也将不需要这个程序,调用glDeleteProgram()函数将其移除,并返回0给调用者。如果一切顺利那么返回程序索引给外部调用者。
5.通过程序链接我们的着色器
现在就可以通过linkProgram()来完成我们顶点和片段着色器的链接。在Renderer中添加如下全局变量:
private var program = 0
在onSurfaceCreated()中添加如下代码:
program = ShaderHelper.linkProgram(vertexShader,frgShader)
使用OpenGL程序完成最后的关联
在前面已经学习了如何定义物体结构的属性数组,创建、加载、编译着色器,并将它们链接成一个OpenGL程序。现在是将这些部分组合起来,准备将我们的第一个图像(矩形)渲染到屏幕上了。
1.验证OpenGL程序
在开始使用OpenGL程序之前,应该验证程序对象是否对当前OpenGL状态有效。验证程序可以帮助了解程序为何可能是低效的或无法运行。
在ShaderHelper中添加如下代码:
fun validateProgram(programObjectId:Int):Boolean{
GLES20.glValidateProgram(programObjectId)
var validateStatus = IntArray(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
}
使用glValidateProgram()函数来验证OpenGL程序。通过调用glGetProgramiv(programObjectId, GL_VALIDATE_STATUS)来获取验证结果。如果OpenGL有关于程序的有用信息,可以通过glGetProgramInfoLog(programObjectId)获取程序日志并打印出来。
建议在开发或调试应用程序时才进行程序验证,而不是在生产环境中。
验证函数编写好了,那么我们继续在Renderer中的onSurfaceCreated()添加如下代码:
if(LoggerConfig.ON){
ShaderHelper.validateProgram(program)
}
接下来就是使用我们花了很大力气创建的OpenGLc程序,onSurfaceCreated()再继续添加:
GLES20.glUseProgram(program)
glUseProgram(program)来告诉OpenGL使用该程序进行绘制,glUseProgram()应在开始绘制任何物体到屏幕之前调用。
2.获取Uniform Location
在OpenGL程序中,每个uniform都有一个与之关联的位置编号,这些编号用于在着色器中发送数据。还得我们在片段着色器中定义了一个名为u_Color的uniform,用于设置绘制对象的颜色吗,在片段着色器的main()函数中,将u_Color的值赋给gl_FragColor,从而确定片段的最终颜色。
在Renderer类创建如下全局变量:
private val U_COLOR = "u_Color"
private var uColorLocation = 0
我们已经为这个uniform的名字创建了一个常量和一个用来容纳它在OpenGL程序对象中的位置的变量。uniform的位置并不是事先指定的,因此,一旦程序链接成功了,我们就要查询这个位置。一个uniform的位置在一个程序对象中是唯一的:即使在两个不同的程序中使用了相同的uniform名字,也不意味着它们使用相同的位置。
继续在onSurfaceCreated()中添加如下代码:
uColorLocation = GLES20.glGetUniformLocation(program,U_COLOR)
我们调用glGetUniformLocationO获取uniform的位置,并把这个位置存人uColorLocation,当我们稍后要更新这个uniform值的时候,我们会使用它。
3.获取Attribute Location
这块就需要回忆下我们在顶点着色器内定义的“attribute vec4 a_Position”了,这里获取的就是这个attribute定义的变量关联的索引,与Uniform Location同样的操作,先在Renderer类创建如下全局变量:
private val A_POSITION = "a_Position"
private var aPositionLocation = 0
然后在onSurfaceCreated()中添加如下代码:
aPositionLocation = GLES20.glGetAttribLocation(program,A_POSITION)
调用glGetAttribLocation()获取属性的位置。有了这个位置,就能告诉OpenGL到哪里去找到这个属性对应的数据了。
4.与顶点数据关联
接下来就是要告诉OpenGL在哪里找到属性a_Position对应的数据。
在onSurfaceCreated()中继续添加如下代码:
vertices.position(0)
GLES20.glVertexAttribPointer(aPositionLocation,POSITION_COMPONENT_COUNT,GLES20.GL_FLOAT,false,0,vertices)
回到开始处,我们创建了一个浮点数的数组,这些浮点数表示组成矩形的顶点位置。然后我们有创建了一个名为vertexData的缓冲区,并把这些位置复制到了该缓冲区。
通过调用position(int)方法,OpenGL可以从缓冲区的指定位置开始读取数据。
使用glVertexAttribPointer函数,OpenGL可以在vertexData中找到a_Position对应的数据,这个函数非常重要,因此,我们来看下每个函数传递了什么。
参数名称 | 参数描述 |
---|---|
int index | 属性位置,传入aPositionLocation,它指向在3.4.3节中获取的位置。 |
int size | 每个属性的数据计数,或与每个顶点相关联的分量数量。我们这里决定每个顶点使用两个浮点数(x和y坐标),需要两个分量。使用常量POSITION_COMPONENT_COUNT标记这一事实,并将该常量值传递进去。 |
int type | 数据类型。由于数据被定义为浮点数列表,因此传递GL_FLOAT。 |
boolean normalized | 仅在使用整型数据时,此参数有意义。可以暂时忽略此参数。 |
int stride | 当一个数组存储多于一个属性时,此参数才有意义。本章中只有一个属性,可以忽略此参数,暂时传递0。后面将会了解更多关于stride的内容。 |
Buffer ptr | 此参数告诉OpenGL从哪里读取数据。注意OpenGL会从缓冲区的当前位置读取数据,如果没有调用vertexData.position(0),它可能会尝试读取缓冲区结尾之后的内容,导致应用程序崩溃。 |
传递错误的参数给glVertexAttribPointer函数可能导致程序产生奇怪的结果甚至崩溃。因此确保参数的正确性对于程序的稳定性至关重要。
5.使能顶点数组
在数据属性链接之后,需要调用glEnableVertexAttribArray函数来使能顶点属性。
在glVertexAttribPointer调用之后,添加以下代码以启用aPositionLocation属性:
GLES20.glEnableVertexAttribArray(aPositionLocation)
这里最后一个调用,OpenGL现在知道去哪里寻找它所需要的数据。
在屏幕上绘制
在onDrawFrame()结尾处,让我们在glClear()调用之后添加如下代码:
GLES20.glUniform4f(uColorLocation,1.0f,1.0f,1.0f,1.0f)
GLES20.glDrawArrays(GLES20.GL_TRIANGLES,0,6)
通过调用glUniform4f函数来更新着色器代码中的u_Color值。与属性不同,uniform没有默认值,如果定义为vec4类型,则必须提供所有四个分量的值。初始颜色设置为白色,红色、绿色和蓝色分量的值均为1.0f。阿尔法分量虽然在此例中不关键,但也需要指定,因为颜色有四个分量。
制定了颜色之后,我们就使用glDrawArrays函数绘制,告诉OpenGL我们要画三角形,第一个参数GLES20.GL_TRIANGLES告诉OpenGL绘制三角形,第二个参数0指示从顶点数组的那个位置开始读取顶点,第三个参数6告诉OpenGL读取六个顶点,因为每个三角形需要三个顶点,而我们每个顶点的分量为2,所以这里调用会绘制出两个三角形。
运行结果
~这里就不贴图了,很简单,大家可以跑起来自行观察结果。
小结
本篇我们了解到了如何创建和编译着色器,顶点着色器和片段着色器如何协同工作,并将其链接形成一个OpenGL程序对象以及如何将顶点着色器内部的属性变量与顶点属性数组关联起来,并将所学知识点整合,成功在屏幕上显示内容。
而在本篇我们编写的样例代码可以在后续项目中重用。
附上完整代码
ShaderHelper:
object ShaderHelper {
const val TAG = "ShaderHelper"
fun compileVertexShader(shaderSource:String):Int{
return compileShader(GLES20.GL_VERTEX_SHADER,shaderSource)
}
fun compileFragmentShader(shaderSource:String):Int{
return compileShader(GLES20.GL_FRAGMENT_SHADER,shaderSource)
}
fun linkProgram(vertexShaderId:Int,frgShaderId:Int):Int{
var programObjectId = GLES20.glCreateProgram()
if(programObjectId == 0){
if(LoggerConfig.ON){
Log.w(TAG,"Could not create new program.")
}
return 0
}
GLES20.glAttachShader(programObjectId,vertexShaderId)
GLES20.glAttachShader(programObjectId,frgShaderId)
GLES20.glLinkProgram(programObjectId)
var linkStatus = IntArray(1)
GLES20.glGetProgramiv(programObjectId,GLES20.GL_LINK_STATUS,linkStatus,0)
if(LoggerConfig.ON){
Log.v(TAG,"Results of linking program: \n${GLES20.glGetProgramInfoLog(programObjectId)}")
}
if(linkStatus[0] == 0){
GLES20.glDeleteProgram(programObjectId)
if(LoggerConfig.ON){
Log.w(TAG,"Linking of program failed.")
}
return 0
}
return programObjectId
}
fun validateProgram(programObjectId:Int):Boolean{
GLES20.glValidateProgram(programObjectId)
var validateStatus = IntArray(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
}
private fun compileShader(type:Int, shaderSource:String):Int{
var shaderObjectId = GLES20.glCreateShader(type)
if(shaderObjectId == 0){
if(LoggerConfig.ON){
Log.w(TAG,"Could not create new shader.")
}
return 0
}
GLES20.glShaderSource(shaderObjectId,shaderSource)
GLES20.glCompileShader(shaderObjectId)
var compileStatus = IntArray(1)
GLES20.glGetShaderiv(shaderObjectId,GLES20.GL_COMPILE_STATUS,compileStatus,0)
if(LoggerConfig.ON){
Log.v(TAG,"Results of compile source:\n $shaderSource \n:${GLES20.glGetShaderInfoLog(shaderObjectId)}")
}
if(compileStatus[0] == 0){
GLES20.glDeleteShader(shaderObjectId)
if(LoggerConfig.ON){
Log.w(TAG,"Compilation of shader failed.")
}
return 0
}
return shaderObjectId
}
}
Renderer:
class FirstOpenGLDemoDrawRenderer:Renderer {
private val POSITION_COMPONENT_COUNT = 2
private val BYTE_PER_FLOAT = 4
private var context: Context
private var vertices:FloatBuffer
private var program = 0
private val U_COLOR = "u_Color"
private var uColorLocation = 0
private val A_POSITION = "a_Position"
private var aPositionLocation = 0
private var rectangleVertices = floatArrayOf(
-0.5f , -0.5f,
0.5f , 0.5f,
-0.5f , 0.5f,
-0.5f , -0.5f,
0.5f , -0.5f,
0.5f , 0.5f
)
constructor(context: Context){
this.context = context
vertices = ByteBuffer.allocateDirect(rectangleVertices.size*BYTE_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
vertices.put(rectangleVertices)
}
override fun onSurfaceCreated(p0: GL10?, p1: EGLConfig?) {
GLES20.glClearColor(0f,1f,0f,1f)
var vertexShaderSource = TextResourceReader.readTextFromResource(context,R.raw.simple_vertex_shader)
var fragShaderSource = TextResourceReader.readTextFromResource(context,R.raw.simple_fragment_shader)
var vertexShader = ShaderHelper.compileVertexShader(vertexShaderSource)
var frgShader = ShaderHelper.compileFragmentShader(fragShaderSource)
program = ShaderHelper.linkProgram(vertexShader,frgShader)
if(LoggerConfig.ON){
ShaderHelper.validateProgram(program)
}
GLES20.glUseProgram(program)
uColorLocation = GLES20.glGetUniformLocation(program,U_COLOR)
aPositionLocation = GLES20.glGetAttribLocation(program,A_POSITION)
vertices.position(0)
GLES20.glVertexAttribPointer(aPositionLocation,POSITION_COMPONENT_COUNT,GLES20.GL_FLOAT,false,0,vertices)
GLES20.glEnableVertexAttribArray(aPositionLocation)
}
override fun onSurfaceChanged(p0: GL10?, width: Int, height: Int) {
GLES20.glViewport(0,0,width,height)
}
override fun onDrawFrame(p0: 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)
}
}
···