Chapter 3. Following the Pipeline

本章目标

了解OpenGL pipeline中的每一步都做了什么
如何将shader和固定处理块联系起来
如何创建一个program使得pipeline中的每一步都同步进行

向Vertex Shader中传输数据

vertex shader是pipeline中的第一个可编程步骤,也是图形管线中唯一一个强制的步骤。在vertex shader之前,还有一步固定处理快,名为vertex fetching(有时也叫vertex pulling),这一步为vertex shader提供了输入。

顶点属性(Vertex Attributes)

在 GLSL 中,将数据传入和传出着色器的机制是使用 in 和 out 存储限定符声明全局变量。in中的数据由固定函数顶点获取阶段自动填充。顶点属性是将顶点数据引入 OpenGL 管道的方式。要声明顶点属性,请使用 in 限定符在顶点着色器中声明变量。

#version 450 core
// 'offset' is an input vertex attribute
layout (location = 0) in vec4 offset;
void main(void)
{
	const vec4 vertices[3] = vec4[3](vec4(0.25, -0.25, 0.5, 1.0),
	vec4(-0.25, -0.25, 0.5, 1.0),
	vec4(0.25, 0.25, 0.5, 1.0));
	// Add 'offset' to our hard-coded vertex position
	gl_Position = vertices[gl_VertexID] + offset;
}

上述的shader接收offset作为gl_Position的偏移量,虽然in中的数据是由fixed function填充的,但是我们需要告诉fixed function要填充什么数据。又很多函数可以实现这个功能,书中介绍了glVertexAttrib4fv()这个方法,void glVertexAttrib4fv(GLuint index, const GLfloat * v); index是输入attribute的索引,也就是layout (location = 0) in vec4 offset;中的location=0中的0,v就是输入的数据,在这里也就是offset这个东西。

virtual void render(double currentTime)
{
    const GLfloat color[] = { (float)sin(currentTime) * 0.5f + 0.5f,
        (float)cos(currentTime) * 0.5f + 0.5f, 0.0f, 1.0f };
    glClearBufferfv(GL_COLOR, 0, color);
    // Use the program object we created earlier for rendering
    glUseProgram(rendering_program);
    GLfloat attrib[] = { (float)sin(currentTime) * 0.5f, 
        (float)cos(currentTime) * 0.6f, 0.0f, 0.0f };
    // Update the value of input attribute 0
    glVertexAttrib4fv(0, attrib);
    // Draw one triangle
    glDrawArrays(GL_TRIANGLES, 0, 3);
}

这个代码运行起来是一个做圆周运动的三角形。

各个Stage之间传递数据

在一个着色器中写入输出变量的任何内容都会发送到在后续阶段使用 in 关键字声明的类似名称的变量。例如,如果顶点着色器使用 out 关键字声明名为 vs_color 的变量,它将与片段着色器阶段中使用 in 关键字声明的名为 vs_color 的变量匹配(假设中间没有其他阶段处于活动状态)。

#shader vertex
layout (location = 0) in vec4 offset;
layout (location = 1) in vec4 color;
out vec4 vs_color;
void main(void)
{
	const vec4 vertices[3] = vec4[3](vec4(0.25, -0.25, 0.5, 1.0),
		vec4(-0.25, -0.25, 0.5, 1.0),
		vec4(0.25, 0.25, 0.5, 1.0));
 // Add 'offset' to our hard-coded vertex position
	gl_Position = vertices[gl_VertexID] + offset;
 // Output a fixed value for vs_color
	vs_color = color;
}

修改vectex shader使得它多接受了一个attribute,由于索引值0已经被占用了,所以这里的索引值为layout = 1。

#shader fragment
#version 450 core
// Input from the vertex shader
in vec4 vs_color;
// Output to the framebuffer
out vec4 color;
void main(void)
{
 // Simply assign the color we were given by the vertex shader to our output
	color = vs_color;
}

在fragment中加上相应的in字段接收数据,并且在我们的主程序中传递数据即可。

