知識0からのUnityShader勉強

知識0からのUnityShader勉強

UnityのShaderをメインとして、0から学んでいくブログです。

【UnityShader】CommandBufferで色を反転 #63

前回の成果

MatCapを理解した。

soramamenatan.hatenablog.com


今回やること

CommandBufferを理解します。


CommandBuffer

レンダリングパイプラインの様々なタイミングで処理を挟むことのできるものになります。

以下の画像の緑の点のタイミングでCommandBufferを追加することができます。

f:id:soramamenatan:20200719124851p:plain

Unity 5 の CommandBuffer を利用したレンダリングパイプラインの拡張について調べてみた - 凹みTips:より引用


CommandBufferを使ってみる

こちらのサイト様を参考にCommandBufferを使ってみたいと思います。

gurutaka-log.com


事前準備

以下アセットをAssetStoreからダウンロードします。
そして、BarrelをScene上に2つ配置します。

f:id:soramamenatan:20200719132055p:plain

assetstore.unity.com


ソースコード

using UnityEngine;
using UnityEngine.Rendering;

public class PostEffectCommandBuffer : MonoBehaviour {
    [SerializeField]
    private Shader _shader;

    void Awake() {
        Initialize();
    }

    private void Initialize() {
        Camera camera = this.GetComponent<Camera>();
        Material material = new Material(_shader);
        CommandBuffer commandBuffer = new CommandBuffer();
        commandBuffer.name = "commandBufferTest";
        int tempTextureIdentifier = Shader.PropertyToID("_PostEffectTemp");
        commandBuffer.GetTemporaryRT(tempTextureIdentifier, -1, -1);
        commandBuffer.Blit(BuiltinRenderTextureType.CameraTarget, tempTextureIdentifier);
        commandBuffer.Blit(tempTextureIdentifier, BuiltinRenderTextureType.CameraTarget, material);
        commandBuffer.ReleaseTemporaryRT(tempTextureIdentifier);
        camera.AddCommandBuffer(CameraEvent.BeforeImageEffects, commandBuffer);
    }
}

まずはCommandBufferのソースになります。


ソースコード解説

では、ソースコードの解説に移ります。


RenderTextureの取得

int tempTextureIdentifier = Shader.PropertyToID("_PostEffectTemp");
commandBuffer.GetTemporaryRT(tempTextureIdentifier, -1, -1);

GetTemporaryRTは、一時的にRenderTextureを取得する関数となります。

引数は以下になります。

/// <summary>
/// 一時的なRenderTextureを取得
/// </summary>
/// <param name="nameID">テクスチャのシェーダープロパティ名</param>
/// <param name="width">ピクセル単位の幅、カメラのピクセルの幅の場合は-1</param>
/// <param name="height">ピクセル単位の高さ、カメラのピクセルの高さの場合は-1</param>
public void GetTemporaryRT(int nameID, int width, int height);

今回はカメラの幅と高さをそのまま使用するので、-1を渡しています。

docs.unity3d.com



PostEffectを反映

commandBuffer.Blit(BuiltinRenderTextureType.CameraTarget, tempTextureIdentifier);
commandBuffer.Blit(tempTextureIdentifier, BuiltinRenderTextureType.CameraTarget, material);

Blitは、1つのRenderTextureを別のテクスチャへとコピーする関数となります。

引数の説明は以下です。

/// <summary>
/// 1つのRenderTextureを別のテクスチャへとコピーする
/// </summary>
/// <param name="source">コピーするテクスチャやレンダーターゲット</param>
/// <param name="dest">コピー先</param>
/// <param name="mat">使用するマテリアル</param>
public void Blit(RenderTargetIdentifier source, RenderTargetIdentifier dest, Material mat);

BuiltinRenderTextureType.CameraTargetは、現在レンダリングしているカメラのターゲットテクスチャとなります。

まず、先程取得したRenderTextureに現在のレンダーターゲットを描画します。
そして、そのRenderTextureにエフェクトをかけてレンダーターゲットに描画しています。

docs.unity3d.com

docs.unity3d.com


RenderTextureを解放

commandBuffer.ReleaseTemporaryRT(tempTextureIdentifier);

ReleaseTemporaryRTは、GetTemporaryRTで一時的に取得したRenderTextureを解放する関数となります。

/// <summary>
/// GetTemporaryRTで一時的に取得したRenderTextureを解放する
/// </summary>
/// <param name="nameID">テクスチャのシェーダープロパティ名</param>
public void ReleaseTemporaryRT(int nameID);

docs.unity3d.com


CommandBufferの追加

camera.AddCommandBuffer(CameraEvent.BeforeImageEffects, commandBuffer);

AddCommandBufferは、指定された場所で実行されるようにCommandBufferを追加する関数となります。

