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

エフアンダーバー

個人でのゲーム開発

【Unity】カスタムイメージエフェクトのつくりかた

Unity5でフリー版でのイメージエフェクト(ImageEffect)が解放されたので、 カスタムイメージエフェクトのつくりかたを書こうかな、と思ったんですが、 すでにけっこうあるんですね・・・
まあ、需要があるかはともかく自己流でも書いてみます。

まずはともあれサンプルを。
f:id:fspace:20150811163101g:plain

イメージエフェクトとは?

イメージエフェクトとは、 様々なオブジェクトをレンダリングした結果としてできる一枚の画像に対してかけるエフェクトのことをいいます。 より一般的にはポストエフェクトと呼ばれます("post-" = 「~後の」、この場合は「レンダリング後の」)。

標準のイメージエフェクトは"Assets"->"Import Package"->"Effect"をインポートすると使用できます。
標準のものをみるとぼかしや発光など特殊な演出に使いそうなものが多いですが、 もっと単純にシーンチェンジのエフェクトなんかもつくれます。 というわけで今回はその辺をつくってみようと思います。

カスタムイメージエフェクトの基本

Cameraと同じGameObjectに

イメージエフェクトは一枚の画像を入力として、一枚の画像を出力します (もちろん入力を増やすことはできますが)。 そこで入力となる画像が必要となるのですが、 UnityではこれをCameraコンポーネントのレンダリング結果から取得します。 そのため作成したイメージエフェクトはCameraコンポーネントと同じGameObjectにアタッチする必要があります。

またCameraのレンダリング結果をもとにエフェクトをかけるため、 そのCameraがレンダリングの対象としていないものにはエフェクトはかかりません。 最初のサンプルをみてもらうと、 画面上部の文字にはエフェクトがかかっていないのが確認できると思います。

OnRenderImageで描画

カスタムイメージエフェクトは通常のスクリプトと同様にMonoBehaviourを継承したクラスを利用します。
その際利用するメッセージが OnRenderImage です。

void OnRenderImage(RenderTexture source, RenderTexture destination)

引数のsourceに入力となる画像、destinationに出力先の画像が渡されてきます。

イメージエフェクトをカスタマイズするためにはこの内部で source画像をもとにdestinationへと描画すればよいのですが、 わざわざそのためにMeshなんかを作成するのは面倒なので 通常は Graphics.Blit という静的メソッドを使います。

public static void Blit(Texture source, RenderTexture dest, Material mat)

いくつかのオーバーロードがありますが、 基本的には上記のものを使うことになります。
このメソッドはsourceに渡した画像をメインテクスチャにセットしたうえで、 画面全体を描画するようなUVつきのMeshを、指定したマテリアルで、destに描画してくれます。

カスタムイメージエフェクトのつくりかた

基礎を抑えたところでソースコードをみていきます。

まずはカスタムイメージエフェクトのベースクラスから。

using UnityEngine;

[RequireComponent(typeof(Camera))]
public abstract class CustomImageEffect : MonoBehaviour
{
   #region Fields

    private Material m_Material;

   #endregion

   #region Properties

    public abstract string ShaderName { get; }

    protected Material Material { get { return m_Material; } }

   #endregion

   #region Messages

    protected virtual void Awake()
    {
        Shader shader = Shader.Find(ShaderName);
        m_Material = new Material(shader);
    }

    protected virtual void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        UpdateMaterial();

        Graphics.Blit(source, destination, m_Material);
    }

   #endregion

   #region Methods

    protected abstract void UpdateMaterial();

   #endregion

}

なにやらごちゃごちゃと書いてありますが、 ほとんどはシェーダ名からマテリアルを作成して子クラスで使えるようにしているだけです。
重要なのはOnRenderImage内のみで、 マテリアルの更新後、Graphics.Blitをそのマテリアルで呼び出しています。

続いて、エフェクトの要となるシェーダプログラム。

Shader "シェーダの名前" {
    // シェーダのプロパティ。必要なら追加する。
    Properties {
        // 入力画像がここにはいる
        _MainTex ("Source", 2D) = "white" {}
    }
    SubShader {
        // 不必要な機能はすべて切る
        ZTest Always
        Cull Off
        ZWrite Off
        Fog { Mode Off }

        Pass{
            // シェーダプログラムの本体
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"

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

            v2f vert(appdata_img v) {
                v2f o;
                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord.xy);
                return o;
            }

            sampler2D _MainTex;

            fixed4 frag(v2f i) : SV_TARGET {
                // ここに表示したい色の計算処理を書く
                ...
            }
            ENDCG
        }
    }
    FallBack Off
}

Unityのシェーダプログラムは若干特殊ですが、 カスタムイメージエフェクトに関してはそれほど難しいことはありません。
だいたいの部分はコピペでどうにかなるので、 ここではCGPROGRAM~ENDCGまでを解説します。

最初の#pragmaは頂点シェーダとフラグメントシェーダを表す関数を指定しています。

次にincludeしている"UnityCG.cginc"はUnityが提供してくれる情報にアクセスするための様々な定義が書いてあります。 公式のUnityダウンロードページに地味に標準シェーダコードをダウンロードする場所があり、 そのなかをみると詳しい内容が読めます。 appdata_img構造体やUNITY_MATRIX_MVP行列、MultiplyUV関数、_ScreenParamsベクトル等すべてここに書いてあります。

v2f構造体は頂点シェーダからフラグメントシェーダへと渡す構造体です。 単純に頂点位置とUV座標を保持します。

