简介

常见的抗锯齿手段有两种,一种是基于采样的 SSAA 和 MSAA,另一种是基于后处理的如 FXAA、TAA。
效果上:SSAA > MSAA > TAA > FXAA (但是 TAA 会让部分玩家头晕,我自己用 FXAA 比较多)
效率上:TAA > FXAA > MSAA > SSAA

TAA 和 FXAA 的效率差距其实很小,并且基于后处理的 AA 要比前一种效率高很多。这些抗锯齿选项基本是每个游戏的标配了。

基本原理

锯齿通常发生在图像边缘的地方,在频域上属于高频分量,但是基于采样的 AA 都有个共同的缺点,那就是会在非边缘部分浪费许多计算。SSAA 尤其明显!

例如在三角形的内部,基本不会出现锯齿(不考虑 Shading 引起的锯齿,例如高光),但是 SSAA 需要为这部分付出额外三倍的计算量和存储空间,MSAA 还好,但也需要额外三倍的 FrameBuffer 开销。

而 FXAA 很好地避免了这个问题,它使用 CV 里常见的图像边缘检测技术,先把图像中的边缘提取出来,然后再做抗锯齿。

算法的基本步骤如下:

  1. 将 RGB 颜色转换成亮度图,可以用 NTSC 1953 的经验公式

    • Gray = 0.30R + 0.59G + 0.11B
  2. 在亮度图下计算每个四周的梯度,梯度大的方向就是边缘的法线方向

  3. 沿着边缘的切线方向前进,计算出边缘的两个端点

  4. 选取最近的一个,并且颜色不相似的端点,计算起点与它的距离

  5. 通过距离算出一个混合系数,接着从原点出发,与沿着法线方向的某个像素点混合,得到输出。

代码实现

设置屏幕空间的顶点

由于我们做的是全屏后处理效果,因此考虑在顶点着色器设置一个足以覆盖整个屏幕空间的三角形,这样在光栅化后,片元着色器就能处理屏幕空间的每个像素。另外还需要配套一个屏幕空间的 UV 坐标。

#version 310 es#extension GL_GOOGLE_include_directive : enable#include "constants.h"layout(location = 0) out vec2 out_uv;void main()
{const vec3 fullscreen_triangle_positions[3] =vec3[3](vec3(3.0, 1.0, 0.5), vec3(-1.0, 1.0, 0.5), vec3(-1.0, -3.0, 0.5));// 计算每个顶点对应屏幕空间下的 UV 坐标out_uv = 0.5 * (fullscreen_triangle_positions[gl_VertexIndex].xy + vec2(1.0, 1.0));// 该三角形在经过裁剪后会成为两个覆盖屏幕空间的直角三角形gl_Position = vec4(fullscreen_triangle_positions[gl_VertexIndex], 1.0);
}

FS 输入

片元着色器的输入和一些宏定义先放在这里,文章末尾有详细代码。

layout(set = 0, binding = 0) uniform sampler2D in_color;layout(location = 0) in vec2 in_uv;layout(location = 0) out vec4 out_color;

计算亮度矩阵

在片元着色器中,in_uv 的值是当前片元的坐标值,将这个片元叫作起点。

首先计算起点周围 3x3 的亮度值,如果周围亮度值的最大值和最小值的差异小于一个阈值,可以认定它并不是我们要找的边缘,直接返回。

mediump ivec2 screen_size = textureSize(in_color, 0);
// 计算屏幕空间下片元的两个边长
highp vec2 uv_step = vec2(1.0 / float(screen_size.x), 1.0 / float(screen_size.y));// 计算当前像片元四周的亮度值
float luma_mat[9];
for(int i = 0; i < 9; i++){luma_mat[i] = RGB2LUMA(texture(in_color, in_uv + uv_step * STEP_MAT[i]).rgb);
}float luma_max = max(luma_mat[CENTER], max(max(luma_mat[LEFT], luma_mat[RIGHT]), max(luma_mat[UP], luma_mat[DOWN])));
float luma_min = min(luma_mat[CENTER], min(min(luma_mat[LEFT], luma_mat[RIGHT]), min(luma_mat[UP], luma_mat[DOWN])));// 如果3x3色块内的亮度差异并不大,那就跳过
if(luma_max - luma_min < max(EDGE_THRESHOLD_MIN, luma_max * EDGE_THRESHOLD_MAX)) {out_color = texture(in_color, in_uv);return;
}

