Ch4-3 片段着色器
片段着色器(fragment shader)是图形管线中的一个着色器,用于处理片段(即像素)的颜色,也可以改变片段或采样点的深度值。
对于一个有片段着色器的图形管线,片段着色器是管线中最后执行的着色器。
片段着色器对每个被图元(即点、线、三角形)覆盖的像素,或(多重采样中开启sample shading时的)采样点发生调用。
注:具体而言,片段着色器被定义为逐片段调用至少一次,而开启sample shading后,可能会发生额外调用(官方文档中的说法见此)。
↑不管你干什么,最好不要依赖于“逐片段调用”这一说法,若Vulkan的实现能判断某个片段最终会被后续其他片段覆盖(即不影响最终渲染结果),那么可能直接不对其执行片段着色器。
片段着色器的内置输入
用GLSL编写的片段着色器有以下内置输入:
全局变量 |
|
---|---|
vec4 gl_FragCoord |
当前采样点在屏幕空间内的坐标 |
bool gl_FrontFacing |
表示生成当前片段的图元是正面还是还反面 |
vec2 gl_PointCoord |
若生成当前片段的图元是点,表示该片段在点内的标准化坐标(点是有大小的) |
int gl_SampleID |
当前采样点的索引 |
vec2 gl_SamplePosition |
当前采样点在像素内的标准化坐标,x和y的范围为[0,1] |
int gl_SampleMaskIn[] |
当前采样点的位遮罩 |
vec2 gl_PrimitiveID |
生成当前片段的图元的索引 |
float gl_ClipDistance[] |
自定义剪裁参数 |
float gl_CullDistance[] |
自定义剔除参数 |
bool gl_HelperInvocation |
说明本次对片段着色器的调用是否是一次辅助调用 |
int gl_Layer |
当前片段所在的图层索引(用于使用了多层帧缓冲的情况) |
int gl_ViewportIndex |
当前片段所在的视口索引(用于开启了多视口(multiViewport)的情况) |
gl_ClipDistance和gl_CullDistance由先前的管线阶段中输出的同名内置输出插值而来(插值方式考虑透视),语义参见顶点着色器的内置输出。
gl_Layer和gl_ViewportIndex的语义参见//TODO 几何着色器的内置输出。
gl_FragCoord
gl_FragCoord的x和y坐标是屏幕空间内的非标准化坐标,在没有多重采样的情况下,坐标选取各个像素的中心位置即数值以.5结尾,以左上角为原点,左上角像素坐标为(0.5, 0.5)。
从OpenGL转来的程序员注意,OpenGL中默认以左下角为原点,并且Vulkan中不再支持pixel_center_integer修饰符。
多重采样时,gl_FragCoord.x和gl_FragCoord.y精确到片段内部采样点的坐标。
gl_FragCoord.z是将NDC坐标的z分量进行视口变换后得到的深度值。
gl_FragCoord.w为对先前阶段中输出的gl_Position.w插值得到的数值的倒数。
gl_FrontFacing
gl_FrontFacing根据三角形顶点的手性决定正反,而顺时针还是逆时针为正,由VkPipelineRasterizationStateCreateInfo::frontFace规定。
如果生成当前片段的图元是三角形正面、线或点,则为true。
gl_PointCoord
绘制点绘制出的是正方形,点的大小由先前阶段中的gl_PointSize指定。
gl_PointCoord以正方形左上角为原点,因是标准化坐标,x和y的范围为[0,1]。
gl_SampleMaskIn
gl_SampleMaskIn中每个bit对应相应索引的采样点,因此其数组元素个数亦取决于采样点个数,int的长度为32个bit,因此32xMSAA及以下时gl_SampleMaskIn中有一个元素,64xMSAA时有两个元素。
(阅读以下文字前请先阅读Ch3-3 管线布局和管线一节中关于VkPipelineMultisampleStateCreateInfo的说明)
开启sample shading时,一个采样点的gl_SampleMaskIn[0]的数值为coverage_mask & sample_mask & (1 << gl_SampleID)
。以4xMSAA为例,若四个采样点皆被图元覆盖且没有指定sample mask,那么各个采样点的gl_SampleMaskIn[0]的数值即1 << gl_SampleID
。
未开sample shading时,一个采样点的gl_SampleMaskIn[0]的数值为coverage_mask & sample_mask
。以4xMSAA为例,若四个采样点皆被图元覆盖且没有指定sample mask,则四个采样点的gl_SampleMaskIn[0]的数值皆为0b1111。
gl_HelperInvocation
在quad group操作时,可能需要进行额外的辅助调用(helper invocation)。
求偏导数需要涉及quad group操作,因为glsl中的函数dFdx(...)和dFdy(...)是对2x2像素的单元(称为一个quad)实行的。
然而,仅栅格化后的被片段覆盖的区域,很有可能在边缘上凑不出这样的2x2像素单元,于是可能就有必要对没被片元覆盖的相邻位置执行辅助调用(helper invocation)来凑出一个quad。
在辅助调用中,不应写入storage缓冲区,也不应使用原子性操作,因此有判别本次调用是否是辅助调用的必要,gl_HelperInvocation正是因此存在的。
片段着色器的内置输出
用GLSL编写的片段着色器有以下内置输出:
全局变量 |
|
---|---|
float gl_FragDepth |
当前片段的深度 |
int gl_SampleMask[] |
当前采样点的位遮罩 |
若未曾使用gl_FragDepth(指着色器代码中没出现过),则gl_FragDepth自动取得gl_FragCoord.z的值。
若未曾使用gl_SampleMask(指着色器代码中没出现过),则gl_SampleMask自动取得gl_SampleMaskIn数组的值。
注意,若在if语句的一个分支中写入了gl_FragDepth或gl_SampleMask,那么哪怕不执行该分支,gl_FragDepth或gl_SampleMask也无法自动取得相应值。
gl_SampleMask
最终,采样点会有一个或0或1的系数,由gl_SampleMask中相应的bit与gl_SampleMaskIn中相应的bit位与决定。
这里所谓的系数更准确地说是在执行过片段着色器后,最终得到的各个采样点的coverage。我不想在这里解释额外的概念,让我用一个具体的例子说明gl_SampleMask的影响:
如前文所说,对于4xMSAA,开启sample shading时,若四个采样点皆被图元覆盖且没有应用sample mask,索引为0的采样点获得的gl_SampleMaskIn[0]为0b0001,索引为1的采样点获得的gl_SampleMaskIn[0]为0b0010,依次类推。那么:
如果输出的gl_SampleMask[0],对应从0开始的各个索引的采样点,依次为0b0001、0b0010、0b0100、0b1000,则最后呈现出的效果与不写入gl_SampleMask时一致。
如果输出的gl_SampleMask[0],对应从0开始的各个索引的采样点,0b1000、0b0100、0b0010、0b0001,则最后的解析得到的颜色为vec4(0,0,0,0)
。
如果输出的gl_SampleMask[0],对应从0开始的各个索引的采样点,皆为0b0001,用colorSample0指代索引为0的采样点,则最后的解析得到的颜色为vec4(colorSample0.rgb, colorSample0.a / 4)
,这个值相当于一个不透明的像素与三个完全透明的像素的平均。
Early Fragment Test
早期片段测试(early fragment test)发生在执行片段着色器前,若片段着色器没有诸如写入storage缓冲区等副作用,也不会更改片段的深度值(gl_FragDepth),那么很显然,完全有理由在执行片段着色器前,进行深度范围测试、深度测试和模板测试,以避免不必要的片段着色器调用。
Vulkan的实现有可能隐式地帮你开启早期片段测试,你也可以在片段着色器中显式地开启它:
layout(early_fragment_tests) in;
Dual-source Blending
有这四个混色因子(VkBlendFactor)的枚举项:
VK_BLEND_FACTOR_SRC1_COLOR、
VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR、
VK_BLEND_FACTOR_SRC1_ALPHA、
VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA
若在混色中使用来自SRC1的色值,这种混色方式称为dual-source blending。
片段着色器可以向一个location输出两个色值,写法如下:
layout(location = 0) out vec4 o_Color; //index为0,对应SRC layout(location = 0, index = 1) out vec4 o_Color1;//index为1,对应SRC1