Done is better than perfect

0%

游戏后处理效果

后处理是指,在正常渲染管线结束后,对渲染出来的结果进行加工,以此来模拟各种效果。

颜色

颜色(color) 对应电磁波的可见光波段,是被后期处理的波长信息。颜色既是物体的客观属性——确定的波长,又带有大脑的主观属性——不同的个体对特定波长的电磁波敏感程度不同,感受的颜色也有差异。 为了表示色彩,人们建立了一维、二维、三维甚至四维空间坐标模型,这些色彩模型称为颜色空间。颜色空间多达百种,常见的有如下5种。

颜色数据表示(Linear,LogC)

在影像制作和后期处理中,Linear、LogC和Gamma是三种关键的概念,它们描述了不同的图像数据处理和表示方法。理解它们之间的区别对于正确处理图像和视频数据非常重要。

Linear(线性): Linear指的是一种线性响应的色彩空间,其中记录的图像亮度值直接对应于场景中的实际光照强度。在线性色彩空间中,如果场景中一个区域的光照强度是另一个区域的两倍,那么记录的数值也会是两倍。这种表示方式使得图像的色彩混合和处理在数学上更加直接和简单,但由于人眼对亮度的感知是非线性的,线性空间通常不适用于最终图像的显示。

LogC(对数) LogC是ARRI摄影机特有的一种对数色彩空间,它旨在通过对数曲线来模拟人眼对亮度的非线性感知,使得在有限的比特深度下能够捕获更宽的动态范围。LogC色彩空间特别适合于记录高动态范围的场景,因为它能够有效地保留高光和阴影中的细节。然而,LogC图像在没有经过适当的色彩校正或应用LUT(查找表)之前,看起来会显得非常低饱和和低对比度。

Gamma Gamma校正是一种用于调整图像亮度的非线性操作,旨在使图像在特定显示设备上的显示更符合人眼的感知特性。Gamma校正可以被视为在图像数据和最终显示之间的一个桥梁,用于调整图像的整体亮度和对比度。不同的显示设备和媒体标准可能会使用不同的Gamma值,如sRGB标准使用大约2.2的Gamma值。

总结

Linear色彩空间: 最适合图像的处理和合成,需要在最终输出前转换到适合观看的色彩空间。

LogC色彩空间: 用于捕获和记录高动态范围的图像,需要在后期处理中进行色彩校正。

Gamma校正: 用于调整图像的显示,以符合人眼对亮度的非线性感知和特定显示设备的要求。

颜色空间

XYZ颜色空间

