【Cocos Creator 3.8】LensDistortion(魚眼レンズ)の実装:アタッチするだけで画面中心を歪ませるワープ/衝撃波エフェクトを実現する汎用スクリプト

このガイドでは、Cocos Creator 3.8.7 + TypeScript で「LensDistortion(魚眼レンズ)」コンポーネントを実装します。
カメラのポストエフェクトとして画面中心を歪ませるシェーダーを適用し、ワープや衝撃波・時間停止のような演出を、カメラにアタッチするだけで使えるようにします。

外部の GameManager やシングルトンに一切依存せず、すべての設定はインスペクタから行えるように設計します。


コンポーネントの設計方針

1. 全体設計

  • 対象は Camera コンポーネントを持つノード(通常はメインカメラ)
  • カメラに postProcess を有効化し、レンズ歪み用の RenderPipeline/PostProcess 用マテリアルを適用する
  • 歪みの強さ・中心位置・半径・フェード時間などを @property から調整できる
  • 「常時かけっぱなし」モードと、「一時的に発動してフェードアウト」モードの両方をサポート
  • シェーダー(effect)は 事前に作成した Material アセットをインスペクタから指定する方式にし、スクリプト側ではそれを使うだけにする
  • カメラやマテリアルが未設定の場合は 防御的にエラーログを出し、実行時に落ちないようにする

※ Cocos Creator 3.8 では、ポストエフェクトの適用方法がプロジェクトのレンダーパイプライン設定に依存します。
ここでは 「カメラの postProcess にマテリアルを 1 つ差し込む」 形で実装します。プロジェクト側で Forward+ などの標準パイプラインを使っている前提です。

2. インスペクタで設定可能なプロパティ

LensDistortion コンポーネントに持たせるプロパティと役割を整理します。

  • targetCamera (Camera | null)
    • このポストエフェクトを適用する対象カメラ。
    • null の場合、onLoadthis.getComponent(Camera) を自動取得する。
  • distortionMaterial (Material | null)
    • レンズ歪み用のポストプロセスマテリアル。
    • カスタムシェーダー(effect)を使って作成した Material アセットを指定する。
    • このマテリアルには、以下の uniform を持たせる想定:
      • _Strength : 歪みの強さ(0〜1 目安)
      • _Radius : 歪みの半径(0〜1、画面の短辺基準)
      • _Center : 歪み中心(vec2、0〜1 の UV 座標)
      • _Fade : フェード係数(0〜1、0 で無効/1 で最大)
  • alwaysOn (boolean)
    • true: ゲーム開始時から常に歪みを適用。
    • false: スクリプトから triggerOnce() で一時的に発動する。
  • baseStrength (number)
    • 常時適用時の歪み強度(alwaysOn = true のときの基準値)。
    • 0 〜 1 程度が目安。0 にすると見た目上は無効。
  • pulseStrength (number)
    • triggerOnce() で発動させるときの最大強度。
    • ワープ/衝撃波のピーク時の歪み量。
  • radius (number)
    • 歪みがかかる範囲(0〜1)。
    • 0.5 なら画面中央から半径 50% 程度まで歪むイメージ。
  • centerX (number)
    • 歪み中心の X 座標(0〜1)。
    • 0.5 で画面中央、0 で左端、1 で右端。
  • centerY (number)
    • 歪み中心の Y 座標(0〜1)。
    • 0.5 で画面中央、0 で下端、1 で上端。
  • autoTriggerOnStart (boolean)
    • ゲーム開始時に一度だけパルス(衝撃波)を自動発動するか。
    • タイトル登場時などに便利。
  • pulseDuration (number)
    • パルスが 0 → 最大 → 0 と変化するまでの総時間(秒)。
    • 例:0.6 秒なら、0.3 秒でピークに達し、残り 0.3 秒で収束するイメージ。
  • useSmoothStep (boolean)
    • パルス強度の変化を smoothstep 的に滑らかにするか。
    • false なら単純な三角波的(線形)変化。
  • debugLog (boolean)
    • 重要イベント(初期化失敗・trigger 呼び出しなど)をログ出力するか。

