シェーダー基礎、RenderGraphで画面全体にシェーダーを反映

シェーダー基礎、RenderGraphで画面全体にシェーダーを反映

[Unityシェーダー基礎] Renderer Graphの解説とGrabPassの代替え処理。
URP環境での全画面反映シェーダーの作り方。

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

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

Renderer Graphについて

UnityのRenderer Graphは、レンダリングパス(描画の工程)を効率的に管理・最適化するためのシステムです。
GrabPassではカメラが描画した画像を一度GPUからVRAMに移して、描画時にまたVRAMから読み込むという非効率な事をしていました。
Render Graphでは極力高速なGPU内のみで処理を完結させます。

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

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

URPの設定について

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

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

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

Rederer 2D Dataは内部で_CameraSortingLayerTextureというテクスチャを作成していて、この中に全画面の描画データが格納されています。

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

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

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

/// <summary>

/// Universal Render Dataに登録させるポストプロセス処理のクラス

/// </summary>

public class URPGrabScreenFeature : ScriptableRendererFeature
{
    
//カメラ描画工程に新たに介入するロジッククラスの宣言

    URPGrabPass m_ScriptablePass;

    
//Baseクラスの初期化処理を上書き、URPGrabPassのインスタンスを生成する

    public override void Create() => m_ScriptablePass = new URPGrabPass();
    
//Unityに対しURPGrabPass.RecordRenderGraphのロジックをカメラ描画工程に追加するように依頼

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) => renderer.EnqueuePass(m_ScriptablePass);
    
    
/// <summary>

    
///具体的なロジック処理クラス

    
/// </summary>

    private class URPGrabPass : ScriptableRenderPass
    {
        
//シェーダー側のコードに参照させるテクスチャ名

        private readonly string _textureName = "_URPGrabTexture";
        
//処理の高速化のためテクスチャ名をintに変換して定義しておく、後のSetGlobalTexturePass関数内で使う

        private readonly int _textureId = Shader.PropertyToID("_URPGrabTexture");

        
//追加される新しい工程に必要なデータ(Context)を受け渡すのためクラス

        private class PassData
        {
            public TextureHandle source; 
//カメラに映っているオリジナルの画像データ

            public TextureHandle destination; 
//画像データのコピー先

            public int textureId; 
//シェーダーにテクスチャ名を伝えるためのID

        }        

        
/// <summary>

        
/// 初期化処理、このポストプロセスの実行タイミングを定義しているだけ

        
/// </summary>

        public URPGrabPass()
        {
            
//uGUIの描画が全て終わってからRecordRenderGraphを実行させる事をUnityに伝える

            renderPassEvent = RenderPassEvent.AfterRenderingTransparents;
        }

        
/// <summary>

        
///カメラの画角に映っている情報をテクスチャとしてシェーダーに渡す処理

        
/// </summary>

        public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
        {
            
//今この瞬間にカメラに書き込んでいる画面の色や深度のデータを取得

            UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();
            
//今この瞬間のカメラ設定(解像度、HDR設定、FOVなど)を取得

            UniversalCameraData cameraData = frameData.Get<UniversalCameraData>();

            
//resourceDataから色情報を抜き出す、その中身が存在しない場合は処理を中断する

            TextureHandle source = resourceData.activeColorTexture;
            if (!source.IsValid()) return;

            
//cameraDataから解像度や色フォーマットの情報を取得

            RenderTextureDescriptor desc = cameraData.cameraTargetDescriptor;
            
//アンチエイリアス(ギザギザ補正)をオフにしてメモリを節約

            desc.msaaSamples = 1;
            
//深度情報を省いてメモリを節約

            desc.depthBufferBits = 0;

            
//descの画像設定と_textureNameを元に、まず空のテクスチャを作成

            TextureHandle destination = UniversalRenderer.CreateRenderGraphTexture(renderGraph, desc, _textureName, false);

            
//カメラに映っている画像を作成したテクスチャにコピーする

            GrabScreenCopyPass(renderGraph, source, destination);
            
//コピーした画像を全シェーダーに流し込む

            SetGlobalTexturePass(renderGraph, destination);
        }

        
/// <summary>

        
/// Render Graphに「Grab Screen Copy Pass」という新しい描画工程のスケジュール予約を入れる

        
/// この工程ではカメラに映っている画像を指定した空テクスチャのpathへコピーを行う

        
/// スケジュール予約: builderに対してはPassDataクラスを通して必要な情報を受け渡す

        
/// </summary>

        private void GrabScreenCopyPass(RenderGraph renderGraph, TextureHandle source, TextureHandle destination)
        {
            using (var builder = renderGraph.AddRasterRenderPass("Grab Screen Copy Pass", out PassData passData))
            {
                
//コピー元の画像データをpassDataに代入

                passData.source = source;
                
//コピー先の画像データ(この時点では空のテクスチャ)をpassDataに代入

                passData.destination = destination;

                
//読み込み対象をbuilderに登録

                builder.UseTexture(passData.source, AccessFlags.Read);
                
//書き込み対象をbuilderに登録

                builder.SetRenderAttachment(passData.destination, 0, AccessFlags.Write);

                
//予約されたスケジュールが実行された時の処理

                builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
                {
                    
//空テクスチャに予約時点での画像データをコピーする

                    
// context.cmd: 命令を溜めておくバッファ

                    
// data.source: コピー元のテクスチャ

                    
// new Vector4(1, 1, 0, 0): テクスチャの全範囲(100%)をコピーするという設定(スケールとオフセット)

                    
// 0: 使用するマテリアルのパス番号(今回は単純コピーなので0)

                    
// false: 反転が必要かどうか(通常はfalse)

                    Blitter.BlitTexture(context.cmd, data.source, new Vector4(1, 1, 0, 0), 0, false);
                });
            }
        }

        