CIE XYZ色彩空间是一种基于人类视觉响应的色彩模型,于1931年由国际照明委员会(CIE,Commission Internationale de l'Éclairage)提出。它是第一个基于人类视觉实验数据的数学定义色彩空间,旨在提供一种不依赖于特定设备的色彩表示方法,从而允许在不同设备和媒介之间准确地转换和比较颜色。

三色刺激值

三色刺激值并不是指人类眼睛对短、中和长波(S、M和L)的反应,而是一组称为X、Y和Z的值,约略对应于红色、绿色和蓝色(但要留意X、Y和Z值并不是真的看起来是红、绿和蓝色,而是从红色、绿色和蓝色导出来的参数),并使用CIE 1931 XYZ颜色匹配函数来计算。两个由多种不同波长的光混合而成的光源可以表现出同样的颜色,这叫做“同色异谱”(metamerism)。当两个光源对标准观察者(CIE 1931标准色度观察者)有相同的视现颜色的时候,它们即有同样的三色刺激值,而不管生成它们的光的光谱分布如何。

xy色度图

因为人类眼睛有响应不同波长范围的三种类型的颜色传感器,所有可视颜色的完整绘图是三维的。但是颜色的概念可以分为两部分:明度和色度。例如,白色是明亮的颜色,而灰色被认为是不太亮的白色。换句话说,白色和灰色的色度是一样的,而明度不同。 CIE Yxy色彩空间故意设计得Y参数是颜色的明度或亮度的测量。颜色的色度接着通过两个导出参数x和y来指定,它们是所有三色刺激值X、Y和Z的函数,规范化下的三个值中的两个:

\[ x = \frac{X}{X+Y+Z} \\ y = \frac{Y}{X+Y+Z} \\ z = \frac{Z}{X+Y+Z} = 1-x-y \]

导出的色彩空间用x, y, Y来指定,它叫做CIE xyY色彩空间并在实践中广泛用于指定颜色。

X和Z三色刺激值可以从色度值x和y与Y三色刺激值计算回来:

\[ X = \frac{Y}{y} x \\ Z = \frac{Y}{y}(1-x-y) \]

xy色度图

具体公式,参见:CIE 1931 XYZ色彩空间

LMS颜色空间

LMS颜色空间基于人眼对光的生理响应,其中L、M、S分别代表长波长(红色)、中波长(绿色)和短波长(蓝色)的锥状细胞。这三种类型的锥细胞是人眼感知颜色的基础。LMS颜色空间尝试模拟这种生理机制,以便在图像处理、色彩校正和视觉研究中更准确地反映和操作色彩。 - L(Long wavelengths):长波锥细胞,主要对红色光敏感。 - M(Medium wavelengths):中波锥细胞,主要对绿色光敏感。 - S(Short wavelengths):短波锥细胞,主要对蓝色光敏感。

在图像处理和色彩管理中,将RGB或其他色彩空间转换到LMS色彩空间可以帮助模拟和理解人类的色彩感知过程,进而进行更精确的色彩校正和调整。

RGB颜色空间

RGB色彩空间基于三原色学说:视网膜存在三种视锥细胞,分别含有对红、绿、蓝三种光线敏感的视色素,当一定波长的光线作用于视网膜时,以一定的比例使三种视锥细胞分别产生不同程度的兴奋,这样的信息传至大脑中枢就产生某一种颜色的感觉。

RGB颜色空间

RGB颜色模型的优点是:

  • 易于理解;
  • 便于硬件实现,现代显示屏一般基于RGB模型;
  • 引入位分辨率(颜色深度) ,指一个像素中,每个颜色分量的比特数。位分辨率决定了色彩等级,例如8位颜色深度,每个颜色分量就有256种可能。

RGB颜色模型的缺点是:

  • 三个分量均用于表示色调,即如果改变某一个分量的数值,这个像素的颜色就发生了改变。在颜色定位等工程中,使用RGB模型就要同时考虑、、三个变量,较为复杂。

CMY/CMYK颜色空间

CMY是工业印刷采用的颜色空间。它与RGB对应。简单的类比RGB来源于是物体发光,而CMY是依据反射光得到的。具体应用如打印机:一般采用四色墨盒,即CMY加黑色墨盒 CMY是青(Cyan)、洋红或品红(Magenta)和黄(Yellow)三种颜色,由于三原色得不到纯黑色,CMYK则是打印时加上墨色(black ink),例如青色可以通过蓝色和绿色光相加得到,则白色通过青色时,没有红色分量。底色为白色进行色彩减法可以得到各种颜色。

Lab颜色空间

Lab色彩空间基于人对颜色的感觉设计,具有感知均匀性(Perceptual Uniform) ,即如果参数L、a、b变化幅度一样,则人视觉上的变化幅度也差不多。

Lab颜色空间

在Lab模式下,通道向量由三个部分组成:

  • 亮度(Lightness)
  • a颜色分量:代表从绿色到红色的分量
  • b颜色分量:代表从蓝色到黄色的分量

Lab同样容易调整——调节亮度仅需关注L通道,调节色彩平衡仅需关注a和b通道。此外,Lab还具有色域广阔、设备无关等性质。

HSV/HSB颜色空间

HSV颜色空间比RGB更接近人们对彩色的感知经验,非常直观地表达颜色的色调、饱和度和明暗程度。

在HSV模式下,通道向量由三个部分组成:

  • 色调、色相(Hue) :与光波的波长有关,它表示人的感官对不同颜色的感受,如红色、绿色、蓝色等,它也可表示一定范围的颜色,如暖色、冷色等。
  • 饱和度(Saturation) :表示颜色的纯度,纯光谱色是完全饱和的,加入白光会稀释饱和度。饱和度越大,颜色看起来就会越鲜艳,反之亦然。
  • 明度(Value, Brightness) :指某种颜色的透光量。与亮度(Lightness) 不同,亮度特指被白光稀释的浓度,任何颜色的高亮都趋于白色,但每种高明度颜色都不同。
HSV/HSB颜色空间

由于HSV可以单独处理色调值,而不会影响到明度和饱和度;或者单独改变明度、饱和度而不影响颜色本身,因此在图像处理中,HSV常用于颜色定位追踪、提取色彩直方图等。

HSV模型的缺点是目前很少有硬件支持,需要从RGB或其他色彩空间进行转换。

HSI/HSL颜色空间

HSV颜色空间比RGB更接近人们对彩色的感知经验,非常直观地表达颜色的色调、饱和度和明暗程度。

在HSI模式下,通道向量由三个部分组成:

  • 色调H(Hue): 与光波的波长有关,它表示人的感官对不同颜色的感受,如红色、绿色、蓝色等,它也可表示一定范围的颜色,如暖色、冷色等。
  • 饱和度S(Saturation): 表示颜色的纯度,纯光谱色是完全饱和的,加入白光会稀释饱和度。饱和度越大,颜色看起来就会越鲜艳,反之亦然。
  • 亮度I(Intensity, Lightness): 对应成像亮度和图像灰度,是颜色的明亮程度。
HSI/HSL颜色空间

由于HSV可以单独处理色调值,而不会影响到明度和饱和度;或者单独改变明度、饱和度而不影响颜色本身,因此在图像处理中,HSV常用于颜色定位追踪、提取色彩直方图等。

HSV模型的缺点是目前很少有硬件支持,需要从RGB或其他色彩空间进行转换。

HSV/HSB与HSI/HSL颜色空间对比

HSV和HSL二者都把颜色描述为在圆柱坐标系内的点,这个圆柱的中心轴底部为黑色,顶部为白色,而它们中间是灰色渐变,绕这个轴的角度对应于“色相”,到这个轴的距离对应于“饱和度”,而沿着这个轴的高度对应于“明度”或“亮度”。

这两种表示在目的上类似,但在方法上有区别。二者在数学上都是圆柱,但HSV(色相、饱和度、明度)在概念上可以被认为是颜色的倒圆锥体(黑点在下顶点,白色在上底面圆心),HSL在概念上表示了一个双圆锥体和圆球体(白色在上顶点,黑色在下顶点,最大横切面的圆心是半程灰色)。注意尽管在HSL和HSV中“色相”指称相同的性质,它们的“饱和度”的定义是明显不同的。

因为HSL和HSV是设备依赖的RGB的简单变换,(h, s, l)或 (h, s, v)三元组定义的颜色依赖于所使用的特定红色、绿色和蓝色“加法原色”。每个独特的RGB设备都伴随着一个独特的HSL和HSV空间。但是 (h, s, l)或 (h, s, v)三元组在被约束于特定RGB空间比如sRGB的时候就更明确了。

HSV模型在1978年由埃尔维·雷·史密斯创立,它是三原色光模式的一种非线性变换,如果说RGB加色法是三维直角座标系,那么HSV模型就是球面座标系。

HSV和HSI对比
HSV和HSI对比

HSV出现的动机

大多数电视机、显示器、投影仪通过将不同强度的红、绿、蓝色光混合来生成不同的颜色,这就是RGB三原色的加色法。通过这种方法可以在RGB色彩空间生成大量不同的颜色,然而,这三种颜色分量的取值与所生成的颜色之间的联系并不直观。 艺术家有时偏好使用HSV或HSL而不选择三原色光模式(即RGB模型)或 印刷四分色模式(即CMYK模型),因为它类似于人类感觉颜色的方式,具有较强的感知度。RGB和CMYK分别是加法原色和减法原色模型,以原色组合的方式定义颜色,而HSV以人类更熟悉的方式封装了关于颜色的信息:“这是什么颜色?深浅如何?明暗如何?”。 但是色彩属性和物理学中的光谱并不是完全对应的,物理学的人类可见光谱是有两个端点的直线形,并不能形成一个环。当然每种颜色都可以找到相应的光波长,但都有一个范围,并不是单一的波长。明度一般和具体某种颜色的光波能量相当,但和整个光谱的能量无关(因为每种波长的光的能量都不相同)。HSV颜色空间在技术上不支持到辐射测定中测量的物理能量谱密度的一一映射。所以一般不建议做在HSV坐标和物理光性质如波长和振幅之间的直接比较。

抗锯齿(Anti-aliasing, AA)

有很多抗锯齿技术,都是在后处理阶段进行的,在分析URP后处理源码前,先了解一下抗锯齿相关的技术。 抗锯齿(Anti-aliasing, AA)技术是用来减少和消除图形渲染中的锯齿现象,即在边缘和细节部分出现的不平滑、断续的像素效果。锯齿通常发生在边缘的像素没有完全覆盖物体的情况下,导致视觉上的不连续性。以下是一些常见的抗锯齿技术及其原理:

全屏抗锯齿(FSAA,Full Screen Anti-Aliasing)

增加渲染分辨率,然后将图像缩小到目标分辨率。通过这种方式,每个最终像素中包含了更多的信息,从而平滑了边缘。FSAA是一种简单直接的方法,但对性能的影响较大,因为它需要渲染更多的像素。 全屏抗锯齿(FSAA,Full Screen Anti-Aliasing)是一种较早的抗锯齿技术,其基本思想是在比最终输出分辨率更高的分辨率上渲染场景,然后将这个高分辨率的图像缩小(下采样)到目标分辨率,以此来减少锯齿效果。虽然FSAA概念简单,但直接实现(如超采样抗锯齿,SSAA)在性能上可能非常昂贵,因为它要求渲染更多的像素。以下是FSAA在图形管线和GPU中的一般实现原理:

图形管线中的FSAA实现

  1. 渲染分辨率调整:首先,渲染目标(RenderTarget)的分辨率被设置为目标显示分辨率的倍数。例如,如果目标是1080p(1920x1080),则可能在4K(3840x2160)分辨率上进行渲染,这实质上是对每个维度进行了2倍的超采样。
  2. 场景渲染:图形管线按照这个高分辨率渲染整个场景,包括3D模型、纹理、光照等。这个过程涉及标准的渲染步骤,如几何处理、栅格化、片段着色等,但每个步骤处理的像素数量明显增加。
  3. 下采样:渲染完成后,图形管线执行一个下采样(或称为图像缩放)步骤,将高分辨率的渲染结果缩减到目标分辨率。这个过程通常使用滤波算法(如双线性或双三次滤波)来合并像素,以保留细节的同时减少锯齿。

GPU硬件中的FSAA支持

  • 硬件加速的下采样:现代GPU提供了硬件加速的图像缩放功能,可以高效地执行高到低分辨率的图像下采样。这有助于减轻FSAA对性能的影响,尤其是在下采样步骤。
  • 专用的渲染缓冲区:为了支持高分辨率渲染,GPU可能提供专用的高容量渲染缓冲区。这些缓冲区设计用来存储更多的像素数据,并且能够快速进行图像处理操作。

实现细节与考虑

  • 性能考量:FSAA(特别是当直接以超采样的形式实现时)对GPU的计算能力和内存带宽要求较高。由于需要渲染更多的像素,这可能导致帧率下降,尤其是在复杂场景和高分辨率设置下。
  • 图像质量:FSAA可以提供非常高质量的抗锯齿效果,因为它不仅影响场景的边缘,也影响场景内部的细节,如纹理的平滑度。
  • 应用场景:由于其性能成本,FSAA(特别是SSAA)可能不适合所有应用。在性能敏感的应用中(如VR或某些游戏),可能会考虑使用其他抗锯齿技术,如MSAA或FXAA。

超采样抗锯齿(SSAA,Super-Sample Anti-Aliasing)

类似于FSAA,SSAA通过在较高分辨率下渲染图像,然后缩小到目标分辨率来实现抗锯齿。与FSAA不同的是,SSAA通常采用更高级的采样算法和滤波技术,以获得更好的图像质量。SSAA对性能的影响非常大,通常只在对图像质量有极高要求的情况下使用。

多重采样抗锯齿(MSAA,Multi-Sample Anti-Aliasing)

在边缘区域采样多个像素,然后将这些采样的颜色值平均化以确定最终像素的颜色。MSAA专注于边缘,不会对整个场景的每个像素进行多重采样,因此相比FSAA,它对性能的影响较小。 多重采样抗锯齿(MSAA)是一种在图形管线和GPU硬件层面实现的抗锯齿技术,旨在减少边缘的锯齿现象而对性能影响尽可能小。MSAA的工作原理涉及多个阶段,从几何处理到像素着色,最后到像素写入帧缓冲区。下面是MSAA在图形管线和GPU中的实现细节:

几何处理阶段

  1. 多重采样缓冲区准备:在GPU中,为实现MSAA,首先需要创建一个多重采样缓冲区。这个缓冲区相比常规的颜色缓冲区有更多的样本点,用于存储每个像素的多个颜色值和深度信息。例如,4x MSAA意味着每个像素将包含4个独立的样本点。
  2. 几何图元的栅格化:当几何图元(如三角形)被栅格化(转换为像素网格)时,图形管线会为每个像素生成多个覆盖该像素的图元片段(片段是像素在渲染过程中的中间表示)。每个片段对应于像素内的一个样本点。

像素着色阶段

  1. 片段着色器执行:对于MSAA,片段着色器可能会被执行多次,每个样本点一次,或者根据具体实现,可能只执行一次并共享结果。不过,通常情况下,片段着色器的执行结果会存储在每个样本点中,允许细节丰富和光滑的边缘渲染。

最终像素写入

  1. 样本点合并(Resolve):在所有相关的几何图元被处理,且每个像素的样本点都被着色后,图形管线会执行一个合并(resolve)步骤,这一步骤将每个像素内的样本点的颜色值合并成一个单一的颜色值。这通常通过简单的平均值计算完成,但也可以包括更复杂的滤波算法。
  2. 写入帧缓冲区:合并后的像素值最终被写入到帧缓冲区中,完成整个渲染过程。

GPU支持和优化

  • 硬件级支持:现代GPU设计有专门的硬件支持MSAA,包括专用的多重采样缓冲区和高效的样本点处理机制。这种硬件级支持使得MSAA能够在保持较高渲染质量的同时,最小化对性能的影响。
  • 内存和带宽优化:虽然MSAA增加了内存使用和带宽需求(因为需要为每个像素存储多个样本点的数据),但GPU厂商实现了多种优化技术,如压缩技术和智能缓冲区管理,以减少这些开销。

快速近似抗锯齿(FXAA,Fast Approximate Anti-Aliasing)

FXAA是一种屏幕空间算法,它在图像的最终阶段处理,通过分析像素的亮度变化来识别边缘,并对边缘进行平滑处理。FXAA对性能的影响较小,实现简单,但可能会导致图像细节的轻微模糊。 快速近似抗锯齿(Fast Approximate Anti-Aliasing, FXAA)是一种屏幕空间的抗锯齿技术,旨在以较低的性能成本减少图像中的锯齿现象。与传统的多重采样抗锯齿(MSAA)相比,FXAA在图形管线的后期处理阶段实施,不需要对每个几何图元进行多次采样,因而对性能的影响较小。以下是FXAA在图形管线和GPU中的实现概览:

图形管线中的FXAA实现

  1. 渲染场景: 首先,场景被正常渲染到一个帧缓冲区中,不应用任何抗锯齿技术。
  2. 后期处理阶段: 场景渲染完成后,FXAA作为后期处理效果应用。这意味着FXAA操作是在图像已经渲染完成的基础上进行的,利用片段着色器对已经渲染好的图像进行处理。
  3. 边缘检测: FXAA首先通过分析像素颜色的局部梯度来识别图像中的边缘。这一步通常涉及比较当前像素与其邻近像素的颜色差异。
  4. 锯齿平滑: 一旦边缘被识别,FXAA算法会沿着边缘方向平滑锯齿,方法是对边缘附近的像素进行混合。这种方式旨在减少锐利边缘处的颜色梯度,从而减少视觉上的锯齿现象。
  5. 色彩校正: 在某些实现中,FXAA还可能包括对处理后图像的色彩进行微调,以保持图像的色彩真实性。

在GPU上的实现

FXAA是通过片段着色器在GPU上实现的。由于其算法主要基于像素操作,FXAA非常适合在GPU上执行,能够充分利用GPU并行处理像素的能力。实现FXAA通常涉及以下步骤:

  • 编写FXAA着色器: 开发者会编写一个片段着色器,该着色器包含FXAA算法的实现逻辑。这个着色器会对每个像素应用FXAA算法,包括边缘检测和锯齿平滑。
  • 应用着色器: 在渲染流程的后期处理阶段,将FXAA着色器应用于已渲染的图像。这通常通过将渲染好的场景作为纹理输入到后期处理管线,并执行FXAA着色器。
  • 输出结果: FXAA处理后的图像被输出到屏幕或下一阶段的渲染目标。

性能优化

FXAA的设计考虑到了性能优化,通过减少算法复杂度和精心设计的着色器代码来降低对性能的影响。相比于MSAA等传统抗锯齿技术,FXAA提供了一个性能成本较低的抗锯齿解决方案,特别适用于性能敏感的应用场景。

子像素抗锯齿(SMAA,Subpixel Morphological Anti-Aliasing)

SMAA是一种高级的屏幕空间抗锯齿技术,结合了MSAA、FXAA和其他技术的优点。它使用局部对比度检测来识别边缘,并使用多种策略(包括子像素级处理)来平滑边缘。SMAA旨在在保持高性能的同时提供较高的图像质量。 每种抗锯齿技术都有其优势和局限性,选择哪一种取决于特定的应用场。 子像素抗锯齿(Subpixel Morphological Antialiasing, SMAA)是一种高效且灵活的抗锯齿技术,它结合了多种抗锯齿技术的优点,如MSAA(多重采样抗锯齿)和FXAA(快速近似抗锯齿),但旨在提供更高的图像质量和更低的性能成本。SMAA通过分析图像来识别锯齿边缘,并采用各种策略对这些边缘进行平滑处理。以下是SMAA在图形管线和GPU上的一般实现流程:

SMAA的实现步骤

  1. 边缘检测: SMAA首先在图像中识别出锯齿边缘。这通常通过分析颜色、亮度或深度差异来实现,以找到可能产生锯齿的像素边界。边缘检测可以使用多种算法,包括基于梯度、Sobel算子或自定义滤波器。
  2. 模式识别: 识别出边缘后,SMAA通过比较这些边缘与一系列预定义的模式(或模板)来进行分类。这些模式对应于常见的锯齿形状和分布,使SMAA能够更精确地确定如何对特定的锯齿边缘进行处理。
  3. 子像素处理: SMAA利用子像素级的信息来改善锯齿边缘的渲染质量。这涉及到对锯齿边缘附近的像素进行细微调整,以模拟更高分辨率下的图像细节和边缘平滑效果。
  4. 形态学合并: 最后,SMAA应用形态学操作来平滑边缘和合并处理过的像素,最终产生抗锯齿效果。这一步骤利用了先前的模式识别结果,以确保边缘处理既平滑又自然,减少了过度模糊或其他视觉伪像。

在GPU和图形管线中的实现

  • Shader实现: SMAA主要在GPU的Shader阶段实现,通常作为一系列的后处理步骤。这包括顶点Shader用于设置屏幕空间的四边形,以及片元Shader来执行边缘检测、模式识别、子像素处理和形态学合并。
  • 后处理管线: 在现代图形API(如DirectX 11/12、Vulkan、OpenGL)中,SMAA通常作为渲染管线的后处理阶段进行,处理完所有的3D渲染和其他视觉效果后,再应用SMAA来改善最终的图像质量。
  • 性能优化: SMAA的实现通常涉及对性能的优化,以最小化对帧率的影响。这可能包括优化Shader代码、减少不必要的纹理采样、使用高效的数据结构和算法等。此外,SMAA允许不同级别的质量设置,开发者可以根据性能需求选择合适的级别。

兼容性和灵活性

SMAA的设计允许它在各种硬件和平台上有效运行,从高端PC显卡到移动设备。这得益于它对资源的有效管理和对性能的细致优化,使其成为当前广泛使用的

时间抗锯齿(TAA,Temporal Anti-Aliasing)

TAA利用连续帧之间的信息来平滑边缘和去除闪烁。它结合了当前帧和之前帧的数据,通过时间上的信息融合来减少锯齿。TAA能够提供非常平滑的图像质量,但可能引入一些运动模糊,特别是在快速移动的场景中。 时间抗锯齿(Temporal Anti-Aliasing, TAA)是一种先进的抗锯齿技术,通过结合多个帧的数据来减少锯齿,同时尽量保持图像细节。与传统的抗锯齿技术如MSAA(多重采样抗锯齿)或FSAA(全屏抗锯齿)不同,TAA不仅仅关注单一帧内的像素处理,而是利用时间维度上的信息来改善图像质量。这种方法在现代游戏和应用中越来越受欢迎,尤其是对于动态场景。

TAA的工作原理

  1. 运动矢量:TAA技术首先需要捕捉场景中对象的运动。这通过使用运动矢量图(Motion Vector Pass)来实现,运动矢量图记录了场景中每个像素点从前一帧到当前帧的移动情况。运动矢量通常由GPU的顶点着色器计算,并在渲染管线的早期阶段生成。
  2. 历史帧混合:TAA将当前帧的图像与之前帧的图像进行混合。通过对运动矢量的分析,TAA能够确定如何将当前帧的像素与历史帧的像素相结合,以减少锯齿并平滑边缘。这一步骤需要精确的运动跟踪和合适的权重分配,以确保图像的连贯性和减少运动模糊。
  3. 锐化和噪声处理:由于TAA在混合过程中可能引入模糊,因此在处理完混合后,通常会应用一定程度的锐化来保持图像的清晰度。同时,TAA也可能引入一些图案噪声,特别是在场景快速变化时,因此可能需要额外的噪声抑制步骤。

GPU和图形管线中的实现

  • 运动矢量的生成:现代GPU提供了高效的方式来计算和存储运动矢量。这通常在顶点着色器或几何着色器中实现,通过比较当前帧和前一帧的顶点位置来生成。
  • 图像处理和混合:TAA的混合处理主要在片段着色器和后处理阶段进行。GPU需要处理大量的纹理读取操作,包括访问当前帧的像素数据和历史帧的像素数据,然后基于运动矢量进行适当的混合。
  • 后处理效果:TAA通常作为一系列后处理效果的一部分实现,在最终图像输出前应用。这意味着TAA可以与其他图像处理效果(如色彩校正、HDR渲染等)一起在GPU的后处理管线中执行。

考虑因素

  • 运动估计的准确性:TAA的效果很大程度上依赖于运动矢量的准确性。不准确的运动跟踪可能导致图像拖影或其他视觉伪像。
  • 性能和质量平衡:TAA在提升图像质量的同时,相对于其他抗锯齿技术,对性能的影响较小。但是,它需要合理的资源分配和优化,特别是在高分辨率渲染或要求高帧率的应用中。

URP后处理源码分析

在URP中后处理源码主要由三个部分组成:

  1. Volume系统, 用于管理后处理组件参数,以及参数的混合。
  2. URP的PostProcessPasses,用于管理后处理的相关Pass(ColorGradingLutPass和PostProcessPass),Pass会将Volume系统提供的参数设置给Shader中使用。
  3. 各种后处理Shader(PostProcessData持久化对象存储了后处理中使用的各种Shader和纹理)

Volume系统

Volume系统由两个部分组成:

  1. 持久化类, 用于存储各种后处理组件的参数,核心类:VolumeProfile,VolumeComponent和VolumeParameter
  2. 运行时类, Volume表示一个后处理对象;VolumeStack记录所有Volume对象中,所有后处理组件根据权重混合后的最终结果;VolumeManager管理所有的Volume对象并在Update中,根据优先级,权重和混合距离对每个Volume中相同后处理组件的参数进行插值混合,具体的插值方式由对应的参数VolumeParameter类提供。

持久化类

VolumeProfile

VolumeProfile用于管理VolumeComponent列表,标准的一些Add,Remove,TryGet等函数。

VolumeComponent

VolumeComponent中包含了后处理组件中的参数(VolumeParameter对象列表)。核心函数就是Override,其作用的是同一个后处理组件中的参数进行插值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

public virtual void Override(VolumeComponent state, float interpFactor)
{
int count = parameters.Count;

for (int i = 0; i < count; i++)
{
var stateParam = state.parameters[i];
var toParam = parameters[i];

if (toParam.overrideState)
{
// Keep track of the override state for debugging purpose
stateParam.overrideState = toParam.overrideState;
stateParam.Interp(stateParam, toParam, interpFactor);
}
}
}

VolumeParameter

后处理组件的参数,有各种类型的派生类(int, float, vector等类型,也可以根据自己需求新增派生类),核心函数就是Interp,每种类型都有自己的Interp。

1
2
3
4
5
// float的插值函数,线性插值
public sealed override void Interp(float from, float to, float t)
{
m_Value = from + (to - from) * t;
}

运行时类

VolumeStack

VolumeStack类记录了所有的后处理组件,也是保存最终参数插值结果的。此类的组件参数值也是最终要被设置到Shader中去的参数值。

VolumeManager

VolumeManager管理了所有Volume组件和当前使用的VolumeStack对象,VolumeManager的目的就是为了将所有的Volume对象中的后处理组件的参数进行插值,并保存在VolumeStack对象中,其核心函数就是Update。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// stack : 保存插值混合后的结果
// trigger:计算混合时使用的位置(Volume对象到此位置来计算混合),默认使用相机位置,也可以在相机组件中拖拽一个对象作为计算混的位置。
// layerMask: 对此相机有效的Volume对象
public void Update(VolumeStack stack, Transform trigger, LayerMask layerMask)
{
Assert.IsNotNull(stack);

// 保证所有的后处理组件已创建
CheckBaseTypes();
// 检查记录最终结果的后处理栈是否创建
CheckStack(stack);

// 使用后处理组件的默认值初始化后处理栈
ReplaceData(stack, m_ComponentsDefaultState);

// 计算混合的触发位置,全局的Volume对象是不会跟触发位置与Volume对象之间的距离进行插值的
bool onlyGlobal = trigger == null;
var triggerPos = onlyGlobal ? Vector3.zero : trigger.position;

// 获取对应层级排序后的Volume对象列表
var volumes = GrabVolumes(layerMask);

// 获取Trigger对应的相机,当然也可以能没有相机(直接拖拽的一个Trigger对象)
Camera camera = null;
if (!onlyGlobal)
trigger.TryGetComponent<Camera>(out camera);

// 遍历所有的Volume,并对他们进行插值,全局的Volume是没有距离插值的
foreach (var volume in volumes)
{
if (volume == null)
continue;

#if UNITY_EDITOR
// 跳过场景视图中当前显示的场景中不存在的Volume
if (!IsVolumeRenderedByCamera(volume, camera))
continue;
#endif

// 跳过哪些被禁用,没有关连VolumeProfile或者权重为0的Volume对象
if (!volume.enabled || volume.profileRef == null || volume.weight <= 0f)
continue;

// 全局Volume没有距离的显示,都生效,本地的则需要根据它与trigger的距离来计算插值,全局Volume只需要权重插值即可
if (volume.isGlobal)
{
OverrideData(stack, volume.profileRef.components, Mathf.Clamp01(volume.weight));
continue;
}

// 全局的就继续下一个Volume对象
if (onlyGlobal)
continue;

// 如果不是全局的,又没有Collider对象,则不能根据Trigger与Volume对象是否相交,来计算插值权重,Trigger越靠近Volume对象上Collider的范围影响越大,权重也就越大。没有Collider则无法计算,所以也排除
var colliders = m_TempColliders;
volume.GetComponents(colliders);
if (colliders.Count == 0)
continue;

// 查找最新的Collider对象
float closestDistanceSqr = float.PositiveInfinity;

foreach (var collider in colliders)
{
if (!collider.enabled)
continue;

var closestPoint = collider.ClosestPoint(triggerPos);
var d = (closestPoint - triggerPos).sqrMagnitude;

if (d < closestDistanceSqr)
closestDistanceSqr = d;
}

colliders.Clear();

// 计算Volume的混合计算的平方
float blendDistSqr = volume.blendDistance * volume.blendDistance;

// 如果最近的距离都大于混合距离,则说明,Trigger位置和Volume对象的Collider不相交,则此Volume对于Trigger来所不生效,所有排除
if (closestDistanceSqr > blendDistSqr)
continue;

// 计算相交程度
float interpFactor = 1f;
if (blendDistSqr > 0f)
interpFactor = 1f - (closestDistanceSqr / blendDistSqr);

// 根据相交程度和Volume的权重值,确定最终的权重值(相交程度因子 x Volume的权重值)
OverrideData(stack, volume.profileRef.components, interpFactor * Mathf.Clamp01(volume.weight));
}
}

Volume

Volume代表一个后处理对象,此对象引用了VolumeProfile,VolumeProfile对象则记录了使用的后处理组件。Volume处理提供了VolumeProfile字段,还有isGlobal表示是全局还是本地Volume,weight表示权重,priority表示优先级(进行插值的先后顺序),blendDistance混合距离此字段仅用于本地Volume对象。

PostProcessPasses和PostProcessPass

PostProcessPasses是PostProcessPass的一个包装类,主要包含三个Pass:

  • ColorGradingLutPass colorGradingLutPass 在后处理之前,渲染LUT纹理。
  • PostProcessPass postProcessPass 对启用的后处理组件,设置参数并根据参数处理图片。
  • PostProcessPass finalPostProcessPass 所有后处理效果处理完后,对最后的图像再进行一次缩放,抗锯齿(FXAA)和颜色转换等。

ColorGradingLutPass

ColorGradingLutPass用于生成LUT纹理。核心函数:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
private static void ExecutePass(ScriptableRenderContext context, PassData passData, ref RenderingData renderingData, RTHandle internalLutTarget)
{
// 生LDR和HDR的LUT纹理的材质(LutBuilderLdr.shader和LutBuilderHdr.shader)
var cmd = renderingData.commandBuffer;
var lutBuilderLdr = passData.lutBuilderLdr;
var lutBuilderHdr = passData.lutBuilderHdr;
var allowColorGradingACESHDR = passData.allowColorGradingACESHDR;

using (new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.ColorGradingLUT)))
{
// 获取所有调色组件参数
var stack = VolumeManager.instance.stack;
var channelMixer = stack.GetComponent<ChannelMixer>();
var colorAdjustments = stack.GetComponent<ColorAdjustments>();
var curves = stack.GetComponent<ColorCurves>();
var liftGammaGain = stack.GetComponent<LiftGammaGain>();
var shadowsMidtonesHighlights = stack.GetComponent<ShadowsMidtonesHighlights>();
var splitToning = stack.GetComponent<SplitToning>();
var tonemapping = stack.GetComponent<Tonemapping>();
var whiteBalance = stack.GetComponent<WhiteBalance>();

ref var postProcessingData = ref renderingData.postProcessingData;
bool hdr = postProcessingData.gradingMode == ColorGradingMode.HighDynamicRange;
ref CameraData cameraData = ref renderingData.cameraData;

// 选择材质
var material = hdr ? lutBuilderHdr : lutBuilderLdr;

// 准备Shader中使用的参数
var lmsColorBalance = ColorUtils.ColorBalanceToLMSCoeffs(whiteBalance.temperature.value, whiteBalance.tint.value);
var hueSatCon = new Vector4(colorAdjustments.hueShift.value / 360f, colorAdjustments.saturation.value / 100f + 1f, colorAdjustments.contrast.value / 100f + 1f, 0f);
var channelMixerR = new Vector4(channelMixer.redOutRedIn.value / 100f, channelMixer.redOutGreenIn.value / 100f, channelMixer.redOutBlueIn.value / 100f, 0f);
var channelMixerG = new Vector4(channelMixer.greenOutRedIn.value / 100f, channelMixer.greenOutGreenIn.value / 100f, channelMixer.greenOutBlueIn.value / 100f, 0f);
var channelMixerB = new Vector4(channelMixer.blueOutRedIn.value / 100f, channelMixer.blueOutGreenIn.value / 100f, channelMixer.blueOutBlueIn.value / 100f, 0f);

var shadowsHighlightsLimits = new Vector4(
shadowsMidtonesHighlights.shadowsStart.value,
shadowsMidtonesHighlights.shadowsEnd.value,
shadowsMidtonesHighlights.highlightsStart.value,
shadowsMidtonesHighlights.highlightsEnd.value
);

var (shadows, midtones, highlights) = ColorUtils.PrepareShadowsMidtonesHighlights(
shadowsMidtonesHighlights.shadows.value,
shadowsMidtonesHighlights.midtones.value,
shadowsMidtonesHighlights.highlights.value
);

var (lift, gamma, gain) = ColorUtils.PrepareLiftGammaGain(
liftGammaGain.lift.value,
liftGammaGain.gamma.value,
liftGammaGain.gain.value
);

var (splitShadows, splitHighlights) = ColorUtils.PrepareSplitToning(
splitToning.shadows.value,
splitToning.highlights.value,
splitToning.balance.value
);

int lutHeight = postProcessingData.lutSize;
int lutWidth = lutHeight * lutHeight;
var lutParameters = new Vector4(lutHeight, 0.5f / lutWidth, 0.5f / lutHeight,
lutHeight / (lutHeight - 1f));

// 设置Shader参数
material.SetVector(ShaderConstants._Lut_Params, lutParameters);
material.SetVector(ShaderConstants._ColorBalance, lmsColorBalance);
material.SetVector(ShaderConstants._ColorFilter, colorAdjustments.colorFilter.value.linear);
material.SetVector(ShaderConstants._ChannelMixerRed, channelMixerR);
material.SetVector(ShaderConstants._ChannelMixerGreen, channelMixerG);
material.SetVector(ShaderConstants._ChannelMixerBlue, channelMixerB);
material.SetVector(ShaderConstants._HueSatCon, hueSatCon);
material.SetVector(ShaderConstants._Lift, lift);
material.SetVector(ShaderConstants._Gamma, gamma);
material.SetVector(ShaderConstants._Gain, gain);
material.SetVector(ShaderConstants._Shadows, shadows);
material.SetVector(ShaderConstants._Midtones, midtones);
material.SetVector(ShaderConstants._Highlights, highlights);
material.SetVector(ShaderConstants._ShaHiLimits, shadowsHighlightsLimits);
material.SetVector(ShaderConstants._SplitShadows, splitShadows);
material.SetVector(ShaderConstants._SplitHighlights, splitHighlights);

// YRGB curves
material.SetTexture(ShaderConstants._CurveMaster, curves.master.value.GetTexture());
material.SetTexture(ShaderConstants._CurveRed, curves.red.value.GetTexture());
material.SetTexture(ShaderConstants._CurveGreen, curves.green.value.GetTexture());
material.SetTexture(ShaderConstants._CurveBlue, curves.blue.value.GetTexture());

// Secondary curves
material.SetTexture(ShaderConstants._CurveHueVsHue, curves.hueVsHue.value.GetTexture());
material.SetTexture(ShaderConstants._CurveHueVsSat, curves.hueVsSat.value.GetTexture());
material.SetTexture(ShaderConstants._CurveLumVsSat, curves.lumVsSat.value.GetTexture());
material.SetTexture(ShaderConstants._CurveSatVsSat, curves.satVsSat.value.GetTexture());

// Tonemapping (baked into the lut for HDR)
if (hdr)
{
material.shaderKeywords = null;

switch (tonemapping.mode.value)
{
case TonemappingMode.Neutral: material.EnableKeyword(ShaderKeywordStrings.TonemapNeutral); break;
case TonemappingMode.ACES: material.EnableKeyword(allowColorGradingACESHDR ? ShaderKeywordStrings.TonemapACES : ShaderKeywordStrings.TonemapNeutral); break;
default: break; // None
}

// HDR output is active
if (cameraData.isHDROutputActive)
{
Vector4 hdrOutputLuminanceParams;
Vector4 hdrOutputGradingParams;

UniversalRenderPipeline.GetHDROutputLuminanceParameters(cameraData.hdrDisplayInformation, cameraData.hdrDisplayColorGamut, tonemapping, out hdrOutputLuminanceParams);
UniversalRenderPipeline.GetHDROutputGradingParameters(tonemapping, out hdrOutputGradingParams);

material.SetVector(ShaderPropertyId.hdrOutputLuminanceParams, hdrOutputLuminanceParams);
material.SetVector(ShaderPropertyId.hdrOutputGradingParams, hdrOutputGradingParams);

HDROutputUtils.ConfigureHDROutput(material, cameraData.hdrDisplayColorGamut, HDROutputUtils.Operation.ColorConversion);
}
}

cameraData.xr.StopSinglePass(cmd);


if (cameraData.xr.supportsFoveatedRendering)
cmd.SetFoveatedRenderingMode(FoveatedRenderingMode.Disabled);

// 渲染LUT纹理
Blitter.BlitCameraTexture(cmd, internalLutTarget, internalLutTarget, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, material, 0);

cameraData.xr.StartSinglePass(cmd);
}
}

PostProcessPass

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
// Pass执行时调用的参数
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
// 内置后处理组件,用于获取用户设置的后处理参数
var stack = VolumeManager.instance.stack;
m_DepthOfField = stack.GetComponent<DepthOfField>();
m_MotionBlur = stack.GetComponent<MotionBlur>();
m_PaniniProjection = stack.GetComponent<PaniniProjection>();
m_Bloom = stack.GetComponent<Bloom>();
m_LensDistortion = stack.GetComponent<LensDistortion>();
m_ChromaticAberration = stack.GetComponent<ChromaticAberration>();
m_Vignette = stack.GetComponent<Vignette>();
m_ColorLookup = stack.GetComponent<ColorLookup>();
m_ColorAdjustments = stack.GetComponent<ColorAdjustments>();
m_Tonemapping = stack.GetComponent<Tonemapping>();
m_FilmGrain = stack.GetComponent<FilmGrain>();
m_UseFastSRGBLinearConversion = renderingData.postProcessingData.useFastSRGBLinearConversion;
m_SupportDataDrivenLensFlare = renderingData.postProcessingData.supportDataDrivenLensFlare;

// 是最后一个Pass?
var cmd = renderingData.commandBuffer;
if (m_IsFinalPass)
{
using (new ProfilingScope(cmd, m_ProfilingRenderFinalPostProcessing))
{
// 渲染最后一个Pass
RenderFinalPass(cmd, ref renderingData);
}
}
else
{
using (new ProfilingScope(cmd, m_ProfilingRenderPostProcessing))
{
// 渲染后处理(各种后处理组件)效果
Render(cmd, ref renderingData);
}
}
}

