基于PostProcessing框架编写自定义后处理

原文链接:https://github.com/Unity-Technologies/PostProcessing/wiki/Writing-Custom-Effects

此框架允许你编写自定义的后处理效果并将它们挂载到效果堆上,并不需要做任何对基类的修改。当然,所有不是在这套框架上编写的效果会在体混合之外运行,并且如果你不需要有循环依赖的特性那么它们在将要到来的srp上也能自动生效。

让我们写一个非常简单的灰度缩放来展示一下。

自定义效果至少需要两个文件:一个C#文件和一个HLSL文件(HLSL文件会被unity跨平台的编译到GLSL,METAL,或其他API,所以并不只限于DirectX)。

注意:这篇快速教程需要有C#和shader编程基础。我们不会讲的的太细,就把它当做是一个大概的预览而不是一个深入的教程。

C#

代码列表:

using System;
using UnityEngine;
using UnityEngine.Rendering.PostProcessing;
 
[Serializable]
[PostProcess(typeof(GrayscaleRenderer), PostProcessEvent.AfterStack, "Custom/Grayscale")]
public sealed class Grayscale : PostProcessEffectSettings
{
    [Range(0f, 1f), Tooltip("Grayscale effect intensity.")]
    public FloatParameter blend = new FloatParameter { value = 0.5f };
}
 
public sealed class GrayscaleRenderer : PostProcessEffectRenderer<Grayscale>
{
    public override void Render(PostProcessRenderContext context)
    {
        var sheet = context.propertySheets.Get(Shader.Find("Hidden/Custom/Grayscale"));
        sheet.properties.SetFloat("_Blend", settings.blend);
        context.command.BlitFullscreenTriangle(context.source, context.destination, sheet, 0);
    }
}

重点:这些代码必须要保存到名为Grayscale.cs的文件。因为unity序列化的原理的问题,你要确保这个文件的类名和文件名一致,不然它可能不会被正确的序列化。

我们需要两个类,一个用来存储设置的数据,一个用来处理渲染逻辑部分。

Settings(设置)

设置类用来保存我们效果的配置数据。它们全是面向使用者的字段,你可以在volume的界面中看到。

[Serializable]
[PostProcess(typeof(GrayscaleRenderer), PostProcessEvent.AfterStack, "Custom/Grayscale")]
public sealed class Grayscale : PostProcessEffectSettings
{
    [Range(0f, 1f), Tooltip("Grayscale effect intensity.")]
    public FloatParameter blend = new FloatParameter { value = 0.5f };
}

首先,你需要确保这个类继承自 PostProcessEffectSettings 并且可以被序列化,所以不要忘了添加 [Serializable] 标签!

其次,你需要告诉Unity 这个类保存了后处理的数据。也就是 [ PostProcess() ] 标签的作用。第一个参数是要关联的渲染器( 详情在后面会说 )。第二个参数是指定此后效的插入点。当前呢,你有三个点可以插入。

  • BeforeTransparent:效果会被应用到不透明物体,即在透明渲染前完成。
  • BeforeStack: 效果会在内置效果队列开始前进行,内置效果队列包括抗锯齿、景深、tonemapping等。
  • AfterStack:效果会在内置在效果队列完成后进项,但是在FXAA(如果开启了)和 final-pass dithering 之前进行。

第三个参数是此效果的按钮,你可以通过/来创建层级目录。

最后,有一个可选的第四个参数 allowInSceneView (允许在scene生效),控制效果在scene视图是否可见。默认情况下这个值为true,但是你可以将其禁用,比如一些比较传统的效果或者在效果让层级编辑变得困难的时候。

至于属性参数你可以使用你需要的任何类型,如果你想要你的属性在volume中可以被修改或混合,你需要使用盒子域。在我们的样例中,我们简单的添加一个浮点类型使用一个0-1的固定区域做限制。你可以通过浏览 /PostProcessing/Runtime/ 中 ParameterOverride.cs 的源码来获得一份内置的参数类型,或则你可以创建你自己的类型,照着源文件的方法做起来很简单。

注意,你可能需要重写 PostProcessEffectSettings 的 IsEnabledAndSupported() 方法,来设置你的效果需要的环境(以防你的效果需要一些特殊的硬件支持),或则通过一些状态来控制效果的开关。例如,在我们的样例中,如果blend值为0,我们将会自动禁用效果。

public override bool IsEnabledAndSupported(PostProcessRenderContext context)
{
    return enabled.value
        && blend.value > 0f;
}

这样效果只有在 blend>0 的时候才会执行。

Renderer(渲染器)

现在我们来看一下渲染逻辑。我们的渲染器扩展自PostProcessEffectRenderer<T>,T 指定了连接到此渲染器的Setting。

public sealed class GrayscaleRenderer : PostProcessEffectRenderer<Grayscale>
{
    public override void Render(PostProcessRenderContext context)
    {
        var sheet = context.propertySheets.Get(Shader.Find("Hidden/Custom/Grayscale"));
        sheet.properties.SetFloat("_Blend", settings.blend);
        context.command.BlitFullscreenTriangle(context.source, context.destination, sheet, 0);
    }
}

