HDRP用のポストエフェクトを書く


HDRPでは、ポストエフェクトはPost-processing Stack v2ではなく、Volumeに統合されています。本記事ではVolumeにカスタムポストエフェクトを追加する方法をまとめます。
内容は[公式ドキュメント](https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@7.1/manual/Custom-Post-Process.html)に基づいているので、そちらもあわせてどうぞ。

# 1. 必要なファイルを作る
カスタムポストエフェクトを書くには、以下の二つのファイルを作成します。

 - CustomPostProcessVolumeComponentとIPostProcessComponentを継承したクラス
    - Projectウィンドウで右クリック > Create > Rendering > C# Post Process Volumeで作成
 - 実際に処理を行うシェーダー（HLSL）
    - Projectウィンドウで右クリック > Create > Shader > HDRP > Post Processで作成

# 2. スクリプトを書く

作成したC# スクリプトを編集します。

```csharp:GrayScale.cs
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.HighDefinition;
using System;

[Serializable, VolumeComponentMenu("Post-processing/Custom/GrayScale")]
public sealed class GrayScale : CustomPostProcessVolumeComponent, IPostProcessComponent
{
    [Tooltip("Controls the intensity of the effect.")]
    public ClampedFloatParameter intensity = new ClampedFloatParameter(0f, 0f, 1f);

    Material m_Material;

    public bool IsActive() => m_Material != null && intensity.value > 0f;

    public override CustomPostProcessInjectionPoint injectionPoint => CustomPostProcessInjectionPoint.AfterPostProcess;

    public override void Setup()
    {
        if (Shader.Find("Hidden/Shader/GrayScale") != null)
            m_Material = new Material(Shader.Find("Hidden/Shader/GrayScale"));
    }

    public override void Render(CommandBuffer cmd, HDCamera camera, RTHandle source, RTHandle destination)
    {
        if (m_Material == null)
            return;

        m_Material.SetFloat("_Intensity", intensity.value);
        m_Material.SetTexture("_InputTexture", source);

        HDUtils.DrawFullScreen(cmd, m_Material, destination);
    }

    public override void Cleanup() => CoreUtils.Destroy(m_Material);

}
```

#### VolumeComponentMenu
```csharp
[Serializable, VolumeComponentMenu("Post-processing/Custom/GrayScale")]
```

VolumeComponentMenuAttributeでは、VolumeのAdd Overrideボタンを押したときの表示階層を定義します。


#### Volumeに露出するプロパティを定義する

```csharp
    [Tooltip("Controls the intensity of the effect.")]
    public ClampedFloatParameter intensity = new ClampedFloatParameter(0f, 0f, 1f);
```

