ImageEffectでラスタースクロールを作ってみた

  • 11
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

ImageEffectとは

Unity でいう ImageEffect とは、一般的に(?)ポストエフェクトと呼ばれる3Dレンダリング後の画面全体にかかる画像処理のことを指します。

Unity の標準の ImageEffect は Assets->Import Package->Effect からインポートできます。
ぼかしや残像等色々な ImageEffect がありますが、今回は勉強がてら1からラスタースクロールを作ってみます。
ラスタースクロールとは、昔のゲームでよくあった画面がうねうね波打つ演出のことです。
ちなみに私の好きなラスタースクロールは、サンダーフォースIIIのGorgonステージの背景です。

完成品はこんな感じになります。
ezgif.com-optimize.gif
うねうね

ImageEffect用スクリプトの基底クラス

ImageEffect は、カメラからのレンダリング結果を加工するため、Camera コンポーネントがついた GameObject にImageEffect用のスクリプトを追加する必要があります。
Camera がレンダリングした後、Camera がついた GameObject にある全てのコンポーネントの OnRenderImage が呼ばれます。
その中で Graphics.Blit(source, destination, material) を呼んで画像を加工します。
まずは基底クラスを作ってそこから派生させていきます。

using UnityEngine;

[RequireComponent(typeof(Camera))] // カメラのレンダリング結果を使うのでカメラコンポーネント必須
public abstract class ImageEffectBase : MonoBehaviour {

    #region Fields
    private Material material; // シェーダーでゴニョゴニョするためのMaterial
    #endregion

    #region Properties
    public abstract string ShaderName { get; } // シェーダーの名前

    protected Material Material { get { return material; } }
    #endregion

    #region Messages
    protected virtual void Awake()
    {
        Shader shader = Shader.Find(ShaderName); // シェーダー取得
        material = new Material(shader); // シェーダーを割り当てたMaterial作成
    }

    // Source:カメラからのレンダリング結果 destination:シェーダーでゴニョゴニョした結果
    protected virtual void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        UpdateMaterial();

        Graphics.Blit(source, destination, material); // ゴニョゴニョする
    }
    #endregion

    #region Methods
    protected abstract void UpdateMaterial(); // シェーダーに渡すパラメータ等の処理
    #endregion

}

ラスタースクロール用スクリプト

前述の ImageEffectBase を継承してラスタースクロールをする ImageEffect スクリプトを作成します。
frequency(周波数), power(ラスタースクロールの強さ), speed(スクロールの速さ)の3つのパラメータが増えました。
これらをラスタースクロール用シェーダーに渡します。

using UnityEngine;

[ExecuteInEditMode]
public class ImageEffectRasterScroll : ImageEffectBase {

    #region Fields
    [SerializeField]
    [Range(0, 100)]
    private float frequency;    // 周波数

    [SerializeField]
    [Range(0, 1)]
    private float power;        // ラスタースクロールの強さ

    [SerializeField]
    [Range(0, 100)]
    private float speed;        // スクロールの速さ

    // シェーダープロパティID
    private int propertyIDFreq;     
    private int propertyIDPower;
    private int propertyIDSpeed;
    #endregion

    #region Properties
    public override string ShaderName
    {
        get
        {
            return "Custom/RasterScroll";
        }
    }
    #endregion

    #region Methods
    protected override void Awake()
    {
        base.Awake();

        // シェーダープロパティID取得
        propertyIDFreq = Shader.PropertyToID("_Freq");
        propertyIDPower = Shader.PropertyToID("_Power");
        propertyIDSpeed = Shader.PropertyToID("_Speed");
    }
    protected override void UpdateMaterial()
    {
        // シェーダーにInspectorで設定したパラメータを渡す
        Material.SetFloat(propertyIDFreq, frequency);
        Material.SetFloat(propertyIDPower, power);
        Material.SetFloat(propertyIDSpeed, speed);
    }
    #endregion
}

ラスタースクロール用シェーダー

ラスタースクロールを実現するには、
UV座標の横方向だけ画面のY座標に合わせて一定周期でずらしていくようにします。
UVのY座標(V?)はそのままです。
そうすることで波打ってるように見えます。
重要な点は2点あり、
一つは vert 関数内の ComputeScreenPos() です。
引数で渡された座標を画面上の座標に変換してくれます。
もう一つは、frag 関数内の uv.x + sin(i.spos.y * _Freq + _Time.y * _Speed) です。
UV座標の横方向に、スクリプトから渡されたパラメータと ComputeScreenPos で取得した画面上のY座標をsin関数でうねうねさせた値を足します。
上記の式をfmod()とabs()で囲っているのは、画面端の方のUV座標がはみ出た時に反対側のUV座標を持ってくる為です。

Shader "Custom/RasterScroll" {
    Properties {
        _MainTex ("Source", 2D) = "white" {}
        _Freq("Frequency", Float) = 0
        _Power("Power", Float) = 0
        _Speed("Speed", Float) = 0
    }
    SubShader{
        ZTest Always
        Cull Off
        ZWrite Off
        Fog { Mode Off}

        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma fragmentoption ARB_precision_hint_fastest
            #pragma target 3.0

            #include "UnityCG.cginc"

            struct v2f {
                fixed4 pos : SV_POSITION;
                fixed2 uv : TEXCOORD0;
                fixed2 spos : TEXCOORD1;
            };

            v2f vert(appdata_img v) {
                v2f o;
                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord.xy);
                o.spos = ComputeScreenPos(o.pos);   // モデルビュープロジェクション変換された座標を画面上に変換する
                return o;
            }

            sampler2D _MainTex;
            fixed _Freq;
            fixed _Power;
            fixed _Speed;

            fixed4 frag(v2f i) : SV_TARGET{
                fixed2 uv = i.uv;
                uv.x = fmod(abs(uv.x + sin(i.spos.y * _Freq + _Time.y * _Speed) * _Power), 1);  // uvの横方向だけ画面のY座標に合わせて一定周期でずらしていく
                return tex2D(_MainTex, uv);
            }
            ENDCG
        }
    }
    FallBack Off
}

実行結果

ezgif.com-optimize.gif
frequency を大きくするとうねうねの縦の間隔が狭くなっていきます。
power を大きくすると左右の揺れ幅が大きくなります。
speed を大きくするとうねうねが流れる速度が速くなります。