-->

【Unity Shader 基礎】Render Graphで全画面反映シェーダー

【Unity Shader 基礎】Render Graphで全画面反映シェーダー
旧Unity標準シェーダー(ビルトインRP)では、 GrabPassというコードを書くことで全画面反映のシェーダーを作ることが出来ました。
GrabPassは大変お手軽でしたが、その分処理が重く、モダンなアプリに耐えられないという問題点がありました。

最新のURP環境では、この問題に対処するために
 Render Graph 
というものが用意されています。
Render Graphを自作し
 カスタムポストプロセス 
として登録することで、旧来のGrabPassと近い感覚で全画面反映シェーダーを作ることが可能です。

Render Graphについて

UnityのRender Graphは、レンダリングパス(描画の工程)を効率的に管理・最適化するためのシステムです。
GrabPassでは、そのコードが呼ばれる度に、何度も画面のコピー処理が発生し深刻な遅延に繋がっていました。
Render Graphでは適切なコードをエンジニアが書くことで、この遅延を解消することができます。

またRender Graphは最終的な画面に映らないものや、後続の処理で使われないテクスチャを解析して、その描画パス自体をキャンセルします。
これにより本来使うはずだったVRAMを丸ごと節約できます。

その分、C#の長いコードを自作しなくてはいけないのですが、慣れてしまえばそれ程難しくない気がします。
今回は
 URPGrabRenderFeature 
というRender Graphを自作し、そこからカメラに映っている全画面の情報をシェーダーに流し込みます。
その後
 URPGrab/InvertColor 
というシェーダー内で、全画面の色を反転させる流れになります。

URPの設定について

URPGrabRenderFeatureを作成する前に、まずプロジェクトが何の
 ScriptableRendererData 
を使用しているか確認します。
詳しくはこちら。 p.74 : 【Unity Shader 基礎】URP環境の確認の仕方

プロジェクト作成時、URP 2Dを指定したなら
 Rederer 2D Data 
が、URP 3Dを指定したなら
 Universal Renderer Data 
が設定されているはずです。

 プロジェクトがRederer 2D Data選択している場合、実はRenderGraphを利用しなくても、全画面反映シェーダーの作成は可能です。 

p.77 : 【Unity Shader 基礎】Render 2D Dataで全画面反映シェーダー
Rederer 2D Dataは内部で_CameraSortingLayerTextureというテクスチャを作成していて、この中に全画面の描画データが格納されています。

ただRenderGraphの力は全画面反映のみで発揮されるわけではないので、RenderGraphを学んでおいて損はないです。
Rederer 2D Dataでも自作RenderGraphの適用は可能です。

カスタムポストプロセスの追加

以下のコードは、
 URPGrabRenderFeature 
というURP環境でGrabPassに近いシェーダーコードを書くためのRender Graphです。
細かくコメントが書いてありますが、必ずしも中身を理解する必要はありません。
まず以下のC#コードをUnityのC#スクリプトに丸コピしてください。
Copy Code

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.RenderGraphModule.Util;
using UnityEngine.Rendering.Universal;

/// <summary>
/// Universal Render Dataに登録させるポストプロセス処理のクラス
/// </summary>
public class URPGrabRenderFeature : ScriptableRendererFeature
{
    //介入する描画処理がGameViewのカメラかどうか。シェーダー側では基本GameViewのカメラにだけ処理を実行させる
    //(後述の_URPGrabTextureはGameViewから画面をキャプチャするので、それ以外のViewで使用するとおかしくなる)
    private readonly int _isGameViewId = Shader.PropertyToID("_IsGameView");

    private URPGrabRenderPass _pass;

    /// <summary>
    /// URPGrabRenderPassのインスタンスを生成
    /// </summary>
    public override void Create() => _pass = new URPGrabRenderPass();

    /// <summary>
    /// Unityに対しURPGrabRenderPass.RecordRenderGraphのロジックをカメラ描画工程に追加するように依頼
    /// </summary>
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        // この描画工程がGameViewのカメラかそれ以外かの情報だけは、この段階で全シェーダーに即時送信する
        bool isGameViewCamera = renderingData.cameraData.cameraType == CameraType.Game;
        Shader.SetGlobalInt(_isGameViewId, isGameViewCamera? 1 : 0);

        //描画工程を追加
        renderer.EnqueuePass(_pass);
    }
}

/// <summary>
///具体的なロジック処理クラス
/// </summary>
public class URPGrabRenderPass : ScriptableRenderPass
{
    private readonly int _textureId = Shader.PropertyToID("_URPGrabTexture");
    private readonly int _texelSizeId = Shader.PropertyToID("_URPGrabTexture_TexelSize");

    /// <summary>
    /// 初期化処理、このポストプロセスの実行タイミングを定義しているだけ
    /// </summary>
    public URPGrabRenderPass()
    {
        //カメラ描画処理が全て終わってからURPGrabRenderPass.RecordRenderGraphを実行させる事をUnityに伝える
        renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
    }

    /// <summary>
    /// 追加される新しい工程に必要なデータ(Context)を受け渡すのためクラス
    /// </summary>
    private class PassData
    {
        public TextureHandle destination; //テクスチャコピー先のメモリ領域
        public int textureId;
        public int texelSizeId;
        public Vector4 texelSize;
    }

    /// <summary>
    ///カメラの画角に映っている情報をテクスチャとしてシェーダーに渡す処理
    /// </summary>
    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
        //今この瞬間にカメラに書き込んでいる画面の色や深度のデータを取得
        UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();
        //resourceDataからカメラが描画しているテクスチャのメモリ領域を取得
        TextureHandle originalTextureHandle = resourceData.activeColorTexture;

        //これから生成するテクスチャの設計図を作成
        TextureDesc grabTextureDesc;
        try
        {
            //テクスチャの設計図をコピー
            grabTextureDesc = renderGraph.GetTextureDesc(originalTextureHandle);
        }
        catch
        {
            return;
        }
        //設計図を出力用に上書き
        grabTextureDesc.name = "URPGrabTexture";
        grabTextureDesc.clearBuffer = false;
        grabTextureDesc.msaaSamples = MSAASamples.None;
        grabTextureDesc.depthBufferBits = 0;
        var texelSize = new Vector4(1.0f / grabTextureDesc.width, 1.0f / grabTextureDesc.height,
                                            grabTextureDesc.width, grabTextureDesc.height);
        //設計図から新しいメモリ領域を作成
        TextureHandle grabTextureHandle = renderGraph.CreateTexture(grabTextureDesc);

        //Blitで実物のテクスチャデータを丸々コピー
        renderGraph.AddBlitPass(originalTextureHandle, grabTextureHandle, Vector2.one, Vector2.zero, passName: "BlitCopyToGrabTexture");

        using (var builder = renderGraph.AddRasterRenderPass("SetGrabTextureToGlobalState", out PassData passData))
        {
            //読み込み対象をbuilderに登録
            builder.UseTexture(grabTextureHandle, AccessFlags.Read);
            //この描画工程の中でのみ「グローバル変数(全シェーダー共通の設定)」を書き換えることを許可する
            builder.AllowGlobalStateModification(true);

            //コピーしたを画像をpassDataに代入
            passData.destination = grabTextureHandle;
            passData.textureId = _textureId;
            passData.texelSizeId = _texelSizeId;
            passData.texelSize = texelSize;

            //予約されたスケジュールが実行された時の処理
            builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
            {
                //GPUに対して「このテクスチャ(destination)を、このIDの名前で公開せよ」と命令を出す
                //これにより、全シェーダーの _URPGrabTexture に中身が流し込まれる
                context.cmd.SetGlobalTexture(data.textureId, data.destination);
                context.cmd.SetGlobalVector(data.texelSizeId, data.texelSize);
            });
        }
    }
}

ファイルを作成したら、Scriptable Renderer Dataのインスペクターを確認してください。
インスペクターの一番下、
 Add Renderer Feature 
から作成したURPGrabRenderFeatureを追加してください。


これでURPのポストプロセス処理に、カメラの全画面の色情報をシェーダーに流し込む工程が追加されました。
あらゆるシェーダーコードで、
 _URPGrabTexture 
というテクスチャを参照することが可能です。

全画面色反転シェーダー