/// <summary>

        
/// Render Graphに「Set Global Texture Pass」という新しい描画工程のスケジュール予約を入れる

        
/// この工程ではコピーしたテクスチャ画像をシェーダーに流し込む

        
/// スケジュール予約: builderに対してはPassDataクラスを通して必要な情報を受け渡す      

        
/// </summary>

        private void SetGlobalTexturePass(RenderGraph renderGraph, TextureHandle destination)
        {
            using (var builder = renderGraph.AddRasterRenderPass("Set Global Texture Pass", out PassData passData))
            {
                
//上のGrab Screen Copy Passの工程でコピーしたを画像をpassDataに代入

                passData.destination = destination;
                
//その画像に紐づく画像名をintでpassDataに代入

                passData.textureId = _textureId;

                
//読み込み対象をbuilderに登録

                builder.UseTexture(passData.destination, AccessFlags.Read);
                
//この描画工程の中でのみ「グローバル変数(全シェーダー共通の設定)」を書き換えることを許可する

                builder.AllowGlobalStateModification(true);

                
//予約されたスケジュールが実行された時の処理                

                builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
                {
                    
//GPUに対して「このテクスチャ(destination)を、このIDの名前で公開せよ」と命令を出す

                    
//これにより、全シェーダーの _URPGrabTexture に中身が流し込まれる

                    context.cmd.SetGlobalTexture(data.textureId, data.destination);
                });
            }
        }
    }
}

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

これでURPのポストプロセス処理に、カメラの全画面の色情報をシェーダーに流し込む工程が追加されました。

全画面色反転シェーダー

次にシェーダー側のコードを作成します。以下の
URPGrab/InvertColor
のコード全文を.shaderファイルに丸コピしてください。
中身の細かい意味については こちらの記事 に目を通しておくと分かり易いです。
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 positionSS : TEXCOORD1; 
//スクリーン空間座標(クリップ空間座表 -w ~ +w  0 ~ w の範囲に修正した座標)

            };

            
//URPGrabScreenFeatureから全画面の色情報を受け取る

            TEXTURE2D(_URPGrabTexture);
            SAMPLER(sampler_URPGrabTexture);

            Varyings vert (Attributes input)
            {
                Varyings output;

                
//オブジェクト空間座標をクリップ空間、スクリーン空間座標にそれぞれ変換                

                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                output.positionSS = ComputeScreenPos(output.positionCS);
                
                return output;
            }

            half4 frag (Varyings input) : SV_Target
            {
                
//スクリーン空間座標から奥行を省いて座標を平面化する

                float2 screenUV = input.positionSS.xy / input.positionSS.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
        }
    }
}

RenderGraphとの連携部分

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

            TEXTURE2D(_URPGrabTexture);
            SAMPLER(sampler_URPGrabTexture);

受け取ったテクスチャ情報はピクセルシェーダー内で 色反転 させています。
            half4 frag (Varyings input) : SV_Target
            {
                
//スクリーン空間座標から奥行を省いて座標を平面化する

                float2 screenUV = input.positionSS.xy / input.positionSS.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);
            }

Unityにシェーダーを反映

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


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


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


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