Shader学习笔记(七)

UnityShader 高级篇(三)

屏幕后处理效果

屏幕后处理效果(screen post-processing effecrs) 是游戏中实现屏幕特效的常见方法。

建立一个基本的屏幕后处理脚本系统

屏幕后处理,顾名思义,通常指的是在渲染完整个场景得到屏幕图像后,再对这个图像进行一系列操作,实现各种屏幕特效。如景深(Depth of Field)、运动模糊(Motion Blur)等。
因此,想要实现屏幕后处理的基础在于得到渲染后的屏幕图像,即抓取屏幕,而Unity提供了这样一个方便的接口——OnRenderImage函数。它的函数声明如下:

MonoBehaviour.OnRenderImage (RenderTexture src , RenderTexture dest)

作用:将源渲染纹理(存在参数一:src)进行处理,输出目标渲染纹理(存在参数二: dest)

处理渲染纹理的函数通常是 Graphics.Bilt 函数
默认情况:OnRenderImage函数 会在所有 透明和不透明 的Pass执行完后才被调用,即对场景中所有物体产生影响

让透明Pass不参与后处理:在 OnRenderImage函数 前添加 ImageEffectOpaque属性,可以让 OnRenderImage函数在 渲染队列小于等于2500的Pass执行后被调用,这样可让 透明Pass(Quene>=3000)不受后处理的影响

public static void Bilt(Texture src , RenderTexture dest);
public static void Bilt(Texture src , RenderTexture dest , Material mat , int pass =-1);
public static void Bilt(Texture src , Material mat , int pass =-1);

src——源纹理,会被传给屏幕后处理Shader中名为 _MainTex 的纹理属性
dest——目标渲染纹理,若值为null,则直接将结果显示到屏幕
mat——屏幕后处理Shader创建的材质
pass——默认值为-1,表示将会调用所有Pass。否则,只调用给定索引的Pass

因此,要在Unity中实现屏幕后处理效果,过程通常如下:
需要在摄像机添加一个用于屏幕后处理的脚本,该脚本会实现OnRenderImage 函数来获取当前屏幕的渲染纹理。然后,再调用Graphics.Blit 函数使用特定的 Unity Shader 来对当前图像进行处理,再把返回的渲染纹理显示到屏幕上。对一些复杂的屏幕特效,可能需要多次调用Graphics.Blit函数来对上一步的输出结果进行下一步处理。

  1. 创建一个用于屏幕后处理效果的基类
    在这里插入图片描述
  2. 每个屏幕后处理效果通常都需要指定一个Shader来创建一个用于处理渲染纹理的材质,因此基类中也提供了这样的方法:
    在这里插入图片描述

调整屏幕的亮度、饱和度和对比度

调整屏幕的亮度、饱和度和对比度

  1. 声明脚本BrightnessSaturationAndContrastTest继承自PostEffectsBaseTest,用于调整相关参数
    在这里插入图片描述

  2. 声明本例需用到的各个属性:
    在这里插入图片描述

  3. 定义用于屏幕后处理的Pass:
    在这里插入图片描述

  4. 顶点着色器,把正确的纹理坐标传递给片元着色器,以便对屏幕图像进行正确采样
    在这里插入图片描述

  5. 实现用于调整亮度、饱和度和对比度的片元着色器:
    在这里插入图片描述

  6. 最后关闭该 Unity Shader 的 Fallback:
    在这里插入图片描述
    再把该Shader拖拽受到摄像机的BrightnessSaturationAndContrastTest.cs脚本中的shader参数中,调整相关参数即可得到

最终效果

在这里插入图片描述

在这里插入图片描述

边缘检测

边缘检测是描边效果的一种实现方式,原理是利用一些边缘检测算子对图像进行卷积(convolution) 操作,首先来了解什么是卷积。

卷积

在图像处理中,卷积操作是使用一个卷积核(kernel)对一张图像中的每个像素进行一系列操作。卷积核通常是一个四方形网格结构。该区域内每个方格都有一个权重值。当对图像中的某个像素进行卷积时,会把卷积核的中心放置于该像素上,翻转核之后再依次计算核中每个元素和其覆盖的图像像素值的乘积并求和,得到的结果是该位置的新像素值。
在这里插入图片描述
这样的计算过程虽简单,但可实现很多常见的图像处理效果,例如图像模糊、边缘检测等。例如,对图像进行均值模糊,可使用一个3x3的卷积核,核内每个元素的值均为1/9

