【Unity】URPを拡張して、Normal描画パスを追加してみた

# はじめに
Unityの従来のレンダリングパイプラインでは、 **_CameraDepthNormalsTexture** を利用することで画面のノーマル情報を取得できます.



```test.shader
sampler2D _CameraDepthNormalsTexture;
```

この_CameraDepthNormalsTexture ですが、残念ながら **URP (Universal Render Pipeline)ではサポートされていません**

# 作ったもの
今回は勉強がてら、URPのレンダリングパイプラインを拡張してノーマル情報をとれるようにしてみました。

シェーダーに **_CameraWorldNormalTexture** プロパティを追加すると、ワールドスペースの法線情報が取得できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/caed5873-8db5-76d1-74bf-87d6bca07f8a.png)


# 環境
Universal RP 8.2.0
Unity 2020.1.2f1

# 実装方法
1. URPの```Lit.shader``` を改造して、メッシュ法線をレンダリングするパス(```NormalOnly```)を追加
2. ```ScriptableRendererFeature``` を利用して、自作パス(```NormalOnly```)を実行し、Normalのみレンダリング
3. レンダーターゲットに ```_CameraWorldNormalTexture``` を指定 

シェーダーに```_CameraWorldNormalTexture``` を定義することで、法線のレンダリング結果を取得できるようになります。


# STEP1 : URPを編集できるようにする
今回はURPパッケージの中身に手を加えたいので、URPを編集可能な状態にします。

Universal RPフォルダを、Assetsフォルダと同階層のPackagesフォルダの中にコピーするだけです。

>Unityプロジェクト
   ├─Assets
   ├─Library
   ├─**Packages**
   └─ProjectSettings

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/4c99c867-5234-3712-d311-e5568b8aaf2d.png)

参考 : ローカルパッケージのインストール
https://docs.unity3d.com/ja/2019.4/Manual/upm-ui-local.html


Packagesフォルダの中にUniversal RPパッケージを配置すると、Package Manager上で Universal RP に **In Development** ラベルが付きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/6ac2c8a4-6b89-6204-0496-33f155321fd9.png)

以上で、Universal RP を編集する準備が整いました。

# STEP2 : ノーマル描画HLSLの追加
まずは**NormalOnlyPass.hlsl** を作成します。
これは3DモデルのNormalだけを描画するHLSLシェーダーです。

```NormalOnlyPass.hlsl
#ifndef UNIVERSAL_DEPTH_ONLY_PASS_INCLUDED
#define UNIVERSAL_DEPTH_ONLY_PASS_INCLUDED

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

struct Attributes
{
    float4 positionOS   : POSITION;
    float3 normalOS     : NORMAL;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct Varyings
{
    float4 positionCS   : SV_POSITION;
    float3 normalWS     : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
    UNITY_VERTEX_OUTPUT_STEREO
};

Varyings NormalOnlyVertex(Attributes input)
{
    Varyings output = (Varyings)0;
    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

    output.positionCS = TransformObjectToHClip(input.positionOS.xyz); 
    output.normalWS = TransformObjectToWorldNormal(input.normalOS.xyz); // object space normal -> world space normal

    return output;
}

half4 NormalOnlyFragment(Varyings input) : SV_TARGET
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
    
    return half4(input.normalWS, 1);
}
#endif
```

## NormalOnlyPass.hlslの追加先
作成した ```NormalOnlyPass.hlsl``` は　Universal RP フォルダ内の **Shaders** フォルダの中に追加します。
同じ階層には```Lit.shader``` や ```DepthOnlyPass.hlsl``` などがあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/698087f9-1ef6-2184-3f04-a4cd4451cbc6.png)


# STEP3 : Lit.shaderにPassを追加
先ほど追加した ```NormalOnlyPass.hlsl``` は .shader ファイルに埋め込んで実行させる必要があります。
今回は、URPで最もスタンダードな **Lit.shader** から ```NormalOnlyPass.hlsl``` を実行させたいと思います。

