用Unity实现FXAA

news2024/11/13 11:23:36

用Unity实现FXAA

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实现FXAA5

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

用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实现FXAA9

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

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

用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

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

在这里插入图片描述

但实际上分隔线的长度不一定只有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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/60504.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

BDD - SpecFlow ExternalData Plugin 导入外部测试数据

BDD - SpecFlow ExternalData Plugin 导入外部测试数据引言SpecFlow ExternalData 插件支持的数据源Tags实践创建一个 Class Libary Project添加 NuGet Packages添加测试数据源文件CSV 文件Excel 文件添加 Feature 文件实现 Step Definition执行Scenario 导入测试数据源Scenari…

深入URP之Shader篇4: Depth Only Pass

Depth only pass unlit shader中包含了一个Depth Only Pass&#xff0c;这个pass的代码在Packages\com.unity.render-pipelines.universal\Shaders\DepthOnlyPass.hlsl中。这是一个公共pass&#xff0c;几乎所有的URP shader都会包含这个pass。本篇说一说这个pass的作用以及实…

Ubuntu映射到Windows网络驱动器

将虚拟机Ubuntu映射到Windows网络驱动器中&#xff0c;我们需要Ubuntu的网络和主机网络处于同一网段下&#xff0c;然后使Ubuntu具备共享文件功能&#xff0c;最后在windows下添加网络地址。 将Ubuntu设置和主机同一网段 查看主机网络信息 在虚拟机中 选择编辑-- 虚拟网络编…

Java的字符串String

文章目录什么是字符串String类的声明为什么我们的String是不可变的为什么String类用final修饰String的创建字符串比较相等关于Java中的比较关于字符串不同赋值操作对应的内存分配那对象如何进行比较内容字符串常量池StringTalbe的位置字符串常见的操作拼接操作获得字符串的子串…

事件驱动的微服务、CQRS、SAGA、Axon、Spring Boot

事件驱动的微服务、CQRS、SAGA、Axon、Spring Boot 学习构建分布式事件驱动的微服务、CQRS、事件溯源、SAGA、事务 课程英文名&#xff1a;Event-Driven Microservices, CQRS, SAGA, Axon, Spring Boot 此视频教程共10.0小时&#xff0c;中英双语字幕&#xff0c;画质清晰无…

一个带有楼中楼的评论系统数据库设置思路

前言 有个需求&#xff0c;需要实现百度贴吧那样能评论帖子中某一楼的评论里的评论 分析 说起来有点拗口&#xff0c;其实这个评论系统分为4个部分&#xff1a; 主题&#xff08;楼主发布的帖子&#xff09;直接返回楼主的评论&#xff08;从帖&#xff09;&#xff1a;直接…

(11)点云数据处理学习——Colored point cloud registration(彩色点注册)

1、主要参考 &#xff08;1&#xff09;官网介绍 Colored point cloud registration — Open3D 0.16.0 documentation 2、原理和实现 2.1原理 本教程演示了使用几何形状和颜色进行配准的ICP变体。实现了[Park2017]算法。颜色信息锁定沿切平面的对齐。因此&#xff0c;该算法…

Yocto创建自己的分区(基于STM32MP1)

Yocto创建自己的分区&#xff08;基于STM32MP1&#xff09; 前几章节我们分析了machine class里面几篇关键的class&#xff0c;还有machine conf里面的inc文件&#xff0c;大致的创建分区的流程都比较清晰了&#xff0c;本章节动手实际操作一把&#xff0c;创建一个自己的分区…

Unity中的协程

一、什么是协程 协程(Coroutines) 是一种比线程更加轻量级的存在&#xff0c;也被称为用户态线程一个进程可以拥有多个线程&#xff0c;一个线程可以拥有多个协程协程并不会增加线程&#xff0c;它在线程中运行&#xff0c;通过分时复用的方式运行多个协程&#xff0c;其切换代…

《Spring 5.x源码解析之Spring AOP 注解驱动使用及其实现原理》

《Spring 5.x源码解析之Spring AOP 注解驱动使用及其实现原理》 学好路更宽&#xff0c;钱多少加班。---- mercyblitz 一、前言 大家好&#xff0c;欢迎阅读《Spring 5.x源码解析》系列&#xff0c;本篇作为该系列的第二篇&#xff0c;重点介绍Spring AOP在注解驱动编程模式上的…