// 渲染各种后处理效果
void Render(CommandBuffer cmd, ref RenderingData renderingData)
{
ref CameraData cameraData = ref renderingData.cameraData;
ref ScriptableRenderer renderer = ref cameraData.renderer;
bool isSceneViewCamera = cameraData.isSceneViewCamera;

// 检查各种后处理是否启用
bool useStopNan = cameraData.isStopNaNEnabled && m_Materials.stopNaN != null;
bool useSubPixeMorpAA = cameraData.antialiasing == AntialiasingMode.SubpixelMorphologicalAntiAliasing && SystemInfo.graphicsDeviceType != GraphicsDeviceType.OpenGLES2;
var dofMaterial = m_DepthOfField.mode.value == DepthOfFieldMode.Gaussian ? m_Materials.gaussianDepthOfField : m_Materials.bokehDepthOfField;
bool useDepthOfField = m_DepthOfField.IsActive() && !isSceneViewCamera && dofMaterial != null;
bool useLensFlare = !LensFlareCommonSRP.Instance.IsEmpty() && m_SupportDataDrivenLensFlare;
bool useMotionBlur = m_MotionBlur.IsActive() && !isSceneViewCamera;
bool usePaniniProjection = m_PaniniProjection.IsActive() && !isSceneViewCamera;

// 编辑下禁用运动模糊
useMotionBlur = useMotionBlur && Application.isPlaying;

// 打印不能使用TAA抗锯齿的原因
bool useTemporalAA = cameraData.IsTemporalAAEnabled();
if (cameraData.antialiasing == AntialiasingMode.TemporalAntiAliasing && !useTemporalAA)
TemporalAA.ValidateAndWarn(ref cameraData);

// 剩余的Pass数量
int amountOfPassesRemaining = (useStopNan ? 1 : 0) + (useSubPixeMorpAA ? 1 : 0) + (useDepthOfField ? 1 : 0) + (useLensFlare ? 1 : 0) + (useTemporalAA ? 1 : 0) + (useMotionBlur ? 1 : 0) + (usePaniniProjection ? 1 : 0);

// 禁用SwapBufferMSAA
if (m_UseSwapBuffer && amountOfPassesRemaining > 0)
{
renderer.EnableSwapBufferMSAA(false);
}

// 定义获取原和目标纹理函数
RTHandle source = m_UseSwapBuffer ? renderer.cameraColorTargetHandle : m_Source;
RTHandle destination = m_UseSwapBuffer ? renderer.GetCameraColorFrontBuffer(cmd) : null;
RTHandle GetSource() => source;
RTHandle GetDestination()
{
if (destination == null)
{
RenderingUtils.ReAllocateIfNeeded(ref m_TempTarget, GetCompatibleDescriptor(), FilterMode.Bilinear, TextureWrapMode.Clamp, name: "_TempTarget");
destination = m_TempTarget;
}
else if (destination == m_Source && m_Descriptor.msaaSamples > 1)
{
// Avoid using m_Source.id as new destination, it may come with a depth buffer that we don't want, may have MSAA that we don't want etc
RenderingUtils.ReAllocateIfNeeded(ref m_TempTarget2, GetCompatibleDescriptor(), FilterMode.Bilinear, TextureWrapMode.Clamp, name: "_TempTarget2");
destination = m_TempTarget2;
}
return destination;
}
// 定义交换函数
void Swap(ref ScriptableRenderer r)
{
--amountOfPassesRemaining;
if (m_UseSwapBuffer)
{
// 交换颜色Buffer
r.SwapColorBuffer(cmd);
source = r.cameraColorTargetHandle;
// 最后一个Pass, blit到 MSAA
if (amountOfPassesRemaining == 0 && !m_HasFinalPass)
r.EnableSwapBufferMSAA(true);
destination = r.GetCameraColorFrontBuffer(cmd);
}
else
{
CoreUtils.Swap(ref source, ref destination);
}
}

// 设置投影矩阵
cmd.SetGlobalMatrix(ShaderConstants._FullscreenProjMat, GL.GetGPUProjectionMatrix(Matrix4x4.identity, true));

// 使用使用Stop Not-a-Number后处理,将浮点数计算结果中的,NaN值修正为正常值(0)
if (useStopNan)
{
using (new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.StopNaNs)))
{
Blitter.BlitCameraTexture(cmd, GetSource(), GetDestination(), RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, m_Materials.stopNaN, 0);
Swap(ref renderer);
}
}

// Subpixel Morphological Anti-aliasing (SMAA)抗锯齿
if (useSubPixeMorpAA)
{
using (new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.SMAA)))
{
DoSubpixelMorphologicalAntialiasing(ref cameraData, cmd, GetSource(), GetDestination());
Swap(ref renderer);
}
}

// 景深效果
if (useDepthOfField)
{
var markerName = m_DepthOfField.mode.value == DepthOfFieldMode.Gaussian
? URPProfileId.GaussianDepthOfField
: URPProfileId.BokehDepthOfField;

using (new ProfilingScope(cmd, ProfilingSampler.Get(markerName)))
{
DoDepthOfField(cameraData.camera, cmd, GetSource(), GetDestination(), cameraData.pixelRect);
Swap(ref renderer);
}
}

// TAA抗锯齿
if (useTemporalAA)
{
using (new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.TemporalAA)))
{

TemporalAA.ExecutePass(cmd, m_Materials.temporalAntialiasing, ref cameraData, source, destination, m_MotionVectors.rt);
Swap(ref renderer);
}
}

// 运动模糊
if (useMotionBlur)
{
using (new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.MotionBlur)))
{
DoMotionBlur(cmd, GetSource(), GetDestination(), ref cameraData);
Swap(ref renderer);
}
}

//Panini projection(帕尼尼投影)后处理是一种用于图形渲染的视觉效果,旨在模拟宽角镜头的视觉效果,同时减少边缘扭曲。
//这种方法得名于18世纪的意大利画家Giovanni Paolo Panini,他以其精湛的视角和全景画作而闻名。在现代图形渲染中,帕尼尼投影被用来改善宽屏幕或宽视角下的视觉体验,特别是在视频游戏和虚拟现实应用中,为用户提供更自然、更沉浸的视觉体验。
if (usePaniniProjection)
{
using (new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.PaniniProjection)))
{
DoPaniniProjection(cameraData.camera, cmd, GetSource(), GetDestination());
Swap(ref renderer);
}
}

//"Uber"一词通常用来形容一个综合性的或"全包"(all-in-one)的着色器或后处理效果,它集成了多种视觉效果和图形处理技术。
//这种"Uber后处理效果"可能包括,但不限于,色彩校正、HDR(高动态范围)渲染、Bloom、景深(Depth of Field)、光晕(Lens Flares)、抗锯齿、阴影、光照效果等多个组件。
using (new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.UberPostProcess)))
{
// 重置Shader关键值
m_Materials.uber.shaderKeywords = null;

// 设置Bloom参数
bool bloomActive = m_Bloom.IsActive();
if (bloomActive)
{
using (new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.Bloom)))
SetupBloom(cmd, GetSource(), m_Materials.uber);
}

// 镜头光晕
if (useLensFlare)
{
bool usePanini;
float paniniDistance;
float paniniCropToFit;
if (m_PaniniProjection.IsActive())
{
usePanini = true;
paniniDistance = m_PaniniProjection.distance.value;
paniniCropToFit = m_PaniniProjection.cropToFit.value;
}
else
{
usePanini = false;
paniniDistance = 1.0f;
paniniCropToFit = 1.0f;
}

using (new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.LensFlareDataDrivenComputeOcclusion)))
{
LensFlareDataDrivenComputeOcclusion(cameraData.camera, cmd, GetSource(), usePanini, paniniDistance, paniniCropToFit);
}
using (new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.LensFlareDataDriven)))
{
LensFlareDataDriven(cameraData.camera, cmd, GetSource(), usePanini, paniniDistance, paniniCropToFit);
}
}

// 设置其他效果参数
// Lens Distortion后处理效果模拟了真实世界相机镜头中常见的畸变现象,主要是桶形畸变(barrel distortion)和枕形畸变(pincushion distortion
SetupLensDistortion(m_Materials.uber, isSceneViewCamera);
//色差(Chromatic Aberration),也称为色彩像差,是一种由于镜头无法将不同颜色的光线聚焦在同一点上而产生的视觉现象。
//这种现象在图像的边缘部分尤为明显,表现为彩色的晕边,通常是紫色或绿色的边缘。色差通常出现在便宜的镜头或极宽角镜头的照片中,而高质量的镜头设计会尽量减少这种效果。
//边缘会有类似彩虹的色斑
SetupChromaticAberration(m_Materials.uber);
//晕影(Vignette)效果是一种在摄影、视频和图形渲染中常用的视觉效果,其特点是图像边缘相对于中心部分显得更暗,有时也呈现出柔和的过渡。
//这种效果可以增强图像的焦点,通过在视觉上引导观众的注意力向中心集中,从而提升图像的美感和深度感。
//在传统摄影中,晕影效果可能由于镜头特性、遮光不足或特定的处理技术而自然产生。而在数字图像处理和图形渲染中,晕影效果通常是通过后处理技术故意添加的。
SetupVignette(m_Materials.uber, cameraData.xr);
//色彩分级是另一种强大的图像后处理技术,用于调整图像的整体色调,包括对比度、色温、饱和度等,以达到特定的视觉风格或情绪效果。
//色彩分级在电影制作、视频游戏和摄影中广泛应用,用于增强视觉叙事或引导观众情绪。将原始颜色通过LUT(颜色查找表),映射为对应颜色
SetupColorGrading(cmd, ref renderingData, m_Materials.uber);

//Film Grain(胶片颗粒)是指在传统胶片摄影中出现的一种视觉效果,由于胶片上的银盐颗粒大小不一而产生的细微颗粒状纹理。在数码摄影和视频制作中,人们通常通过后期处理添加模拟的胶片颗粒效果,以达到增加质感、增强视觉深度或复古风格的目的。
SetupGrain(ref cameraData, m_Materials.uber);
//Dithering(抖动)是一种在数字图像处理中常用的技术,用于在有限的颜色深度显示设备上模拟更广泛的颜色范围。这种技术通过在像素之间故意添加噪声或图案,来模拟中间色调或渐变效果,从而减少颜色带(色阶突变)的视觉影响。
SetupDithering(ref cameraData, m_Materials.uber);

//是否需要转换回SRGB
if (RequireSRGBConversionBlitToBackBuffer(ref cameraData))
m_Materials.uber.EnableKeyword(ShaderKeywordStrings.LinearToSRGBConversion);

//是否需要转换HDR输出
bool requireHDROutput = RequireHDROutput(ref cameraData);
if (requireHDROutput)
{
// Color space conversion is already applied through color grading, do encoding if uber post is the last pass
// Otherwise encoding will happen in the final post process pass or the final blit pass
HDROutputUtils.Operation hdrOperation = !m_HasFinalPass && m_EnableColorEncodingIfNeeded ? HDROutputUtils.Operation.ColorEncoding : HDROutputUtils.Operation.None;
SetupHDROutput(cameraData.hdrDisplayInformation, cameraData.hdrDisplayColorGamut, m_Materials.uber, hdrOperation);
}

if (m_UseFastSRGBLinearConversion)
{
m_Materials.uber.EnableKeyword(ShaderKeywordStrings.UseFastSRGBLinearConversion);
}

DebugHandler debugHandler = GetActiveDebugHandler(ref renderingData);
bool resolveToDebugScreen = debugHandler != null && debugHandler.WriteToDebugScreenTexture(ref cameraData);
debugHandler?.UpdateShaderGlobalPropertiesForFinalValidationPass(cmd, ref cameraData, !m_HasFinalPass && !resolveToDebugScreen);

// 完成参数设置后,blit
var colorLoadAction = RenderBufferLoadAction.DontCare;
if (m_Destination == k_CameraTarget && !cameraData.isDefaultViewport)
colorLoadAction = RenderBufferLoadAction.Load;

// Note: We rendering to "camera target" we need to get the cameraData.targetTexture as this will get the targetTexture of the camera stack.
// Overlay cameras need to output to the target described in the base camera while doing camera stack.
RenderTargetIdentifier cameraTargetID = BuiltinRenderTextureType.CameraTarget;
#if ENABLE_VR && ENABLE_XR_MODULE
if (cameraData.xr.enabled)
cameraTargetID = cameraData.xr.renderTarget;
#endif

if (!m_UseSwapBuffer)
m_ResolveToScreen = cameraData.resolveFinalTarget || m_Destination.nameID == cameraTargetID || m_HasFinalPass == true;

// 使用了SwapBuffer但又不是相机栈的最后
if (m_UseSwapBuffer && !m_ResolveToScreen)
{
if (!m_HasFinalPass)
{
renderer.EnableSwapBufferMSAA(true);
destination = renderer.GetCameraColorFrontBuffer(cmd);
}
Blitter.BlitCameraTexture(cmd, GetSource(), destination, colorLoadAction, RenderBufferStoreAction.Store, m_Materials.uber, 0);
renderer.ConfigureCameraColorTarget(destination);
Swap(ref renderer);
}
else if (!m_UseSwapBuffer)
{
var firstSource = GetSource();
Blitter.BlitCameraTexture(cmd, firstSource, GetDestination(), colorLoadAction, RenderBufferStoreAction.Store, m_Materials.uber, 0);
Blitter.BlitCameraTexture(cmd, GetDestination(), m_Destination, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, m_BlitMaterial, m_Destination.rt?.filterMode == FilterMode.Bilinear ? 1 : 0);
}
else if (m_ResolveToScreen)
{
// 将最终结果Blit到调试器的纹理上
if (resolveToDebugScreen)
{
Blitter.BlitCameraTexture(cmd, GetSource(), debugHandler.DebugScreenColorHandle, RenderBufferLoadAction.Load, RenderBufferStoreAction.Store, m_Materials.uber, 0);
renderer.ConfigureCameraTarget(debugHandler.DebugScreenColorHandle, debugHandler.DebugScreenDepthHandle);
}
else
{
// 将最终结果Blit到屏幕
RenderTargetIdentifier cameraTarget = cameraData.targetTexture != null ? new RenderTargetIdentifier(cameraData.targetTexture) : cameraTargetID;
RTHandleStaticHelpers.SetRTHandleStaticWrapper(cameraTarget);
var cameraTargetHandle = RTHandleStaticHelpers.s_RTHandleWrapper;

RenderingUtils.FinalBlit(cmd, ref cameraData, GetSource(), cameraTargetHandle, colorLoadAction, RenderBufferStoreAction.Store, m_Materials.uber, 0);
renderer.ConfigureCameraColorTarget(cameraTargetHandle);
}
}
}
}

内建后处理Shader

色彩校正(色彩调整)

Unity是将色彩校正信息存在LUT纹理中,最后再查找LUT纹理中的对应颜色来替换当前颜色。Unity内建的色彩校正后处理组件有:WhiteBalance(白平衡), ColorAdjustments(颜色调整), SplitToning(色调分离), ChannelMixer(通道混合), ShadowsMidtonesHighlights(阴影,中间调,高光),LiftGammaGain(提升,伽马,增强), ColorCurves(颜色曲线), Tonemapping(色调映射)。

Neutral LUT图,是一个“neutral”查找表(LUT)指的是一种不改变色彩的LUT,通常用于作为起点或基准。这种LUT不对图像的色彩、对比度或亮度做任何改变,使得原始图像保持不变。neutral LUT通常用于测试、校准或作为创建自定义色彩分级预设的基础。

WhiteBalance(白平衡)

白平衡是图像处理中的一个重要步骤,目的是校正图像中的色彩,使得白色对象无论在什么样的光线下都被渲染为白色,从而提高图像的色彩准确性。在进行白平衡处理时,LMS系数用于调整图像中的颜色分量,以反映光源下的真实颜色。不同环境色温下拍出来的拍色是有一定差异的,白平衡的目的就是通过后期的手段将白色还原成白色。

色温图:

色温图

在调色的过程中,如果图片本身偏高色温(蓝色),那么就要补偿低色温(橙色),来达到白色。如果想营造某种艺术效果,可以根据需求调整。

过程:

  1. 将白平衡的色温(temperature)和色调(tint)参数转换成LMS系数
  2. 将原颜色在LMS空间应用白平衡参数
  3. 最后将LMS空间中的值转回线性RGB空间

代码: 色温(temperature)和色调(tint)参数转换成LMS系数:

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
public static Vector3 ColorBalanceToLMSCoeffs(float temperature, float tint)
{
// 将色温和色调映射到,大约[-1.5;1.5]的范围,works best
float t1 = temperature / 65f;
float t2 = tint / 65f;

// 获取参考白点(D65)的的xy色度值, D65指的时,色温6500K(开尔文)是纯白的色度值
// D65白点的色图值:x = 0.3127, y = 0.3290
// 具体计算公式参见维基百科的[CIE 1931 XYZ色彩空间](https://zh.wikipedia.org/wiki/CIE_1931%E8%89%B2%E5%BD%A9%E7%A9%BA%E9%97%B4)
float x = 0.31271f - t1 * (t1 < 0f ? 0.1f : 0.05f);
float y = StandardIlluminantY(x) + t2 * 0.05f; // 2.87f * x - 3f * x * x - 0.27509507f

// 计算LMS空间中的系数。
var w1 = new Vector3(0.949237f, 1.03542f, 1.08728f); // D65白点对应的LMS系数
var w2 = CIExyToLMS(x, y);
return new Vector3(w1.x / w2.x, w1.y / w2.y, w1.z / w2.z);
}

//将CIExy转换到LMS空间下
public static Vector3 CIExyToLMS(float x, float y)
{
// 三刺值
float Y = 1f;
float X = Y * x / y;
float Z = Y * (1f - x - y) / y;

// 三刺值到LMS的变化矩阵
float L = 0.7328f * X + 0.4296f * Y - 0.1624f * Z;
float M = -0.7036f * X + 1.6975f * Y + 0.0061f * Z;
float S = 0.0030f * X + 0.0136f * Y + 0.9834f * Z;

return new Vector3(L, M, S);
}

在LMS空间中计算白平衡

1
2
3
4
5
6
// 当前Lut位置的Neutral值
float3 colorLMS = LinearToLMS(colorLinear);
// 计算白平衡
colorLMS *= _ColorBalance.xyz;
// 转换回线性空间,进行后续的计算
colorLinear = LMSToLinear(colorLMS);

ColorAdjustments(颜色调整)

Color Adjustments是图像处理和图形设计中常见的一组技术,用于修改和优化图像的色彩属性,包括亮度、对比度、饱和度、色调、曝光等。这些调整能够帮助图像更好地传达预期的视觉效果或情绪,提升图像质量,或者适应特定的显示设备和环境。

  • 对比度(Contrast):调整图像明暗区域之间的差异,增加对比度会使图像的明暗区域更加分明,而降低对比度则会使图像看起来更加平坦。
  • 色调(Hue):改变图像中的色彩平衡,常用于为图像创建特定的色彩风格或修正图像的色温。
  • 饱和度(Saturation):调整图像中颜色的强度,饱和度高的图像色彩会更加鲜明,而饱和度低的图像则色彩更加柔和。
  • 亮度(Brightness):调整图像的明暗程度,使图像看起来更亮或更暗。
  • 曝光(Exposure):模拟调整摄影中的曝光效果,可以使图像整体变亮或变暗,用于修正过曝或欠曝的图像

对比度

过程:

  1. 将颜色转换到对数(LogC)空间
  2. 应用对比度调节公式, 如下
  3. 将结果转换回线性(Linear)空间

\((color - middleGray) * contrast + middleGray\)

说明: color : 需要调节的颜色 middleGray : 中性灰色的定义(0.5或ACES定义的0.4135884) contrast :对比度调节系数

代码:

1
2
3
float3 colorLog = LinearToLogC(colorLinear);
colorLog = (colorLog - ACEScc_MIDGRAY) * _HueSatCon.z + ACEScc_MIDGRAY;
colorLinear = LogCToLinear(colorLog);

色调

过程:

  1. 将RGB颜色转换到HSV色彩空间,
  2. 调整饱和度H值,
  3. 将结果转换回RGB空间

代码:

1
2
3
float3 hsv = RgbToHsv(colorLinear);
float hue = hsv.x + _HueSatCon.x;
colorLinear = HsvToRgb(hsv);

饱和度

过程:

  1. 将RGB颜色转换到HSV色彩空间,
  2. 调整饱和度S值,
  3. 将结果转换回RGB空间

代码:

1
2
luma = GetLuminance(colorLinear);    // real3(0.2126729, 0.7151522, 0.0721750)  ACES:half3(0.272229, 0.674082, 0.0536895)
colorLinear = luma.xxx + (_HueSatCon.yyy * satMult) * (colorLinear - luma.xxx);

曝光和亮度

曝光(Exposure)亮度(Brightness) 是两个在摄影、视频制作和图像处理中常用的术语,它们描述图像的光照特性,但指代的概念和调整方式有所不同。

曝光(Exposure) 曝光是指在拍摄过程中,光线作用在相机传感器上的量,这个量由光圈大小(Aperture)、快门速度(Shutter Speed)和ISO感光度(ISO Sensitivity)三个基本相机设置共同决定。曝光决定了图像最初捕捉到的光量,是摄影的一个基础概念。

光圈:控制镜头进光量的大小。 快门速度:控制光线照射在传感器上的时间长度。 ISO感光度:控制传感器对光线的敏感度。