TypeScriptコードの実装

以下が完成した LensDistortion.ts のコードです。


import { _decorator, Component, Camera, Material, renderer, math, log, warn, error } from 'cc';
const { ccclass, property, executeInEditMode, menu } = _decorator;

/**
 * LensDistortion
 * カメラのポストプロセスに魚眼レンズ風の歪みエフェクトを適用するコンポーネント。
 * - 対象ノードに Camera が必要。
 * - distortionMaterial にはポストエフェクト用のマテリアルを指定すること。
 *
 * マテリアル側で想定している uniform:
 *   - float _Strength : 歪みの強さ (0〜1)
 *   - float _Radius   : 歪みの半径 (0〜1)
 *   - vec2  _Center   : 歪み中心 (0〜1 の UV 座標)
 *   - float _Fade     : フェード係数 (0〜1) 0 で無効 / 1 で最大
 */
@ccclass('LensDistortion')
@executeInEditMode(true)
@menu('Custom/LensDistortion')
export class LensDistortion extends Component {

    @property({
        type: Camera,
        tooltip: '歪みエフェクトを適用する対象カメラ。\n未設定の場合、このノードの Camera を自動取得します。'
    })
    public targetCamera: Camera | null = null;

    @property({
        type: Material,
        tooltip: 'レンズ歪み用のポストプロセスマテリアル。\n' +
            'カスタムシェーダー(effect)を使って作成した Material アセットを指定してください。\n' +
            'マテリアルには _Strength, _Radius, _Center, _Fade の uniform がある想定です。'
    })
    public distortionMaterial: Material | null = null;

    @property({
        tooltip: 'true にすると、ゲーム開始時から常に歪みを適用します。\n' +
            'false の場合、triggerOnce() で一時的に発動させる運用がメインになります。'
    })
    public alwaysOn: boolean = false;

    @property({
        tooltip: '常時適用時の歪み強度 (0〜1 目安)。\n' +
            'alwaysOn = true のとき、この値がベースとして適用されます。'
    })
    public baseStrength: number = 0.2;

    @property({
        tooltip: 'triggerOnce() で発動させるパルスの最大強度 (0〜1 目安)。\n' +
            'ワープや衝撃波のピーク時の歪み量を決めます。'
    })
    public pulseStrength: number = 0.6;

    @property({
        tooltip: '歪みがかかる半径 (0〜1)。\n0.5 なら画面中央から半径 50% 程度まで歪みます。'
    })
    public radius: number = 0.6;

    @property({
        tooltip: '歪み中心の X 座標 (0〜1)。\n0.5 で画面中央、0 で左端、1 で右端。'
    })
    public centerX: number = 0.5;

    @property({
        tooltip: '歪み中心の Y 座標 (0〜1)。\n0.5 で画面中央、0 で下端、1 で上端。'
    })
    public centerY: number = 0.5;

    @property({
        tooltip: 'ゲーム開始時に一度だけパルス(衝撃波)を自動発動するかどうか。'
    })
    public autoTriggerOnStart: boolean = false;

    @property({
        tooltip: 'パルスが 0 → 最大 → 0 と変化するまでの総時間(秒)。\n' +
            '0.6 なら、約0.3秒でピークに達し、残り0.3秒で収束します。'
    })
    public pulseDuration: number = 0.6;

    @property({
        tooltip: 'true: パルス強度の変化を滑らか(smoothstep風)にします。\n' +
            'false: 単純な三角波(線形)的な変化になります。'
    })
    public useSmoothStep: boolean = true;

    @property({
        tooltip: 'true にすると、初期化失敗や trigger 呼び出しなどの情報をログ出力します。'
    })
    public debugLog: boolean = false;

    // 内部状態: 実際にカメラに適用するマテリアル (clone して使う)
    private _runtimeMaterial: Material | null = null;

    // パルス制御用
    private _pulseElapsed: number = 0;
    private _pulseActive: boolean = false;

