用Unity实现FXAA

FXAA是现代的常用抗锯齿手段之一,这次我们来在Unity中从零开始实现它。

首先我们来看一个测试场景,我们在Game视角下将scale拉到2x:

可以看到画面的锯齿比较严重,下面我们将一步一步地实现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));
}

看一下亮度图长啥样:

有了亮度信息,接下来就可以计算对比度了。我们取当前像素周围上下左右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;}

对于对比度比较小的像素,我们应该将其过滤掉。这里可以使用绝对阈值和相对阈值,来过滤值比较小或者相对周围值比较小的对比度:

     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;}

有了对比度信息之后,下一步就是要考虑如何根据对比度对像素进行融合。显然,当前像素周围的像素亮度差异越大,融合的比例越高。为了比较准确地计算周围像素的亮度,这次把对角的像素也考虑进来。当然,对角的像素所占的权重会相对低一些:

        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);return filter;}float4 ApplyFXAA (float2 uv) {LuminanceData l = SampleLuminanceNeighborhood(uv);if (ShouldSkipPixel(l)) {return 0;}float pixelBlend = DeterminePixelBlendFactor(l);return pixelBlend;}

为了让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;}

有了融合系数之后,接下来就要考虑怎么融合,对哪两个像素进行融合。我们的目标是降低整个画面的对比度,也就是说要对亮度差异比较大的像素进行融合。这里可以先简单地假设,不同亮度的区域是由水平方向或者竖直方向区分开的,然后比较水平方向的亮度差异和竖直方向的亮度差异,最终决定融合的方向:

     struct 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;}

来看一下画面中有哪些像素融合时会选择水平方向:

选择水平方向作为亮度区域的分界线,意味着融合时需要选取竖直方向上的像素。但是竖直方向上也有正负两个选择。类似地,我们比较正负方向的亮度差异,哪个差异更大,就选哪个:

         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;}

来看一下画面中有哪些像素融合时会选择负方向:

现在万事俱备,可以真正开始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);}

我们还可以再加上一个外部控制融合系数的参数,这样就可以动态看到不同融合强度下的效果:

但实际上分隔线的长度不一定只有3个像素大小,我们可以通过计算当前像素和分隔线另一侧的像素的亮度平均值,作为分隔线的亮度,然后不断地沿着这条线向两端进行采样,当采样得到的亮度和分隔线的亮度有明显差异时,就认为找到了这条线的末端:

我们设定每一端的最大查找次数为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;}

看看找到的分隔线:

当然,寻找分隔线端点的步长也不一定是定值,可以灵活设置,并且在超过最大迭代次数时,可以大胆地往前步进一个步长,作为预测结果:

#define EDGE_STEP_COUNT 10
#define EDGE_STEPS 1, 1.5, 2, 2, 2, 2, 2, 2, 2, 4
#define EDGE_GUESS 8static 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作为最终效果:

如果你觉得我的文章有帮助,欢迎关注我的微信公众号:我是真的想做游戏啊

Reference

[1] FXAA

