一、理论基础

unity只提供了很有限的几种后处理效果,这显然是不能满足策划的脑洞和开发者的需求的;而unity的后处理框架允许开发者扩展自己的后处理效果,并将其放入到后处理的堆栈中;参考官方提供的自定义效果,官方给出了一个灰度效果比较见到那,给出一个稍微复杂点的高斯模糊的效果;如下,是一个grascale+高斯模糊的混合效果图;

https://github.com/zhimo1997/UnityPostProcess

2d post processing 没有效果_#pragma

官方文档:写一个自定义后处理效果

二、着色器编写

后处理部分的shader和渲染3D所用的shader稍有不同,而unity框架使用的shader即是使用了hlsl而不是cg,但是hlsl交叉编译后也可以跨平台所以不用担心这一点;所以,之前的cg在这里不能再用,包括之前常用的UnityCG文件里包含的各种内置类型,这里也不能用,也不需要再用,而且不再有fixed类型;

首先是顶点着色器的不同,这里参考框架自带的vertexDefault,顶点坐标的计算并不需要mvp矩阵变换,纹理坐标的计算也是根据顶点做一个基本变换;

struct AttributesDefault
{
    float3 vertex : POSITION;
};

VaryingsDefault VertUVTransform(AttributesDefault v)
{
    VaryingsDefault o;

#if STEREO_DOUBLEWIDE_TARGET
    o.vertex = float4(v.vertex.xy * _PosScaleOffset.xy + _PosScaleOffset.zw, 0.0, 1.0);
#else
    o.vertex = float4(v.vertex.xy, 0.0, 1.0);
#endif
    o.texcoord = TransformTriangleVertexToUV(v.vertex.xy) * _UVTransform.xy + _UVTransform.zw;
    o.texcoordStereo = TransformStereoScreenSpaceTex(o.texcoord, 1.0);
#if STEREO_INSTANCING_ENABLED
    o.stereoTargetEyeIndex = (uint)_DepthSlice;
#endif
    return o;
}

然后就是渲染状态的设置,设置如下即可;

Cull Off ZWrite Off ZTest Always

基本区别就在这里,下面是一个用于后处理的高斯模糊的shader;

Shader "Hidden/Custom/Gaussian"
{
    HLSLINCLUDE

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

        // TEXTURE2D_SAMPLER2D(_MainTex, sampler_MainTex);
        
		sampler2D _MainTex;  
		half4 _MainTex_TexelSize;
		float _BlurSize;

		struct a2v {
			float4 vertex : POSITION;
		};
		  
		struct v2f {
			float4 pos : SV_POSITION;
			half2 uv[5]: TEXCOORD0;
		};
		  
		v2f vertBlurVertical(a2v v) {
			v2f o;
			o.pos = float4(v.vertex.xy, 0.0, 1.0);
			
			float2 uv = TransformTriangleVertexToUV(v.vertex.xy);
#if UNITY_UV_STARTS_AT_TOP
    		uv = uv * float2(1.0, -1.0) + float2(0.0, 1.0);
#endif
			
			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;
					 
			return o;
		}
		
		v2f vertBlurHorizontal(a2v v) {
			v2f o;
			o.pos = float4(v.vertex.xy, 0.0, 1.0);
			
			float2 uv = TransformTriangleVertexToUV(v.vertex.xy);
#if UNITY_UV_STARTS_AT_TOP
    		uv = uv * float2(1.0, -1.0) + float2(0.0, 1.0);
#endif
			
			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;
					 
			return o;
		}
		
		float4 fragBlur(v2f i) : SV_Target {
			float weight[3] = {0.4026, 0.2442, 0.0545};
			
			float3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
			
			for (int it = 1; it < 3; it++) {
				sum += tex2D(_MainTex, i.uv[it*2-1]).rgb * weight[it];
				sum += tex2D(_MainTex, i.uv[it*2]).rgb * weight[it];
			}
			
			return float4(sum, 1.0);
		}

    ENDHLSL

    SubShader
    {
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            HLSLPROGRAM

                #pragma vertex vertBlurVertical
                #pragma fragment fragBlur

            ENDHLSL
        }

        Pass
        {
            HLSLPROGRAM

                #pragma vertex vertBlurHorizontal
                #pragma fragment fragBlur

            ENDHLSL
        }
    }
}

三、C#的渲染逻辑

首先要有一个类来存储高斯模糊的参数设置,这里定义了一个可序列化的类,并需要继承自PostProcessEffectSettings,

[Serializable]
[PostProcess(typeof(GaussianBlurRenderer), PostProcessEvent.AfterStack, "Custom/GaussianBlur")]
public sealed class GaussianBlurSettings : PostProcessEffectSettings
{
    public FloatParameter blurSize = new FloatParameter { value = 0.5f };
}

重点在下面这个渲染类,它继承自PostProcessEffectRenderer<T>,并且需要重写Render函数;

因为一个高斯模糊需要两个pass,需要做两次blit操作,所以我们需要一张RT来暂存第一个pass的处理结果;

public sealed class GaussianBlurRenderer : PostProcessEffectRenderer<GaussianBlurSettings> {
    int _tempRTID;
    public override void Init() {
        _tempRTID = Shader.PropertyToID("_TempRT");
    }
    public override void Render(PostProcessRenderContext context)
    {
        var sheet = context.propertySheets.Get(Shader.Find("Hidden/Custom/Gaussian"));
        context.command.GetTemporaryRT(_tempRTID, context.width, context.height, 0, FilterMode.Bilinear, context.sourceFormat);
        sheet.properties.SetFloat("_BlurSize", settings.blurSize);
        context.command.BlitFullscreenTriangle(context.source, _tempRTID, sheet, 0);
        context.command.BlitFullscreenTriangle(_tempRTID, context.destination, sheet, 1);
        context.command.ReleaseTemporaryRT(_tempRTID);
    }
}

四、实现逐渐模糊的动态效果

https://docs.unity3d.com/Packages/com.unity.postprocessing@2.3/manual/Manipulating-the-Stack.html

参考官方文档中的案例,这里将高斯模糊随着时间逐渐变得模糊;

using UnityEngine;
using UnityEngine.Rendering.PostProcessing;

public class GaussianCtrl : MonoBehaviour
{
    PostProcessVolume m_Volume;
    GaussianBlurSettings m_Gaussian;
    // Start is called before the first frame update
    void Start()
    {
        m_Gaussian = ScriptableObject.CreateInstance<GaussianBlurSettings>();
        m_Gaussian.enabled.Override(true);
        m_Gaussian.blurSize.Override(1.0f);

        m_Volume = PostProcessManager.instance.QuickVolume(gameObject.layer, 100f, m_Gaussian);
    }

    // Update is called once per frame
    void Update()
    {
        m_Gaussian.blurSize.value = Mathf.PingPong(Time.time, 3);
    }
}