    // カメラの postProcess 設定キャッシュ (簡易)
    private _postProcess: renderer.scene.PostProcess | null = null;

    onLoad() {
        // カメラ自動取得
        if (!this.targetCamera) {
            const cam = this.getComponent(Camera);
            if (cam) {
                this.targetCamera = cam;
                if (this.debugLog) {
                    log('[LensDistortion] Camera 自動取得:', this.node.name);
                }
            } else {
                warn('[LensDistortion] このノードに Camera コンポーネントがありません。' +
                    'targetCamera をインスペクタから設定してください。');
            }
        }

        // マテリアルの clone を作成(ランタイム専用)
        if (this.distortionMaterial) {
            this._runtimeMaterial = new Material();
            this._runtimeMaterial.copy(this.distortionMaterial);
        } else {
            warn('[LensDistortion] distortionMaterial が設定されていません。' +
                'ポストエフェクト用の Material アセットをインスペクタで指定してください。');
        }

        // エディタ上でもプレビューしやすいように、onLoad で一度適用を試みる
        this._setupPostProcess();
        this._applyUniforms(0); // 初期値(パルスなし)を反映
    }

    start() {
        if (this.autoTriggerOnStart) {
            this.triggerOnce();
        }
    }

    update(dt: number) {
        if (!this._runtimeMaterial || !this.targetCamera) {
            return;
        }

        // パルス処理
        let pulseFactor = 0;
        if (this._pulseActive && this.pulseDuration > 0) {
            this._pulseElapsed += dt;
            if (this._pulseElapsed >= this.pulseDuration) {
                this._pulseElapsed = this.pulseDuration;
                this._pulseActive = false;
            }
            const t = this._pulseElapsed / this.pulseDuration; // 0〜1
            // 0→1→0 の三角波的カーブ
            let tri = t <= 0.5 ? (t / 0.5) : (1 - (t - 0.5) / 0.5);
            tri = math.clamp01(tri);

            if (this.useSmoothStep) {
                // smoothstep(0,1,tri) * smoothstep(1,0,tri) 風の滑らかな山型
                const s1 = tri * tri * (3 - 2 * tri); // smoothstep(0,1,tri)
                const inv = 1 - tri;
                const s2 = inv * inv * (3 - 2 * inv); // smoothstep(0,1,1-tri)
                pulseFactor = s1 * s2 * 4; // 最大値を1付近に持っていくために係数調整
            } else {
                pulseFactor = tri;
            }
        }

        // 実際の強度 = ベース + パルス
        const strength = math.clamp01(this.baseStrength + this.pulseStrength * pulseFactor);

        this._applyUniforms(strength);
    }

    /**
     * 一度だけパルス(衝撃波)を発動させる。
     * alwaysOn に関係なく使用可能。
     */
    public triggerOnce() {
        if (!this._runtimeMaterial || !this.targetCamera) {
            warn('[LensDistortion] triggerOnce() が呼ばれましたが、初期化が完了していません。' +
                'Camera と distortionMaterial の設定を確認してください。');
            return;
        }
        if (this.pulseDuration <= 0) {
            warn('[LensDistortion] pulseDuration が 0 以下のため、パルスを発動できません。');
            return;
        }

        this._pulseActive = true;
        this._pulseElapsed = 0;

        if (this.debugLog) {
            log('[LensDistortion] triggerOnce() パルス発動');
        }
    }

