US3S10L7——高斯模糊

本章代码关键字

1
2
CGINCLUDE        // CG共享代码关键字
ENDCG // CG共享代码结束关键字
1
2
RenderTexture.GetTemporary()            // 申请中间纹理缓存
RenderTexture.ReleaseTemporary() // 释放中间纹理缓存

卷积回顾

卷积就是利用一个 N*N 的卷积核(滤波核),和图像中目标像素及其周围像素信息进行对位相乘后相加的结果
之前学习的边缘检测,只是卷积在图形学中的其中一种应用而已,它还可以用来完成其他效果的制作

注意:

  1. 卷积核一般为 奇数*奇数 大小,主要原因是需要进行中心对齐以及保持对称性

  2. 卷积核在图像边缘计算时,会缺少周围像素,常见处理方法有零填充、镜像填充、循环填充

    在 UV 坐标中,卷积核在边缘计算时,周围像素如何采集取决于贴图的平铺设置,详见:U3L2-4——参数设置—平铺拉伸

高斯模糊效果

高斯模糊效果,是一种用于平滑图像并减少图像噪声和细节的图像处理技术
高斯模糊的主要目的是使图像的边缘和细节变得模糊和平滑
高斯模糊相当于利用 Shader 代码自动给屏幕图像进行模糊处理

image

高斯模糊基本原理

一句话概括 Unity Shader 中实现高斯效果的基本原理:
高斯模糊是利用 高斯函数 计算出 高斯滤波核 中每个元素并进行归一化处理后,再和目标像素通过卷积计算后得到最终的效果

关键知识点:高斯函数,高斯滤波核,归一化处理

注意:高斯滤波核中的数值是定死的规则,可以直接写死参与计算,不要在代码中用高斯函数计算,会浪费性能!!!

高斯滤波核

高斯滤波核也称为高斯核,它其实就是一个 NN 的卷积核,它的大小可以自定义,但是一般会是一个 奇数奇数 的大小
通常会是一个 33 、55、77、99 的大小,滤波核越大,模糊效果越明显

从效果和效率综合考虑,我们通常会使用 5*5 的大小,高斯滤波核中各元素的具体值,我们通过高斯函数来确定

高斯函数

高斯函数 是由德国数学家和物理学家 卡尔·弗里德里希·高斯(Carl Friedrich Gauss)提出的。
我们将使用二维高斯函数(也就是 二维正态分布)来计算高斯模糊效果中的卷积核:

G(x,y)=12πσ2ex2+y22σ2G(x,y)=\frac{1}{2 \pi \sigma^2}e^{-\frac{x^2+y^2}{2\sigma^2}}

其中:

  • σ\sigma 是标准方差,一般取值为 1 即可
  • xxyy 是相对于高斯核中心的整数距离
  • ee 是自然对数的底 2.71828\approx 2.71828
  • π\pi 是圆周率 3.14159\approx 3.14159

因此一个 5*5 的高斯卷积核计算下来就是这样的:

imageimage

归一化处理

刚才利用高斯函数得到的高斯滤波核,我们并没有对它进行归一化处理
所谓归一化处理就是让该卷积核中,各元素值 除以 所有元素总和,使其所有元素的和为1
这样是为了避免在卷积计算过程中引入额外的亮度变化或偏差

举例:假设一个 3*3 的卷积核,如果不进行归一化,卷积会将图像的亮度放大 9 倍,归一化后卷积核为 1,图像亮度将保持不变

imageimage

最终,利用计算出来的这个高斯滤波核(高斯核或卷积核或滤波核)和对应的像素点进行卷积计算,便可以得到最终该像素高斯模糊后的结果

高斯模糊效果的计算公式优化

潜在问题抛出:如果我们直接基于它的基本原理进行计算,计算效率是较低的
因为对于一张 长 W,宽 H 的图像,会进行 5*5*W*H 次纹理采样的颜色计算

为了降低计算次数,我们可以利用二维高斯函数的数学特性—— 可分离性
即二维高斯函数可以表示为两个一维高斯函数(即正态分布)的乘积,从而大幅减少计算量

G(x,y)=12πσ2ex2+y22σ2=G(x)G(y)=12πσex22σ212πσey22σ2G(x,y) = \frac{1}{2\pi\sigma^2}e^{-\frac{x^2+y^2}{2\sigma^2}} = G(x) \cdot G(y) = \frac{1}{\sqrt{2\pi}\sigma}e^{-\frac{x^2}{2\sigma^2}} \cdot \frac{1}{\sqrt{2\pi}\sigma}e^{-\frac{y^2}{2\sigma^2}}