常见的边缘检测算子

卷积操作的神奇之处在于选择的卷积核。
如果相邻像素之间存在差别明显的颜色、亮度、纹理等属性,会认为它们之间应该有一条边界,这种相邻像素之间的差值可以用**梯度(gradient)**来表示,可想象到,边缘处的梯度绝对值会比较大。基于这样的理解,有几种不同的边缘检测算字被先后提出来。
在这里插入图片描述
3中藏剑的边缘检测算字都包含了两个方向的卷积,分辨用于检测水平和竖直方向上的边缘信息。在进行边缘检测时,需对每个像素分别进行一次卷积计算,得到两个方向上的梯度值Gx和Gy,而整体的梯度可按下面的公式计算而得:在这里插入图片描述

实现

本节将会使用Sobel 算子进行边缘检测,实现描边效果。

  1. 声明脚本EdgeDetectionTest继承自PostEffectsBaseTest,用于调整相关参数
    在这里插入图片描述
  2. 声明本例使用的各个属性:
    在这里插入图片描述
  3. 设置屏幕后处理的Pass,并设置相关的渲染状态:
    在这里插入图片描述
  4. 除属性所需变量外,新增变量_MainTex_TexelSize
    在这里插入图片描述
  5. 顶点着色器计算边缘检测时需要的纹理坐标:
    在这里插入图片描述
  6. 片元着色器是重点
    在这里插入图片描述

最终效果

在这里插入图片描述
本文实现的边缘检测仅仅利用了屏幕颜色信息,而在实际应用中,物体的纹理、阴影等信息均会影响边缘检测的结果,使得结果包含许多非预期的描边。为了得到更加准确的边缘信息,往往会在屏幕的深度纹理和法线纹理上进行边缘检测

高斯模糊

学习卷积的另一个常见应用——高斯模糊。模糊的实现有很多方法,例如均值模糊和中值模糊。均值模糊同样使用了卷积操作,它使用的卷积核中的各个元素值都相等,且相加等于1,也就是说,卷积后得到的像素值是其领域内各个像素值的平均值。而中值模糊则是选择领域内对所有像素排序后的中值替换掉原颜色。一个更高级的模糊方法是高斯模糊。

高斯滤波

高斯模糊同样利用了卷积计算,它使用的卷积核名为高斯核。高斯核是一个正方形大小的滤波核,其中每个元素的计算都是基于下面的高斯方程:
在这里插入图片描述
其中,σ是标准方差(一般取值为1),x和y分别对应了当前位置到卷积核中心的整数距离。要构建一个高斯核,只需计算高斯核中各个位置对应的高斯值。为了保证滤波后的图像不会变暗,需要对高斯核中的权重进行归一化,即让每个权重除以所有权重的和,这样可保证所有权重的和为1。因此,高斯函数中e前面的系数实际不会对结果有任何影响。
高斯方程很好地模拟了领域每个像素对当前处理像素的影响程度——距离越大,影响越大。使用一个NxN 的高斯核对图像进行卷积滤波,就需要NxNxWxH(W和H分别是图像的宽和高)次纹理采样。当N的大小不断增加时,采样次数会变得非常巨大,幸运的是,可把这个二维高斯函数拆分成两个一维函数。也就是说,可使用两个一维的高斯核先后对图像进行滤波,它们得到的结果和直接使用二维高斯核是一样的,但采样次数只需要2xNxWxH.。可进一步观察到,两个一维高斯核中包含了很多重复的权重。对于一个大小为5的一维高斯核。实际只需要记录3个权重值即可。
在这里插入图片描述
本节将会使用上述 5x5 的高斯核对原图像进行高斯模糊。将先后调用两个Pass,第一个Pass将会使用竖直方向的一维高斯核对图像进行滤波,第二个Pass再使用水平方向的一维高斯核对图像进行滤波,得到最终的目标图像。在实现中,还将利用图像缩放来进一步提高性能,并通过调整高斯滤波的应用次数来控制模糊程度(次数越多,图像越模糊)

