管线
最近加班有点厉害,时隔一个多月才重拾 OpenGL 的学习。对着上次第 2 章的笔记,能快速回忆起之前的知识点,这让我感受到了笔记的好处。由此也勉励自己能更好的记录自己所学的内容。
第 3 章通读下来感觉是一个摘要,把各个管线阶段都一一简述了一下。也正因为是简述,所以实验起来略有困难,而且不解的地方也较多。
图 1 是涉及到的管线的概括,其中圆角框表示固定函数阶段,方角框表示可编程阶段。这章内容就是各个阶段的摘要概述。本篇文章针对其中的可编程阶段,跟着做了实验。
向顶点着色器传递数据
从图 1 可以看到顶点着色器是管线中第一个可编程阶段。在此之前还有一个顶点获取阶段,该阶段自动向顶点着色器提供输入。
顶点属性表示顶点数据引入 OpenGL 管线的手段。若要声明一个顶点属性,则需要在顶点着色器中用 in 储存限定符声明一个变量。清单 3.1 中使用 in 声明的 offset 变量就是顶点属性,借此我们就可以在之前固定的顶点位置上加上输入可变的偏移数据。
可以用顶点属性函数 glVertexAttrib*() 多个变量中的一个来指示本阶段用什么来填充该变量。在本例中使用 glVertexAttrib4fv() 函数:
- void glVertexAttrib4fv(GLuint index, const GLfloat *v);
第二个参数好理解,就是需要输入的 'float vector' 数据。而第一个参数需要看到清单 3.1 中的 layout (location = 0),这是一个布局限定符,此处将顶点属性的位置设置为 0。顶点属性位置也就是第一个参数 index,所以这边我们需要对照传入 0。
清单 3.2 使用 glVertexAttrib4fv 将每帧不断变化的 offset 传入清单 3.1 的顶点着色器。如例 1 中的视频所示,运行程序后就能得到一个以椭圆轨迹运行的三角形。
书中所给代码都是局部的。像例 1 中还使用上一章中的简单片段着色器(青色)。
同样,流程也是上一章的基本流程:基本的顶点着色器、片段着色器;编译着色器;创建和绑定顶点数组对象。
在阶段之间传递数据
在前面的例子中,我们已经能了解到数据输入输出的概念。接着肯定会有一个自然的想法:能否让颜色也逐帧变化?颜色数据可以通过顶点着色器传入,顶点着色器能否再将这个数据传出?
清单 3.3 实现了上述想法,内容基于清单 3.1,多声明了一个 color 输入变量。注意 color 变量的位置限定符指定属性位置为 1,后续通过 glVertexAttrib4fv 传入此属性时,index 参数也要设置为 1。再者,多声明了一个输出变量 vs_color,将颜色数据输入(color)传递给 vs_color(输出)。
清单 3.4 是一个片段着色器,它接收顶点着色器的输入,并将其按原本值输出。
同样我们使用 glVertexAttrib4fv 函数来指定 color 属性的值,如例 2 中我们就可以看到一个颜色变化的三角形。
接口块
当需要传递大量数据时,接口块显得更加紧凑和统一。接口块的概念和结构体非常类似,所以我们直接看代码,从中学习如何使用。
从清单 3.5 中可以看到声明的接口块名称为 VS_OUT,可以类比结构体名称。接口的实例名称是 vs_out,可以类比结构体变量名称。这边的接口块使用 out 关键字进行声明的,因为其用作输出。
接口块数据像这样使用:vs_out.color。可以看到使用方法和结构体也一致,非常好懂。
再看到清单 3.4 中使用接口块的片段着色器,接口块名称必须也是 VS_OUT,实例名称随意。这点也和结构体的惯用手段一致,想要数据内容一致,就使用同一名称的结构体,而声明的变量叫什么则没有关系。注意这边接口块使用 in 关键字声明,因为其获取的是输入数据。
实验时将两边的接口块名称或其中的成员故意写成不一致,虽然没编译错误,但是运行时发现三角形没有显示。
细分曲面
关于细分曲面,书中对它的定义为:细分曲面是将高阶基元(OpenGL 中称为贴片)分解为许多更小的、更简单的基元进行渲染的过程。按照自己通俗一点的理解就是,在一种基元内再拆分成多个小基元。这有点像建模里高模的意思,细分数越多,模型越精致。
细分曲面控制着色器
再看到图 1,细分曲面是一个固定的过程,而细分曲面控制着色器在其前面。从名字中也可以推测,细分曲面控制着色器是控制细分曲面如何分解贴片的。这部分还是一知半解,我们直接看着色器代码。
首先看到细分曲面控制着色器的清单 3.7,有一句 layout (vertices = 3) out:之前我们提及到细分曲面会将基元分解成贴片,而贴片又是由多个控制点组成的。这边的 vertices = 3 指定每个贴片的控制点数量为 3。细分曲面控制着色器负责将各种控制信息传递给细分曲面,所以这边使用 out 关键字声明。每个贴片的控制点数量可通过 glPatchParameteri() 进行设置,其中 pname 设置为 GL_PATCH_VERTICES 以及 value 设置为构建每个贴片将用到的控制点数量:
- void glPatchParameteri(GLenum pname, GLint value);
针对四个控制点,本地没有实验成功。留作一个问题。
细分曲面控制着色器还可以控制内外细分等级 gl_TessLevelInner 和 gl_TessLevelOuter,值设置的越高,细分的越细。我们可以更改它们的值,主观了解一下这两个变量的概念:图 2 是清单 3.7 中指定参数生成的结果;图 3 是在图 2 基础上仅加大 gl_TessLevelInner 值的结果,可以看到内部点变多了;图 4 是在图 2 基础上仅加大 gl_TessLevelOuter 值的结果,可以看到边上的点变多了,同时可以看到边上的段数就是 gl_TessLevelOuter 指定的值。
细分曲面评估着色器
下面说一下自己目前对细分曲面评估着色器的理解:细分曲面控制着色器将控制参数传递给细分曲面,细分曲面计算得出顶点作为细分曲面评估着色器的输入。所以细分曲面评估着色器的工作是评估这些输入顶点,以决定如何将这些顶点再次以基元的方式渲染出来。这边同样是直接关注到代码。
layout (triangles, equal_spacing, cw) in 指定选择模式为三角形,equal_spacing 和 cw 等其他限定符表示应沿细分多边形边缘等距生成新的顶点,以及应使用顺时针顶点环绕顺序生成三角形。在指定顶点位置时,可以看到使用到了 gl_TessCoord 变量,含义未知,待后续了解。
为了查看如图 2 至图 4 的这种细分曲面,我们需要仅绘制三角形的轮廓,否则颜色填充上去就看不到细节了。我们使用如下代码:
- glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
在这篇文章中,我们第一次接触细分曲面相关的着色器,需要注意 glCreateShader 创建着色器的参数类型指定。细分曲面控制着色器为 GL_TESS_CONTROL_SHADER,细分曲面评估着色器为 GL_TESS_EVALUATION_SHADER。同时因为着色器变多了,所以自己把着色器编译部分的代码重写了一下,后续着色器的变动只需要修改 TShader 数组里的内容即可。
- struct TShader
- {
- GLenum type;
- std::string codePath;
- };
- GLuint CompileShaders(void)
- {
- GLuint program;
- TShader tShaders[] = {
- { GL_VERTEX_SHADER, "VertexShader.vert" },
- { GL_TESS_CONTROL_SHADER, "3_7_TessControlShader.tesc" },
- { GL_TESS_EVALUATION_SHADER, "3_8_TessEvaluationShader.tese" },
- { GL_FRAGMENT_SHADER, "FragmentShader.frag" }
- };
- program = glCreateProgram();
- for (int i = 0; i < sizeof(tShaders) / sizeof(tShaders[0]); i++)
- {
- TGLcharPtr shaderSource = ReadShaderText(tShaders[i].codePath);
- const GLchar* shaderSources[] = { shaderSource.get() };
- GLuint shader = glCreateShader(tShaders[i].type);
- glShaderSource(shader, 1, shaderSources, NULL);
- glCompileShader(shader);
- // Compile Result
- GLint result = GL_FALSE;
- static GLchar log[10240];
- glGetShaderInfoLog(shader, sizeof(log), NULL, log);
- printf("%s\n", log);
- glGetShaderiv(shader, GL_COMPILE_STATUS, &result);
- assert(result == 1);
- glAttachShader(program, shader);
- glDeleteShader(shader);
- }
- glLinkProgram(program);
- return program;
- }
几何着色器
几何着色器提供了操作每个顶点的能力,我们可以结合“片段着色器提供操作每个像素的能力”,加以对比理解。
同时几何着色器还可以改变管线的中间基元模式。我们看到几何着色器代码:
清单 3.9 中,第一个配置限定符表明几何着色器期望输入数据为三角形。第二个配置限定符指示 OpenGL 此着色器将生成点,且每个着色器产生的点数最多为 3 个。为了能看清点,我们还需要使用 glPointSize() 来将点的大小设大。在细分着色器的基础上,最终生成的效果如图 5 所示,可以看到基元已经变成了点。
片段着色器
片段着色器是管线最后一个可编程阶段,这个着色器我们一开始就接触过了,通俗的讲就是和颜色相关。在几何着色器中,我们也提及到了片段着色器,说它提供操作每个像素的能力,让我们结合代码看看它如何可以操作每个像素。
从清单 3.10 中,我们可以看到使用到了 gl_FragCoord 内置变量。它提供每个片段的位置,代码中根据它为每个片段单独生成颜色。最终的效果如图 6 所示,可以看到网格的样式。
OpenGL 在渲染片段着色器阶段会进行插值处理,为此我们更改代码,做以下的实验:
清单 3.11 中定义了一个新的顶点着色器,它依据顶点输出不同的颜色,因此会输出 3 种不同的颜色。清单 3.12 简单的设置顶点着色器传来的颜色。实验结果如图 7 所示,可以看到图中不止 3 种颜色,三角形中间的颜色会有一个平滑的过渡。
记得去掉中间的细分着色器。如何跨越多个阶段传递数据是目前遗留的问题。
总结
至此图 1 中的每个可编程的阶段都已经介绍完毕,所以我在文章开头就说这章就像是摘要,书籍的后续内容应该就是针对各个着色器详细的铺开。
本章还讲解了视口、计算着色器等其余概念,笔记里没有记录(我也没看),因为书中没有对应的实验,等后续用到了再回过头来看。