基于J2EE的大型视频影音系统的设计与实现

目 录 毕业设计&#xff08;论文&#xff09;任务书 I 摘 要 II ABSTRACT III 第1章 绪 论 1 1.1 课题的提出 1 1.1.1 Web2.0浪潮进一步影响全球互联网发展 1 1.1.2 视频分享成为2.0浪潮的最新爆发点 1 1.2 系统研究目的 2 1.3 系统设计目标 2 第2章 关键技术介绍 4 2.1 网页…

C#使用策略模式或者委托替代多IfElse判断和Switch语句

这篇文件介绍使用设计模式中的策略模式和委托来解决多个IfElse判断语句和Switch语句&#xff0c;这种替换方式在其他语言也一样可以做到&#xff0c;比如PHP、JavaScript、Python或者Java等。 这里以C#为例进行演示。 需要为一个程序编写计算方法&#xff0c;根据标签名称来决定…

【华为上机真题 2022】TLV解码

&#x1f388; 作者&#xff1a;Linux猿 &#x1f388; 简介&#xff1a;CSDN博客专家&#x1f3c6;&#xff0c;华为云享专家&#x1f3c6;&#xff0c;Linux、C/C、云计算、物联网、面试、刷题、算法尽管咨询我&#xff0c;关注我&#xff0c;有问题私聊&#xff01; &…

abc280

D 解法1&#xff0c;直接暴力&#xff0c;答案一定在2~1e6里面或者k本身&#xff08;如果k是个质数的话&#xff09; #include<bits/stdc.h> using namespace std; signed main() {long long k;cin>>k;for(long long i1;i<2000010;i) {k/__gcd(k,i);if(k1) {co…

在Linux中,使用Docker,安装es和kibana

1.部署单点es 1.1.创建网络 因为我们还需要部署kibana容器&#xff0c;因此需要让es和kibana容器互联。这里先创建一个网络&#xff1a; # 创建一个网络&#xff1a;es-net docker network create es-net# 查看本机的网络 docker network ls# 删除一个网络&#xff1a;es-ne…

Allegro如何缩放数据操作指导

Allegro如何缩放数据操作指导 Allegeo上可以缩放数据,尤其是在做结构时候非常有用,具体操作如下 以下图为例,需要把这个数据缩小0.5倍 点击Create Detail命令 Option里面选定一个层面,比如放在Board Geomertry,silkscreen top层 Scaling Factor输入0.5 Find选择所有 …

UE5 中 LiveLink 的开发全流程教程

注意&#xff0c;需要有源代码版本的 Unreal Engine&#xff0c;而不是从游戏 Launcher 中下载的 Unreal 版本。 本文使用是 Unreal Engine 5.1 版本。关于一些基础 API 介绍&#xff0c;可以参考之前的一篇。 起点 可以将 Engine\Source\Programs\BlankProgram 作为模板拷贝…

虚拟机搭载Linux · VMware + Ubuntu 部署 路线参考(20.04.5)

提前回家&#xff0c;要部署OS的实验环境。感谢广源同学给予的帮助和支持~ 电脑文件系统进行了整理&#xff0c;重型文件大部分转移到移动硬盘上。 &#xff08;解压了好久然后我找到镜像源了呜呜没发过来&#xff09; 一、VMware 16 安装 VMware虚拟机安装Linux教程(超详细)…

详解 Spring Boot 项目中的日志文件

目录 1. 日志的作用 2. 自定义日志打印 2.1 日志的基本格式 2.2 得到日志对象 2.3 使用日志对象提供的方法&#xff0c; 打印自定义的日志内容 2.4 日志框架的说明 3. 日志的持久化 3.1 配置日志文件的文件名 3.2 配置日志文件的保存路径 3.3 持久化日志的特性 4. 日…

Java集合(Collection List Set Map)

文章目录Collection接口和常用方法Collection接口遍历元素方式1 -使用Iterator(迭代器)Collection接口遍历对象方式2-for循环增强List接口和常用方法List[ArrayList, LinkedList, Vector]的三种遍历方式ArrayList的注意事项ArrayList的底层操作机制源码分析Vector和ArrayList的…