    /**
     * カメラにポストプロセスとしてマテリアルを適用する。
     * プロジェクトのレンダーパイプライン設定に依存するため、
     * うまく動作しない場合は Camera の postProcess 設定を確認してください。
     */
    private _setupPostProcess() {
        if (!this.targetCamera) {
            return;
        }
        if (!this._runtimeMaterial) {
            return;
        }

        const camera = this.targetCamera;

        // 3.8 では Camera に postProcess プロパティが存在し、
        // そこから renderer.scene.PostProcess を取得できる前提。
        // 型定義上は private の場合もあるため、as any でアクセスする。
        const anyCam = camera as any;
        if (!anyCam.postProcess) {
            // postProcess が無効な場合、ここで有効化を試みる
            if (this.debugLog) {
                log('[LensDistortion] Camera.postProcess が無効のため、有効化を試みます。');
            }
            try {
                anyCam.enablePostProcess = true;
            } catch (e) {
                warn('[LensDistortion] Camera の postProcess を有効化できませんでした。' +
                    'プロジェクトのレンダーパイプライン設定を確認してください。', e);
            }
        }

        this._postProcess = anyCam.postProcess || null;

        if (!this._postProcess) {
            warn('[LensDistortion] PostProcess オブジェクトを取得できませんでした。' +
                'カメラの postProcess 設定が有効か確認してください。');
            return;
        }

        // ここでは簡易的に、ポストプロセスの最初のマテリアルとして差し込む想定。
        // 実プロジェクトでは、既存のエフェクトとの順序や数に応じて調整してください。
        const pp = this._postProcess;
        const materials: Material[] = (pp.materials || []) as Material[];

        // すでに同じマテリアルが登録されていないか確認
        const exists = materials.find(m => m === this._runtimeMaterial);
        if (!exists) {
            materials.unshift(this._runtimeMaterial);
            (pp as any).materials = materials;
            if (this.debugLog) {
                log('[LensDistortion] PostProcess に歪みマテリアルを追加しました。');
            }
        }
    }

    /**
     * マテリアルの uniform に現在のパラメータを反映する。
     * @param strength 実際に適用する歪みの強さ
     */
    private _applyUniforms(strength: number) {
        if (!this._runtimeMaterial) {
            return;
        }

        const mat = this._runtimeMaterial;

        // alwaysOn = false かつ パルスもない場合は完全無効化したい場合は、
        // strength が 0 のときに _Fade を 0 にするなどの運用が可能。
        // ここでは常に _Fade を 1 にしておき、strength=0 で見た目上無効とする。
        const fade = this.alwaysOn || this._pulseActive ? 1.0 : 1.0;

        try {
            mat.setProperty('_Strength', strength);
        } catch (e) {
            // uniform 名が異なる場合などはここに来る
            error('[LensDistortion] Material に _Strength プロパティが存在しません。effect の定義を確認してください。', e);
        }

        try {
            mat.setProperty('_Radius', math.clamp01(this.radius));
        } catch (e) {
            error('[LensDistortion] Material に _Radius プロパティが存在しません。effect の定義を確認してください。', e);
        }

        try {
            // UV ベース(0〜1)で中心座標を設定
            const cx = math.clamp01(this.centerX);
            const cy = math.clamp01(this.centerY);
            mat.setProperty('_Center', new math.Vec2(cx, cy));
        } catch (e) {
            error('[LensDistortion] Material に _Center プロパティが存在しません。effect の定義を確認してください。', e);
        }

        try {
            mat.setProperty('_Fade', fade);
        } catch (e) {
            error('[LensDistortion] Material に _Fade プロパティが存在しません。effect の定義を確認してください。', e);
        }
    }

    onDestroy() {
        // PostProcess からマテリアルを取り外す(過剰な副作用を避けるため)
        if (this._postProcess && this._runtimeMaterial) {
            const pp = this._postProcess;
            const materials: Material[] = (pp.materials || []) as Material[];
            const idx = materials.indexOf(this._runtimeMaterial);
            if (idx !== -1) {
                materials.splice(idx, 1);
                (pp as any).materials = materials;
                if (this.debugLog) {
                    log('[LensDistortion] PostProcess から歪みマテリアルを削除しました。');
                }
            }
        }
        this._postProcess = null;
        this._runtimeMaterial = null;
    }
}