G(x)=12πσex22σ2G(y)=12πσey22σ2\begin{align*} G(x) & = \frac{1}{\sqrt{2\pi}\sigma}e^{-\frac{x^2}{2\sigma^2}} \\ G(y) & = \frac{1}{\sqrt{2\pi}\sigma}e^{-\frac{y^2}{2\sigma^2}} \end{align*}

G(x)G(x)G(y)G(y) 可以分别代表沿 xx 轴和 yy 轴的一维高斯函数,
我们只需要让每个像素分别与 G(x)G(x) 进行水平卷积计算,与 G(y)G(y) 进行垂直卷积计算,
再将最终的计算结果相乘即可得出和之前一样的结果

因此我们可以利用这两个一维高斯函数,得到相同的卷积核结果,一个是水平方向的,一个是竖直方向的,但是内容一致

imageimage

也就是说,我们可以利用计算出来的这两个一维高斯滤波核(高斯核或卷积核或滤波核)和对应的像素点进行卷积计算,
得到水平方向上的结果和垂直方向上的结果,再将两个结果相乘,便可以得到最终该像素高斯模糊后的结果

image​​imageimage

通过对计算公式的优化,我们将原本需要 N*N*W*H 的计算次数(N为高斯核大小)
优化为了 2*N*W*H,相当于将时间复杂度从 O(n2)O(n^2) 降低到了 O(n)O(n)

利用两个一维高斯滤波核来计算高斯模糊时,对于 5*5 的滤波核,
它其中的元素,不管是水平还是竖直方向,都是 (0.0545, 0.2442, 0.4026, 0.2442, 0.0545)
而其中主要的数值就三个:0.4026、0.2442、0.0545
因此我们实际上只需要记录这三个值即可,卷积计算时将其放在对应的位置上即可

高斯模糊效果的计算方式优化

刚才的知识点中提到,如果想要图片越模糊,那么需要扩大高斯滤波核的大小,越大越模糊,
但是同样通过刚才的基础原理和计算公式优化的讲解,也能够感受到,
如果通过扩大高斯滤波核的大小来达到更模糊的目的,付出的代价就是会更加消耗性能。

因此在 Shader 中我们不会提供控制高斯滤波核大小的参数,我们的滤波核始终会使用 5*5 大小的
因此,我们就只能使用其他方式来控制模糊程度了,我们一般会使用以下三种方式:

  1. 控制缩放纹理大小

    高斯模糊的目的是让源纹理看起来模糊,那么我们完全可以缩放源纹理,
    Shader 从更小的主纹理中进行采样,尺寸小了,自然计算的也少,也能更模糊

  2. 控制模糊代码执行次数

    我们可以在 OnRenderImage​ 中多次执行 Shader 代码在模糊的基础上更加模糊,对源纹理,进行多次迭代模糊处理

  3. 控制纹理采样间隔距离

    我们在 Shader 中进行 uv 采样时,可以自己控制采样像素的间隔位置,而不是只以一个单位来计算间隔,
    具体间隔几个单位,我们可以自定义,通过这种方式也能增加模糊程度

    也就是说,在高斯滤波核对周围像素采样时,原本需要偏移1个和2个像素,这时我们控制可以偏移2个和4个像素,甚至更大

通过以上三种方式,而没有通过改变高斯滤波核的大小来控制最终的模糊程度,虽然这样并不符合高斯模糊的理论
但是这样更加的高效简单,灵活性也更强,效果也是可以接受的

这样更加印证了,计算机图形学中,只要最终的效果是好的,那么不必严格遵循数学和物理规则,
我们应该更多的从效果优先、性能优先、开发效率优先的方向去解决问题

高斯模糊效果基础实现

知识补充 —— CG封装共享代码

在实现高斯模糊效果时,我们将在 Shader 中利用两个 Pass​ 来分别计算水平卷积和竖直卷积
而两个 Pass​ 会存在相同的代码,因此我们将使用一个新的预处理指令,它写在 SubShader​ 语句块中,Pass​ 外

1
2
3
4
5
6
7
8
9
10
SubShader
{
CGINCLUDE
// ....
// 中间包裹CG代码
// ...
ENDCG

Pass {/*...*/}
}

它的作用是用于封装共享代码,可以在其中定义常量、函数、结构体、宏等等内容
这些封装起来的代码可以在同一个 Shader 文件中的多个 Pass 中使用,也可以在其他 Shader 文件中引用
使用它可以避免我们重复编写一些相同的代码,从而提高代码复用性和可维护性

实现高斯模糊基础效果的制作思路

