Unityを触り始めた頃は、つい何でもかんでも Update() に書いてしまいがちですよね。移動処理、入力処理、エフェクト制御、アニメーション切り替え……全部ひとつのスクリプトに詰め込むと、ちょっと仕様変更が入っただけでコードが壊れやすくなります。

とくに「残像」や「ブラー」などの見た目系エフェクトは、プレイヤーの移動ロジックとごちゃ混ぜにしやすい代表例です。移動処理とシェーダーパラメータの更新が同じクラスにベタ書きされていると、後からエフェクトだけ差し替えたいときに地獄を見ます。

そこでこの記事では、移動ロジックとエフェクト制御をきれいに分離するために、「移動の逆方向にブラーをかける」シェーダー用の専用コンポーネント AfterImageShader を用意します。
このコンポーネントは、オブジェクトの移動ベクトルを計算して、シェーダーのパラメータへ流し込むだけに責務を絞っています。見た目のチューニングはシェーダー側で、ロジックはこのコンポーネント側で、というきれいな分業ができますね。

【Unity】移動方向から自動計算!「AfterImageShader」コンポーネント

ここでは、

  • オブジェクトの移動方向(速度)を毎フレーム計算
  • その逆方向ベクトルをシェーダーへ渡し、ブラー方向として利用
  • 移動速度に応じてブラーの強さも自動調整

といった処理を行う AfterImageShader コンポーネントを実装します。

シェーダー側では、例えば以下のようなパラメータを想定します(名前は後で変更してもOKです)。

  • _BlurDirection : Vector4(x, y, z を使用)… ブラー方向(ワールド空間 or ローカル空間)
  • _BlurStrength : float … ブラーの強さ

それでは、コンポーネントのフルコードを見ていきましょう。

AfterImageShader.cs(フルコード)


using UnityEngine;

/// <summary>
/// オブジェクトの移動方向から、シェーダーのブラー方向&強さを制御するコンポーネント。
/// 
/// ・移動ベクトル = 現在位置 - 前フレーム位置
/// ・ブラー方向 = -移動ベクトル(=移動の逆方向)
/// ・ブラー強さ = 速度の大きさ × スケール係数
/// 
/// シェーダー側には、以下のプロパティがある前提で動作します。
///   _BlurDirection : Vector4 (x, y, z を使用)
///   _BlurStrength  : float
/// 
/// 責務を「シェーダーパラメータ更新」に限定しているので、
/// 移動ロジックや入力処理とは分離して使うことを想定しています。
/// </summary>
[DisallowMultipleComponent]
[RequireComponent(typeof(Renderer))]
public class AfterImageShader : MonoBehaviour
{
    // --- シリアライズフィールド(インスペクターから調整可能) ---

    [Header("マテリアル設定")]
    [Tooltip("シェーダーパラメータを書き込む対象マテリアル。\n" +
             "未指定の場合、Renderer の Material (インスタンス) を自動取得します。")]
    [SerializeField] private Material _targetMaterial;

    [Header("シェーダープロパティ名")]
    [Tooltip("ブラー方向を書き込む Vector プロパティ名")]
    [SerializeField] private string _blurDirectionProperty = "_BlurDirection";

    [Tooltip("ブラー強さを書き込む Float プロパティ名")]
    [SerializeField] private string _blurStrengthProperty = "_BlurStrength";

    [Header("ブラー強度設定")]
    [Tooltip("速度1.0あたりのブラー強さスケール")]
    [SerializeField] private float _strengthPerSpeed = 0.5f;

    [Tooltip("ブラー強さの最大値(上限)")]
    [SerializeField] private float _maxStrength = 2.0f;

    [Tooltip("ブラー強さの最小値(しきい値未満なら 0 にする)")]
    [SerializeField] private float _minStrengthThreshold = 0.01f;

    [Header("座標系設定")]
    [Tooltip("true の場合、ブラー方向をローカル空間に変換してシェーダーへ渡します。\n" +
             "false の場合、ワールド空間のまま渡します。")]
    [SerializeField] private bool _useLocalSpaceDirection = false;

    [Header("補間設定(オプション)")]
    [Tooltip("変化をなめらかにするための補間係数。0 なら補間なし、1 に近いほど追従が遅くなります。")]
    [Range(0f, 0.99f)]
    [SerializeField] private float _smoothing = 0.2f;

    [Tooltip("一定時間動いていない場合、ブラーを完全にオフにするまでの秒数")]
    [SerializeField] private float _idleFadeTime = 0.2f;


    // --- 内部状態 ---

    private Renderer _renderer;
    private Vector3 _prevPosition;
    private Vector3 _smoothedDirection = Vector3.zero;
    private float _currentStrength = 0f;
    private float _idleTimer = 0f;

    // プロパティIDをキャッシュ(文字列検索コスト削減)
    private int _blurDirectionId;
    private int _blurStrengthId;

