GLSL:中心还是质心? 或在着色器攻击时

在为即将到来的游戏修改着色器时,我遇到了不愉快的伪像,该伪像仅在打开硬件MSAA时才会显现出来。 在风景的屏幕截图中,您可以看到一些太亮的像素。 其中几个的颜色值是如此之大,以至于在开花后它们变成了多色的“鬼影”。

图片

敬请注意,我翻译了一篇文章,详细解释了这种现象的原因以及解决方法。

图片

图1-正确(左)和不正确(右)图像。 注意“错误”图像左边缘的黄色条。 尽管变量myMixer在0到1之间变化,但它在“错误”图像中以某种方式超出了此范围。

考虑具有简单非线性转换的简单片段着色器:

smooth in float myMixer; //      . //  sqrt    . void main( void ) { const vec3 blue = vec3( 0.0, 0.0, 1.0 ); const vec3 yellow = vec3( 1.0, 1.0, 0.0 ); float a = sqrt( myMixer ); //    myMixer < 0.0 vec3 color = mix( blue, yellow, a ); //   gl_FragColor = vec4( color, 1.0 ); } 

错误图像左侧的黄色条纹从何而来? 为了更好地了解出了什么问题,让我们首先来看一个情况,该情况下,一切都会(几乎)始终正确运行。

图片

这是一个带有示例的经典栅格化。 灰色正方形是像素,黄色点是位于半整数窗口坐标处的像素中心(默认情况下,gl_FragCoord中左下方像素的坐标为(0.5,0.5) -trans。 )。

图片

在上图中,正割线分隔了图元的一半空间。 在此行的上方和左侧,变量myMixer为正,而下方和右侧的变量为负。

经典的一样本光栅化会根据图元隶属度对像素进行分类,并仅针对其中心位于图元内部的像素创建片段。 在此示例中,将产生六个片段,如左上方所示。 标记为柔和颜色的像素不会落入图元。 不会为其生成碎片。

图片

绿色表示将要计算片段着色器的点。 将为每个像素的中心计算myMixer的值。 请注意,绿点在该行的上方和左侧,因此其中的myMixer值将为正。 与顶点关联的所有输入数据(变量或输入/输出变量)也将在这些点进行插值。

我们的简单着色器不使用导数(例如,从具有mip级别的纹理进行采样时是显式的或隐式的),但是导数dFdx(水平)和dFdy(垂直)用箭头标记。 在原语内部,它们定义得很好且规则。

总结一下:在一次选择中, 当像素中心位于图元内部时才生成片段, 为像素中心计算片段数据,仅在图元内部执行顶点数据插值和着色器计算。 一切都很好并且“正确”。 (几乎总是。现在,让我们忽略沿图元边界的像素上的某些导数的不准确性)。

因此,通过一个选择,一切都(几乎)在栅格化方面非常出色。 但是,打开多重采样会出什么问题?

图片

这是经典的多采样栅格化。 灰色方块表示像素。 黄点是半整数坐标中的像素中心。 在蓝点处,发生采样。 此示例显示了两个旋转样本的简单示意图。 可以将所有参数归纳为任意数量的样本。

图片

该行仍然分隔图元的一半空间。 在其上方和左侧,myMixer的值为正。 降低和向右-负。

使用多重采样进行栅格化时,如果至少一个像素采样落入图元内部,则像素分类器将生成一个片段。

在此示例中,将生成10个片段,如左上半平面所示。 注意沿面添加的四个片段,其中一个样本落在图元内部,尽管中心在外部。 图元之外的像素仍标记为暗淡。

图片

在像素中心进行计算会怎样?

对于每个片段,着色器将以绿色红色点计算。 与myMixer相关的数据在每个像素的中心进行计算。 在绿点中,这些值将为正,因为它们位于边框的上方和左侧。 红点在图元之外,因为其中的myMixer值为负。 在红点处,外推相关数据而不是内插。

在我们的着色器中,sqrt(myMixer)值未使用负myMixer定义。 即使顶点着色器记录的myMixer值处于从零到1的间隔中,在片段着色器中,由于外推,myMixer可能也会超出此间隔。 因此,对于否定的myMixer,将不定义片段着色器的结果。

图片

我们仍在考虑在像素中心计算着色器,图中的箭头显示dFdx和dFdy。 在多边形的内部片段上,它们定义得很好,因为所有计算都是在等间隔的像素中心进行的。

图片

在像素中心以外的其他点进行计算时会发生什么?

绿点是将计算着色器的点。 myMixer的关联值在每个像素的心中计算。

像素的质心是像素的平方与图元内部的交点的重心。 对于完全覆盖的像素,质心为中心。 对于部分覆盖的像素,质心通常与中心不同。

OpenGL标准允许实现在图元和像素的交点处选择任意点,而不是理想的质心。 例如,它可能是一个采样点。

在此示例中,如果中心位于图元内部,则将为该中心计算顶点数据。 否则,将在图元内部的任何采样点处计算它们。 沿边框的四个像素会发生这种情况。 所有绿点都位于边框的上方和左侧,因此它们中的值始终是插值的,而不是插值的。

为什么不总是计算质心着色器? 通常,它比中心计算要贵。 但是,这不是主要因素。

这一切都与计算衍生产品有关。 注意绿点之间的箭头。 对于不同的点对,它们之间的距离并不相同。 另外,y对于dFdx不是恒定的,并且x对于dFdy不是恒定的。 以质心计算时,导数的准确性较低

这是一个折衷方案,因此从GLSL 1.20开始的OpenGL使用质心限定符为着色器开发人员提供了在中心和质心之间进行选择的功能:

 centroid in float myMixer; //  centroid  smooth //      . //  sqrt    . void main( void ) { const vec3 blue = vec3( 0.0, 0.0, 1.0 ); const vec3 yellow = vec3( 1.0, 1.0, 0.0 ); float a = sqrt( myMixer ); //    myMixer < 0.0 vec3 color = mix( blue, yellow, a ); //   gl_FragColor = vec4( color, 1.0 ); } 

什么时候应该使用质心?

  1. 当外推值可能导致模糊的结果时。 请特别注意内置函数,该函数的说明为“如果...,则结果未定义”。
  2. 当外推值与非常非线性或不连续的函数一起使用时。 这些包括阶跃函数或耀斑计算,尤其是在指数足够大时。

什么时候不应该使用质心?

  1. 如果您需要精确的导数。 导数可以是显式的(dFdx调用)或隐式的,例如,具有mip级别或各向异性过滤的纹理样本。 在GLSL规范中,质心中的导数被认为是如此不可用,以至于它们被声明为未定义。 在这种情况下,请尝试编写:

     centroid in float myMixer; //  ! smooth in float myCenterMixer; //     . 

  2. 如果渲染的网格中的图元的大部分边界都是内部的,并且始终定义良好。 最简单的示例是一个带100个三角形的条(TRIANGLE_STRIP),其中仅第一个和最后一个三角形需要外推。 质心限定符将导致在这两个三角形上进行插值,但会损失其余98个三角形的准确性和连续性。
  3. 如果您知道不确定的,非线性的或不连续的函数可能会出现伪像,但实际上这些伪像几乎是不可见的。 如果着色器没有攻击-不要修复它!

Source: https://habr.com/ru/post/zh-CN434046/


All Articles