【Unity2019 LWRP】ポストエフェクトを自作してゲームから利用する

# 環境
Unity2019.1.0f2
LWRP - Version 5.7.2

# はじめに
ポストエフェクトを自作する場合、従来のUnityではOnRenderImage()を実装したMonoBehaviourスクリプトをカメラにアタッチすることでこれを実装することができました。

```ExampleClass.cs
using UnityEngine;
using System.Collections;

public class ExampleClass : MonoBehaviour {
    public Material mat;
    void OnRenderImage(RenderTexture src, RenderTexture dest) {
        Graphics.Blit(src, dest, mat); // ポストエフェクト
    }
}
```

しかし、この方法は**LWRP環境では使うことができません**。


LWRPのようなSRP環境ではレンダリングパイプラインに自作レンダリングパスを追加することで、ポストエフェクトを実装します。
今回はLWRP環境でポストエフェクトを実装するまでの手順を紹介します。

## ポストエフェクトの実装例
Unity1Weekというオンラインイベントで制作した<a href="https://unityroom.com/games/to-the-right">ToTheRight</a>というゲームでは2つのポストエフェクトを実装しました。

・死亡時に画面の色を反転させるポストエフェクト
![色反転2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/f0ccf298-9049-3d53-8e64-5741877cba02.gif)

・画面を歪ませるグリッチエフェクト
![gif_animation_005.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/8318f354-5687-105b-a8d6-288a5ec9e1c3.gif)

今回はこの二つのポストエフェクトの実装について解説します。

# ポストエフェクトの実装の流れ
ポストエフェクトを実装するには以下を作成する必要があります。
・CustomForwardRendererアセット
・ScriptableRendererFeatureクラス (パラメータやレンダーパスの追加はここで行う)
・ScriptableRendererPassクラス (レンダーパスの実装。実際の描画処理はここで実装)

処理の流れとしては以下のようになります。
LWRPSettingsへ登録したForwardRendererDataが実行される
-> ScriptableRendererFeatureが実行される
-> ScriptableRendererPassが実行され、描画が行われる。

# ポストエフェクトの作成手順
## 1. CustomForwardRendererアセットの作成
メニューから Rendering > Lightweight Pipeline > Forward Rendererを選択し、CustomForwardRendererを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/41d2bd17-92e6-1c79-c170-9a0ae32ed502.png)

## 2. Forward Rendererアセットを登録
LWRP SettingsアセットのRendererType をCustomに設定。
Dataの部分に先ほど作成したForward Rendererアセットを登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/0027adde-67ed-a270-d5f0-8658774d8663.png)

これでCustomForwardRendererが実行されるようになります。


### 補足 : LWRP Settingsアセットについて
Project SettingsのScriptable Render Pipeline Settingsの部分に登録しているアセットがLWRP Settingsアセットになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/2c5378ea-7545-dc24-9975-77909f1bd901.png)

## 3. RendererFeatrureスクリプトの作成
ScriptableRendererFeatureの派生クラスを作成します。

```GameRendererFeature.cs
using UnityEngine;
using UnityEngine.Rendering.LWRP;

public class GameRendererFeature : ScriptableRendererFeature
{
    [SerializeField] private bool hoge = true; // テキトーな変数
    [SerializeField] private int fuga = 123; // テキトーな変数

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
    }

    public override void Create()
    {
    }
}
```

## 4.RendererFeatureの登録
CustomForwardRendererにRendererFeatureを登録し、RendererFeatureが実行されるようにします。

+ボタンをクリックし、RendererFeatureを登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/07740d67-6f82-71cc-e0dd-6f6d5a9e6ec3.png)

登録すると以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/7dacccdf-fa47-c624-4927-1d0fca844529.png)

Projectビューを見るとCustomForwardRendererの中にRendererFeatureができています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/cae22e70-e328-7e4e-0418-043b443e7459.png)

RendererFeatureは、通常のScriptableObjectと同じようにInspectorタブからシリアライズ変数の値を編集できます。
レンダリングに関係するパラメータはRendererFeatureの中に定義しておくと良いでしょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/041acffd-2978-d6dd-578f-8f6fc932b06c.png)