次にシェーダー側のコードを作成します。以下の
 URPGrab/InvertColor 
のコード全文を.shaderファイルに丸コピしてください。
中身の細かい意味については こちらの記事に目を通しておくと分かり易いです。
Copy Code

Shader "URPGrab/InvertColor"
{
    Properties
    {
        [HideInInspector]_MainTex("-",2D)="white"{}
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "RenderPipeline" = "UniversalPipeline"
        }
        Cull Off
        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            //後述のLinearToSRGB、SRGBToLinearで必要なライブラリ
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"

            struct Attributes
            {
                float4 positionOS : POSITION; //オブジェクト空間座標(対象の大元の座標と寸法)
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION; //クリップ空間座表(カメラの四角い画角内での対象の座標)
                float4 positionVP : TEXCOORD1; //ビューポート空間座標(クリップ空間座表 -w ~ +w を 0 ~ w の範囲に修正した座標)
            };

            //URPGrabRenderFeatureから全画面の色情報などを受け取る
            int _IsGameView;
            TEXTURE2D(_URPGrabTexture);
            SAMPLER(sampler_URPGrabTexture);

            Varyings vert (Attributes input)
            {
                Varyings output;

                //オブジェクト空間座標をクリップ空間、ビューポート空間座標にそれぞれ変換
                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                output.positionVP = ComputeScreenPos(output.positionCS);

                return output;
            }

            half4 frag (Varyings input) : SV_Target
            {
                //GameViewのカメラ以外に対しては半透明ピンクを返すだけ
                if (_IsGameView == 0)
                {
                    return half4(1, 0, 1, 0.2);
                }

                //ビューポート空間座標から奥行を省いて座標を平面化する
                float2 screenUV = input.positionVP.xy / input.positionVP.w;
                //受け取った全画面情報から指定uv座標のピクセル色を抜き取る
                half4 background = SAMPLE_TEXTURE2D(_URPGrabTexture, sampler_URPGrabTexture, screenUV);

                //ピクセル色をリニア空間ルールから一旦ガンマ空間ルールへ変換
                half3 sRGB = LinearToSRGB(background.rgb);
                //ガンマ空間ルール上で色を反転
                sRGB = abs(1 - sRGB);
                //色をガンマ空間ルールからリニア空間ルールに戻す
                half3 invertColor = SRGBToLinear(sRGB);

                return half4(invertColor, 1);
            }
            ENDHLSL
        }
    }
}

Render Graphとの連携部分

コードの以下の部分で、URPGrabRenderFeatureから
 _URPGrabTexture 
というテクスチャ情報を受け取っている事が分かります。
Copy Code

            //URPGrabRenderFeatureから全画面の色情報を受け取る
            TEXTURE2D(_URPGrabTexture);
            SAMPLER(sampler_URPGrabTexture);

受け取ったテクスチャ情報はピクセルシェーダー内で 色反転させています。
Copy Code

                //ビューポート空間座標から奥行を省いて座標を平面化する
                float2 screenUV = input.positionVP.xy / input.positionVP.w;
                //受け取った全画面情報から指定uv座標のピクセル色を抜き取る
                half4 background = SAMPLE_TEXTURE2D(_URPGrabTexture, sampler_URPGrabTexture, screenUV);
                
                //ピクセル色をリニア空間ルールから一旦ガンマ空間ルールへ変換
                half3 sRGB = LinearToSRGB(background.rgb);
                //ガンマ空間ルール上で色を反転
                sRGB = abs(1 - sRGB);
                //色をガンマ空間ルールからリニア空間ルールに戻す
                half3 invertColor = SRGBToLinear(sRGB);

Unityにシェーダーを反映

Hierarchy上で_URPGrabTexture専用のOverlay CameraとCanvasを追加します。
作成したカメラは、Base CameraのStackの一番下に登録してください。


作成したCanvasにuGUI Imageを配置して、ストレッチで全画面に伸ばします。
Imageのマテリアルには先ほど作ったURPGrab/InvertColorを指定したマテリアルをアタッチします。


すると以下のようにカメラに映っている全ての色が反転するのを確認できるはずです。


Imageの描画範囲を変えると、色反転の範囲も追随して変わります。
0
0