    private void Awake()
    {
        _renderer = GetComponent<Renderer>();

        // マテリアル未指定なら Renderer.material を使用
        if (_targetMaterial == null)
        {
            // material はインスタンス化されるので、他オブジェクトへの影響を避けられる
            _targetMaterial = _renderer.material;
        }

        // プロパティIDをキャッシュ
        _blurDirectionId = Shader.PropertyToID(_blurDirectionProperty);
        _blurStrengthId  = Shader.PropertyToID(_blurStrengthProperty);

        // 初期位置を保存
        _prevPosition = transform.position;

        // 初期値としてブラーをゼロにしておく
        ApplyToMaterial(Vector3.zero, 0f, immediate: true);
    }

    private void Update()
    {
        // 現在位置と前フレーム位置から移動ベクトルを算出
        Vector3 currentPosition = transform.position;
        Vector3 delta = currentPosition - _prevPosition;

        // Unity の Update はフレームレート依存なので、速度は delta / deltaTime で計算
        float deltaTime = Time.deltaTime;
        Vector3 velocity = deltaTime > 0f ? delta / deltaTime : Vector3.zero;

        // 速度の大きさ(スカラー)
        float speed = velocity.magnitude;

        // ブラー方向は移動の逆方向(-velocity)
        Vector3 blurDirection = speed > 0.0001f ? -velocity.normalized : Vector3.zero;

        // 速度に応じて強さを計算
        float targetStrength = speed * _strengthPerSpeed;

        // 上限・下限をクリップ
        targetStrength = Mathf.Clamp(targetStrength, 0f, _maxStrength);

        // ほぼ停止している場合は、しきい値未満を 0 とみなす
        if (targetStrength < _minStrengthThreshold)
        {
            targetStrength = 0f;
        }

        // 補間処理
        if (_smoothing > 0f)
        {
            // 方向は Lerp で補間(ゼロベクトルもあり得るので normalized は最後に)
            _smoothedDirection = Vector3.Lerp(_smoothedDirection, blurDirection, 1f - _smoothing);

            // 強さも同様に補間
            _currentStrength = Mathf.Lerp(_currentStrength, targetStrength, 1f - _smoothing);
        }
        else
        {
            _smoothedDirection = blurDirection;
            _currentStrength = targetStrength;
        }

        // 一定時間動いていない場合は、フェードアウト
        if (speed < _minStrengthThreshold)
        {
            _idleTimer += deltaTime;
            if (_idleTimer >= _idleFadeTime && _idleFadeTime > 0f)
            {
                // 完全に停止とみなして強さを 0 に
                _currentStrength = 0f;
            }
        }
        else
        {
            _idleTimer = 0f;
        }

        // 実際にマテリアルへ反映
        ApplyToMaterial(_smoothedDirection, _currentStrength, immediate: false);

        // 次フレームのために位置を保存
        _prevPosition = currentPosition;
    }

    /// <summary>
    /// マテリアルへシェーダーパラメータを適用する。
    /// </summary>
    private void ApplyToMaterial(Vector3 direction, float strength, bool immediate)
    {
        if (_targetMaterial == null)
        {
            return;
        }

        // ローカル空間を使用する場合は変換
        if (_useLocalSpaceDirection && direction != Vector3.zero)
        {
            // ワールド方向ベクトルをローカル空間へ変換
            direction = transform.InverseTransformDirection(direction);
        }

        // Vector4 へ変換してセット(w は未使用だが 0 にしておく)
        Vector4 dir4 = new Vector4(direction.x, direction.y, direction.z, 0f);

        _targetMaterial.SetVector(_blurDirectionId, dir4);
        _targetMaterial.SetFloat(_blurStrengthId, strength);

        // immediate フラグは、将来的に CommandBuffer などに切り替える際の拡張ポイントとして残しています。
        // 現状では特に分岐処理は行っていません。
    }

    /// <summary>
    /// 外部からブラー強さスケールを変更したい場合用の簡易API。
    /// レベルデザイン時にスクリプトから数値をいじるときなどに使えます。
    /// </summary>
    /// <param name="scale">速度1.0あたりのブラー強さスケール</param>
    public void SetStrengthPerSpeed(float scale)
    {
        _strengthPerSpeed = Mathf.Max(0f, scale);
    }

    /// <summary>
    /// 外部からブラーを一時的にオフにしたい場合に 0 を指定するなどして使うAPI。
    /// </summary>
    public void SetMaxStrength(float maxStrength)
    {
        _maxStrength = Mathf.Max(0f, maxStrength);
    }
}

使い方の手順

