着色器和程序

这一章开始介绍着色器程序相关的内容。因为着色器的语法和 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 所示,增加了对编译和链接过程的检查。如果编译或链接过程中发生问题,则会直接断言,我们可以查看日志信息来进一步定位问题。

代码清单 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,来学习需要使用的函数。

代码清单 2 输出界面信息
  1. // Get the number of outputs
  2. GLint outputs;
  3. glGetProgramInterfaceiv(program, GL_PROGRAM_OUTPUT, GL_ACTIVE_RESOURCES, &outputs);
  4.  
  5. // A list of token describing the properties we wish to query
  6. static const GLenum props[] = { GL_TYPE, GL_LOCATION, GL_ARRAY_SIZE };
  7. static const char* prop_name[] = { "type", "location", "array size" };
  8.  
  9. // Various local variables
  10. GLint i;
  11. GLint params[4];
  12. GLchar name[64];
  13. const char* type_name;
  14. char buffer[1024];
  15.  
  16. glGetProgramInfoLog(program, sizeof(buffer), NULL, buffer);
  17.  
  18. overlay.print("Program linked\n");
  19. overlay.print(buffer);
  20.  
  21. for (i = 0; i < outputs; i++)
  22. {
  23.     // Get the name of the output
  24.     glGetProgramResourceName(program, GL_PROGRAM_OUTPUT, i, sizeof(name), NULL, name);
  25.     // Get other properties of the output
  26.     glGetProgramResourceiv(program, GL_PROGRAM_OUTPUT, i, 3, props, 3, NULL, params);
  27.     // type_to_name() is a function that returns the GLSL name of
  28.     // type given its enumerant value
  29.     type_name = type_to_name(params[0]);
  30.  
  31.     // print the result
  32.     if (params[2] != 0)
  33.     {
  34.         sprintf(buffer, "Index %d: %s %s[%d] @ location %d.\n",
  35.             i, type_name, name, params[2], params[1]);
  36.     }
  37.     else
  38.     {
  39.         sprintf(buffer, "Index %d: %s %s @ location %d.\n",
  40.             i, type_name, name, params[1]);
  41.     }
  42.     overlay.print(buffer);
  43. }

首先看到第 3 行使用 glGetProgramInterfaceiv() 函数来获取程序中的输出接口数量,其原型为

  • void glGetProgramInterfaceiv(GLuint program,
  •                              GLenum programInterface,
  •                              GLenum pname,
  •                              GLint *params);

其中,参数 program 为程序对象;programInterface 为 GL_PROGRAM_OUTPUTGL_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 关键字来声明,存放在统一变量中。

代码清单 3.1 片段着色器
  • #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

  1. subroutines[0] = glGetSubroutineIndex(render_program, GL_FRAGMENT_SHADER, "myFunction1");
  2. subroutines[1] = glGetSubroutineIndex(render_program, GL_FRAGMENT_SHADER, "myFunction2");
  3.  
  4. 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);
  • }

总结

至此,书本的第一部分——基础知识,就已经都学习完毕了。主要是了解了基本的顶点着色器和片段着色器:顶点着色器中我们可以利用顶点数组对象传递大量顶点数据,片段着色器中可以使用纹理数据赋予想要的贴图。同时这章也学习了着色器语言的基本特性。

后续的第二部分——深入探索,我翻目录看了一下,应该是按深入各个着色阶段进行组织的。接着学习吧!