在 Shader 中写两个 Pass

  • 一个 Pass​ 用来计算 水平方向卷积

  • 一个 Pass​ 用来计算 竖直方向卷积

  • 两个Pass的区别:

    顶点着色器中计算的 uv​ 偏移位置不同,一个水平偏移,一个竖直偏移

  • 两个Pass的共同点:

    1. 使用的内置文件相同
    2. 使用的属性相同
    3. 片元着色器的计算规则可以相同

    我们可以用 uv​ 数组存储 5 个像素的UV坐标偏移,数组中存储的像素 UV 偏移分别为:

    数组索引 0 1 2 3 4
    x 或 y 偏移 0 1 -1 2 -2

    其中:

    • 第 0 个元素 对应的高斯核元素为:0.4026
    • 第 1,2 个元素 对应的高斯核元素为:0.2442
    • 第 3,4 个元素 对应的高斯核元素为:0.0545

    因此,使用这种存储顺序,不管竖直还是水平可以统一一套计算规则进行计算

实现高斯模糊基础屏幕后期处理效果Shader

  1. 新建 Shader,取名为高斯模糊 GaussianBlur​,删除无用代码

  2. 声明属性

    • 主纹理 _MainTex
    1
    2
    3
    4
    Properties
    {
    _MainTex("Texture", 2D) = "white"{}
    }
  3. 利用补充知识 CGINCLUDE ... ENDCG​,实现两个Pass共享的代码

    1. 内置文件引用

    2. 属性映射,注意映射纹素,需要用于 uv​ 偏移计算

      1
      2
      3
      4
      #include "UnityCG.cginc"

      sampler2D _MainTex; // 主纹理
      half4 _MainTex_TexelSize; // 主纹理的纹素
    3. 结构体

      顶点和 uv​ 数组(用于存储5个像素的 uv​ 偏移)

      1
      2
      3
      4
      5
      struct v2f
      {
      half2 uv[5] : TEXCOORD0; // 5个像素的uv坐标偏移
      float4 vertex : SV_POSITION; // 顶点在裁剪空间下的坐标
      };
    4. 两个顶点着色器函数

      1. 一个水平偏移采样
      2. 一个竖直偏移采样
      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
      // 水平方向上的顶点着色器函数
      v2f vertBlurHorizontal(appdata_base v)
      {
      v2f o;
      o.vertex = UnityObjectToClipPos(v.vertex); // 将顶点转换到裁剪空间下
      // 5个像素的水平方向上的偏移
      half2 uv = v.texcoord;
      o.uv[0] = uv;
      o.uv[1] = uv + half2(_MainTex_TexelSize.x, 0);
      o.uv[2] = uv - half2(_MainTex_TexelSize.x, 0);
      o.uv[3] = uv + half2(_MainTex_TexelSize.x * 2, 0);
      o.uv[4] = uv - half2(_MainTex_TexelSize.x * 2, 0);

      return o;
      }

      // 垂直方向上的顶点着色器函数
      v2f vertBlurVertical(appdata_base v)
      {
      v2f o;
      o.vertex = UnityObjectToClipPos(v.vertex); // 将顶点转换到裁剪空间下
      // 5个像素的垂直方向上的偏移
      half2 uv = v.texcoord;
      o.uv[0] = uv;
      o.uv[1] = uv + half2(0, _MainTex_TexelSize.y);
      o.uv[2] = uv - half2(0, _MainTex_TexelSize.y);
      o.uv[3] = uv + half2(0, _MainTex_TexelSize.y * 2);
      o.uv[4] = uv - half2(0, _MainTex_TexelSize.y * 2);

      return o;
      }
    5. 片元着色器

      共同的卷积计算方式,对位相乘后相加

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      // 片元着色器函数,两个Pass可以共用
      fixed4 fragBlur(v2f i) : SV_Target
      {
      float weight[3] = { 0.4026, 0.2442, 0.0545 }; // 两个一维高斯卷积核中会使用的三个数
      fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0]; // 先计算当前像素点颜色
      for (int index = 1; index < 3; index++)
      {
      sum += tex2D(_MainTex, i.uv[index * 2 - 1]).rgb * weight[index]; // 取左(上)边的元素,在uv数组内索引1,3
      sum += tex2D(_MainTex, i.uv[index * 2]).rgb * weight[index]; // 取右(下)边的元素,在uv数组内索引2,4
      }
      return fixed4(sum, 1);
      }
  4. 屏幕后处理效果标配

    1. ZTest Always
    2. Cull Off
    3. ZWrite Off
    1
    2
    3
    4
    5
    Tags { "RenderType"="Opaque" }

    ZTest Always
    Cull Off
    ZWrite Off
  5. 实现两个 Pass

    主要用编译指令指明顶点和片元着色器调用的函数即可
    这里的为两个 Pass​ 命名,方便后续其他 Shader 复用,例如:US3S10L8——Bloom效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 水平方向上的Pass
    Pass
    {
    Name "GAUSSIAN_BLUR_HORIZONTAL"

    CGPROGRAM
    #pragma vertex vertBlurHorizontal
    #pragma fragment fragBlur
    ENDCG
    }

    // 垂直方向上的Pass
    Pass
    {
    Name "GAUSSIAN_BLUR_VERTICAL"

    CGPROGRAM
    #pragma vertex vertBlurVertical
    #pragma fragment fragBlur
    ENDCG
    }
  6. FallBack Off

    这里不需要后备 Shader,因为如果 Shader 不支持就直接不执行了