補足 : URPプロジェクトのSampleSceneの3dモデルではLit.shaderが使われています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/02edb45a-c677-0754-4596-cb782d50fa8f.png)

```Lit.shader``` に以下のPassを追加します。

```hlsl:Lit.shaderに追加するPass
Pass
{
    Name "NormalOnly"
    Tags{"LightMode" = "NormalOnly"}

    ZWrite On
    Cull[_Cull]

    HLSLPROGRAM
    // Required to compile gles 2.0 with standard srp library
    #pragma prefer_hlslcc gles
    #pragma exclude_renderers d3d11_9x
    #pragma target 2.0

    #pragma vertex NormalOnlyVertex
    #pragma fragment NormalOnlyFragment

    //--------------------------------------
    // GPU Instancing
    #pragma multi_compile_instancing

    #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/Shaders/NormalOnlyPass.hlsl"
    ENDHLSL
}
```

上記Passの ```Tags{"LightMode" = "NormalOnly"}``` はレンダラーからレンダリング対象としてPassを識別させる際に必要な情報になります。


# STEP4 : レンダーパス(ScriptableRendererPass)を追加

.shaderに埋め込んだPassはそのままでは実行されません。Unityのレンダラーにレンダリングを実行させる必要があります。

レンダリングを実行させるには、 **ScriptableRendererFeature** と **ScriptableRenderPass** を利用します。


まずは ```NormalOnlyPass.cs```(ScriptableRenderPass) を追加します。
この ```NormalOnlyPass.cs``` はLit.shaderが持っている**NormalOnly**パスに対し、レンダリング命令を発行するものです。

```NormalOnlyPass.cs
using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class NormalOnlyPass : ScriptableRenderPass
{
    private const string m_ProfilerTag = "[DEV] Normal Only Pass"; // フレームデバッガ―上の名前表示
    private ProfilingSampler m_ProfilingSampler = new ProfilingSampler(m_ProfilerTag);
    private ShaderTagId m_ShaderTagId = new ShaderTagId("NormalOnly"); // NormalOnlyタグを持っているシェーダーパスを実行する
    private FilteringSettings m_FilteringSettings; // レンダリングのフィルタリング設定
    private RenderTargetHandle depthAttachmentHandle; // RenderTextureの識別用struct
    private RenderTextureDescriptor descriptor; // 作成するRenderTextureの情報を入れるstruct

    /// <summary>
    /// Create Normal Only Pass
    /// </summary>
    public NormalOnlyPass(RenderPassEvent evt, RenderQueueRange renderQueueRange, LayerMask layerMask)
    {
        m_FilteringSettings = new FilteringSettings(renderQueueRange, layerMask);
        renderPassEvent = evt;
    }
    
    /// <summary>
    /// Configure the pass
    /// </summary>
    public void Setup(
        RenderTextureDescriptor baseDescriptor,
        RenderTargetHandle depthAttachmentHandle)
    {
        this.depthAttachmentHandle = depthAttachmentHandle;
        baseDescriptor.colorFormat = RenderTextureFormat.ARGB32; // ARGB32は　RGBAチャンネルそれぞれに8bitずつ入るテクスチャフォーマット
        baseDescriptor.depthBufferBits = 32; // デプスバッファを32bitにする(Zテストに必要)
        
        baseDescriptor.msaaSamples = 1; // MSAAは使用しない
        descriptor = baseDescriptor;
    }
    
    // This method is called before executing the render pass.
    // It can be used to configure render targets and their clear state. Also to create temporary render target textures.
    // When empty this render pass will render to the active camera render target.
    // You should never call CommandBuffer.SetRenderTarget. Instead call <c>ConfigureTarget</c> and <c>ConfigureClear</c>.
    // The render pipeline will ensure target setup and clearing happens in an performance manner.
    public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
    {
        // RenderTextureの確保
        cmd.GetTemporaryRT(depthAttachmentHandle.id, descriptor, FilterMode.Point);
        ConfigureTarget(depthAttachmentHandle.Identifier());
        ConfigureClear(ClearFlag.All, Color.black);
    }

    // Here you can implement the rendering logic.
    // Use <c>ScriptableRenderContext</c> to issue drawing commands or execute command buffers
    // https://docs.unity3d.com/ScriptReference/Rendering.ScriptableRenderContext.html
    // You don't have to call ScriptableRenderContext.submit, the render pipeline will call it at specific points in the pipeline.
    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        CommandBuffer cmd = CommandBufferPool.Get(m_ProfilerTag);
        using (new ProfilingScope(cmd, m_ProfilingSampler))
        {
            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();

            var sortFlags = renderingData.cameraData.defaultOpaqueSortFlags;
            var drawSettings = CreateDrawingSettings(m_ShaderTagId, ref renderingData, sortFlags);
            drawSettings.perObjectData = PerObjectData.None;

            context.DrawRenderers(renderingData.cullResults, ref drawSettings, ref m_FilteringSettings);

        }
        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }

    /// Cleanup any allocated resources that were created during the execution of this render pass.
    public override void FrameCleanup(CommandBuffer cmd)
    {
        if (cmd == null)
            throw new ArgumentNullException("cmd");

        // RenderTextureの解放
        if (depthAttachmentHandle  != RenderTargetHandle.CameraTarget)
        {
            cmd.ReleaseTemporaryRT(depthAttachmentHandle.id);
            depthAttachmentHandle = RenderTargetHandle.CameraTarget;
        }
    }
}
```