正确的曝光是指在特定的拍摄条件下,选取适当的光圈、快门速度和ISO设置,以便传感器接收到足够的光量,使得图像既不过曝(太亮,丢失细节)也不欠曝(太暗,缺乏细节)。 曝光调节模拟了在拍摄阶段改变光量的效果,它可以更加广泛地影响图像的整体明暗,包括极亮和极暗的区域。曝光调节通常对图像的动态范围有较大的影响,可能会导致亮部过曝或暗部过暗而丢失细节。在数学上,曝光调节可能被模拟为对图像每个像素值的乘法调整: \[C^{'} = C x 2^{\delta EV} \]

  • \(C^{'}\) 是调整后的像素值。
  • \(C\) 是原始像素值
  • \(\delta EV\) 是曝光值的变化,以停(stop)为单位。正值表示增加曝光,负值表示减少曝光。每增加1个停,图像的亮度翻倍;每减少1个停,图像的亮度减半。

亮度(Brightness)

亮度是指图像看起来的光线亮度感觉,是图像处理和显示设备中的一个概念,通常用于描述图像的视觉感受。亮度调整通常在图像已经被捕捉并需要在后期处理时改变其整体明暗程度时进行。 亮度调节可以通过多种方式在图像处理中实现。最直接的方法是对图像的每个像素的亮度分量进行调整。下面是一些基本的亮度调节方法:

线性亮度调整

最简单的亮度调整方法是对图像的每个像素值直接加上一个固定的量。对于RGB图像,这意味着对R、G、B三个通道的值都进行相同的调整:

\[C^{'} = C + \delta \]

  • \(C^{'}\) 是调整后的颜色值
  • \(C\) 是原始颜色值,
  • \(\delta\) 是要调整的亮度量(可以为正值或负值)。

保持色彩比例的亮度调整 为了避免在调整亮度时改变色彩的相对比例(即色调和饱和度),可以先将RGB颜色转换到一个能够分离亮度信息的色彩空间(如HSV或HSL),只对亮度分量进行调整,然后再转换回RGB空间。亮度分量的调整可以表示为: \[V^{'} = V + \delta \]

  • \(V^{'}\) 是调整后的亮度值
  • \(V\) 是原始的亮度值
  • \(\delta\) 是要调整的亮度量

伽马校正亮度调整 伽马校正提供了一种更复杂的亮度调整方式,可以更符合人眼对亮度的非线性感知:

\[C^{'}= 255x(\frac{C}{255})^{1/ \gamma}\]

  • \(C^{'}\) 是调整后的颜色值
  • \(C\) 是原始颜色值
  • \(\gamma\) 是伽马校正值。通过调整\(\gamma\)值,可以在增亮或减暗图像的同时保持细节的自然外观。

区别

  • 计算方式: 曝光调节通过乘法影响像素值,而亮度调节通过加法改变像素值。
  • 影响范围: 曝光调节可以更显著地改变图像的整体亮度,包括最亮和最暗的区域,可能会导致亮部或暗部细节的丢失。亮度调节则更加温和,通常不会导致亮部或暗部细节的丢失,但对动态范围的影响较小。
  • 应用场景: 在需要大幅度调整图像明暗,模拟不同曝光效果时,使用曝光调节;在需要微调图像亮度,保持大部分细节时,使用亮度调节。

SplitToning(色调分离)

Split Toning调节可以通过多种方法实现,特别是在处理数字图像时。基础思路涉及分别向图像的阴影(Shadows)和高光(Highlights)部分应用不同的色调,而保持中间调的颜色相对不变。

过程:

  1. 为了和Adobe产品的计算方式一样,要将线性颜色转换到Gamma
  2. 计算像素的亮度值,根据亮度值,确定分离后的阴影和高光颜色
  3. 将分离出来的阴影和高光颜色与原颜色使用柔光的混合模式进行混合
  4. 最后将Gamma空间颜色转换回线性空间

公式:

基础公式 假设我们有两个目标色调:一个用于阴影部分\(C_{shadow}\),另一个用于高光部分 \(C_{highlight}\)。图像中每个像素点的最终色调\(C_{final}\)可以通过以下方式计算得出:

\[C_{final} = w * C_{shadow} + (1-w)*C_{highlight}\]

其中,\(w\)是一个根据像素亮度确定的权重,用于控制该像素更接近阴影色调还是高光色调。权重\(w\)的计算可以通过多种方式实现,一种简单的方法是使用像素亮度的线性或非线性函数。

权重计算 权重\(w\)可以根据像素的亮度\(L\)(通常是从0到1)进行计算,其中低亮度值接近阴影,高亮度值接近高光。一个简单的线性模型是:

\[w = \frac{L-L_{min}}{L_{max}-L_{min}}\]

​这里,​\(L_{min}\)\(L_{max}\)分别表示图像中亮度的最小和最大值,用于确定阴影和高光的界限。为了更平滑的过渡,也可以使用基于S型函数的模型,如:

\[w = \frac{1}{1+e^{-(L-L_{mid})*k}}\]

这里,\(L_{mid}\)是阴影和高光之间的中间亮度值,\(k\)是控制过渡平滑程度的参数。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 尽管与直觉相反,但让色调分离的工作方式与Adobe中的工作方式相同
// 我们必须在伽玛空间中进行所有数学运算的产品
// 亮度调节参数
float balance = _SplitShadows.w;
// 转换到Gamma空间
float3 colorGamma = PositivePow(colorLinear, 1.0 / 2.2);

// 计算像素本身的亮度 + 调节亮度
float luma = saturate(GetLuminance(saturate(colorGamma)) + balance);
// 当luma变小时,会把大部分像素颜色往阴影颜色拉,否者像高光颜色拉
float3 splitShadows = lerp((0.5).xxx, _SplitShadows.xyz, 1.0 - luma);
float3 splitHighlights = lerp((0.5).xxx, _SplitHighlights.xyz, luma);
// 用柔光混合模式混合原颜色和阴影色,以及高光色
colorGamma = SoftLight(colorGamma, splitShadows);
colorGamma = SoftLight(colorGamma, splitHighlights);
// 将颜色转换回线性空间
colorLinear = PositivePow(colorGamma, 2.2);
1
2
3
4
5
6
7
8
// 用于色调分离的柔光混合模式。 只要 `blend` 为 [0;1] 即可在 HDR 中工作
float3 SoftLight(float3 base, float3 blend)
{
float3 r1 = 2.0 * base * blend + base * base * (1.0 - 2.0 * blend);
float3 r2 = sqrt(base) * (2.0 * blend - 1.0) + 2.0 * base * (1.0 - blend);
float3 t = step(0.5, blend);
return r2 * t + (1.0 - t) * r1;
}

ChannelMixer(通道混合)

过程:

Channel Mixer,它允许用户调整图像中红色、绿色和蓝色通道的比例,以此来改变图像的总体色彩。通过混合这些颜色通道,用户可以实现广泛的色彩效果,包括颜色校正、黑白转换、色彩强化等。在数字图像处理中,一个像素的颜色通常由红色(R)、绿色(G)、蓝色(B)三个颜色通道的值组合而成。Channel Mixer 允许用户调整这些颜色通道的输出比例,通过修改每个通道对最终图像颜色的贡献度来改变图像的颜色平衡。

公式:

\(R_{out} = R_{in} * R_x + G_{in} * G_y + B_{in} * B_z\) \(G_{out} = R_{in} * R_x + G_{in} * G_y + B_{in} * B_z\) \(B_{out} = R_{in} * R_x + G_{in} * G_y + B_{in} * B_z\)

x,y和z分别是红,绿和蓝色通道的混合系数

代码:

1
2
3
4
5
 colorLinear = float3(
dot(colorLinear, _ChannelMixerRed.xyz), // R
dot(colorLinear, _ChannelMixerGreen.xyz), // G
dot(colorLinear, _ChannelMixerBlue.xyz) // B
);

ShadowsMidtonesHighlights(阴影,中间调,高光)

在图像处理和色彩分级中,对阴影(Shadows)、中间调(Midtones)、高光(Highlights)的调节是一种常见的技术,用于细致调整图像的色调平衡和对比度。这种方法允许你分别调整图像中暗部、中亮部和亮部的亮度、色彩和饱和度,以达到更为精细的视觉效果。

过程:

  1. 分离阴影、中间调、高光:首先,基于像素的亮度值将图像分割成阴影、中间调和高光三个部分。这通常通过设置亮度阈值来实现,不同的亮度范围对应于阴影、中间调和高光。
  2. 应用调节:对每个部分(阴影、中间调、高光)分别应用亮度、色彩和饱和度的调节。调节可以是线性的,也可以是基于特定曲线的,如S型曲线。

公式:

虽然具体的调节公式可能根据不同的软件和实现而异,但可以用以下基础形式来近似描述:

\[C^{'} = C + \Delta C x W(L)\]

其中: - \(C^{'}\)是调整后的颜色值。 - \(C\)是原颜色。 - \(\Delta C\)是想应用的调整量,可以是亮度、色彩或饱和度的变化。 - \(W(L)\) 是一个基于像素亮度\(L\)的权重函数,用于确定该像素属于阴影、中间调还是高光,以及应该如何应用调整。

代码:

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
37
38
public static (Vector4, Vector4, Vector4) PrepareShadowsMidtonesHighlights(in Vector4 inShadows, in Vector4 inMidtones, in Vector4 inHighlights)
{
float weight;
// 根据权重设置最终应用的阴影颜色值
var shadows = inShadows;
shadows.x = Mathf.GammaToLinearSpace(shadows.x);
shadows.y = Mathf.GammaToLinearSpace(shadows.y);
shadows.z = Mathf.GammaToLinearSpace(shadows.z);
weight = shadows.w * (Mathf.Sign(shadows.w) < 0f ? 1f : 4f);
shadows.x = Mathf.Max(shadows.x + weight, 0f);
shadows.y = Mathf.Max(shadows.y + weight, 0f);
shadows.z = Mathf.Max(shadows.z + weight, 0f);
shadows.w = 0f;

// 根据权重设置最终应用的中间调颜色值
var midtones = inMidtones;
midtones.x = Mathf.GammaToLinearSpace(midtones.x);
midtones.y = Mathf.GammaToLinearSpace(midtones.y);
midtones.z = Mathf.GammaToLinearSpace(midtones.z);
weight = midtones.w * (Mathf.Sign(midtones.w) < 0f ? 1f : 4f);
midtones.x = Mathf.Max(midtones.x + weight, 0f);
midtones.y = Mathf.Max(midtones.y + weight, 0f);
midtones.z = Mathf.Max(midtones.z + weight, 0f);
midtones.w = 0f;

// 根据权重设置最终应用的高光颜色值
var highlights = inHighlights;
highlights.x = Mathf.GammaToLinearSpace(highlights.x);
highlights.y = Mathf.GammaToLinearSpace(highlights.y);
highlights.z = Mathf.GammaToLinearSpace(highlights.z);
weight = highlights.w * (Mathf.Sign(highlights.w) < 0f ? 1f : 4f);
highlights.x = Mathf.Max(highlights.x + weight, 0f);
highlights.y = Mathf.Max(highlights.y + weight, 0f);
highlights.z = Mathf.Max(highlights.z + weight, 0f);
highlights.w = 0f;

return (shadows, midtones, highlights);
}
1
2
3
4
5
6
7
8
9
10
// 计算亮度值
luma = GetLuminance(colorLinear);
// 计算阴影,中间调和高光的调节系数(最终只有一个部分的系数为1,其他的都为0)
float shadowsFactor = 1.0 - smoothstep(_ShaHiLimits.x, _ShaHiLimits.y, luma);
float highlightsFactor = smoothstep(_ShaHiLimits.z, _ShaHiLimits.w, luma);
float midtonesFactor = 1.0 - shadowsFactor - highlightsFactor;
// 计算最终的颜色
colorLinear = colorLinear * _Shadows.xyz * shadowsFactor
+ colorLinear * _Midtones.xyz * midtonesFactor
+ colorLinear * _Highlights.xyz * highlightsFactor;

LiftGammaGain(提升,伽马,增强)

Lift Gamma Gain是一种常用于色彩分级和图像调整中的技术,特别是在视频和电影后期制作中。它允许用户独立调整图像的阴影(Lift)、中间调(Gamma)和高光(Gain)部分,提供了对图像色彩和对比度的精细控制。这三个参数共同作用,可以实现复杂的视觉效果和色彩调整。

原理:

  • Lift(提升):主要影响图像的暗部(阴影)。调整Lift会向上或向下移动色彩的黑点,从而改变图像的整体明暗度,而不大幅影响高光。
  • Gamma(伽马):影响图像的中间调。调整Gamma值是对图像中间亮度级别的调整,可以在不显著改变高光和阴影的情况下,增加或减少图像的对比度。
  • Gain(增益):主要影响图像的亮部(高光)。调整Gain会增加或减少图像高光部分的亮度,而对暗部的影响较小。

代码:

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
37
38
39
public static (Vector4, Vector4, Vector4) PrepareLiftGammaGain(in Vector4 inLift, in Vector4 inGamma, in Vector4 inGain)
{
var lift = inLift;
lift.x = Mathf.GammaToLinearSpace(lift.x) * 0.15f;
lift.y = Mathf.GammaToLinearSpace(lift.y) * 0.15f;
lift.z = Mathf.GammaToLinearSpace(lift.z) * 0.15f;

float lumLift = Luminance(lift);
lift.x = lift.x - lumLift + lift.w;
lift.y = lift.y - lumLift + lift.w;
lift.z = lift.z - lumLift + lift.w;
lift.w = 0f;

var gamma = inGamma;
gamma.x = Mathf.GammaToLinearSpace(gamma.x) * 0.8f;
gamma.y = Mathf.GammaToLinearSpace(gamma.y) * 0.8f;
gamma.z = Mathf.GammaToLinearSpace(gamma.z) * 0.8f;

float lumGamma = Luminance(gamma);
gamma.w += 1f;
gamma.x = 1f / Mathf.Max(gamma.x - lumGamma + gamma.w, 1e-03f);
gamma.y = 1f / Mathf.Max(gamma.y - lumGamma + gamma.w, 1e-03f);
gamma.z = 1f / Mathf.Max(gamma.z - lumGamma + gamma.w, 1e-03f);
gamma.w = 0f;

var gain = inGain;
gain.x = Mathf.GammaToLinearSpace(gain.x) * 0.8f;
gain.y = Mathf.GammaToLinearSpace(gain.y) * 0.8f;
gain.z = Mathf.GammaToLinearSpace(gain.z) * 0.8f;

float lumGain = Luminance(gain);
gain.w += 1f;
gain.x = gain.x - lumGain + gain.w;
gain.y = gain.y - lumGain + gain.w;
gain.z = gain.z - lumGain + gain.w;
gain.w = 0f;

return (lift, gamma, gain);
}
1
2
colorLinear = colorLinear * _Gain.xyz + _Lift.xyz;
colorLinear = sign(colorLinear) * pow(abs(colorLinear), _Gamma.xyz);

ColorCurves(颜色曲线)

颜色曲线(Color Curves)是一种强大的工具,它允许用户通过调整色彩通道的输入和输出值之间的曲线关系来精细控制图像的亮度、对比度、色彩平衡和色调。色彩曲线提供了比基本亮度和对比度调节更细致和灵活的控制方式。

Unity的ColorCurves提供了两种类型的曲线:色彩映射曲线和YRGB曲线

色彩映射曲线 色彩映射曲线是在图像和视频后期处理中使用的常见方法,用于细粒度地调整色彩属性。尽管每种调节关注的色彩属性不同,它们都旨在通过不同的映射关系来改变图像的色彩表现。其中包括:HueVsHue、HueVsSat、SatVsSat和LumVsSat

  1. HueVsHue:这是色相对色相的调整,允许你根据原始色相改变色相。例如,可以将所有的绿色调整为更偏蓝的色相。

  2. HueVsSat:这是色相对饱和度的调整,允许你基于图像中特定色相的存在来增加或减少饱和度。例如,可以仅增加红色的饱和度而不影响其他色彩。

  3. SatVsSat:这是饱和度对饱和度的调整,允许你根据原始饱和度改变饱和度级别。这可以用来增强色彩鲜艳度或者使图像看起来更自然。

  4. LumVsSat:这是亮度对饱和度的调整,允许你基于像素的亮度值来增加或减少饱和度。例如,可以减少亮度最高区域的饱和度来防止色彩过饱和。

YRGB曲线 YRGB曲线是一种用于图像处理和颜色校正中的工具,它允许用户分别调整图像中的亮度(Y)和红色(R)、绿色(G)、蓝色(B)三个颜色通道。通过调整这些曲线,可以影响图像的整体色调、对比度和颜色平衡。

  • Y曲线:Y通常代表亮度(Luminance)或亮度信息,通过调整Y曲线,可以不影响色彩的情况下调整图像的亮暗程度。这是因为Y曲线单独控制亮度信息,不直接改变色彩。

  • RGB曲线:RGB分别代表红色、绿色和蓝色三个颜色通道。通过独立调整每个颜色通道的曲线,可以控制图像中特定颜色的饱和度和色调。例如,提高红色曲线可以使图像看起来更暖,而调低蓝色曲线则会使图像看起来更凉。

过程:

  1. 将曲线值映射到大小128*1的纹理中,曲线值离散到128个值存在纹理。
  2. 在Shader中计算Lut中的值。

代码:

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
37
38
39
40
41
42
43
44
// 色彩映射曲线
float satMult;
float3 hsv = RgbToHsv(colorLinear);
{
// Hue Vs Sat
satMult = EvaluateCurve(_CurveHueVsSat, hsv.x) * 2.0;

// Sat Vs Sat
satMult *= EvaluateCurve(_CurveSatVsSat, hsv.y) * 2.0;

// Lum Vs Sat
satMult *= EvaluateCurve(_CurveLumVsSat, Luminance(colorLinear)) * 2.0;

// Hue Vs Hue
float hue = hsv.x + _HueSatCon.x;
float offset = EvaluateCurve(_CurveHueVsHue, hue) - 0.5;
hue += offset;
hsv.x = RotateHue(hue, 0.0, 1.0);
}
colorLinear = HsvToRgb(hsv);

// 计算饱和度
luma = GetLuminance(colorLinear);
colorLinear = luma.xxx + (_HueSatCon.yyy * satMult) * (colorLinear - luma.xxx);

// YRGB curves
{
const float kHalfPixel = (1.0 / 128.0) / 2.0;
float3 c = colorLinear;

// Y (master)
c += kHalfPixel.xxx;
float mr = EvaluateCurve(_CurveMaster, c.r);
float mg = EvaluateCurve(_CurveMaster, c.g);
float mb = EvaluateCurve(_CurveMaster, c.b);
c = float3(mr, mg, mb);

// RGB
c += kHalfPixel.xxx;
float r = EvaluateCurve(_CurveRed, c.r);
float g = EvaluateCurve(_CurveGreen, c.g);
float b = EvaluateCurve(_CurveBlue, c.b);
colorLinear = float3(r, g, b);
}

Tonemapping(色调映射)

色调映射(Tonemapping)算法

Neutral Tonemapping和ACES Tonemapping是两种不同的色调映射(Tonemapping)算法,它们在处理图像时有一些区别:

Neutral Tonemapping(中性色调映射):

  • 中性色调映射是一种简单的色调映射算法,旨在保持图像的整体对比度和亮度,并尽可能地保留原始图像的色彩和细节。
  • 中性色调映射通常采用简单的灰度映射函数,将图像的亮度值进行线性或对数调整,以使得整个图像的亮度范围适应于显示设备的动态范围。
  • 中性色调映射不太复杂,易于实现,并且通常用于一般的图像处理任务中。

ACES Tonemapping(Academy Color Encoding System色调映射):

  • ACES是一种广泛使用的颜色管理系统,旨在实现在各种不同设备和平台上的一致色彩表现。ACES Tonemapping是基于这一系统的色调映射算法。
  • ACES Tonemapping考虑了更多的颜色科学原理和视觉感知模型,以更好地模拟人眼对真实世界场景的感知。
  • ACES Tonemapping通常包括对色彩、对比度和亮度的调整,以实现更加自然和逼真的图像呈现效果。
  • ACES Tonemapping算法更复杂,需要更多的计算和参数调整,但可以产生更高质量的图像处理结果,特别是对于视觉特效和电影制作等专业领域。

代码:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 片原函数
float4 FragLutBuilderHdr(Varyings input) : SV_Target
{
//Lut 空间 我们使用 Alexa LogC (El 1000) 来存储 LUT,因为它提供了足够好的范围 (~58.85666),并且足以存储在 fp16 中,而不会在暗部失去精度
float3 colorLutSpace = GetLutStripValue(input.texcoord, _Lut_Params);

// 上面那些后处理步骤,只是会在不同的空间中计算
float3 gradedColor = ColorGrade(colorLutSpace);


#ifdef HDR_COLORSPACE_CONVERSION
//处理输出到HDR显示器的转换
gradedColor = ProcessColorForHDR(gradedColor);
#else
gradedColor = Tonemap(gradedColor);
#endif

return float4(gradedColor, 1.0);
}

// 处理输入到HDR显示器时的转换
float3 ProcessColorForHDR(float3 colorLinear)
{
#ifdef HDR_COLORSPACE_CONVERSION
#ifdef _TONEMAP_ACES
float3 aces = ACEScg_to_ACES(colorLinear);
return HDRMappingACES(aces.rgb, PaperWhite, MinNits, MaxNits, RangeReductionMode, true);
#elif _TONEMAP_NEUTRAL
return HDRMappingFromRec2020(colorLinear.rgb, PaperWhite, MinNits, MaxNits, RangeReductionMode, HueShift, true);
#else
// 在 Rec2020 中完成分级,转换为预期的色彩空间和 [0, 10k] 尼特范围
return RotateRec2020ToOutputSpace(colorLinear) * PaperWhite;
#endif
#endif

return colorLinear;
}

// 色调映射
float3 Tonemap(float3 colorLinear)
{
#if _TONEMAP_NEUTRAL
{
colorLinear = NeutralTonemap(colorLinear);
}
#elif _TONEMAP_ACES
{
// Note: input is actually ACEScg (AP1 w/ linear encoding)
float3 aces = ACEScg_to_ACES(colorLinear);
colorLinear = AcesTonemap(aces);
}
#endif

return colorLinear;
}

视觉效果(VFX)

景深(Depth of Field,DOF)

景深后处理是一种用于模拟相机镜头产生的景深效果的图像处理技术。它可以在后期处理阶段模拟出景深的效果,使得图像中的某些部分变得模糊,而其他部分保持清晰,从而增强了图像的艺术感和视觉效果。

实现原理: 景深后处理的实现原理主要基于两个关键概念:景深和高斯模糊。

  1. 景深(Depth of Field,DOF):景深是指相机镜头焦点前后一定范围内的物体都能保持清晰的范围。景深受到相机参数(如焦距、光圈大小)和拍摄场景的影响。在景深后处理中,通过模拟景深范围内的物体变得模糊来实现景深效果。
  2. 高斯模糊:高斯模糊是一种常用的图像模糊技术,它通过对图像中每个像素的周围像素应用高斯函数来降低像素的清晰度。模糊半径决定了模糊的程度,通常用于模拟景深中物体的模糊效果。
  3. 合成:将模糊处理后的图像与原始图像进行混合,以获得最终的景深效果。通常使用混合模式(如线性混合或增加混合)来调整模糊图像与原始图像之间的比例。

URP提供了两种模糊算法:高斯模糊和散景模糊,代码如下:

C#部分

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
void DoDepthOfField(Camera camera, CommandBuffer cmd, RTHandle source, RTHandle destination, Rect pixelRect)
{
// 高斯模糊方案
if (m_DepthOfField.mode.value == DepthOfFieldMode.Gaussian)
DoGaussianDepthOfField(camera, cmd, source, destination, pixelRect);
// 散景模糊方案
else if (m_DepthOfField.mode.value == DepthOfFieldMode.Bokeh)
DoBokehDepthOfField(cmd, source, destination, pixelRect);
}

// 设置高斯模糊参数
void DoGaussianDepthOfField(Camera camera, CommandBuffer cmd, RTHandle source, RTHandle destination, Rect pixelRect)
{
int downSample = 2;
var material = m_Materials.gaussianDepthOfField;
int wh = m_Descriptor.width / downSample;
int hh = m_Descriptor.height / downSample;
float farStart = m_DepthOfField.gaussianStart.value;
float farEnd = Mathf.Max(farStart, m_DepthOfField.gaussianEnd.value);

// 假设半径 1 在 1080p 下为 1。超过一定半径后,我们的高斯核看起来会非常糟糕,因此在非常高的分辨率 (4K+)时,我们将其限制它
float maxRadius = m_DepthOfField.gaussianMaxRadius.value * (wh / 1080f);
maxRadius = Mathf.Min(maxRadius, 2f);

CoreUtils.SetKeyword(material, ShaderKeywordStrings.HighQualitySampling, m_DepthOfField.highQualitySampling.value);
material.SetVector(ShaderConstants._CoCParams, new Vector3(farStart, farEnd, maxRadius));

RenderingUtils.ReAllocateIfNeeded(ref m_FullCoCTexture, GetCompatibleDescriptor(m_Descriptor.width, m_Descriptor.height, m_GaussianCoCFormat), FilterMode.Bilinear, TextureWrapMode.Clamp, name: "_FullCoCTexture");
RenderingUtils.ReAllocateIfNeeded(ref m_HalfCoCTexture, GetCompatibleDescriptor(wh, hh, m_GaussianCoCFormat), FilterMode.Bilinear, TextureWrapMode.Clamp, name: "_HalfCoCTexture");
RenderingUtils.ReAllocateIfNeeded(ref m_PingTexture, GetCompatibleDescriptor(wh, hh, GraphicsFormat.R16G16B16A16_SFloat), FilterMode.Bilinear, TextureWrapMode.Clamp, name: "_PingTexture");
RenderingUtils.ReAllocateIfNeeded(ref m_PongTexture, GetCompatibleDescriptor(wh, hh, GraphicsFormat.R16G16B16A16_SFloat), FilterMode.Bilinear, TextureWrapMode.Clamp, name: "_PongTexture");

PostProcessUtils.SetSourceSize(cmd, m_Descriptor);
cmd.SetGlobalVector(ShaderConstants._DownSampleScaleFactor, new Vector4(1.0f / downSample, 1.0f / downSample, downSample, downSample));

//Compute CoC 计算焦外圈,CoC是指焦平面上物体的像散焦在焦平面上的像上所形成的圆形面积,通常被称为"焦外圆"。Compute CoC是计算这个圆的大小的过程,它是景深模拟算法中的重要部分。景深模拟是一种技术,通过模拟相机对焦的效果,以便在数字图像中产生与真实世界中相似的景深效果。Compute CoC用于确定在图像中哪些部分是焦外的,以便进行适当的模糊处理,从而模拟真实的景深效果。
Blitter.BlitCameraTexture(cmd, source, m_FullCoCTexture, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, material, 0);

// 降采样和预处理颜色和coc,
m_MRT2[0] = m_HalfCoCTexture.nameID;
m_MRT2[1] = m_PingTexture.nameID;

cmd.SetGlobalTexture(ShaderConstants._FullCoCTexture, m_FullCoCTexture.nameID);
CoreUtils.SetRenderTarget(cmd, m_MRT2, m_HalfCoCTexture);
Vector2 viewportScale = source.useScaling ? new Vector2(source.rtHandleProperties.rtHandleScale.x, source.rtHandleProperties.rtHandleScale.y) : Vector2.one;
Blitter.BlitTexture(cmd, source, viewportScale, material, 1);

// 模糊
cmd.SetGlobalTexture(ShaderConstants._HalfCoCTexture, m_HalfCoCTexture.nameID);
cmd.SetGlobalTexture(ShaderConstants._ColorTexture, source);
Blitter.BlitCameraTexture(cmd, m_PingTexture, m_PongTexture, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, material, 2);
Blitter.BlitCameraTexture(cmd, m_PongTexture, m_PingTexture, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, material, 3);

// 合并
cmd.SetGlobalTexture(ShaderConstants._ColorTexture, m_PingTexture.nameID);
cmd.SetGlobalTexture(ShaderConstants._FullCoCTexture, m_FullCoCTexture.nameID);
Blitter.BlitCameraTexture(cmd, source, destination, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, material, 4);
}

// 设置散景模糊参数
void DoBokehDepthOfField(CommandBuffer cmd, RTHandle source, RTHandle destination, Rect pixelRect)
{
int downSample = 2;
var material = m_Materials.bokehDepthOfField;
int wh = m_Descriptor.width / downSample;
int hh = m_Descriptor.height / downSample;

// 生成合成图像的镜头和光圈相机模型[Potmesil81]
//Potmesil(1981)的论文,提出了一种用于合成图像生成的镜头和光圈相机模型。该论文介绍了一种基于物理原理的相机模型,旨在模拟真实相机的成像过程,以生成高质量的合成图像。
//这个模型考虑了光线从场景中的对象经过透镜和光圈到达成像平面的过程。透镜模型通常用于描述光线的折射和聚焦效应,而光圈模型则用于控制进入相机的光线的量和分布。
//Potmesil 的相机模型还考虑了各种参数,例如透镜的焦距、光圈的直径以及相机与场景之间的距离等,以更准确地模拟真实相机的行为。这样的模型对于计算机图形学和合成图像生成非常有用,因为它们可以产生更逼真的图像,更好地模拟真实世界的光学效应。
float F = m_DepthOfField.focalLength.value / 1000f;
float A = m_DepthOfField.focalLength.value / m_DepthOfField.aperture.value;
float P = m_DepthOfField.focusDistance.value;
float maxCoC = (A * F) / (P - F);
float maxRadius = GetMaxBokehRadiusInPixels(m_Descriptor.height);
float rcpAspect = 1f / (wh / (float)hh);

CoreUtils.SetKeyword(material, ShaderKeywordStrings.UseFastSRGBLinearConversion, m_UseFastSRGBLinearConversion);
cmd.SetGlobalVector(ShaderConstants._CoCParams, new Vector4(P, maxCoC, maxRadius, rcpAspect));

// 准备散景内核参数
int hash = m_DepthOfField.GetHashCode();
if (hash != m_BokehHash || maxRadius != m_BokehMaxRadius || rcpAspect != m_BokehRCPAspect)
{
m_BokehHash = hash;
m_BokehMaxRadius = maxRadius;
m_BokehRCPAspect = rcpAspect;
PrepareBokehKernel(maxRadius, rcpAspect);
}

cmd.SetGlobalVectorArray(ShaderConstants._BokehKernel, m_BokehKernel);

RenderingUtils.ReAllocateIfNeeded(ref m_FullCoCTexture, GetCompatibleDescriptor(m_Descriptor.width, m_Descriptor.height, GraphicsFormat.R8_UNorm), FilterMode.Bilinear, TextureWrapMode.Clamp, name: "_FullCoCTexture");
RenderingUtils.ReAllocateIfNeeded(ref m_PingTexture, GetCompatibleDescriptor(wh, hh, GraphicsFormat.R16G16B16A16_SFloat), FilterMode.Bilinear, TextureWrapMode.Clamp, name: "_PingTexture");
RenderingUtils.ReAllocateIfNeeded(ref m_PongTexture, GetCompatibleDescriptor(wh, hh, GraphicsFormat.R16G16B16A16_SFloat), FilterMode.Bilinear, TextureWrapMode.Clamp, name: "_PongTexture");

PostProcessUtils.SetSourceSize(cmd, m_Descriptor);
cmd.SetGlobalVector(ShaderConstants._DownSampleScaleFactor, new Vector4(1.0f / downSample, 1.0f / downSample, downSample, downSample));
float uvMargin = (1.0f / m_Descriptor.height) * downSample;
cmd.SetGlobalVector(ShaderConstants._BokehConstants, new Vector4(uvMargin, uvMargin * 2.0f));

// Compute CoC 计算焦外圈
Blitter.BlitCameraTexture(cmd, source, m_FullCoCTexture, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, material, 0);
cmd.SetGlobalTexture(ShaderConstants._FullCoCTexture, m_FullCoCTexture.nameID);

//降采样和预处理颜色和coc
Blitter.BlitCameraTexture(cmd, source, m_PingTexture, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, material, 1);

// 散景模糊
Blitter.BlitCameraTexture(cmd, m_PingTexture, m_PongTexture, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, material, 2);

// 散景模糊后再使用9-tap tent滤波核进行模糊,“9-tap tent filter” 意味着您想要应用一个 9个采样点的滤波器,滤波器的形状类似于一个帐篷(tent)。然而我们使用4个双线性(bilinear)采样点线性插值的方式来计算这9个采样点的值。
Blitter.BlitCameraTexture(cmd, m_PongTexture, m_PingTexture, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, material, 3);

// 合并
cmd.SetGlobalTexture(ShaderConstants._DofTexture, m_PingTexture.nameID);
Blitter.BlitCameraTexture(cmd, source, destination, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, material, 4);
}

Shader部分

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
// 高斯模糊的景深效果
Shader "Hidden/Universal Render Pipeline/GaussianDepthOfField"
{
HLSLINCLUDE

#pragma target 3.5
#pragma exclude_renderers gles

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Filtering.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"
#include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"

TEXTURE2D_X(_ColorTexture);
TEXTURE2D_X(_FullCoCTexture);
TEXTURE2D_X(_HalfCoCTexture);

float4 _SourceSize;
float4 _DownSampleScaleFactor;

float3 _CoCParams;

#define FarStart _CoCParams.x
#define FarEnd _CoCParams.y
#define MaxRadius _CoCParams.z

#define BLUR_KERNEL 0

#if BLUR_KERNEL == 0

// 可分离双线性 3-tap 高斯滤波器,相当于5-tap滤波
const static int kTapCount = 3;
const static float kOffsets[] = {
-1.33333333,
0.00000000,
1.33333333
};
const static half kCoeffs[] = {
0.35294118,
0.29411765,
0.35294118
};

#elif BLUR_KERNEL == 1

// // 可分离双线性 5-tap 高斯滤波器,相当于9-tap滤波
const static int kTapCount = 5;
const static float kOffsets[] = {
-3.23076923,
-1.38461538,
0.00000000,
1.38461538,
3.23076923
};
const static half kCoeffs[] = {
0.07027027,
0.31621622,
0.22702703,
0.31621622,
0.07027027
};

#endif

// 计算Coc
half FragCoC(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);

float depth = LOAD_TEXTURE2D_X(_CameraDepthTexture, _SourceSize.xy * uv).x;
depth = LinearEyeDepth(depth, _ZBufferParams);
half coc = (depth - FarStart) / (FarEnd - FarStart);
return saturate(coc);
}

struct PrefilterOutput
{
half coc : SV_Target0;
half3 color : SV_Target1;
};

// 进行滤波处理
PrefilterOutput FragPrefilter(Varyings input)
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);

#if _HIGH_QUALITY_SAMPLING

//《High Quality Antialiasing》(Lorach07)提出了使用旋转网格来最小化水平和垂直边界带来的伪影。这种技术旨在改善图形渲染中的抗锯齿效果,特别是在处理水平和垂直边界时。
//这种方法的核心思想是,通过旋转采样网格,使得锯齿边缘的能量在整个图像上更加均匀地分布。通常情况下,水平和垂直边界会导致锯齿效应更加显著,因为它们的方向与像素阵列的方向相对应。通过旋转网格,可以打破这种规律,减少锯齿效应。
//具体实现时,可以在像素级别上应用旋转网格。这意味着在像素着色器中,对于每个像素,都可以使用不同的旋转网格来进行采样。这样,即使在处理水平和垂直边界时,也可以获得更加均匀的采样,从而减少锯齿效应。
//通过这种方法,可以提高图形渲染的质量,特别是对于需要处理锯齿效应的场景,例如渲染具有许多直线或边缘的场景,如建筑物或栅格图形。
const int kCount = 5;
const float2 kTaps[] = {
float2( 0.0, 0.0),
float2( 0.9, -0.4),
float2(-0.9, 0.4),
float2( 0.4, 0.9),
float2(-0.4, -0.9)
};

half3 colorAcc = 0.0;
half farCoCAcc = 0.0;

UNITY_UNROLL
for (int i = 0; i < kCount; i++)
{
float2 tapCoord = _SourceSize.zw * kTaps[i] + uv;
half3 tapColor = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, tapCoord).xyz;
half coc = SAMPLE_TEXTURE2D_X(_FullCoCTexture, sampler_LinearClamp, tapCoord).x;

// 预先乘以焦平面模糊(CoC)可以帮助减少背景模糊向聚焦区域的渗透。这个技术常用于渲染引擎中,以改善焦外区域的逼真度,同时保持焦点区域的清晰度。
colorAcc += tapColor * coc;
farCoCAcc += coc;
}

half3 color = colorAcc * rcp(kCount);
half farCoC = farCoCAcc * rcp(kCount);

#else

//理论上,对 CoC 进行双线性采样可能不太准确,因为 CoC 的大小通常不是线性变化的。然而,在某些情况下,为了提高速度和简化实现,可以选择使用双线性采样来近似 CoC。
//双线性采样是一种简单且快速的插值方法,适用于许多图形渲染情景。它通过对四个最近的像素进行加权平均来估计一个给定位置的值。虽然这种方法在处理 CoC 时可能会引入一些误差,但在实践中,它通常可以提供足够的准确性,特别是在需要快速渲染的情况下。
half farCoC = SAMPLE_TEXTURE2D_X(_FullCoCTexture, sampler_LinearClamp, uv).x;

//快速的双线性下采样源目标,并预先将 CoC 与颜色值相乘以减少背景模糊渗透到焦点区域
half3 color = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv).xyz;
color *= farCoC;

#endif

PrefilterOutput o;
o.coc = farCoC;
o.color = color;
return o;
}

half4 Blur(Varyings input, float2 dir, float premultiply)
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);

// Use the center CoC as radius
int2 positionSS = int2(_SourceSize.xy * _DownSampleScaleFactor.xy * uv);
half samp0CoC = LOAD_TEXTURE2D_X(_HalfCoCTexture, positionSS).x;

float2 offset = _SourceSize.zw * _DownSampleScaleFactor.zw * dir * samp0CoC * MaxRadius;
half4 acc = 0.0;

UNITY_UNROLL
for (int i = 0; i < kTapCount; i++)
{
float2 sampCoord = uv + kOffsets[i] * offset;
half sampCoC = SAMPLE_TEXTURE2D_X(_HalfCoCTexture, sampler_LinearClamp, sampCoord).x;
half3 sampColor = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, sampCoord).xyz;

// Weight & pre-multiply to limit bleeding on the focused area
half weight = saturate(1.0 - (samp0CoC - sampCoC));
acc += half4(sampColor, premultiply ? sampCoC : 1.0) * kCoeffs[i] * weight;
}

acc.xyz /= acc.w + 1e-4; // Zero-div guard
return half4(acc.xyz, 1.0);
}

half4 FragBlurH(Varyings input) : SV_Target
{
return Blur(input, float2(1.0, 0.0), 1.0);
}

half4 FragBlurV(Varyings input) : SV_Target
{
return Blur(input, float2(0.0, 1.0), 0.0);
}

half4 FragComposite(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);

half3 baseColor = LOAD_TEXTURE2D_X(_BlitTexture, _SourceSize.xy * uv).xyz;
half coc = LOAD_TEXTURE2D_X(_FullCoCTexture, _SourceSize.xy * uv).x;

#if _HIGH_QUALITY_SAMPLING && !defined(SHADER_API_GLES)
half3 farColor = SampleTexture2DBicubic(TEXTURE2D_X_ARGS(_ColorTexture, sampler_LinearClamp), uv, _SourceSize * _DownSampleScaleFactor, 1.0, unity_StereoEyeIndex).xyz;
#else
half3 farColor = SAMPLE_TEXTURE2D_X(_ColorTexture, sampler_LinearClamp, uv).xyz;
#endif

half3 dstColor = 0.0;
half dstAlpha = 1.0;

UNITY_BRANCH
if (coc > 0.0)
{
// Non-linear blend
// "CryEngine 3 Graphics Gems" [Sousa13]
half blend = sqrt(coc * TWO_PI);
dstColor = farColor * saturate(blend);
dstAlpha = saturate(1.0 - blend);
}

return half4(baseColor * dstAlpha + dstColor, 1.0);
}

ENDHLSL

SubShader
{
Tags { "RenderPipeline" = "UniversalPipeline" }
LOD 100
ZTest Always ZWrite Off Cull Off

Pass
{
Name "Gaussian Depth Of Field CoC"

HLSLPROGRAM
#pragma vertex Vert
#pragma fragment FragCoC
ENDHLSL
}

Pass
{
Name "Gaussian Depth Of Field Prefilter"

HLSLPROGRAM
#pragma vertex Vert
#pragma fragment FragPrefilter
#pragma multi_compile_local _ _HIGH_QUALITY_SAMPLING
ENDHLSL
}

Pass
{
Name "Gaussian Depth Of Field Blur Horizontal"

HLSLPROGRAM
#pragma vertex Vert
#pragma fragment FragBlurH
ENDHLSL
}

Pass
{
Name "Gaussian Depth Of Field Blur Vertical"

HLSLPROGRAM
#pragma vertex Vert
#pragma fragment FragBlurV
ENDHLSL
}

Pass
{
Name "Gaussian Depth Of Field Composite"

HLSLPROGRAM
#pragma vertex Vert
#pragma fragment FragComposite
#pragma multi_compile_local _ _HIGH_QUALITY_SAMPLING
ENDHLSL
}
}
}

// 散景模糊的景深效果
Shader "Hidden/Universal Render Pipeline/BokehDepthOfField"
{
HLSLINCLUDE
#pragma exclude_renderers gles
#pragma multi_compile_local_fragment _ _USE_FAST_SRGB_LINEAR_CONVERSION

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"
#include "Packages/com.unity.render-pipelines.universal/Shaders/PostProcessing/Common.hlsl"

// Do not change this without changing PostProcessPass.PrepareBokehKernel()
#define SAMPLE_COUNT 42

// Toggle this to reduce flickering - note that it will reduce overall bokeh energy and add
// a small cost to the pre-filtering pass
#define COC_LUMA_WEIGHTING 0

TEXTURE2D_X(_DofTexture);
TEXTURE2D_X(_FullCoCTexture);

half4 _SourceSize;
half4 _HalfSourceSize;
half4 _DownSampleScaleFactor;
half4 _CoCParams;
half4 _BokehKernel[SAMPLE_COUNT];
half4 _BokehConstants;

#define FocusDist _CoCParams.x
#define MaxCoC _CoCParams.y
#define MaxRadius _CoCParams.z
#define RcpAspect _CoCParams.w

half FragCoC(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);
float depth = LOAD_TEXTURE2D_X(_CameraDepthTexture, _SourceSize.xy * uv).x;
float linearEyeDepth = LinearEyeDepth(depth, _ZBufferParams);

half coc = (1.0 - FocusDist / linearEyeDepth) * MaxCoC;
half nearCoC = clamp(coc, -1.0, 0.0);
half farCoC = saturate(coc);

return saturate((farCoC + nearCoC + 1.0) * 0.5);
}

half4 FragPrefilter(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);

#if SHADER_TARGET >= 45 && defined(PLATFORM_SUPPORT_GATHER)

// Sample source colors
half4 cr = GATHER_RED_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv);
half4 cg = GATHER_GREEN_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv);
half4 cb = GATHER_BLUE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv);

half3 c0 = half3(cr.x, cg.x, cb.x);
half3 c1 = half3(cr.y, cg.y, cb.y);
half3 c2 = half3(cr.z, cg.z, cb.z);
half3 c3 = half3(cr.w, cg.w, cb.w);

// Sample CoCs
half4 cocs = GATHER_TEXTURE2D_X(_FullCoCTexture, sampler_LinearClamp, uv) * 2.0 - 1.0;
half coc0 = cocs.x;
half coc1 = cocs.y;
half coc2 = cocs.z;
half coc3 = cocs.w;

#else

float3 duv = _SourceSize.zwz * float3(0.5, 0.5, -0.5);
float2 uv0 = uv - duv.xy;
float2 uv1 = uv - duv.zy;
float2 uv2 = uv + duv.zy;
float2 uv3 = uv + duv.xy;

// Sample source colors
half3 c0 = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv0).xyz;
half3 c1 = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv1).xyz;
half3 c2 = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv2).xyz;
half3 c3 = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv3).xyz;

// Sample CoCs
half coc0 = SAMPLE_TEXTURE2D_X(_FullCoCTexture, sampler_LinearClamp, uv0).x * 2.0 - 1.0;
half coc1 = SAMPLE_TEXTURE2D_X(_FullCoCTexture, sampler_LinearClamp, uv1).x * 2.0 - 1.0;
half coc2 = SAMPLE_TEXTURE2D_X(_FullCoCTexture, sampler_LinearClamp, uv2).x * 2.0 - 1.0;
half coc3 = SAMPLE_TEXTURE2D_X(_FullCoCTexture, sampler_LinearClamp, uv3).x * 2.0 - 1.0;

#endif

#if COC_LUMA_WEIGHTING

// Apply CoC and luma weights to reduce bleeding and flickering
half w0 = abs(coc0) / (Max3(c0.x, c0.y, c0.z) + 1.0);
half w1 = abs(coc1) / (Max3(c1.x, c1.y, c1.z) + 1.0);
half w2 = abs(coc2) / (Max3(c2.x, c2.y, c2.z) + 1.0);
half w3 = abs(coc3) / (Max3(c3.x, c3.y, c3.z) + 1.0);

// Weighted average of the color samples
half3 avg = c0 * w0 + c1 * w1 + c2 * w2 + c3 * w3;
avg /= max(w0 + w1 + w2 + w3, 1e-5);

#else

half3 avg = (c0 + c1 + c2 + c3) / 4.0;

#endif

// Select the largest CoC value
half cocMin = min(coc0, Min3(coc1, coc2, coc3));
half cocMax = max(coc0, Max3(coc1, coc2, coc3));
half coc = (-cocMin > cocMax ? cocMin : cocMax) * MaxRadius;

// Premultiply CoC
avg *= smoothstep(0, _SourceSize.w * 2.0, abs(coc));

#if defined(UNITY_COLORSPACE_GAMMA)
avg = GetSRGBToLinear(avg);
#endif

return half4(avg, coc);
}

void Accumulate(half4 samp0, float2 uv, half4 disp, inout half4 farAcc, inout half4 nearAcc)
{
half4 samp = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + disp.wy);

// Compare CoC of the current sample and the center sample and select smaller one
half farCoC = max(min(samp0.a, samp.a), 0.0);

// Compare the CoC to the sample distance & add a small margin to smooth out
half farWeight = saturate((farCoC - disp.z + _BokehConstants.y) / _BokehConstants.y);
half nearWeight = saturate((-samp.a - disp.z + _BokehConstants.y) / _BokehConstants.y);

// Cut influence from focused areas because they're darkened by CoC premultiplying. This is only
// needed for near field
nearWeight *= step(_BokehConstants.x, -samp.a);

// Accumulation
farAcc += half4(samp.rgb, 1.0h) * farWeight;
nearAcc += half4(samp.rgb, 1.0h) * nearWeight;
}

half4 FragBlur(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);

half4 samp0 = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv);

half4 farAcc = 0.0; // Background: far field bokeh
half4 nearAcc = 0.0; // Foreground: near field bokeh

// Center sample isn't in the kernel array, accumulate it separately
Accumulate(samp0, uv, 0.0, farAcc, nearAcc);

UNITY_LOOP
for (int si = 0; si < SAMPLE_COUNT; si++)
{
Accumulate(samp0, uv, _BokehKernel[si], farAcc, nearAcc);
}

// Get the weighted average
farAcc.rgb /= farAcc.a + (farAcc.a == 0.0); // Zero-div guard
nearAcc.rgb /= nearAcc.a + (nearAcc.a == 0.0);

// Normalize the total of the weights for the near field
nearAcc.a *= PI / (SAMPLE_COUNT + 1);

// Alpha premultiplying
half alpha = saturate(nearAcc.a);
half3 rgb = lerp(farAcc.rgb, nearAcc.rgb, alpha);

return half4(rgb, alpha);
}