/// <summary>
/// 指定された場所で実行されるようにCommandBufferを追加する
/// </summary>
/// <param name="evt">CommandBufferの実行タイミング</param>
/// <param name="buffer">CommandBuffer</param>
public void AddCommandBuffer(CameraEvent evt, CommandBuffer buffer);

今回は、BeforeImageEffectsを指定しているのでImageEffectsの前となります。
以下画像のタイミングとなります。
f:id:soramamenatan:20200719141236p:plain

これは、Unityの機能のFrameDebugを使用するとよくわかります。
FrameDebugは、Window -> Analysis > Frame Debuggerから開くことができます。

f:id:soramamenatan:20200719141609p:plain

docs.unity3d.com

これでCommandBufferの準備は終わりました。


Stencilで描画結果を変える

今回は参考サイト様通り、ステンシルバッファを使用して描画結果を変えていきたいと思います。
ステンシルバッファについてはこちらを参照してください。

soramamenatan.hatenablog.com


Barrelにアタッチするシェーダー

Shader "Unlit/StencilLambert" {
  Properties {
    _MainTex ("Texture", 2D) = "white" {}
  }
  SubShader {
    Pass {
      Tags { "LightMode"="ForwardBase"}

      Stencil {
          Ref 1
          Comp Always
          Pass Replace
      }

      CGPROGRAM
      #pragma vertex vert
      #pragma fragment frag

      #include "UnityCG.cginc"
      #include "Lighting.cginc"

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

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

      sampler2D _MainTex;
      float4 _MainTex_ST;

      void vert (in appdata v, out v2f o) {
        o.vertex = UnityObjectToClipPos(v.vertex);
        o.worldNormal = UnityObjectToWorldNormal(v.normal);
        o.uv = TRANSFORM_TEX(v.uv, _MainTex);
      }

      void frag (in v2f i, out fixed4 col : SV_Target) {
        float3 lightDir = _WorldSpaceLightPos0.xyz;
        float3 normal = normalize(i.worldNormal);
        float NL = dot(normal, lightDir);

        float3 baseColor = tex2D(_MainTex, i.uv);
        float3 lightColor = _LightColor0;

        col = fixed4(baseColor * lightColor * max(NL, 0), 0);
      }
      ENDCG
    }
  }
}

普通のランバート反射をするシェーダーになります。
着目してほしい点が、以下になります。

Stencil {
    Ref 1
    Comp Always
    Pass Replace
}

これは、バッファに常に1を書き込む意味となります。


SerializeFieldにアタッチするシェーダー

Shader "Unlit/StencilColorInversion" {
    Properties {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader {
        Tags { "RenderType"="Opaque" }

        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 = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                fixed4 col = tex2D(_MainTex, i.uv);
                return 1 - col;
            }
            ENDCG
        }
    }
}

色を反転させるシェーダーになります。
こちらもStencilに着目して頂きたいです。

Stencil {
    Ref 1
    Comp Equal
}

これは、バッファ値が1の場合のみレンダリングという意味となります。


各種アタッチする

まず、Main CameraにCommandBufferのScriptをアタッチします。
そのScriptのShaderに、色反転シェーダーをアタッチします。

f:id:soramamenatan:20200719142559p:plain

そして、片方のBarrelのマテリアルにランバート反射マテリアルをアタッチします。
Textureは、Barrelのものをアタッチします。

f:id:soramamenatan:20200719142702p:plain

アタッチ出来たらUnityを実行してください。


結果

ランバート反射をアタッチしたものだけ反転したら成功です。

f:id:soramamenatan:20200719142950p:plain


ソースコードにコメントを付与

using UnityEngine;
using UnityEngine.Rendering;

public class PostEffectCommandBuffer : MonoBehaviour {
    [SerializeField]
    private Shader _shader;

    void Awake() {
        Initialize();
    }

    private void Initialize() {
        Camera camera = this.GetComponent<Camera>();
        Material material = new Material(_shader);
        // CommandBuffer生成
        CommandBuffer commandBuffer = new CommandBuffer();
        commandBuffer.name = "commandBufferTest";
        int tempTextureIdentifier = Shader.PropertyToID("_PostEffectTemp");
        // RenderTextureをカメラの解像度で取得
        commandBuffer.GetTemporaryRT(tempTextureIdentifier, -1, -1);
        // PostEffectを反映
        // RenderTextureに描画
        commandBuffer.Blit(BuiltinRenderTextureType.CameraTarget, tempTextureIdentifier);
        // RenderTargetにmaterialの結果を反映させて描画
        commandBuffer.Blit(tempTextureIdentifier, BuiltinRenderTextureType.CameraTarget, material);
        // RenderTextureの解放
        commandBuffer.ReleaseTemporaryRT(tempTextureIdentifier);
        // ImageEffects前にCommnadBufferを追加
        camera.AddCommandBuffer(CameraEvent.BeforeImageEffects, commandBuffer);
    }
}

今回は以上となります。
ここまでご視聴ありがとうございました。