着色器和程序
这一章开始介绍着色器程序相关的内容。因为着色器的语法和 C 语言类似,所以尽管没有讲这章,我们在此之前也已经写了很多着色器程序。
一些着色器的数据类型和内置函数这边就不赘述了,我们从编译、链接开始讲起。
检查编译和链接
编译
在一开始我们就知道了通过 glCompileShader() 函数编译着色器程序。此时会生成编译的日志信息,我们可以调用 glGetShaderInfoLog() 函数来获取,其原型为
- void glGetShaderInfoLog(GLuint shader,
- GLsizei bufSize,
- GLsizei *length,
- GLchar *infoLog);
可以看到获取编译日志信息时,需要指定存储日志字符串缓冲的大小。这个大小我们还不知道,我们可以事先通过 glGetShaderiv() 函数来获取,其原型为
- void glGetShaderiv(GLuint shader, GLenum pname, GLint *params);
要获取编译日志长度,第二个 pname 参数需要指定为 GL_INFO_LOG_LENGTH。
pname 还有一个参数很好用:GL_COMPILE_STATUS 可以获取编译的状态,即是否编译成功。
链接
同样 glLinkProgram() 的时候也会产生链接的日志信息。与编译类似,可以通过 glGetProgramInfoLog() 函数来获取,函数名就是从 Shader 变成了 Program。
类似的,glGetProgramiv() 的 GL_INFO_LOG_LENGTH 参数获取链接日志长度,GL_LINK_STATUS 参数获取链接状态。
值得一提的是,之前网上查到的关于着色器检查的文章中,对链接过程的检查似乎没有着重提及。链接过程的检查也相当有用,比如自己之前把 main 函数写成了 mian,如果有对链接过程进行检查,那么就能更快定位问题。
这一小节的内容,我感觉可以提前。可以免去不少定位“抄错的拼写”问题的时间。
后续我改写完善一下自己写的编译链接函数,如代码清单 1 所示,增加了对编译和链接过程的检查。如果编译或链接过程中发生问题,则会直接断言,我们可以查看日志信息来进一步定位问题。
接口匹配
OpenGL 中可以将各个着色器阶段组合,配置可分离程序的管线。这部分书中没有详细的完整例子,留做后续研究。因为编译时是各个独立的程序,对输入输出接口无法进行匹配,当程序真正组合在一起的时候,接口无法匹配的话,就可能发生错误。
这节书中给出了一个完整的例子,来获取程序中的输出接口信息。我们直接看到代码片段 2,来学习需要使用的函数。
首先看到第 3 行使用 glGetProgramInterfaceiv() 函数来获取程序中的输出接口数量,其原型为
- void glGetProgramInterfaceiv(GLuint program,
- GLenum programInterface,
- GLenum pname,
- GLint *params);
其中,参数 program 为程序对象;programInterface 为 GL_PROGRAM_OUTPUT 或 GL_PROGRAM_INPUT,此例中为输出;pname 参数应为 GL_ACTIVE_RESOURCES。指定这些参数后,参数 params 会返回接口的数量,我们后续可以根据接口的索引来进一步获取接口信息(索引范围为 0 到数量减 1)。
看到第 24 行,我们通过索引继续通过 glGetProgramResourceName() 函数来获取接口的名字,其原型为
- void glGetProgramResourceName(GLuint program,
- GLenum programInterface,
- GLuint index,
- GLsizei bufSize,
- GLsizei *length,
- GLchar *name);
其中的 program 和 programInterface 参数和 glGetProgramInterfaceiv 的含义一样。index 参数指定接口对应的索引;bufSize 参数指定存储接口名字字符串的空间大小;length 参数返回实际的字符串大小;name 参数返回接口名字。
再看到第 26 行,通过接口索引查询想要的属性信息,使用到了 glGetProgramResourceiv() 函数,其函数原型为
- void glGetProgramResourceiv(GLuint program,
- GLenum programInterface,
- GLuint index,
- GLsizei propCount,
- const GLenum *props,
- GLsizei bufSize,
- GLsizei *length,
- GLint *params);
其中的 program、programInterface 和 index 参数和 glGetProgramResourceiv 的含义一样。propCount 参数指定需要查询的属性数量,此处为 3 个,分别为 type、location 和 array size;props 参数指定需要查询的属性;bufSize 参数指定存储返回属性的空间大小;length 参数返回实际的查询结果大小;params 参数返回查询结果。
书中例子使用了 sb7::text_overlay 类,用来向窗体打印字符信息。其内部也是一个着色器程序,将字符通过纹理数据画出来,所以不要忘了引入资源文件。
使用 sb7::text_overlay 类时,要确保资源文件的路径能被读取到。
着色器子程序
不同但近似的功能使用分离的程序再组合起来,这样性能开销会有点大。这时使用子程序会是一个好的选择,它和 C 语言里的函数指针非常类似。函数指针对应于 OpenGL 里的子程序统一变量,同时我们还需要获取各个子程序对应的“地址”,以及如何指定子程序统一变量。
我们首先看到子程序的定义,如代码清单 3.1 所示,它定义在片段着色器中(总程序的顶点着色器这边就不贴出了,就是简单的全屏范围的四个点)。可以看到子程序类型通过关键字 subroutine 来声明。之后我们可以通过声明的子程序类型来定义具体的子程序,这边声明了 myFunction1 和 myFunction2,它们返回不一样的颜色。“函数指针”通过 subroutine uniform 关键字来声明,存放在统一变量中。
如下代码所示,我们可以通过 glGetSubroutineIndex() 函数来获取实际子程序的索引,即类比的函数地址。使用 glGetSubroutineUniformLocation() 函数获取子程序统一变量的位置。这边需要注意一下第二个参数,它代表着色器阶段,这边我们的子程序声明在片段着色器中,所以参数指定为 GL_FRAGMENT_SHADER。
- subroutines[0] = glGetSubroutineIndex(render_program, GL_FRAGMENT_SHADER, "myFunction1");
- subroutines[1] = glGetSubroutineIndex(render_program, GL_FRAGMENT_SHADER, "myFunction2");
- uniforms.subroutine1 = glGetSubroutineUniformLocation(render_program, GL_FRAGMENT_SHADER, "mySubroutineUniform");
最后看到如下的渲染函数,通过 glUniformSubroutinesuiv() 函数设置子程序统一变量的值。即会根据当前运行时间设置不同的背景颜色。
- void subroutines_app::render(double currentTime)
- {
- int i = int(currentTime);
- glUseProgram(render_program);
- glUniformSubroutinesuiv(GL_FRAGMENT_SHADER, 1, &subroutines[i & 1]);
- glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
- }
总结
至此,书本的第一部分——基础知识,就已经都学习完毕了。主要是了解了基本的顶点着色器和片段着色器:顶点着色器中我们可以利用顶点数组对象传递大量顶点数据,片段着色器中可以使用纹理数据赋予想要的贴图。同时这章也学习了着色器语言的基本特性。
后续的第二部分——深入探索,我翻目录看了一下,应该是按深入各个着色阶段进行组织的。接着学习吧!