読者です 読者をやめる 読者になる 読者になる

しゅみぷろ

プログラミングとか

1つのカメラでMRT出力を用いたPostEffectを実現する

C# Shader Unity

はじめに

今までにも何度かCommandBufferを用いたエフェクトについて書いてきたので今更なのですが、Unityのコミュニティでの会話でなんとなくきっかけがあったので書いてみることにしました。

今回は1つのカメラでMRTによる出力を使ったPostEffectをしてみようと思います。 DeferredShadingで行っていることですね。DeferredShadingでは1つのパスで複数のデータを出力し、後のパスで利用できるようになっています。

Deferred Shadingについてまとめてみた - しゅみぷろ

PostEffectをかけようと思うと、通常スクリプトでカメラのOnRenderImageイベント関数を用いると思います。このメソッドを用いる場合、カメラのレンダリングターゲットが自動的に「ImageEffects Temp」に設定されます。

たとえば

public RenderTexture buf1;
public RenderTexture buf2;

camera.SetTargetBuffers(new RenderBuffer[2] { buf1.colorBuffer, buf2.colorBuffer }, buf1.depthBuffer);

といったコードを記述し、カメラのレンダリングターゲットをbuf1,buf2というRenderTextureに設定し、シェーダーで

struct buffer {
    half4 buf1 : COLOR0;
    half4 buf2 : COLOR1;
};

buffer frag (v2f i)
{
    buffer o;
    float4 col = tex2D(_MainTex, i.uv) * i.diffuse;
    o.buf1 = col;
    o.buf2 = float4(0, 0, 1, 1);
    return o;
}

というように複数の出力をする場合、当然といえば当然ですがこの2つのバッファは正常に出力されます。

しかし、スクリプトにOnRenderImageイベント関数を定義してエフェクトをかけようとすると話は代わり、自動的にレンダリングターゲットが(この場合「RT0, RT1」から)「ImageEffects Temp」にすり替わってしまいます。MRTによる出力が上手くいかなくなってしまうわけです。

これを解決するために今回用いる手法が、「Unity標準のImageEffectパスを使わず独自に拡張したImageEffect用のパスでエフェクトをかける」ことです。もしもっと良い方法をご存知でしたらご教示下さいませ。

今回作成したサンプルのリポジトリは以下で公開しています。

github.com

実装について

まずはMRTによる複数の出力をもつシェーダーを作ります。

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

        CGINCLUDE
       #include "UnityCG.cginc"

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

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

        struct buffer {
            half4 buf1 : COLOR0;
            half4 buf2 : COLOR1;
        };

        sampler2D _MainTex;
        float4 _MainTex_ST;
        float4 _LightColor0;

        v2f vert (appdata v)
        {
            v2f o;
            o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
            o.uv = TRANSFORM_TEX(v.uv, _MainTex);
            float3 normal = UnityObjectToWorldNormal(v.normal);
            //Diffuseカラー計算
            o.diffuse = max(0, dot(normal, _WorldSpaceLightPos0.xyz)) * _LightColor0;
            //球面調和
            o.diffuse.rgb += ShadeSH9(half4(normal,1));
            return o;
        }

        ENDCG

        Pass
        {
            Tags{"LightMode"="ForwardBase"}

            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag

            buffer frag (v2f i)
            {
                buffer o;

                float4 col = tex2D(_MainTex, i.uv) * i.diffuse;

                o.buf1 = col;
                o.buf2 = float4(0, 0, 1, 1);

                return o;
            }
            ENDCG
        }
    }
}

次に、カメラのレンダリングターゲットの設定とコマンドバッファーによるレンダリングパイプラインを拡張するスクリプトを作成します。

  public Material imgEffMat;
  public RenderTexture camTarget;
  public RenderTexture buf1;
  public RenderTexture buf2;

  private Camera cam;

  private void Awake()
  {
    cam = GetComponent<Camera>();

    CommandBuffer customImgEff = new CommandBuffer();
    RenderTargetIdentifier[] ids = new RenderTargetIdentifier[2] { buf1, buf2 };
    RenderTargetIdentifier tar = new RenderTargetIdentifier(camTarget);
    imgEffMat.SetTexture("_Buf1", buf1);
    imgEffMat.SetTexture("_Buf2", buf2);
    customImgEff.name = "CunstomImageEffect";
    customImgEff.Blit(tar, tar, imgEffMat);

    cam.AddCommandBuffer(CameraEvent.BeforeImageEffectsOpaque, customImgEff);
    cam.SetTargetBuffers(new RenderBuffer[2] { buf1.colorBuffer, buf2.colorBuffer }, buf1.depthBuffer);
  }

f:id:es_program:20160426000233p:plain

今回はOpaque Image Effectsパスの前にCustomImageEffectを構築してます。長くなるので細かい部分や最適化の余地がある部分はそのままです。imgEffMatはPostEffectのシェーダーを設定したマテリアルで、これにバッファとして作成したRenderTextureをセットしています。

PostEffectのシェーダーは以下のような感じで書きました。

Shader "Hidden/ImageEffectShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Buf1("", 2D) = "white"{}
        _Buf2("", 2D) = "white"{}
        _Blend("Blend", Range(0,1)) = 0.5
    }
    SubShader
    {
        // No culling or depth
        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;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv = v.uv;
                return o;
            }

            sampler2D _MainTex;
            sampler2D _Buf1;
            sampler2D _Buf2;
            float _Blend;

            fixed4 frag (v2f i) : SV_Target
            {
                float4 col0 = tex2D(_Buf1, i.uv);
                float4 col1 = tex2D(_Buf2, i.uv);

                return col0 * _Blend + col1*(1 - _Blend);
            }
            ENDCG
        }
    }
}

作成したバッファを見ると以下のようになっています。

buf1,buf2は任意の情報をRenderTextureに書き込んだ結果で、tarはbuf1,buf2を用いたPostEffectの出力結果です。

f:id:es_program:20160426000254p:plain

あとはこのバッファを好きに使えます。

まとめ

駆け足でしたが、CommandBufferを使い、1つのカメラでMRTとPostEffectを併用する方法を簡単に見ていきました。 ちなみにMRTを用いていますのでShader Modelは3.0以降が要求されますのでご注意下さい。