完整 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
Shader "PostEffect/GaussianBlur"
{
Properties
{
_MainTex("Texture", 2D) = "white"{}
}

SubShader
{
// 用于包裹共用代码,在之后的多个Pass中都可以复用的代码
CGINCLUDE

#include "UnityCG.cginc"

sampler2D _MainTex; // 主纹理
half4 _MainTex_TexelSize; // 主纹理的纹素

struct v2f
{
half2 uv[5] : TEXCOORD0; // 5个像素的uv坐标偏移
float4 vertex : SV_POSITION; // 顶点在裁剪空间下的坐标
};

// 水平方向上的顶点着色器函数
v2f vertBlurHorizontal(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex); // 将顶点转换到裁剪空间下
// 5个像素的水平方向上的偏移
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + half2(_MainTex_TexelSize.x, 0);
o.uv[2] = uv - half2(_MainTex_TexelSize.x, 0);
o.uv[3] = uv + half2(_MainTex_TexelSize.x * 2, 0);
o.uv[4] = uv - half2(_MainTex_TexelSize.x * 2, 0);

return o;
}

// 垂直方向上的顶点着色器函数
v2f vertBlurVertical(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex); // 将顶点转换到裁剪空间下
// 5个像素的垂直方向上的偏移
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + half2(0, _MainTex_TexelSize.y);
o.uv[2] = uv - half2(0, _MainTex_TexelSize.y);
o.uv[3] = uv + half2(0, _MainTex_TexelSize.y * 2);
o.uv[4] = uv - half2(0, _MainTex_TexelSize.y * 2);

return o;
}

// 片元着色器函数,两个Pass可以共用
fixed4 fragBlur(v2f i) : SV_Target
{
float weight[3] = { 0.4026, 0.2442, 0.0545 }; // 两个一维高斯卷积核中会使用的三个数
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0]; // 先计算当前像素点颜色
for (int index = 1; index < 3; index++)
{
sum += tex2D(_MainTex, i.uv[index * 2 - 1]).rgb * weight[index]; // 取左(上)边的元素,在uv数组内索引1,3
sum += tex2D(_MainTex, i.uv[index * 2]).rgb * weight[index]; // 取右(下)边的元素,在uv数组内索引2,4
}
return fixed4(sum, 1);
}

ENDCG

Tags { "RenderType"="Opaque" }

ZTest Always
Cull Off
ZWrite Off

// 水平方向上的Pass
Pass
{
Name "GAUSSIAN_BLUR_HORIZONTAL"

CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}

// 垂直方向上的Pass
Pass
{
Name "GAUSSIAN_BLUR_VERTICAL"

CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
}
}

知识补充 —— 执行两次 Pass 时存储缓存中间纹理

由于我们需要用两个 Pass​ 对图像进行处理,
相当于先让捕获的图像进行水平卷积计算得到一个结果,再用这个结果进行竖直卷积计算得到最终结果
因此我们需要利用 Graphics.Blit​ 进行两次 Pass​ 代码的执行

如果一次使用两个 Pass​ 来卷积图像,则竖直卷积的图像是不会基于水平卷积的结果而是基于原始图像去计算的,这样的结果肯定是有问题
因此,我们必须要依次使用两个 Pass​,这样才能让垂直卷积的图像是基于水平卷积的结果去计算的

所以我们需要一个中间纹理缓存区,用于记录中间的处理结果,我们需要用到 RenderTexture.GetTemporary()​ 方法
它的作用是获取一个临时的 RenderTexture​ 对象,我们可以利用它来存储中间结果,我们使用它传入 3 个参数的重载

1
public static RenderTexture GetTemporary(int width, int height, int depthBuffer);
  • 参数一:纹理宽
  • 参数二:纹理高
  • 参数三:深度缓冲 —— 一般填0即可

需要注意的是,使用该方法返回的 RenderTexture​ 对象,需要配合使用 RenderTexture.ReleaseTemporary()​ 方法来释放该对象缓存

1
2
3
4
5
6
void OnRenderImage(RenderTexture source, RenderTexture destination)
{
Graphics.Blit(source, destination);
RenderTexture buffer = RenderTexture.GetTemporary(source.width, source.height, 0);
RenderTexture.ReleaseTemporary(buffer);
}