half4 FragPostBlur(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);

// 9-tap tent filter with 4 bilinear samples
float4 duv = _SourceSize.zwzw * _DownSampleScaleFactor.zwzw * float4(0.5, 0.5, -0.5, 0);
half4 acc;
acc = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv - duv.xy);
acc += SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv - duv.zy);
acc += SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + duv.zy);
acc += SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + duv.xy);
return acc * 0.25;
}

half4 FragComposite(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);

half4 dof = SAMPLE_TEXTURE2D_X(_DofTexture, sampler_LinearClamp, uv);
half coc = SAMPLE_TEXTURE2D_X(_FullCoCTexture, sampler_LinearClamp, uv).r;
coc = (coc - 0.5) * 2.0 * MaxRadius;

// Convert CoC to far field alpha value
float ffa = smoothstep(_SourceSize.w * 2.0, _SourceSize.w * 4.0, coc);

half4 color = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv);

#if defined(UNITY_COLORSPACE_GAMMA)
color = GetSRGBToLinear(color);
#endif

half alpha = Max3(dof.r, dof.g, dof.b);
color = lerp(color, half4(dof.rgb, alpha), ffa + dof.a - ffa * dof.a);

#if defined(UNITY_COLORSPACE_GAMMA)
color = GetLinearToSRGB(color);
#endif
return color;
}

