US3S10L9——运动模糊

运动模糊效果

运动模糊效果,是一种用于模拟真实世界中快速移动物体产生的模糊现象的图像处理技术
当一个物体以较高速度移动时,由于人眼或摄像机的曝光时间过长,该物体会在图像中留下模糊的运动轨迹。
这种效果游戏、动画、电影中被广泛应用,以增加视觉真实性和动感。

imageimageimage

运动模糊效果的基本原理

想要在屏幕后期处理中实现运动模糊,一般有两种常用方式:

  1. 累积缓存:物体快速移动时存储多帧图像信息,取它们之间的加权平均值作为最后的运动模糊图像

    • 优点:质量高、效果好
    • 缺点:计算量大,存储开销大
  2. 速度缓存:物体快速移动时存储多帧运动速度信息,利用速度来决定模糊的方向和大小

    • 优点:性能较累积缓存好
    • 缺点:效果较差,可能产生重影和伪影

这里我们使用基于累积缓存来实现动态模糊效果。但是我们不需要像累积缓存中那样存储多张场景信息
而是需要保存之前的渲染结果,不断把当前的渲染图像叠加到之前的渲染图像中,从而产生一种运动轨迹的视觉效果。
相当于是基于累积缓存的优化,性能会更好,但是模糊效果可能略有欠缺,但是效果也是可以接受的。

它的基本原理是:用一个 RenderTexture​ 记录上一次渲染的信息,
然后每一次用新的屏幕图像信息和上一次的图像信息进行混合渲染,从而产生模糊效果(相当于用一张图保留了之前 n 次的叠加渲染结果)

也就是说,这里会保存之前的渲染结果,不断把当前的渲染图像叠加到之前的渲染图像中
通过 RenderTexture​ 来进行保存,用 2 个 Pass​ 来进行混合叠加

  • 一个 Pass​ 混合 RGB 通道,由两张图片根据模糊程度决定最终混合效果
  • 一个 Pass​ 混合 A 通道,由当前屏幕图像的 A 通道来决定

image

在使用 Graphics.Blit(源纹理,目标纹理,材质)​ 方法时,如果目标纹理中包含内容,会直接认为目标纹理中的颜色为颜色缓冲区中的颜色
因此我们完全可以利用该方法,配合 Shader 代码将两张图片信息进行混合处理,从而实现运动模糊效果。它的主要混合思路是:

  1. RGB 通道由两张图片根据模糊程度决定最终效果
  2. A 通道根据当前屏幕图像决定

利用一个模糊程度变量来控制运动模糊程度,值越大模糊程度越强;越小模糊程度越弱, 利用两个Pass进行混合处理的方式:

  • 第一个 Pass​:让 当前屏幕图像 和上一次的屏幕图像 进行指定 RGB 通道的颜色混合

    目的是利用模糊程度参数控制两张图片的混合效果,值越大上一次屏幕内容保留的越多

  • 第二个 Pass​:利用第一个 Pass​ 处理后得到的颜色在和源纹理进行 A 通道的颜色混合

    目的是保留源纹理透明度信息

混合方式设置:

  • 第一个Pass:

    ​Blend SrcAlpha OneMinusSrcAlpha​:(源颜色 * SrcAlpha​) + (目标颜色 * (1 - SrcAlpha​))
    ​ColorMask RGB​:只改变颜色缓冲区中的 RGB 通道

  • 第二个Pass:

    Blend One Zero​:最终颜色 = (源颜色 * 1) + (目标颜色 * 0)
    ColorMask A​:只改变颜色缓冲区中的 A 通道

1
2
3
4
5
6
7
8
9
fixed4 fragRGB(v2f i) : SV_Target
{
return fixed4(tex2D(_MainTex, i.uv).rgb, 模糊程度变量);
}

fixed4 fragA(v2f i) : SV_Target
{
return tex2D(_MainTex, i.uv);
}

