基于OpenGL ES 2.0 for Android。
自OpenGL ES 2.0开始,可编程着色器(Programmable Shader)开始逐渐替代1.x时代的部分固定功能图形API。它的出现使得OpenGL ES编程更加灵活和强大。OpenGL ES 2.0至今仍是最流行、最广泛、也是最可靠的移动端图形API。
最开始,可编程着色器采用一种混合开发的方式,这种方式存在着不直观、程序复杂的问题,对开发者很不友好。因此,混合开发方式并没有完全释放可编程着色器强大的可编程能量。后来,一种更加直观的GPU程序设计语言出现了,它就是GLSL(OpenGL Shading Language)。它的出现真正促进了OpenGL的发展和繁荣。
GLSL不仅仅是一门开发语言,它同样是一个GPU图形程序设计标准。它具有以下几种显而易见的好处:
- 具有跨平台的特性,能够支持各种主流操作系统;
- 所有支持OpenGL的GPU都可以支持GLSL开发的程序;
- 不同的GPU厂商可以在统一标准下实现自己特定的优化。
程序的入口main函数
GLSL是基于C语言修改的编程语言。一个最简单的顶点着色器(Vertex Shader)是这样的:
void main() {
gl_Position = vec4(0);
}
所有的GLSL实现的着色器都有一个类似于C语言入口函数的函数:void main(void)
,它同样是GLSL程序的入口函数。其中,参数类型void
是可选的。
众所周知,一个类C程序从源代码到可执行文件通常需要经过编译和链接。GLSL也不例外,不过它的编译和链接方式从命令行/GUI变成了OpenGL ES的API。
一个编译并使用GLSL着色器程序的完整过程是这样的:
- 创建着色器对象(Shader Object),并获得对象引用
- 为着色器对象注入GLSL源代码,并编译GLSL源代码
- 创建一个GLSL程序对象(Program Object),并挂载着色器对象
- 链接GLSL程序
- 使用GLSL程序
在Android中,通常是这样一段代码。
//GLSL 顶点着色器源代码
val shaderCodeString = """
void main() {
glPosition = vec4(0);
}
""".trimIndent()
//1,创建着色器对象(Shader Object),并获得对象引用
val shaderHandle = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER)
//2,为着色器对象注入GLSL源代码,并编译GLSL源代码
GLES20.glShaderSource(shaderHandle, shaderCodeString)
GLES20.glCompileShader(shaderHandle)
//3,创建一个GLSL程序对象(Program Object),并挂载着色器对象
val program = GLES20.glCreateProgram()
GLES20.glAttachShader(program, shaderHandle)
//4,链接GLSL程序
GLES20.glLinkProgram(program)
//5,使用GLSL程序
GLES20.glUseProgram(program)
尽管这是一个完整的GLSL程序使用流程,但是这样做的结果是程序运行到第四步,链接GLSL程序(GLES20.glLinkProgram(program)
),就已经出错了。
那么,如何获取到整个过程中的的错误信息呢?GLES方法glGetError()
可以获得函数执行的错误代码,当然这种方式只能获得一些简单的信息。我们可以通过它定位到出错的函数,然后去查阅OpenGL ES官方文档了解函数的报错信息和正确用法。另外,glGetProgramInfoLog(int program)
可以获取GLSL程序编译过程中较为详细的日志。例如,上面例子中,我们可以在步骤4执行后得到如下日志信息:
Link failed because of missing fragment shader.
日志显示,链接过程失败了,原因是缺少片元着色器。要知道OpenGL ES的着色器有两大类:顶点着色器、片元着色器(Fragment Shader)。这两类着色器必须成对且仅有一对出现在一个GLSL程序中才能够链接通过。一个正确的GLSL程序应当创建一个顶点着色器和一个片元着色器并挂载到同一个GLSL程序对象,之后再链接,使用。
下面看一看重新整理后的流程:
- 创建并编译一个顶点着色器对象
- 创建并编译一个片元着色器对象
- 创建一个GLSL程序对象,并挂载1,2中创建的着色器对象,然后链接GLSL程序对象
- 使用3中的GLSL程序对象
fun loadProgram(vertexCode: String, fragmentCode: String): Int {
//1. 创建并编译一个顶点着色器对象
val vertexHandle = loadShader(GLES20.GL_VERTEX_SHADER, vertexCode)
//2. 创建并编译一个片元着色器对象
val fragmentHandle =loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentCode)
//3. 创建一个GLSL程序对象,并挂载1,2中创建的着色器对象,然后链接GLSL程序对象
return linkProgram(vertexHandle, fragmentHandle)
}
fun loadShader(type: Int, code: String): Int {
val shader = GLES20.glCreateShader(type)
GLES20.glShaderSource(shader, code)
GLES20.glCompileShader(shader)
return shader
}
fun linkProgram(vertexShader: Int, fragmentShader: Int): Int {
val program = GLES20.glCreateProgram()
GLES20.glAttachShader(program, vertexShader)
GLES20.glAttachShader(program, fragmentShader)
GLES20.glLinkProgram(program)
return program
}
虽然整个流程十分简单,但是新手由于对API不熟悉,仍可能出错。因此学会使用诸如glGetError()
、glGetProgramInfoLog(int)
、glGetShaderInfoLog(int)
等调试工具很重要。一个好的习惯是开发阶段每一次API调用都打印日志,这能够帮助新手快速定位问题,节约时间。同时,尽量实现封装,减少重复代码出错率。