ENDHLSL

SubShader
{
Tags { "RenderPipeline" = "UniversalPipeline" }
LOD 100
ZTest Always ZWrite Off Cull Off

Pass
{
Name "Bokeh Depth Of Field CoC"

HLSLPROGRAM
#pragma vertex Vert
#pragma fragment FragCoC
#pragma target 4.5
ENDHLSL
}

Pass
{
Name "Bokeh Depth Of Field Prefilter"

HLSLPROGRAM
#pragma vertex Vert
#pragma fragment FragPrefilter
#pragma target 4.5
ENDHLSL
}

Pass
{
Name "Bokeh Depth Of Field Blur"

HLSLPROGRAM
#pragma vertex Vert
#pragma fragment FragBlur
#pragma target 4.5
ENDHLSL
}

Pass
{
Name "Bokeh Depth Of Field Post Blur"

HLSLPROGRAM
#pragma vertex Vert
#pragma fragment FragPostBlur
#pragma target 4.5
ENDHLSL
}

Pass
{
Name "Bokeh Depth Of Field Composite"

HLSLPROGRAM
#pragma vertex Vert
#pragma fragment FragComposite
#pragma target 4.5
ENDHLSL
}
}
}

运动模糊(Motion Blur)

Motion Blur 的后处理实现原理是在图像已经捕获或生成后,在整个图像上应用模糊效果,以模拟相机或物体运动时的模糊效果。

以下是 Motion Blur 后处理的基本原理:

1. 获取速度信息: 在图像处理中,速度信息通常是由每个像素的运动向量表示的。这些向量可以通过两个连续帧之间的像素位移或动作估计算法(如光流法)来获取。

2. 计算模糊效果: 一旦获得了速度信息,就可以根据每个像素的运动向量来计算应用于该像素的模糊程度。通常情况下,速度越高的像素,应用的模糊效果就越强烈。

3. 模糊处理: 一种常见的模糊方法是使用卷积核。对于每个像素,可以根据其周围像素的权重来计算模糊效果。这可以通过高斯模糊、运动模糊或其他模糊核来实现。

4. 混合: 最后,将计算得到的模糊效果应用到原始图像上。通常采用像素值的混合或者合成技术来实现。混合的方式可以是简单的加权平均,也可以是更复杂的技术,如逐像素运动模糊等。

核心函数代码:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 计算每个像素的移动速度
half2 GetCameraVelocity(float4 uv)
{
#if UNITY_REVERSED_Z
half depth = SampleSceneDepth(uv.xy).x;
#else
half depth = lerp(UNITY_NEAR_CLIP_VALUE, 1, SampleSceneDepth(uv.xy).x);
#endif

float4 worldPos = float4(ComputeWorldSpacePosition(uv.xy, depth, UNITY_MATRIX_I_VP), 1.0);

float4 prevClipPos = mul(_PrevViewProjM, worldPos);
float4 curClipPos = mul(_ViewProjM, worldPos);

half2 prevPosCS = prevClipPos.xy / prevClipPos.w;
half2 curPosCS = curClipPos.xy / curClipPos.w;

// Backwards motion vectors
half2 velocity = (prevPosCS - curPosCS);
#if UNITY_UV_STARTS_AT_TOP
velocity.y = -velocity.y;
#endif
return ClampVelocity(velocity, _Clamp);
}

half3 GatherSample(half sampleNumber, half2 velocity, half invSampleCount, float2 centerUV, half randomVal, half velocitySign)
{
half offsetLength = (sampleNumber + 0.5h) + (velocitySign * (randomVal - 0.5h));
float2 sampleUV = centerUV + (offsetLength * invSampleCount) * velocity * velocitySign;
return SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_PointClamp, sampleUV).xyz;
}

half4 DoMotionBlur(VaryingsCMB input, int iterations)
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord.xy);
half2 velocity = GetCameraVelocity(float4(uv, input.texcoord.zw)) * _Intensity;
half randomVal = InterleavedGradientNoise(uv * _SourceSize.xy, 0);
half invSampleCount = rcp(iterations * 2.0);

half3 color = 0.0;

UNITY_UNROLL
for (int i = 0; i < iterations; i++)
{
color += GatherSample(i, velocity, invSampleCount, uv, randomVal, -1.0);
color += GatherSample(i, velocity, invSampleCount, uv, randomVal, 1.0);
}

return half4(color * invSampleCount, 1.0);
}

Panini投影(Panini Projection)

Panini Projection 是一种透视投影技术,它可以在保持图像的透视感的同时,扭曲图像以适应更宽的视角,从而产生一种类似鱼眼镜头的效果。实现 Panini Projection 的后处理通常涉及以下步骤:

1. 确定投影参数: 首先,需要确定用于 Panini 投影的参数,包括视角(FOV)、压缩因子等。这些参数将影响最终投影效果的弯曲程度和透视感。

2. 图像扭曲: 将原始图像应用于 Panini 投影的算法,对图像进行扭曲。这通常涉及到对图像中的每个像素进行重新定位,以适应所选的投影参数。在这一步中,图像中的像素位置会根据所选的参数进行重新映射,以产生弯曲的效果。

3. 插值和填充: 由于进行投影扭曲可能会导致某些像素位置在新图像中没有对应的值,因此需要进行插值和填充处理。通常采用的插值方法包括双线性插值、双三次插值等,以确保图像的平滑过渡和连续性。

4. 边缘处理: 在扭曲后的图像边缘可能会出现拉伸或压缩的情况,因此需要进行边缘处理以消除这种失真。常见的方法包括使用遮罩或者边界像素的加权平均来平滑边缘过渡。

总的来说,Panini Projection 的后处理实现原理涉及将原始图像应用于 Panini 投影算法,对图像进行扭曲和重新映射,然后进行插值、填充和边缘处理,最终得到扭曲的图像以产生所需的透视效果。

核心函数代码:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
float2 Panini_UnitDistance(float2 view_pos)
{
// Given
// S----------- E--X-------
// | ` . /,´
// |-- --- Q
// 1 | ,´/ `
// | ,´ / ´
// | ,´ / `
// | ,´ / .
// O` / .
// | / `
// | / ´
// 1 | / ´
// | / ´
// |/_ . ´
// P
//
// Have E
// Want to find X
//
// First apply tangent-secant theorem to find Q
// PE*QE = SE*SE
// QE = PE-PQ
// PQ = PE-(SE*SE)/PE
// Q = E*(PQ/PE)
// Then project Q to find X

const float d = 1.0;
const float view_dist = 2.0;
const float view_dist_sq = 4.0;

float view_hyp = sqrt(view_pos.x * view_pos.x + view_dist_sq);

float cyl_hyp = view_hyp - (view_pos.x * view_pos.x) / view_hyp;
float cyl_hyp_frac = cyl_hyp / view_hyp;
float cyl_dist = view_dist * cyl_hyp_frac;

float2 cyl_pos = view_pos * cyl_hyp_frac;
return cyl_pos / (cyl_dist - d);
}

float2 Panini_Generic(float2 view_pos, float d)
{
// Given
// S----------- E--X-------
// | ` ~. /,´
// |-- --- Q
// | ,/ `
// 1 | ,´/ `
// | ,´ / ´
// | ,´ / ´
// |,` / ,
// O /
// | / ,
// d | /
// | / ,
// |/ .
// P
// | ´
// | , ´
// +- ´
//
// Have E
// Want to find X
//
// First compute line-circle intersection to find Q
// Then project Q to find X

float view_dist = 1.0 + d;
float view_hyp_sq = view_pos.x * view_pos.x + view_dist * view_dist;

float isect_D = view_pos.x * d;
float isect_discrim = view_hyp_sq - isect_D * isect_D;

float cyl_dist_minus_d = (-isect_D * view_pos.x + view_dist * sqrt(isect_discrim)) / view_hyp_sq;
float cyl_dist = cyl_dist_minus_d + d;

float2 cyl_pos = view_pos * (cyl_dist / view_dist);
return cyl_pos / (cyl_dist - d);
}

half4 FragPaniniProjection(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

float2 view_pos = (2.0 * input.texcoord - 1.0) * _Params.xy * _Params.w;
#if _GENERIC
float2 proj_pos = Panini_Generic(view_pos, _Params.z);
#else // _UNIT_DISTANCE
float2 proj_pos = Panini_UnitDistance(view_pos);
#endif

float2 proj_ndc = proj_pos / _Params.xy;
float2 coords = proj_ndc * 0.5 + 0.5;

return SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, coords);
}

泛光(Bloom)

泛光(Bloom)后处理是一种常用的图像处理技术,用于增强图像中亮度较高区域的效果,使其产生发光的感觉。其原理如下:

1. 提取高亮区域: 首先,对原始图像进行处理,提取出高亮区域。这些高亮区域通常是亮度值较高的像素,可以通过阈值处理或者高通滤波器等方法来提取。

2. 生成泛光图: 将提取出的高亮区域进行模糊处理,生成泛光图。这一步可以使用高斯模糊、径向模糊等模糊算法,使高亮区域周围产生较大的光晕效果。

3. 叠加到原始图像: 将生成的泛光图与原始图像进行叠加。通常情况下,叠加时会根据泛光图中的亮度值进行加权叠加,使高亮区域的发光效果更加明显。

C#代码:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
void SetupBloom(CommandBuffer cmd, RTHandle source, Material uberMaterial)
{
// 降分辨率
int downres = 1;
switch (m_Bloom.downscale.value)
{
case BloomDownscaleMode.Half:
downres = 1;
break;
case BloomDownscaleMode.Quarter:
downres = 2;
break;
default:
throw new System.ArgumentOutOfRangeException();
}
int tw = m_Descriptor.width >> downres;
int th = m_Descriptor.height >> downres;

// 确定迭代次数
int maxSize = Mathf.Max(tw, th);
int iterations = Mathf.FloorToInt(Mathf.Log(maxSize, 2f) - 1);
int mipCount = Mathf.Clamp(iterations, 1, m_Bloom.maxIterations.value);

// 过滤器参数
float clamp = m_Bloom.clamp.value;
float threshold = Mathf.GammaToLinearSpace(m_Bloom.threshold.value);
float thresholdKnee = threshold * 0.5f; // Hardcoded soft knee

// 设置材质参数
float scatter = Mathf.Lerp(0.05f, 0.95f, m_Bloom.scatter.value);
var bloomMaterial = m_Materials.bloom;
bloomMaterial.SetVector(ShaderConstants._Params, new Vector4(scatter, clamp, threshold, thresholdKnee));
CoreUtils.SetKeyword(bloomMaterial, ShaderKeywordStrings.BloomHQ, m_Bloom.highQualityFiltering.value);
CoreUtils.SetKeyword(bloomMaterial, ShaderKeywordStrings.UseRGBM, m_UseRGBM);

// 创建RT
var desc = GetCompatibleDescriptor(tw, th, m_DefaultHDRFormat);
for (int i = 0; i < mipCount; i++)
{
RenderingUtils.ReAllocateIfNeeded(ref m_BloomMipUp[i], desc, FilterMode.Bilinear, TextureWrapMode.Clamp, name: m_BloomMipUp[i].name);
RenderingUtils.ReAllocateIfNeeded(ref m_BloomMipDown[i], desc, FilterMode.Bilinear, TextureWrapMode.Clamp, name: m_BloomMipDown[i].name);
desc.width = Mathf.Max(1, desc.width >> 1);
desc.height = Mathf.Max(1, desc.height >> 1);
}

// 将原图Blit到m_BloomMipDown[0]
Blitter.BlitCameraTexture(cmd, source, m_BloomMipDown[0], RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, bloomMaterial, 0);

//Gaussian Pyramid是一种图像金字塔构建方法,通常用于图像处理和计算机视觉中的多尺度分析。它的基本思想是通过逐级下采样来生成一系列分辨率逐渐降低的图像。具体而言,它的操作步骤如下:
//高斯模糊(Gaussian Blur):首先,对原始图像进行高斯模糊操作,这有助于去除图像中的高频细节,使图像更加平滑。
//下采样(Downsampling):然后,对模糊后的图像进行下采样操作,即将图像尺寸减小为原始图像的一半(或其他比例),这样就得到了一个分辨率降低的图像。
//重复操作:这两个步骤可以迭代多次,每次都在前一级图像上进行高斯模糊和下采样操作,生成更低分辨率的图像。
var lastDown = m_BloomMipDown[0];
for (int i = 1; i < mipCount; i++)
{
Blitter.BlitCameraTexture(cmd, lastDown, m_BloomMipUp[i], RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, bloomMaterial, 1);
Blitter.BlitCameraTexture(cmd, m_BloomMipUp[i], m_BloomMipDown[i], RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, bloomMaterial, 2);

lastDown = m_BloomMipDown[i];
}

//上采样(默认情况下为双线性,HQ 过滤采用双三次)
for (int i = mipCount - 2; i >= 0; i--)
{
var lowMip = (i == mipCount - 2) ? m_BloomMipDown[i + 1] : m_BloomMipUp[i + 1];
var highMip = m_BloomMipDown[i];
var dst = m_BloomMipUp[i];

cmd.SetGlobalTexture(ShaderConstants._SourceTexLowMip, lowMip);
Blitter.BlitCameraTexture(cmd, highMip, dst, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, bloomMaterial, 3);
}

//在 uber 上设置 Bloom
var tint = m_Bloom.tint.value.linear;
var luma = ColorUtils.Luminance(tint);
tint = luma > 0f ? tint * (1f / luma) : Color.white;

var bloomParams = new Vector4(m_Bloom.intensity.value, tint.r, tint.g, tint.b);
uberMaterial.SetVector(ShaderConstants._Bloom_Params, bloomParams);
uberMaterial.SetFloat(ShaderConstants._Bloom_RGBM, m_UseRGBM ? 1f : 0f);

cmd.SetGlobalTexture(ShaderConstants._Bloom_Texture, m_BloomMipUp[0]);

// 在uber上设置镜头污垢参数
// 保持纵横比正确并将污垢纹理居中,不被拉伸或挤压
var dirtTexture = m_Bloom.dirtTexture.value == null ? Texture2D.blackTexture : m_Bloom.dirtTexture.value;
float dirtRatio = dirtTexture.width / (float)dirtTexture.height;
float screenRatio = m_Descriptor.width / (float)m_Descriptor.height;
var dirtScaleOffset = new Vector4(1f, 1f, 0f, 0f);
float dirtIntensity = m_Bloom.dirtIntensity.value;

if (dirtRatio > screenRatio)
{
dirtScaleOffset.x = screenRatio / dirtRatio;
dirtScaleOffset.z = (1f - dirtScaleOffset.x) * 0.5f;
}
else if (screenRatio > dirtRatio)
{
dirtScaleOffset.y = dirtRatio / screenRatio;
dirtScaleOffset.w = (1f - dirtScaleOffset.y) * 0.5f;
}

uberMaterial.SetVector(ShaderConstants._LensDirt_Params, dirtScaleOffset);
uberMaterial.SetFloat(ShaderConstants._LensDirt_Intensity, dirtIntensity);
uberMaterial.SetTexture(ShaderConstants._LensDirt_Texture, dirtTexture);

if (m_Bloom.highQualityFiltering.value)
uberMaterial.EnableKeyword(dirtIntensity > 0f ? ShaderKeywordStrings.BloomHQDirt : ShaderKeywordStrings.BloomHQ);
else
uberMaterial.EnableKeyword(dirtIntensity > 0f ? ShaderKeywordStrings.BloomLQDirt : ShaderKeywordStrings.BloomLQ);
}