頂点シェーダvertはappdata_img構造体から頂点位置とUV座標を変換して、 フラグメントシェーダに渡しています。 特別な処理はしていません。

struct appdata_img
{
    float4 vertex : POSITION;
    half2 texcoord : TEXCOORD0;
};

最後にフラグメントシェーダにてUV座標とメインテクスチャを(必要ならば他のパラメータも)用いて、 表示したい色を計算します。

カスタムイメージエフェクトいろいろ

最後に作成してみたイメージエフェクトのソースコードを置いておきます。

正直、手抜き満載でパフォーマンス的にどうなの?というところはありますが、 まあ必要ならば適当に直して使ってください。
ちなみに何か起こっても一切責任は負いませんので自己責任でお願いします。

簡易モザイクエフェクト

ボックスの中心の色とっているだけという簡単なものですが、 なんとなくそれっぽいですよね・・・?

f:id:fspace:20150811163101g:plain

using UnityEngine;

public class Mosaic : CustomImageEffect
{
   #region Fields

    [SerializeField]
    [Range(1, 100)]
    private float m_Size;

   #endregion

   #region Properties

    public override string ShaderName
    {
        get { return "Custom/Mosaic"; }
    }

   #endregion

   #region Methods

    protected override void UpdateMaterial()
    {
        Material.SetFloat("_Size", m_Size);
    }

   #endregion

}
Shader "Custom/Mosaic" {
    Properties {
        _MainTex ("Source", 2D) = "white" {}
        _Size ("Size", Float) = 1
    }
    SubShader {
        ZTest Always
        Cull Off
        ZWrite Off
        Fog { Mode Off }

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

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

            v2f vert(appdata_img v) {
                v2f o;
                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord.xy);
                return o;
            }

            sampler2D _MainTex;
            float _Size;

            fixed4 frag(v2f i) : SV_TARGET {
                float2 delta = _Size / _ScreenParams.xy;
                float2 uv = (floor(i.uv / delta) + 0.5 ) * delta;

                return tex2D(_MainTex, uv);
            }
            ENDCG
        }
    }
    FallBack Off
}

切れ込みエフェクト

ノベルゲームのシーン変更とかで使われそうなやつ。

f:id:fspace:20150811163105g:plain

using UnityEngine;

public class Slit : CustomImageEffect
{
   #region Fields

    [SerializeField]
    [Range(1, 100)]
    private float m_Size;

    [SerializeField]
    [Range(0, 1)]
    private float m_Time;

   #endregion

   #region Properties

    public override string ShaderName
    {
        get { return "Custom/Slit"; }
    }

   #endregion

   #region Methods

    protected override void UpdateMaterial()
    {
        Material.SetFloat("_Size", m_Size);
        Material.SetFloat("_AnimTime", m_Time);
    }

   #endregion

}
Shader "Custom/Slit" {
    Properties {
        _MainTex ("Source", 2D) = "white" {}
        _Size ("Size", Float) = 1
        _AnimTime ("Animation Time", Range(0,1)) = 0
    }
    SubShader {
        ZTest Always
        Cull Off
        ZWrite Off
        Fog { Mode Off }

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

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

            v2f vert(appdata_img v) {
                v2f o;
                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord.xy);
                return o;
            }

            sampler2D _MainTex;
            float _Size;
            float _AnimTime;

            fixed4 frag(v2f i) : SV_TARGET {
                float delta = _Size / _ScreenParams.x;
                float visible = 1.0 - floor(frac(i.uv.x / delta) + _AnimTime);

                return fixed4(tex2D(_MainTex, i.uv).rgb * visible, 1.0);
            }
            ENDCG
        }
    }
    FallBack Off
}

のぞきあなエフェクト

昔のゲームでよく使われていたような気がする。

f:id:fspace:20150811163110g:plain

using UnityEngine;

public class Hole : CustomImageEffect
{
   #region Fields

    [SerializeField]
    private Vector2 m_Center;

    [SerializeField]
    [Range(100, 2000)]
    private float m_Size;

    [SerializeField]
    [Range(0, 1)]
    private float m_Time;

   #endregion

   #region Properties

    public override string ShaderName
    {
        get { return "Custom/Hole"; }
    }

   #endregion

   #region Methods

    protected override void UpdateMaterial()
    {
        Material.SetVector("_Center", m_Center);
        Material.SetFloat("_Size", m_Size);
        Material.SetFloat("_AnimTime", m_Time);
    }

   #endregion

}
Shader "Custom/Hole" {
    Properties {
        _MainTex ("Source", 2D) = "white" {}
        _Center ("Center", Vector) = (0,0,0,0)
        _Size ("Size", Float) = 1
        _AnimTime ("Animation Time", Range(0,1)) = 0
    }
    SubShader {
        ZTest Always
        Cull Off
        ZWrite Off
        Fog { Mode Off }

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

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

            v2f vert(appdata_img v) {
                v2f o;
                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord.xy);
                return o;
            }

            sampler2D _MainTex;
            float2 _Center;
            float _Size;
            float _AnimTime;

            fixed4 frag(v2f i) : SV_TARGET {
                float d = distance(_Center, i.uv * _ScreenParams.xy);
                float visible = 1.0 - floor(d / _Size + _AnimTime);

                return fixed4(tex2D(_MainTex, i.uv).rgb * visible, 1.0);
            }
            ENDCG
        }
    }
    FallBack Off
}

ユニティちゃんライセンス

ユニティちゃんライセンス

このコンテンツは、『ユニティちゃんライセンス』で提供されています