US3S10L4——屏幕后处理基类

本章代码关键字

1
2
3
4
shader.isSupported        // 判断此Shader在目标平台和硬件上是否能正确运行
[ExecuteInEditMode] // 使脚本在编辑器模式下也能执行的特性,此特性不考虑预制体模式下的情况
[RequireComponent()] // 指定某个脚本所依赖的组件,它确保当你将脚本附加到游戏对象时,所需的组件也会自动添加到该游戏对象中
HideFlags // 用于控制对象的销毁、保存和在Inspector中的可见性的枚举

知识点补充

  1. Shader.isSupported

    如何判断 Shader 在目标平台和硬件上是否能正确运行
    我们可以通过获取 Shader 对象中的 isSupported​ 属性判断

    • 如果返回 false​,说明当前平台不支持此 Shader 正确
    • 如果返回 true​,说明当前平台能够正确运行此 ​Shader
  2. [ExecuteInEditMode]​ 特性

    用于使脚本在编辑器模式下也能执行,此特性不考虑预制体模式下的情况。
    如果在预制体模式下编辑一个带有 MonoBehaviour​ 并启用了此属性的预制体,然后进入播放模式,
    则编辑器将退出预制体模式,以防止由仅用于播放模式的逻辑引起的预制体意外修改。

    要指示 MonoBehaviour​ 正确考虑了预制体模式并且在播放模式期间以预制体模式打开是安全的,可使用 [ExecuteAlways]​ 取代。

  3. [RequireComponent(typeof(组件名))]​ 特性

    指定某个脚本所依赖的组件,它确保当你将脚本附加到游戏对象时,所需的组件也会自动添加到该游戏对象中
    如果这些组件已经存在,它们不会被重复添加,因为后处理脚本一般添加到摄像机上,因此我们用于依赖摄像机

    代码示例:

    1
    2
    [RequireComponent(typeof(Camera))]
    public class Lesson101 : MonoBehaviour { }

    将此脚本添加到某个空对象上,这个空对象就会自动添加 [RequireComponent()]​ 内指定的脚本 Camera​,
    并且此 Camera​ 也不能直接从此对象上删除,需要先删除此脚本才能移除 Camera

    image

  4. UnityEngine.Object​ 中的 HideFlags​ 枚举

    Unity 对象 UnityEngine.Object​ (包括材质球)中可以点出 HideFlags​ 枚举

    注意,以下的一些枚举如果设置给对象,则此对象在不需要时不会自动销毁,需要使用 DestroyImmediate()​ 手动销毁避免内存泄漏

    • HideFlags.None​:不改变对象的可见性,对象是完全可见和可编辑的。这是默认值。

    • HideFlags.HideInHierarchy​:对象在层级视图中被隐藏,但仍然存在于场景中。

      若隐藏父物体,那么子物体也会被隐藏掉。隐藏后物体能被看见,但是在 Scene 视图中无法被选择。

    • HideFlags.HideInInspector​:对象在检查器中被隐藏,但仍然存在于层级视图中。

      设置对象在 Inspector 窗口内是否被隐藏。
      如果一个对象使用了 HideFlags.HideInInspector​,则其所有的组件在 Inspector 中都会被隐藏,但其子类组件依然显示。
      同理,如果只隐藏对象的某个组件,那么其他的组件均不受影响。

    • HideFlags.DontSaveInEditor​:对象不会被保存到场景中。适用于编辑器模式,不会影响播放模式。

      设置对象编辑模式下不会被保存,在对象不再需要的时候,必须配合使用 DestroyImmediate()​ 从内存中手动清除该对象以避免内存泄漏。

    • HideFlags.NotEditable​:对象在检查器中是只读的,不能被修改。

      • GameObject​ 对象使用 HideFlags.NotEditable​ 会使此 GameObject​ 的所有组件在 Inspector 中都处于不可编辑的状态。
        不过此操作并不会影响其子物体的可编辑性。
      • 对象的某个组件使用 HideFlags.NotEditable​,例如 Transform​ 组件,
        只会导致当前 Transform​ 组件不可被编辑,其他的组件均不受影响。
    • HideFlags.DontSaveInBuild​:对象不会被包含在构建中。

      设置对象构建后将不会被保存。在对象不再需要的时候,必须配合使用 DestroyImmediate()​ 从内存中手动清除该对象以避免内存泄漏。

    • HideFlags.DontUnloadUnusedAsset​:对象在资源清理时不会被卸载,即使它没有被引用。

      设置对象不会被 Resources.UnloadUnusedAssets()​ 卸载无用资源时卸掉。
      在对象不再需要的时候,必须配合使用 DestroyImmediate()​ 从内存中手动清除该对象以避免内存泄漏。

    • HideFlags.DontSave​:对象不会被保存到场景中,不会在构建中保存,也不会在编辑器中保存。

      此枚举是 DontSaveInEditor | DontSaveInBuild | DontUnloadUnusedAsset​ 的组合。
      设置此对象不会被保存,仅在编辑器模式下使用,运行时会自动剔除。该对象不保存到场景。加载新场景时,也不会销毁它。
      在对象不再需要的时候,必须配合使用 DestroyImmediate()​ 从内存中手动清除该对象以避免内存泄漏。

    • HideFlags.HideAndDontSave​:设置对象隐藏,并且不会被保存。

      游戏对象不会显示在 Hierarchy 窗口中,不会保存到场景中,也不会被 Resources.UnloadUnusedAssets()​ 方法卸载,
      通常用于由脚本创建并完全由脚本控制的游戏物体。对于设置了这个标识的物体,在 Inspector 面板中不可编辑。
      因此在对象不再需要的时候,例如 OnDisable()​ 时,必须配合使用 DestroyImmediate()​ 从内存中手动清除该对象以避免内存泄漏

    如果想要设置枚举满足多个条件,直接多个枚举,进行位或运算即可 |

