今日まなび007:ステンシルバッファを使って画面内の一部にだけポストプロセスをかける

「今日まなび」は最低1日1h、ゲーム作るために学びたいことなんでもいいから学んでいくコーナー。
完全自分用でまとめることは考えなくてOK.1記事1h。(1hでどんだけ学べるかのスピード感もみていきたいので)
1hのうちに次回学ぶことも決定しておくこと

環境:unity2021.3.40f


今日の学び

ステンシルバッファとはピクセル数分、値を保持することができる領域。
たとえば、ステンシルバッファの値が1の場合だけ描画する、といったことでマスク処理なんかに使われたりする。

nn-hokuson.hatenablog.com

これを使うには、


・ステンシルバッファに値を書き込む
・ステンシルバッファの値を参照してやりたい処理をやる

の2段階踏むことになる。

今回はQuad以外(↓の絵だと赤い四角)の部分をモノクロにするものを作ってみる。

ステンシルバッファに値を書き込む

Shader "Test/test20240902write"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Tint("Tint",Color) = (1,1,1,1)
    }
    SubShader
    {
        Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
        LOD 100

        Pass {

            Stencil
            {
                Ref 1
                Comp Always
                Pass Replace
            }

            Tags { "LightMode" = "ForwardBase" }
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
           #pragma target 2.0

            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _Tint;

            struct appdata_t {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f {
                float4 vertex : SV_POSITION;
                float2 texcoord : TEXCOORD0;
                float4 pos : TEXCOORD1;

                UNITY_FOG_COORDS(1)
                UNITY_VERTEX_OUTPUT_STEREO
            };

            v2f vert (appdata_t v)
            {
                v2f o;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
                return o;
            }

            fixed4 frag (v2f_img i) : COLOR
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col * _Tint;
            }
            ENDCG
        }
    }
}

今回の学び部分は以下。
これはステンシルバッファに1を書き込むだけの定義である。
ステンシルバッファはSubShader単位でもかけるし、Pass単位でもかけるみたい。

Stencil
{
        Ref 1 //対象の値は1
        Comp Always //条件を定義できる。Alwaysは判定なしにTrue
        Pass Replace //条件がTrueの場合に行う処理。 Replace=書き込む ということを表す
}

ステンシルバッファの値を参照してやりたい処理をやる

値を参照する側は、このようなコードになる。
これは1つ目のPassでステンシルバッファが0ならモノクロ化、2つ目のPassで1ならそのままの色を描画するという処理になる。

Shader "Test/20240831"
{
    Properties {
        _MainTex("MainTex", 2D) = ""{}
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            Stencil
            {
                Ref 0
                Comp Equal
            }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            sampler2D _MainTex;

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }
            
            fixed4 frag(v2f_img i) : COLOR {
                fixed4 c = tex2D(_MainTex, i.uv);
                float gray = c.r * 0.3 + c.g * 0.6 + c.b * 0.1;
                c.rgb = fixed3(gray, gray, gray);
                return c;
            }
            ENDCG
        }


        Pass
        {
            Stencil
            {
                Ref 1
                Comp Equal
            }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            sampler2D _MainTex;

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }
            
            fixed4 frag(v2f_img i) : COLOR {
                fixed4 c = tex2D(_MainTex, i.uv);
                return c;
            }
            ENDCG
        }
    }
}

ポストプロセス用のShaderでステンシルバッファの値がうまく参照できない?

さて、上述したモノクロ化のshaderはポストプロセスとしてカメラに写っているものに対して行おうとした処理なのだが、
これまで通り以下のコードでポストプロセスをかけた際に、ステンシルバッファの値を意図したように参照できていなかった。

   public void OnRenderImage(RenderTexture source, RenderTexture dest)
    {
        Graphics.Blit(source, dest, mat);
    }

このあたりは正直現状よくわからないが、CommandBufferという仕組みをすることでポストプロセスでもステンシルバッファを使うことができる。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class Test20240902 : MonoBehaviour
{

    /// <summary>
    /// コマンドバッファ名
    /// </summary>
    private const string CommandBufferName = "StencilImageEffect";

    /// <summary>
    /// イメージエフェクトで使用するマテリアル
    /// </summary>
    [SerializeField] private Material material;

    /// <summary>
    /// イメージエフェクト用コマンドバッファ
    /// </summary>
    private CommandBuffer commandBuffer;


    private void OnEnable()
    {
        if (material == null) return;
        if (commandBuffer != null) return;

        var cam = GetComponent<Camera>();
        var cbs = cam.GetCommandBuffers(CameraEvent.BeforeImageEffects);
        foreach (var cb in cbs)
        {
            // 多重登録を回避するため、名前でチェック
            if (cb.name == CommandBufferName) return;
        }

        commandBuffer = new CommandBuffer();
        commandBuffer.name = CommandBufferName;

        // Blitでmaterialを適用してイメージエフェクトをかける
        commandBuffer.Blit(
            BuiltinRenderTextureType.CameraTarget,
            BuiltinRenderTextureType.CameraTarget,
            material);

        // カメラにコマンドバッファを登録
        cam.AddCommandBuffer(CameraEvent.BeforeImageEffects, commandBuffer);
    }

    private void OnDisable()
    {
        if (commandBuffer == null) return;

        var cam = GetComponent<Camera>();
        cam.RemoveCommandBuffer(CameraEvent.BeforeImageEffects, commandBuffer);
        commandBuffer = null;
    }
}