Rollshade のエフェクトを three.js で使う
Rollshade は、three.js のゲームに完成した技・状態・設置エフェクトを渡します。いちばん手軽なのは npm パッケージ rollshade です(第 1 部)。ツールからファイルとして書き出すことも、ノード図で編集するループの TSL シェーダーを作ることもできます(第 2 部)。どれも WebGPURenderer で動き、WebGPU が無い環境では WebGL2 で動きます。
第 1 部 · パッケージ rollshade
入れ方
npm i rollshade [email protected]
ツールが使っているものと同じランタイムで、MIT License です。three の r186 と、three/webgpu の WebGPURenderer が要ります。WebGPU が無い環境では自動で WebGL2 に切り替わります。先にデモゲームで動きを見ることもできます。
import * as THREE from 'three/webgpu';
import { FXSystem, effect } from 'rollshade';
const renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.toneMapping = THREE.ACESFilmicToneMapping;
await renderer.init();
const fx = new FXSystem({ scene, camera, renderer, post: true, feel: true });
fx.add(effect('meteor', 'fire'));
await fx.prewarm();
const timer = new THREE.Timer();
renderer.setAnimationLoop((ms) => {
timer.update(ms);
fx.update(Math.min(timer.getDelta(), 0.05));
fx.render();
});
技
技は、溜め・飛翔・着弾・光・煙までそろった攻撃や回復やワープです。effect(レシピ, 属性) で定義を作り、fx.play() で撃ちます。返ってくるハンドルの hit イベントで、ダメージを入れる瞬間が分かります。
fx.play('fire-meteor', { from: hero.hand, to: enemy }).on('hit', (e) => enemy.damage(10 * e.power));
- レシピ 25 種、属性 10 種。魔法(projectile・lance・beam・explosion・pillar・meteor・nova・barrier・shockwave・summon・missiles・tornado・storm・drill)、近接(slash・thrust・spin・cross・smash・iaido・strike)、補助(heal・buff・warp)、演出(finale)。属性は fire・ice・thunder・wind・earth・water・light・dark・poison・arcane です。
- バリエーション。
effect('meteor', 'fire', { seed: 'k3x9ab' })で変化を選べます。探すならツールの演出モードが手軽です。シードを振り、スライダーを動かして、「npm で使う」のコードをコピーすると、見ているものと同じ技になります。 - 位置とイベント。
fromとtoにはVector3かObject3Dを渡します。Object3Dなら動きに追従します。命中にはpower、role、画面揺れとヒットストップの推奨値が載ります。ワープはvanishとappearを出します。
状態
どのメッシュにも、アニメーションするキャラクターにも掛けられます。元のテクスチャは残り、模様はアニメしても表面に貼り付いたままです。状態が外れると元の材質に戻ります。
await fx.prewarmStatus(enemyModel); // モデルの種類ごとに 1 回
const frozen = fx.status(enemy, 'freeze', { duration: 1.2 });
frozen.on('full', () => (mixer.timeScale = 0)); // アニメを止めるのはゲーム側
frozen.stop(); // 解けて氷が砕ける
燃焼(burn)・凍結(freeze)・感電(shock)・毒(poison)・石化(petrify)・消滅(dissolve)・出現(appear)・祝福(bless)・呪い(curse)・シールド(shield)・気絶(stun)。progress(0〜1)で凍結・石化・消滅の広がりを動かせます。シールドは impact(命中点) で波紋が出ます。appear 以外は、止めるまで続きます。演出モードの「状態」で見られます。
設置エフェクト
fx.loop('torch', new THREE.Vector3(2, 0, -3));
fx.loop('portal', gate.position, { rotation: Math.PI / 2, element: 'dark' });
const aura = fx.loop('aura', hero); // キャラクターに追従
aura.stop();
たいまつ(torch)・焚き火(campfire)・燭台(candles)・ポータル(portal)・魔法陣(sigil)・セーブポイント(savepoint)・結界(barrier)・オーラ(aura)・光の柱(beacon)。簡単なローポリの台が付きます。自分のモデルに付けるときは prop: false を渡してください。点光源の数が変わると three.js が全部の材質を作り直すので、設置エフェクトは決まった数(loopLights、既定 4)の点光源を分け合います。
React Three Fiber
import { FX, useFX, Status, Loop } from 'rollshade/react';
<Canvas gl={async (props) => { const r = new THREE.WebGPURenderer(props); await r.init(); return r; }}>
<FX effects={[effect('meteor', 'fire')]} feel>
<Status target={enemyRef} name="burn" active={burning} />
<Loop name="torch" position={[2, 0, -3]} />
</FX>
</Canvas>
useFX() は、コンパイルの準備が済むと fx.play() に使うシステムを返します。React Three Fiber 9 以降が要ります。
AI のコーディングアシスタントと使う
パッケージにはエージェントのスキル node_modules/rollshade/skills/rollshade/SKILL.md が入っています。npx skills add ./node_modules/rollshade/skills/rollshade で Claude Code・Cursor・Codex などに入れられます。言語モデル向けの要約は /llms.txt、全文は /llms-full.txt にあります。知らない名前を渡すと、使える名前の一覧を添えたエラーになるので、アシスタントが自分で直せます。
性能
- 読み込み中に
fx.prewarm()とfx.prewarmStatus()を呼んでおくと、ゲームの途中でシェーダーのコンパイルが起きません(特に WebGL2 で効きます)。 quality: 'auto'にすると、フレームが予算を超えている間は粒の数と描画の解像度を下げ、軽くなると戻します。fx.setQuality('medium' | 'low')で固定でき(解像度 75% と 55%)、fx.renderScaleで解像度を直接決めることもできます。粒は画面の半分より大きくならず、カメラの近くでは薄くなります。- 自前のポスト処理を使うときは
post: falseにして、いつもどおり描画してください。組み込みのブルームと画面揺れは使えなくなります。
第 2 部 · ツールからの書き出しと TSL シェーダー工房
演出モードは、同じランタイムと定義をファイルとして書き出せます(動く例の入った zip、2D ゲーム向けのスプライトシート、動画)。状態と設置エフェクトも書き出せます。シェーダーのモードは、関数 createEffect を 1 つだけ export する ES モジュールを作ります。TSL のノードマテリアルでエフェクトを組み立て、必要なメッシュを足し、光る部分だけにブルームを掛けるレンダーパイプラインを返します。
レンダラーの準備
npm install three
コードをファイルとして保存し(ダウンロードボタンが使えます)、次のように読み込みます。
import * as THREE from 'three/webgpu';
import { createEffect } from './rollshade_fire_k3x9ab.js';
const renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
document.body.append(renderer.domElement);
await renderer.init();
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(35, innerWidth / innerHeight, 0.05, 100);
camera.position.set(0, 1.6, 6);
camera.lookAt(0, 0.75, 0);
three は three/webgpu から import してください。three と両方を 1 つのページで読み込むと、ノードマテリアルが動きません。
ランタイム版(既定)
コードの区画の「ランタイム版」は、同じエフェクトを共通ランタイム rollshade-fx.js(演出のエフェクト)に載せる形で書き出します。補助関数はランタイムから import し、export default defineLoop({ id, period, bloom, create }) を 1 つ出します。演出の技と一緒に fx.add() し、fx.spawn(id, { at }) で置きます(「モデルに掛ける」で作ったものは fx.spawn(id, { object: mesh }))。描画とブルームは fx.render() の 1 回で済み、エフェクトをいくつ置いても補助関数は重複しません。戻り値のハンドルは uniforms・pause()・resume()・moveTo()・stop() を持ちます。下の説明は「単体ファイル」(1 ファイルで完結する今までの形)のものです。
単体エフェクト
const fx = createEffect({ scene, camera, renderer });
renderer.setAnimationLoop((ms) => {
fx.update(ms / 1000);
fx.renderPipeline.render();
});
エフェクトは原点の地面に立ち、高さはおよそ 2 です。fx.object がメッシュとパーティクルをまとめたグループなので、ほかのオブジェクトと同じように動かしたり、回したり、大きさを変えたりできます。
モデルに掛けるエフェクト
「モデルに掛ける」で作ったエフェクトは、飾るオブジェクトを受け取ります。
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const gltf = await new GLTFLoader().loadAsync('hero.glb');
scene.add(gltf.scene);
scene.environment = myEnvironmentMap;
const fx = createEffect({ object: gltf.scene, scene, camera, renderer });
objectの中のすべてのメッシュに、元の色・テクスチャ・法線マップ・粗さ・透明度を引き継いだノードマテリアルを付け、その上にエフェクトを重ねます。マテリアルが複数あるメッシュでは、そのすべてに掛けます。- 外殻・輪郭・足元のエフェクト・パーティクルの大きさは、
createEffectを呼んだ時点のモデルの外接箱から決めます。先にモデルをシーンに足し、位置と大きさを決めてから呼んでください。 - スキニングやモーフのあるモデルは、そのままアニメーションします。外殻と輪郭は動きに追従し、表面の模様は肌に貼り付いたまま動きます。
- 表面のレイヤーは標準のマテリアルなので、光が要ります。シーンにライトか環境マップを置いてください。プレビューでは、キーライト、半球ライト、強さ 0.45 の
RoomEnvironmentを使っています。
時間とループ
fx.update(seconds) は、エフェクトの位相を (seconds / period) % 1 にします。動く部分はすべて 1 周期でちょうど 1 回繰り返すので、どんな時計を渡しても継ぎ目なくループします。周期の既定値は Rollshade で選んだループの長さで、createEffect に period を渡すと変えられます。スクラブや一時停止をしたいときは、update を呼ぶ代わりに fx.uniforms.phase.value(0〜1)を直接設定します。
演出(Motion)のエフェクト
「演出」モードの技は、ほかのモードとは作りが違います。1 つのファイルに全部を詰めるのではなく、次の 2 つに分かれます。
- 共通ランタイム
rollshade-fx.js。パーティクル、光源、ポスト処理、スケジューラをまとめたもので、技をいくつ使っても 1 つだけ置きます。 - 技ごとの定義
fx/<id>.js。レシピ・属性・調整値だけを持つ数百バイトのファイルです。
「zip で書き出し」を使うと、ランタイム、型定義、選んだ技、それらをまとめて import する fx/index.js、そのまま動く example.html が 1 つにまとまります。複数の技は「書き出しリスト」に足してから書き出します。
import * as THREE from 'three/webgpu';
import { FXSystem } from './rollshade-fx.js';
import effects from './fx/index.js';
const fx = new FXSystem({ scene, camera, renderer, post: true, feel: true });
fx.add(...effects);
await fx.prewarm();
const h = fx.play('fire-projectile-k3', { from: hero.hand, to: enemy });
h.on('hit', (e) => enemy.damage(10 * e.power));
const timer = new THREE.Timer();
renderer.setAnimationLoop((ms) => {
timer.update(ms);
fx.update(timer.getDelta());
fx.render();
});
- 何回でも重ねて撃てます。
fx.play()は呼ぶたびに新しく 1 回ぶんを始め、戻り値のハンドルで個別に扱えます。同じ技を連打しても、違う技を同時に撃っても、パーティクルとポスト処理は 1 セットを共有します。 - 狙い。
fromとtoはVector3でもObject3Dでもよく、Object3Dなら動いている相手を追います。近接の技はキャラクターの胸あたりをfromにしてください。剣の位置はh.blade.baseとh.blade.tipに毎フレーム入るので、自分の剣のモデルを合わせられます。 - イベント。
h.on(name, fn)のnameは'cast'(溜め開始)・'release'(発射・振り始め)・'hit'(命中)・'end'(技が終わった。最後の粒子はまだ消えている途中のことがあります)です。barrier・buff・warpはhitを出しません。ハンドラの中でh.stop()やfx.clear()を呼んでも大丈夫で、ハンドラで起きたエラーでエフェクトが止まることもありません。hitには当たった位置・威力と、推奨の画面揺れ・ヒットストップ秒数、命中の役割role('first'最初・'link'つなぎ・'final'最後・'tick'連続ヒット)が入ります。多段の技では最初と最後だけしっかり止まるよう、推奨の秒数はこの役割で変わります。当たり判定やダメージはこのイベントから自分のゲームで動かしてください。回復の技は威力 0 のhitを回復が入る瞬間に出します。ワープは'vanish'(消えた位置)と'appear'(現れる位置)を出すので、キャラクターの移動はこの 2 つに合わせてください。 - 画面の演出は選べます。
feel: trueで命中時の画面揺れ・ヒットストップ・色収差を自動で掛けます。外すと何もせず、hitの推奨値を使って自分で掛けられます(fx.shake()・fx.hitStop()も使えます)。post: trueはブルームと歪みを含むパイプラインで描きます。自分のポスト処理がある場合はpost: falseにし、fx.render()の代わりにいつもどおり描いてください。 - 最初に
prewarm()。シェーダーを読み込み時にまとめて作ります。呼ばないと、最初に使う瞬間に一度だけ引っかかります。 - 重さの調整。粒は画面の高さの半分より大きくならず(
maxScreenSize、既定 0.5)、カメラの 0.4〜1.4 m 手前で薄れて消えます(nearFade)。quality: 'auto'にすると、フレームが予算(frameBudget、既定 1/58 秒)を超え続けたときに粒の量を減らし、軽くなったら戻します。autoResolution: trueを足すと、描画の解像度(ピクセル比)も一緒に下げ上げします。これはゲーム全体の描画に効くので、自分で解像度を管理しているなら付けないでください。 - 時間。
fx.update(seconds)には前のフレームからの秒数を渡します。fx.timeScaleでスローにでき、ヒットストップ中も全部の演出が一緒に止まります。 - 光に敏感な方への配慮。
photosensitive: trueで画面の閃光を弱めます。白黒が反転する 1 コマ(impactFrames)は既定で切ってあります。 - 片付け。
fx.clear()で再生中の技を全部止め、fx.dispose()で GPU の資源を解放します。
スプライトシート(2D ゲーム向け)
「スプライトシート…」から、技を 1 回ぶん再生した全コマを 1 枚の PNG に並べて書き出せます。人形・剣・床は写らず、演出だけが残ります。
- カメラ。今の視点、真横(横スクロール向け)、術者の後ろから、真上(見下ろし型向け)、アイソメトリック、斜め上からの見下ろし、から選べます。今の視点以外は望遠で撮るので、遠近の歪みがほとんどありません。ズームで寄り引きを調整できます。
- 背景。「透明」は黒と白の 2 回描いて差から透明度を求めるので、光も煙も半透明のまま残ります。加算合成で重ねるエンジンなら「黒」も使えます。
- JSON。TexturePacker 形式(hash)で、Phaser の
load.atlasや PixiJS のAssets.loadでそのまま読めます。animationsにコマの並び、meta.fpsにフレームレート、meta.hitFramesに命中したコマの番号が入るので、ダメージの発生をそのコマに合わせられます。 - 続く部品のループ。強化のオーラ・嵐・竜巻の「本体」は、続いている間の 1 周を継ぎ目なくループするシートとして書き出します(
meta.loopが true)。技の効果が続く間、ゲーム側で繰り返し再生してください。
ブルームと描画のループ
「含める」でブルームを選んでいると、モジュールは RenderPipeline を作ります。シーンを描き、発光する部分だけにブルームを足すので、シーンのほかの部分は光りません。renderer.render() の代わりに fx.renderPipeline.render() を呼んでください。透明度も保つので、透過したキャンバスの上でも使えます。
すでに自分のポストエフェクトがあるときは、ブルームを外してください。モジュールはパイプラインを返さなくなり、いつもどおり renderer.render(scene, camera) か自分のパイプラインで描けます。エフェクトは光を emissive の出力に書くので、emissive を読むブルームなら拾えます。
実行中の調整
fx.uniforms には、レイヤーのパネルにあるスライダーが、レイヤー名を付けた名前で入っています。
fx.uniforms.flame1Intensity.value = 2;
fx.uniforms.flame1Color.value.set('#ff8800');
fx.uniforms.bloomStrength.value = 0.8;
uniform を変えてもシェーダーは作り直さないので、毎フレーム動かしても軽く済みます。
エフェクトを外す
fx.dispose();
dispose は、モデルに元のマテリアルを戻し、エフェクトが足したメッシュとパーティクルを外し、GPU の資源を解放します。
UV の無いモデル
模様によっては、メッシュの UV に沿わせることができます。「座標」が「Auto」のとき、コードはメッシュごとに UV の有無を調べ、あれば UV、無ければ位置を使うので、UV の無いモデルでも動きます。使うモデルが決まっているなら、「含める」で「UV だけ」か「位置だけ」を選ぶと、この分岐がコードから消えます。多くの模様はもともと位置を使うので、UV が無くても表面に貼り付いたまま動きます。
含めるもの
- マテリアル: モデル自身の表面の模様。
- 外殻・輪郭: モデルを包む光の外殻と、後ろ側に出る輪郭線。
- 要素・パーティクル: 炎、光、魔法陣、輪、パーティクル。
- ブルーム: 上のレンダーパイプライン。
プレビューもこの切替に従うので、見えているものがそのままコードの動きです。
TypeScript
TS のタブは、同じモジュールに型を付けたものです。使っている three のバージョンに合った @types/three が要ります。TSL の型はノードの種類に厳しいため、一部の値は any(type N = any)にしています。
うまくいかないとき
- 何も表示されない。最初のフレームの前に
await renderer.init()を待ち、import がすべてthree/webgpuとthree/tslからになっているか確かめてください(import map を手で書くときはthree/addons/も要ります)。 three/addons/…が見つからない。このパスは three パッケージのthree/examples/jsm/…を指します。Vite などのバンドラーは解決できます。import map を使うときは、three/addons/をそのフォルダに割り当ててください。- プレビューより明るい・暗い。プレビューは ACES Filmic のトーンマッピング、キーライト、環境マップを使っています。それに合わせるか、強さの uniform で調整してください。
- スマホで重い。パーティクルの数やノイズのオクターブは、コードの中のただの数値です。減らすか、ピクセル比を下げてください。
- 明滅。エフェクトには明滅が含まれることがあります。光に敏感な方のため、明滅は 1 秒に 3 回以下にしてください。ループの周期を長くすると回数が減ります。
ライセンス
Rollshade から書き出したもの(技の定義とエフェクトのコード、スプライトシート、画像、動画)は CC0 1.0 で提供します。条件はなく、クレジットも不要です。営利・非営利を問わず、使う・改変する・共有する・販売する、どれも自由です。共通ランタイム(rollshade-fx.js。npm の rollshade と同じもの)は MIT License なので、著作権表示を残してください。psrdnoise を元にした補助関数も MIT なので、ファイルの先頭の表示を残してください。three.js も MIT です。zip には LICENSE.txt が入っています。画像や動画に写るモデルは対象外で、プリセットの Rollpoly のモデルには Rollpoly Asset License が適用されます。詳しくはライセンスと利用規約をご覧ください。