一.前言
本篇文章会继续上一篇文章开始的工作,在这篇文章中,我们首先会加载并编译前面定义的着色器,然后把他们链接在一起放在OpenGL的一个程序里,接下来就可以使用这个着色器程序在屏幕上绘制空气曲棍球桌子结构了。
二.加载着色器
1.我们已经为着色器写了代码,下一步则要把他们加载进内存中。为此,我们首先需要定义一个可以从资源文件夹中读取那些代码的方法。我们可以写一个工具类TextResourceReader,用于读取着色器代码,代码如下:
class TextResourceReader {
companion object{
fun readTextFileFromResource(context: Context, id:Int):String{
val stringBuilder=StringBuilder()
val inputStream=context.resources.openRawResource(id)
val inputStreamReader= InputStreamReader(inputStream)
val bufferedReader= BufferedReader(inputStreamReader)
var nextLine:String?=bufferedReader.readLine()
while(nextLine!=null){
stringBuilder.append(nextLine)
stringBuilder.append("\n")
nextLine=bufferedReader.readLine()
}
return stringBuilder.toString()
}
}
}
2.下面,我们使用这个工具类去读取着色器代码,我们需要修改MyRenderer.kt文件,在onSurfaceCreated方法的末尾加入如下代码:
//读取着色器代码
val vertexShaderCode=TextResourceReader.readTextFileFromResource(context,R.raw.simple_vertex_shader)
val fragmentShaderCode=TextResourceReader.readTextFileFromResource(context,R.raw.simple_fragment_shader)
3.由于这个工具类中的方法需要传入Context对象,所以我们需要给MyRenderer类新增一个成员变量context,并在MainActivity类中传入this引用。
三.编译着色器,链接程序,绘制图形
1.现在,我们已经把每个着色器的源代码读取出来了,下一步就是编译每个着色器了。我们可以新建一个辅助类ShaderHelper,它可以创建新的着色器对象,编译着色器代码并返回代表那段着色器代码的着色器对象。代码入下:
class ShaderHelper {
companion object{
//编译顶点着色器
fun compileVertexShader(shaderCode:String):Int{
return compileShader(GL_VERTEX_SHADER,shaderCode)
}
//编译片段着色器
fun compileFragmentShader(shaderCode:String):Int{
return compileShader(GL_FRAGMENT_SHADER,shaderCode)
}
//根据不同类型编译不同的着色器
fun compileShader(type:Int,shaderCode:String):Int{
//创建着色器对象
val shaderObjectId=glCreateShader(type)
//返回0表示创建失败
if(shaderObjectId==0){
Log.i("ShaderHelper","Could not create new shader")
return 0
}
//上传着色器源代码
glShaderSource(shaderObjectId,shaderCode)
//编译着色器对象
glCompileShader(shaderObjectId)
//检查着色器是否编译成功,并把结果存入数组的首地址
val status= IntArray(1)
glGetShaderiv(shaderObjectId,GL_COMPILE_STATUS,status,0)
//结果为0表示编译着色器失败
if(status[0]==0){
glDeleteShader(shaderObjectId)//删除着色器对象
return 0
}
return shaderObjectId
}
}
}
2.接下来,我们在MyRenderer这个类中使用以上定义的辅助类编译顶点着色器和片段着色器,在onSurfaceCreated方法末尾加入如下代码:
//编译顶点着色器和片段着色器
val vertexShader=ShaderHelper.compileVertexShader(vertexShaderCode)
val fragmentShader=ShaderHelper.compileFragmentShader(fragmentShaderCode)
3.把着色器一起链接进OpenGL程序,编译完顶点着色器和片段着色器后,我们需要把他们绑定到一起,然后放入单个的OpenGL程序中。我们在ShaderHelper类中新增一个方法linkProgram()用于实现这个链接功能,代码如下:
fun linkProgram(vertexShaderId:Int,fragmentShaderId:Int):Int{
//新建程序对象
val programObjectId=glCreateProgram()
if(programObjectId==0){//新建程序对象失败
Log.i("ShaderHelper","Could not create programObject")
return 0
}
//附上顶点着色器对象和片段着色器对象
glAttachShader(programObjectId,vertexShaderId)
glAttachShader(programObjectId,fragmentShaderId)
//链接程序
glLinkProgram(programObjectId)
//检查程序是否链接成功
val status=IntArray(1)
glGetProgramiv(programObjectId,GL_LINK_STATUS,status,0)
if(status[0]==0){//程序链接失败
glDeleteProgram(programObjectId)//删除程序对象
Log.i("ShaderHelper","Could not link program")
return 0
}
return programObjectId
}
4.修改MyRenderer类,新增一个成员变量program,用于存储程序对象id,并在onSurfaceCreated方法末尾加入如下代码:
//链接并使用这个OpenGL程序
program=ShaderHelper.linkProgram(vertexShader,fragmentShader)
glUseProgram(program)
5.关联属性和顶点数据的数组,也就是告诉OpenGL到哪里去找属性a_Position所对应的数据,代码如下:
//从开头处开始读取数据
vertexData.position(0)
//关联属性和顶点数据的数组
glVertexAttribPointer(0, POSITION_COMPONENT_COUNT, GL_FLOAT,false,0,vertexData)
//使能顶点数组,参数传属性的位置
glEnableVertexAttribArray(0)
6.glVertexAttribPointer()函数就是用来绑定属性和顶点数组的,它的定义如下:
public static void glVertexAttribPointer(
int indx,//属性的位置,这里指a_Position的位置
int size,//每个属性的数据计数,这里我们只用了两个,x和y
int type,//数据的类型
boolean normalized,//只有使用整型数据时才有意义
int stride,//步长,只有当数组中存储的属性多于一个才有意义,比如同时存储顶点位置和颜色。这里只存储了位置,设为0即可
java.nio.Buffer ptr//缓冲区指针
)
7.开始绘制,在onDrawFrame函数的末尾加入如下代码:
//绘制矩形
glUniform4f(0,1f,1f,1f,1f)
glDrawArrays(GL_TRIANGLES,0,6)
//绘制分割线
glUniform4f(0,1f,0f,0f,1f)
glDrawArrays(GL_LINES,6,2)
//绘制木槌,用点表示
glUniform4f(0,0f,1f,0f,1f)
glDrawArrays(GL_POINTS,8,1)
glUniform4f(0,0f,1f,0f,1f)
glDrawArrays(GL_POINTS,9,1)
现在可以运行程序,但是此时我们只能看到桌子的一个角,看不到完整的桌子。想要解决这个问题,我们需要知道OpenGL怎么将我们定义的坐标映射到屏幕上实际的物理坐标的。OpenGL希望在所有的顶点着色器运行后,所有可见的点都变为标准化设备坐标,也就是说x,y,z的范围都在-1到1之间,超出这个范围的点都是不可见的。所以我们需要重新修改下顶点坐标,让其在-1到1之间,修改后的坐标如下:
val tableVertices=floatArrayOf(
//Triangle one
-0.5f,-0.5f,
0.5f,0.5f,
-0.5f,0.5f,
//Triangle two
-0.5f,-0.5f,
0.5f,-0.5f,
0.5f,0.5f,
//Mid Line
-0.5f,0f,
0.5f,0f,
//Mallets
0f,-0.25f,
0f,0.25f,
)
修改完顶点坐标后,我们再把清除屏幕的颜色设置为黑色,就可以看到下面的效果了: