FXAA像素平滑(翻译二十六)
- 计算图像亮度(Luminance)。
- 寻找高对比度像素。
- 识别对比度边缘。
- 选择性混合。
- 搜索边缘端点。
1 搭建场景
显示器的分辨率是有限的。因此,未与像素网格对齐的图像特征会产生锯齿(Aliasing)。对角线和曲线看起来像阶梯,通常被称为“锯齿(jaggies)”。细线可能会断开。比像素还小的高对比度特征有时出现,有时不出现,导致物体移动时产生闪烁,通常被称为“火花(fireflies)”。为了减轻这些问题,开发了一系列抗锯齿技术。本教程涵盖了经典的 FXAA 解决方案。
1.1 测试场景
本教程我创建了一个与景深教程类似的测试场景。它包含高对比度和低对比度区域、明亮和黑暗区域、多个直线和曲线边缘以及微小特征。我们一如既往地使用 HDR 和线性色彩空间。所有场景截图都放大了 4 倍,以便更容易区分单个像素。
1.2 超采样 (Supersampling)
消除锯齿最直接的方法是以高于显示器的分辨率进行渲染,然后对其进行降采样。这是一种空间抗锯齿方法。
超采样抗锯齿(SSAA)就是这样做的。场景渲染到两倍分辨率的缓冲,然后对四个像素的块求平均值以产生最终图像。这种方法可以消除锯齿,但也会稍微模糊整个图像,而且非常昂贵。
多采样抗锯齿(MSAA)是对 SSAA 的改进,它减少了填充率。但它不适用于延迟渲染等技术。
1.3 后期处理 (Post E!ect)
第三种抗锯齿方法是通过后期处理 pass 完成。这些技术在最终分辨率下工作,因此无法访问实际的子像素数据,而是必须分析图像并根据这种解释选择性地模糊。
我们将创建自己的快速近似抗锯齿(fast approximate anti-aliasing,简称 FXAA)版本。它由 NVIDIA 的 Timothy Lottes 开发。我们将创建最新版本——FXAA 3.11——特别是 PC 高质量变体。
我们将为新的 FXAA shader 使用与 DepthOfField 类似的设置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Shader "Hidden/FXAA" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
}
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_TexelSize;
struct VertexData {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct Interpolators {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
Interpolators VertexProgram (VertexData v) {
Interpolators i;
i.pos = UnityObjectToClipPos(v.vertex);
i.uv = v.uv;
return i;
}
ENDCG
SubShader {
Cull Off ZTest Always ZWrite Off
Pass { // 0 blitPass
CGPROGRAM
#pragma vertex VertexProgram
#pragma fragment FragmentProgram
float4 FragmentProgram (Interpolators i) : SV_Target {
return tex2D(_MainTex, i.uv);
}
ENDCG
}
}
}
创建一个极简的 FXAAEffect 组件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using UnityEngine;
using System;
[ExecuteInEditMode, ImageEffectAllowedInSceneView]
public class FXAAEffect : MonoBehaviour {
[HideInInspector]
public Shader fxaaShader;
[NonSerialized]
Material fxaaMaterial;
void OnRenderImage (RenderTexture source, RenderTexture destination) {
if (fxaaMaterial == null) {
fxaaMaterial = new Material(fxaaShader);
fxaaMaterial.hideFlags = HideFlags.HideAndDontSave;
}
Graphics.Blit(source, destination, fxaaMaterial);
}
}
确保禁用摄像机的 MSAA,并应用效果。
2 亮度 (Luminance)
FXAA 通过选择性地降低图像对比度来工作。对比度是由像素的光强度决定的,即它们的亮度(luminance)。FXAA 实际上是在包含像素亮度的灰阶图像上工作的。
2.1 计算亮度
我们可以使用 LinearRgbToLuminance 函数计算亮度。由于我们不使用色调映射,请使用 clamp 后的颜色。
1
sample.rgb = LinearRgbToLuminance(saturate(sample.rgb));
2.2 提供亮度数据
FXAA 并不自己计算亮度,数据通常必须由之前的 pass 放入 alpha 通道。我们支持三种模式:Alpha, Green, Calculate。
1
2
3
4
5
6
7
8
9
10
11
Pass { // 0 luminancePass
CGPROGRAM
#pragma vertex VertexProgram
#pragma fragment FragmentProgram
half4 FragmentProgram (Interpolators i) : SV_Target {
half4 sample = tex2D(_MainTex, i.uv);
sample.a = LinearRgbToLuminance(saturate(sample.rgb));
return sample;
}
ENDCG
}
3 混合高对比度像素
FXAA 通过混合高对比度像素来工作。步骤包括:计算局部对比度、决定是否超过阈值、计算混合因子、确定边缘方向,最后进行混合。
3.1 确定相邻像素的对比度
FXAA 使用直接的水平和垂直相邻像素(NEWS 交叉)以及中间像素来确定对比度。
1
2
3
4
struct LuminanceData {
float m, n, e, s, w;
float highest, lowest, contrast;
};
局部对比度只是最高和最低亮度值之间的差。
3.2 跳过低对比度像素
我们可以设置一个对比度阈值来跳过非锯齿区域。
如果对比度低于阈值,则跳过像素(图中红色部分)。此外还有一个相对阈值(图中绿色部分)。
3.3 计算混合因子
对于需要处理的像素,我们需要确定混合因子。这取决于中心像素与整个 3×3 邻域(包括对角像素)之间的对比度。
我们使用一种低通滤波器,它类似于帐篷滤波器。计算中心像素与该平均值之间的差,得到高通滤波器。
最后,通过对比度进行归一化,并使用 smoothstep 平滑结果。
3.4 混合方向
FXAA 会根据对比度梯度决定将中心像素与其 NEWS 邻域中的哪一个混合。
我们比较水平和垂直对比度。如果水平对比度大于等于垂直对比度,则为水平边缘。
3.5 混合 (Blending)
最终结果是通过混合因子在中心像素与其邻居之间进行线性插值。
你可以通过一个子像素混合因子(Subpixel Blending)来调节 FXAA 的强度。
4 沿边缘混合
由于 3×3 块只能平滑局部特征,我们需要沿长边缘搜索端点,以更好地平滑阶梯。
4.1 边缘亮度
我们跟踪边缘梯度,并在边缘两侧寻找端点。
4.2 沿边缘行走
我们沿边缘两个方向行走,直到找到对比度梯度不再匹配的端点。
在搜索过程中,我们取每步之间纹理采样的平均亮度。
4.3 迭代搜索
由于在 Shader 中无限搜索是不可能的,我们限制搜索步数(例如 10 步)。
4.4 计算边缘混合因子
通过端点距离计算一个边缘混合因子,并将其与之前的像素混合因子(子像素混合)结合,取其中的最大值。
你现在拥有了一个基本的 FXAA 实现,可以根据质量设置调整搜索步数。



