# ToTheRightで実装したポストエフェクト

## 1. RendererFeatureクラスの実装
今回のゲームToTheRightでは、色反転Passとグリッチ効果Passの二つを作成しています。
RendererFeatureクラスのコードは以下のようになりました。

```GameRendererFeature.cs
using UnityEngine;
using UnityEngine.Rendering.LWRP;

public sealed class GameRendererFeature : ScriptableRendererFeature
{
    [field: SerializeField] public bool ReverseActive { get; set; } = false;
    [field: SerializeField] public bool GlitchActive { get; set; } = false;
    [field: SerializeField, Range(0, 1)] public float GlitchWeight { get; set; } = 1f;
    [field: SerializeField, Range(0, 1)] public float NoiseWeight { get; set; } = 1f;
    private ReverseColorRendererPass reverseColorPass = null;
    private GlitchEffectRendererPass glitchPass = null;

    [SerializeField] private Material reverseColorMaterial;

    // レンダーパスの作成を行う
    public override void Create()
    {
        reverseColorPass = reverseColorPass ?? new ReverseColorRendererPass();
        glitchPass = glitchPass ?? new GlitchEffectRendererPass();
    }

    // レンダーパスの追加を行う
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        // 色反転 ポストエフェクトPass 追加
        reverseColorPass.SetRenderTarget(renderer.cameraColorTarget);
        reverseColorPass.Active = ReverseActive;
        renderer.EnqueuePass(reverseColorPass);

        // グリッチ ポストエフェクトPass 追加
        glitchPass.SetRenderTarget(renderer.cameraColorTarget);
        glitchPass.Active = GlitchActive;
        glitchPass.GlitchWeight = GlitchWeight;
        renderer.EnqueuePass(glitchPass);
    }
}
```

## 2. 色反転エフェクトの実装

### 色を反転するShader

```ReverseColor.shader
Shader "Hidden/ReverseColor"
{
	Properties
	{
		_MainTex("Texture", 2D) = "white" {}
	}

	SubShader
	{
		Cull Off ZWrite Off ZTest Always
		
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

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

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

			sampler2D _MainTex;

			fixed4 frag(v2f i) : SV_Target
			{
				fixed4 col = tex2D(_MainTex, i.uv);
				// just invert the colors
				col.rgb = 1 - col.rgb;
				return col;
			}
			ENDCG
		}
	}
}
```

### 色の反転Passの実装

```ReverseColorRendererPass.cs
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.LWRP;

public sealed class ReverseColorRendererPass : ScriptableRenderPass
{
    private const string Tag = nameof(ReverseColorRendererPass);

    private RenderTargetIdentifier currentTarget;

    public bool Active { get; set; }

    public ReverseColorRendererPass()
    {
        renderPassEvent = RenderPassEvent.AfterRenderingSkybox;
    }

    public void SetRenderTarget(RenderTargetIdentifier target)
    {
        currentTarget = target;
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (!Active) { return; }

        var shader = Shader.Find("Hidden/ReverseColor"); // 色を反転するシェーダー取得
        if (!shader) { return; }

        var material = new Material(shader);
        var commandBuffer = CommandBufferPool.Get(Tag);
        var renderTextureId = Shader.PropertyToID("_SampleLWRPScriptableRenderer");
        var cameraData = renderingData.cameraData;
        var w = cameraData.camera.scaledPixelWidth;
        var h = cameraData.camera.scaledPixelHeight;
        int shaderPass = 0;

        commandBuffer.GetTemporaryRT(renderTextureId, w, h, 0, FilterMode.Point, RenderTextureFormat.Default);
        commandBuffer.Blit(currentTarget, renderTextureId);
        commandBuffer.Blit(renderTextureId, currentTarget, material, shaderPass);

        context.ExecuteCommandBuffer(commandBuffer);
        CommandBufferPool.Release(commandBuffer);
    }
}
```

