Post

FXAA像素平滑(翻译二十六)

FXAA像素平滑(翻译二十六)
  • 计算图像亮度(Luminance)。
  • 寻找高对比度像素。
  • 识别对比度边缘。
  • 选择性混合。
  • 搜索边缘端点。

1 搭建场景

显示器的分辨率是有限的。因此,未与像素网格对齐的图像特征会产生锯齿(Aliasing)。对角线和曲线看起来像阶梯,通常被称为“锯齿(jaggies)”。细线可能会断开。比像素还小的高对比度特征有时出现,有时不出现,导致物体移动时产生闪烁,通常被称为“火花(fireflies)”。为了减轻这些问题,开发了一系列抗锯齿技术。本教程涵盖了经典的 FXAA 解决方案。

Thin lines and their aliased rasterization
Thin lines and their aliased rasterization

1.1 测试场景

本教程我创建了一个与景深教程类似的测试场景。它包含高对比度和低对比度区域、明亮和黑暗区域、多个直线和曲线边缘以及微小特征。我们一如既往地使用 HDR 和线性色彩空间。所有场景截图都放大了 4 倍,以便更容易区分单个像素。

Test scene, zoomed in 4×, without any anti-aliasing
Test scene, zoomed in 4×, without any anti-aliasing

1.2 超采样 (Supersampling)

消除锯齿最直接的方法是以高于显示器的分辨率进行渲染,然后对其进行降采样。这是一种空间抗锯齿方法。

Sampling at double resolution and averaging 2×2 blocks
Sampling at double resolution and averaging 2×2 blocks

超采样抗锯齿(SSAA)就是这样做的。场景渲染到两倍分辨率的缓冲,然后对四个像素的块求平均值以产生最终图像。这种方法可以消除锯齿,但也会稍微模糊整个图像,而且非常昂贵。

SSAA 2×
SSAA 2×

多采样抗锯齿(MSAA)是对 SSAA 的改进,它减少了填充率。但它不适用于延迟渲染等技术。

MSAA 2× and 8×
MSAA 2× and 8×
MSAA 8×
MSAA 8×

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,并应用效果。

HDR camera without MSAA and with FXAA
HDR camera without MSAA and with FXAA

2 亮度 (Luminance)

FXAA 通过选择性地降低图像对比度来工作。对比度是由像素的光强度决定的,即它们的亮度(luminance)。FXAA 实际上是在包含像素亮度的灰阶图像上工作的。

2.1 计算亮度

我们可以使用 LinearRgbToLuminance 函数计算亮度。由于我们不使用色调映射,请使用 clamp 后的颜色。

1
sample.rgb = LinearRgbToLuminance(saturate(sample.rgb));
Luminance
Luminance

2.2 提供亮度数据

FXAA 并不自己计算亮度,数据通常必须由之前的 pass 放入 alpha 通道。我们支持三种模式:Alpha, Green, Calculate。

Luminance source, set to calculate
Luminance source, set to 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 交叉)以及中间像素来确定对比度。

NESW cross plus middle pixel
NESW cross plus middle pixel
1
2
3
4
struct LuminanceData {
    float m, n, e, s, w;
    float highest, lowest, contrast;
};

局部对比度只是最高和最低亮度值之间的差。

Local contrast
Local contrast

3.2 跳过低对比度像素

我们可以设置一个对比度阈值来跳过非锯齿区域。

Contrast threshold
Contrast threshold

如果对比度低于阈值,则跳过像素(图中红色部分)。此外还有一个相对阈值(图中绿色部分)。

Red pixels are skipped
Red pixels are skipped
Green pixels are skipped
Green pixels are skipped

3.3 计算混合因子

对于需要处理的像素,我们需要确定混合因子。这取决于中心像素与整个 3×3 邻域(包括对角像素)之间的对比度。

Entire neighborhood
Entire neighborhood

我们使用一种低通滤波器,它类似于帐篷滤波器。计算中心像素与该平均值之间的差,得到高通滤波器。

High-pass filter
High-pass filter

最后,通过对比度进行归一化,并使用 smoothstep 平滑结果。

Blend factor
Blend factor

3.4 混合方向

FXAA 会根据对比度梯度决定将中心像素与其 NEWS 邻域中的哪一个混合。

Blend directions
Blend directions

我们比较水平和垂直对比度。如果水平对比度大于等于垂直对比度,则为水平边缘。

Red pixels are on horizontal edges
Red pixels are on horizontal edges

3.5 混合 (Blending)

最终结果是通过混合因子在中心像素与其邻居之间进行线性插值。

With and without blending
With and without blending

你可以通过一个子像素混合因子(Subpixel Blending)来调节 FXAA 的强度。

Adjusting the amount of blending
Adjusting the amount of blending

4 沿边缘混合

由于 3×3 块只能平滑局部特征,我们需要沿长边缘搜索端点,以更好地平滑阶梯。

Edge blend comparison
Edge blend comparison

4.1 边缘亮度

我们跟踪边缘梯度,并在边缘两侧寻找端点。

Edge gradients
Edge gradients

4.2 沿边缘行走

我们沿边缘两个方向行走,直到找到对比度梯度不再匹配的端点。

Searching for the ends of an edge
Searching for the ends of an edge

在搜索过程中,我们取每步之间纹理采样的平均亮度。

Texture samples while searching
Texture samples while searching

4.3 迭代搜索

由于在 Shader 中无限搜索是不可能的,我们限制搜索步数(例如 10 步)。

Edge search iterations
Edge search iterations

4.4 计算边缘混合因子

通过端点距离计算一个边缘混合因子,并将其与之前的像素混合因子(子像素混合)结合,取其中的最大值。

Final FXAA
Final FXAA

你现在拥有了一个基本的 FXAA 实现,可以根据质量设置调整搜索步数。

This post is licensed under CC BY 4.0 by the author.