假设你已经编写好了一对shader,一个顶点shader和一个像素shader,那么你将如何在你编写的应用程序中使用这两个shader呢?这就是本章要解决的问题。
与C语言类似,每个shader源文件都必须被独立地编译成类似于C编译器生成的目标文件,它们将被连接在一起来组成一个程序。
下图为大家展示了,一对shader(一个顶点shader和一个像素shader)是如何经过编译、连接最后被应用程序所使用的过程。
创建一个Shader
下图像大家展示了编译一个shader对象(类似与生成一个C语言目标文件)的过程。
首先,我们来创建一个容纳shader的容器,我们称之为shader容器。我们使用glCreateShader函数来完成这个工作。glCreateShader的原型如下:
GLuint glCreateShader(GLenum shaderType);
这个函数只有一个参数,指定了shader容器所容纳的shader的类型。其中GL_VERTEX_SHADER代表顶点shader,GL_FRAGMENT_SHADER代表像素shader。如果调用成功的话,函数将返回一个整形数作为shader容器的句柄。
接下来,我们要在创建好的shader容器中添加shader的源代码。源代码应该以字符串数组的形式表示(如char* SourceCode[5])。当然,你也可以只用一个字符串来包含所有的源代码。然后,存储在字符串数组中的源代码将作为glShaderSource函数的参数,被设置到shader容器中。glShaderSource函数的原型如下:
void glShaderSource(GLuint shader, int numOfStrings, const char **strings, int *lenOfStrings);
其中,shader是代表shader容器的句柄(由glCreateShader返回的整形数);numOfStrings是包含源程序的字符串数组中字符串的个数;strings是包含源程序的字符串数组;lenOfStrings是一个数组,数组中的元素代表了strings相应下标的字符串的长度,如果这个值被设置成NULL,则代表每个字符串是以NULL结尾的。
最后,我们使用glCompileShader函数来对shader容器中的源代码进行编译。glCompileShader函数的原型如下:
void glCompileShader(GLuint shader);
其中,shader是代表shader容器的句柄。
创建一个程序
下图为大家展示了如何将编译后的shader连接成一个程序。
首先创建一个容纳程序的容器,我们称之为程序容器。我们可以通过glCreateProgram函数来创建一个程序容器。glCreateProgram函数的原型如下:
GLuint glCreateProgram(void);
如果函数调用成功将返回一个整形数作为程序的句柄。
接下来,我们要将shader容器添加到程序中。这时的shader容器不一定需要被编译,他们甚至不需要包含任何的代码。我们要做的只是将shader容器添加到程序中。我们使用glAttachShader函数来为程序添加shader容器。glAttachShader函数的原型如下:
void glAttachShader(GLuint program, GLuint shader);
其中,program是程序容器的句柄;shader是你要添加的shader容器的句柄。如果你同时拥有了,顶点shader和像素shader,你们需要分别将他们各自的两个shader容器添加的程序容器中。
最后,我们使用glLinkProgram来连接程序。glLinkProgram函数的原型如下:
void glLinkProgram(GLuint program);
其中,program是程序容器的句柄。在连接操作执行以后,你可以任意修改shader的源代码,对shader重新编译不会影响整个程序,除非重新连接程序。
如前面的图所示,在连接了程序以后,我们可以使用glUseProgram函数来加载并使用连接好的程序。glUseProgram函数原型如下:
void glUseProgram(GLuint prog);
其中,prog是你要使用的程序的句柄,你也可以将它设置为0来使用固定功能管线。如果程序已经在使用的时候,对程序进行重新编译,编译后的应用程序会自动替代以前的那个被调用,这时你不需要再次调用这个函数。
完整的源代码
void setShaders()
{
char *vs; /* 顶点shader的源代码 */
char *fs; /* 像素shader的源代码 */
/* 创建shader容器 */
v = glCreateShader(GL_VERTEX_SHADER);
f = glCreateShader(GL_FRAGMENT_SHADER);
/* 从文件读取源代码 */
vs = textFileRead("toon.vert");
fs = textFileRead("toon.frag");
const char * vv = vs;
const char * ff = fs;
/* 给shader容器设置源代码 */
glShaderSource(v, 1, &vv,NULL);
glShaderSource(f, 1, &ff,NULL);
free(vs);
free(fs);
/* 编译shader */
glCompileShader(v);
glCompileShader(f);
/* 创建程序容器 */
p = glCreateProgram();
/* 为程序添加shader */
glAttachShader(p,v);
glAttachShader(p,f);
/* 连接并加载程序 */
glLinkProgram(p);
glUseProgram(p);
}
使用InfoLog
调试一个shader是非常困难的。shader的世界里没有printf,你无法在控制台中打印调试信息。但是你可以通过一些OpenGL提供的函数来获取编译和连接过程中的信息。
在shader的编译阶段,你可以使用下面的函数来查询相关信息。
void glGetShaderiv(GLuint object, GLenum type, int *param);
其中,object是一个shader的句柄;type使用GL_COMPILE_STATUS;param是返回值,如果一切正常返回GL_TRUE代,否则返回GL_FALSE。
在连接阶段,你可以使用下面的函数来查询相关的信息。
void glGetProgramiv(GLuint object, GLenum type, int *param);
其中,object是一个程序的句柄;type是GL_LINK_STATUS;param是返回值,如果一切正常返回GL_TRUE代,否则返回GL_FALSE。
上面的两个函数中的type参数还可以取其他的值来获取其他的信息,具体内容参见相关书籍。
当错误产生的时候,我们可以从InfoLog中获得更多的信息。InfoLog中存储了关于上一个操作执行时的相关信息,比如编译阶段的警告和错误,以及连接阶段产生的问题。不幸的是对于错误信息没有统一的标准,所以不同的硬件或驱动程序将提供不同的错误信息。
为了能够获得特定的shader或程序的InfoLog,我们可以调用下面的函数:
void glGetShaderInfoLog(GLuint object, int maxLen, int *len, char *log);
void glGetProgramInfoLog(GLuint object, int maxLen, int *len, char *log);
其中,object是一个shader的句柄或是一个程序的句柄;maxLen从InfoLog中获得的最大字符数;len实际从InfoLog中返回的字符数;log就是log本身。
上面的两个函数需要知道InfoLog的具体长度,以便为保存返回信息的字符数组分配空间。我们可以通过下面的函数来得到InfoLog的实际长度:
void glGetShaderiv(GLuint object, GLenum type, int *param);
void glGetProgramiv(GLuint object, GLenum type, int *param);
其中,其中,object是一个shader的句柄或是一个程序的句柄;type使用GL_INFO_LOG_LENGTH;param是返回值,返回了InfoLog的长度。
清理
当不再需要某个shader或某个程序的时候,需要对其进行清理,以释放资源。前面,提到过如何向一个程序中添加一个shader。我们也可调用下面的函数来将一个shader从一个程序中除掉:
void glDetachShader(GLuint program, GLuint shader);
其中,program包含shader的程序;shader是要被排除的shader。
我们可以使用下面的函数来删除一个shader或一个程序:
void glDeleteShader(GLuint id);
void glDeleteProgram(GLuint id);
其中,id是要删除的shader或程序的句柄。
如果,一个shader被删除之前没有从相应的程序中排除,那么这个shader不会被实际删除,而只是被标记为被删除;当shader被从程序中排除的时候,才会被真正地删除。