コードのポイント解説

  • onLoad
    • Camera が未設定なら getComponent(Camera) で自動取得。
    • distortionMaterialcopy して、ランタイム専用の _runtimeMaterial を生成。
    • _setupPostProcess() でカメラのポストプロセスにマテリアルを差し込む。
    • _applyUniforms(0) で初期パラメータを反映。
  • start
    • autoTriggerOnStart が true の場合、ゲーム開始時に自動で一度だけパルスを発動。
  • update
    • パルスがアクティブなら pulseDuration に基づいて時間経過を計算。
    • 0 → 1 → 0 の三角波(線形 or smoothstep)で pulseFactor を計算。
    • 最終的な強度 strength = baseStrength + pulseStrength * pulseFactor を算出し、_applyUniforms でマテリアルに反映。
  • triggerOnce()
    • パルスを手動で発動する公開メソッド。
    • 外部スクリプトやアニメーションイベントから呼び出して、任意のタイミングで衝撃波を出せる。
  • _setupPostProcess()
    • Camera の postProcess を取得し、マテリアルリストの先頭に _runtimeMaterial を追加。
    • プロジェクトのレンダーパイプラインによっては挙動が異なる可能性があるため、ログとコメントで注意喚起。
  • _applyUniforms()
    • マテリアルの _Strength / _Radius / _Center / _Fade を更新。
    • uniform 名が間違っている場合は error() ログを出し、原因を特定しやすくしている。
  • onDestroy
    • コンポーネント破棄時に PostProcess からマテリアルを取り外し、他のカメラエフェクトへの影響を最小限にする。

使用手順と動作確認

ここからは、実際にエディタ上で LensDistortion コンポーネントを使う手順を説明します。

1. シェーダー(effect)とマテリアルの準備

このコンポーネントは「ポストエフェクト用のマテリアル」を前提としています。
プロジェクト内で以下のような流れで用意してください。

  1. Assets パネルで右クリック → Create → Effect を選択し、lens-distortion.effect のような名前で作成。
  2. 作成した effect ファイルを開き、ポストプロセス用のパスと、以下のような uniform を定義します(例・擬似コード)。
    
    // 非完全版・イメージ用の擬似コードです。
    // 実際の記述はプロジェクトのレンダーパイプラインに合わせて調整してください。
    
    CCEffect %{
      techniques:
      - passes:
        - vert: post-process-vs
          frag: lens-distortion-fs
          properties:
            _Strength: { value: 0.0 }
            _Radius:   { value: 0.5 }
            _Center:   { value: [0.5, 0.5] }
            _Fade:     { value: 1.0 }
    }%
    
    #include "cc-global"
    #include "cc-local"
    
    uniform float _Strength;
    uniform float _Radius;
    uniform vec2  _Center;
    uniform float _Fade;
    
    in vec2 v_uv;
    out vec4 fragColor;
    
    uniform sampler2D cc_gbuffer_albedo; // 画面テクスチャ (パイプラインに合わせて変更)
    
    void main() {
        vec2 uv = v_uv;
        vec2 dir = uv - _Center;
        float dist = length(dir);
    
        float mask = smoothstep(_Radius, 0.0, dist); // 中心ほど 1、外側で 0
        float strength = _Strength * _Fade * mask;
    
        // 簡易的な魚眼変形
        float factor = 1.0 + strength * dist * dist;
        vec2 warped = _Center + dir * factor;
    
        vec4 col = texture(cc_gbuffer_albedo, warped);
        fragColor = col;
    }
    
  3. Assets パネルで右クリック → Create → Material を選択し、LensDistortionMat などの名前で作成。
  4. 作成した Material を選択し、Inspector の Effect に先ほどの lens-distortion.effect を指定。
  5. この Material を、後で LensDistortion コンポーネントの distortionMaterial に割り当てます。

※ effect の実装はプロジェクトごとのレンダーパイプラインに強く依存します。
上記はあくまで「必要な uniform 名と概念」の参考とし、実際には Cocos のドキュメントや既存の PostProcess サンプルをベースに調整してください。

2. TypeScript スクリプトの作成

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

3. カメラノードにコンポーネントをアタッチ

  1. Hierarchy パネルで、メインカメラのノード(通常は Main Camera)を選択。
  2. Inspector の Add Component ボタンをクリック。
  3. Custom → LensDistortion を選択してアタッチします。
    • もし Custom カテゴリに表示されない場合は、スクリプトの @ccclass('LensDistortion') と保存状態を確認し、エディタを再起動してください。