计算梯度

分别沿着竖直方向和水平方向计算梯度,我们通过两个方向的梯度值的大小来判断直线是水平走势还是垂直走势。

例如竖直方向的梯度大的话,那就说明边缘是沿着水平方向的。

// 沿着竖直方向的梯度
float luma_horizontal = abs(luma_mat[UP_LEFT]  + luma_mat[DOWN_LEFT]  - 2.0*luma_mat[LEFT]) +abs(luma_mat[UP_RIGHT] + luma_mat[DOWN_RIGHT] - 2.0*luma_mat[RIGHT]) +abs(luma_mat[UP] + luma_mat[DOWN] - 2.0*luma_mat[CENTER]);// 沿着水平方向的梯度
float luma_vertial =abs(luma_mat[UP_LEFT]   + luma_mat[UP_RIGHT]   - 2.0*luma_mat[UP]) +abs(luma_mat[DOWN_LEFT] + luma_mat[DOWN_RIGHT] - 2.0*luma_mat[DOWN]) +abs(luma_mat[LEFT] + luma_mat[RIGHT] - 2.0*luma_mat[CENTER]);// 竖直方向的梯度大的话,那就说明边缘是沿着水平方向的
bool is_horizontal = abs(luma_horizontal) > abs(luma_vertial);

计算混合方向

上一步我们找到了边缘和它的方向,但是对于一个片元来说,我们应该和谁去混合?混合的比例是多少?

答案是片元应该沿着梯度方向去混合,混合比例根据由它所处的位置决定。我们会计算距离这个片元最近的端点,然后看一下这个端点是什么颜色,由它来决定我们的混合比例。

因此代码首先需要计算出梯度的方向,以水平方向的边缘为例,如果朝下的梯度比朝上的梯度大,那么说明梯度方向是朝下的。那么在后续混合的时候我们应该和下面的片元的颜色值进行混合。

// 计算沿着边缘的法线方向上下(左右)的梯度
float grandient_down_left = (is_horizontal ? luma_mat[DOWN] : luma_mat[LEFT]) - luma_mat[CENTER];
float grandient_up_right = (is_horizontal ? luma_mat[UP] : luma_mat[RIGHT]) - luma_mat[CENTER];// 如果下面的梯度大于上面的梯度,则法线是沿着朝下的,竖直方向同理
bool is_down_left = abs(grandient_down_left) > abs(grandient_up_right);
float gradient_start = is_down_left ? grandient_down_left : grandient_up_right;// 计算切线方向的法线方向的步长
vec2 step_tangent = (is_horizontal ? vec2(1.0, 0.0) : vec2(0.0, 1.0)) * uv_step;
vec2 step_normal = (is_down_left ? -1.0 : 1.0) * (is_horizontal ? vec2(0.0, 1.0) : vec2(1.0, 0.0)) * uv_step;// 沿着法线方向前进0.5格,到达片元的边界
vec2 uv_start = in_uv + 0.5 * step_normal;

端点搜索

接下来要先计算出端点,然后根据端点的距离和像素值来决定混合比例。我们沿着切线方向每次步进一定距离,那么如何能够确定我们到达了端点呢?

可以计算每个点的梯度,如果它的梯度绝对值比起点的梯度绝对值 1/4 要大,那么就说明找到端点了。

// 边界附近两个片元亮度的均值
float luma_average_start = luma_mat[CENTER] + 0.5 * gradient_start;// 从起点出发
vec2 uv_forward  = uv_start;
vec2 uv_backward = uv_start;float delta_luma_forward  = 0.0;
float delta_luma_backward = 0.0;bool reached_forward   = false;
bool reached_backward  = false;
bool reached_both      = false;for(int i = 1; i < STEP_COUNT_MAX; i++){if(!reached_forward)  uv_forward += QUALITY(i) * step_tangent;if(!reached_backward) uv_backward += - QUALITY(i) * step_tangent;// 计算出移动后的亮度值delta_luma_forward = RGB2LUMA(texture(in_color, uv_forward).rgb) - luma_average_start;delta_luma_backward = RGB2LUMA(texture(in_color, uv_backward).rgb) - luma_average_start;// 前面半部分是用平均亮度计算的梯度,因此所以算出的梯度会偏小// 这里只是为了找到端点,所以对 gradient_start 乘以缩放因子 1/4reached_forward = abs(delta_luma_forward) > GRADIENT_SCALE * abs(gradient_start);reached_backward = abs(delta_luma_backward) > GRADIENT_SCALE * abs(gradient_start);reached_both = reached_forward && reached_backward;if(reached_both) break;
}

