后处理

Unity 中常见的后处理方法 :

  • OnRenderImage 是一个摄像机事件,在渲染完成后调用。这个方法的主要作用是接收一个源纹理(源图像)和一个目标纹理,然后对源图像进行处理,最后将处理结果输出到目标纹理。

  • Camera Stack (相机叠加),如果使用 Universal Render Pipeline (URP) 或 High Definition Render Pipeline (HDRP),可以通过相机叠加来实现后处理效果。在这种情况下,可以将一个摄像机设置为后处理相机,并将其输出叠加到主相机的输出上。

  • Command Buffers 允许你在渲染管线的不同阶段插入自定义渲染命令。可以在特定的渲染事件(如 CameraEvent.AfterImageEffects)中添加后处理命令。

  • Image Effects (旧版后处理效果)
    在 Unity 的早期版本中,Unity 提供了一套 Image Effects(如模糊、景深、色彩校正等)。这些效果通常以组件的形式添加到摄像机上。虽然这不是直接的方法,但这些组件会在内部使用 OnRenderImage 进行后处理。

  • 可以使用 RenderTexture 来手动管理渲染过程。例如,可以先将场景渲染到一个 RenderTexture 中,然后在这个纹理上执行后处理操作,再将结果显示到屏幕上。

  • 通过在屏幕上绘制一个全屏 Quad 来实现后处理。可以在 OnRenderObject 或 OnPostRender 中执行这个操作:

1
2
3
4
5
6
void OnRenderObject()
{
material.SetTexture("_MainTex", sourceTexture);
Graphics.DrawMeshNow(quadMesh, Matrix4x4.identity);
}

  • Post Processing Stack 虽然不再被称为 “Image Effects”,Unity 的 Post Processing Stack 提供了一个组件系统来应用后处理效果。可以将 Post Processing Volume 添加到场景中,并为其配置不同的效果(如 Bloom、Vignette、Color Grading 等)。这通常是 URP 和 HDRP 项目的标准方法。

OnRenderImage

Unity 为我们提供的函数是:

1
MonoBehaviour.OnRenderImage (RenderTexture src, RenderTexture dest)

在 OnRenderImage 函数中,我们通常是利用 Graphics.Blit 函数来完成对渲染纹理的处理。

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

其中,src 是源纹理,dest 是目标纹理,如果它的值为 null 就会直接将结果显示在屏幕上。 mat 是我们使用的材质,而 src 纹理会被传递
材质上 Shader 中名为 _MainText 的纹理属性。
参数 pass 的默认值为 -1,表示将依次调用 Shader 内的所有 Pass。否则只会调用给定索引的 Pass。

关键点

  • 纹理源:在后处理过程中,_MainTex 实际上代表的是来自相机渲染的整个屏幕纹理(即之前渲染的场景图像)。在 OnRenderImage 方法中,Graphics.Blit(src, dest, material) 的 src 参数就是你想要处理的屏幕纹理,Unity 会自动将它传递给 Shader 的 _MainTex。

  • 纹理坐标:在后处理 Shader 中,i.uv 的坐标会被映射到屏幕的 UV 坐标,这样你可以对整个屏幕的每个像素进行采样和处理。

  • 不需要手动设置:由于 Unity 会在 OnRenderImage 调用时自动处理和绑定这些纹理,你无需在 Inspector 或 Shader 中手动设置纹理。只需确保你的 Shader 声明了 _MainTex,Unity 就会处理好它。

  • 在 Frag 函数中拿到的 UV 坐标是 NDC 进行了映射但是取值范围还在 [0,1] 的坐标,此时如果乘以屏幕的长宽参数就可以得到屏幕坐标。

实际案例

高斯模糊是用高斯核进行卷积操作,将邻域的像素对自己的影响计算进来进行模糊。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Shader 中的核心逻辑

//垂直方向卷积
...
o.uv[0] = uv;
o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
...

//水平方向卷积
...
o.uv[0] = uv;
o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;

C# 端的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for (int i = 0; i < iterations; i++)
{
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);

RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);

// Render the vertical pass
Graphics.Blit(buffer0, buffer1, material, 0);

RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);

// Render the horizontal pass
Graphics.Blit(buffer0, buffer1, material, 1);

RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}

C# 端通过 For 循环进行多次卷积,每一次 For 循环都会进行两个方向上的卷积逻辑,最后达到模糊的效果。