Godot 4で「動きのあるキャラをカッコよく見せたい」と思ったとき、まず思いつくのがアニメーションの強化やパーティクルですよね。でも、シルエットの残像やブラー表現を入れようとすると、

  • 毎フレーム残像用のスプライトをスポーンして管理する
  • アニメーションに合わせて残像の透明度や色を調整する
  • ShaderMaterialを各シーンごとに個別設定していく

…と、途端に面倒くさくなります。さらに、プレイヤー、敵、動く床など「動くもの」全部に似たような残像ロジックを書き始めると、継承地獄かコピペ地獄に陥るのは時間の問題です。

そこで今回は、「シェーダー残像」をコンポーネントとして切り出し、どんなノードにもポン付けできる AfterImageShader コンポーネントを用意しました。
移動の逆方向にブラーをかけるタイプの残像表現を、スクリプト1本+シェーダー1本で完結させます。シーン階層を深くせず、「必要なノードにだけコンポーネントをアタッチする」構成でいきましょう。

【Godot 4】動きに“キレ”を足す残像ブラー!「AfterImageShader」コンポーネント

このコンポーネントは、以下の2つで構成されます。

  1. GDScript: AfterImageShader.gd(Sprite2D / MeshInstance2D / Sprite3D などにアタッチ)
  2. Shader: after_image.gdshader(移動の逆方向にブラーをかける)

GDScript側は「速度ベクトルを計算してシェーダーパラメータに渡す」担当、
Shader側は「渡されたベクトル方向にブラーをかける」担当、というきれいな責務分離になっています。


シェーダーコード:after_image.gdshader

shader_type canvas_item;

// 移動ベクトル(ピクセル/秒)をGDScript側から渡す
uniform vec2 u_motion_dir = vec2(0.0, 0.0);

// ブラーの強さ(0.0〜1.0くらいを想定)
uniform float u_blur_strength : hint_range(0.0, 2.0) = 0.5;

// サンプリング回数(多いほど滑らかだが重くなる)
uniform int u_sample_count = 8;

// 残像の色補正(1.0, 1.0, 1.0 で元の色)
uniform vec3 u_tint_color = vec3(1.0, 1.0, 1.0);

// 残像の全体的な透明度
uniform float u_alpha_scale : hint_range(0.0, 1.0) = 0.7;

// 速度が小さいときにブラーを抑えるための閾値
uniform float u_min_speed_threshold = 5.0;

// 移動ベクトルをテクスチャUV空間にスケールする係数
// (ピクセル単位の速度をUVのオフセットに変換)
uniform float u_motion_to_uv_scale = 0.002;

// Godot 4 の fragment() 関数
void fragment() {
    // 元のテクスチャ色
    vec4 base_color = texture(TEXTURE, UV);
    
    // 速度の大きさ(ピクセル/秒想定)
    float speed = length(u_motion_dir);

    // 速度が小さいときはブラーをほぼ無効化
    float speed_factor = clamp((speed - u_min_speed_threshold) / u_min_speed_threshold, 0.0, 1.0);

    // 移動の「逆方向」にブラーをかけたいので、ベクトルを反転
    vec2 blur_dir = normalize(-u_motion_dir);
    if (speed <= 0.0001) {
        blur_dir = vec2(0.0, 0.0);
    }

    // UV空間でのオフセット量を計算
    // speed * u_motion_to_uv_scale で「どれだけ引き伸ばすか」を調整
    float blur_amount = u_blur_strength * speed_factor;
    vec2 uv_offset = blur_dir * blur_amount * u_motion_to_uv_scale;

    // サンプリング回数が1以下の場合はそのまま描画
    if (u_sample_count <= 1) {
        COLOR = base_color;
        return;
    }

    // マルチサンプリングによる簡易モーションブラー
    vec4 accum = vec4(0.0);
    int samples = max(u_sample_count, 1);
    float inv_samples = 1.0 / float(samples);

    for (int i = 0; i < samples; i++) {
        // t: 0.0〜1.0 の補間係数
        float t = float(i) * inv_samples;
        // 現在のUVから「過去方向」に向かってサンプリング
        vec2 sample_uv = UV + uv_offset * t;
        accum += texture(TEXTURE, sample_uv);
    }

    vec4 blurred = accum * inv_samples;

    // 色補正と透明度スケール
    blurred.rgb *= u_tint_color;
    blurred.a *= u_alpha_scale;

    // 元のピクセルとブラーをブレンド(ここでは単純にブラーを優先)
    // 好みに応じて mix(base_color, blurred, 0.5) などに変えてもOK
    COLOR = blurred;
}