计算混合比例

混合比例的计算公式是下面的式子:

L1=∣LeftEnd−Start∣L2=∣LeftRight−Start∣Alpha=−1.0∗min(L1,L2)/(L1+L2)+0.5L1 = |LeftEnd - Start|\\ L2 = |LeftRight - Start|\\ Alpha = -1.0 * min(L1, L2) / (L1 + L2) + 0.5L1=∣LeftEnd−Start∣L2=∣LeftRight−Start∣Alpha=−1.0∗min(L1,L2)/(L1+L2)+0.5

意思就是找到最近的端点的距离,然后除以总距离。因为分子总是分母两项的 Min,所以它的值域是 (0.0,0.5](0.0, 0.5](0.0,0.5]。

另外实现中有个细节,我们需要判断更加接近的那个点和自己的颜色是否接近,如果接近那就不用混合了。

如果不这么做,那么混合就具有对称性:片元 A 可以混合片元 B 反过来 B 也会混合 A。 边缘两边的片元都执行混合不是我们想要的,我们需要打破这种对称性。

// 计算混合比例
float length_forward = max(abs(uv_forward - uv_start).x, abs(uv_forward - uv_start).y);
float length_backward = max(abs(uv_backward - uv_start).x, abs(uv_backward - uv_start).y);
bool is_forward_near = length_forward < length_backward;
float pixel_offset = -1.0 * ((is_forward_near ? length_forward : length_backward) / (length_forward + length_backward)) + 0.5;// 判断更加接近的那个点,和自己的颜色是否接近,如果接近那就不用混合
if( ((is_forward_near ? delta_luma_forward : delta_luma_backward) < 0.0) ==(luma_mat[CENTER] < luma_average_start)) pixel_offset = 0.0;out_color = texture(in_color, in_uv + pixel_offset * step_normal);

低通滤波

上面的那些都是为了解决边缘引起的锯齿,但是在文章开头也说了,有一些锯齿并不是由于边缘引起了,典型的就是高光,它在通常出现在物体表面的内部(而不是边界上),并且它们所占有的像素很少,属于图像中极高频的信息。

那么如何判断一个点是不是所谓的高频信息呢?可以计算它与周围点平均亮度的差值,再通过一个经验公式将差值映射成混合比例。

float luma_average_center = 0.0;
float average_weight_mat[] = float[9](1.0, 2.0, 1.0,2.0, 0.0, 2.0,1.0, 2.0, 1.0
);
for (int i = 0; i < 9; i++) luma_average_center += average_weight_mat[i] * luma_mat[i];
luma_average_center /= 12.0;float subpixel_luma_range = clamp(abs(luma_average_center - luma_mat[CENTER]) / (luma_max - luma_min), 0.0, 1.0);
float subpixel_offset = (-2.0 * subpixel_luma_range + 3.0) * subpixel_luma_range * subpixel_luma_range;
subpixel_offset = subpixel_offset * subpixel_offset * SUBPIXEL_QUALITY;pixel_offset = max(pixel_offset, subpixel_offset);

这个公式长这样,虽然我们不知道是怎么总结出来的,但从函数的图像可以看出,在 xxx 值不大的时候,说明这个点并不是高频点,输出的函数值比较小,max(pixel_offset, subpixel_offset) 会得到前面边缘的混合比例。

当 xxx 比较大的时候,说明找到高频点了,这时候混合比例是有可能超过 0.5 的(前面计算的混合比例最大也才 0.5)。

所以当画面上高光产生锯齿比较明显的时候,例如波光粼粼的海面,我们可以调整公式,增大混合比例。但这也带来的后果是高光会被平滑。

f(x)=0.75∗x4(−2x+3)2f(x) = 0.75 * x^4(-2x+3)^2f(x)=0.75∗x4(−2x+3)2

最终效果