实现

  1. 声明脚本GaussianBlurTest继承自PostEffectsBaseTest
    在这里插入图片描述
  2. 需要定义关键的OnRenderImage 函数,首先来看第一个版本
    在这里插入图片描述
  3. 第二个版本将利用缩放对图像进行降采样,从而减少需要处理的像素个数,提高性能。
    在这里插入图片描述
  4. 最后一个版本的代码还考虑了高斯模糊的迭代次数:
    在这里插入图片描述
    在迭代开始前,先定义了第一个缓存buffer0,并把src中的图像缩放后存储到buffer0 中。在迭代过程中,又定义了第二个缓存buffer1。在执行第一个Pass时,输入是buffer0,输出是buffer1,完毕后首先把 buffer0 释放,再把结果值 buffer1 存储到 buffer0 中,重新分配 buffer1,然后再调用第二个Pass。重复上述过程。迭代完成后,buffer0 将存储最终的图像,再利用 Graphics.Blit(buffer0,dest) 把结果显示到屏幕上,并释放缓存。

Shader部分

  1. 声明
    在这里插入图片描述
  2. 分别定义两个Pass使用的顶点着色器
    在这里插入图片描述
  3. 定义两个Pass共用的片元着色器:
    在这里插入图片描述
  4. 定义了高斯模糊使用的两个Pass:
    在这里插入图片描述

最终效果

在这里插入图片描述

Bloom

Bloom 特效是游戏中常见的一种屏幕效果。
这种特效可以模拟真实摄像机的一种图像效果,让画面中较亮的区域“扩散”到周围的区域中,造成一种朦胧的效果。
Bloom 的实现原理非常简单:首先根据一个阈值提取出图像中的较亮区域,把它们存储在一张渲染纹理中,再利用高斯模糊对这张渲染纹理进行模糊处理,模拟光线扩散的效果,最后再将其和原图像进行混合,得到最终的效果。

  1. 声明脚本BloomTest继承自PostEffectsBaseTest
    在这里插入图片描述
  2. Bloom效果是建立在高斯模糊的基础上的,因此提供的参数几乎完全一样,只增加了一个新的参数luminanceThreshold
    在这里插入图片描述
  3. 定义关键的 OnRenderImage 函数
    在这里插入图片描述
    对于Shader部分
  4. 声明属性:
    在这里插入图片描述
  5. 声明各个需要的变量
    在这里插入图片描述
  6. 定义提取较亮区域需要使用的顶点着色器和片元着色器
    在这里插入图片描述
  7. 定义了混合亮部图像和原图像时使用的顶点着色器和片元着色器:
    在这里插入图片描述
  8. 定义了Bloom 效果需要的4个Pass:
    在这里插入图片描述
    需要注意的是,通过 UsePass 语义的Name必须使用大写形式的名字。

最终效果

在这里插入图片描述

运动模糊

运动模糊的实现有多种方法。一种实现方法是利用一块累积缓存(accummulation buffer) 来混合多张连续的图像。当物体快速移动产生多张图像后,取它们之间的平均值作为最后的运动模糊图像。然后这种暴力的方法对性能的消耗很大,因为想要获取多张帧图像往往意味着需要在同一帧里渲染多次场景。
另一种应用广泛的方法是创建和使用速度缓存(velocity buffer),这个缓存中存储了各个像素当前的运动速度,然后利用该值来决定模糊的方向和大小。
本节将使用上述第一种方法的实现来模拟运动模糊的效果。

  1. 声明脚本MotionBlurTest继承自PostEffectsBaseTest
    在这里插入图片描述
  2. 定义运动模糊使用的 OnRenderImage 函数:
    在这里插入图片描述
    Shader 部分
  3. 声明相关属性和参数
    在这里插入图片描述
  4. 顶点与之前章节使用的代码完全一样:
    在这里插入图片描述
  5. 定义两个片元着色器
    在这里插入图片描述
  6. 定义运动模糊所需的Pass
    在这里插入图片描述

最终效果

请添加图片描述

猜你喜欢

转载自blog.csdn.net/weixin_42050609/article/details/124921422