实现高斯模糊基础屏幕后期处理效果C#脚本

  1. 创建C#脚本,名为高斯模糊 GaussianBlur

  2. 继承屏幕后处理基类 PostEffect

    具体代码详见:US3S10L4——屏幕后处理基类

  3. 重写 OnRenderImage​ 函数

  4. 在其中利用 Graphics.Blit()​、RenderTexture.GetTemporary()​,RenderTexture.ReleaseTemporary()​,函数对纹理进行两次 Pass​ 处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using UnityEngine;

public class GaussianBlur : PostEffect
{
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
base.UpdateProperty();

if (PostEffectMaterial == null)
{
Graphics.Blit(source, destination);
return;
}

RenderTexture buffer = RenderTexture.GetTemporary(source.width, source.height, 0);
Graphics.Blit(source, buffer, PostEffectMaterial, 0); // 第一次卷积计算(水平卷积)
Graphics.Blit(buffer, destination, PostEffectMaterial, 1); // 第二次卷积计算(垂直卷积),基于上次卷积的Color相乘计算
RenderTexture.ReleaseTemporary(buffer);
}
}

显示效果:

  • 原始图像:

    image-20250105001900-zt1x9tp

  • 模糊效果:

    QQ20250105-144022

可见,图片有了高斯模糊效果,但是目前我们不能控制模糊程度

高斯模糊效果完整实现

目前的高斯模糊还不能控制模糊程度,想要图片变模糊,那么需要扩大高斯滤波核的大小,越大越模糊
如果通过扩大高斯滤波核的大小来达到更模糊的目的,付出的代价就是会更加消耗性能
因此在 Shader 中我们不会提供控制高斯滤波核大小的参数,我们的滤波核始终会使用 5*5 大小的
因此,我们就只能使用其他方式来控制模糊程度了,我们一般会使用以下三种方式:

  1. 控制缩放纹理大小
  2. 控制模糊代码执行次数
  3. 控制纹理采样间隔距离

通过以上三种方式,而没有通过改变高斯滤波核的大小来控制最终的模糊程度,
虽然这样并不符合高斯模糊的理论,但是这样更加的高效简单,灵活性也更强,效果也是可以接受的

这样更加印证了,计算机图形学中,只要最终的效果是好的,那么不必严格遵循数学和物理规则,
我们应该更多的从效果优先、性能优先、开发效率优先的方向去解决问题

添加控制纹理大小参数

在高斯模糊效果的 C# 代码中加入一个控制缩放的参数,它主要是用来降低采样质量的,因此取名叫 downSample​(降低采样)

如何使用该参数:
OnRenderImage​ 函数中,我们使用 RenderTexture.GetTemporary()​ 获取渲染纹理缓存区时
用源纹理尺寸除以 downSample​,这样在调用 Graphics.Blit()​ 进行图像复制处理时
相当于就将源纹理缩小了,同时在缩小的过程过程中还会用材质球进行效果处理

设置缩放过滤模式

在进行复制处理之前,我们可以设置渲染纹理缓存对象的缩放过滤模式

1
renderTexture.filterMode = FilterMode.Bilinear;
  • FilterMode.Point​:点过滤。不进行插值。每个像素都直接从最近的纹理像素获取颜色
  • FilterMode.Bilinear​:双线性过滤。它在纹理采样时使用相邻四个纹理像素的加权平均值进行插值,以生成更平滑的图像
  • FilterMode.Trilinear​:三线性过滤。它在双线性过滤的基础上增加了在不同 MIP 贴图级别之间的插值。
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
using UnityEngine;

public class GaussianBlur : PostEffect
{
[Range(1f, 8f)] public int downSample = 1;

protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
base.UpdateProperty();

if (PostEffectMaterial == null)
{
Graphics.Blit(source, destination);
return;
}

// 对原纹理降低采样
int renderTextureW = source.width / downSample;
int renderTextureH = source.height / downSample;

RenderTexture buffer = RenderTexture.GetTemporary(renderTextureW, renderTextureH, 0);
buffer.filterMode = FilterMode.Bilinear; // 采用双线性过滤模式来缩放,可以让缩放效果更平滑
Graphics.Blit(source, buffer, PostEffectMaterial, 0); // 第一次卷积计算(水平卷积)
Graphics.Blit(buffer, destination, PostEffectMaterial, 1); // 第二次卷积计算(垂直卷积),基于上次卷积的Color相乘计算
RenderTexture.ReleaseTemporary(buffer);
}
}

显示效果(设置降低采样程度为4):

QQ20250105-145710

可见,图片变得更模糊了,而且计算量也变得更少了,但如果降低采样程度太高,则图像会呈现颗粒感且会变重

添加控制模糊代码执行次数参数

在高斯模糊效果的 C# 代码中加入一个控制模糊代码执行次数的参数,
它主要是用来多次执行材质球中的两个Pass​,因此取名叫 iteration​(迭代)