Interface Blocks(接口块)

我们可能希望传递数组等复杂的数据结构,所以需要用到接口块

#shader vertex
#version 450 core
// 'offset' is an input vertex attribute
layout (location = 0) in vec4 offset;
layout (location = 1) in vec4 color;
// Declare VS_OUT as an output interface block
out VS_OUT
{
	vec4 color; // Send color to the next stage
} vs_out;
void main(void)
{
	const vec4 vertices[3] = vec4[3](vec4(0.25, -0.25, 0.5, 1.0),
		vec4(-0.25, -0.25, 0.5, 1.0),
		vec4(0.25, 0.25, 0.5, 1.0));
	// Add 'offset' to our hard-coded vertex position
	gl_Position = vertices[gl_VertexID] + offset;
	// Output a fixed value for vs_color
	vs_out.color = color;
}

请注意,上述接口模块既有块名(VS_OUT,大写),也有实例名(vs_out,小写)。接口块使用块名称(在本例中为 VS_OUT)在阶段之间匹配,但在着色器中使用实例名称进行引用。也就是说,我们在in和out中,需要保证块名的一致,但是实例名可以自己写。

#shader fragment
#version 450 core
// Declare VS_OUT as an input interface block
in VS_OUT
{
	vec4 color; // Send color to the next stage
} fs_in;
// Output to the framebuffer
out vec4 color;
void main(void)
{
 // Simply assign the color we were given by the vertex shaderto our output
	color = fs_in.color;
}

看起来这样做(既有块名,又有实例名)好像是多余的,但是按块名称匹配接口块,却允许块实例在每个着色器阶段具有不同的名称有两个重要目的。第一个显而易见,就是可以让shader内部使用的名字不同,因为实例名是shader内部自定义的可以随意改变的。第二个原因也是主要的原因是因为可能虽然前一个shader输出的不是数组,但是后一个可以用数组接收,比如说从vertex shader到fragment shader,因为点可以有很多,所以每一个点传递到fragment shader都会有一个out值,这样的话我们可以在实例名的地方加上[i]来指定特定的点传输的数据。

out VS_OUT {
    vec3 color;
} vs_out;

in VS_OUT {
    vec3 color;
} gs_in[]; // 接口块数组,每个元素对应一个顶点的数据

Tessellation(曲面细分)

曲面细分是将高阶基元(在 OpenGL 中称为patch(面片))分解为许多更小、更简单的基元(例如用于渲染的三角形)的过程。OpenGL 包括一个固定功能(fixed-function)、可配置的曲面细分引擎(configurable tessellation engine),该引擎能够将四边形、三角形和直线分解为大量可能更小的点、直线或三角形,这些点、直线或三角形可以直接由管道中的普通光栅化硬件(normal rasterization hardware)使用。从逻辑上讲,曲面细分阶段直接位于 OpenGL 管道中的顶点着色阶段之后,由三个部分组成:曲面细分控制着色器(tessellation control shader)、固定函数曲面细分引擎(fixed-function tessellation engine)和曲面细分评估着色器(tessellation evaluation shader)。

Tessellation Control Shaders(曲面细分控制着色器)

三个曲面细分阶段中的第一个阶段是曲面细分控件着色器(tessellation control shader)。此着色器从顶点着色器(Vertex shader)获取输入,主要负责两件事:确定将发送到曲面细分引擎(fixed-function tessellation engine)的曲面细分级别,以及生成将发送到曲面细分后运行的曲面细分评估着色器(tessellation evaluation shader)的数据。

OpenGL 中的曲面细分的工作原理是将称为面片的高阶曲面分解为点、线或三角形。每个贴片由多个控制点组成。每个面片的控制点数是可配置的,并通过调用 glPatchParameteri(GLenum pname, GLint value)进行设置,其中 pname 设置为 GL_PATCH_VERTICES,value 设置为将用于构建每个补丁的控制点数。

默认情况下,每个面片的控制点数为 3 个,可用于形成单个补丁的最大控制点数是实现定义的,但保证至少为 32 个。