GDScriptコード:AfterImageShader.gd

extends Node
class_name AfterImageShader
## シェーダー残像コンポーネント
## 対象のノードの移動量から「移動の逆方向」にブラーをかけるための
## シェーダーパラメータを自動更新します。
##
## 想定ターゲット:
## - Sprite2D / AnimatedSprite2D / MeshInstance2D
## - Sprite3D / MeshInstance3D(canvas_item 版ではなく spatial 版に書き換えればOK)
##
## ポイント:
## - 移動ベクトルを毎フレーム計算してシェーダーに渡す
## - シーン構造を汚さず、必要なノードにだけポン付けできる

@export var target_node: CanvasItem
## ブラーをかけたい対象ノード。
## 通常は Sprite2D / AnimatedSprite2D / MeshInstance2D などを指定します。
## 未指定の場合、自身の親ノードを CanvasItem として利用しようとします。

@export var enable_after_image: bool = true
## 残像エフェクトのON/OFFを切り替えます。
## デバッグ時や特定の状態だけ残像を出したいときに便利です。

@export_range(0.0, 2.0, 0.01)
var blur_strength: float = 0.5
## ブラーの強さ。
## シェーダーの u_blur_strength にそのまま渡されます。

@export_range(1, 32, 1)
var sample_count: int = 8
## ブラーのサンプリング回数。
## 値を増やすと滑らかになりますが、その分描画コストが上がります。

@export_range(0.0, 1.0, 0.01)
var alpha_scale: float = 0.7
## 残像全体の透明度。0.0 で完全に透明、1.0 で元のアルファそのまま。

@export var tint_color: Color = Color(1, 1, 1, 1)
## 残像の色味を調整します。
## 例: Color(0.4, 0.7, 1.0) で青っぽいスピードライン風の残像に。

@export_range(0.0, 200.0, 1.0)
var min_speed_threshold: float = 5.0
## この速度以下ではブラーをほぼかけないための閾値。
## 微妙な揺れやアイドルアニメーションでは残像を抑えたい場合に有効です。

@export_range(0.0, 0.01, 0.0001)
var motion_to_uv_scale: float = 0.002
## ピクセル単位の速度を UV オフセットに変換する係数。
## 速度の大きさと組み合わせて、ブラーの「伸び具合」を調整します。

@export var use_global_position: bool = true
## グローバル座標ベースで移動量を計算するかどうか。
## - true: 画面全体での動きをベースに残像を出す(通常はこちら)
## - false: 親ノードに対するローカル移動だけを見る

@export var max_tracked_speed: float = 2000.0
## 異常値(ワープなど)によってブラーが伸びすぎないようにするためのクランプ値。

# 内部状態
var _last_position: Vector2
var _material: ShaderMaterial

func _ready() -> void:
    # target_node が未指定なら、親ノードを CanvasItem として使う
    if target_node == null:
        if get_parent() is CanvasItem:
            target_node = get_parent() as CanvasItem
        else:
            push_warning("AfterImageShader: target_node が設定されておらず、親も CanvasItem ではありません。何も処理されません。")
            return

    # 対象ノードに ShaderMaterial がアタッチされているか確認
    var mat := target_node.material
    if mat == null:
        push_warning("AfterImageShader: target_node に ShaderMaterial が設定されていません。after_image.gdshader を割り当ててください。")
    elif mat is ShaderMaterial:
        _material = mat
    else:
        push_warning("AfterImageShader: target_node.material は ShaderMaterial ではありません。残像エフェクトは動作しません。")

    # 開始時点の座標を記録
    _last_position = _get_current_position()

    # 初期パラメータをシェーダーに反映
    _apply_static_params()