ここからは、実際に Unity6 上で AfterImageShader コンポーネントを使う手順を見ていきます。例として「プレイヤーキャラクター」に残像ブラーを付けるケースで説明しますが、敵や動く床などにも同じ流れで適用できます。

  1. ① シェーダー(またはマテリアル)を用意する
    • URP / HDRP / Built-in いずれでも構いません。
    • 使いたいシェーダーに、以下のプロパティを追加してください(名前はコンポーネント側と合わせる):
      _BlurDirection ("Blur Direction", Vector) = (0,0,0,0)
      _BlurStrength ("Blur Strength", Float) = 0
    • シェーダー内では、_BlurDirection.xyz をブラー方向、_BlurStrength を強さとして使います。例えばスクリーンスペースでテクスチャをサンプルするときに、この方向へずらす実装などが典型ですね。
    • このシェーダーを使った マテリアル を作成しておきます。
  2. ② メッシュオブジェクトにマテリアルを適用する
    • プレイヤーキャラクターの GameObject(例: Player)を選択します。
    • MeshRenderer / SkinnedMeshRenderer など、見た目を描画している Renderer コンポーネントの Materials に、先ほど作ったマテリアルを設定します。
    • プレイヤーが複数の Renderer を持つ場合は、AfterImageShader をそれぞれの Renderer を持つ GameObject に付けるか、共通のマテリアルを使うように調整しましょう。
  3. ③ AfterImageShader コンポーネントを追加する
    • 同じくプレイヤーの GameObject を選択し、
      メニューから Add Component > AfterImageShader を追加します。
    • Target Material を空欄のままにすると、そのオブジェクトの Renderer.material が自動的に使われます。
    • もし特定のマテリアルだけを制御したい場合は、Target Material にそのマテリアルをドラッグ&ドロップしてください。
    • シェーダープロパティ名を変更している場合は、Blur Direction PropertyBlur Strength Property をシェーダー側の名前に合わせて変更します。
    • ブラーの強さを調整したいときは、Strength Per SpeedMax Strength をいじって好みの残像具合にチューニングしましょう。
  4. ④ 動かして確認する(プレイヤー / 敵 / 動く床の例)
    • プレイヤー
      すでに Rigidbody や CharacterController などで移動ロジックが組まれている前提で、その GameObject に AfterImageShader を付けるだけで、移動方向に応じて自動的にブラー方向が更新されます。
    • 敵キャラクター
      パトロールで左右に動く敵に付けると、往復のたびに移動方向が変わり、ブラーの向きも自動で反転します。移動AIのコードには一切手を入れず、見た目だけを簡単に強化できます。
    • 動く床
      スクロールする足場やエレベーターなどに付けると、床が動いている方向と逆向きにブラーが伸びるので、スピード感のある演出ができます。床の移動スクリプトとは完全に独立しているので、プレハブとして量産も楽ですね。

メリットと応用

AfterImageShader コンポーネントを使うメリットは、「移動ロジックとエフェクト制御の分離」にあります。

  • プレハブ化がしやすい
    プレイヤー、敵、ギミックなど、どのオブジェクトにも「見た目の残像」を付けたいときは、このコンポーネントを足すだけ。移動スクリプトを一切触らずに済むので、プレハブの再利用性が高まります。
  • レベルデザインが楽になる
    ステージごとに「ここはスピード感を強調したい」「ここはあまり残像を出したくない」といった調整も、インスペクターから Strength Per SpeedMax Strength をいじるだけでOK。レベルデザイナーがコードを書かずに演出を調整できます。
  • SRP / シェーダー差し替えに強い
    シェーダー側の実装(どうブラーを描画するか)は自由に作り直せますが、_BlurDirection_BlurStrength さえ受け取っていれば、C#側はそのまま使い回せます。レンダーパイプラインを変えても、コンポーネントの責務は変わりません。

さらに、応用として「一定以上の速度のときだけ残像を強める」「ダッシュ中だけブラーを増幅する」といった拡張も簡単にできます。

例えば、ダッシュ中にブラー強度を2倍にする簡単な改造案はこんな感じです(既存クラスに追記できるイメージの void 関数です)。


    /// <summary>
    /// 外部から「ダッシュ中かどうか」を通知して、ブラー強度をブーストする例。
    /// ダッシュ中は内部的に MaxStrength を一時的に増やすイメージです。
    /// </summary>
    /// <param name="isDashing">ダッシュ中なら true</param>
    public void SetDashState(bool isDashing)
    {
        if (isDashing)
        {
            // ダッシュ中は最大強度を 2 倍に
            _maxStrength *= 2f;
        }
        else
        {
            // 元の値に戻したい場合は、別途「基準値」を保持しておくと安全です。
            _maxStrength = Mathf.Clamp(_maxStrength * 0.5f, 0f, _maxStrength);
        }
    }

実際に導入するときは、プレイヤーの「ダッシュ管理コンポーネント」から SetDashState(true/false) を呼ぶだけで、ロジックと見た目をきれいに分離したまま、リッチな残像表現を実現できます。

巨大な God クラスにエフェクト処理を詰め込むのではなく、AfterImageShader のような小さな責務のコンポーネントを組み合わせていくと、プロジェクト全体の見通しもぐっと良くなります。ぜひ自分のプロジェクト流にカスタマイズしてみてください。