Shader代码:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
Shader "Hidden/Universal Render Pipeline/Bloom"
{
HLSLINCLUDE
#pragma exclude_renderers gles
#pragma multi_compile_local _ _USE_RGBM

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Filtering.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/FoveatedRendering.hlsl"

float4 _BlitTexture_TexelSize;

TEXTURE2D_X(_SourceTexLowMip);
float4 _SourceTexLowMip_TexelSize;

float4 _Params; // x: scatter, y: clamp, z: threshold (linear), w: threshold knee

#define Scatter _Params.x
#define ClampMax _Params.y
#define Threshold _Params.z
#define ThresholdKnee _Params.w

half4 EncodeHDR(half3 color)
{
#if _USE_RGBM
half4 outColor = EncodeRGBM(color);
#else
half4 outColor = half4(color, 1.0);
#endif

#if UNITY_COLORSPACE_GAMMA
return half4(sqrt(outColor.xyz), outColor.w); // linear to γ
#else
return outColor;
#endif
}

half3 DecodeHDR(half4 color)
{
#if UNITY_COLORSPACE_GAMMA
color.xyz *= color.xyz; // γ to linear
#endif

#if _USE_RGBM
return DecodeRGBM(color);
#else
return color.xyz;
#endif
}

half4 FragPrefilter(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);

#if defined(_FOVEATED_RENDERING_NON_UNIFORM_RASTER)
uv = RemapFoveatedRenderingResolve(uv);
#endif

#if _BLOOM_HQ
float texelSize = _BlitTexture_TexelSize.x;
half4 A = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + texelSize * float2(-1.0, -1.0));
half4 B = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + texelSize * float2(0.0, -1.0));
half4 C = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + texelSize * float2(1.0, -1.0));
half4 D = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + texelSize * float2(-0.5, -0.5));
half4 E = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + texelSize * float2(0.5, -0.5));
half4 F = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + texelSize * float2(-1.0, 0.0));
half4 G = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv);
half4 H = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + texelSize * float2(1.0, 0.0));
half4 I = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + texelSize * float2(-0.5, 0.5));
half4 J = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + texelSize * float2(0.5, 0.5));
half4 K = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + texelSize * float2(-1.0, 1.0));
half4 L = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + texelSize * float2(0.0, 1.0));
half4 M = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + texelSize * float2(1.0, 1.0));

half2 div = (1.0 / 4.0) * half2(0.5, 0.125);

half4 o = (D + E + I + J) * div.x;
o += (A + B + G + F) * div.y;
o += (B + C + H + G) * div.y;
o += (F + G + L + K) * div.y;
o += (G + H + M + L) * div.y;

half3 color = o.xyz;
#else
half3 color = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv).xyz;
#endif

color = min(ClampMax, color);

// 阈值化
half brightness = Max3(color.r, color.g, color.b);
half softness = clamp(brightness - Threshold + ThresholdKnee, 0.0, 2.0 * ThresholdKnee);
softness = (softness * softness) / (4.0 * ThresholdKnee + 1e-4);
half multiplier = max(brightness - Threshold, softness) / max(brightness, 1e-4);
color *= multiplier;

color = max(color, 0);
return EncodeHDR(color);
}

half4 FragBlurH(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
float texelSize = _BlitTexture_TexelSize.x * 2.0;
float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);

// 9-tap高斯模糊
half3 c0 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv - float2(texelSize * 4.0, 0.0)));
half3 c1 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv - float2(texelSize * 3.0, 0.0)));
half3 c2 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv - float2(texelSize * 2.0, 0.0)));
half3 c3 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv - float2(texelSize * 1.0, 0.0)));
half3 c4 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv ));
half3 c5 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + float2(texelSize * 1.0, 0.0)));
half3 c6 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + float2(texelSize * 2.0, 0.0)));
half3 c7 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + float2(texelSize * 3.0, 0.0)));
half3 c8 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + float2(texelSize * 4.0, 0.0)));

half3 color = c0 * 0.01621622 + c1 * 0.05405405 + c2 * 0.12162162 + c3 * 0.19459459
+ c4 * 0.22702703
+ c5 * 0.19459459 + c6 * 0.12162162 + c7 * 0.05405405 + c8 * 0.01621622;

return EncodeHDR(color);
}

half4 FragBlurV(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
float texelSize = _BlitTexture_TexelSize.y;
float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);

// 双线性插值方式用5个采样点模拟9-tap高斯核
half3 c0 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv - float2(0.0, texelSize * 3.23076923)));
half3 c1 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv - float2(0.0, texelSize * 1.38461538)));
half3 c2 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv ));
half3 c3 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + float2(0.0, texelSize * 1.38461538)));
half3 c4 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + float2(0.0, texelSize * 3.23076923)));

half3 color = c0 * 0.07027027 + c1 * 0.31621622
+ c2 * 0.22702703
+ c3 * 0.31621622 + c4 * 0.07027027;

return EncodeHDR(color);
}

half3 Upsample(float2 uv)
{
half3 highMip = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv));

#if _BLOOM_HQ && !defined(SHADER_API_GLES)
half3 lowMip = DecodeHDR(SampleTexture2DBicubic(TEXTURE2D_X_ARGS(_SourceTexLowMip, sampler_LinearClamp), uv, _SourceTexLowMip_TexelSize.zwxy, (1.0).xx, unity_StereoEyeIndex));
#else
half3 lowMip = DecodeHDR(SAMPLE_TEXTURE2D_X(_SourceTexLowMip, sampler_LinearClamp, uv));
#endif

return lerp(highMip, lowMip, Scatter);
}

half4 FragUpsample(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
half3 color = Upsample(UnityStereoTransformScreenSpaceTex(input.texcoord));
return EncodeHDR(color);
}

ENDHLSL

SubShader
{
Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline"}
LOD 100
ZTest Always ZWrite Off Cull Off

Pass
{
Name "Bloom Prefilter"

HLSLPROGRAM
#pragma vertex Vert
#pragma fragment FragPrefilter
#pragma multi_compile_local _ _BLOOM_HQ
#pragma multi_compile_fragment _ _FOVEATED_RENDERING_NON_UNIFORM_RASTER
// Foveated rendering currently not supported in dxc on metal
#pragma never_use_dxc metal
ENDHLSL
}

Pass
{
Name "Bloom Blur Horizontal"

HLSLPROGRAM
#pragma vertex Vert
#pragma fragment FragBlurH
ENDHLSL
}

Pass
{
Name "Bloom Blur Vertical"

HLSLPROGRAM
#pragma vertex Vert
#pragma fragment FragBlurV
ENDHLSL
}

Pass
{
Name "Bloom Upsample"

HLSLPROGRAM
#pragma vertex Vert
#pragma fragment FragUpsample
#pragma multi_compile_local _ _BLOOM_HQ
ENDHLSL
}
}
}

晕影效果(Vignette)

晕影效果(Vignette)是一种在图像边缘逐渐减弱亮度或增加饱和度的效果,通常用于突出图像中心或增加视觉焦点。实现晕影效果的后处理方法通常基于图像处理技术,其实现原理可以概括如下:

  1. 基于距离的衰减: 一种常见的实现方法是根据像素与图像中心的距离来计算衰减系数,距离中心越远的像素,衰减系数越大。这样,在后处理过程中,可以根据像素与中心的距离来对像素的亮度或饱和度进行加权,使得边缘逐渐变暗或变饱和。

  2. 径向函数: 晕影效果的实现也可以利用径向函数来描述衰减的形式,例如高斯函数或多项式函数。通过选择合适的径向函数参数,可以实现不同形式的晕影效果,例如较平滑的渐变或较陡峭的渐变。

  3. 遮罩技术: 另一种实现晕影效果的方法是使用遮罩技术,即在图像上叠加一个透明的黑色或彩色遮罩,使得边缘透明度逐渐增加。通过调整遮罩的形状和透明度,可以实现不同形式的晕影效果。

C#代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 设置晕影参数
void SetupVignette(Material material, XRPass xrPass)
{
var color = m_Vignette.color.value;
var center = m_Vignette.center.value;
var aspectRatio = m_Descriptor.width / (float)m_Descriptor.height;

var v1 = new Vector4(
color.r, color.g, color.b,
m_Vignette.rounded.value ? aspectRatio : 1f
);
var v2 = new Vector4(
center.x, center.y,
m_Vignette.intensity.value * 3f,
m_Vignette.smoothness.value * 5f
);

material.SetVector(ShaderConstants._Vignette_Params1, v1);
material.SetVector(ShaderConstants._Vignette_Params2, v2);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
half3 ApplyVignette(half3 input, float2 uv, float2 center, float intensity, float roundness, float smoothness, half3 color)
{
center = UnityStereoTransformScreenSpaceTex(center);
float2 dist = abs(uv - center) * intensity;

#if defined(UNITY_SINGLE_PASS_STEREO)
dist.x /= unity_StereoScaleOffset[unity_StereoEyeIndex].x;
#endif

dist.x *= roundness;
float vfactor = pow(saturate(1.0 - dot(dist, dist)), smoothness);
return input * lerp(color, (1.0).xxx, vfactor);
}

胶片颗粒(Film Grain)

胶片颗粒效果(Film Grain)是一种模拟传统胶片摄影中出现的颗粒状噪点的效果,可以增加图像的质感和艺术感。实现胶片颗粒效果的后处理方法通常基于图像处理技术,其实现原理可以概括如下:

  1. 随机噪声生成: 胶片颗粒效果的实现通常涉及生成随机的颗粒噪声。可以使用伪随机数生成器来生成服从特定分布(如高斯分布)的随机数序列,然后将这些随机数映射到图像像素上,以模拟胶片颗粒的分布。

  2. 混合叠加: 生成的颗粒噪声可以与原始图像进行混合叠加。可以通过调整混合的透明度或混合模式(如叠加、乘法等)来控制颗粒效果的强度和影响范围。

  3. 空间滤波: 在一些情况下,可以使用空间滤波技术来模拟胶片颗粒的空间分布特征。例如,可以使用卷积滤波器(如高斯滤波器)来对图像进行模糊处理,然后通过减去模糊图像和原始图像之间的差异来生成颗粒噪声。

  4. 颗粒参数调整: 胶片颗粒效果的外观可以通过调整参数来控制,例如颗粒的大小、密度、形状和颜色等。通过调整这些参数,可以实现不同类型和风格的胶片颗粒效果。

C#代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void ConfigureFilmGrain(PostProcessData data, FilmGrain settings, int cameraPixelWidth, int cameraPixelHeight, Material material)
{
var texture = settings.texture.value;

if (settings.type.value != FilmGrainLookup.Custom)
texture = data.textures.filmGrainTex[(int)settings.type.value];

#if LWRP_DEBUG_STATIC_POSTFX
float offsetX = 0f;
float offsetY = 0f;
#else
Random.InitState(Time.frameCount);
float offsetX = Random.value;
float offsetY = Random.value;
#endif

var tilingParams = texture == null
? Vector4.zero
: new Vector4(cameraPixelWidth / (float)texture.width, cameraPixelHeight / (float)texture.height, offsetX, offsetY);

material.SetTexture(ShaderConstants._Grain_Texture, texture);
material.SetVector(ShaderConstants._Grain_Params, new Vector2(settings.intensity.value * 4f, settings.response.value));
material.SetVector(ShaderConstants._Grain_TilingParams, tilingParams);
}

Shader代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
half3 ApplyGrain(half3 input, float2 uv, TEXTURE2D_PARAM(GrainTexture, GrainSampler), float intensity, float response, float2 scale, float2 offset, float oneOverPaperWhite)
{
// 颗粒范围为 [0;1],中性值为 0.5
half grain = SAMPLE_TEXTURE2D(GrainTexture, GrainSampler, uv * scale + offset).w;

// 重映射范围 [-1;1]
grain = (grain - 0.5) * 2.0;

// 基于场景亮度的噪声响应曲线
float lum = Luminance(input);
#ifdef HDR_INPUT
lum *= oneOverPaperWhite;
#endif
lum = 1.0 - sqrt(lum);
lum = lerp(1.0, lum, response);

return input + input * grain * intensity * lum;
}

Uber

"Uber"一词通常用来形容一个综合性的或"全包"(all-in-one)的着色器或后处理效果,它集成了多种视觉效果和图形处理技术。 这种"Uber后处理效果"可能包括,但不限于,色彩校正、HDR(高动态范围)渲染、Bloom、晕影效果(Vignette)、光晕(Lens Flares)、胶片颗粒(Film Grain)等多个组件。

Shader代码

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
Shader "Hidden/Universal Render Pipeline/UberPost"
{
HLSLINCLUDE
#pragma exclude_renderers gles
#pragma multi_compile_local_fragment _ _DISTORTION
#pragma multi_compile_local_fragment _ _CHROMATIC_ABERRATION
#pragma multi_compile_local_fragment _ _BLOOM_LQ _BLOOM_HQ _BLOOM_LQ_DIRT _BLOOM_HQ_DIRT
#pragma multi_compile_local_fragment _ _HDR_GRADING _TONEMAP_ACES _TONEMAP_NEUTRAL
#pragma multi_compile_local_fragment _ _FILM_GRAIN
#pragma multi_compile_local_fragment _ _DITHERING
#pragma multi_compile_local_fragment _ _GAMMA_20 _LINEAR_TO_SRGB_CONVERSION
#pragma multi_compile_local_fragment _ _USE_FAST_SRGB_LINEAR_CONVERSION
#pragma multi_compile_fragment _ _FOVEATED_RENDERING_NON_UNIFORM_RASTER
// Foveated rendering currently not supported in dxc on metal
#pragma never_use_dxc metal
#pragma multi_compile_fragment _ DEBUG_DISPLAY
#pragma multi_compile_fragment _ SCREEN_COORD_OVERRIDE
#pragma multi_compile_local_fragment _ HDR_INPUT HDR_ENCODING

#ifdef HDR_ENCODING
#define HDR_INPUT 1 // this should be defined when HDR_ENCODING is defined
#endif

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Filtering.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/ScreenCoordOverride.hlsl"
#if defined(HDR_ENCODING)
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/HDROutput.hlsl"
#endif

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/Shaders/PostProcessing/Common.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Debug/DebuggingFullscreen.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/FoveatedRendering.hlsl"

// Hardcoded dependencies to reduce the number of variants
#if _BLOOM_LQ || _BLOOM_HQ || _BLOOM_LQ_DIRT || _BLOOM_HQ_DIRT
#define BLOOM
#if _BLOOM_LQ_DIRT || _BLOOM_HQ_DIRT
#define BLOOM_DIRT
#endif
#endif

TEXTURE2D_X(_Bloom_Texture);
TEXTURE2D(_LensDirt_Texture);
TEXTURE2D(_Grain_Texture);
TEXTURE2D(_InternalLut);
TEXTURE2D(_UserLut);
TEXTURE2D(_BlueNoise_Texture);
TEXTURE2D_X(_OverlayUITexture);

float4 _Lut_Params;
float4 _UserLut_Params;
float4 _Bloom_Params;
float _Bloom_RGBM;
float4 _LensDirt_Params;
float _LensDirt_Intensity;
float4 _Distortion_Params1;
float4 _Distortion_Params2;
float _Chroma_Params;
half4 _Vignette_Params1;
float4 _Vignette_Params2;
#ifdef USING_STEREO_MATRICES
float4 _Vignette_ParamsXR;
#endif
float2 _Grain_Params;
float4 _Grain_TilingParams;
float4 _Bloom_Texture_TexelSize;
float4 _Dithering_Params;
float4 _HDROutputLuminanceParams;

#define DistCenter _Distortion_Params1.xy
#define DistAxis _Distortion_Params1.zw
#define DistTheta _Distortion_Params2.x
#define DistSigma _Distortion_Params2.y
#define DistScale _Distortion_Params2.z
#define DistIntensity _Distortion_Params2.w

#define ChromaAmount _Chroma_Params.x

#define BloomIntensity _Bloom_Params.x
#define BloomTint _Bloom_Params.yzw
#define BloomRGBM _Bloom_RGBM.x
#define LensDirtScale _LensDirt_Params.xy
#define LensDirtOffset _LensDirt_Params.zw
#define LensDirtIntensity _LensDirt_Intensity.x

#define VignetteColor _Vignette_Params1.xyz
#ifdef USING_STEREO_MATRICES
#define VignetteCenterEye0 _Vignette_ParamsXR.xy
#define VignetteCenterEye1 _Vignette_ParamsXR.zw
#else
#define VignetteCenter _Vignette_Params2.xy
#endif
#define VignetteIntensity _Vignette_Params2.z
#define VignetteSmoothness _Vignette_Params2.w
#define VignetteRoundness _Vignette_Params1.w

#define LutParams _Lut_Params.xyz
#define PostExposure _Lut_Params.w
#define UserLutParams _UserLut_Params.xyz
#define UserLutContribution _UserLut_Params.w

#define GrainIntensity _Grain_Params.x
#define GrainResponse _Grain_Params.y
#define GrainScale _Grain_TilingParams.xy
#define GrainOffset _Grain_TilingParams.zw

#define DitheringScale _Dithering_Params.xy
#define DitheringOffset _Dithering_Params.zw

#define MinNits _HDROutputLuminanceParams.x
#define MaxNits _HDROutputLuminanceParams.y
#define PaperWhite _HDROutputLuminanceParams.z
#define OneOverPaperWhite _HDROutputLuminanceParams.w

// UV扭曲,部分后处理需要扭曲UV
float2 DistortUV(float2 uv)
{
#if _DISTORTION
{
uv = (uv - 0.5) * DistScale + 0.5;
float2 ruv = DistAxis * (uv - 0.5 - DistCenter);
float ru = length(float2(ruv));

UNITY_BRANCH
if (DistIntensity > 0.0)
{
float wu = ru * DistTheta;
ru = tan(wu) * (rcp(ru * DistSigma));
uv = uv + ruv * (ru - 1.0);
}
else
{
ru = rcp(ru) * DistTheta * atan(ru * DistSigma);
uv = uv + ruv * (ru - 1.0);
}
}
#endif

return uv;
}

half4 FragUberPost(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

float2 uv = SCREEN_COORD_APPLY_SCALEBIAS(UnityStereoTransformScreenSpaceTex(input.texcoord));
float2 uvDistorted = DistortUV(uv);

half3 color = (0.0).xxx;

//色差(Chromatic Aberration),也称为色彩像差,是一种由于镜头无法将不同颜色的光线聚焦在同一点上而产生的视觉现象。
//这种现象在图像的边缘部分尤为明显,表现为彩色的晕边,通常是紫色或绿色的边缘。色差通常出现在便宜的镜头或极宽角镜头的照片中,而高质量的镜头设计会尽量减少这种效果。
//边缘会有类似彩虹的色斑
#if _CHROMATIC_ABERRATION
{
//高清渲染管线(HDRP)中的色差的超快速版本,使用3个样本和硬编码的光谱LUT。在低端GPU上性能显著提升。
float2 coords = 2.0 * uv - 1.0;
float2 end = uv - coords * dot(coords, coords) * ChromaAmount;
float2 delta = (end - uv) / 3.0;

half r = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, SCREEN_COORD_REMOVE_SCALEBIAS(uvDistorted) ).x;
half g = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, SCREEN_COORD_REMOVE_SCALEBIAS(DistortUV(delta + uv) )).y;
half b = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, SCREEN_COORD_REMOVE_SCALEBIAS(DistortUV(delta * 2.0 + uv))).z;

color = half3(r, g, b);
}
#else
{
color = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, SCREEN_COORD_REMOVE_SCALEBIAS(uvDistorted)).xyz;
}
#endif

//Gamma 空间...只需将 Uber 的其余部分以线性方式进行,最后再转换回 sRGB
#if UNITY_COLORSPACE_GAMMA
{
color = GetSRGBToLinear(color);
}
#endif