func _process(delta: float) -> void:
    if not enable_after_image:
        # OFFのときはシェーダーのブラーを0にしておく
        _set_shader_param_safe("u_blur_strength", 0.0)
        return

    if target_node == null or _material == null:
        return

    if delta <= 0.0:
        return

    # 現在位置と前フレーム位置から速度ベクトルを計算
    var current_pos := _get_current_position()
    var frame_delta := current_pos - _last_position

    # delta で割って「ピクセル/秒」相当にする
    var velocity := frame_delta / delta

    # 異常に大きな速度はクランプ(ワープなどの対策)
    if velocity.length() > max_tracked_speed:
        velocity = velocity.normalized() * max_tracked_speed

    # シェーダーに渡す(移動の「逆方向」ブラーはシェーダー側で反転している)
    _set_shader_param_safe("u_motion_dir", velocity)

    # 静的パラメータも毎フレーム同期しておくと、インスペクタからリアルタイム調整しやすい
    _apply_static_params()

    # 座標を更新
    _last_position = current_pos

func _get_current_position() -> Vector2:
    if target_node == null:
        return Vector2.ZERO
    if use_global_position:
        return target_node.get_global_transform().origin
    return target_node.position

func _apply_static_params() -> void:
    # GDScript 側の設定値をシェーダーの uniform に反映
    _set_shader_param_safe("u_blur_strength", blur_strength)
    _set_shader_param_safe("u_sample_count", sample_count)
    _set_shader_param_safe("u_alpha_scale", alpha_scale)
    _set_shader_param_safe("u_tint_color", Vector3(tint_color.r, tint_color.g, tint_color.b))
    _set_shader_param_safe("u_min_speed_threshold", min_speed_threshold)
    _set_shader_param_safe("u_motion_to_uv_scale", motion_to_uv_scale)

func _set_shader_param_safe(name: StringName, value) -> void:
    if _material == null:
        return
    # シェーダー側に存在しない uniform 名を設定しようとするとエラーになるのでガード
    if _material.shader != null and _material.shader.has_param(name):
        _material.set_shader_parameter(name, value)

使い方の手順

ここからは、実際に「プレイヤー」「敵」「動く床」に適用する具体例を交えながら、手順①〜④で見ていきます。

① シェーダーファイルを用意する

  1. res://shaders/after_image.gdshader という名前で新規ファイルを作成。
  2. 先ほどの shader_type canvas_item のコードを丸ごとコピペします。

このシェーダーは 2D用(canvas_item) なので、Sprite2D / MeshInstance2D などで使えます。

② スクリプトをプロジェクトに追加する

  1. res://components/AfterImageShader.gd を作成。
  2. 上記の GDScript コードをコピペします。

これで「AfterImageShader」というコンポーネントクラスがプロジェクト全体で使えるようになります。

③ ノード構成にコンポーネントを追加する

例として、2Dアクションゲームのプレイヤーキャラに残像を付けてみます。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── AfterImageShader (Node)   ← このノードに AfterImageShader.gd をアタッチ
  • AfterImageShader ノードを Player の子として追加。
  • AfterImageShader ノードに AfterImageShader.gd をアタッチ。
  • インスペクタで target_nodeSprite2D に指定。
  • Sprite2D の MaterialShaderMaterial を設定し、after_image.gdshader を割り当てる。

敵キャラに使う場合もほぼ同じです。

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── AfterImageShader (Node)

動く床に使う場合:

MovingPlatform (Node2D)
 ├── Sprite2D
 └── AfterImageShader (Node)

