Godot 4で「動きのあるキャラをカッコよく見せたい」と思ったとき、まず思いつくのがアニメーションの強化やパーティクルですよね。でも、シルエットの残像やブラー表現を入れようとすると、
- 毎フレーム残像用のスプライトをスポーンして管理する
- アニメーションに合わせて残像の透明度や色を調整する
- ShaderMaterialを各シーンごとに個別設定していく
…と、途端に面倒くさくなります。さらに、プレイヤー、敵、動く床など「動くもの」全部に似たような残像ロジックを書き始めると、継承地獄かコピペ地獄に陥るのは時間の問題です。
そこで今回は、「シェーダー残像」をコンポーネントとして切り出し、どんなノードにもポン付けできる AfterImageShader コンポーネントを用意しました。
移動の逆方向にブラーをかけるタイプの残像表現を、スクリプト1本+シェーダー1本で完結させます。シーン階層を深くせず、「必要なノードにだけコンポーネントをアタッチする」構成でいきましょう。
【Godot 4】動きに“キレ”を足す残像ブラー!「AfterImageShader」コンポーネント
このコンポーネントは、以下の2つで構成されます。
- GDScript:
AfterImageShader.gd(Sprite2D / MeshInstance2D / Sprite3D などにアタッチ) - 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)
使い方の手順
ここからは、実際に「プレイヤー」「敵」「動く床」に適用する具体例を交えながら、手順①〜④で見ていきます。
① シェーダーファイルを用意する
res://shaders/after_image.gdshaderという名前で新規ファイルを作成。- 先ほどの
shader_type canvas_itemのコードを丸ごとコピペします。
このシェーダーは 2D用(canvas_item) なので、Sprite2D / MeshInstance2D などで使えます。
② スクリプトをプロジェクトに追加する
res://components/AfterImageShader.gdを作成。- 上記の GDScript コードをコピペします。
これで「AfterImageShader」というコンポーネントクラスがプロジェクト全体で使えるようになります。
③ ノード構成にコンポーネントを追加する
例として、2Dアクションゲームのプレイヤーキャラに残像を付けてみます。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── AfterImageShader (Node) ← このノードに AfterImageShader.gd をアタッチ
- AfterImageShader ノードを Player の子として追加。
- AfterImageShader ノードに
AfterImageShader.gdをアタッチ。 - インスペクタで target_node を
Sprite2Dに指定。 - Sprite2D の Material に
ShaderMaterialを設定し、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か所に閉じ込められるので、ゲーム全体の見た目を一貫してコントロールしやすくなります。
継承より合成、コンポーネントで気持ちよく演出を積み上げていきましょう。