所有的操作都在 Render() 方法中执行,它将一个PostProcessRenderContext (后处理渲染 上下文)作为参数。这个上下文包含了一些你可以使用的有用数据,在渲染时被传递到效果内。可以看一下/PostProcessing/Runtime/PostProcessRenderContext.cs的源文件,了解一下哪些是可用的(这个文件做了很多的注释)。

PostProcessEffectRenderer<T> 还包含一些其它的可以重写的方法,如:
*void Init():在渲染器创建后被调用

  • DepthTextureMode GetLegacyCameraFlags():用来设置相机的Flags,对深度图的需求,动态向量的需求等。
  • void RestHistory():当接收到 "reset history" 事件时被调用,用于一般效果清除历史buffer或其它东西。
  • void Release(): 当渲染器销毁时被调用。如果有清理操作可以放在这里执行。

我们的效果非常简单,我们只做两件事:

  • 将blend属性值传递到shader
  • 用我们的资源图作为输入,通过shader的pass做一个全屏blit 将结果放到目标对象。

由于我们使用 command buffer (命令缓存),这个系统依赖于MaterialPropertyBlock 来保存 shader 数据。你无需亲自创建这些,框架做了自动池来为你节省时间并确保性能足够优化。所以我们只需要一个来自我们 shader 的 PropertySheet(属性列表),并给它们传递参数。

最后,我们利用上下文提供的command buffer,使用我们的源对象,目标对象,属性列表,pass 索引来做一次全屏的blit。

以上就是 C# 的部分

Shader

写一个自定义的后处理 shader 是相当简单的,但是在写之前有一些点你是需要知道的。这套框架使用了大量的宏定义来抽象不同的平台让你的工作变得简单。主要目的就是处理兼容西,甚至是即将到来的upcoming Scriptable Render Pipelines(可编程渲染管线)。

代码

Shader "Hidden/Custom/Grayscale"
{
    HLSLINCLUDE

        #include "Packages/com.unity.postprocessing/PostProcessing/Shaders/StdLib.hlsl"

        TEXTURE2D_SAMPLER2D(_MainTex, sampler_MainTex);
        float _Blend;

        float4 Frag(VaryingsDefault i) : SV_Target
        {
            float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.texcoord);
            float luminance = dot(color.rgb, float3(0.2126729, 0.7151522, 0.0721750));
            color.rgb = lerp(color.rgb, luminance.xxx, _Blend.xxx);
            return color;
        }

    ENDHLSL

    SubShader
    {
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            HLSLPROGRAM

                #pragma vertex VertDefault
                #pragma fragment Frag

            ENDHLSL
        }
    }
}

首先要注意的一点是:我们不在使用 CG 代码块。如果你将来打算兼容 Scriptable Render Pipelines 最好不要使用他,因为在转换的过程中它会添加一些你不想要的隐式代码将shader拆散,所以我们是 用 HLSL 代码块。

你至少 需要引入 StdLib.hlsl 文件。它包含预先配置好的顶点着色器和不同的结构体(顶点默认,不同的默认)和大部分你写效果时需要的数据。

纹理声明是通过宏定义进行的。我们建议你通过浏览 /PostProcessing/Shaders/API/ 的 API 文件来了解有哪些可用的宏。

此外,剩下的就是标准的 shader 代码了。在这里我们计算当前像素的明度,然后使用blend值来插值像素颜色和明度值,并将结果返回。

重点: 如果shader在你的任何场景中没有被引用,那么它将不会被构建,在编辑器以外的环境运行时效果也不会生效。可以将它添加到Resources folder文件夹下,或则添加到 Always Included Shaders 列表里,从 Edit(编辑)-> Project Settings(项目设置) -> Graphics(显示) 可以找到

效果顺序

内置效果是自动排序的,那么自定义效果呢?当你创建或导入一个新的效果到项目时,他会被添加到你相机的Post Process Layer组件的 Custom Effect Sorting 列表中。


自定义排序,原文是个TODO,自己截的图

他们会根据插入点被预先排序,当然你可以重新进行排序。顺序数据是保留才layer上的,这意味着你每个相机都可以有不同的排序。

自定义编辑器

默认情况下 setting 类的Editor(编辑器)是自动为你创建的。但有时你可能需要更好的控制你的属性如何显示。就如经典的 Unity 组件一样,你可以创建自定的Editor。

重点:和讲点的编辑器一样,你需要把这些编辑器文件放置Editor文件夹内。

如果为我们的 Grayscale 效果重写默认的编辑器,那么它看起来将会是这样。

using UnityEngine.Rendering.PostProcessing;
using UnityEditor.Rendering.PostProcessing;

[PostProcessEditor(typeof(Grayscale))]
public sealed class GrayscaleEditor : PostProcessEffectEditor<Grayscale>
{
    SerializedParameterOverride m_Blend;

    public override void OnEnable()
    {
        m_Blend = FindParameterOverride(x => x.blend);
    }

    public override void OnInspectorGUI()
    {
        PropertyField(m_Blend);
    }
}

补充说明

处于性能的考虑,FXAA 期望它的源对象的每个像素的 LDR Luminance value (低动态亮度值)保存在其 Alpha通道。如果你需要 FXAA 效果,并且将自定义效果放在了 AfterStack 插入点,请确保最后执行的效果其 Alpha 通道包含 LDR Luminance 值(或者从源对象中简单的拷贝 Alpha)。否则,FXAA不能正常运行。

推荐阅读更多精彩内容