2.5 着色器的编译
OpenGL着色器程序的编写与C语言等基于编译器的语言非常类似。我们使用编译器来解析程序,检查是否存在错误,然后将它翻译为目标代码。然后,在链接过程中将一系列目标文件合并,并产生最终的可执行程序。在程序中使用GLSL着色器的过程与之类似,只不过编译器和链接器都是OpenGL API的一部分而已。
图2-1给出了创建GLSL着色器对象并且通过链接来生成可执行着色器程序的过程。
对于每个着色器程序,我们都需要在应用程序中通过下面的步骤进行设置。
对于每个着色器对象:
1)创建一个着色器对象。
2)将着色器源代码编译为对象。
3)验证着色器的编译是否成功。
然后需要将多个着色器对象链接为一个着色器程序,包括:
1)创建一个着色器程序。
2)将着色器对象关联到着色器程序。
3)链接着色器程序。
4)判断着色器的链接过程是否成功完成。
5)使用着色器来处理顶点和片元。
为什么要创建多个着色器对象?这是因为我们有可能在不同的程序中复用同一个函数,而GLSL程序也是同一个道理。我们创建的通用函数可以在多个着色器中得到复用。因此不需要使用大量的通用代码来编译大量的着色器资源,只需要将合适的着色器对象链接为一个着色器程序即可。
调用glCreateShader()来创建着色器对象。
GLuint glCreateShader(GLenum type);
分配一个着色器对象。type必须是GL_VERTEX_SHADER、GL_FRAGMENT_SHADER、GL_TESS_CONTROL_SHADER、GL_TESS_EVALUATION_SHADER、GL_GEOMETRY_SHADER或GL_COMPUTE_SHADER中的一个。返回值可能是一个非零的整数值,如果为0则说明发生了错误。
当我们使用glCreateShader()创建了着色器对象之后,就可以将着色器的源代码关联到这个对象上。这一步需要调用glShaderSource()函数。
void glShaderSource(GLuint shader, GLsizei count, const GLchar* string, const GLint length);
将着色器源代码关联到一个着色器对象shader上。string是一个由count行GLchar类型的字符串组成的数组,用来表示着色器的源代码数据。string中的字符串可以是NULL结尾的,也可以不是。而length可以是以下三种值的一种。如果length是NULL,那么我们假设string给出的每行字符串都是NULL结尾的。否则,length中必须有count个元素,它们分别表示string中对应行的长度。如果length数组中的某个值是一个整数,那么它表示对应的字符串中的字符数。如果某个值是负数,那么string中的对应行假设为NULL结尾。
如果要编译着色器对象的源代码,需要使用glCompileShader()函数。
void glCompileShader(GLuint shader);
编译着色器的源代码。结果查询可以调用glGetShaderiv(),并且参数为GL_COMPILE_STATUS。
这里与C语言程序的编译类似,需要自己判断编译过程是否正确地完成。调用glGetShaderiv()并且参数为GL_COMPILE_STATUS,返回的就是编译过程的状态。如果返回为GL_TRUE,那么编译成功,下一步可以将对象链接到一个着色器程序中。如果编译失败,那么可以通过调取编译日志来判断错误的原因。glGetShaderInfoLog()函数会返回一个与具体实现相关的信息,用于描述编译时的错误。这个错误日志的大小可以通过调用glGetShaderiv()(带参数GL_INFO_LOG_LENGTH)来查询。
void glGetShaderInfoLog(GLuint shader, GLsizei bufSize, GLsizei length, char infoLog);
返回shader的最后编译结果。返回的日志信息是一个以NULL结尾的字符串,它保存在infoLog缓存中,长度为length个字符串。日志可以返回的最大值是通过bufSize来定义的。
如果length设置为NULL,那么将不会返回infoLog的大小。
当创建并编译了所有必要的着色器对象之后,下一步就是链接它们以创建一个可执行的着色器程序。这个过程与创建着色器对象的过程类似。首先,我们创建一个着色器程序,以便将着色器对象关联到其上。这里用到了glCreateProgram()函数。
GLuint glCreateProgram(void);
创建一个空的着色器程序。返回值是一个非零的整数,如果为0则说明发生了错误。
当得到着色器程序之后,下一步可以将它关联到必要的着色器对象上,以创建可执行的程序。关联着色器对象的步骤可以通过调用glAttachShader()函数来完成。
void glAttachShader(GLuint program, GLuint shader);
将着色器对象shader关联到着色器程序program上。着色器对象可以在任何时候关联到着色器程序,但是它的功能只有经过程序的成功链接之后才是可用的。着色器对象可以同时关联到多个不同的着色器程序上。
与之对应的是,如果我们需要从程序中移除一个着色器对象,从而改变着色器的操作,那么可以调用glDetachShader()函数,设置对应的着色器对象标识符来解除对象的关联。
void glDetachShader(GLuint program, GLuint shader);
移除着色器对象shader与着色器程序program的关联。如果着色器已经被标记为要删除的对象(调用glDeleteShader()),然后又被解除了关联,那么它将会被即时删除。
当我们将所有必要的着色器对象关联到着色器程序之后,就可以链接对象来生成可执行程序了。这一步需要调用函数glLinkProgram()。
void glLinkProgram(GLuint program);
处理所有与program关联的着色器对象来生成一个完整的着色器程序。链接操作的结果查询可以调用glGetProgramiv(),且参数为GL_LINK_STATUS。如果返回GL_TRUE,那么链接成功;否则,返回GL_FALSE。
由于着色器对象中可能存在问题,因此链接过程依然可能会失败。我们可以调用glGetProgramiv()(带参数GL_LINK_STATUS)来查询链接操作的结果。如果返回GL_TRUE,那么链接操作成功,然后我们可以指定着色器程序来处理顶点和片元数据了。如果链接失败,即返回结果为GL_FALSE,那么我们可以通过调用glGetProgramInfoLog()函数来获取程序链接的日志信息并判断错误原因。
void glGetProgramInfoLog(GLuint program, GLsizei bufSize, GLsizei length, char infoLog);
返回最后一次program链接的日志信息。日志返回的字符串以NULL结尾,长度为length个字符,保存在infoLog缓存中。log可返回的最大值通过bufSize指定。如果length为NULL,那么不会再返回infoLog的长度。
如果我们成功地完成了程序的链接,那么就可以调用函数glUseProgram()来运行着色器代码,并且参数设置为程序对象的句柄来启用顶点或者片元程序。
void glUseProgram(GLuint program);
使用链接过的着色器程序program。如果program为零,那么所有当前使用的着色器都会被清除。如果没有绑定任何着色器,那么OpenGL的操作结果是未定义的,但是不会产生错误。
如果已经启用了一个程序,而它需要关联新的着色器对象,或者解除之前关联的对象,那么我们需要重新对它进行链接。如果链接过程成功,那么新的程序会直接替代之前启用的程序。如果链接失败,那么当前绑定的着色器程序依然是可用的,不会被替代,直到我们成功地重新链接或者使用glUseProgram()指定了新的程序为止。
当着色器对象的任务完成之后,我们可以通过glDeleteShader()将它删除,并且不需要关心它是否关联到某个活动程序上。这一点与C语言程序的链接是相同的,当我们得到可执行程序之后,就不再需要对象文件了,直到我们再次进行编译为止。
void glDeleteShader(GLuint shader);
删除着色器对象shader。如果shader当前已经链接到一个或者多个激活的着色器程序上,那么它将被标识为“可删除”,当对应的着色器程序不再使用的时候,就会自动删除这个对象。
与此类似,如果我们不再使用某个着色器程序,也可以直接调用glDeleteProgram()删除它。
void glDeleteProgram(GLuint program);
立即删除一个当前没有在任何环境中使用的着色器程序program,如果程序正在被某个环境使用,那么等到它空闲时再删除。
最后,为了确保接口的完整性,还可以调用glIsShader()来判断某个着色器对象是否存在,或者通过glIsProgram()判断着色器程序是否存在。
GLboolean glIsShader(GLuint shader);
如果shader是一个通过glCreateShader()生成的着色器对象的名称,并且没有被删除,那么返回GL_TRUE。如果shader是零或者不是着色器对象名称的非零值,则返回GL_FALSE。
GLboolean glIsProgram(GLuint program);
如果program是一个通过glCreateProgram()生成的程序对象的名称,并且没有被删除,那么返回GL_TRUE。如果program是0或者不是着色器程序名称的非零值,则返回GL_FALSE。
为了简化应用程序中使用着色器的过程,我们在示例中使用一个LoadShaders()函数来辅助载入和创建着色器程序。我们已经在第1章的第一个程序中用到了这个函数来加载简单的着色器代码。