4. Inspector でプロパティを設定

メインカメラノードを選択した状態で、Inspector の LensDistortion コンポーネントを確認します。

  • targetCamera
    • 通常は自動で同じノードの Camera が設定されます。
    • もし空欄の場合は、Hierarchy から Camera ノードをドラッグ&ドロップして設定してください。
  • distortionMaterial
    • 先ほど作成した LensDistortionMat をドラッグ&ドロップして割り当てます。
  • alwaysOntrue にしてテストする場合:
    • baseStrength: 0.2
    • pulseStrength: 0.6(とりあえずそのまま)
    • radius: 0.6
    • centerX: 0.5
    • centerY: 0.5
    • autoTriggerOnStart: false
    • pulseDuration: 0.6
    • useSmoothStep: true
    • debugLog: 必要に応じて true(動作確認時に便利)
  • alwaysOnfalse にして「衝撃波だけ」使う場合:
    • alwaysOn: false
    • baseStrength: 0(常時は歪ませない)
    • pulseStrength: 0.8 〜 1.0 くらいに上げて派手に
    • radius: 0.5 〜 0.8(好みで調整)
    • autoTriggerOnStart: true にすると、再生開始時に一度だけ衝撃波

5. シーンを再生して動作確認

  1. エディタ右上の ▶︎(再生)ボタン を押してゲームを実行します。
  2. alwaysOn = true の場合:
    • ゲーム画面の中央が魚眼レンズのように歪んでいれば成功です。
    • baseStrengthradius をリアルタイムに変更して、見た目の違いを確認してみてください。
  3. alwaysOn = false, autoTriggerOnStart = true の場合:
    • 再生開始直後に、中央から外側へ広がるような歪み(衝撃波)が一瞬発生すれば成功です。

6. スクリプトから任意タイミングで衝撃波を発生させる

LensDistortion は triggerOnce() メソッドを公開しているので、他のスクリプトから簡単に呼び出せます。
※ ここでは説明のために簡易スクリプトを書きますが、LensDistortion 自体は他のスクリプトに依存していません


import { _decorator, Component, Node } from 'cc';
import { LensDistortion } from './LensDistortion';
const { ccclass, property } = _decorator;

@ccclass('TestShockwave')
export class TestShockwave extends Component {

    @property(LensDistortion)
    lens: LensDistortion | null = null;

    start() {
        // 1秒後に衝撃波を発生
        this.scheduleOnce(() => {
            if (this.lens) {
                this.lens.triggerOnce();
            }
        }, 1.0);
    }
}

このように、イベントや攻撃ヒット時など、任意のタイミングで triggerOnce() を呼び出すことで、画面全体のワープ/衝撃波演出を簡単に追加できます。


まとめ

  • LensDistortion コンポーネントは、カメラにアタッチしてマテリアルを指定するだけで、魚眼レンズ風の歪みエフェクトを実現できます。
  • 歪みの強さ・半径・中心位置・パルス時間などを @property から調整できるため、ゲームごとの演出に合わせて柔軟にチューニング可能です。
  • 常時かけっぱなしの「ワープ空間」や、「テレポート時の画面ゆがみ」「ボス攻撃の衝撃波」「必殺技発動時の時間停止風エフェクト」など、幅広い視覚効果に応用できます。
  • 外部の GameManager やシングルトンに依存せず、このコンポーネント単体で完結しているため、他プロジェクトへの移植も容易です。

ポストエフェクト系のコンポーネントは、一度汎用化しておくと「カメラにアタッチしてパラメータを触るだけ」で多彩な演出を量産でき、ゲーム開発の効率が大きく向上します。
今回の LensDistortion をベースに、ブラーやカラーグレーディング、グリッチなど、他のポストエフェクトも同じ設計方針で増やしていくと、演出の引き出しが一気に広がります。