**ClampedFloatParameter**は、エディターではスライダーで表示されます。ほかにもFloatParameter、Vector3Parameterなどいろいろなものが用意されており、[リファレンス](https://docs.unity3d.com/Packages/com.unity.render-pipelines.core@7.2/api/UnityEngine.Rendering.VolumeParameter-1.html)を見ると一覧があります。

#### IsActive()
```csharp
    public bool IsActive() => m_Material != null && intensity.value > 0f;
```
IsActive()がfalseを返すと、エフェクトの実行がスキップされます。パラメーターによってエフェクトの実行をスキップできる場合は、スキップして無駄な処理を省きましょう。

#### エフェクトの実行タイミングを定義する
```csharp
    public override CustomPostProcessInjectionPoint injectionPoint => CustomPostProcessInjectionPoint.AfterPostProcess;
```
**CustomPostProcessInjectionPoint**はエフェクトが実行されるタイミングを定義します。指定できるタイミングには

- AfterOpaqueAndSky
- BeforePostProcess
- AfterPostProcess

があります。[ここ](https://github.com/Unity-Technologies/ScriptableRenderPipeline/blob/master/com.unity.render-pipelines.high-definition/Documentation~/Images/HDRP-frame-graph-diagram.png)とかを見るとパイプラインのどこにあたるかがわかると思います。

#### シェーダーの読み込み
```csharp
    public override void Setup()
    {
        if (Shader.Find("Hidden/Shader/GrayScale") != null)
            m_Material = new Material(Shader.Find("Hidden/Shader/GrayScale"));
    }
```

エフェクトに使用するシェーダーからマテリアルを作ります。ここのシェーダー名は先ほど作成したシェーダーの名前を指定します。

#### Render()
```csharp
    public override void Render(CommandBuffer cmd, HDCamera camera, RTHandle source, RTHandle destination)
    {
        if (m_Material == null)
            return;

        m_Material.SetFloat("_Intensity", intensity.value);
        m_Material.SetTexture("_InputTexture", source);

        HDUtils.DrawFullScreen(cmd, m_Material, destination);
    }
```
実際に毎フレームの描画時に呼び出されるメソッドです。ImageEffectやLegacy RPにおけるCommandBufferと同じノリで、sourceからdestinationに書き込めばいいという感じです。ただし、シェーダーの実行には**HDUtils.DrawFullScreen**を使用しますが、これには入力テクスチャの指定がないため、テクスチャの入力はMaterialの**SetTexture**や、**CommandBuffer.SetGlobalTexture**から行う必要があります。

#### Cleanup()
```csharp
    public override void Cleanup() => CoreUtils.Destroy(m_Material);
```
リソースの開放を行います。

# 3. シェーダーを書く
```hlsl
Shader "Hidden/Shader/GrayScale"
{

    HLSLINCLUDE

    #pragma target 4.5
    #pragma only_renderers d3d11 ps4 xboxone vulkan metal switch

    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
    #include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl"
    #include "Packages/com.unity.render-pipelines.high-definition/Runtime/PostProcessing/Shaders/FXAA.hlsl"
    #include "Packages/com.unity.render-pipelines.high-definition/Runtime/PostProcessing/Shaders/RTUpscale.hlsl"

    struct Attributes
    {
        uint vertexID : SV_VertexID;
        UNITY_VERTEX_INPUT_INSTANCE_ID
    };

    struct Varyings
    {
        float4 positionCS : SV_POSITION;
        float2 texcoord   : TEXCOORD0;
        UNITY_VERTEX_OUTPUT_STEREO
    };

    Varyings Vert(Attributes input)
    {
        Varyings output;
        UNITY_SETUP_INSTANCE_ID(input);
        UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
        output.positionCS = GetFullScreenTriangleVertexPosition(input.vertexID);
        output.texcoord = GetFullScreenTriangleTexCoord(input.vertexID);
        return output;
    }

    // List of properties to control your post process effect
    float _Intensity;
    TEXTURE2D_X(_InputTexture);

    float4 CustomPostProcess(Varyings input) : SV_Target
    {
        UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
        uint2 positionSS = input.texcoord * _ScreenSize.xy;
        float3 outColor = LOAD_TEXTURE2D_X(_InputTexture, positionSS).xyz;
        return float4(lerp(outColor, Luminance(outColor).xxx, _Intensity), 1);
    }

    ENDHLSL

    SubShader
    {
        Pass
        {
            Name "GrayScale"
            ZWrite Off
            ZTest Always
            Blend Off
            Cull Off

            HLSLPROGRAM

                #pragma fragment CustomPostProcess
                #pragma vertex Vert

            ENDHLSL
        }
    }

    Fallback Off
}
```

シェーダーはHLSLで記述します。いつものCg/HLSLと言語自体の構文はほとんど同じですが、様々なプラットフォームに対応する都合上、いくつかの命令はHDRP側で定義されたマクロを使用します。

例えば、テクスチャのサンプリングひとつをとっても、

```
TEXTURE2D(_InputTexture)
```

でテクスチャを定義し、

```
LOAD_TEXTURE2D_X(_InputTexture, positionSS)
```

でピクセルを参照します。さらにこの座標系はUV ( (0,0) ~ (1,1) ) ではなく、ピクセル単位です。
このマクロが曲者で、どういうときにどういうマクロを使用するかは、**ソースとにらめっこしながらなんとなく把握するしかない**ので、includeしているHLSLなどを片手に書いていくのがよいです。

# 4. エフェクトを登録する
さて、スクリプトとシェーダーの両方が準備できたら、HDRP側にエフェクトを登録してあげる必要があります。
**Project Settings**を開き、**HDRP Default Settings**を開きます。

下の方に**Custom Post Process Orders**があるので、スクリプトで指定したタイミングのところに、作成したエフェクトを登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124429/3a7a31a6-8ebb-cafe-645f-bd1d9004aaac.png)

# 5. 使う
Volumeから追加して使えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124429/09d486b0-c0a5-c490-4323-1cc333041771.png)