FXAA OFF FXAA ON

有了低通滤波器以后,看到右边墙面上由于高光产生的锯齿就没那么强烈了。

无低通滤波 有低通滤波

完整代码

#version 310 es#extension GL_GOOGLE_include_directive : enable#include "constants.h"
precision highp float;
precision highp int;layout(set = 0, binding = 0) uniform sampler2D in_color;layout(location = 0) in vec2 in_uv;layout(location = 0) out vec4 out_color;#define UP_LEFT      0
#define UP           1
#define UP_RIGHT     2
#define LEFT         3
#define CENTER       4
#define RIGHT        5
#define DOWN_LEFT    6
#define DOWN         7
#define DOWN_RIGHT   8#define EDGE_THRESHOLD_MIN  0.0312
#define EDGE_THRESHOLD_MAX  0.125
#define SUBPIXEL_QUALITY    0.75
#define GRADIENT_SCALE      0.25#define STEP_COUNT_MAX   12
float QUALITY(int i) {if (i < 5) return 1.0;if (i == 5) return 1.5;if (i < 10) return 2.0;if (i == 10) return 4.0;if (i == 11) return 8.0;return 8.0;
}vec2 STEP_MAT[] = vec2[9](vec2(-1.0, 1.0), vec2( 0.0, 1.0), vec2( 1.0, 1.0),vec2(-1.0, 0.0), vec2( 0.0, 0.0), vec2( 1.0, 0.0),vec2(-1.0,-1.0), vec2( 0.0,-1.0), vec2( 1.0,-1.0)
);float RGB2LUMA(vec3 rgb_color){return dot(vec3(0.299, 0.578, 0.114), rgb_color);
}void main()
{mediump ivec2 screen_size = textureSize(in_color, 0);// 计算屏幕空间下片元的两个边长highp vec2 uv_step = vec2(1.0 / float(screen_size.x), 1.0 / float(screen_size.y));// 计算当前像片元四周的亮度值   float luma_mat[9];for(int i = 0; i < 9; i++){luma_mat[i] = RGB2LUMA(texture(in_color, in_uv + uv_step * STEP_MAT[i]).rgb);}float luma_max = max(luma_mat[CENTER], max(max(luma_mat[LEFT], luma_mat[RIGHT]), max(luma_mat[UP], luma_mat[DOWN])));float luma_min = min(luma_mat[CENTER], min(min(luma_mat[LEFT], luma_mat[RIGHT]), min(luma_mat[UP], luma_mat[DOWN])));// 如果3x3色块内的亮度差异并不大,那就跳过if(luma_max - luma_min < max(EDGE_THRESHOLD_MIN, luma_max * EDGE_THRESHOLD_MAX)) {out_color = texture(in_color, in_uv);return;}// 沿着竖直方向的梯度float luma_horizontal = abs(luma_mat[UP_LEFT]  + luma_mat[DOWN_LEFT]  - 2.0*luma_mat[LEFT]) +abs(luma_mat[UP_RIGHT] + luma_mat[DOWN_RIGHT] - 2.0*luma_mat[RIGHT]) +abs(luma_mat[UP] + luma_mat[DOWN] - 2.0*luma_mat[CENTER]);// 沿着水平方向的梯度float luma_vertial =abs(luma_mat[UP_LEFT]   + luma_mat[UP_RIGHT]   - 2.0*luma_mat[UP]) +abs(luma_mat[DOWN_LEFT] + luma_mat[DOWN_RIGHT] - 2.0*luma_mat[DOWN]) +abs(luma_mat[LEFT] + luma_mat[RIGHT] - 2.0*luma_mat[CENTER]);// 竖直方向的梯度大的话,那就说明边缘是沿着水平方向的bool is_horizontal = abs(luma_horizontal) > abs(luma_vertial);// 计算沿着边缘的法线方向上下(左右)的梯度float grandient_down_left = (is_horizontal ? luma_mat[DOWN] : luma_mat[LEFT]) - luma_mat[CENTER];float grandient_up_right = (is_horizontal ? luma_mat[UP] : luma_mat[RIGHT]) - luma_mat[CENTER];// 如果下面的梯度大于上面的梯度,则法线是沿着朝下的,竖直方向同理bool is_down_left = abs(grandient_down_left) > abs(grandient_up_right);float gradient_start = is_down_left ? grandient_down_left : grandient_up_right;vec2 step_tangent = (is_horizontal ? vec2(1.0, 0.0) : vec2(0.0, 1.0)) * uv_step;vec2 step_normal = (is_down_left ? -1.0 : 1.0) * (is_horizontal ? vec2(0.0, 1.0) : vec2(1.0, 0.0)) * uv_step;// 沿着法线方向前进0.5格,到达片元的边界vec2 uv_start = in_uv + 0.5 * step_normal;// 边界附近两个片元亮度的均值float luma_average_start = luma_mat[CENTER] + 0.5 * gradient_start;// 从起点出发vec2 uv_forward  = uv_start;vec2 uv_backward = uv_start;float delta_luma_forward = 0.0;float delta_luma_backward = 0.0;bool reached_forward   = false;bool reached_backward = false;bool reached_both      = false;for(int i = 1; i < STEP_COUNT_MAX; i++){if(!reached_forward)  uv_forward += QUALITY(i) * step_tangent;if(!reached_backward) uv_backward += - QUALITY(i) * step_tangent;// 计算出移动后的亮度值delta_luma_forward = RGB2LUMA(texture(in_color, uv_forward).rgb) - luma_average_start;delta_luma_backward = RGB2LUMA(texture(in_color, uv_backward).rgb) - luma_average_start;// 前面半部分是用平均亮度计算的梯度,因此所以算出的梯度会偏小// 这里只是为了找到端点,所以对 gradient_start 乘以缩放因子 1/4reached_forward = abs(delta_luma_forward) > GRADIENT_SCALE * abs(gradient_start);reached_backward = abs(delta_luma_backward) > GRADIENT_SCALE * abs(gradient_start);reached_both = reached_forward && reached_backward;if(reached_both) break;}// 计算混合比例float length_forward = max(abs(uv_forward - uv_start).x, abs(uv_forward - uv_start).y);float length_backward = max(abs(uv_backward - uv_start).x, abs(uv_backward - uv_start).y);bool is_forward_near = length_forward < length_backward;float pixel_offset = -1.0 * ((is_forward_near ? length_forward : length_backward) / (length_forward + length_backward)) + 0.5;// 判断更加接近的那个点,和自己的颜色是否接近,如果接近那就不用混合if( ((is_forward_near ? delta_luma_forward : delta_luma_backward) < 0.0) ==(luma_mat[CENTER] < luma_average_start)) pixel_offset = 0.0;float luma_average_center = 0.0;float average_weight_mat[] = float[9](1.0, 2.0, 1.0,2.0, 0.0, 2.0,1.0, 2.0, 1.0);for (int i = 0; i < 9; i++) luma_average_center += average_weight_mat[i] * luma_mat[i];luma_average_center /= 12.0;// 经验公式float subpixel_luma_range = clamp(abs(luma_average_center - luma_mat[CENTER]) / (luma_max - luma_min), 0.0, 1.0);float subpixel_offset = (-2.0 * subpixel_luma_range + 3.0) * subpixel_luma_range * subpixel_luma_range;subpixel_offset = subpixel_offset * subpixel_offset * SUBPIXEL_QUALITY;pixel_offset = max(pixel_offset, subpixel_offset);out_color = texture(in_color, in_uv + pixel_offset * step_normal);
}

