vfx について
vfxは、3Dシーン向けの短命なパーティクルエフェクトを設計するスキルです。プロシージャルなバーストと事前作成のフリップブックの使い分け、プールの利用、イベントに応じた演出を説明します。
主な機能
- 01プロシージャルなバーストとフリップブックの選択
- 02イベントごとの割り当てを避けるパーティクルプール
- 03加算発光エフェクトの作成
- 04寿命に合わせたパーティクル挙動の調整
活用シーン
- 01衝撃に火花や砂ぼこりを加える
- 02銃口の閃光や爆発の破片を作る
- 03アイテム取得や回復に輝きを加える
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ drawcall-ai/ skills --skill vfxインストール後、Claude Code で/vfxを入力して読み込まれたか確認します。
詳細
VFX とパーティクル
印象的な瞬間には、パーティクルの素早いバーストが必要です。そうでないと、平坦な印象になります。発射時のマズルフラッシュ、弾着地点の火花や砂ぼこり、足音に伴う煙、被弾時の血や破片の飛散、爆発時の閃光と煙、アイテム取得時のきらめきなどです。「閃光」の代わりに静止したメッシュを1つ置いただけでは安っぽく見えますが、短いパーティクルバーストなら本物のエフェクトらしく見えます。
まず、事前制作エフェクトか、プロシージャルなバーストか?
膨らみながら広がる爆発、炎、魔法の着弾など、リッチでアートディレクションされたエフェクトには、手作りのパーティクルより、事前レンダリングされたスプライトシート(フリップブック)を優先してください。コードで生成する四角形より見栄えがはるかによく、実行時のコストもほとんどかかりません。Market にはこの用途にぴったりの flipbook アセットが用意されており、flipbook スキル(@drawcall/flipbook)でビルボードとして再生できます(Flipbook クラスで KTX2 シートを読み込み、毎フレーム .update(delta) を呼び出します)。適したアセットがある場合や、エフェクトがループまたは単発のアニメーションビルボードで表現できる場合は、こちらを選びましょう。
以下のプロシージャルなパターンは、エフェクトにパラメーター指定またはデータ駆動が必要な場合に使います。たとえば、サーフェス法線に沿って飛び散る火花、物理挙動を伴う破片、イベントごとに変化する数や方向、環境に散らばるパーティクルなど、単一のスプライトシートでは表現できないエフェクトです。両者は組み合わせられます。フリップブックの爆発にプロシージャルな破片のバーストを加えると、どちらか一方だけの場合より見栄えがよくなります。
パターン:プール化、加算合成、短命
パーティクルエフェクトは、同時に生成されて外向きに飛び、数分の1秒で消える、小さな四角形の集まりです。プールは一度だけ作成して再利用し、イベントごとに割り当てないでください(割り当てるとカクつきます。エフェクトごとのコストについては lights スキルを参照してください)。
- プール:固定数のパーティクルプールを事前に作成します(
Pointsの雲、またはSpriteの集合)。イベント発生時に、ヒット地点で N 個をランダムな速度で有効化します。寿命が尽きたら無効化して再利用します。Pointsは1回のドローコールで済み、同一のビルボードによるバーストに適したデフォルトの選択肢です。個別の回転や独立したトランスフォームが必要な場合に限り、Spriteを使います。 - 加算発光:炎、火花、魔法、エネルギーには、重なり合った部分が光るよう、
blending: AdditiveBlending、depthWrite: false、柔らかな円形テクスチャを使います。(実際のライトを追加せずにマズルフラッシュや爆発の光を再現する方法でもあります。) - 寿命:各パーティクルの寿命は短く(約0.1〜0.6秒)、その間は速度に応じて移動させます(破片や血には重力を加えます)。フェードアウトまたは縮小させてから解放します(パーティクルごとのフェードや縮小には
Spriteまたはカスタムシェーダーが必要です。後述を参照してください)。
プールの要点は、イベントごとの割り当てをなくすことです。ジオメトリのバッファーは一度だけ割り当て、毎フレーム内容だけを書き換えます。Points を使ったバーストの例:
const MAX = 200
const PARKED_Y = -1e4 // park unused/dead particles here, off-screen below the scene
const positions = new Float32Array(MAX * 3)
for (let i = 0; i < MAX; i++) positions[i * 3 + 1] = PARKED_Y // park the whole pool until used — the buffer is zero-filled, so otherwise all MAX points render as a bright additive blob at the origin
const geometry = new THREE.BufferGeometry()
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
// soft round sprite, generated once — no asset needed
const c = document.createElement('canvas'); c.width = c.height = 64
const g = c.getContext('2d')!.createRadialGradient(32, 32, 0, 32, 32, 32)
g.addColorStop(0, 'rgba(255,255,255,1)'); g.addColorStop(1, 'rgba(255,255,255,0)')
const ctx2d = c.getContext('2d')!; ctx2d.fillStyle = g; ctx2d.fillRect(0, 0, 64, 64)
const sprite = new THREE.CanvasTexture(c)
const material = new THREE.PointsMaterial({
map: sprite, size: 0.3, transparent: true,
blending: THREE.AdditiveBlending, depthWrite: false,
})
const points = new THREE.Points(geometry, material)
points.frustumCulled = false // positions change every frame; the cached bounding sphere would cull the whole cloud wrongly
const vel = new Float32Array(MAX * 3)
const life = new Float32Array(MAX) // seconds remaining; 0 = free
function burst(at: THREE.Vector3, n: number) {
let spawned = 0
for (let i = 0; i < MAX && spawned < n; i++) {
if (life[i] > 0) continue
life[i] = 0.4
positions.set([at.x, at.y, at.z], i * 3)
vel.set([(Math.random() - 0.5) * 4, Math.random() * 4, (Math.random() - 0.5) * 4], i * 3)
spawned++
}
}
function update(delta: number) {
for (let i = 0; i < MAX; i++) {
if (life[i] <= 0) continue
life[i] -= delta
if (life[i] <= 0) { positions[i * 3 + 1] = PARKED_Y; continue } // died this frame — park it so it stops rendering
vel[i * 3 + 1] -= 9.8 * delta // accumulate gravity into velocity so debris accelerates and arcs — folding it straight into position (`(vel - 9.8*delta)*delta`) is a constant, frame-rate-dependent nudge, not a fall
positions[i * 3] += vel[i * 3] * delta
positions[i * 3 + 1] += vel[i * 3 + 1] * delta
positions[i * 3 + 2] += vel[i * 3 + 2] * delta
}
geometry.attributes.position.needsUpdate = true // the line agents forget — without it nothing moves
}
この最小限の Points 版では、寿命が尽きたパーティクルをシーンのかなり下方に移動させて瞬時に消します。おおむね水平なゲームカメラなら画面外になりますが、真上から見下ろすカメラや傾斜のきついカメラでは、その退避位置が見えることがあります(depthWrite: false の加算合成ポイントは、移動させるだけでは完全に見えなくなりません)。パーティクルごとに滑らかにフェードまたは縮小させることや、視点に左右されずに非表示にすること(size/alpha → 0)は、共有の PointsMaterial ではできません。頂点ごとの alpha/size 属性を持つカスタムシェーダーか、それぞれが独自の material.opacity と scale を持つ Sprite が必要です。
イベントに合わせてエフェクトを選ぶ
- マズルフラッシュ:銃身の先端に、数フレームだけ表示する加算合成のスプライトを1つ置きます。
- 着弾:ヒット地点で小さな火花や砂ぼこりのバーストを発生させ、サーフェス法線に沿って飛ばします。金属には火花、石や地面には砂ぼこり、肉には血を使います。
- 死亡/爆発:より大きなバーストにします。明るい閃光、外向きに飛び散る破片、その後に残る煙(暗く、加算合成ではなく、ゆっくり上昇して薄れる煙)を加えます。
- アイテム取得/回復:穏やかに上昇するきらめきを使います。
パーティクル数を控えめにし、プールのサイズに上限を設けることで、激しいアクション中も滑らかに動作します。