Using Rollshade effects in three.js
Rollshade gives three.js games ready-made moves, status effects and placed effects. The easiest way to use them is the npm package rollshade (part 1). The tool can also export effects as files and generate looping TSL shaders you edit as node graphs (part 2). Everything runs on WebGPURenderer, which falls back to WebGL2.
Part 1 · The rollshade package
Install
npm i rollshade [email protected]
The package is the same runtime the tool uses, under the MIT License. It needs three r186 and a WebGPURenderer from three/webgpu, which falls back to WebGL2 on its own. Try everything in the demo game first if you like.
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();
});
Moves
A move is a complete attack, heal or warp: charge-up, travel, impact, lights and smoke. effect(recipe, element) makes its definition; fx.play() fires it and returns a handle whose hit events tell your game when to apply damage.
fx.play('fire-meteor', { from: hero.hand, to: enemy }).on('hit', (e) => enemy.damage(10 * e.power));
- 25 recipes, 10 elements. Magic (projectile, lance, beam, explosion, pillar, meteor, nova, barrier, shockwave, summon, missiles, tornado, storm, drill), melee (slash, thrust, spin, cross, smash, iaido, strike), support (heal, buff, warp) and events (finale), in fire, ice, thunder, wind, earth, water, light, dark, poison and arcane.
- Variations.
effect('meteor', 'fire', { seed: 'k3x9ab' })picks a variation. The easiest way to find one is the Motion mode of the tool: roll seeds, move the sliders, and copy the code under Use it from npm, which reproduces exactly what you see. - Anchors and events.
fromandtotake aVector3or anObject3Dthat is followed while it moves. Hits carrypower, aroleand suggested shake and hit-stop values; warps sendvanishandappear.
Status effects
Put a status on any mesh or animated character. Its textures stay, the pattern sticks to the surface while it animates, and the original materials come back when the status is gone.
await fx.prewarmStatus(enemyModel); // once per kind of model
const frozen = fx.status(enemy, 'freeze', { duration: 1.2 });
frozen.on('full', () => (mixer.timeScale = 0)); // stopping the animation is up to you
frozen.stop(); // thaw; the ice shatters
Burn, freeze, shock, poison, petrify, dissolve, appear, bless, curse, shield and stun. progress (0–1) drives how far freeze, petrify and dissolve have spread; a shield ripples where you call impact(point). Statuses last until you stop them, except appear. Browse them in the Status effects group of the Motion mode.
Placed effects
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); // follows the character
aura.stop();
Torch, campfire, candles, portal, magic circle, save point, barrier dome, aura and beacon. They come with simple low-poly stands; pass prop: false to put the effect on your own model. Loops share a fixed pool of point lights (loopLights, 4 by default) because changing the number of lights makes three.js recompile every material.
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() returns the system once it has prewarmed, for fx.play(). Needs React Three Fiber 9 or later.
With AI coding assistants
The package ships an agent skill at node_modules/rollshade/skills/rollshade/SKILL.md; install it for Claude Code, Cursor, Codex and others with npx skills add ./node_modules/rollshade/skills/rollshade. A compact reference for language models is at /llms.txt and the full one at /llms-full.txt. Unknown names throw errors that list the valid ones, so an assistant can correct itself.
Performance
- Call
fx.prewarm()andfx.prewarmStatus()while loading so no shader compiles mid-game (this matters most on WebGL2). quality: 'auto'lowers particle counts and the render resolution while frames run over budget and raises them again;fx.setQuality('medium' | 'low')fixes them (75% and 55% resolution), andfx.renderScalesets the resolution directly. Particles never grow past half the screen and fade near the camera.- With your own post processing, pass
post: falseand render as usual; you lose the built-in bloom and camera shake.
Part 2 · Exporting from the tool and the TSL shader lab
The Motion mode exports the same runtime and definitions as files (a ZIP with an example, sprite sheets for 2D games, or video), for status effects and placed effects too. The Shader modes generate a single ES module that exports createEffect: TSL node materials, the meshes the effect needs, and a render pipeline with bloom on the glowing parts only.
Set up the renderer
npm install three
Save the code as a file next to your script (for example with Download), then:
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);
Import everything from three/webgpu, not from three. Loading both copies in one page breaks node materials.
Runtime format (default)
The Runtime tab of the code panel writes the same effect for the shared runtime rollshade-fx.js (Motion effects). Helpers are imported from the runtime and the module exports defineLoop({ id, period, bloom, create }). Add it with fx.add() next to Motion effects and place it with fx.spawn(id, { at }) (or fx.spawn(id, { object: mesh }) for effects made on a model). One fx.render() draws everything with one bloom, and helpers are never duplicated however many effects you place. The handle has uniforms, pause(), resume(), moveTo() and stop(). The rest of this section describes the Single file format, a self-contained module as before.
A standalone effect
const fx = createEffect({ scene, camera, renderer });
renderer.setAnimationLoop((ms) => {
fx.update(ms / 1000);
fx.renderPipeline.render();
});
The effect stands on the ground at the origin and is about two units tall. fx.object is the group that holds its meshes and particles, so you can move, rotate or scale it like any other object.
An effect on a model
Effects made with On a model take the object to decorate:
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 });
- Every mesh inside
objectgets a node material that keeps its color, texture, normal map, roughness and transparency, with the effect layered on top. Meshes with several materials get the effect on each of them. - Shells, outlines, ground effects and particles are sized from the model's bounding box, measured when you call
createEffect. Add the model to the scene and place and scale it first. - Skinned and morphing models keep animating. Shells and outlines follow the animation, and surface patterns stay attached to the skin.
- The surface layers use standard materials, so they need light: give the scene lights or an environment map. The preview uses a key light, a hemisphere light and
RoomEnvironmentat 0.45.
Time and the loop
fx.update(seconds) sets the effect's phase to (seconds / period) % 1. Every moving part repeats exactly once per period, so any clock works and the effect always loops seamlessly. The period defaults to the loop length you picked in Rollshade; pass period to createEffect to change it. To scrub or pause, set fx.uniforms.phase.value (0 to 1) yourself instead of calling update.
Motion effects
Effects from the Motion mode are built differently from the other modes. Instead of one file that holds everything, you get two kinds of files:
- The shared runtime
rollshade-fx.js. Particles, lights, post processing and the scheduler live here. You need one copy no matter how many effects you use. - One definition per effect,
fx/<id>.js. A few hundred bytes with the recipe, element and tuned values.
Download zip packs the runtime, its types, the chosen effects, an fx/index.js that imports them all and a runnable example.html. Add effects to the Export list to download several at once.
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();
});
- Play as often as you like. Every
fx.play()starts a new instance and returns its own handle. Spamming one effect or firing different ones together all share one set of particle buffers and one post pass. - Aiming.
fromandtoaccept aVector3or anObject3D; anObject3Dis followed while it moves. For melee effects pass the character's chest asfrom. The blade position is written toh.blade.baseandh.blade.tipevery frame so you can move your own sword model with it. - Events.
h.on(name, fn)takes'cast'(charge starts),'release'(fired or swung),'hit'and'end'(the move is over; its last particles may still be fading).barrier,buffandwarpsend no hit. Callingh.stop()orfx.clear()inside a handler is safe, and an error in your handler does not stop the effects. A hit carries the point, the power, suggested shake and hit-stop values and arole('first','link','final'or'tick'); multi-hit moves suggest a full stop only on the first and final hits. Drive collisions and damage from it. Heals send a hit with power 0 at the moment the heal lands. Warp sends'vanish'(where the caster disappears) and'appear'(where they reappear); move your character on those. - Screen effects are opt-in.
feel: trueapplies camera shake, hit-stop and chromatic aberration on hits. Without it nothing touches your camera; use the suggested values yourself (or callfx.shake()andfx.hitStop()).post: truerenders through a pipeline with bloom and distortion. If you already have post processing, passpost: falseand render as usual instead of callingfx.render(). - Call
prewarm()once. It compiles every shader while loading. Without it the first use of each part stalls for a frame. - Performance. Particles never grow past half the screen height (
maxScreenSize, default 0.5) and fade out between 0.4 and 1.4 m from the camera (nearFade). Withquality: 'auto'the runtime lowers particle counts when frames keep running over budget (frameBudget, default 1/58 s) and raises them again once things are light. AddautoResolution: trueto let it change the renderer pixel ratio as well; that affects your whole scene, so leave it off if you manage resolution yourself. - Time. Pass the seconds since the last frame to
fx.update().fx.timeScalegives slow motion, and hit-stop slows every running effect together. - Photosensitivity.
photosensitive: truetones down screen flashes. One-frame inverted impact frames (impactFrames) are off by default. - Cleanup.
fx.clear()stops everything that is playing;fx.dispose()frees the GPU resources.
Sprite sheets for 2D games
Sprite sheet… plays the effect once and lays every frame out in one PNG. The dummies, the sword and the floor are left out; only the effect remains.
- Camera. Current view, side (for side-scrollers), behind the caster, top-down (for top-down games), isometric or a high three-quarter angle. Every preset except the current view uses a long lens, so there is almost no perspective distortion. Zoom adjusts the framing.
- Background. Transparent renders each frame on black and on white and derives alpha from the difference, so both glows and smoke keep their partial transparency. Black suits engines that draw the sprite with additive blending.
- JSON. TexturePacker hash format, read directly by Phaser's
load.atlasand PixiJSAssets.load.animationsholds the frame order,meta.fpsthe frame rate andmeta.hitFramesthe frames where hits land, so you can apply damage on those frames. - Sustained loops. The
mainpart of the power-up aura, the storm and the tornado is exported as a seamless loop of the sustained phase (meta.loopis true). Repeat it for as long as the effect lasts in your game.
Bloom and your render loop
With Bloom ticked under Include, the module creates a RenderPipeline that renders the scene and adds bloom only to the emissive parts, so the rest of your scene does not glow. Call fx.renderPipeline.render() instead of renderer.render(). The pipeline keeps transparency, so it also works over a transparent canvas.
If you already have your own post-processing, untick Bloom. The module then returns no pipeline, and you render as usual with renderer.render(scene, camera) or your own pipeline. The effect still writes its glow to the emissive output, so a bloom pass that reads emissive picks it up.
Adjusting the effect at runtime
fx.uniforms holds the sliders from the Layers panel, named after the layer:
fx.uniforms.flame1Intensity.value = 2;
fx.uniforms.flame1Color.value.set('#ff8800');
fx.uniforms.bloomStrength.value = 0.8;
Changing a uniform does not rebuild any shader, so it is cheap to animate every frame.
Removing the effect
fx.dispose();
dispose puts the original materials back on the model, removes the meshes and particles the effect added and frees its GPU resources.
Models without UVs
Some patterns can follow a mesh's UV layout. With Coordinates set to Auto, the code checks each mesh and uses its UVs when it has them and its positions when it does not, so models without UVs still work. If you know your models, choose UV only or Position only under Include to remove that branch from the code. Most patterns use positions in any case, so they stay attached to the surface and need no UVs.
What to include
- Material: the patterns on the model's own surface.
- Shell & outline: glowing shells around the model and outlines behind it.
- Elements & particles: flames, glows, magic circles, rings and particles.
- Bloom: the render pipeline described above.
The preview follows these switches, so what you see is what the code does.
TypeScript
The TS tab gives the same module with types. It needs @types/three for your three version. Some node values are typed any (type N = any) because TSL's types are strict about node kinds.
Troubleshooting
- Nothing appears. Wait for
await renderer.init()before the first frame, and check that all imports come fromthree/webgpuandthree/tsl(a hand-written import map also needsthree/addons/). three/addons/…cannot be found. That path maps tothree/examples/jsm/…inside the three package. Bundlers such as Vite resolve it; with an import map, mapthree/addons/to that folder.- It looks brighter or darker than the preview. The preview uses ACES Filmic tone mapping, a key light and an environment map. Match those, or adjust the intensity uniforms.
- It is slow on phones. Particle counts and noise octaves are plain numbers in the code; lower them, or reduce the pixel ratio.
- Flashing. Effects can flicker. Keep flashes to three per second or fewer for people who are sensitive to light; a longer loop period lowers the rate.
License
Everything you export from Rollshade (effect definitions and code, sprite sheets, images and videos) is released under CC0 1.0: no conditions, no credit needed. Use it, modify it, share it or sell it, commercial or not. The shared runtime (rollshade-fx.js, also on npm as rollshade) is under the MIT License, so keep its copyright notice. The helper functions based on psrdnoise are MIT too; keep the notice at the top of the file. three.js is MIT. Every ZIP carries LICENSE.txt. Models shown in images and videos are not covered: the preset Rollpoly models follow the Rollpoly Asset License. See License and Terms of Use for the details.