如何使用该参数:在 OnRenderImage​ 函数中,我们使用一个 for​ 循环,来对原图像进行多次高斯模糊效果处理

注意:要保证每次使用完 RenderTexture.GetTemporary()​ 分配的缓存区,都要使用 RenderTexture.ReleaseTemporary()​ 函数将其释放

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
using UnityEngine;

public class GaussianBlur : PostEffect
{
[Range(1, 8)] public int downSample = 1; // 降低采样程度
[Range(1, 8)] public int iterations = 1; // 迭代次数

protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
base.UpdateProperty();

if (PostEffectMaterial == null)
{
Graphics.Blit(source, destination);
return;
}

// 对原纹理降低采样
int renderTextureW = source.width / downSample;
int renderTextureH = source.height / downSample;
RenderTexture buffer = RenderTexture.GetTemporary(renderTextureW, renderTextureH, 0);
buffer.filterMode = FilterMode.Bilinear; // 采用双线性过滤模式来缩放,可以让缩放效果更平滑
Graphics.Blit(source, buffer); // 将缩放结果存储到缓存纹理中

// 高斯模糊迭代
for (int i = 0; i < iterations; i++)
{
// 第一次卷积计算(水平卷积)
RenderTexture iterationBuffer = RenderTexture.GetTemporary(renderTextureW, renderTextureH, 0);
Graphics.Blit(buffer, iterationBuffer, PostEffectMaterial, 0);
RenderTexture.ReleaseTemporary(buffer); // 释放使用完毕的缓存
// 第二次卷积计算(垂直卷积),基于上次卷积的Color相乘计算
buffer = iterationBuffer; // 使用水平卷积完毕的迭代缓存
iterationBuffer = RenderTexture.GetTemporary(renderTextureW, renderTextureH, 0);
Graphics.Blit(buffer, iterationBuffer, PostEffectMaterial, 1);

RenderTexture.ReleaseTemporary(buffer); // 释放使用完毕的缓存
buffer = iterationBuffer; // 使用卷积完毕的缓存
}

Graphics.Blit(buffer, destination);
RenderTexture.ReleaseTemporary(buffer); // 释放使用完毕的缓存
}
}

显示效果(设置迭代次数为8):

QQ20250105-152014

可见,迭代次数越高,图像越模糊,但如果降低迭代程度太高,会严重影响性能,
因此最好配合上文的降低采样程度使用,降低采样程度可以降低迭代的计算量

添加控制纹理采样间隔距离

在高斯模糊效果的 Shader 代码中加入一个控制纹理采样间隔距离的属性
它主要是用来控制间隔多少单位偏移 uv​ 坐标,因此取名叫 _BlurSpread​(模糊半径)

如何使用该参数:在顶点着色器进行 uv​ 坐标偏移时,乘以该属性,可以通过它控制偏移的多少

注意:理论上来说像素是 1 个单位 1 个单位偏移的,_BlurSpread​ 应该为整数变化
但是为了更精细的控制模糊程度,我们可以让其为小数,小数变化可以更细微的调整模糊程序

1
2
3
4
5
Properties
{
_MainTex("Texture", 2D) = "white"{}
_BlurSpread("BlurSpread", Float) = 1
}
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
sampler2D _MainTex;         // 主纹理
half4 _MainTex_TexelSize; // 主纹理的纹素
float _BlurSpread; // 纹理偏移间隔单位

struct v2f
{
half2 uv[5] : TEXCOORD0; // 5个像素的uv坐标偏移
float4 vertex : SV_POSITION; // 顶点在裁剪空间下的坐标
};

// 水平方向上的顶点着色器函数
v2f vertBlurHorizontal(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex); // 将顶点转换到裁剪空间下
// 5个像素的水平方向上的偏移
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + half2(_MainTex_TexelSize.x, 0) * _BlurSpread;
o.uv[2] = uv - half2(_MainTex_TexelSize.x, 0) * _BlurSpread;
o.uv[3] = uv + half2(_MainTex_TexelSize.x * 2, 0) * _BlurSpread;
o.uv[4] = uv - half2(_MainTex_TexelSize.x * 2, 0) * _BlurSpread;

return o;
}

// 垂直方向上的顶点着色器函数
v2f vertBlurVertical(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex); // 将顶点转换到裁剪空间下
// 5个像素的垂直方向上的偏移
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + half2(0, _MainTex_TexelSize.y) * _BlurSpread;
o.uv[2] = uv - half2(0, _MainTex_TexelSize.y) * _BlurSpread;
o.uv[3] = uv + half2(0, _MainTex_TexelSize.y * 2) * _BlurSpread;
o.uv[4] = uv - half2(0, _MainTex_TexelSize.y * 2) * _BlurSpread;

return o;
}

然后,在 C# 脚本内添加对应的属性,并用来控制 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
using UnityEngine;