Vertex shader为每一个顶点都运行一次,而Tessellation Control Shader按组运行。顶点着色器的结果,按照上述面片控制的点数,分批传送到Tessellation Control Shader。可以更改每个面片的控制的点的数量,以便Tessellation Control Shader输出的控制点数可以不同于它使用的控制点数。Tessellation Control Shader生成的控制点数是使用控件着色器源代码中的输出布局限定符设置的

layout (vertices = N) out;

这里,N 是每个补丁的控制点数。Tessellation Control Shader负责计算输出控制点的值,并为将发送到固定功能曲面细分引擎(fixed-function tessellation engine)的生成面片的曲面细分因子(tessellation factors for the resulting patch)。输出的曲面细分因子将写入gl_TessLevelInner和gl_TessLevelOuter这两个内置输出变量,而沿管道传递的任何其他数据将照常写入用户定义的输出变量(使用 out 关键字或特殊的内置 gl_out 数组声明的输出变量)。

#shader tesselation_control
#version 450 core
layout (vertices = 3) out;
void main(void)
{
 // Only if I am invocation 0 ...
	 if (gl_InvocationID == 0)
	 {
		gl_TessLevelInner[0] = 5.0;
		gl_TessLevelOuter[0] = 5.0;
		gl_TessLevelOuter[1] = 5.0;
		gl_TessLevelOuter[2] = 5.0;
	 }
	 // Everybody copies their input to their output
	gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}

gl_InvocationID是分批传进control shader的批次,gl_TessLevelInner用于设置面片内部的细分等级。对于三角形面片,只有一个元素。gl_TessLevelOuter用于设置面片边缘的细分等级,对于三角形面片,有三个元素分别对应三角形的三条边。if (gl_InvocationID == 0)是因为这个基础设置只需要调用一次即可不需要重复执行。gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;将每一批的坐标点原封不动的发送给下一步的shader(也就是evaluation shader)

The Tessellation Engine(曲面细分引擎)

曲面细分引擎是 OpenGL 管道的固定功能部分,它的输入是面片的高阶曲面,将它们分解为更简单的基元,例如点、线或三角形。在曲面细分引擎接收面片之前,曲面细分控件着色器会处理传入的控制点,并设置用于分解补丁的曲面细分因子。曲面细分引擎负责生成参数,这些参数提供给曲面细分评估着色器的调用,然后使用这些参数来转换生成的基元并使其准备好进行栅格化。

Tessellation Evaluation Shaders(细分曲面计算着色器)

上述过程运行结束之后,生成了一些表示基元的点。细分曲面计算着色器会对曲面细分器生成的每一个顶点进行一次调用(也就是说,如果你上面设置的曲面细分等级过高的话,这里会有巨大的运算量)

#shader tessellation_evaluation
#version 450 core
layout (triangles, equal_spacing, cw) in;
void main(void)
{
	gl_Position = (gl_TessCoord.x * gl_in[0].gl_Position +
	gl_TessCoord.y * gl_in[1].gl_Position +
	gl_TessCoord.z * gl_in[2].gl_Position);
}

triangles 表示细分图元的类型是三角形,equal_spacing 指定细分点的间距模式为等距,cw 表示顶点顺序为顺时针(clockwise)。

其实一开始我很迷惑,明明中间的那步细分器对点的数量进行了膨胀,为什么这里可以直接使用上一步control shader传来的数据,点的数量都变了那么gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;这一步中的gl_out[idx]中的数组长度也应该变化啊怎么能直接使用呢?
其实本质还是我根本没搞清这个地方在干什么。

在三角形细分的情况下,gl_TessCoord 是一个包含三个分量的向量(vec3),每个分量代表当前顶点相对于三角形补丁中一个控制点的重心权重。这三个分量的和总是等于 1。例如,如果 gl_TessCoord 为 (1, 0, 0),那么当前顶点就位于第一个控制点的位置;如果 gl_TessCoord 为 (0, 1, 0),那么当前顶点就位于第二个控制点的位置;如果 gl_TessCoord 为 (0, 0, 1),那么当前顶点就位于第三个控制点的位置。如果 gl_TessCoord 为 (0.5, 0.5, 0),那么当前顶点就位于第一个和第二个控制点之间的中点。