参考资料

[1] Catlike Coding, Advanced Rendering, FXAA Smoothing Pixels

GLSL 实现 FXAA 后处理效果相关推荐

  1. Unity后处理效果之边角压暗

    我使用的版本为2019.4.12(LTS)版本,项目是HDRP项目. 边角压暗效果的触发,可以按钮触发,也可以按键触发,按钮触发直接调用ButtonEvent()方法就好了.两种方式稍微有点差距,但不 ...

  2. unity脚本控制逐渐消失_Unity实现只狼弹反后处理效果

    简介 今天是只狼发售一周年,作为去年的goty,只狼最核心的系统莫过于弹反,笔者去年也是和弦一郎大战几百回合,通关之后很想实现以下弹反瞬间的效果. 最终实现效果如下: Unity实现只狼弹反后处理效果 ...

  3. Unity3D URP中使用Render Feature实现后处理效果

    unity urp 自带了一个后处理组件Volume,提供了不少后处理效果: 除此之外,Render Feature 也可以实现类似的效果,并且自由度更高. 使用方式是,在RenderPiplineA ...

  4. Unity Post Processing(后处理效果)添加方法及注意事项-最全最新

    Unity版本:2021.1.3: 前言:Post Processing(后处理效果)使用前需要区分渲染管线,不同的渲染管线有不同的添加方式,本文以通用和URP两种渲染管线举例:(这在之前的教程中没有 ...

  5. UnityShader屏幕后处理效果之碎屏效果

    前言:本篇是开始撰写的第一篇关于屏幕后处理效果的图片,所有的屏幕后处理效果其实就是对相机渲染得到的纹理再次处理,本质上都属于Image Processing(IP),即图像处理,包括之后会提到的各种滤 ...

  6. 使用GLSL实现雾化的效果

    from:http://www.cnblogs.com/dawn/archive/2010/03/31/1701327.html 1 为什么需要在GLSL中实现雾的效果? D3D10已经不再支持固定管 ...

  7. UnityShader入门精要-屏幕后处理效果 亮度饱和度对比度、边缘检测、高斯模糊、bloom效果、运动模糊

    (从这里开始可能会记录的更简略一些,时间紧张想迅速读完这本书的主要内容,可能有的部分不会自己上手做) 屏幕后处理通常指渲染完整个场景得到屏幕图像后,再对图像进行操作,抓取屏幕可以使用OnRenderI ...

  8. 使用 LUT 实现 Color Grading 后处理效果

    LUT 查找表 LUT 可以用于后处理中的颜色矫正,它能将一个 RGB 颜色唯一地映射到另一个颜色. 它是一个三维的颜色查找表,但图形渲染中,由于图形 API 通常并不支持三维纹理的查找,所以我们会将 ...

  9. Unity 不同渲染管线添加后处理效果方式

    普通项目使用后处理方式: Window->Package Manager->Post Processing->Instsll添加工程内即可 选择Main Camera->Lay ...

最新文章

  1. 使用MSBuild实现完整daily build流程 .
  2. caffe编译报错 cudnn.hpp:127:41: error: too few arguments to function ‘cudnnStatus_t cudnnSetPooling2dDe
  3. html网页访问计数器,HTML添加网站计数器(Cookie)
  4. ABAP Business switc和business function简介
  5. linux 网络服务器 源码下载,linux下 各类tcp网络服务器的实现源代码.doc
  6. Comparable与Comparator
  7. Asp.Net异步加载
  8. 这次是16.7亿元!新能源汽车骗补何时休?
  9. EPSON/爱普生打印机Linux打印服务器基于ARM驱动安装踩坑CUPS实现支持远程打印AirPrint
  10. 直播类 APP 项目开发实战(原理篇)
  11. 打印机登录无密码计算机,无密码,引发共享打印机拒绝访问故障
  12. 孩子,外面的世界不会轻易原谅你…
  13. 如何解决移动端300ms延迟的问题
  14. 《大咖讲Wireshark网络分析》—再来一个很妖的问题
  15. asp.net保存图片
  16. 填表统计预约打卡表单系统
  17. 学习笔记(十八):MoRe-Fi用深度学习网络从非线性信号中恢复呼吸波形
  18. 1万条数据大概占多大空间_mysql亿级数据数据库优化方案测试-银行交易流水记录的查询...
  19. CUDA编译(一)---使用nvcc编译cuda
  20. acwing——数学知识(四)Nim游戏

热门文章

  1. 大白菜android模拟器,大白菜U盘启动制作工具uefi体验版 V5.0
  2. 8266 lua贝壳物联智能开关,更新修正tmr.alarm问题
  3. kafka文档(3)----0.8.2-kafka API(java版本)
  4. Python编辑基础课后习题(持续更新)
  5. Windows10环境下自己配置Pytracking详细流程(有参考博客)
  6. omap-l138烧写程序之 - 启动模式选择及确认
  7. 通过LiveNVS(免费使用)集中化管理多个LiveNVR-数据透传摆脱局域网的公网IP限制
  8. 最珍贵的角落-赞美之泉(音乐河2)
  9. 19 Three.js实现雾化效果
  10. 人民大学的AI学院,教师团队很凡尔赛