このように、「残像を付けたいノード」に対して AfterImageShader ノードを1個ぶら下げるだけでOKです。
Player / Enemy / MovingPlatform がどんな継承関係でも関係ありません。コンポーネントが速度を勝手に計算してくれます。

④ パラメータを調整して仕上げる

AfterImageShader ノードを選択し、インスペクタから以下のパラメータを調整します。

  • blur_strength: ブラーの強さ。0.3〜0.8あたりから試すと良いです。
  • sample_count: 8〜16程度がバランス良いです。重い場合は減らしましょう。
  • alpha_scale: 0.5〜0.8くらいで「うっすら見える」感じになります。
  • tint_color:
    • プレイヤー: Color(0.4, 0.7, 1.0)(青系)
    • 敵: Color(1.0, 0.3, 0.3)(赤系)
    • 動く床: Color(1.0, 1.0, 1.0)(白のまま)
  • min_speed_threshold: 5〜30あたり。小さくするとゆっくり動きでも残像が出やすくなります。
  • motion_to_uv_scale: 0.001〜0.004くらい。伸びすぎる場合は小さく、物足りなければ大きく。
  • use_global_position: カメラに追従する場合は true のままでOK。

ゲームを再生してキャラを動かすと、移動方向の「逆側」に向かってブラーが伸びるはずです。
ダッシュ時だけ enable_after_image = true にするなど、状態に応じてON/OFFするのも簡単ですね。


メリットと応用

この AfterImageShader コンポーネントを使うメリットはかなり多いです。

  • 継承に縛られない
    プレイヤーも敵もギミックも、クラス構造に関係なく「残像を付けたいノード」にだけコンポーネントをアタッチすればOKです。
    「残像付きプレイヤー」「残像付き敵」みたいなサブクラスを増やさなくて済みます。
  • シーン構造がスッキリする
    残像用のダミースプライトやパーティクル用ノードを量産せず、1ノード+1マテリアルで完結します。
    シーンツリーが「役割ごとのコンポーネント」で整理されていくのは、後から見ても気持ちいい構成ですね。
  • 再利用性が高い
    どのシーンでも同じコンポーネントを使えるので、「この敵だけ挙動が違うけど残像は同じで良い」といったケースで特に威力を発揮します。
  • アーティスト/レベルデザイナーフレンドリー
    パラメータはすべてエディタから調整可能なので、プログラマがいなくても「このボスの残像だけ青くて長い」みたいな微調整が可能です。

さらに応用として、

  • ダッシュ中だけ blur_strength を一時的に上げる
  • HPが少ないときに tint_color を赤くして「焦り感」を出す
  • スローモーション中だけ sample_count を増やしてリッチに見せる

といった演出も、コンポーネントを触るだけで実現できます。

改造案:状態に応じてブラーを自動変化させる

例えば、「一定速度以上で動いているときだけブラーを強くする」簡単な改造は以下のようにできます。

func _process(delta: float) -> void:
    if not enable_after_image:
        _set_shader_param_safe("u_blur_strength", 0.0)
        return
    if target_node == null or _material == null or delta <= 0.0:
        return

    var current_pos := _get_current_position()
    var frame_delta := current_pos - _last_position
    var velocity := frame_delta / delta

    var speed := velocity.length()
    if speed > max_tracked_speed:
        velocity = velocity.normalized() * max_tracked_speed

    # スピードに応じてブラー強度を自動調整(例: 0〜1.0 にマッピング)
    var speed_norm := clamp(speed / 600.0, 0.0, 1.0)
    var dynamic_blur := lerp(0.0, blur_strength, speed_norm)
    _set_shader_param_safe("u_motion_dir", velocity)
    _set_shader_param_safe("u_blur_strength", dynamic_blur)

    _apply_static_params()
    _last_position = current_pos

このように、コンポーネント化しておくと「残像の演出ロジック」を1か所に閉じ込められるので、ゲーム全体の見た目を一貫してコントロールしやすくなります。
継承より合成、コンポーネントで気持ちよく演出を積み上げていきましょう。