## 3. グリッチエフェクトの実装

### グリッチエフェクトのシェーダー
今回のグリッチ効果は以下のリンク先のGlitchShader.shaderを利用させていただきました。
https://github.com/staffantan/unityglitch

```GlitchShader.shader
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

// This work is licensed under a Creative Commons Attribution 3.0 Unported License.
// http://creativecommons.org/licenses/by/3.0/deed.en_GB
//
// You are free:
//
// to copy, distribute, display, and perform the work
// to make derivative works
// to make commercial use of the work


Shader "Hidden/GlitchShader" {
	Properties{
		_MainTex("Base (RGB)", 2D) = "white" {}
		_DispTex("Base (RGB)", 2D) = "bump" {}
		_Intensity("Glitch Intensity", Range(0.1, 1.0)) = 1
		_ColorIntensity("Color Bleed Intensity", Range(0.1, 1.0)) = 0.2
	}

		SubShader{
			Pass {
				ZTest Always Cull Off ZWrite Off
				Fog { Mode off }

				CGPROGRAM
				#pragma vertex vert
				#pragma fragment frag
				#pragma fragmentoption ARB_precision_hint_fastest 

				#include "UnityCG.cginc"

				uniform sampler2D _MainTex;
				uniform sampler2D _DispTex;
				float _Intensity;
				float _ColorIntensity;
				float _GlitchWeight;

				fixed4 direction;

				float filterRadius;
				float flip_up, flip_down;
				float displace;
				float scale;

				struct v2f {
					float4 pos : POSITION;
					float2 uv : TEXCOORD0;
				};

				v2f vert(appdata_img v)
				{
					v2f o;
					o.pos = UnityObjectToClipPos(v.vertex);
					o.uv = v.texcoord.xy;

					return o;
				}

				half4 frag(v2f i) : COLOR
				{
					half4 normal = tex2D(_DispTex, i.uv.xy * scale);
					fixed2 uv = i.uv;

					i.uv.y -= (1 - (i.uv.y + flip_up)) * step(i.uv.y, flip_up) + (1 - (i.uv.y - flip_down)) * step(flip_down, i.uv.y);

					i.uv.x += (normal.x - 0.5) * displace * _Intensity;

					i.uv = lerp(uv, i.uv, _GlitchWeight);

#define DirectionMulti 0.005
					half4 baseColor = tex2D(_MainTex, i.uv.xy);
					half4 color = baseColor;
					half4 redcolor = tex2D(_MainTex, i.uv.xy + direction.xy * DirectionMulti * filterRadius * _ColorIntensity);
					half4 greencolor = tex2D(_MainTex,  i.uv.xy - direction.xy * DirectionMulti * filterRadius * _ColorIntensity);

					color += fixed4(redcolor.r, redcolor.b, redcolor.g, 1) *  step(filterRadius, -0.001);
					color *= 1 - 0.5 * step(filterRadius, -0.001);

					color += fixed4(greencolor.g, greencolor.b, greencolor.r, 1) *  step(0.001, filterRadius);
					color *= 1 - 0.5 * step(0.001, filterRadius);

					//return color;
					return baseColor + color * _GlitchWeight;
				}
				ENDCG
			}
		}

			Fallback off

}
```

### グリッチエフェクトPassの実装

