UnityでRenderer Feature使ってみたい

この記事は [Akatsuki Advent Calendar 2021](https://adventar.org/calendars/6566) の18日目の記事です．

# はじめに

皆さんお久しぶりです！今年もAdvent Calendarの季節がやってきましたね．
前回の投稿からまたもや1年経ってますね笑．
2019年は [Unityでシェーダー描いてみたい](https://qiita.com/ShaderError/items/f349110679a9ec47cca9)
2020年は [Unityでポストプロセス描いてみたい](https://qiita.com/ShaderError/items/7213818bfde469a84f5a)
を書きました．今回もグラフィックス周りのことを紹介しようと思います．

Scriptable Render Pipelineが提供されて，以下の2つのパイプラインがUnityから配布されています．
・Universal Render Pipeline(URP)
・High Definition Render Pipeline(HDRP)
Scriptable Render Pipelineで構築されているので，カスタマイズが容易にできる点が魅力的です．
仕事や個人開発でよくURPを利用します．

というわけで，本稿では **Renderer Feature** について紹介します．

# Renderer Featureって何？

URPで提供されている機能で，
ユーザーが自由にカスタマイズした描画パスを追加することができます．

Renderer Featureを利用すれば，
・カスタムポストプロセス
・レイトレーシング
・縮小バッファ
・複数パスのシェーダーの実行
など，URPのレンダリングパイプライン上の好きなタイミングでカスタム描画パスを追加できます．

Unity上で独自のパイプラインを構築するのに非常に便利で強力な機能なので，是非覚えていただけると嬉しいです！

# Renderer Featureの使い方

Renderer Featureに必要なものは大きくわけて以下の2つです．
・Scriptable Renderer Feature
・Scriptable Render Pass

## Scriptable Renderer Feature

Scriptable Renderer Featureは，上記で紹介したRenderer Featureの実体です．
Scriptable Renderer Featureでユーザーが自由にカスタマイズしたRender Passを描画パイプライン上に追加します．

中身は以下のようになっています．

```C#:SampleRendererFeature.cs
using UnityEngine;
using UnityEngine.Rendering.Universal;

public class SampleRendererFeature : ScriptableRendererFeature
{
    [SerializeField] private float _sampleFloat = 10f;
    [SerializeField] private string _sampleString = "test";

    public override void Create()
    {
        // Renderer Feature が生成されたときの処理
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        // Render Pass をレンダリングパイプラインに追加する
    }
}
```

上記をURPのRenderer Dataに追加すると，以下のようになります．

|URPのForward Renderer Data|
|:-:|
|![Forward Renderer Data](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528036/7e01cd2f-9649-fb46-79b8-73a07753e934.png)|

## Scriptable Render Pass

Scriptable Render Passは，Renderer Featureによって追加される描画パスです．
Scriptable Render Passの中で，具体的にどんな描画が行われるのかを記載します．

中身は以下のようになっています．

```C#:SampleRenderPass.cs
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class SampleRenderPass : ScriptableRenderPass
{
    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        // 具体的な描画内容
    }
}
```

## 適当な描画が実行できるように書き換える

上記のScriptable Renderer FeatureとScriptable Render Passを使って，
何か描画をレンダリングパイプラインに追加してみましょう．

描画の具体的な内容は以下のようにします．
・Shader TagがSample Passのものを対象にする
・描画先は内部で用意するRender Textureとする


SampleRendererFeature.csとSampleRenderPass.csを以下のように書き換えます．

```C#:SampleRendererFeature.cs
using UnityEngine;
using UnityEngine.Rendering.Universal;

public class SampleRendererFeature : ScriptableRendererFeature
{
    // Render Texture の横幅
    [SerializeField] private int _width = 960;

    // Render Texture の縦幅
    [SerializeField] private int _height = 540;

    // レンダリングパイプラインに追加する描画パス
    private SampleRenderPass _sampleRenderPass;

    public override void Create()
    {
        // 描画パスの生成
        _sampleRenderPass = new SampleRenderPass
        {
            renderPassEvent = RenderPassEvent.AfterRendering
        };
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        // 描画パスのセットアップと，レンダリングパイプラインへの追加
        _sampleRenderPass.Setup(_width, _height);
        renderer.EnqueuePass(_sampleRenderPass);
    }
}
```

```C#:SampleRenderPass.cs
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class SampleRenderPass : ScriptableRenderPass
{
    private RenderTexture _renderTexture;

    // Render Texture の横幅
    private int _width;

    // Render Texture の縦幅
    private int _height;

    public void Setup(int width, int height)
    {
        _width = width;
        _height = height;
    }

    // 描画の設定
    public override void Configure(CommandBuffer commandBuffer, RenderTextureDescriptor cameraTextureDescriptor)
    {
        // Render Texture を生成する
        _renderTexture = RenderTexture.GetTemporary(_width, _height);

        // 描画先を Render Texture にする
        ConfigureTarget(new RenderTargetIdentifier(_renderTexture));

        // クリアフラグの設定(描画されなかった領域は青色で塗りつぶす)
        ConfigureClear(ClearFlag.Color, Color.blue);
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        // コマンドバッファの取得
        var commandBuffer = CommandBufferPool.Get();

        // あとでこの描画を確認する際にわかりやすくするために，"Sample"で切り分ける
        using (new ProfilingScope(commandBuffer, new ProfilingSampler("Sample")))
        {
            // 事前に実行しておくコマンドバッファ
            context.ExecuteCommandBuffer(commandBuffer);
            commandBuffer.Clear();

            // 描画に必要なカメラ等の設定
            var camera = renderingData.cameraData.camera;
            var sortingSettings = new SortingSettings(camera);
            var drawingSettings = new DrawingSettings(new ShaderTagId("SamplePass"), sortingSettings);
            var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);

            // 上記設定で描画の実行
            context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref filteringSettings);
        }

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

    public override void OnCameraCleanup(CommandBuffer cmd)
    {
        // Render Texture をリリース
        if (_renderTexture)
        {
            RenderTexture.ReleaseTemporary(_renderTexture);
        }
    }
}
```

また，使用するシェーダーは以下の通りです．

```SamplePass.shader
Shader "SamplePass"
{
    Properties
    {
        _Color ("Color", Color) = (1, 1, 1, 1)
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" "LightMode"="SamplePass" }

        Pass
        {
            HLSLPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Input.hlsl"

            CBUFFER_START(UnityPerMaterial)
                half4 _Color;
            CBUFFER_END

            struct VertexInput
            {
                float4 positionOS : POSITION;
            };

            struct VertexOutput
            {
                float4 positionCS : SV_POSITION;
            };

            VertexOutput vert(VertexInput v)
            {
                VertexOutput o = (VertexOutput)0;

                o.positionCS = mul(unity_MatrixVP, mul(unity_ObjectToWorld, v.positionOS));

                return o;
            }

            half4 frag(VertexOutput i) : SV_Target
            {
                return _Color;
            }

            ENDHLSL
        }
    }
}
```

## 描画結果

上記のシェーダーでマテリアルを作り，適当なオブジェクト(今回はCube)に貼り付けると以下のようになります．

|レンダリング結果…？|
|:-:|
|![レンダリング結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528036/2161ff6d-e069-6073-aeea-3661ff9703c4.png)|

描画先を内部で定義したRender Textureにしたので，もちろん何も映りません．
Frame Debuggerで確認してみましょう．

|描画先のRender Texture|
|:-:|
|![描画先のRender Texture](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528036/ffdf1605-c673-6f2d-1831-075a181140ca.png)|

ちゃんとRender Textureに描画されているのがわかります．

## 描画結果から分かること

本稿の例では**「Shader Tagを指定して，任意のシェーダーを任意のバッファサイズにオフスクリーンレンダリング」**させました．

例えば以下のようにすると，半透明の負荷を下げる縮小バッファが可能になります．
①Shader Tagで半透明を指定
②1/4のバッファサイズにオフスクリーンレンダリング
③ポストエフェクトでスクリーンバッファに合成

このようにRenderer Featureでは様々な描画アプローチをすることができます．
是非，Renderer Featureを改修して縮小バッファをやってみてください！

# まとめ

本稿ではRenderer Featureについて紹介しました．
Renderer Featureでレンダリングパイプラインを拡張すれば，どんなことでもできそうですね！
(時間があればLumenとかRenderer Featureで作ってみたいですね)