这个简单的FxAA是如何工作的



我遇到了这个FxAA着色器,它可以消除混叠,而且似乎工作得很好。但是,不知怎的无法理解其中的逻辑。有人能解释一下吗?

[[FX]]
// Samplers
sampler2D buf0 = sampler_state {
    Address = Clamp;
    Filter = None;
};
context FXAA {
    VertexShader = compile GLSL VS_FSQUAD;
    PixelShader = compile GLSL FS_FXAA;
}

[[VS_FSQUAD]]
uniform mat4 projMat;
attribute vec3 vertPos;
varying vec2 texCoords;
void main(void) {
    texCoords = vertPos.xy; 
    gl_Position = projMat * vec4( vertPos, 1 );
}

[[FS_FXAA]]
uniform sampler2D buf0;
uniform vec2 frameBufSize;
varying vec2 texCoords;
void main( void ) {
    //gl_FragColor.xyz = texture2D(buf0,texCoords).xyz;
    //return;
    float FXAA_SPAN_MAX = 8.0;
    float FXAA_REDUCE_MUL = 1.0/8.0;
    float FXAA_REDUCE_MIN = 1.0/128.0;
    vec3 rgbNW=texture2D(buf0,texCoords+(vec2(-1.0,-1.0)/frameBufSize)).xyz;
    vec3 rgbNE=texture2D(buf0,texCoords+(vec2(1.0,-1.0)/frameBufSize)).xyz;
    vec3 rgbSW=texture2D(buf0,texCoords+(vec2(-1.0,1.0)/frameBufSize)).xyz;
    vec3 rgbSE=texture2D(buf0,texCoords+(vec2(1.0,1.0)/frameBufSize)).xyz;
    vec3 rgbM=texture2D(buf0,texCoords).xyz;
    vec3 luma=vec3(0.299, 0.587, 0.114);
    float lumaNW = dot(rgbNW, luma);
    float lumaNE = dot(rgbNE, luma);
    float lumaSW = dot(rgbSW, luma);
    float lumaSE = dot(rgbSE, luma);
    float lumaM  = dot(rgbM,  luma);
    float lumaMin = min(lumaM, min(min(lumaNW, lumaNE), min(lumaSW, lumaSE)));
    float lumaMax = max(lumaM, max(max(lumaNW, lumaNE), max(lumaSW, lumaSE)));
    vec2 dir;
    dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE));
    dir.y =  ((lumaNW + lumaSW) - (lumaNE + lumaSE));
    float dirReduce = max(
        (lumaNW + lumaNE + lumaSW + lumaSE) * (0.25 * FXAA_REDUCE_MUL),
        FXAA_REDUCE_MIN);
    float rcpDirMin = 1.0/(min(abs(dir.x), abs(dir.y)) + dirReduce);
    dir = min(vec2( FXAA_SPAN_MAX,  FXAA_SPAN_MAX),
          max(vec2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX),
          dir * rcpDirMin)) / frameBufSize;
    vec3 rgbA = (1.0/2.0) * (
        texture2D(buf0, texCoords.xy + dir * (1.0/3.0 - 0.5)).xyz +
        texture2D(buf0, texCoords.xy + dir * (2.0/3.0 - 0.5)).xyz);
    vec3 rgbB = rgbA * (1.0/2.0) + (1.0/4.0) * (
        texture2D(buf0, texCoords.xy + dir * (0.0/3.0 - 0.5)).xyz +
        texture2D(buf0, texCoords.xy + dir * (3.0/3.0 - 0.5)).xyz);
    float lumaB = dot(rgbB, luma);
    if((lumaB < lumaMin) || (lumaB > lumaMax)){
        gl_FragColor.xyz=rgbA;
    }else{
        gl_FragColor.xyz=rgbB;
    }
}
FxAA是一种对图像执行抗锯齿的过滤算法。与其他AA技术相反,它应用于图像的像素,而不是在绘制其基元时。在游戏等3D应用程序中,它被应用为渲染场景顶部的后处理步骤。

基本思想是:寻找垂直和水平边缘。如果在边缘的末端,则在正交方向上模糊。

这里有一个很好的描述和关于这个主题的原始论文。

标准FxAA的理念是:对于每个像素,对8个相邻像素进行采样,以确定它是否是高对比度边缘的一部分。如果是,请沿边缘方向将其平滑。

问题是,当只看像素周围的3x3正方形时,无法准确检测边缘的方向。想象一下,如果在消除混叠之前,您有一条楼梯线,如下所示:

XXXXXXX   
       XXXXXXX
              XXXXXXX
                     XXXXXXX

这条线上的像素无法从直接的邻居那里看到斜率的切线是1/7。最初的算法通过以下步骤解决了这个问题:

  • 从3x3邻域确定坡度是更接近水平还是更接近垂直
  • 在循环中向左/向右(或垂直向上/向下)步进,再次对周围像素采样
  • 当您到达本地边缘的末端或达到最大FXAA_SEARCH_STEPS时停止
  • 根据行进的距离,移动原始像素的uv,以便使用纹理过滤对其进行平滑处理

搜索循环非常昂贵,所以您发布的FxAA代码使用了另一种方法。它是这样工作的:

首先,它不是对所有8个邻居进行采样,而是只对4个对角邻居进行采样。然后,它通过计算其梯度vec2 dir来近似边缘的方向。该矢量由rcpDirMin缩放,并且dir.xy被箝位为(-8,8)个像素。我不知道长度缩放的确切计算方法,但梯度越接近完美的水平或垂直,就越接近最大长度。平滑是通过沿着这个梯度对像素进行采样来实现的,分为两个量:

  • rgbA是短模糊,它在-1/6*dir+1/6*dir处采样(我不知道为什么这些常数被写在代码中作为1/3-0.52/3-0.5,这让我更困惑)

  • rgbB是较长的模糊,在-3/6*dir+3/6*dir处采样并将其与rgbA混合。

示例:当要平滑的像素在水平线上,并且dir的长度为8时,它将向左和向右采样1.33和4.0个像素。

最终检查CCD_ 15选择CCD_ 16或CCD_。它更喜欢较长的模糊rgbB,除非这导致亮度超出3x3邻域的原始亮度范围,这表明它采样的像素不是边缘的一部分。

差异

  • 原来的使用8个邻居来计算梯度,替代方案只使用4个邻居
  • 原始FxAA将梯度四舍五入为"水平"或"垂直"。另一种选择没有,但会缩放梯度,使其在这些基本方向上更长
  • 原始FxAA在循环中沿着(圆形)梯度逐级采样,以找到如何将像素的UV移动到平滑。备选方案沿梯度采样固定的4个步骤,平滑

替代FxAA着色器的优点是,它执行的纹理查找更少,分支更少,从而以更低的精度为代价获得更好、更一致的性能。

值得注意的是,原始FxAA没有快速退出,跳过了深色或低对比度区域,而替代的FxAA仍然混合了4个纹理查找。当您的图像充满深色/单色时,性能差异可能较小。

我想知道这个FxAA版本是谁写的。GitHub对rcpDirMin的搜索显示它被复制粘贴了很多,但我还没有找到一个有归属的版本。最多可以参考蒂莫西·洛特斯的原始论文,其中没有提到这种变体。

相关内容

  • 没有找到相关文章

最新更新