FXAA是现代的常用抗锯齿手段之一,此次我们来在Unity中从零起头实现它。
起首我们来看一个测试场景,我们在Game视角下将scale拉到2x:
用Unity实现FXAA1能够看到画面的锯齿比力严峻,下面我们将一步一步地实现FXAA,消弭锯齿。起首,FXAA是一种降低整个画面临比度的手段,通过降低比照度来消弭掉明显的锯齿和一些孤立的像素。而权衡比照度的一种体例就是计算像素的亮度。那么,我们先新建一个后处置效果,计算整个画面上像素的亮度,Unity内置了API LinearRgbToLuminance来停止计算亮度:
// Convert rgb to luminance // with rgb in linear space with sRGB primaries and D65 white point half LinearRgbToLuminance(half3 linearRgb) { return dot(linearRgb, half3(0.2126729f, 0.7151522f, 0.0721750f)); }看一下亮度图长啥样:
用Unity实现FXAA2有了亮度信息,接下来就能够计算比照度了。我们取当前像素四周上下摆布4个像素的亮度信息,然后别离计算出它们的更大值和最小值,更大值和最小值之差做为当前像素的比照度:
struct LuminanceData { float m, n, e, s, w; float highest, lowest, contrast; }; LuminanceData SampleLuminanceNeighborhood (float2 uv) { LuminanceData l; l.m = SampleLuminance(uv); l.n = SampleLuminance(uv, 0, 1); l.e = SampleLuminance(uv, 1, 0); l.s = SampleLuminance(uv, 0, -1); l.w = SampleLuminance(uv,-1, 0); l.highest = max(max(max(max(l.n, l.e), l.s), l.w), l.m); l.lowest = min(min(min(min(l.n, l.e), l.s), l.w), l.m); l.contrast = l.highest - l.lowest; return l; } float4 ApplyFXAA (float2 uv) { LuminanceData l = SampleLuminanceNeighborhood(uv); return l.contrast; } 用Unity实现FXAA3关于比照度比力小的像素,我们应该将其过滤掉。那里能够利用绝对阈值和相对阈值,来过滤值比力小或者相对四周值比力小的比照度:
bool ShouldSkipPixel (LuminanceData l) { float threshold = max(_ContrastThreshold, _RelativeThreshold * l.highest); return l.contrast < threshold; } float4 ApplyFXAA (float2 uv) { LuminanceData l = SampleLuminanceNeighborhood(uv); if (ShouldSkipPixel(l)) { return 0; } return l.contrast; } 用Unity实现FXAA4有了比照度信息之后,下一步就是要考虑若何按照比照度对像素停止交融。显然,当前像素四周的像素亮度差别越大,交融的比例越高。为了比力准确地计算四周像素的亮度,此次把对角的像素也考虑进来。当然,对角的像素所占的权重会相对低一些:
用Unity实现FXAA5float DeterminePixelBlendFactor (LuminanceData l) { float filter = 2 * (l.n + l.e + l.s + l.w); filter += l.ne + l.nw + l.se + l.sw; filter *= 1.0 / 12; filter = abs(filter - l.m); filter = saturate(filter / l.contrast); return filter; } float4 ApplyFXAA (float2 uv) { LuminanceData l = SampleLuminanceNeighborhood(uv); if (ShouldSkipPixel(l)) { return 0; } float pixelBlend = DeterminePixelBlendFactor(l); return pixelBlend; } 用Unity实现FXAA6为了让blend系数光滑一点,也能够加上smoothstep和square:
float DeterminePixelBlendFactor (LuminanceData l) { float filter = 2 * (l.n + l.e + l.s + l.w); filter += l.ne + l.nw + l.se + l.sw; filter *= 1.0 / 12; filter = abs(filter - l.m); filter = saturate(filter / l.contrast); float blendFactor = smoothstep(0, 1, filter); return blendFactor * blendFactor; } 用Unity实现FXAA8有了交融系数之后,接下来就要考虑怎么交融,对哪两个像素停止交融。我们的目的是降低整个画面的比照度,也就是说要对亮度差别比力大的像素停止交融。那里能够先简单地假设,差别亮度的区域是由程度标的目的或者竖曲标的目的区分隔的,然后比力程度标的目的的亮度差别和竖曲标的目的的亮度差别,最末决定交融的标的目的:
用Unity实现FXAA9struct EdgeData { bool isHorizontal; }; EdgeData DetermineEdge (LuminanceData l) { EdgeData e; float horizontal = abs(l.n + l.s - 2 * l.m) * 2 + abs(l.ne + l.se - 2 * l.e) + abs(l.nw + l.sw - 2 * l.w); float vertical = abs(l.e + l.w - 2 * l.m) * 2 + abs(l.ne + l.nw - 2 * l.n) + abs(l.se + l.sw - 2 * l.s); e.isHorizontal = horizontal >= vertical; return e; } float4 ApplyFXAA (float2 uv) { LuminanceData l = SampleLuminanceNeighborhood(uv); if (ShouldSkipPixel(l)) { return 0; } float pixelBlend = DeterminePixelBlendFactor(l); EdgeData e = DetermineEdge(l); return e.isHorizontal ? float4(1, 0, 0, 0) : 1; }来看一下画面中有哪些像素交融时会选择程度标的目的:
用Unity实现FXAA10选择程度标的目的做为亮度区域的分界限,意味着交融时需要拔取竖曲标的目的上的像素。但是竖曲标的目的上也有正负两个选择。类似地,我们比力正负标的目的的亮度差别,哪个差别更大,就选哪个:
float pLuminance = e.isHorizontal ? l.n : l.e; float nLuminance = e.isHorizontal ? l.s : l.w; float pGradient = abs(pLuminance - l.m); float nGradient = abs(nLuminance - l.m); e.pixelStep = e.isHorizontal ? _MainTex_TexelSize.y : _MainTex_TexelSize.x; if (pGradient < nGradient) { e.pixelStep = -e.pixelStep; }来看一下画面中有哪些像素交融时会选择负标的目的:
用Unity实现FXAA11如今万事俱备,能够实正起头blend了。起首我们需要把tex2D换成tex2Dlod来制止mipmap带来的干扰;其次我们能够借助纹理过滤来帮我们主动blend,即采样点位于两个像素之间,按照交融系数的大小,调整采样点到两个像素的间隔:
float4 Sample (float2 uv) { return tex2Dlod(_MainTex, float4(uv, 0, 0)); } float4 ApplyFXAA (float2 uv) { LuminanceData l = SampleLuminanceNeighborhood(uv); if (ShouldSkipPixel(l)) { return Sample(uv); } float pixelBlend = DeterminePixelBlendFactor(l); EdgeData e = DetermineEdge(l); if (e.isHorizontal) { uv.y += e.pixelStep * pixelBlend; } else { uv.x += e.pixelStep * pixelBlend; } return float4(Sample(uv).rgb, l.m); } 用Unity实现FXAA12我们还能够再加上一个外部控造交融系数的参数,如许就能够动态看到差别交融强度下的效果:
用Unity实现FXAA13但现实上分隔线的长度纷歧定只要3个像素大小,我们能够通过计算当前像素和分隔线另一侧的像素的亮度均匀值,做为分隔线的亮度,然后不竭地沿着那条线向两头停止采样,当采样得到的亮度和分隔线的亮度有明显差别时,就认为找到了那条线的末端:
用Unity实现FXAA14我们设定每一端的更大查找次数为10:
float DetermineEdgeBlendFactor (LuminanceData l, EdgeData e, float2 uv) { float2 uvEdge = uv; float2 edgeStep; if (e.isHorizontal) { uvEdge.y += e.pixelStep * 0.5; edgeStep = float2(_MainTex_TexelSize.x, 0); } else { uvEdge.x += e.pixelStep * 0.5; edgeStep = float2(0, _MainTex_TexelSize.y); } float edgeLuminance = (l.m + e.oppositeLuminance) * 0.5; float gradientThreshold = e.gradient * 0.25; float2 puv = uvEdge + edgeStep; float pLuminanceDelta = SampleLuminance(puv) - edgeLuminance; bool pAtEnd = abs(pLuminanceDelta) >= gradientThreshold; for (int i = 0; i < 9 && !pAtEnd; i++) { puv += edgeStep; pLuminanceDelta = SampleLuminance(puv) - edgeLuminance; pAtEnd = abs(pLuminanceDelta) >= gradientThreshold; } float2 nuv = uvEdge - edgeStep; float nLuminanceDelta = SampleLuminance(nuv) - edgeLuminance; bool nAtEnd = abs(nLuminanceDelta) >= gradientThreshold; for (int i = 0; i < 9 && !nAtEnd; i++) { nuv -= edgeStep; nLuminanceDelta = SampleLuminance(nuv) - edgeLuminance; nAtEnd = abs(nLuminanceDelta) >= gradientThreshold; } return pAtEnd || nAtEnd; }看看找到的分隔线:
用Unity实现FXAA15当然,寻找分隔线端点的步长也纷歧定是定值,能够灵敏设置,而且在超越更大迭代次数时,能够斗胆地往前步进一个步长,做为预测成果:
#define EDGE_STEP_COUNT 10 #define EDGE_STEPS 1, 1.5, 2, 2, 2, 2, 2, 2, 2, 4 #define EDGE_GUESS 8 static const float edgeSteps[EDGE_STEP_COUNT] = { EDGE_STEPS }; for (int i = 2; i < EDGE_STEP_COUNT && !pAtEnd; i++) { puv += edgeStep * edgeSteps[i]; pLuminanceDelta = SampleLuminance(puv) - edgeLuminance; pAtEnd = abs(pLuminanceDelta) >= gradientThreshold; } if (!pAtEnd) { puv += edgeStep * EDGE_GUESS; }接下来,我们需要确定一下交融的系数。起首,越靠近端点的像素,交融的系数越大;其次,端点像素亮度要和当前像素亮度要在分隔线的统一侧,即都要比分隔线亮度更大或者更小:
float pDistance, nDistance; if (e.isHorizontal) { pDistance = puv.x - uv.x; nDistance = uv.x - nuv.x; } else { pDistance = puv.y - uv.y; nDistance = uv.y - nuv.y; } float shortestDistance; bool deltaSign; if (pDistance <= nDistance) { shortestDistance = pDistance; deltaSign = pLuminanceDelta >= 0; } else { shortestDistance = nDistance; deltaSign = nLuminanceDelta >= 0; } if (deltaSign == (l.m - edgeLuminance >= 0)) { return 0; } return 0.5 - shortestDistance / (pDistance + nDistance);最初,我们得到了两种计算体例下的交融系数,简单粗暴点,间接取max做为最末效果:
用Unity实现FXAA16若是你觉得我的文章有帮忙,欢送存眷我的微信公家号:我是实的想做游戏啊
Reference[1] FXAA