实现运动模糊屏幕后期处理效果的Shader

  1. 新建 Shader 命名为运动模糊(MotionBlur​)删除其中无用代码

  2. 属性声明

    • 主纹理:_MainTex
    • 模糊程度:_BlurAmount
    1
    2
    3
    4
    5
    Properties
    {
    _MainTex("Texture", 2D) = "white"{}
    _BlurAmount("BlurAmount", Float) = 0.5 // 模糊程度
    }
  3. 共享CG代码 CGINCLUDE...ENDCG

    • 内置文件 UnityCG.cginc​ 引用
    • 属性映射
    • 结构体(顶点和 UV)
    • 顶点着色器(裁剪空间转换 uv​ 坐标赋值)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    CGINCLUDE

    #include "UnityCG.cginc"

    sampler2D _MainTex;
    fixed _BlurAmount;

    struct v2f
    {
    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;
    };

    v2f vert (appdata_base v)
    {
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = v.texcoord;
    return o;
    }

    ENDCG
  4. 屏幕后处理效果标配

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

    ZTest Always
    Cull Off
    ZWrite Off
  5. 第一个 Pass​(用于混合RGB通道)

    • 混合因子 和 颜色蒙版设置

      Blend SrcAlpha OneMinusSrcAlpha​:(源颜色 * SrcAlpha) + (目标颜色 * (1 - SrcAlpha))
      ColorMask RGB​:只改变颜色缓冲区中的RGB通道

    • 片元着色器
      对主纹理采样后利用模糊程度作为A通道与颜色缓冲区颜色进行混合

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 第一个Pass 用于混合RGB通道
    Pass
    {
    Blend SrcAlpha OneMinusSrcAlpha // 混合方式:(源颜色 * _BlurAmount) + (目标颜色 * (1 - _BlurAmount))
    ColorMask RGB // 只改变颜色缓冲区中的RGB通道

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment fragRGB

    fixed4 fragRGB (v2f i) : SV_Target
    {
    // 将模糊程度变量作为SrcAlpha值,这样来混合源图像颜色和颜色缓冲区的颜色
    return fixed4(tex2D(_MainTex, i.uv).rgb, _BlurAmount);
    }
    ENDCG
    }
  6. 第二个Pass(用户混合A通道)

    • 混合因子 和 颜色蒙版设置

      Blend One Zero​:最终颜色 = (源颜色 * 1) + (目标颜色 * 0)
      ColorMask A​:只改变颜色缓冲区中的A通道

    • 片元着色器

      对主纹理采样

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 第二个Pass 用于混合A通道
    Pass
    {
    Blend One Zero // 混合方式:只使用源颜色
    ColorMask A // 只改变颜色缓冲区中的A通道

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment fragA

    fixed4 fragA (v2f i) : SV_Target
    {
    // 完全保留主纹理的A通道的值,写入到颜色缓冲区内
    return tex2D(_MainTex, i.uv);
    }
    ENDCG
    }
  7. FallBack Off

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

实现运动模糊屏幕后期处理效果的C#脚本

  1. 创建 C# 脚本,名为运动模糊 MotionBlur

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

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

  3. 声明成员属性

    • 公共的模糊程度
    • 私有的堆积纹理 accumulationTexture​(用于存储上一次渲染结果)
  4. 重写 OnRenderImage​ 函数

    1. 若堆积纹理为空或宽高变化,则初始化渲染纹理

      设置其 hideFlags​ 为 HideFlags.HideAndDontSave​(让其不保存)

    2. 设置模糊程度属性

    3. 将源纹理利用材质写入到堆积纹理中(相当于记录本次渲染结果)

    4. 将堆积纹理写入目标纹理中

  5. 组件失活时销毁堆积纹理

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 MotionBlur : PostEffect
{
[Range(0, 0.9f)] public float blurAmount = 0.5f; // 运动模糊(混合)程度(不能超过1)
private RenderTexture accumulationTexture; // 用于存储之前渲染结果的堆积纹理

protected void OnDisable()
{
// 如果堆积纹理在失活前存在,需要手动销毁避免内存泄漏
if (accumulationTexture != null)
{
DestroyImmediate(accumulationTexture);
accumulationTexture = null;
}
}

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

// 当堆积纹理不存在或者和屏幕纹理宽高对不上时,就需要初始化堆积纹理
if (accumulationTexture == null ||
accumulationTexture.width != source.width ||
accumulationTexture.height != source.height)
{
// 如果堆积纹理之前存在,需要手动销毁避免内存泄漏
if (accumulationTexture != null)
{
DestroyImmediate(accumulationTexture);
accumulationTexture = null;
}
accumulationTexture = new RenderTexture(source.width, source.height, 0);
accumulationTexture.hideFlags = HideFlags.HideAndDontSave;
Graphics.Blit(source, accumulationTexture); // 保证第一次累积纹理中也是有内容的,因为之后其颜色会作为颜色缓冲区的颜色
}

// 在Shader中_BlurAmount作为透明度,越大堆积纹理越不明显,需要 1 - 模糊程度,来实现模糊程度越大运动模糊越明显
PostEffectMaterial.SetFloat("_BlurAmount", 1f - blurAmount);
// 将目前的屏幕内容混合到堆积纹理内,让堆积纹理先记录当前渲染结果
Graphics.Blit(source, accumulationTexture, PostEffectMaterial);
Graphics.Blit(accumulationTexture, destination); // 将堆积纹理写入到目标纹理内
}
}

显示效果(模糊程度0.9,上图为右边方块快速向右移动,下图为摄像机快速逼近各个物体):

image

image

可以看到,当场景内的物体移动时,因为保留了之前的渲染结果混合到目前的这一帧,就显示出了运动模糊的效果