我遇到了这个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的理念是:对于每个像素,对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.5
和2/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
的搜索显示它被复制粘贴了很多,但我还没有找到一个有归属的版本。最多可以参考蒂莫西·洛特斯的原始论文,其中没有提到这种变体。