# STEP5 : ScriptableRendererFeatureを追加

作成した```NormalOnlyPass```はそのままでは実行されません。
Unityのレンダラーに認識させる必要があります。

認識させるためには以下のような手順を踏みます
1. ```ScriptableRendererFeature``` を作成
2. ```ScriptableRendererFeature``` に ```NormalOnlyPass``` を組み込んで実行させる
3. ```ForwardRendererData``` に ```ScriptableRendererFeature``` を登録


## ScriptableRendererFeatureの作成

以下の **MyRendererFeature.cs** を作成し、Assets/フォルダ以下の好きな場所に配置します。

```MyRendererFeature.cs
using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class MyRendererFeature : ScriptableRendererFeature
{
    [SerializeField] private MyFeatureSettings settings = new MyFeatureSettings();
    private NormalOnlyPass normalOnlyPass = null;
    private LayerMask layerMask;
    private RenderTargetHandle m_NormalTexture;

    [Serializable]
    public class MyFeatureSettings
    {
        public RenderPassEvent renderPassEvent = RenderPassEvent.BeforeRenderingOpaques; 
        public LayerMask layerMask = -1; // Everything
    }

    public override void Create()
    {
        normalOnlyPass = new NormalOnlyPass(settings.renderPassEvent, RenderQueueRange.opaque, settings.layerMask);
        m_NormalTexture.Init("_CameraWorldNormalTexture"); // 内部では Shader.PropertyToID() が呼ばれている
    }

    // Here you can inject one or multiple render passes in the renderer.
    // This method is called when setting up the renderer once per-camera.
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        var cameraData = renderingData.cameraData;
        if (cameraData.isPreviewCamera || cameraData.isSceneViewCamera) return;
        
        var cameraTargetDescriptor = renderingData.cameraData.cameraTargetDescriptor;
        normalOnlyPass.Setup(cameraTargetDescriptor, m_NormalTexture);
        renderer.EnqueuePass(normalOnlyPass);
    }
}
```


<details><summary>補足 : レンダリング先の指定について</summary><div>

### 補足 : レンダリング先の指定について
以下のコードで、シェーダープロパティの```_CameraWorldNormalTexture``` を作成しています。
```Init``` メソッドの内部では ```Shader.PropertyToID()``` が呼ばれているので、実際にはint型のIDとしてシェーダープロパティが格納されます。

```MyRendererFeature.cs
m_NormalTexture.Init("_CameraWorldNormalTexture"); // 内部では Shader.PropertyToID() が呼ばれている
```

