【Cocos Creator】アタッチするだけ!AfterImageShader (シェーダー残像)の実装方法【TypeScript】

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

【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 スクリプトの作成

  1. Assets パネルで右クリック → Create → TypeScript を選択します。
  2. ファイル名を AfterImageShader.ts にします。
  3. 自動生成された中身をすべて削除し、前述の TypeScript コードをそのまま貼り付けて保存します。

2. シェーダー用マテリアルの準備

ここでは、すでに「ブラー残像用のカスタムシェーダー」と「そのマテリアル」がある前提で進めます。
(例:after-image-effect.effectafter-image-effect.mtl など)

  1. 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_blurDiru_blurStrength にしているので、コンポーネント側も同じ名前をデフォルトにしてあります。

3. テスト用ノードの作成

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、テスト用の Sprite ノードを作成します。
  2. Inspector の Sprite コンポーネントで、適当な画像(キャラクターや弾丸など)を SpriteFrame に設定します。

この Sprite ノードに、AfterImageShader コンポーネントを追加していきます。

4. AfterImageShader コンポーネントのアタッチ

  1. Sprite ノードを選択します。
  2. Inspector の一番下にある Add Component ボタンをクリックします。
  3. CustomAfterImageShader を選択してアタッチします。

5. インスペクタでのプロパティ設定

Inspector に表示される AfterImageShader の各プロパティを、以下のように設定してみてください。

  • Enabled Effect:チェック ON
  • Material:先ほど準備したブラー残像用 Material(例: after-image-effect.mtl)をドラッグ&ドロップ
  • Blur Direction Propertyu_blurDir
  • Blur Strength Propertyu_blurStrength
  • Max Blur Strength1.0(最初は 1.0 くらいが扱いやすい)
  • Speed To Strength0.05(速度に対してどれくらい強度を上げるか)
  • Min Speed Threshold1.0(微小な揺れではブラーを出さない)
  • Smooth Factor0.2(0.1〜0.3 くらいが自然になりやすい)
  • Use World Space:チェック ON(通常はこちらでOK)

6. 簡単な動作確認(エディタ内プレビュー)

動きを確認するために、簡単な移動スクリプトを同じノードに追加するか、Animation/ Tween で動かします。ここでは Tween を使う例を挙げます。

  1. 同じ Sprite ノードに、テスト用スクリプトを追加します(名前例:TestMove.ts)。
  2. 以下のような簡単な移動コードを書きます(テスト用・参考):

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_blurDiru_blurStrength を、実際にブラー計算に使っているか。

まとめ

この AfterImageShader コンポーネントは、

  • ノードの移動量から 自動的に移動方向と速度を算出し、
  • シェーダーに「逆方向ブラー」のパラメータを送り続ける、
  • 完全に独立した汎用コンポーネントです。

外部の GameManager やシングルトンに依存せず、

  • 任意のノードにアタッチ
  • マテリアルとプロパティ名を Inspector で指定

するだけで、どのシーン・どのプロジェクトでも同じように使えます。

応用例としては:

  • ダッシュ中のキャラクターにだけ強い残像を付ける(enabledEffect の ON/OFF を切り替える)。
  • 弾丸やエフェクトの種類ごとに、maxBlurStrengthspeedToStrength を変えて個性を出す。
  • UI アニメーションに適用して、素早く動くパネルに残像を付ける。

シェーダー側の実装を差し替えれば、「色のフェード」「トレイル風」「方向性ノイズ」など、同じコンポーネントからさまざまな残像表現に発展させることもできます。
本記事のコードをベースに、あなたのプロジェクトに合った AfterImage エフェクトを自由にカスタマイズしてみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!