public class GaussianBlur : PostEffect
{
[Range(1, 8)] public int downSample = 1; // 降低采样程度
[Range(1, 8)] public int iterations = 1; // 迭代次数
[Range(0f, 3f)] public float blurSpread = 1f; // 模糊半径

protected override void UpdateProperty()
{
if (PostEffectMaterial != null)
{
PostEffectMaterial.SetFloat("_BlurSpread", blurSpread);
}
}

protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (PostEffectMaterial == null)
{
Graphics.Blit(source, destination);
return;
}

UpdateProperty();

// 对原纹理降低采样
int renderTextureW = source.width / downSample;
int renderTextureH = source.height / downSample;
RenderTexture buffer = RenderTexture.GetTemporary(renderTextureW, renderTextureH, 0);
buffer.filterMode = FilterMode.Bilinear; // 采用双线性过滤模式来缩放,可以让缩放效果更平滑
Graphics.Blit(source, buffer); // 将缩放结果存储到缓存纹理中

// 高斯模糊迭代
for (int i = 0; i < iterations; i++)
{
// 第一次卷积计算(水平卷积)
RenderTexture iterationBuffer = RenderTexture.GetTemporary(renderTextureW, renderTextureH, 0);
Graphics.Blit(buffer, iterationBuffer, PostEffectMaterial, 0);
RenderTexture.ReleaseTemporary(buffer); // 释放使用完毕的缓存
// 第二次卷积计算(垂直卷积),基于上次卷积的Color相乘计算
buffer = iterationBuffer; // 使用水平卷积完毕的迭代缓存
iterationBuffer = RenderTexture.GetTemporary(renderTextureW, renderTextureH, 0);
Graphics.Blit(buffer, iterationBuffer, PostEffectMaterial, 1);

RenderTexture.ReleaseTemporary(buffer); // 释放使用完毕的缓存
buffer = iterationBuffer; // 使用卷积完毕的缓存
}

Graphics.Blit(buffer, destination);
RenderTexture.ReleaseTemporary(buffer); // 释放使用完毕的缓存
}
}

显示效果(模糊半径为3):

QQ20250105-154618

关于模糊半径的的使用,除了上文的那种写法以外,还可以让模糊半径随着迭代次数的增加而增加,使得模糊半径影响效果更强烈

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
using UnityEngine;

public class GaussianBlur : PostEffect
{
[Range(1, 8)] public int downSample = 1; // 降低采样程度
[Range(1, 8)] public int iterations = 1; // 迭代次数
[Range(0f, 3f)] public float blurSpread = 1f; // 模糊半径

protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (PostEffectMaterial == null)
{
Graphics.Blit(source, destination);
return;
}

UpdateProperty();

// 对原纹理降低采样
int renderTextureW = source.width / downSample;
int renderTextureH = source.height / downSample;
RenderTexture buffer = RenderTexture.GetTemporary(renderTextureW, renderTextureH, 0);
buffer.filterMode = FilterMode.Bilinear; // 采用双线性过滤模式来缩放,可以让缩放效果更平滑
Graphics.Blit(source, buffer); // 将缩放结果存储到缓存纹理中

// 高斯模糊迭代
for (int i = 0; i < iterations; i++)
{
// 模糊半径随着迭代次数增加而增加
PostEffectMaterial.SetFloat("_BlurSpread", 1 + i * blurSpread);

// 第一次卷积计算(水平卷积)
RenderTexture iterationBuffer = RenderTexture.GetTemporary(renderTextureW, renderTextureH, 0);
Graphics.Blit(buffer, iterationBuffer, PostEffectMaterial, 0);
RenderTexture.ReleaseTemporary(buffer); // 释放使用完毕的缓存
// 第二次卷积计算(垂直卷积),基于上次卷积的Color相乘计算
buffer = iterationBuffer; // 使用水平卷积完毕的迭代缓存
iterationBuffer = RenderTexture.GetTemporary(renderTextureW, renderTextureH, 0);
Graphics.Blit(buffer, iterationBuffer, PostEffectMaterial, 1);

RenderTexture.ReleaseTemporary(buffer); // 释放使用完毕的缓存
buffer = iterationBuffer; // 使用卷积完毕的缓存
}

Graphics.Blit(buffer, destination);
RenderTexture.ReleaseTemporary(buffer); // 释放使用完毕的缓存
}
}

显示效果(模糊半径为3,迭代次数为4):

QQ20250105-155106

可见,模糊半径配合迭代次数能够更好的实现模糊效果

