◈ Rollshade ツールを開く

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));

状態

どのメッシュにも、アニメーションするキャラクターにも掛けられます。元のテクスチャは残り、模様はアニメしても表面に貼り付いたままです。状態が外れると元の材質に戻ります。

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 にあります。知らない名前を渡すと、使える名前の一覧を添えたエラーになるので、アシスタントが自分で直せます。

性能

第 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 });

時間とループ

fx.update(seconds) は、エフェクトの位相を (seconds / period) % 1 にします。動く部分はすべて 1 周期でちょうど 1 回繰り返すので、どんな時計を渡しても継ぎ目なくループします。周期の既定値は Rollshade で選んだループの長さで、createEffect に period を渡すと変えられます。スクラブや一時停止をしたいときは、update を呼ぶ代わりに fx.uniforms.phase.value(0〜1)を直接設定します。

演出(Motion)のエフェクト

「演出」モードの技は、ほかのモードとは作りが違います。1 つのファイルに全部を詰めるのではなく、次の 2 つに分かれます。

「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();
});

スプライトシート(2D ゲーム向け)

「スプライトシート…」から、技を 1 回ぶん再生した全コマを 1 枚の PNG に並べて書き出せます。人形・剣・床は写らず、演出だけが残ります。

ブルームと描画のループ

「含める」でブルームを選んでいると、モジュールは 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)にしています。

うまくいかないとき

ライセンス

Rollshade から書き出したもの(技の定義とエフェクトのコード、スプライトシート、画像、動画)は CC0 1.0 で提供します。条件はなく、クレジットも不要です。営利・非営利を問わず、使う・改変する・共有する・販売する、どれも自由です。共通ランタイム(rollshade-fx.js。npm の rollshade と同じもの)は MIT License なので、著作権表示を残してください。psrdnoise を元にした補助関数も MIT なので、ファイルの先頭の表示を残してください。three.js も MIT です。zip には LICENSE.txt が入っています。画像や動画に写るモデルは対象外で、プリセットの Rollpoly のモデルには Rollpoly Asset License が適用されます。詳しくはライセンスと利用規約をご覧ください。