```GlitchEffectRendererPass.cs
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.LWRP;

public sealed class GlitchEffectRendererPass : ScriptableRenderPass
{
    private const string Tag = nameof(GlitchEffectRendererPass);

    private RenderTargetIdentifier currentTarget;

    public bool Active { get; set; } 
    public float GlitchWeight { get; set; }
    public Material Material { get; set; }

    private GlitchEffect glitchEffect = null;

    public GlitchEffectRendererPass()
    {
        renderPassEvent = RenderPassEvent.AfterRendering;
    }

    public void SetRenderTarget(RenderTargetIdentifier target)
    {
        currentTarget = target;
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (!Active) { return; }

        if (glitchEffect == null || glitchEffect.gameObject == null)
        {
            glitchEffect = Camera.main.gameObject.GetComponent<GlitchEffect>();
        }
        //glitchEffect = glitchEffect ?? Camera.main.gameObject.GetComponent<GlitchEffect>();
        if (glitchEffect == null) { return; }

        glitchEffect.Execute();
        if (glitchEffect.Material == null) { return; }

        var commandBuffer = CommandBufferPool.Get(Tag);
        var renderTextureId = Shader.PropertyToID("_SampleLWRPScriptableRenderer");
        var cameraData = renderingData.cameraData;
        var w = cameraData.camera.scaledPixelWidth;
        var h = cameraData.camera.scaledPixelHeight;
        int shaderPass = 0;

        glitchEffect.Material.SetFloat("_GlitchWeight", GlitchWeight);

        commandBuffer.GetTemporaryRT(renderTextureId, w, h, 0, FilterMode.Point, RenderTextureFormat.Default);
        commandBuffer.Blit(currentTarget, renderTextureId);
        commandBuffer.Blit(renderTextureId, currentTarget, glitchEffect.Material, shaderPass);

        context.ExecuteCommandBuffer(commandBuffer);
        CommandBufferPool.Release(commandBuffer);
    }
}
```


# ポストエフェクトの制御について
今回はGameRendererFeatureで定義したパラメータを変更することでポストエフェクトを制御しました。
・色反転エフェクトのON/OFF
・グリッチエフェクトのかかり具合

### ポストエフェクトのON/OFF
色反転エフェクトのON/OFFを切り替えたい場合はReverseActiveの値を変更します。

```csharp:色反転エフェクトのON/OFF
    /// <summary>
    /// 色を反転
    /// </summary>
    public void SetReverseColor(bool active)
    {
        if (gameRendererFeature == null) return;
        gameRendererFeature.ReverseActive = active;
    }
```

ReverseActiveはGameRendererFeature.cs内にてReverseColorRendererPassに値をそのまま渡しています。

```GameRendererFeature.cs
        // 色反転 ポストエフェクトPass 追加
        reverseColorPass.SetRenderTarget(renderer.cameraColorTarget);
        reverseColorPass.Active = ReverseActive;
        renderer.EnqueuePass(reverseColorPass);
```


ReverseColorRendererPassではActiveの数値を見て、falseの場合にスキップさせるようにしています。

```ReverseColorRendererPass.cs
    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (!Active) { return; }

```

### グリッチのかかり具合変更

グリッチのかかり具合を変更したい場合はGlitchWeightの数値を変更します。

```csharp:グリッチエフェクトの重み変更
gameRendererFeature.GlitchWeight = scale * postEffectConfig.EnemyDeathGlitchCurve.Evaluate(time);
```

GameRendererFeatureのGlitchWeightをGlitchEffectRendererPassへ値を流しています。

```GameRendererFeature.cs
        // グリッチPass 追加
        glitchPass.SetRenderTarget(renderer.cameraColorTarget);
        glitchPass.Active = GlitchActive;
        glitchPass.GlitchWeight = GlitchWeight;
        renderer.EnqueuePass(glitchPass);
```


GlitchEffectRendererPassでは ```GlitchWeight``` の値をシェーダーへ流しています。

```GlitchEffectRendererPass.cs
        glitchEffect.Material.SetFloat("_GlitchWeight", GlitchWeight);
```


シェーダー内では ```_GlitchWeight``` はディストーションをかけたUVと元のUVの線形補間する際の数値として利用しています。
_GlitchWeight=0.0なら歪みなしUV、_GlitchWeight=1.0の場合は歪みありUVになります。

```GlitchShader.shader
	fixed2 uv = i.uv;
	i.uv.y -= (1 - (i.uv.y + flip_up)) * step(i.uv.y, flip_up) + (1 - (i.uv.y - flip_down)) * step(flip_down, i.uv.y);
	i.uv.x += (normal.x - 0.5) * displace * _Intensity;
	i.uv = lerp(uv, i.uv, _GlitchWeight);
```