为什么要实现屏幕后处理基类

  • 原因一:

    为了实现屏幕后期处理效果,我们每次都需要做的事情一定是

    1. 实现一个继承子 MonoBehaviour​ 的自定义C#脚本
    2. 关联对应的材质球或者 Shader
    3. 实现 OnRenderImage​ 函数
    4. OnRenderImage​ 函数中使用 Graphics.Blit​ 函数

    那么这些共同点我们完全可以抽象到一个基类中去完成,以后只需要在子类中实现各自的基本逻辑即可

  • 原因二:

    我们可以在基类中用代码动态创建材质球,不需要为每个后处理效果都手动创建材质球
    只需要在 Inspector 窗口关联对应使用的 Shader 即可

  • 原因三:

    在进行屏幕后处理之前,我们往往需要检查一系列条件是否满足,
    比如:当前平台是否支持当前使用的 Unity Shader,我们可以在基类中进行判断,避免每次书写相同逻辑

    注意!

    在一些老版本中,你可能还会在基类中判断目标平台是否支持屏幕后处理和渲染纹理,
    一般通过 Unity 中的 SystemInfo​ 类判断,该类可以用于确定底层平台和硬件相关的功能是否被支持
    官方说明:SystemInfo - Unity 脚本 API

    但是随着时代发展,目前几乎所有的现代图形硬件都是支持屏幕后处理和渲染纹理了,
    因此我们无需再进行类似的判断的,只需要判断 Shader 是否被支持即可

实现屏幕后处理基类功能

主要目标

  1. 声明基类,让其依赖 Camera​,并且让其在编辑模式下可运行,保证我们可以随时看到效果
  2. 基类中声明 公共 ​Shader,用于在 Inspector 窗口关联
  3. 基类中声明 私有 Material​,用于动态创建
  4. 基类中实现判断 Shader 是否可用,并且动态创建 Material​ 的方法
  5. 基类中实现 OnRenderImage​ 的虚方法,完成基本逻辑
  6. 在脚本被销毁时手动释放动态创建的 Material

代码如下:

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

[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class PostEffect : MonoBehaviour
{
public Shader shader;
private Material _material;

protected Material PostEffectMaterial
{
get
{
// 不存在Shader或Shader不支持当前平台,返回null
if (shader == null || !shader.isSupported)
return null;
// 若material存在且shader未变化,直接返回material
if (_material != null && _material.shader == shader)
return _material;
// 若material不存在或shader发生变化,就需要创建material,并让其不可保存
if (_material != null)
{
// 若之前存在_material,需要手动销毁避免内存泄露
DestroyImmediate(_material);
_material = null;
}
_material = new Material(shader);
_material.hideFlags = HideFlags.DontSave;
return _material;
}
}

protected virtual void OnDestroy()
{
// 当脚本被销毁时,由于其引用的_material.hideFlags是HideFlags.DontSave,因此需要手动销毁避免内存泄露
if (_material != null)
{
DestroyImmediate(_material);
_material = null;
}
}

/// <summary>
/// 更新材质球属性
/// </summary>
protected virtual void UpdateProperty() {}

protected virtual void OnRenderImage(RenderTexture source, RenderTexture destination)
{
// 在进行渲染前更新材质上的属性,在子类中重写即可
UpdateProperty();

// 如果材质不存在,说明Shader有问题,将原屏幕纹理复制到目标纹理,相对于无后处理效果
if (PostEffectMaterial == null)
{
Graphics.Blit(source, destination);
return;
}

Graphics.Blit(source, destination, PostEffectMaterial);
}
}

这样,将脚本挂载到 Camera 后,再关联一个 Shader,即可让屏幕的显示内容被 Shader 处理了
以之前实现的棋盘格程序纹理 Shader 为例,代码详见:US3S8L11——Shader代码动态生成程序纹理

image

显示效果:

image