用Unity实现FXAA相关推荐

  1. Unity SRP初识之URP

    URP是Unity基于SRP提供的兼顾表现与性能的渲染管线.URP前身命名为LWRP(轻量级渲染管线),后更名为URP. URP已包含在新建工程的工程模板中 URP使用简化的基于物理的照明和材质来实现 ...

  2. Unity 5.6正式版发布,Unity 2017即将来临

    最新版Unity 5.6正式发布,也是Unity 5.x系列的最后一个版本.其中包括改进的2D功能,更好的图形性能,新的视频播放器,Progressive Lightmapper预览版,新的光照模式, ...

  3. Unity游戏画面参数解析与应用:垂直同步、动态模糊、抗锯齿

    前言 最近会在B站刷到一些关于 30帧暴涨90帧! 高 中 低端显卡运行3A大作优化指南[干货向] 游戏画质设置教程 等等这样关于画面与性能调整的的视频,看完之后受益良多,UP主们经过实际测试获取到宝 ...

  4. 盛大游戏技术总监徐峥:Unity引擎使用的三种方式

    在5月13日Unite 2017 案例分享专场上,盛大游戏技术总监徐峥分享了使用Unity引擎的三种方式,以下为详细内容: 大家好,我先简单介绍一下我自己,我是盛大游戏的技术总监徐峥.我今天想分享的主 ...

  5. Unity线性工作流下UI保持Gamma的解决方案收集

    本文地址:https://blog.csdn.net/t163361/article/details/125746935 传统的UI都是基于Gamma空间来制作的.当Unity中切换为线性工作流后,导 ...

  6. unity lookat导致物体颠倒怎么解决_爆款的诞生:《胡闹厨房2》的多人游戏模式解决方案...

    5月11日,由Unity主办的Unite Shanghai 2019开发者大会上,Team17技术总监David Smethurst以"爆款的诞生:<胡闹厨房2>的多人游戏模式解 ...

  7. Unity后期处理-抗锯齿

    一.产生原因 顶点插值可以产生任意位置的顶点.但是像素不是,像素着色器如何着色是通过他的中心点是否被三角形覆盖决定的.所以会产生突变,在外围看来就是锯齿.      二 解决方案 1.  MSAA首先 ...

  8. 图形学中的抗锯齿讨论以及在unity中的应用

    抗锯齿(Anti-Aliasing)是图形学中,很重要的一个部分.本文旨在做一些分析总结,并对平时不理解的细节,做了调研,但毕竟不是做GPU行家,所以有不对的地方,欢迎拍砖^^. 1 什么是锯齿 下图 ...

  9. unity 插件之灯光 效果 调节

    *Post-process插件的使用 此效果不能用于webgl1.0 如果想打包成webgl必须在playersetting中移除webgl1.0 如果想要好的话 Color Space 一定要选择L ...

最新文章

  1. LESSON 11.4 原理进阶:AdaBoost算法流程详解
  2. 电脑无法连接到系统服务器,请问怎么客户端的电脑连接不到服务器?这是什么原因?...
  3. url、base64、blob,三者之间的转化
  4. 为什么应该用模块取代C/C++中的头文件?
  5. 微积分和概率统计有什么用?用来表白呀!
  6. 作者:武永卫(1974-),男,清华大学计算机科学与技术系教授
  7. 百度SEO Keyword Surfer v0.3.7(关键词优化)
  8. 字节跳动招聘【三维视觉】算法实习生
  9. Web前端期末大作业-食品零售综合商城模板网页设计源码(HTML+CSS)
  10. 磁盘不见了只剩一个c盘_电脑开机后磁盘都不见了,只剩下C盘了,为什么啊,求大神指教。...
  11. 明解c语言第7章答案,明解C语言 入门篇 第六章答案
  12. (MATLAB)绘制三维曲线(plot3/plot)
  13. 装逼技能:怎样优雅地摆放桌面图标?
  14. 安卓flash插件_谷歌Chrome 76稳定版正式发布:默认禁用Flash
  15. [技术分享 – FCS 篇] 驭龙五式3之飞龙在天:安装 FCS 服务器
  16. 螺旋模型的优点与缺点
  17. 解决: Error Code: 2013. Lost connection to MySQL server during query
  18. 前端面试题之浏览器系列
  19. Hadoop配置历史服务器、日志聚集、常用端口号(2.x/3.x)
  20. WINDOWS 系统自定义编程 键盘

热门文章

  1. python对象不接受参数什么意思_python的类和对象2(self参数)
  2. 不同的计算机硬件设备之间,计算机应用基础习题答案22257.doc
  3. css让文字成竖排方式呈现
  4. 在Android手机上编写并运行Lua脚本
  5. Python时间戳转换成时间方法
  6. JavaScript分类显示随机颜色【红绿蓝青黄紫、黑白、全彩】
  7. 动态创建WebService
  8. linux wifi配置命令,wifi配置常用命令总结
  9. AndroidStudio编译失败:Could not initialize class com.android.repository.api.RepoManager
  10. linux系统优化项目,Linux之系统优化