【Cocos Creator 3.8】AfterImageShader の実装:アタッチするだけで「移動方向に応じたブラー残像」を実現する汎用スクリプト
このガイドでは、Cocos Creator 3.8.7 + TypeScript で、ノードにアタッチするだけで「移動の逆方向にブラーが伸びる残像エフェクト」を制御できるコンポーネントを実装します。
移動するキャラクターや弾丸、UIアニメーションなどに付けるだけで、速度や方向に応じてシェーダーパラメータを自動更新してくれる汎用スクリプトです。
ポイントは以下です。
- 独立コンポーネント:外部の GameManager やシングルトン不要
- インスペクタからマテリアル・プロパティ名・強度などを自由に設定
- ノードの移動量から「移動方向ベクトル」「速度」を計算し、シェーダーに渡す
コンポーネントの設計方針
1. 機能要件の整理
- ノードの現在位置と前フレーム位置から「移動ベクトル」を算出する。
- 移動方向の逆向きにブラーが伸びるように、シェーダーに「ブラー方向ベクトル」を渡す。
- 移動速度に応じてブラー強度を変化させる(速いほど強く)。
- マテリアル側の任意のプロパティ名(例:
u_blurDir,u_blurStrength)に値を設定できるようにする。 - Sprite / Label / UIModel など、Material を扱えるレンダラー系コンポーネントに対応する。
- 外部スクリプトに依存せず、このコンポーネント単体で完結する。
2. 外部依存をなくすためのアプローチ
- シェーダーのマテリアルは、Inspector で直接指定してもらう。
- シェーダー内のプロパティ名も、文字列で Inspector に入力してもらう。
- レンダラーコンポーネント(
Sprite/Label/MeshRenderer等)は、onLoad 時に自動取得を試みる。 - 見つからなければ
console.errorで明示的に通知し、記事中で「このノードに Sprite などを付けてください」と説明する。 - Material は
sharedMaterialではなくmaterialを複製して使い、他ノードと共有しないようにする。
3. インスペクタで設定可能なプロパティ
AfterImageShader コンポーネントで用意する @property 一覧と役割です。
- enabledEffect (boolean)
- 残像エフェクトの ON/OFF。
- チェックを外すとシェーダーパラメータの更新を停止。
- material (Material | null)
- ブラー残像用のカスタムマテリアル。
- ここで指定したマテリアルのインスタンスを、このノード専用に複製して使用する。
- blurDirectionProperty (string)
- シェーダー側でブラー方向ベクトルを受け取るプロパティ名。
- 例:
u_blurDir(vec2 または vec3 を想定)。
- blurStrengthProperty (string)
- シェーダー側でブラー強度(スカラー)を受け取るプロパティ名。
- 例:
u_blurStrength(float)。
- maxBlurStrength (number)
- ブラー強度の最大値。
- 移動速度がどれだけ速くても、この値を超えないようにクランプする。
- speedToStrength (number)
- 「移動速度 → ブラー強度」への変換係数。
blurStrength = clamp(speed * speedToStrength, 0, maxBlurStrength)
- minSpeedThreshold (number)
- この速度未満のときはブラーを 0 にするしきい値。
- 微小な揺れや誤差で常にブラーが出続けるのを防ぐ。
- smoothFactor (number)
- フレームごとの強度変化を平滑化する係数(0〜1)。
- 0 に近いほど急激に変化し、1 に近いほどなめらかに追従。
- useWorldSpace (boolean)
- 移動ベクトルの計算に「ワールド座標」を使うか、「ローカル座標」を使うか。
- 通常は true(ワールド空間)で問題ない。
シェーダー側では、例として以下のようなプロパティを用意しておく想定です(GLSL/HLSL 風イメージ)。
// 例: シェーダー内のプロパティ
uniform vec2 u_blurDir; // ブラー方向(正規化ベクトル)
uniform float u_blurStrength; // ブラー強度
TypeScriptコードの実装
以下が完成した AfterImageShader.ts のコードです。
import { _decorator, Component, Node, Material, Renderer, Sprite, Label, UITransform, Vec2, Vec3, math } from 'cc';
const { ccclass, property } = _decorator;
/**
* AfterImageShader
* ノードの移動量から「逆方向ブラー」をシェーダーに渡す汎用コンポーネント。
*
* 前提:
* - このノードには Sprite / Label / Renderer 派生コンポーネントのいずれかがアタッチされていること。
* - マテリアルには、ブラー方向(vec2/vec3)とブラー強度(float)用のプロパティが定義されていること。
*/
@ccclass('AfterImageShader')
export class AfterImageShader extends Component {
@property({
tooltip: '残像エフェクトの有効/無効。OFFにするとシェーダーパラメータの更新を停止します。'
})
public enabledEffect: boolean = true;
@property({
type: Material,
tooltip: 'ブラー残像用のカスタムマテリアル。\nこのマテリアルを複製して、このノード専用のインスタンスとして適用します。'
})
public material: Material | null = null;
@property({
tooltip: 'シェーダー側の「ブラー方向」プロパティ名。\n例: u_blurDir(vec2 または vec3 を想定)。'
})
public blurDirectionProperty: string = 'u_blurDir';
@property({
tooltip: 'シェーダー側の「ブラー強度」プロパティ名。\n例: u_blurStrength(float)。'
})
public blurStrengthProperty: string = 'u_blurStrength';
@property({
tooltip: 'ブラー強度の最大値。移動速度が速くてもこの値を超えないようにクランプします。'
})
public maxBlurStrength: number = 1.0;
@property({
tooltip: '「移動速度 → ブラー強度」への変換係数。\nblurStrength = clamp(speed * speedToStrength, 0, maxBlurStrength)'
})
public speedToStrength: number = 0.05;
@property({
tooltip: 'この速度未満の場合、ブラー強度を0にします。\n微小な揺れで常にブラーが出るのを防ぐためのしきい値。'
})
public minSpeedThreshold: number = 1.0;
@property({
tooltip: 'ブラー強度の平滑化係数(0〜1)。\n0に近いほど急激に変化し、1に近いほどなめらかに追従します。'
})
public smoothFactor: number = 0.2;
@property({
tooltip: '移動量の計算にワールド座標を使うかどうか。\n通常はON(ワールド空間)で問題ありません。'
})
public useWorldSpace: boolean = true;
// 内部状態
private _renderer: Renderer | null = null;
private _instanceMaterial: Material | null = null;
private _lastPosWorld: Vec3 = new Vec3();
private _lastPosLocal: Vec3 = new Vec3();
private _currentBlurStrength: number = 0;
onLoad() {
// レンダラーコンポーネントの取得を試みる
this._renderer = this.getComponent(Renderer);
// Sprite や Label は Renderer を継承しているが、
// 万が一 Renderer が直接取得できない場合に備えて個別に取得を試みる
if (!this._renderer) {
const sprite = this.getComponent(Sprite);
if (sprite) {
this._renderer = sprite;
}
}
if (!this._renderer) {
const label = this.getComponent(Label);
if (label) {
this._renderer = label;
}
}
if (!this._renderer) {
console.error('[AfterImageShader] Renderer/Sprite/Label などのレンダラー系コンポーネントが見つかりません。このノードに Sprite などを追加してください。', this.node);
return;
}
// マテリアルのインスタンスを作成して適用
if (this.material) {
this._instanceMaterial = new Material();
this._instanceMaterial.copy(this.material);
this._renderer.setMaterial(this._instanceMaterial, 0);
} else {
// Inspector でマテリアル未設定の場合は、既存のマテリアルをインスタンス化して使う
const sharedMat = this._renderer.getSharedMaterial(0);
if (sharedMat) {
this._instanceMaterial = new Material();
this._instanceMaterial.copy(sharedMat);
this._renderer.setMaterial(this._instanceMaterial, 0);
console.warn('[AfterImageShader] Inspectorでmaterialが設定されていないため、既存の共有マテリアルを複製して使用します。', this.node);
} else {
console.error('[AfterImageShader] 適用可能なマテリアルが見つかりません。Inspectorでmaterialを設定してください。', this.node);
}
}
// 初期位置の記録
this.node.getWorldPosition(this._lastPosWorld);
this.node.getPosition(this._lastPosLocal);
}
start() {
// 初期フレームでブラー強度を0にしておく
this._currentBlurStrength = 0;
this._applyToMaterial(new Vec3(0, 0, 0), 0);
}
update(deltaTime: number) {
if (!this.enabledEffect) {
// OFFのときは強度を0にリセットしておくと安全
this._currentBlurStrength = 0;
this._applyToMaterial(new Vec3(0, 0, 0), 0);
return;
}
if (!this._renderer || !this._instanceMaterial) {
// 必要なコンポーネント/マテリアルがない場合は何もしない
return;
}
// 現在位置の取得
const currentWorldPos = new Vec3();
const currentLocalPos = new Vec3();
this.node.getWorldPosition(currentWorldPos);
this.node.getPosition(currentLocalPos);
// 使用座標空間に応じて移動ベクトルを計算
let moveVec = new Vec3();
if (this.useWorldSpace) {
Vec3.subtract(moveVec, currentWorldPos, this._lastPosWorld);
this._lastPosWorld.set(currentWorldPos);
} else {
Vec3.subtract(moveVec, currentLocalPos, this._lastPosLocal);
this._lastPosLocal.set(currentLocalPos);
}
// 速度の計算(距離 / deltaTime)
const distance = moveVec.length();
const speed = deltaTime > 0 ? distance / deltaTime : 0;
// 速度がしきい値以下ならブラーなし
let targetStrength = 0;
let blurDir = new Vec3(0, 0, 0);
if (speed >= this.minSpeedThreshold && distance > 0) {
// 移動方向ベクトル(正規化)
const dir = moveVec.normalize();
// ブラーは「移動の逆方向」に伸ばしたいので、符号を反転
blurDir.set(-dir.x, -dir.y, -dir.z);
// 速度からブラー強度を算出し、クランプ
targetStrength = math.clamp(speed * this.speedToStrength, 0, this.maxBlurStrength);
}
// 平滑化(線形補間)
this._currentBlurStrength = math.lerp(this._currentBlurStrength, targetStrength, this.smoothFactor);
// マテリアルへ反映
this._applyToMaterial(blurDir, this._currentBlurStrength);
}
/**
* マテリアルにブラー方向と強度を適用するヘルパー
*/
private _applyToMaterial(blurDir: Vec3, strength: number) {
if (!this._instanceMaterial) {
return;
}
// ブラー方向
if (this.blurDirectionProperty && this._instanceMaterial.passes.length > 0) {
// vec2/vec3 のどちらでも扱えるように setProperty でベクトルを渡す
try {
// CocosのMaterialはVec2/Vec3を内部でfloat配列に変換してくれる
this._instanceMaterial.setProperty(this.blurDirectionProperty, blurDir);
} catch (e) {
console.warn(`[AfterImageShader] ブラー方向プロパティ "${this.blurDirectionProperty}" の設定に失敗しました。シェーダー側の型(vec2/vec3)と名前を確認してください。`, e);
}
}
// ブラー強度
if (this.blurStrengthProperty && this._instanceMaterial.passes.length > 0) {
try {
this._instanceMaterial.setProperty(this.blurStrengthProperty, strength);
} catch (e) {
console.warn(`[AfterImageShader] ブラー強度プロパティ "${this.blurStrengthProperty}" の設定に失敗しました。シェーダー側の型(float)と名前を確認してください。`, e);
}
}
}
}
コードのポイント解説
- onLoad
- ノードに付いている
Renderer/Sprite/Labelのいずれかを取得。 - Inspector で指定された
materialを複製し、このノード専用のマテリアルインスタンスを作成して適用。 - Inspector 未設定の場合は、
getSharedMaterial(0)を複製して使う(警告ログを出す)。 - 初期のワールド座標・ローカル座標を記録。
- ノードに付いている
- start
- 初期フレームでブラー強度を 0 にリセットし、シェーダーにも 0 を適用。
- update(deltaTime)
enabledEffectが false のときは、強度を 0 にして早期 return。- 現在位置と前フレーム位置から「移動ベクトル」を算出。
- 距離 / deltaTime で「速度」を計算。
- 速度が
minSpeedThreshold未満ならブラー 0。 - 速度がしきい値以上なら:
- 方向ベクトル dir = moveVec.normalize()
- ブラー方向 = -dir(逆向き)
- 強度 = clamp(speed * speedToStrength, 0, maxBlurStrength)
smoothFactorによる lerp で強度をスムージング。_applyToMaterialでマテリアルのプロパティに反映。
- _applyToMaterial
blurDirectionPropertyに Vec3(内部で vec2/vec3 に対応)をセット。blurStrengthPropertyに float をセット。- プロパティ名や型が合わない場合は try-catch で警告を出し、ゲームが落ちないよう防御的に実装。
使用手順と動作確認
1. TypeScript スクリプトの作成
- Assets パネルで右クリック → Create → TypeScript を選択します。
- ファイル名を
AfterImageShader.tsにします。 - 自動生成された中身をすべて削除し、前述の TypeScript コードをそのまま貼り付けて保存します。
2. シェーダー用マテリアルの準備
ここでは、すでに「ブラー残像用のカスタムシェーダー」と「そのマテリアル」がある前提で進めます。
(例:after-image-effect.effect と after-image-effect.mtl など)
- Assets パネルで、ブラー残像用の Material アセット を用意します。
- Effect ファイル内に以下のようなプロパティを定義しておきます(名前は任意)。
// 例: effect ファイル内の properties セクション
properties: {
u_blurDir: { value: [0, 0, 0], editor: { type: 'vec3' } },
u_blurStrength: { value: 0.0, editor: { type: 'float', range: [0, 5, 0.01] } },
// ... その他のプロパティ
}
この例ではプロパティ名を u_blurDir と u_blurStrength にしているので、コンポーネント側も同じ名前をデフォルトにしてあります。
3. テスト用ノードの作成
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、テスト用の Sprite ノードを作成します。
- Inspector の Sprite コンポーネントで、適当な画像(キャラクターや弾丸など)を SpriteFrame に設定します。
この Sprite ノードに、AfterImageShader コンポーネントを追加していきます。
4. AfterImageShader コンポーネントのアタッチ
- Sprite ノードを選択します。
- Inspector の一番下にある Add Component ボタンをクリックします。
- Custom → AfterImageShader を選択してアタッチします。
5. インスペクタでのプロパティ設定
Inspector に表示される AfterImageShader の各プロパティを、以下のように設定してみてください。
- Enabled Effect:チェック ON
- Material:先ほど準備したブラー残像用 Material(例:
after-image-effect.mtl)をドラッグ&ドロップ - Blur Direction Property:
u_blurDir - Blur Strength Property:
u_blurStrength - Max Blur Strength:
1.0(最初は 1.0 くらいが扱いやすい) - Speed To Strength:
0.05(速度に対してどれくらい強度を上げるか) - Min Speed Threshold:
1.0(微小な揺れではブラーを出さない) - Smooth Factor:
0.2(0.1〜0.3 くらいが自然になりやすい) - Use World Space:チェック ON(通常はこちらでOK)
6. 簡単な動作確認(エディタ内プレビュー)
動きを確認するために、簡単な移動スクリプトを同じノードに追加するか、Animation/ Tween で動かします。ここでは Tween を使う例を挙げます。
- 同じ Sprite ノードに、テスト用スクリプトを追加します(名前例:
TestMove.ts)。 - 以下のような簡単な移動コードを書きます(テスト用・参考):
import { _decorator, Component, tween, Vec3 } from 'cc';
const { ccclass } = _decorator;
@ccclass('TestMove')
export class TestMove extends Component {
start() {
const startPos = new Vec3(-300, 0, 0);
const endPos = new Vec3(300, 0, 0);
this.node.setPosition(startPos);
tween(this.node)
.to(1.0, { position: endPos })
.to(1.0, { position: startPos })
.union()
.repeatForever()
.start();
}
}
この状態で 再生ボタン(Preview) を押すと:
- Sprite が左右に往復運動する。
- 移動中、移動方向の逆向きにブラーが引き伸ばされる(シェーダー実装に応じた見た目)。
- 停止中や低速時はブラーが弱く/消える。
もしブラーが出ない場合は、以下を確認してください。
- AfterImageShader の Material に、ブラー対応シェーダーのマテリアルが設定されているか。
- Effect ファイルのプロパティ名と、
blurDirectionProperty/blurStrengthPropertyが一致しているか。 - シェーダー側で
u_blurDirとu_blurStrengthを、実際にブラー計算に使っているか。
まとめ
この AfterImageShader コンポーネントは、
- ノードの移動量から 自動的に移動方向と速度を算出し、
- シェーダーに「逆方向ブラー」のパラメータを送り続ける、
- 完全に独立した汎用コンポーネントです。
外部の GameManager やシングルトンに依存せず、
- 任意のノードにアタッチ
- マテリアルとプロパティ名を Inspector で指定
するだけで、どのシーン・どのプロジェクトでも同じように使えます。
応用例としては:
- ダッシュ中のキャラクターにだけ強い残像を付ける(
enabledEffectの ON/OFF を切り替える)。 - 弾丸やエフェクトの種類ごとに、
maxBlurStrengthやspeedToStrengthを変えて個性を出す。 - UI アニメーションに適用して、素早く動くパネルに残像を付ける。
シェーダー側の実装を差し替えれば、「色のフェード」「トレイル風」「方向性ノイズ」など、同じコンポーネントからさまざまな残像表現に発展させることもできます。
本記事のコードをベースに、あなたのプロジェクトに合った AfterImage エフェクトを自由にカスタマイズしてみてください。