// 泛光(Bloom)
#if defined(BLOOM)
{
float2 uvBloom = uvDistorted;
#if defined(_FOVEATED_RENDERING_NON_UNIFORM_RASTER)
uvBloom = RemapFoveatedRenderingDistort(uvBloom);
#endif

#if _BLOOM_HQ && !defined(SHADER_API_GLES)
half4 bloom = SampleTexture2DBicubic(TEXTURE2D_X_ARGS(_Bloom_Texture, sampler_LinearClamp), SCREEN_COORD_REMOVE_SCALEBIAS(uvBloom), _Bloom_Texture_TexelSize.zwxy, (1.0).xx, unity_StereoEyeIndex);
#else
half4 bloom = SAMPLE_TEXTURE2D_X(_Bloom_Texture, sampler_LinearClamp, SCREEN_COORD_REMOVE_SCALEBIAS(uvBloom));
#endif

#if UNITY_COLORSPACE_GAMMA
bloom.xyz *= bloom.xyz; // γ to linear
#endif

UNITY_BRANCH
if (BloomRGBM > 0)
{
bloom.xyz = DecodeRGBM(bloom);
}

bloom.xyz *= BloomIntensity;
color += bloom.xyz * BloomTint;

#if defined(BLOOM_DIRT)
{
//污垢纹理的 UV 应该是 DistortUV(uv * DirtScale + DirtOffset),但考虑到我们在污垢纹理上使用了覆盖式比例,差异并不大,因此我们选择在这里保存一些 ALU,以防镜头畸变处于活动状态 。
half3 dirt = SAMPLE_TEXTURE2D(_LensDirt_Texture, sampler_LinearClamp, uvDistorted * LensDirtScale + LensDirtOffset).xyz;
dirt *= LensDirtIntensity;
color += dirt * bloom.xyz;
}
#endif
}
#endif

// To save on variants we'll use an uniform branch for vignette. Lower end platforms
// don't like these but if we're running Uber it means we're running more expensive
// effects anyway. Lower-end devices would limit themselves to on-tile compatible effect
// and thus this shouldn't too much of a problem (famous last words).
//为了节省变体,我们将使用统一的小插图分支。 低端平台不喜欢这些,但如果我们运行 Uber,这意味着我们无论如何都会运行更昂贵的效果。 低端设备会将自己限制为瓷砖兼容效果,因此这应该不是什么太大的问题(著名的遗言)。
UNITY_BRANCH
if (VignetteIntensity > 0)
{
color = ApplyVignette(color, uvDistorted, VignetteCenter, VignetteIntensity, VignetteRoundness, VignetteSmoothness, VignetteColor);
}

// 颜色修正
{
color = ApplyColorGrading(color, PostExposure, TEXTURE2D_ARGS(_InternalLut, sampler_LinearClamp), LutParams, TEXTURE2D_ARGS(_UserLut, sampler_LinearClamp), UserLutParams, UserLutContribution);
}
// 胶片颗粒(Film Grain)
#if _FILM_GRAIN
{
color = ApplyGrain(color, uv, TEXTURE2D_ARGS(_Grain_Texture, sampler_LinearRepeat), GrainIntensity, GrainResponse, GrainScale, GrainOffset, OneOverPaperWhite);
}
#endif

// 当 Unity 配置为使用 gamma 颜色编码时,我们忽略转换为 gamma 2.0 的请求,而是回退到 sRGB 编码
#if _GAMMA_20 && !UNITY_COLORSPACE_GAMMA
{
color = LinearToGamma20(color);
}
// Back to sRGB
#elif UNITY_COLORSPACE_GAMMA || _LINEAR_TO_SRGB_CONVERSION
{
color = GetLinearToSRGB(color);
}
#endif

//Dithering(抖动)是一种在数字图像处理中常用的技术,用于在有限的颜色深度显示设备上模拟更广泛的颜色范围。这种技术通过在像素之间故意添加噪声或图案,来模拟中间色调或渐变效果,从而减少颜色带(色阶突变)的视觉影响。
#if _DITHERING
{
color = ApplyDithering(color, uv, TEXTURE2D_ARGS(_BlueNoise_Texture, sampler_PointRepeat), DitheringScale, DitheringOffset, PaperWhite, OneOverPaperWhite);
//假设颜色 > 0 并防止 0 - ditherNoise。
//如果通过渲染到 FP16 纹理反馈到后处理,负色可能会导致问题。
color = max(color, 0);
}
#endif

// HDR编码颜色
#ifdef HDR_ENCODING
{
float4 uiSample = SAMPLE_TEXTURE2D_X(_OverlayUITexture, sampler_PointClamp, input.texcoord);
color.rgb = SceneUIComposition(uiSample, color.rgb, PaperWhite, MaxNits);
color.rgb = OETF(color.rgb, MaxNits);
}
#endif

return half4(color, 1.0);
}

ENDHLSL

SubShader
{
Tags
{
"RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline"
}
LOD 100
ZTest Always ZWrite Off Cull Off

Pass
{
Name "UberPost"

HLSLPROGRAM
#pragma vertex Vert
#pragma fragment FragUberPost
ENDHLSL
}
}
}

FinalPass

FinalPass是在后处理Pass处理完后,对其进行最后一次修改,主要包括:HDR输出,升/降分辨率和应用各种抗锯齿算法(FXAA和TAA等)

C#代码

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
// 渲染最终Pass
void RenderFinalPass(CommandBuffer cmd, ref RenderingData renderingData)
{
ref var cameraData = ref renderingData.cameraData;
var material = m_Materials.finalPass;
material.shaderKeywords = null;

//设置目标纹理大小(计算动态分辨率)
PostProcessUtils.SetSourceSize(cmd, cameraData.cameraTargetDescriptor);

//设置Film Grain(胶片颗粒)
SetupGrain(ref cameraData, material);
//Dithering(抖动)
SetupDithering(ref cameraData, material);

//是否需要转换回SRGB
if (RequireSRGBConversionBlitToBackBuffer(ref cameraData))
material.EnableKeyword(ShaderKeywordStrings.LinearToSRGBConversion);

//是否输出HDR
HDROutputUtils.Operation hdrOperations = HDROutputUtils.Operation.None;
bool requireHDROutput = RequireHDROutput(ref cameraData);
if (requireHDROutput)
{
// If there is a final post process pass, it's always the final pass so do color encoding
hdrOperations = m_EnableColorEncodingIfNeeded ? HDROutputUtils.Operation.ColorEncoding : HDROutputUtils.Operation.None;
// If the color space conversion wasn't applied by the uber pass, do it here
if (!cameraData.postProcessEnabled)
hdrOperations |= HDROutputUtils.Operation.ColorConversion;

SetupHDROutput(cameraData.hdrDisplayInformation, cameraData.hdrDisplayColorGamut, material, hdrOperations);
}

//配置调式处理器
DebugHandler debugHandler = GetActiveDebugHandler(ref renderingData);
bool resolveToDebugScreen = debugHandler != null && debugHandler.WriteToDebugScreenTexture(ref cameraData);
debugHandler?.UpdateShaderGlobalPropertiesForFinalValidationPass(cmd, ref cameraData, m_IsFinalPass && !resolveToDebugScreen);

if (m_UseSwapBuffer)
m_Source = cameraData.renderer.GetCameraColorBackBuffer(cmd);

RTHandle sourceTex = m_Source;

var colorLoadAction = cameraData.isDefaultViewport ? RenderBufferLoadAction.DontCare : RenderBufferLoadAction.Load;

bool isFxaaEnabled = (cameraData.antialiasing == AntialiasingMode.FastApproximateAntialiasing);

//是否FSR重采样是否开启
bool isFsrEnabled = ((cameraData.imageScalingMode == ImageScalingMode.Upscaling) && (cameraData.upscalingFilter == ImageUpscalingFilter.FSR));

//重复使用 RCAS 通道作为 TAA 的可选独立后锐化通道。 这避免了 EASU 的成本,并且可用于其他升级选项。如果启用 FSR,则 FSR 设置将覆盖 TAA 设置,并且我们仅执行一次 RCAS。
bool isTaaSharpeningEnabled = (cameraData.IsTemporalAAEnabled() && cameraData.taaSettings.contrastAdaptiveSharpening > 0.0f) && !isFsrEnabled;

//最终渲染目标需要缩放
if (cameraData.imageScalingMode != ImageScalingMode.None)
{
// 当在缩放渲染中启用 FXAA 时,我们在单独的 blit 中执行它,因为它不是设计用于在
// 输入和输出分辨率不匹配的情况。
// 当 FSR 处于活动状态时,我们总是需要额外的通道,因为它有非常特殊的颜色编码要求。
// 注意:理想的实现可以将此颜色转换逻辑内联到 UberPost 通道中,但当前的代码结构会使
// 这个过程非常复杂。 具体来说,我们需要保证 uber post 输出始终写入 UNORM 格式渲染
// 目标是为了保留特殊编码的颜色数据的精度。
bool isSetupRequired = (isFxaaEnabled || isFsrEnabled);

// 确保从临时渲染目标中删除任何 MSAA 和附加的深度缓冲区
var tempRtDesc = cameraData.cameraTargetDescriptor;
tempRtDesc.msaaSamples = 1;
tempRtDesc.depthBufferBits = 0;

// 选择 UNORM 格式,因为我们已经执行了色调映射。 (值在0-1范围内)
// 这可以提高精度,如果我们想在使用 FSR 时避免过度条带,这是必需的。
if (!requireHDROutput)
tempRtDesc.graphicsFormat = UniversalRenderPipeline.MakeUnormRenderTextureGraphicsFormat();

m_Materials.scalingSetup.shaderKeywords = null;
//是否启用了FXAA抗锯齿或是否启用了FSR重采样
if (isSetupRequired)
{
//需要HDR输出
if (requireHDROutput)
{
SetupHDROutput(cameraData.hdrDisplayInformation, cameraData.hdrDisplayColorGamut, m_Materials.scalingSetup, hdrOperations);
}
//启用了FXAA
if (isFxaaEnabled)
{
m_Materials.scalingSetup.EnableKeyword(ShaderKeywordStrings.Fxaa);
}
//启用了FRS
if (isFsrEnabled)
{
m_Materials.scalingSetup.EnableKeyword(hdrOperations.HasFlag(HDROutputUtils.Operation.ColorEncoding) ? ShaderKeywordStrings.Gamma20AndHDRInput : ShaderKeywordStrings.Gamma20);
}

//Blit
RenderingUtils.ReAllocateIfNeeded(ref m_ScalingSetupTarget, tempRtDesc, FilterMode.Point, TextureWrapMode.Clamp, name: "_ScalingSetupTexture");
Blitter.BlitCameraTexture(cmd, m_Source, m_ScalingSetupTarget, colorLoadAction, RenderBufferStoreAction.Store, m_Materials.scalingSetup, 0);

sourceTex = m_ScalingSetupTarget;
}

//根据缩放模式(放大或缩小)
switch (cameraData.imageScalingMode)
{
case ImageScalingMode.Upscaling:
{
// 在放大情况下,根据所选的放大过滤器设置材质关键字
// 注意:如果启用了 FSR,无论当前渲染比例如何,我们都会沿着这条路径走下去。 我们这样做是因为
// FSR 在 100% 比例下仍然提供视觉效果。 这也将实现 99% 和 100% 之间的过渡
// 对于 FSR 与动态分辨率缩放一起使用的情况,缩放不太明显。
switch (cameraData.upscalingFilter)
{
case ImageUpscalingFilter.Point:
{
//TAA 后锐化是 RCAS 通道,避免用点采样覆盖它。
if(!isTaaSharpeningEnabled)
material.EnableKeyword(ShaderKeywordStrings.PointSampling);
break;
}
case ImageUpscalingFilter.Linear:
{
//Shader的模式重采样算法,不做任何设置
break;
}
case ImageUpscalingFilter.FSR:
{
//FSR重采样
m_Materials.easu.shaderKeywords = null;

var upscaleRtDesc = tempRtDesc;
upscaleRtDesc.width = cameraData.pixelWidth;
upscaleRtDesc.height = cameraData.pixelHeight;

// EASU
RenderingUtils.ReAllocateIfNeeded(ref m_UpscaledTarget, upscaleRtDesc, FilterMode.Point, TextureWrapMode.Clamp, name: "_UpscaledTexture");
var fsrInputSize = new Vector2(cameraData.cameraTargetDescriptor.width, cameraData.cameraTargetDescriptor.height);
var fsrOutputSize = new Vector2(cameraData.pixelWidth, cameraData.pixelHeight);
FSRUtils.SetEasuConstants(cmd, fsrInputSize, fsrInputSize, fsrOutputSize);

Blitter.BlitCameraTexture(cmd, sourceTex, m_UpscaledTarget, colorLoadAction, RenderBufferStoreAction.Store, m_Materials.easu, 0);

// RCAS
// 如果可用,则使用覆盖值,否则使用默认值。
float sharpness = cameraData.fsrOverrideSharpness ? cameraData.fsrSharpness : FSRUtils.kDefaultSharpnessLinear;

// 设置 RCAS 通道的参数,除非锐度值表明它不会产生任何影响。
if (cameraData.fsrSharpness > 0.0f)
{
// RCAS 在最后的 post blit 期间执行,但我们在这里设置参数是为了更好的逻辑分组。
material.EnableKeyword(requireHDROutput ? ShaderKeywordStrings.EasuRcasAndHDRInput : ShaderKeywordStrings.Rcas);
FSRUtils.SetRcasConstantsLinear(cmd, sharpness);
}

// 更新源纹理以进行下一步操作
sourceTex = m_UpscaledTarget;
PostProcessUtils.SetSourceSize(cmd, upscaleRtDesc);

break;
}
}

break;
}

case ImageScalingMode.Downscaling:
{
// 在缩小的情况下,我们不执行任何类型的过滤器覆盖逻辑,因为我们总是想要线性过滤
// 它已经是着色器中的默认选项。

// 缩小尺寸时还禁用 TAA 后锐化通道。
isTaaSharpeningEnabled = false;
break;
}
}
}
else if (isFxaaEnabled)
{
// 在未缩放的渲染中,FXAA 可以在 FinalPost 着色器中安全地执行
material.EnableKeyword(ShaderKeywordStrings.Fxaa);
}

// 重用 RCAS 作为 TAA 的独立锐化过滤器。
// 如果启用了 FSR,那么它会覆盖 TAA 设置,我们会跳过它。
if(isTaaSharpeningEnabled)
{
material.EnableKeyword(ShaderKeywordStrings.Rcas);
FSRUtils.SetRcasConstantsLinear(cmd, cameraData.taaSettings.contrastAdaptiveSharpening);
}

var cameraTarget = RenderingUtils.GetCameraTargetIdentifier(ref renderingData);

if (resolveToDebugScreen)
{
// Blit 到调试器纹理而不是相机目标
Blitter.BlitCameraTexture(cmd, sourceTex, debugHandler.DebugScreenColorHandle, RenderBufferLoadAction.Load, RenderBufferStoreAction.Store, material, 0);
cameraData.renderer.ConfigureCameraTarget(debugHandler.DebugScreenColorHandle, debugHandler.DebugScreenDepthHandle);
}
else
{
// Blit到最终屏幕
RTHandleStaticHelpers.SetRTHandleStaticWrapper(cameraTarget);
var cameraTargetHandle = RTHandleStaticHelpers.s_RTHandleWrapper;
RenderingUtils.FinalBlit(cmd, ref cameraData, sourceTex, cameraTargetHandle, colorLoadAction, RenderBufferStoreAction.Store, material, 0);
}
}

Shader代码

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
Shader "Hidden/Universal Render Pipeline/FinalPost"
{
HLSLINCLUDE
#pragma multi_compile_local_fragment _ _POINT_SAMPLING _RCAS _EASU_RCAS_AND_HDR_INPUT
#pragma multi_compile_local_fragment _ _FXAA
#pragma multi_compile_local_fragment _ _FILM_GRAIN
#pragma multi_compile_local_fragment _ _DITHERING
#pragma multi_compile_local_fragment _ _LINEAR_TO_SRGB_CONVERSION
#pragma multi_compile_fragment _ DEBUG_DISPLAY
#pragma multi_compile_fragment _ SCREEN_COORD_OVERRIDE
#pragma multi_compile_local_fragment _ HDR_INPUT HDR_COLORSPACE_CONVERSION HDR_ENCODING HDR_COLORSPACE_CONVERSION_AND_ENCODING

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/ScreenCoordOverride.hlsl"
#if defined(HDR_COLORSPACE_CONVERSION) || defined(HDR_ENCODING) || defined(HDR_COLORSPACE_CONVERSION_AND_ENCODING)
#define HDR_INPUT 1 // this should be defined when HDR_COLORSPACE_CONVERSION or HDR_ENCODING are defined
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/HDROutput.hlsl"
#endif
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Debug/DebuggingFullscreen.hlsl"
#include "Packages/com.unity.render-pipelines.universal/Shaders/PostProcessing/Common.hlsl"

TEXTURE2D(_Grain_Texture);
TEXTURE2D(_BlueNoise_Texture);
TEXTURE2D_X(_OverlayUITexture);

float4 _SourceSize;
float2 _Grain_Params;
float4 _Grain_TilingParams;
float4 _Dithering_Params;
float4 _HDROutputLuminanceParams;

#define GrainIntensity _Grain_Params.x
#define GrainResponse _Grain_Params.y
#define GrainScale _Grain_TilingParams.xy
#define GrainOffset _Grain_TilingParams.zw

#define DitheringScale _Dithering_Params.xy
#define DitheringOffset _Dithering_Params.zw

#define MinNits _HDROutputLuminanceParams.x
#define MaxNits _HDROutputLuminanceParams.y
#define PaperWhite _HDROutputLuminanceParams.z
#define OneOverPaperWhite _HDROutputLuminanceParams.w

#if SHADER_TARGET >= 45
#define FSR_INPUT_TEXTURE _BlitTexture
#define FSR_INPUT_SAMPLER sampler_LinearClamp

// 如果定义了 HDR_INPUT,我们还必须在包含 FSR 公共标头之前定义 FSR_EASU_ONE_OVER_PAPER_WHITE。
// URP 实际上并不使用 FinalPost 着色器中的 EASU,仅使用 RCAS。
#define FSR_EASU_ONE_OVER_PAPER_WHITE OneOverPaperWhite
#include "Packages/com.unity.render-pipelines.core/Runtime/PostProcessing/Shaders/FSRCommon.hlsl"
#endif

half4 FragFinalPost(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);
float2 positionNDC = uv;
int2 positionSS = uv * _SourceSize.xy;

#if _POINT_SAMPLING
half3 color = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_PointClamp, uv).xyz;
#elif (_RCAS || _EASU_RCAS_AND_HDR_INPUT) && SHADER_TARGET >= 45
half3 color = ApplyRCAS(positionSS);
// 当Unity配置为使用gamma颜色编码时,我们必须在执行RCAS后从线性转换回来。
//(此着色器变体的输入颜色数据始终是线性编码的,因为 RCAS 需要它)
#if _EASU_RCAS_AND_HDR_INPUT
// Revert operation from ScalingSetup.shader
color.rgb = FastTonemapInvert(color.rgb) * PaperWhite;
#endif
#if UNITY_COLORSPACE_GAMMA
color = GetLinearToSRGB(color);
#endif
#else
half3 color = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv).xyz;
#endif

#if _FXAA
{
color = ApplyFXAA(color, positionNDC, positionSS, _SourceSize, _BlitTexture, PaperWhite, OneOverPaperWhite);
}
#endif

#if _FILM_GRAIN
{
color = ApplyGrain(color, SCREEN_COORD_APPLY_SCALEBIAS(positionNDC), TEXTURE2D_ARGS(_Grain_Texture, sampler_LinearRepeat), GrainIntensity, GrainResponse, GrainScale, GrainOffset, OneOverPaperWhite);
}
#endif

#if _LINEAR_TO_SRGB_CONVERSION
{
color = LinearToSRGB(color);
}
#endif

#if _DITHERING
{
color = ApplyDithering(color, SCREEN_COORD_APPLY_SCALEBIAS(positionNDC), TEXTURE2D_ARGS(_BlueNoise_Texture, sampler_PointRepeat), DitheringScale, DitheringOffset, PaperWhite, OneOverPaperWhite);
}
#endif

#ifdef HDR_COLORSPACE_CONVERSION
{
color.rgb = RotateRec709ToOutputSpace(color.rgb) * PaperWhite;
}
#endif

#ifdef HDR_ENCODING
{
float4 uiSample = SAMPLE_TEXTURE2D_X(_OverlayUITexture, sampler_PointClamp, input.texcoord);
color.rgb = SceneUIComposition(uiSample, color.rgb, PaperWhite, MaxNits);
color.rgb = OETF(color.rgb, MaxNits);
}
#endif

half4 finalColor = half4(color, 1);

#if defined(DEBUG_DISPLAY)
half4 debugColor = 0;

if(CanDebugOverrideOutputColor(finalColor, uv, debugColor))
{
return debugColor;
}
#endif

return finalColor;
}

ENDHLSL

/// Standard FinalPost shader variant with support for FSR
/// Note: FSR requires shader target 4.5 because it relies on texture gather instructions
SubShader
{
Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline"}
LOD 100
ZTest Always ZWrite Off Cull Off

Pass
{
Name "FinalPost"

HLSLPROGRAM
#pragma vertex Vert
#pragma fragment FragFinalPost
#pragma target 4.5
ENDHLSL
}
}

/// Fallback version of FinalPost shader which lacks support for FSR
SubShader
{
Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline"}
LOD 100
ZTest Always ZWrite Off Cull Off

Pass
{
Name "FinalPost"

HLSLPROGRAM
#pragma vertex Vert
#pragma fragment FragFinalPost
ENDHLSL
}
}
}

总结

Unity的后处理系统主要包含三个部分:

  1. Volume系统,主要负责后处理数据的编辑,包括不同Volume对象中的相交部分的插值计算。
  2. URP的后处理Pass, 主要有三个Pass:生成LUT的Pass, 后处理Pass,FinalPass。Pass主要是计算和设置Shader中要用到的参数以及调用Blit
  3. 各种后处理Shader,处理图像。

参考