完整高斯模糊效果代码

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
Shader "PostEffect/GaussianBlur"
{
Properties
{
_MainTex("Texture", 2D) = "white"{}
_BlurSpread("BlurSpread", Float) = 1 // 模糊半径
}

SubShader
{
// 用于包裹共用代码,在之后的多个Pass中都可以复用的代码
CGINCLUDE

#include "UnityCG.cginc"

sampler2D _MainTex; // 主纹理
half4 _MainTex_TexelSize; // 主纹理的纹素
float _BlurSpread; // 纹理偏移间隔单位

struct v2f
{
half2 uv[5] : TEXCOORD0; // 5个像素的uv坐标偏移
float4 vertex : SV_POSITION; // 顶点在裁剪空间下的坐标
};

// 水平方向上的顶点着色器函数
v2f vertBlurHorizontal(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex); // 将顶点转换到裁剪空间下
// 5个像素的水平方向上的偏移
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + half2(_MainTex_TexelSize.x, 0) * _BlurSpread;
o.uv[2] = uv - half2(_MainTex_TexelSize.x, 0) * _BlurSpread;
o.uv[3] = uv + half2(_MainTex_TexelSize.x * 2, 0) * _BlurSpread;
o.uv[4] = uv - half2(_MainTex_TexelSize.x * 2, 0) * _BlurSpread;

return o;
}

// 垂直方向上的顶点着色器函数
v2f vertBlurVertical(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex); // 将顶点转换到裁剪空间下
// 5个像素的垂直方向上的偏移
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + half2(0, _MainTex_TexelSize.y) * _BlurSpread;
o.uv[2] = uv - half2(0, _MainTex_TexelSize.y) * _BlurSpread;
o.uv[3] = uv + half2(0, _MainTex_TexelSize.y * 2) * _BlurSpread;
o.uv[4] = uv - half2(0, _MainTex_TexelSize.y * 2) * _BlurSpread;

return o;
}

// 片元着色器函数,两个Pass可以共用
fixed4 fragBlur(v2f i) : SV_Target
{
float weight[3] = { 0.4026, 0.2442, 0.0545 }; // 两个一维高斯卷积核中会使用的三个数
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0]; // 先计算当前像素点颜色
for (int index = 1; index < 3; index++)
{
sum += tex2D(_MainTex, i.uv[index * 2 - 1]).rgb * weight[index]; // 取左(上)边的元素,在uv数组内索引1,3
sum += tex2D(_MainTex, i.uv[index * 2]).rgb * weight[index]; // 取右(下)边的元素,在uv数组内索引2,4
}
return fixed4(sum, 1);
}

ENDCG

Tags { "RenderType"="Opaque" }

ZTest Always
Cull Off
ZWrite Off

// 水平方向上的Pass
Pass
{
Name "GAUSSIAN_BLUR_HORIZONTAL"

CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}

// 垂直方向上的Pass
Pass
{
Name "GAUSSIAN_BLUR_VERTICAL"

CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
}
}

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
using UnityEngine;

public class GaussianBlur : PostEffect
{
[Range(1, 8)] public int downSample = 1; // 降低采样程度
[Range(1, 8)] public int iterations = 1; // 迭代次数
[Range(0f, 3f)] public float blurSpread = 1f; // 模糊半径

protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (PostEffectMaterial == null)
{
Graphics.Blit(source, destination);
return;
}

UpdateProperty();

// 对原纹理降低采样
int renderTextureW = source.width / downSample;
int renderTextureH = source.height / downSample;
RenderTexture buffer = RenderTexture.GetTemporary(renderTextureW, renderTextureH, 0);
buffer.filterMode = FilterMode.Bilinear; // 采用双线性过滤模式来缩放,可以让缩放效果更平滑
Graphics.Blit(source, buffer); // 将缩放结果存储到缓存纹理中

// 高斯模糊迭代
for (int i = 0; i < iterations; i++)
{
// 模糊半径随着迭代次数增加而增加
PostEffectMaterial.SetFloat("_BlurSpread", 1 + i * blurSpread);

// 第一次卷积计算(水平卷积)
RenderTexture iterationBuffer = RenderTexture.GetTemporary(renderTextureW, renderTextureH, 0);
Graphics.Blit(buffer, iterationBuffer, PostEffectMaterial, 0);
RenderTexture.ReleaseTemporary(buffer); // 释放使用完毕的缓存
// 第二次卷积计算(垂直卷积),基于上次卷积的Color相乘计算
buffer = iterationBuffer; // 使用水平卷积完毕的迭代缓存
iterationBuffer = RenderTexture.GetTemporary(renderTextureW, renderTextureH, 0);
Graphics.Blit(buffer, iterationBuffer, PostEffectMaterial, 1);

RenderTexture.ReleaseTemporary(buffer); // 释放使用完毕的缓存
buffer = iterationBuffer; // 使用卷积完毕的缓存
}

Graphics.Blit(buffer, destination);
RenderTexture.ReleaseTemporary(buffer); // 释放使用完毕的缓存
}
}