一、说明
片段着色器是着色器阶段,用于将光栅化生成的片段处理成一组颜色和单个深度值。片段着色器是基元栅格化后的 OpenGL 管道阶段。对于基元覆盖的每个像素样本,都会生成一个“片段”。每个片段都有一个窗口空间位置和一些其他值,并且它包含上一个顶点处理阶段的所有插值每个顶点输出值。
片段着色器的输出是一个深度值、一个可能的模具值(片段着色器未修改)以及可能写入当前帧缓冲区中的缓冲区的零个或多个颜色值。
片段着色器将单个片段作为输入,并生成单个片段作为输出。
二、自选
从技术上讲,片段着色器是一个可选的着色器阶段。如果未使用片段着色器,则输出片段的颜色值具有未定义的值。但是,输出片段的深度和模具值与输入具有相同的值。
这对于进行渲染非常有用,其中唯一有用的输出是片段的深度,并且您希望使用系统计算的深度,而不是其他深度。这种仅深度渲染用于阴影贴图操作以及深度预通过优化。
三、特别行动
与其他着色器阶段不同,片段着色器生成了隐式派生。因此,他们可以使用大多数纹理功能。您仍然需要注意不均匀的流量控制。
片段着色器还可以访问 discard 命令。执行时,此命令将导致片段的输出值被丢弃。因此,片段不会进入下一个管道阶段,并且任何片段着色器输出都将丢失。尽管从技术上讲,片段着色器的执行会因丢弃而停止,但在实际系统上,它可能会继续执行。此类系统需要防止在丢弃后发出的图像存储、原子计数器和着色器存储缓冲区对象写入工作(在丢弃之前执行的此类操作按预期工作)。
通常,大多数每个样本处理步骤都发生在片段着色器之后。但是,在 OpenGL 4.2 或 ARB_shader_image_load_store 中,着色器可以强制执行早期片段测试,这可确保在片段着色器执行之前进行丢弃片段的条件每样本测试。为此,在着色器中使用了以下语法:
layout(early_fragment_tests) in;
这对于确保仅在片段可见时才执行昂贵的加载/存储操作非常有用。
警告:这并不意味着你可以颠覆深度测试的意义。例如,不能使用一个值执行深度测试,然后将与测试的值不同的值写入深度缓冲区。如果在强制执行早期片段测试时尝试写入 gl_FragDepth,则将忽略写入的值。写入深度缓冲区的值将始终是针对深度缓冲区测试的值。同样,如果丢弃片段,如果设置了适当的操作,模板测试仍将导致更新模板缓冲区。
四、输入
片段着色器的输入要么由系统生成,要么从先前的固定函数操作传递,并可能在基元的表面上插值。
此片段着色器接收的用户定义输入将根据在此片段着色器声明的输入变量上声明的插值限定符进行插值。片段着色器的输入变量必须根据着色器阶段之间的接口匹配规则进行声明。具体而言,在此阶段和程序或管道对象中的最后一个顶点处理着色器阶段之间。
4.1 系统输入
片段着色器具有以下内置输入变量。
in vec4 gl_FragCoord;
in bool gl_FrontFacing;
in vec2 gl_PointCoord;
- gl_FragCoord片段在窗口空间中的位置。X、Y 和 Z 分量是片段的窗口空间位置。如果此着色器阶段未写入 Z 值,则 Z 值将写入深度缓冲区gl_FragDepth。gl_FragCoord 的 W 分量是 1/W剪辑,其中 W剪辑是从最后一个顶点处理阶段
- gl_Position的剪辑空间顶点位置输出的插值 W 分量。
可以通过使用特殊输入布局限定符重新声明gl_FragCoord来修改gl_FragCoord空间:
layout(origin_upper_left) in vec4 gl_FragCoord;
这意味着gl_FragCoord窗口空间的原点将是屏幕的左上角,而不是通常的左下角。
layout(pixel_center_integer) in vec4 gl_FragCoord;
OpenGL 窗口空间的定义使像素中心位于半整数边界上。所以左下角像素的中心是 (0.5, 0.5)。使用pixel_center_integer调整gl_FragCoord,使整数值表示像素中心。
这两者都与 D3D 的窗口空间兼容。除非您需要着色器具有此兼容性,否则建议您不要使用这些功能。
- gl_FrontFacing
如果片段是由基元的背面生成的,则为假;在所有其他情况下(包括没有背面的基元)都是如此。 - gl_PointCoord
点基元中的位置,用于定义片段相对于点边的位置。点被有效地栅格化为特定像素大小的窗口空间方块。由于点由单个顶点定义,因此判断特定片段在该正方形中的位置的唯一方法是使用gl_PointCoord。 - gl_PointCoord
坐标的值范围为 [0, 1]。默认情况下,OpenGL 使用左上角的点坐标原点,因此 (0, 0) 是左上角。但是,可以通过调用:
glPointParameteri(GL_POINT_SPRITE_COORD_ORIGIN, GL_LOWER_LEFT)`
将原点切换到左下角的原点;
OpenGL 4.0 及更高版本定义了系统生成的其他输入值:
in int gl_SampleID;
in vec2 gl_SamplePosition;
in int gl_SampleMaskIn[];
- gl_SampleID
这是栅格化此片段的当前样本的整数标识符。
警告:任何使用此变量都会强制按样本评估此着色器。由于多重采样的大部分意义在于避免这种情况,因此您应该只在必要时使用它。 - gl_SamplePosition
这是片段的当前样本在像素区域内的位置,其值范围为 [0, 1]。原点是像素区域的左下角。
警告:任何使用此变量都会强制按样本评估此着色器。由于多重采样的大部分意义在于避免这种情况,因此您应该只在必要时使用它。 - gl_SampleMaskIn
使用多重采样时,此变量包含要生成的片段的样本掩码的位字段。数组的长度与填充 GL 实现支持的样本数所需的长度一样长。
某些 Fragment 着色器内置输入将采用 OpenGL 指定的值,但这些值可以被用户控件覆盖。
in float gl_ClipDistance[];
in int gl_PrimitiveID;
- gl_ClipDistance
此数组包含插值裁剪平面半空间,作为上一个顶点处理阶段的顶点的输出。 - gl_PrimitiveID
此值是此绘图命令呈现的当前基元的索引。这包括应用于网格的任何曲面细分,因此每个单独的基元都具有唯一的索引。
但是,如果 Geometry Shader 处于活动状态,则gl_PrimitiveID完全是 GS 提供的输出。通常,gl_PrimitiveID保证是唯一的,因此,如果两个 FS 调用具有相同的基元 ID,则它们来自同一个基元。但是,如果 GS 处于活动状态并输出非唯一值,则对不同基元的不同片段着色器调用将获得相同的值。如果 GS 未输出 gl_PrimitiveID 的值,则片段着色器将获得未定义的值。
警告:上面对 gl_PrimitiveID 的讨论是基于对 OpenGL 4.6 规范的特定解读。然而,规范本身与这种观点有些不一致,这表明基元 ID 只能根据馈送到系统的数据来递增,而不是根据曲面细分器生成的数据。Vulkan 规范似乎同意这种解释,并且已知至少有一个实现也同意这一点。在对这个问题进行一些澄清之前,您应该认为上述内容是有问题的。
GL 4.3 提供以下附加输入:
in int gl_Layer;
in int gl_ViewportIndex;
- gl_Layer : 这要么是 0,要么是 Geometry Shader 输出的图层编号。
- gl_ViewportIndex:这要么是 0,要么是 Geometry Shader 的此基元输出的视口索引。
五、输出
片段着色器中用户定义的输出变量只能是以下 GLSL 类型:浮点数、整数、向量。它们也可以是这些类型之一的数组,包括数组数组(尽管强烈建议您避免这种情况)。用户定义的输出变量也不能聚合到接口块中。
5.1 输出缓冲区
片段着色器的用户定义输出表示一系列“颜色”。这些颜色值根据 glDrawBuffers 状态定向到特定缓冲区。这些被称为“片段颜色”,但您可以像对待任何任意数据一样对待它们。任何不是由 FS 写入的片段颜色值都将具有未定义的值;这些未定义的值仍可以通过“glDrawBuffers”路由到缓冲区。
与顶点着色器输入分配非常相似,有三种方法可以将输出变量与颜色编号相关联。分配这些方法的方法按优先级顺序列出,优先级最高的方法在前面。优先级较高的方法优先于后面的方法。
5.2 着色器内规范
着色器在内部定义片段颜色。这是使用以下布局语法完成的:
layout(location = 3) out vec4 diffuseColor;
这会将 diffuseColor 的片段颜色设置为 3。
5.3 预链接规范
在链接包含片段着色器的程序之前,用户可以告诉 OpenGL 将特定的输出变量分配给特定的片段颜色。这是通过以下函数完成的:
void glBindFragDataLocation(GLuint program, GLuint colorNumber, const char * name);
- colorNumber 是要分配的片段颜色。name 是要为其分配给定片段颜色的片段着色器输出的名称。
请注意,为片段着色器中未提及的片段颜色分配名称是完全合法的。链接过程将仅使用片段着色器中实际提及的名称。正因为如此,为同一个号码分配多个名字也是完全合法的;仅当您尝试链接使用这两个名称的程序时,这才是一个错误。
5.4 自动分配
如果前两种方法均未将输出分配给片段颜色,则在链接程序时,OpenGL 会自动分配片段颜色。分配的片段颜色是完全任意的,对于链接的不同程序,即使它们使用完全相同的片段着色器代码,也可能不同。
片段着色器输出的自动分配甚至比顶点着色器输入的更没有意义。颜色编号(如下所述)是指由 glDrawBuffers 定义的绘制缓冲区。您很可能会对许多不同的程序使用相同的帧缓冲(而您可能会经常更改 VAO);更改 glDrawBuffers 状态并每次重新验证 FBO 可能不是一个好主意。
数组输出的片段颜色索引是连续分配的,就像顶点属性一样。
片段颜色数量的限制由GL_MAX_DRAW_BUFFERS定义,使用双源混合时GL_MAX_DUAL_SOURCE_DRAW_BUFFERS如下。
片段颜色与帧缓冲区中实际缓冲区之间的映射由 glDrawBuffers 定义,这是帧缓冲区状态的一部分。例如,如果我们像这样设置绘制缓冲区:
const GLenum buffers[] = {GL_COLOR_ATTACHMENT4, GL_COLOR_ATTACHMENT2, GL_NONE, GL_COLOR_ATTACHMENT0};
glDrawBuffers(4, buffers);
我们设置输出变量如下:
layout(location = 1) out int materialID;
layout(location = 4) out vec3 normal;
layout(location = 0) out vec4 diffuseColor;
layout(location = 3) out vec3 position;
layout(location = 2) out vec4 specularColor;
diffuseColor 使用片段颜色 0。传递给 glDrawBuffers 的数组的第 0 个索引包含GL_COLOR_ATTACHMENT4。因此,写入 diffuseColor 的值将转到绑定到 GL_COLOR_ATTACHMENT4 插槽的缓冲区。下面是输出和相关缓冲区的表格:
为 normal 分配了 GL_NONE (因此写入的值是 discard) ,因为它尝试访问超出传递给 glDrawBuffers 的数组大小的索引。所有这些索引都隐式使用 GL_NONE。specularColor 被分配GL_NONE因为这是为数组的第二个索引(从零开始)给出的。
六、双源混合
主条目:混合#Dual_Source_Blending
双源混合是一种技术,通过混合,可以在同一缓冲区上使用两个(或理论上更多)输出值。这是一种将更多值放入混合方程的方法。
这是通过为分段输出变量分配另一个参数来实现的:索引。除了片段颜色编号外,所有输出都有一个索引。如果为任何输出分配了非零索引,则片段着色器将使用双源混合。
当片段着色器提供双源输出时,它会减少它可以写入的缓冲区数。新的最大片段颜色输出为 GL_MAX_DUAL_SOURCE_DRAW_BUFFERS,在支持此功能的每个硬件上为 1。简而言之:如果要双源混合,则只能从片段着色器写入一个缓冲区。
片段着色器输出的索引可以与片段颜色类似地分配。有一个着色器内规范:
layout(location = 0, index = 1) out vec4 diffuseColor1;
layout(location = 0) out vec4 diffuseColor0;
如果未分配索引,则使用默认索引 0。因此,diffuseColor0 被分配给片段颜色 0,索引 0,而 diffuseColor1 被分配片段颜色 0,索引 1。
还可以使用 glBindFragDataLocationIndex 为索引分配预链接。优先级规则的工作方式与 glBindFragDataLocation 的工作方式相同。说到这里,glBindFragDataLocation 总是分配一个索引 0。
如果这两种方法均未使用,则分配的索引将始终为 0。如果没有某种手动分配,您将无法获得双源输出。
七、其他输出
片段着色器具有以下内置输出变量。
out float gl_FragDepth;
gl_FragDepth
此输出是片段的深度。如果着色器没有静态写入此值,则它将采用 gl_FragCoord.z 的值。
对变量进行“静态写入”意味着在程序中的任何位置都写入该变量。即使由于某种原因在技术上无法访问编写代码,如果着色器中的任何位置都存在 gl_FragDepth = … 表达式,则该表达式是静态编写的。
警告:如果片段着色器静态写入gl_FragDepth,则着色器负责在所有情况下静态写入该值。无论采用或不采用什么分支,着色器都必须确保写入值。因此,如果你在一个地方有条件地写入它,你至少应该确保在此之前的某个时间有一个非条件写入。
GLSL 4.20 或 ARB_conservative_depth 允许用户指定对gl_FragDepth的修改(相对于它本来具有的 gl_FragCoord.z 值)将以某些方式进行。这允许实施在某些情况下自由地不关闭早期深度测试。
这是通过使用特殊布局限定符重新声明gl_FragDepth来完成的:
layout (depth_<condition>) out float gl_FragDepth;
条件可以是下列条件之一:
- any: 默认值。您可以自由更改深度,但会失去最有潜力的性能。
- greater:与 gl_FragCoord.z 相比,您只会使深度更大。
- less:与 gl_FragCoord.z 相比,您只会使深度更小。
- unchanged:如果你写信给gl_FragDepth,你将完全写gl_FragCoord.z。
违反该条件将产生未定义的行为。
GLSL 4.00 或 ARB_sample_shading 为我们带来了:
out int gl_SampleMask[];
gl_SampleMask
这将定义在执行多采样渲染时片段的样本蒙版。如果着色器没有静态写入,则它将被gl_SampleMaskIn填充。此处的示例蒙版输出将与光栅器计算的示例蒙版在逻辑上进行 AND’d。
警告:与 gl_FragDepth 一样,如果片段着色器写入gl_SampleMask,它必须确保写入所有执行路径的值。但它也必须确保写入数组中的每个元素。数组的大小与 gl_SampleMaskIn 相同。