LoginSignup
13
12

More than 5 years have passed since last update.

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

Posted at

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 を大きくするとうねうねが流れる速度が速くなります。

13
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
12