Unity(URP)での自作ポストエフェクトの作り方をまとめてみた

# はじめに
ゲーム制作において「何かを自作する」ということは必須です。それはポストエフェクトも例外ではありません。
ということで、シェーダーの勉強の集大成の一つとして自作のポストエフェクトを作成してみました。ポストエフェクトのなかでも、今回はURPで使用できる自作ポストエフェクトの作成方法の例を紹介してきます。

# 目次
URPのポストエフェクトとは
目標物（今回作ったもの）
実行環境
ファイル構成
エディタ上の設定
実際に躓いた箇所
まとめ

# URPのポストエフェクトとは
まずURPとは、Unityにあるレンダリングパイプラインの一種であるUniversalRenderPipelineの略称です。そしてカメラに対しての発光やぼかしなどのエフェクトをポストエフェクトといいます。URPのポストエフェクト自体はVolumeコンポーネントで実装されています。初期から実装されているエフェクトを使用する場合は以下の記事に実装方法をまとめているのでそちらを参照してください。

[URPにおけるポストエフェクトのかけ方](https://qiita.com/Shu-Nogami/items/daaa77eae9973885fffe)

# 目標物（今回作ったもの）
今回作ったエフェクトはなんちゃってトゥーンシェーダーです。画面を一度グレースケールにし、一定の閾値で表示する色を区切っています。
![c00b3f129dbfad00e4901b18177cd1bf.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3127325/b87d41d0-c221-3198-a015-9e537565f589.png)

# 実行環境
Unity 2020.1.2f1

:::note warn
注意
バージョンが違うとエディタ上の設定方法などが変わるため今回の方法で設定する場合はバージョンをなるべく合わせてください。
:::
# ファイル構成
URPで自作エフェクトを反映させるためにはC#スクリプト（2～3個）とシェーダープログラムが必要です。今回はC#スクリプトを3個のバージョンで作成していきます。
## C#スクリプト
### Tone(C#)
このプログラムはVolumeコンポーネントにエフェクトを追加した際パラメータを表示します。
![2aaa28c29232ee1a3ca525524b4c0cb2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3127325/d860be6d-9c24-c8e3-5a5a-ce000173bc44.png)

パラメータを表示する際は~~Parameterといった型を使用します。これにはIntParameterやFloatParameterなど様々存在します。
```c#:Tone.cs
using UnityEngine;
using UnityEngine.Rendering;

namespace VolumeScripts.ToneShader
{
    public class Tone : VolumeComponent
    {
        public ColorParameter darkColor = new ColorParameter(Color.black);
        public ColorParameter normalColor = new ColorParameter(Color.gray);
        public ColorParameter lightColor = new ColorParameter(Color.white);

        public ClampedFloatParameter darkBorder = new ClampedFloatParameter(0, 0, 1);
        public ClampedFloatParameter normalBorder = new ClampedFloatParameter(0, 0, 1);
        public bool IsActive => darkBorder.value > 0f && normalBorder.value > darkBorder.value;
    }
}
```
### TonePass
ここではマテリアルやシェーダーの設定、描画方法などを決めています。基本的にScriptableRenderPassを継承します。
```C#:TonePass.cs
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace VolumeScripts.ToneShader
{
    public class TonePass : ScriptableRenderPass
    {
        private static readonly int TempColorBufferId = UnityEngine.Shader.PropertyToID("_TempColorBuffer");
        private readonly Shader _shader;
        private Material _material;
        private Tone _tone;
        private RenderTargetIdentifier _renderTargetIdentifier;
        private RenderTargetIdentifier _tempRenderTargetIdentifier;

        public TonePass(RenderPassEvent renderPassEvent, Shader shader)
        {
            this.renderPassEvent = renderPassEvent;
            _shader = shader;
            if (_shader == null) return;
            _material = CoreUtils.CreateEngineMaterial(_shader);
        }

        private static readonly int ToneTempId = UnityEngine.Shader.PropertyToID("_ToneTempTex");
        private static readonly int DarkColorId = UnityEngine.Shader.PropertyToID("_DarkColor");
        private static readonly int NormalColorId = UnityEngine.Shader.PropertyToID("_NormalColor");
        private static readonly int LightColorId = UnityEngine.Shader.PropertyToID("_LightColor");
        private static readonly int DarkBorderId = UnityEngine.Shader.PropertyToID("_DarkBorder");
        private static readonly int NormalBorderId = UnityEngine.Shader.PropertyToID("_NormalBorder");

        private string RenderTag => "Tone";
        public void Setup(in RenderTargetIdentifier renderTargetIdentifier)
        {
            _renderTargetIdentifier = renderTargetIdentifier;
            _tempRenderTargetIdentifier = new RenderTargetIdentifier(TempColorBufferId);
        }
        
        private bool IsActive()
        {
            return _tone.IsActive;
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            if (_material == null || !renderingData.cameraData.postProcessEnabled) return;
            var volumeStack = VolumeManager.instance.stack;
            _tone = volumeStack.GetComponent<Tone>();
            if (_tone == null || !_tone.active || !IsActive()) return;

            var commandBuffer = CommandBufferPool.Get(RenderTag);
            Render(commandBuffer, ref renderingData);
            context.ExecuteCommandBuffer(commandBuffer);
            CommandBufferPool.Release(commandBuffer);
        }
        
        private void Render(CommandBuffer commandBuffer, ref RenderingData renderingData)
        {
            ref var cameraData = ref renderingData.cameraData;
            var source = _renderTargetIdentifier;
            var dest = _tempRenderTargetIdentifier;
            var desc = new RenderTextureDescriptor(cameraData.camera.scaledPixelWidth, cameraData.camera.scaledPixelHeight);
            desc.colorFormat = cameraData.isHdrEnabled ? RenderTextureFormat.DefaultHDR : RenderTextureFormat.Default;
            commandBuffer.GetTemporaryRT(TempColorBufferId, desc);
            
            _material.SetColor(DarkColorId, _tone.darkColor.value);
            _material.SetColor(NormalColorId, _tone.normalColor.value);
            _material.SetColor(LightColorId, _tone.lightColor.value);
            _material.SetFloat(DarkBorderId, _tone.darkBorder.value);
            _material.SetFloat(NormalBorderId, _tone.normalBorder.value);   
            
            commandBuffer.Blit(source, dest);
            commandBuffer.Blit(dest, source, _material);
            commandBuffer.ReleaseTemporaryRT(TempColorBufferId);
            
        }
    }
}
```

### ToneRenderFeature
これはURPのForwardRendererでシェーダーや描画方法を設定するプログラムです。基本的にScriptableRendererFeatureを継承します。
```C#:ToneRenderFeature.cs
using UnityEngine;
using UnityEngine.Rendering.Universal;

namespace VolumeScripts.ToneShader
{
    public class ToneRenderFeature : ScriptableRendererFeature
    {
        [System.Serializable]
        public class Setting
        {
            public RenderPassEvent renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
            public Shader shader;
        }

        public Setting setting = new Setting();

        private TonePass _pass;
        
        public override void Create()
        {
            this.name = "Tone";
            _pass = new TonePass(setting.renderPassEvent, setting.shader);
        }

        public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
        {
            _pass.Setup(renderer.cameraColorTarget);
            renderer.EnqueuePass(_pass);
        }
    }
}
```
## シェーダープログラム
### Tone（Shader）
今回実装するポストエフェクトの元となるシェーダープログラムです。URPのポストエフェクトではHLSL言語を使用して記述していきます。
```Tone.Shader
Shader "Hidden/Tone"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
	}
	HLSLINCLUDE
	#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
	#include "Packages/com.unity.render-pipelines.universal/Shaders/PostProcessing/Common.hlsl"

	sampler2D _MainTex;
	float4 _DarkColor;
	float4 _NormalColor;
	float4 _LightColor;
	float _DarkBorder;
	float _NormalBorder;

	half4 Frag(Varyings input) : COLOR
	{
		half4 color = tex2D(_MainTex, input.uv);
		const float grayScaleColor = color.r * 0.3 + color.g * 0.6 + color.b * 0.1;
		if(grayScaleColor < _DarkBorder) return _DarkColor; 
		else if(grayScaleColor < _NormalBorder) return _NormalColor;
		else return _LightColor;
	}
	
	ENDHLSL
	SubShader
	{
		Tags { "RenderType" = "Qpaque" "RenderPipeline" = "UniversalPipeline" }
		LOD 100
		Cull Off ZWrite Off ZTest Always

		Pass
		{
			HLSLPROGRAM
			#pragma vertex Vert
			#pragma fragment Frag
			ENDHLSL
		}
	}
}
```
# エディタ上の設定
## UniversalRenderPipeline
ここではRenderPipeline Assetの生成と各種設定を行っていきます。
まずProject → Rendering → Universal Render Pipeline → Pipeline Asset(Forward Renderer)で生成してきます。
![6bfa4bab3da0fc2bf9891c62e518473c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3127325/b7367801-a11f-f494-17a1-922b22aa12c4.png)

そうするとUniversal Render Pipeline AssetとForward Renderer Dataが生成されると思います。（設定したファイル名の後ろにかっこで自動的についてると思います）主に設定するのはForward Renderer Dataです。
![24a4a9c7246b08b6f4c3a8ee02e43259.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3127325/f2850ecf-2ccd-f5d0-3324-e2cf13cc48d2.png)

設定する項目しては一番下にあるAdd Renderer Featureから先ほど作ったToneRenderFeature.csをアタッチします。
そしてSettingの中にあるShaderにToneのシェーダープログラムをアタッチして設定終了です。
![1d976ab4b2d526d547a03f2309f680bd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3127325/92cec56a-762a-b6f4-f271-a1e9ed078498.png)

もしシェーダーを反映させるタイミングなどを変更したい場合はRender Pass Eventから対応したものを選択してください。今回のシェーダーは初期状態のBefore Rendering Post Processingで問題ないです。

次に生成したUniversal Render Pipeline Assetの設定とメインカメラの設定を確認していきます。
Renderer Listに先ほど設定したForward Renderer Dataがアタッチされているか確認してください。（おそらく生成時に自動的に入っているとは思います）
![607b9ac1642d83d87ef92fb7d6cfcba6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3127325/078b0a8e-0a4f-551c-7c06-5638b570e86b.png)

メインで使用しているカメラのコンポーネントからRendering → Post Processingにチェックが入っているか確認してください。これが入っていないとポストエフェクトが反映されません。
![5bb222ce662beee7d21b9529b573c381.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3127325/9d490e4c-8033-c87a-7289-85002c3860cf.png)


最後にプロジェクトの設定を変更していきます。
Edit → Project Settings → Graphicsから先ほど生成したUniversal Render Pipeline Assetをアタッチします。
![fe807c6d836600f72f588090e006b040.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3127325/498b8483-da85-0023-d885-e566945d0fb1.png)


## Volume
URPでポストエフェクトを使用する際に使うVolumeコンポーネントを設定していきます。
Hierarchy → Volumeから用途に合わせたVolumeオブジェクトを生成します。（特にこだわりがなければGlobal Volumeで大丈夫です。）
![cda425e48f31d93e649b1a328b9548cc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3127325/7ed6521a-14a2-c5a7-16a2-f8015f288628.png)

![0fc42d823075259777380b811b0f7d4c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3127325/71ab88c8-aa40-b1d7-fcaf-e81e44ec9fec.png)


# 実際に躓いた箇所
## ForwardRendererがない
Unityのバージョンが合っているか確認してください。バージョンによっては表記が変わっていたり、機能が変更されいる場合があります。（ありました）

## シェーダーが反映されない
これは原因が２種類ありました。一つ目はシェーダープログラム自体の記述ミスがある。二つ目はScriptableRenderPassを継承しているプログラムの記述ミスです。
### シェーダープログラムの記述ミス
原則としてScriptableRenderPassを継承したプログラム内で定義しているPropertyToID内の名前とシェーダープログラムの変数は一致させる必要があります。

:::note info
正しい例
どちらのプログラムにも`_DarkColor`がある。
```C#: ScriptableRenderPassを継承したプログラム
private static readonly int DarkColorId = UnityEngine.Shader.PropertyToID("_DarkColor");
```
```C#:シェーダープログラム
float4 _DarkColor;
```
:::

:::note alert
悪い例
ScriptableRenderPassを継承したプログラムには`_GreenColor`があるが、シェーダープログラムには`_GreenColor`は存在していない。
```C#: ScriptableRenderPassを継承したプログラム
private static readonly int DarkColorId = UnityEngine.Shader.PropertyToID("_GreenColor");
```
```C#:シェーダープログラム
float4 _DarkColor;
```
:::

### ScriptableRenderPassを継承したプログラムの記述ミス
上記のプログラム（TonePass.cs）内にもあるのですが、1つのテクスチャから別のものにコピーする（基本的にはエフェクトをカメラに反映するときという認識で大丈夫だと思います）際に以下のコードを書きます。
```C#
commandBuffer.Blit(Rendering.RenderTargetIdentifier source, Rendering.RenderTargetIdentifier dest);
```
しかし、マテリアルを反映させる関係上sourceとdestが逆になることがあります。実際にTonePass.csでは逆で実装しています。
```C#:TonePass.cs
commandBuffer.Blit(source, dest);
commandBuffer.Blit(dest, source, _material);
```
この原因としてはcommandBuffer.Blitを実行するとdestの部分がアクティブなレンダーターゲットになるためです。[Unity Documentation参照](https://docs.unity3d.com/ja/2020.3/ScriptReference/Rendering.CommandBuffer.Blit.html)
今回のdestはあくまで一時的なテクスチャのため一度カメラからのテクスチャをコピーして、それを元にマテリアルを反映させています。

### UniversalRenderPipelineの設定ミス
これに関しては原因がよくわかっていないのですが、Forward Rendererを複数個作成した状態だとうまいことシェーダーが反映されないということがありました。私の環境ではForward Rendererを1つだけにして、設定をすべて見直しすることで直りましたが完全な改善策というわけではないため今のところは1つに絞ることで対処しています。
原因がわかり次第追記していこうかなと思っています。

# まとめ
今回はURPでの自作ポストエフェクトの作成方法と各種設定の解説を行いました。ポストエフェクトが自作できると自由な表現が可能なのでぜひ色々試してみてください！また、実際に私が経験した躓き箇所が何かしらで役に立ったのであれば幸いです。最後まで読んでいただきありがとうございました！