こんにちは、Unityエンジニアのオオバです。

お悩みさん
お悩みさん
  • URPの拡張をしてみたいけどやり方がわからない
  • そもそもURP拡張の始め方がわからない
  • オオバ
    オオバ
    本記事ではこれらの悩みを解決します。

    結論から話すと Universal Render Pipeline(以下:URP) は描画処理(パス)を簡単に追加することができます。
    URPの拡張をこれからやっていきたい人に向けてわかりやすく紹介していきたいと思います。URPの拡張と聞くと「シェーダー」が出てきそうな予感がしますが安心してください。 今回はシェーダーは登場しません。 あくまでURPを拡張する基本的な方法を解説していきます。

    ちなみに今回作るものはコチラです。

    【超基本編】URPに独自のパスを追加する方法_0

    解像度を徐々に下げていくモザイクのような表現を作っていきます。

    この表現をシェーダーを使うことなく実装してきます。

    最小コードでURPの描画拡張を作っています。 URP拡張を始めたことがない人にとって有益な記事となるため、ぜひ最後まで読んでみてください。

    👉DOTweenの教科書を読んでUnityアニメーションをプログラミングしてみよう!

    【URP拡張の準備】PassとFeatureの用意

    まずURPを拡張する上で2つのオブジェクトを用意します。PassとFeatureです。
    具体的には「ScriptableRenderPassクラス」と「ScriptableRendererFeatureクラス」を使います。

    「ScriptableRenderPass」 は実際の描画処理を実装したクラスです。一方 「ScriptableRendererFeature」 はURPに追加する機能の単位でありPassの生成する役割を担っています。現時点ではよくわからないかもしれません。詳しく後述しますので安心してください。

    URP拡張の準備としてこれらのクラスを継承した2つのクラスを用意します。

    ScriptableRenderPassクラスを継承した、 DownSamplingRenderPassクラス 。ScriptableRendererFeatureクラスを継承した DownSamplingRenderFeatureクラス を用意しました。

    以降の章で詳しく解説していきます。

    手順①Passの実装

    最初にPass(パス)の実装をしていきます。Passには2つの役割があります。1つは 「描画処理の実装」 。もう1つはその 「描画処理の実行タイミングの定義」 です。

    Passの全体像を確認するために処理を割愛したソースコードを確認してみます。

    💻ソースコード : DownSamplingRenderPass.cs抜粋
    public class DownSamplingRenderPass : ScriptableRenderPass  
    {
        public override void Execute(ScriptableRenderContext context,  
            ref RenderingData renderingData)  
        {
            // ①描画処理を記述  
        }
    
        /// <summary>Constructor</summary>  
        public DownSamplingRenderPass() =>  
            // ②描画タイミングを決めるところ  
            renderPassEvent = RenderPassEvent.AfterRenderingTransparents;  
    }
    

    ソースコード内のコメントアウトの通り、Executeメソッド内で描画処理を、renderPassEventに描画タイミングを設定するのです。

    では次の章で、今回作成するモザイクの描画処理を実装していきます。

    モザイクの描画処理を実装する

    先の解説の通り、ScriptableRenderPass内の Executeメソッド に描画処理を記述していきます。最初に処理のイメージを図にしてみました。

    処理のイメージ図

    【超基本編】URPに独自のパスを追加する方法_1

    処理の手順は以下です。

    1. 小さいサイズのRenderTextureを生成
    2. カメラ画像をRenderTextureにコピー
    3. RenderTextureを元のカメラのサイズに引き伸ばしてコピー

    これらの処理を実装したソースコードがこちらです。ソースコード内にコメントアウトで解説しているため、参考にしてください。

    DownSamplingRenderPass Executeメソッド抜粋
    public override void Execute(ScriptableRenderContext context,  
        ref RenderingData renderingData)  
    {
        // 描画処理を記述  
        // CommandBuffer作成(描画コマンドです)  
        var commandBuffer = CommandBufferPool.Get(CommandBufferName);  
    
        var cameraData = renderingData.cameraData;  
        // 現在描画しているカメラの解像度を 「_downSample」で除算  
        var w = cameraData.camera.scaledPixelWidth / _downSample;  
        var h = cameraData.camera.scaledPixelHeight / _downSample;  
    
        // 小さいサイズのRenderTextureを生成  
        commandBuffer.GetTemporaryRT(RenderTextureId,  
            w, h, 0, FilterMode.Point, RenderTextureFormat.Default);  
    
        // 現在のカメラ画像をRenderTextureにコピー  
        commandBuffer.Blit(_currentTarget, RenderTextureId);  
    
        // RenderTextureを現在のRenderTarget(カメラ)サイズに引き伸ばしてコピー(モニタに写し出される)  
        commandBuffer.Blit(RenderTextureId, _currentTarget);  
    }
    

    表示解像度を小さくしたあと元のサイズに引き伸ばしているためモザイクされた状態に見えるという仕組みです。だから今回の場合はシェーダーは不要なのです。

    CommandBufferでさまざまな処理をさせる

    ソースコード内に登場した「CommandBuffer」 は描画コマンドの単位です。描画処理を作っていく上で非常に重要な存在です。

    CommandBufferに処理を詰め込みGPUにわたすことで描画処理が実行されます。先のソースコードでは以下の2つの処理をCommandBufferに行っています。

    CommandBufferには他にもたくさんの処理が定義されていますので、ぜひチェックしてみてください。

    Passの実行タイミングを決める

    次にこのモザイク描画処理を実行するタイミングを決めます。このタイミングはとても重要です。
    先のソースコード内に 「RenderPassEvent」 が登場しました。描画処理のタイミングはPassのコンストラクタで設定します。

    public DownSamplingRenderPass() =>  
        renderPassEvent = RenderPassEvent.AfterRenderingTransparents;  
    

    「RenderPassEvent」とはEnumです。RenderPassEventにはさまざまな描画タイミングが定義されており、変数「renderPassEvent」に指定することでPassの実行タイミングを設定できます。

    今回は 「AfterRenderingTransparents」 を指定しました。 「透明オブジェクトを描画した後に実行する」 という処理になります。

    以上でPassの実装は完了です。

    手順②URP拡張の最小単位「Feature」の実装

    次にFeatureを実装していきます。最初にFeatureの2つの役割について紹介します。

    2つの役割とは ①Passの生成②生成したPassをScriptableRendererに渡すこと です。新たに「ScriptableRenderer」というキーワードが登場しました。描画処理を記述したScriptableRenderPassをScriptableRendererにわたすことで初めて描画処理が走ります。

    FeatureはScriptableRendererとの橋渡しを担っているのです。 ではFeatureの2つの役割を1つずつ確認していきます。

    ①Passの生成

    PassはScriptableRenderFeatureの「Createメソッド」をオーバーライドしてPassを生成します。

    DownSamplingRenderPass _renderPass;  
    
    public override void Create() =>  
        // Passの作成  
        _renderPass = new DownSamplingRenderPass();  
    

    作ったパスは後に実行するメソッド内で使用するためインスタンス変数の中に保持しておきましょう。

    ②生成したPassをScriptableRendererに渡す

    次に生成したPassをScriptableRendererに渡します。

    public override void AddRenderPasses(  
        ScriptableRenderer renderer, ref RenderingData renderingData)  
    {
        // ScriptableRendererにPassを渡す  
        renderer.EnqueuePass(_renderPass);  
    }
    

    ScriptableRenderFeatureの「AddRenderPassesメソッド」をオーバーライドしてPassをScriptableRendererに渡します。

    以上の2つの役割を実装したソースコードは以下です。

    💻ソースコード : DownSamplingRenderFeature.cs抜粋
    public class DownSamplingRenderFeature : ScriptableRendererFeature  
    {
        DownSamplingRenderPass _renderPass;  
    
        public override void Create() =>  
            // Passの作成  
            _renderPass = new DownSamplingRenderPass();  
    
        public override void AddRenderPasses(  
            ScriptableRenderer renderer, ref RenderingData renderingData)  
        {
            // ScriptableRendererにPassを渡す  
            renderer.EnqueuePass(_renderPass);  
        }
    }
    

    つまりFeatureとPassの関係は以下の図で表せます。

    【超基本編】URPに独自のパスを追加する方法_2

    Featureの中でPassを生成し、ScriptableRendererにPassを渡します。今回パスは1つしか扱いませんが、 Featureは複数のパスを管理することができます。

    FeatureをRendererに登録

    作成したFeatureを動かすためにRendererに登録する必要があります。

    【超基本編】URPに独自のパスを追加する方法_3

    URPのForwardRendererにFeatureを追加していきます。

    ForwardRendererのインスペクタ

    【超基本編】URPに独自のパスを追加する方法_4

    Add Renderer Feature をクリックして作成したDown Sampling Render Featureを追加します。

    ForwardRendererのインスペクタ

    【超基本編】URPに独自のパスを追加する方法_5

    するとFeatureはRendererに適用されます。

    以上で作成したPass、Featureが共に描画されるようになりました。

    Passの描画(Execute)前にパラメータを渡す

    今回はPass側に 「SetParamメソッド」を定義して描画前にFeatureからパラメータを渡しように実装しています。

    💻ソースコード : DownSamplingRenderFeature.csの抜粋
    public override void AddRenderPasses(  
        ScriptableRenderer renderer, ref RenderingData renderingData)  
    {
        // 描画処理前にパラメータをPassに渡す  
        _renderPass.SetParam(renderer.cameraColorTarget, downSample);  
        renderer.EnqueuePass(_renderPass);  
    }
    

    具体的に描画前のパスに渡しているのは次の2つ「カメラ情報」と「ダウンサンプル値」です。

    カメラ情報とはカメラが写している画像情報 です。カメラの解像度はExecuteメソッドの引数RenderingDataから取得可能です。「ダウンサンプル値」とはモザイク表現するための値でこの値を毎フレーム変化させることでモザイクアニメーションを表現しています。

    「描画前のPassに対してFeatureからパラメータを渡す」 手順は今後も出てくると思うので覚えておきましょう。

    手順③モザイクの大きさをアニメーションさせる

    Featureとはは「ScriptableObject」なので、事前にシリアライズすることが可能です。つま りコンポーネントにFeatureをシリアライズしてUnity実行中にパラメータを渡すことができる のです。

    URP ForwardRendererの中にFeatureは作られる

    【超基本編】URPに独自のパスを追加する方法_6

    コンポーネント(Script)からFeature経由でPassにパラメータを渡す流れを以下の図にまとめました。

    スクリプトからFeatureのパラメータを更新しているイメージ図

    スクリプトからFeatureのパラメータを更新しているイメージ図

    以下のコードにように更新したパラメータをFeatureに渡すことでモザイクアニメーションを表現できます。

    サイン波を使ったアニメーション処理のサンプル
    public class DownSampleAnimation : MonoBehaviour  
    {
        // Featureの参照を保持  
        [SerializeField] private DownSamplingRenderFeature _feature;  
    
        void Update()  
        {
            var tmp = (Mathf.Sin(Time.frameCount * Mathf.PI / 180f) + 1f) / 2f;  
            // ダウンサンプリング最小1/120のサイズまで適用  
            var value = tmp * 120f;  
            // Featureのパラメータを更新  
            _feature.downSample = (int)value;  
        }
    }
    

    するとこのようなこのようなアニメーションが作成できます。

    【超基本編】URPに独自のパスを追加する方法_7

    ゲームの状態によるパラメータ調整ってよくありますよね。ダメージを受けたら画面を赤くするとか。

    そういう処理もURPの拡張を使えばわかりやすく実装できそうです。

    FrameDebuggerで描画タイミングの確認とデバッグ方法

    最後に描画処理のデバッグ、確認方法の紹介です。

    メニューWindow > Analysis > Frame Debugger からFrame Debuggerウィンドウを表示してみましょうFrame Debuggerウィンドウを使うと各描画処理のタイミングを確認できる超便利ツールです。

    【超基本編】URPに独自のパスを追加する方法_8

    FrameDebuggerの Enableボタン をクリック。

    Frame Debugウィンドウ

    【超基本編】URPに独自のパスを追加する方法_9

    すると「DownSamplingRenderPass」がFrame Debuggerに表示されます。このFrame Debuggerに表示される名前はCommandBufferを生成したときの名前になります。

    var commandBuffer = CommandBufferPool.Get("コマンドバッファ名");  
    

    DrawTransparentObjects(透明オブジェクトの描画)の後に実行されていることが分かります。意図通りですね。

    Frame Debuggerを使うことで、意図した処理になっているのか確認できるためとても便利です。Frame DebuggerはUnityEditorだけではなく実機につないでテストすることもできるためとてもおすすめのツールです。

    まとめ

    URPの拡張を最小コードで紹介してきました。

    URP拡張のまとめ

    ①Feature、Passの作成

    ②Featureの中でPassを生成

    ③描画前にFeatureからPassにパラメータを渡す

    ④PassのExecuteに描画処理を書く

    ⑤FeatureをRendererに登録

    URP拡張は5つのステップで大まかな描画の流れは実装できます。そして「Frame Debugger」を使うことで意図したタイミングで処理が走っているか確認可能です。

    今後Unityでものづくりするときはビルトインパイプラインではなく、URPに置き換わる未来が来るかもしれません。パフォーマンス的には完全にURPの勝利です。ぜひ、今のうちからURPのキャッチアップをしてゲームのクオリティを上げていきましょう。

    こちらの記事ではのシェーダーを使ってURPの拡張をしていますのであわせて確認してみてください。

    全ソースコードはこちら

    最後にサンプル全ソースを公開しておきます。
    サンプルも内包されて分かりづらいですが「Assets/DownSampling」フォルダに今回のサンプルが一式格納しています。

    何か参考になれば幸いです。
    GitHub - baobao/URP-DownSamplingSample

    この記事があなたのゲーム開発に少しでもお役に立てたら嬉しいです。

    「Unity初心者大学」というUnity初心者向けのYouTube始めました!!
    ぜひチャンネル登録をお願いします!

    最後まで読んでいただきありがとうございました!
    すばらしいURP拡張ライフをお過ごしください。

    オススメ記事
    検証環境