以下のコードでは```NormalOnlyPass``` に ```m_NormalTexture``` を登録しています。
これにより、```_CameraWorldNormalTexture``` に対してワールド法線のレンダリングが行われるようになります。

```MyRendererFeature.cs
normalOnlyPass.Setup(cameraTargetDescriptor, m_NormalTexture);
renderer.EnqueuePass(normalOnlyPass);
```

</div></details>

## RendererFeatureの登録
```MyRendererFeature.cs``` を作成したら、
次は ```ForwardRendererData``` アセット に ```MyRendererFeature``` を登録します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/3ba254f0-c12c-285b-4466-17e68bdfdb19.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/4146bcf0-12f2-c333-47dd-cc0470582278.png)

以上で URPの拡張は終わりです。

# Frame Debuggerで描画を確認する
**Frame Debugger** を見ると、 ```NormalOnlyPass``` のレンダリング結果を見ることができます。

## Frame Debuggerの起動
Window -> Analysis -> Frame Debugger を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/62016416-29a7-fc7f-49e1-c0450d7d5298.png)

ウィンドウ左上のEnable をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/9f83ff35-5971-417a-3048-45e0c7371900.png)

ドローコールの一覧が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/abd79c60-2372-d818-9938-3df47a169715.png)

**[DEV] Normal Only Pass** という項目を選択すると、今回の ```NormalOnlyPass``` が描画したものを見ることができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/fa54e4d1-7a29-4545-a552-753bf85d3b9b.png)

モデルの法線情報だけが表示されていることが確認できます。ここに表示されているのはワールド空間の法線情報です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/e110a18c-6d33-9ce9-e545-1a0a3a6683bf.png)

# ShaderGraphでノーマル情報を取得してみる
フレームデバッガーの上の方に注目すると、**RenderTarget _CameraWorldNormalTexture** という表示があります。

**「この描画パスのレンダリングのターゲットは ```_CameraWorldNormalTexture``` です」** 　ということを意味するメッセージです。

この ```_CameraWorldNormalTexture``` をShaderGraphから取得してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/4ceb7371-7218-5ac3-b2b4-d9975753d168.png)


## ShaderGraphの作成
メニューから
Create -> Shader -> Unlit Graph 
を選択して、Unlitのシェーダーグラフを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/72444ddb-574e-49de-d157-49e622d6e023.png)

##_CameraWorldNormalTexture の追加
シェーダープロパティに以下のようなTexture2Dプロパティを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/7a22d67d-6303-6298-8cc2-6ebf8dc064b9.png)

NormalTexをSample Texture 2Dノードに接続し、Unlit MasterのColorへ出力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/72eb5109-d6ea-ba5d-94c3-c602d169b618.png)

Unlit Master の設定はデフォルトのままで大丈夫です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/c9560588-d3b0-c074-0ca9-c6957e465ffc.png)

```NormalOnlyPass```にてワールド法線のレンダリングのタイミングを ```RenderPassEvent.BeforeRenderingOpaques``` に設定しているため、
色が正しく取得できるはずです。

シェーダーグラフ絡めてリアルを作成し、適当なオブジェクトに貼り付けると、GameViewでワールド法線が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44288/32c884b7-d95e-884a-c4e8-b4d226e832eb.png)


## 補足 : Sceneビューでワールドノーマルが表示されない理由について
Sceneビューだと、```NormalOnlyPass```の描画がチラチラしてしまう問題が発生してしまうため、
シーンカメラ(プレビューカメラ)では```NormalOnlyPass```をスキップしています。

```MyRendererFeature.cs
// Here you can inject one or multiple render passes in the renderer.
// This method is called when setting up the renderer once per-camera.
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
    var cameraData = renderingData.cameraData;
    if (cameraData.isPreviewCamera || cameraData.isSceneViewCamera) return;

    var cameraTargetDescriptor = renderingData.cameraData.cameraTargetDescriptor;
    normalOnlyPass.Setup(cameraTargetDescriptor, m_NormalTexture);
    renderer.EnqueuePass(normalOnlyPass);
}
```