后处理是指,在正常渲染管线结束后,对渲染出来的结果进行加工,以此来模拟各种效果。
颜色
颜色(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实现
渲染分辨率调整: 首先,渲染目标(RenderTarget)的分辨率被设置为目标显示分辨率的倍数。例如,如果目标是1080p(1920x1080),则可能在4K(3840x2160)分辨率上进行渲染,这实质上是对每个维度进行了2倍的超采样。
场景渲染: 图形管线按照这个高分辨率渲染整个场景,包括3D模型、纹理、光照等。这个过程涉及标准的渲染步骤,如几何处理、栅格化、片段着色等,但每个步骤处理的像素数量明显增加。
下采样: 渲染完成后,图形管线执行一个下采样(或称为图像缩放)步骤,将高分辨率的渲染结果缩减到目标分辨率。这个过程通常使用滤波算法(如双线性或双三次滤波)来合并像素,以保留细节的同时减少锯齿。
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中的实现细节:
几何处理阶段
多重采样缓冲区准备: 在GPU中,为实现MSAA,首先需要创建一个多重采样缓冲区。这个缓冲区相比常规的颜色缓冲区有更多的样本点,用于存储每个像素的多个颜色值和深度信息。例如,4x MSAA意味着每个像素将包含4个独立的样本点。
几何图元的栅格化: 当几何图元(如三角形)被栅格化(转换为像素网格)时,图形管线会为每个像素生成多个覆盖该像素的图元片段(片段是像素在渲染过程中的中间表示)。每个片段对应于像素内的一个样本点。
像素着色阶段
片段着色器执行: 对于MSAA,片段着色器可能会被执行多次,每个样本点一次,或者根据具体实现,可能只执行一次并共享结果。不过,通常情况下,片段着色器的执行结果会存储在每个样本点中,允许细节丰富和光滑的边缘渲染。
最终像素写入
样本点合并(Resolve): 在所有相关的几何图元被处理,且每个像素的样本点都被着色后,图形管线会执行一个合并(resolve)步骤,这一步骤将每个像素内的样本点的颜色值合并成一个单一的颜色值。这通常通过简单的平均值计算完成,但也可以包括更复杂的滤波算法。
写入帧缓冲区: 合并后的像素值最终被写入到帧缓冲区中,完成整个渲染过程。
GPU支持和优化
硬件级支持: 现代GPU设计有专门的硬件支持MSAA,包括专用的多重采样缓冲区和高效的样本点处理机制。这种硬件级支持使得MSAA能够在保持较高渲染质量的同时,最小化对性能的影响。
内存和带宽优化: 虽然MSAA增加了内存使用和带宽需求(因为需要为每个像素存储多个样本点的数据),但GPU厂商实现了多种优化技术,如压缩技术和智能缓冲区管理,以减少这些开销。
快速近似抗锯齿(FXAA,Fast Approximate Anti-Aliasing)
FXAA是一种屏幕空间算法,它在图像的最终阶段处理,通过分析像素的亮度变化来识别边缘,并对边缘进行平滑处理。FXAA对性能的影响较小,实现简单,但可能会导致图像细节的轻微模糊。 快速近似抗锯齿(Fast Approximate Anti-Aliasing, FXAA)是一种屏幕空间的抗锯齿技术,旨在以较低的性能成本减少图像中的锯齿现象。与传统的多重采样抗锯齿(MSAA)相比,FXAA在图形管线的后期处理阶段实施,不需要对每个几何图元进行多次采样,因而对性能的影响较小。以下是FXAA在图形管线和GPU中的实现概览:
图形管线中的FXAA实现
渲染场景: 首先,场景被正常渲染到一个帧缓冲区中,不应用任何抗锯齿技术。
后期处理阶段: 场景渲染完成后,FXAA作为后期处理效果应用。这意味着FXAA操作是在图像已经渲染完成的基础上进行的,利用片段着色器对已经渲染好的图像进行处理。
边缘检测: FXAA首先通过分析像素颜色的局部梯度来识别图像中的边缘。这一步通常涉及比较当前像素与其邻近像素的颜色差异。
锯齿平滑: 一旦边缘被识别,FXAA算法会沿着边缘方向平滑锯齿,方法是对边缘附近的像素进行混合。这种方式旨在减少锐利边缘处的颜色梯度,从而减少视觉上的锯齿现象。
色彩校正: 在某些实现中,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的实现步骤
边缘检测: SMAA首先在图像中识别出锯齿边缘。这通常通过分析颜色、亮度或深度差异来实现,以找到可能产生锯齿的像素边界。边缘检测可以使用多种算法,包括基于梯度、Sobel算子或自定义滤波器。
模式识别: 识别出边缘后,SMAA通过比较这些边缘与一系列预定义的模式(或模板)来进行分类。这些模式对应于常见的锯齿形状和分布,使SMAA能够更精确地确定如何对特定的锯齿边缘进行处理。
子像素处理: SMAA利用子像素级的信息来改善锯齿边缘的渲染质量。这涉及到对锯齿边缘附近的像素进行细微调整,以模拟更高分辨率下的图像细节和边缘平滑效果。
形态学合并: 最后,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的工作原理
运动矢量: TAA技术首先需要捕捉场景中对象的运动。这通过使用运动矢量图(Motion Vector Pass)来实现,运动矢量图记录了场景中每个像素点从前一帧到当前帧的移动情况。运动矢量通常由GPU的顶点着色器计算,并在渲染管线的早期阶段生成。
历史帧混合: TAA将当前帧的图像与之前帧的图像进行混合。通过对运动矢量的分析,TAA能够确定如何将当前帧的像素与历史帧的像素相结合,以减少锯齿并平滑边缘。这一步骤需要精确的运动跟踪和合适的权重分配,以确保图像的连贯性和减少运动模糊。
锐化和噪声处理: 由于TAA在混合过程中可能引入模糊,因此在处理完混合后,通常会应用一定程度的锐化来保持图像的清晰度。同时,TAA也可能引入一些图案噪声,特别是在场景快速变化时,因此可能需要额外的噪声抑制步骤。
GPU和图形管线中的实现
运动矢量的生成: 现代GPU提供了高效的方式来计算和存储运动矢量。这通常在顶点着色器或几何着色器中实现,通过比较当前帧和前一帧的顶点位置来生成。
图像处理和混合: TAA的混合处理主要在片段着色器和后处理阶段进行。GPU需要处理大量的纹理读取操作,包括访问当前帧的像素数据和历史帧的像素数据,然后基于运动矢量进行适当的混合。
后处理效果: TAA通常作为一系列后处理效果的一部分实现,在最终图像输出前应用。这意味着TAA可以与其他图像处理效果(如色彩校正、HDR渲染等)一起在GPU的后处理管线中执行。
考虑因素
运动估计的准确性: TAA的效果很大程度上依赖于运动矢量的准确性。不准确的运动跟踪可能导致图像拖影或其他视觉伪像。
性能和质量平衡: TAA在提升图像质量的同时,相对于其他抗锯齿技术,对性能的影响较小。但是,它需要合理的资源分配和优化,特别是在高分辨率渲染或要求高帧率的应用中。
URP后处理源码分析
在URP中后处理源码主要由三个部分组成:
Volume系统, 用于管理后处理组件参数,以及参数的混合。
URP的PostProcessPasses,用于管理后处理的相关Pass(ColorGradingLutPass和PostProcessPass),Pass会将Volume系统提供的参数设置给Shader中使用。
各种后处理Shader(PostProcessData持久化对象存储了后处理中使用的各种Shader和纹理)
Volume系统
Volume系统由两个部分组成:
持久化类, 用于存储各种后处理组件的参数,核心类:VolumeProfile,VolumeComponent和VolumeParameter
运行时类, 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) { stateParam.overrideState = toParam.overrideState; stateParam.Interp(stateParam, toParam, interpFactor); } } }
VolumeParameter
后处理组件的参数,有各种类型的派生类(int, float, vector等类型,也可以根据自己需求新增派生类),核心函数就是Interp,每种类型都有自己的Interp。
1 2 3 4 5 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 public void Update (VolumeStack stack, Transform trigger, LayerMask layerMask ) { Assert.IsNotNull(stack); CheckBaseTypes(); CheckStack(stack); ReplaceData(stack, m_ComponentsDefaultState); bool onlyGlobal = trigger == null ; var triggerPos = onlyGlobal ? Vector3.zero : trigger.position; var volumes = GrabVolumes(layerMask); Camera camera = null ; if (!onlyGlobal) trigger.TryGetComponent<Camera>(out camera); foreach (var volume in volumes) { if (volume == null ) continue ; #if UNITY_EDITOR if (!IsVolumeRenderedByCamera(volume, camera)) continue ; #endif if (!volume.enabled || volume.profileRef == null || volume.weight <= 0f ) continue ; if (volume.isGlobal) { OverrideData(stack, volume.profileRef.components, Mathf.Clamp01(volume.weight)); continue ; } if (onlyGlobal) continue ; var colliders = m_TempColliders; volume.GetComponents(colliders); if (colliders.Count == 0 ) continue ; 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(); float blendDistSqr = volume.blendDistance * volume.blendDistance; if (closestDistanceSqr > blendDistSqr) continue ; float interpFactor = 1f ; if (blendDistSqr > 0f ) interpFactor = 1f - (closestDistanceSqr / blendDistSqr); 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 ) { 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; 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 )); 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); 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()); 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()); 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 ; } 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); 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 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; var cmd = renderingData.commandBuffer; if (m_IsFinalPass) { using (new ProfilingScope(cmd, m_ProfilingRenderFinalPostProcessing)) { 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; bool useTemporalAA = cameraData.IsTemporalAAEnabled(); if (cameraData.antialiasing == AntialiasingMode.TemporalAntiAliasing && !useTemporalAA) TemporalAA.ValidateAndWarn(ref cameraData); int amountOfPassesRemaining = (useStopNan ? 1 : 0 ) + (useSubPixeMorpAA ? 1 : 0 ) + (useDepthOfField ? 1 : 0 ) + (useLensFlare ? 1 : 0 ) + (useTemporalAA ? 1 : 0 ) + (useMotionBlur ? 1 : 0 ) + (usePaniniProjection ? 1 : 0 ); 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 ) { 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) { r.SwapColorBuffer(cmd); source = r.cameraColorTargetHandle; 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 )); 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); } } 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); } } 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); } } if (usePaniniProjection) { using (new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.PaniniProjection))) { DoPaniniProjection(cameraData.camera, cmd, GetSource(), GetDestination()); Swap(ref renderer); } } using (new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.UberPostProcess))) { m_Materials.uber.shaderKeywords = null ; 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); } } SetupLensDistortion(m_Materials.uber, isSceneViewCamera); SetupChromaticAberration(m_Materials.uber); SetupVignette(m_Materials.uber, cameraData.xr); SetupColorGrading(cmd, ref renderingData, m_Materials.uber); SetupGrain(ref cameraData, m_Materials.uber); SetupDithering(ref cameraData, m_Materials.uber); if (RequireSRGBConversionBlitToBackBuffer(ref cameraData)) m_Materials.uber.EnableKeyword(ShaderKeywordStrings.LinearToSRGBConversion); bool requireHDROutput = RequireHDROutput(ref cameraData); if (requireHDROutput) { 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); var colorLoadAction = RenderBufferLoadAction.DontCare; if (m_Destination == k_CameraTarget && !cameraData.isDefaultViewport) colorLoadAction = RenderBufferLoadAction.Load; 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 ; 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) { if (resolveToDebugScreen) { Blitter.BlitCameraTexture(cmd, GetSource(), debugHandler.DebugScreenColorHandle, RenderBufferLoadAction.Load, RenderBufferStoreAction.Store, m_Materials.uber, 0 ); renderer.ConfigureCameraTarget(debugHandler.DebugScreenColorHandle, debugHandler.DebugScreenDepthHandle); } else { 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系数用于调整图像中的颜色分量,以反映光源下的真实颜色。不同环境色温下拍出来的拍色是有一定差异的,白平衡的目的就是通过后期的手段将白色还原成白色。
色温图:
色温图
在调色的过程中,如果图片本身偏高色温(蓝色),那么就要补偿低色温(橙色),来达到白色。如果想营造某种艺术效果,可以根据需求调整。
过程:
将白平衡的色温(temperature)和色调(tint)参数转换成LMS系数
将原颜色在LMS空间应用白平衡参数
最后将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 ) { float t1 = temperature / 65f ; float t2 = tint / 65f ; float x = 0.31271f - t1 * (t1 < 0f ? 0.1f : 0.05f ); float y = StandardIlluminantY(x) + t2 * 0.05f ; var w1 = new Vector3(0.949237f , 1.03542f , 1.08728f ); var w2 = CIExyToLMS(x, y); return new Vector3(w1.x / w2.x, w1.y / w2.y, w1.z / w2.z); } public static Vector3 CIExyToLMS (float x, float y ) { float Y = 1f ; float X = Y * x / y; float Z = Y * (1f - x - y) / y; 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 float3 colorLMS = LinearToLMS(colorLinear); colorLMS *= _ColorBalance.xyz; colorLinear = LMSToLinear(colorLMS);
ColorAdjustments(颜色调整)
Color Adjustments是图像处理和图形设计中常见的一组技术,用于修改和优化图像的色彩属性,包括亮度、对比度、饱和度、色调、曝光等。这些调整能够帮助图像更好地传达预期的视觉效果或情绪,提升图像质量,或者适应特定的显示设备和环境。
对比度(Contrast) :调整图像明暗区域之间的差异,增加对比度会使图像的明暗区域更加分明,而降低对比度则会使图像看起来更加平坦。
色调(Hue) :改变图像中的色彩平衡,常用于为图像创建特定的色彩风格或修正图像的色温。
饱和度(Saturation) :调整图像中颜色的强度,饱和度高的图像色彩会更加鲜明,而饱和度低的图像则色彩更加柔和。
亮度(Brightness) :调整图像的明暗程度,使图像看起来更亮或更暗。
曝光(Exposure) :模拟调整摄影中的曝光效果,可以使图像整体变亮或变暗,用于修正过曝或欠曝的图像
对比度 :
过程:
将颜色转换到对数(LogC)空间
应用对比度调节公式, 如下
将结果转换回线性(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);
色调 :
过程:
将RGB颜色转换到HSV色彩空间,
调整饱和度H值,
将结果转换回RGB空间
代码:
1 2 3 float3 hsv = RgbToHsv(colorLinear); float hue = hsv.x + _HueSatCon.x; colorLinear = HsvToRgb(hsv);
饱和度 :
过程:
将RGB颜色转换到HSV色彩空间,
调整饱和度S值,
将结果转换回RGB空间
代码:
1 2 luma = GetLuminance(colorLinear); 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)部分应用不同的色调,而保持中间调的颜色相对不变。
过程:
为了和Adobe产品的计算方式一样,要将线性颜色转换到Gamma
计算像素的亮度值,根据亮度值,确定分离后的阴影和高光颜色
将分离出来的阴影和高光颜色与原颜色使用柔光的混合模式进行混合
最后将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 float balance = _SplitShadows.w;float3 colorGamma = PositivePow(colorLinear, 1.0 / 2.2 ); float luma = saturate(GetLuminance(saturate(colorGamma)) + balance);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 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)的调节是一种常见的技术,用于细致调整图像的色调平衡和对比度。这种方法允许你分别调整图像中暗部、中亮部和亮部的亮度、色彩和饱和度,以达到更为精细的视觉效果。
过程:
分离阴影、中间调、高光:首先,基于像素的亮度值将图像分割成阴影、中间调和高光三个部分。这通常通过设置亮度阈值来实现,不同的亮度范围对应于阴影、中间调和高光。
应用调节:对每个部分(阴影、中间调、高光)分别应用亮度、色彩和饱和度的调节。调节可以是线性的,也可以是基于特定曲线的,如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); 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-03 f); gamma.y = 1f / Mathf.Max(gamma.y - lumGamma + gamma.w, 1e-03 f); gamma.z = 1f / Mathf.Max(gamma.z - lumGamma + gamma.w, 1e-03 f); 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
HueVsHue :这是色相对色相的调整,允许你根据原始色相改变色相。例如,可以将所有的绿色调整为更偏蓝的色相。
HueVsSat :这是色相对饱和度的调整,允许你基于图像中特定色相的存在来增加或减少饱和度。例如,可以仅增加红色的饱和度而不影响其他色彩。
SatVsSat :这是饱和度对饱和度的调整,允许你根据原始饱和度改变饱和度级别。这可以用来增强色彩鲜艳度或者使图像看起来更自然。
LumVsSat :这是亮度对饱和度的调整,允许你基于像素的亮度值来增加或减少饱和度。例如,可以减少亮度最高区域的饱和度来防止色彩过饱和。
YRGB曲线 YRGB曲线是一种用于图像处理和颜色校正中的工具,它允许用户分别调整图像中的亮度(Y)和红色(R)、绿色(G)、蓝色(B)三个颜色通道。通过调整这些曲线,可以影响图像的整体色调、对比度和颜色平衡。
过程:
将曲线值映射到大小128*1的纹理中,曲线值离散到128个值存在纹理。
在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); { satMult = EvaluateCurve(_CurveHueVsSat, hsv.x) * 2.0 ; satMult *= EvaluateCurve(_CurveSatVsSat, hsv.y) * 2.0 ; satMult *= EvaluateCurve(_CurveLumVsSat, Luminance(colorLinear)) * 2.0 ; 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); { const float kHalfPixel = (1.0 / 128.0 ) / 2.0 ; float3 c = colorLinear; 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); 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 { float3 colorLutSpace = GetLutStripValue(input.texcoord, _Lut_Params); float3 gradedColor = ColorGrade(colorLutSpace); #ifdef HDR_COLORSPACE_CONVERSION gradedColor = ProcessColorForHDR(gradedColor); #else gradedColor = Tonemap(gradedColor); #endif return float4(gradedColor, 1.0 ); } 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 return RotateRec2020ToOutputSpace(colorLinear) * PaperWhite; #endif #endif return colorLinear; } float3 Tonemap (float3 colorLinear) { #if _TONEMAP_NEUTRAL { colorLinear = NeutralTonemap(colorLinear); } #elif _TONEMAP_ACES { float3 aces = ACEScg_to_ACES(colorLinear); colorLinear = AcesTonemap(aces); } #endif return colorLinear; }
视觉效果(VFX)
景深(Depth of Field,DOF)
景深后处理是一种用于模拟相机镜头产生的景深效果的图像处理技术。它可以在后期处理阶段模拟出景深的效果,使得图像中的某些部分变得模糊,而其他部分保持清晰,从而增强了图像的艺术感和视觉效果。
实现原理: 景深后处理的实现原理主要基于两个关键概念:景深和高斯模糊。
景深(Depth of Field,DOF) :景深是指相机镜头焦点前后一定范围内的物体都能保持清晰的范围。景深受到相机参数(如焦距、光圈大小)和拍摄场景的影响。在景深后处理中,通过模拟景深范围内的物体变得模糊来实现景深效果。
高斯模糊 :高斯模糊是一种常用的图像模糊技术,它通过对图像中每个像素的周围像素应用高斯函数来降低像素的清晰度。模糊半径决定了模糊的程度,通常用于模拟景深中物体的模糊效果。
合成 :将模糊处理后的图像与原始图像进行混合,以获得最终的景深效果。通常使用混合模式(如线性混合或增加混合)来调整模糊图像与原始图像之间的比例。
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 ); 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)); Blitter.BlitCameraTexture(cmd, source, m_FullCoCTexture, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, material, 0 ); 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; 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 )); Blitter.BlitCameraTexture(cmd, source, m_FullCoCTexture, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, material, 0 ); cmd.SetGlobalTexture(ShaderConstants._FullCoCTexture, m_FullCoCTexture.nameID); 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 ); 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 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 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 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 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; colorAcc += tapColor * coc; farCoCAcc += coc; } half3 color = colorAcc * rcp(kCount); half farCoC = farCoCAcc * rcp(kCount); #else half farCoC = SAMPLE_TEXTURE2D_X(_FullCoCTexture, sampler_LinearClamp, uv).x; 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); 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; half weight = saturate(1.0 - (samp0CoC - sampCoC)); acc += half4(sampColor, premultiply ? sampCoC : 1.0 ) * kCoeffs[i] * weight; } acc.xyz /= acc.w + 1e-4 ; 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 ) { 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" #define SAMPLE_COUNT 42 #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) 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); 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; 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; 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 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 ); 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 half cocMin = min (coc0, Min3(coc1, coc2, coc3)); half cocMax = max (coc0, Max3(coc1, coc2, coc3)); half coc = (-cocMin > cocMax ? cocMin : cocMax) * MaxRadius; 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); half farCoC = max (min (samp0.a, samp.a), 0.0 ); half farWeight = saturate((farCoC - disp.z + _BokehConstants.y) / _BokehConstants.y); half nearWeight = saturate((-samp.a - disp.z + _BokehConstants.y) / _BokehConstants.y); nearWeight *= step (_BokehConstants.x, -samp.a); farAcc += half4(samp.rgb, 1.0 h) * farWeight; nearAcc += half4(samp.rgb, 1.0 h) * 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 ; half4 nearAcc = 0.0 ; Accumulate(samp0, uv, 0.0 , farAcc, nearAcc); UNITY_LOOP for (int si = 0 ; si < SAMPLE_COUNT; si++) { Accumulate(samp0, uv, _BokehKernel[si], farAcc, nearAcc); } farAcc.rgb /= farAcc.a + (farAcc.a == 0.0 ); nearAcc.rgb /= nearAcc.a + (nearAcc.a == 0.0 ); nearAcc.a *= PI / (SAMPLE_COUNT + 1 ); 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); 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; 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; 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.5 h) + (velocitySign * (randomVal - 0.5 h)); 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) { 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) { 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 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 ; 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); 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 ); } Blitter.BlitCameraTexture(cmd, source, m_BloomMipDown[0 ], RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, bloomMaterial, 0 ); 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]; } 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 ); } 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 ]); 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; #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); #else return outColor; #endif } half3 DecodeHDR (half4 color) { #if UNITY_COLORSPACE_GAMMA color.xyz *= color.xyz; #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); 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); 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 #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)是一种在图像边缘逐渐减弱亮度或增加饱和度的效果,通常用于突出图像中心或增加视觉焦点。实现晕影效果的后处理方法通常基于图像处理技术,其实现原理可以概括如下:
基于距离的衰减: 一种常见的实现方法是根据像素与图像中心的距离来计算衰减系数,距离中心越远的像素,衰减系数越大。这样,在后处理过程中,可以根据像素与中心的距离来对像素的亮度或饱和度进行加权,使得边缘逐渐变暗或变饱和。
径向函数: 晕影效果的实现也可以利用径向函数来描述衰减的形式,例如高斯函数或多项式函数。通过选择合适的径向函数参数,可以实现不同形式的晕影效果,例如较平滑的渐变或较陡峭的渐变。
遮罩技术: 另一种实现晕影效果的方法是使用遮罩技术,即在图像上叠加一个透明的黑色或彩色遮罩,使得边缘透明度逐渐增加。通过调整遮罩的形状和透明度,可以实现不同形式的晕影效果。
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)是一种模拟传统胶片摄影中出现的颗粒状噪点的效果,可以增加图像的质感和艺术感。实现胶片颗粒效果的后处理方法通常基于图像处理技术,其实现原理可以概括如下:
随机噪声生成: 胶片颗粒效果的实现通常涉及生成随机的颗粒噪声。可以使用伪随机数生成器来生成服从特定分布(如高斯分布)的随机数序列,然后将这些随机数映射到图像像素上,以模拟胶片颗粒的分布。
混合叠加: 生成的颗粒噪声可以与原始图像进行混合叠加。可以通过调整混合的透明度或混合模式(如叠加、乘法等)来控制颗粒效果的强度和影响范围。
空间滤波: 在一些情况下,可以使用空间滤波技术来模拟胶片颗粒的空间分布特征。例如,可以使用卷积滤波器(如高斯滤波器)来对图像进行模糊处理,然后通过减去模糊图像和原始图像之间的差异来生成颗粒噪声。
颗粒参数调整: 胶片颗粒效果的外观可以通过调整参数来控制,例如颗粒的大小、密度、形状和颜色等。通过调整这些参数,可以实现不同类型和风格的胶片颗粒效果。
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) { half grain = SAMPLE_TEXTURE2D(GrainTexture, GrainSampler, uv * scale + offset).w; 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 #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 #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" #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 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; #if _CHROMATIC_ABERRATION { 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 #if UNITY_COLORSPACE_GAMMA { color = GetSRGBToLinear(color); } #endif #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; #endif UNITY_BRANCH if (BloomRGBM > 0 ) { bloom.xyz = DecodeRGBM(bloom); } bloom.xyz *= BloomIntensity; color += bloom.xyz * BloomTint; #if defined(BLOOM_DIRT) { half3 dirt = SAMPLE_TEXTURE2D(_LensDirt_Texture, sampler_LinearClamp, uvDistorted * LensDirtScale + LensDirtOffset).xyz; dirt *= LensDirtIntensity; color += dirt * bloom.xyz; } #endif } #endif 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); } #if _FILM_GRAIN { color = ApplyGrain(color, uv, TEXTURE2D_ARGS(_Grain_Texture, sampler_LinearRepeat), GrainIntensity, GrainResponse, GrainScale, GrainOffset, OneOverPaperWhite); } #endif #if _GAMMA_20 && !UNITY_COLORSPACE_GAMMA { color = LinearToGamma20(color); } #elif UNITY_COLORSPACE_GAMMA || _LINEAR_TO_SRGB_CONVERSION { color = GetLinearToSRGB(color); } #endif #if _DITHERING { color = ApplyDithering(color, uv, TEXTURE2D_ARGS(_BlueNoise_Texture, sampler_PointRepeat), DitheringScale, DitheringOffset, PaperWhite, OneOverPaperWhite); color = max (color, 0 ); } #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 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 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); SetupGrain(ref cameraData, material); SetupDithering(ref cameraData, material); if (RequireSRGBConversionBlitToBackBuffer(ref cameraData)) material.EnableKeyword(ShaderKeywordStrings.LinearToSRGBConversion); HDROutputUtils.Operation hdrOperations = HDROutputUtils.Operation.None; bool requireHDROutput = RequireHDROutput(ref cameraData); if (requireHDROutput) { hdrOperations = m_EnableColorEncodingIfNeeded ? HDROutputUtils.Operation.ColorEncoding : HDROutputUtils.Operation.None; 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); bool isFsrEnabled = ((cameraData.imageScalingMode == ImageScalingMode.Upscaling) && (cameraData.upscalingFilter == ImageUpscalingFilter.FSR)); bool isTaaSharpeningEnabled = (cameraData.IsTemporalAAEnabled() && cameraData.taaSettings.contrastAdaptiveSharpening > 0.0f ) && !isFsrEnabled; if (cameraData.imageScalingMode != ImageScalingMode.None) { bool isSetupRequired = (isFxaaEnabled || isFsrEnabled); var tempRtDesc = cameraData.cameraTargetDescriptor; tempRtDesc.msaaSamples = 1 ; tempRtDesc.depthBufferBits = 0 ; if (!requireHDROutput) tempRtDesc.graphicsFormat = UniversalRenderPipeline.MakeUnormRenderTextureGraphicsFormat(); m_Materials.scalingSetup.shaderKeywords = null ; if (isSetupRequired) { if (requireHDROutput) { SetupHDROutput(cameraData.hdrDisplayInformation, cameraData.hdrDisplayColorGamut, m_Materials.scalingSetup, hdrOperations); } if (isFxaaEnabled) { m_Materials.scalingSetup.EnableKeyword(ShaderKeywordStrings.Fxaa); } if (isFsrEnabled) { m_Materials.scalingSetup.EnableKeyword(hdrOperations.HasFlag(HDROutputUtils.Operation.ColorEncoding) ? ShaderKeywordStrings.Gamma20AndHDRInput : ShaderKeywordStrings.Gamma20); } 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: { switch (cameraData.upscalingFilter) { case ImageUpscalingFilter.Point: { if (!isTaaSharpeningEnabled) material.EnableKeyword(ShaderKeywordStrings.PointSampling); break ; } case ImageUpscalingFilter.Linear: { break ; } case ImageUpscalingFilter.FSR: { m_Materials.easu.shaderKeywords = null ; var upscaleRtDesc = tempRtDesc; upscaleRtDesc.width = cameraData.pixelWidth; upscaleRtDesc.height = cameraData.pixelHeight; 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 ); float sharpness = cameraData.fsrOverrideSharpness ? cameraData.fsrSharpness : FSRUtils.kDefaultSharpnessLinear; if (cameraData.fsrSharpness > 0.0f ) { material.EnableKeyword(requireHDROutput ? ShaderKeywordStrings.EasuRcasAndHDRInput : ShaderKeywordStrings.Rcas); FSRUtils.SetRcasConstantsLinear(cmd, sharpness); } sourceTex = m_UpscaledTarget; PostProcessUtils.SetSourceSize(cmd, upscaleRtDesc); break ; } } break ; } case ImageScalingMode.Downscaling: { isTaaSharpeningEnabled = false ; break ; } } } else if (isFxaaEnabled) { material.EnableKeyword(ShaderKeywordStrings.Fxaa); } if (isTaaSharpeningEnabled) { material.EnableKeyword(ShaderKeywordStrings.Rcas); FSRUtils.SetRcasConstantsLinear(cmd, cameraData.taaSettings.contrastAdaptiveSharpening); } var cameraTarget = RenderingUtils.GetCameraTargetIdentifier(ref renderingData); if (resolveToDebugScreen) { Blitter.BlitCameraTexture(cmd, sourceTex, debugHandler.DebugScreenColorHandle, RenderBufferLoadAction.Load, RenderBufferStoreAction.Store, material, 0 ); cameraData.renderer.ConfigureCameraTarget(debugHandler.DebugScreenColorHandle, debugHandler.DebugScreenDepthHandle); } else { 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 #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 #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); #if _EASU_RCAS_AND_HDR_INPUT 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 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 } } 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的后处理系统主要包含三个部分:
Volume系统,主要负责后处理数据的编辑,包括不同Volume对象中的相交部分的插值计算。
URP的后处理Pass, 主要有三个Pass:生成LUT的Pass, 后处理Pass,FinalPass。Pass主要是计算和设置Shader中要用到的参数以及调用Blit
各种后处理Shader,处理图像。
参考