【Unity】CommandBufferでポストエフェクトを掛けてステンシルバッファも使ってみる

前回、CommandBufferの入門記事を書きました。

light11.hatenadiary.com

CommandBufferを使うと独自のタイミングでポストエフェクトを掛けることもできますが、ちょっとハマりポイントがあるのでメモします。

Unity5.6.3

簡単なポストエフェクトを掛けてみる

まず、ポストエフェクト用のシェーダを用意します。

Shader "CommandBufferPostEffect"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Cull Off ZWrite Off ZTest Always

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

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

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

            sampler2D _MainTex;
            float4 _MainTex_ST;
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return 1.0 - col;
            }
            ENDCG
        }
    }
}

中身としては色を反転しているだけです。

次にこれをコマンドバッファから使います。

using UnityEngine;
using UnityEngine.Rendering;

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

    [SerializeField]
    private Shader _shader;

    private void Awake () {
        Initialize();
    }

    private void Initialize()
    {
        var camera = GetComponent<Camera>();
        if (camera.allowMSAA) {
            // MSAAがONになっていると正常に動作しない
            return;
        }
        var material = new Material(_shader);
        var commandBuffer = new CommandBuffer();
        
        // 一時テクスチャを取得する
        // テクスチャのIDを取得するにはShader.PropertyToIDを使う
        int tempTextureIdentifier = Shader.PropertyToID("_PostEffectTempTexture");
        commandBuffer.GetTemporaryRT(tempTextureIdentifier, -1, -1);

        // 現在のレンダーターゲットを一時テクスチャにコピー
        // 一時テクスチャからレンダーターゲットにポストエフェクトを掛けつつ描画
        commandBuffer.Blit(BuiltinRenderTextureType.CurrentActive, tempTextureIdentifier);
        commandBuffer.Blit(tempTextureIdentifier, BuiltinRenderTextureType.CurrentActive, material);

        // 一時テクスチャを解放
        commandBuffer.ReleaseTemporaryRT(tempTextureIdentifier);

        camera.AddCommandBuffer(CameraEvent.AfterEverything, commandBuffer);
    }
}

基本的な流れは

  1. 一時レンダーテクスチャを取得する
  2. このテクスチャに現在のレンダーターゲットを描画
  3. 一時テクスチャにポストエフェクトをかけつつレンダーターゲットに描画

となります。

一時テクスチャの取得方法は少し変わっていて、CommandBuffer.GetTemporaryRT()を使います。

このメソッドの第一引数にはテクスチャのIDを渡します。
このIDは任意ですが、Shader.PropertyToID()に任意の文字列を渡すことで取得します。

また、第2引数と第3引数はwidthとheightですが、コマンドバッファを登録するカメラのサイズを使いたい場合には-1を渡します。

このあたりはマニュアルに書かれています。

docs.unity3d.com

レンダリング結果は下図の右のようになります。

f:id:halya_11:20180404211609p:plain

MSAAがONになっていると結果がおかしくなる件

前節のソースコードで、CameraのMSAAがONになっているときにさりげなくreturnしています。

if (camera.allowMSAA) {
    // MSAAがONになっていると正常に動作しない
    return;
}

これはMSAAが有効化されていると今のレンダーターゲットがうまく取れていないような挙動になることがあるためです。
CameraEventの種類やプラットフォームによって取れたり取れなかったりも・・(ちゃんと検証はしてない)

色々と試行錯誤されている方もいますが、ブラックボックスな部分も多いためMSAAはOFFのときのみ有効にしています。

https://forum.unity.com/threads/commandbuffer-blit-isnt-stencil-buffer-friendly.432776/
フォーラムでも議論されています。

ステンシルバッファを使う

ポストエフェクトでステンシルバッファを使うのは結構大変、というか情報が錯綜している感がありますが、
このCommandBufferを使う方法は安定しているように思います。

ポストエフェクトのシェーダを書き換えればステンシルが使えます。

Shader "CommandBufferPostEffect"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Cull Off ZWrite Off ZTest Always
        
                // ステンシル
        Stencil{
            Ref 1
            Comp Equal
        }

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

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

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

            sampler2D _MainTex;
            float4 _MainTex_ST;
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                // 描画部分を緑色に
                return fixed4(0, 1, 0, 1);
            }
            ENDCG
        }
    }
}

レンダリング結果は次のようになります。

f:id:halya_11:20180404212048p:plain
※説明を省略してますが、緑色になっている部分のステンシルバッファには1を入れています

参考サイト

qiita.com

https://forum.unity.com/threads/commandbuffer-blit-isnt-stencil-buffer-friendly.432776/

関連

light11.hatenadiary.com