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

# ImageEffectとは

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

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

完成品はこんな感じになります。
![ezgif.com-optimize.gif](https://qiita-image-store.s3.amazonaws.com/0/22909/3474ecbb-78c0-9cc4-afcb-c02aee0e1c23.gif)
うねうね

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

```csharp
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つのパラメータが増えました。
これらをラスタースクロール用シェーダーに渡します。

```csharp
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
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](https://qiita-image-store.s3.amazonaws.com/0/22909/3474ecbb-78c0-9cc4-afcb-c02aee0e1c23.gif)
frequency を大きくするとうねうねの縦の間隔が狭くなっていきます。
power を大きくすると左右の揺れ幅が大きくなります。
speed を大きくするとうねうねが流れる速度が速くなります。