gl_in[0],gl_in[1]和gl_in[2] 与 control shader 中的gl_out[gl_InvocationID]并不是一一对应的,gl_out[gl_InvocationID]是针对于面片id而言,也就是说一个面片会对应一个ID,而evaluation shader中gl_in的索引是当前面片中的第n个点。也就是说如果是三角形,那么这个gl_in的长度就是3,如果是四边形,那么这个gl_in的长度就是4。所以总的来说,可以理解为这个gl_in对于当前若干个由第x个面片生成的点来说,调用的这个shader中的gl_TessCoord是相对于原本顶点的位置,是不同的;而gl_in[0], gl_in[1], gl_in[2]是对应tessellation_control输出的gl_out[x]中的值,是相同的。所以这份shader代码就是将三个顶点权重各自乘上他们的坐标而已。比如说gl_TessCoord.x * gl_in[0].gl_Position 是计算当前顶点与第一个控制点的位置之间的插值。(这里的x, y, z并不是坐标的xyz)

glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
GLCall(glDrawArrays(GL_PATCHES, 0, 3));

调用glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);使得我们看到线条而不是整个填充的三角形
别忘了glDrawArrays(GL_PATCHES, 0, 3)改成GL_PATCHES

Geometry Shaders(几何着色器)

几何着色器是front end中的最后一步。几何着色器为每个基元运行一次,并且可以访问构成正在处理的基元的所有顶点的所有输入顶点数据。
几何着色器很特别因为它可以控制pipeline中数据的数量,几何着色器有两个函数EmitVertex()和EndPrimitive(),可以显式地生成发送到图元装配(Primitive assembly)和光栅化(rasterization)的顶点。
几何着色器另一个特点是可以在管线中改变图元模式。例如,他们可以将三角形作为输入并生成一堆点或线作为输出,甚至可以从独立点创建三角形。

#shader geometry
#version 450 core
layout (triangles) in;
layout (points, max_vertices = 3) out;
void main(void)
{
	int i;
	for (i = 0; i < gl_in.length(); i++)
	{
		gl_Position = gl_in[i].gl_Position;
		EmitVertex();
	}
}

layout (points, max_vertices = 3) out;告诉 OpenGL,几何着色器将生成点,并且每个着色器将生成的最大点数为 3 个。EmitVertex()将gl_Position发送到下一个管线阶段。EndPrimitive()表示结束当前图元。由于我们这里一次shader就是一个图元,就不用手动调用了,shader最后会帮我们调用一次EndPrimitive()。

多加了个shader,居然变成点了!这是因为layout (points, max_vertices = 3) out;将输出换成点了,如果这句代码改成layout (triangle_strip, max_vertices = 3) out; 就还是个完整的三角形。

Primitive Assembly, Clipping, and Rasterization(图元装配,剪裁和光栅化)

管道的前端运行后(包括顶点着色、曲面细分和几何着色),管道的固定功能部分会执行一系列任务,这些任务获取场景的顶点表示并将其转换为一系列像素,依次需要着色并写入屏幕。

此过程的第一步是图元装配(Primitive Assembly),即将顶点分组为直线和三角形。对于点,基元装配仍然会发生,但在这种情况下,它是微不足道的。从其各个顶点构造基元后,它们将针对可显示区域(通常意味着窗口或屏幕)进行裁剪(Clip)。最后,确定为可能可见的基元部分被发送到称为光栅器(rasterizer)的固定功能子系统(fixed-function subsystem)。此块确定基元(点、线或三角形)覆盖哪些像素,并将像素列表发送到下一阶段,即片段着色。

Clipping(裁剪)

先滚去学图形学了

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

大纲

Share the Post:
滚动至顶部