着色器和程序
这一章开始介绍着色器程序相关的内容。因为着色器的语法和 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 所示,增加了对编译和链接过程的检查。如果编译或链接过程中发生问题,则会直接断言,我们可以查看日志信息来进一步定位问题。
- GLuint TUtils::CompileShaders(TShaderFileInfo shaderInfos[], uint32_t shaderCount)
- {
- GLint logLen;
- GLuint program = glCreateProgram();
- for (uint32_t i = 0; i < shaderCount; i++)
- {
- TGLcharPtr shaderSource = ReadShaderText(shaderInfos[i].codePath);
- const GLchar* shaderSources[] = { shaderSource.get() };
- GLuint shader = glCreateShader(shaderInfos[i].type);
- glShaderSource(shader, 1, shaderSources, NULL);
- glCompileShader(shader);
- // Now, get the info log length
- glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLen);
- // Allocate a string for it
- std::string str;
- str.reserve(logLen);
- // Get the log
- glGetShaderInfoLog(shader, logLen, NULL, (GLchar*)str.data());
- printf("%s\n", str.c_str());
- GLint result = GL_FALSE;
- glGetShaderiv(shader, GL_COMPILE_STATUS, &result);
- assert(result == 1);
- glAttachShader(program, shader);
- glDeleteShader(shader);
- }
- glLinkProgram(program);
- glGetProgramiv(program, GL_INFO_LOG_LENGTH, &logLen);
- std::string str;
- str.reserve(logLen);
- glGetProgramInfoLog(program, logLen, NULL, (GLchar*)str.data());
- printf("%s\n", str.c_str());
- GLint result = GL_FALSE;
- glGetProgramiv(program, GL_LINK_STATUS, &result);
- assert(result == 1);
- return program;
- }
接口匹配
OpenGL 中可以将各个着色器阶段组合,配置可分离程序的管线。这部分书中没有详细的完整例子,留做后续研究。因为编译时是各个独立的程序,对输入输出接口无法进行匹配,当程序真正组合在一起的时候,接口无法匹配的话,就可能发生错误。
这节书中给出了一个完整的例子,来获取程序中的输出接口信息。我们直接看到代码片段 2,来学习需要使用的函数。
- // Get the number of outputs
- GLint outputs;
- glGetProgramInterfaceiv(program, GL_PROGRAM_OUTPUT, GL_ACTIVE_RESOURCES, &outputs);
- // A list of token describing the properties we wish to query
- static const GLenum props[] = { GL_TYPE, GL_LOCATION, GL_ARRAY_SIZE };
- static const char* prop_name[] = { "type", "location", "array size" };
- // Various local variables
- GLint i;
- GLint params[4];
- GLchar name[64];
- const char* type_name;
- char buffer[1024];
- glGetProgramInfoLog(program, sizeof(buffer), NULL, buffer);
- overlay.print("Program linked\n");
- overlay.print(buffer);
- for (i = 0; i < outputs; i++)
- {
- // Get the name of the output
- glGetProgramResourceName(program, GL_PROGRAM_OUTPUT, i, sizeof(name), NULL, name);
- // Get other properties of the output
- glGetProgramResourceiv(program, GL_PROGRAM_OUTPUT, i, 3, props, 3, NULL, params);
- // type_to_name() is a function that returns the GLSL name of
- // type given its enumerant value
- type_name = type_to_name(params[0]);
- // print the result
- if (params[2] != 0)
- {
- sprintf(buffer, "Index %d: %s %s[%d] @ location %d.\n",
- i, type_name, name, params[2], params[1]);
- }
- else
- {
- sprintf(buffer, "Index %d: %s %s @ location %d.\n",
- i, type_name, name, params[1]);
- }
- overlay.print(buffer);
- }
首先看到第 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 关键字来声明,存放在统一变量中。
- #version 420 core
- // First, declare the subroutine type
- subroutine vec4 sub_mySubroutine(vec4 param1);
- // Next declare a coule of function that can be used as subroutine
- subroutine (sub_mySubroutine)
- vec4 myFunction1(vec4 param1)
- {
- return param1 * vec4(1.0, 0.25, 0.25, 1.0);
- }
- subroutine (sub_mySubroutine)
- vec4 myFunction2(vec4 param1)
- {
- return param1 * vec4(0.25, 0.25, 1.0, 1.0);
- }
- // Finally, declare a subroutine uniform that can be 'pointed'
- // at subroutine functions matching its signature
- subroutine uniform sub_mySubroutine mySubroutineUniform;
- // Output color
- out vec4 color;
- void main(void)
- {
- // Call subroutine through uniform
- color = mySubroutineUniform(vec4(1.0));
- }
如下代码所示,我们可以通过 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);
- }
总结
至此,书本的第一部分——基础知识,就已经都学习完毕了。主要是了解了基本的顶点着色器和片段着色器:顶点着色器中我们可以利用顶点数组对象传递大量顶点数据,片段着色器中可以使用纹理数据赋予想要的贴图。同时这章也学习了着色器语言的基本特性。
后续的第二部分——深入探索,我翻目录看了一下,应该是按深入各个着色阶段进行组织的。接着学习吧!