@gameable/vfx
Classes
ParticlePool
Six vec4 columns in a fixed ring; spawn copies a flat row of 24 floats.
Example
ts
import { ParticlePool } from 'gameable/vfx';
const pool = new ParticlePool(64);
const row = new Float32Array(24);
row[7] = 1;
pool.spawn(row);
pool.flush();
console.log(pool.alive(0)); // 1Constructors
Constructor
ts
new ParticlePool(capacity?): ParticlePool;Creates a fixed particle ring.
Parameters
| Parameter | Type | Default value | Description |
|---|---|---|---|
capacity | number | 4096 | Maximum particles in this pool. |
Returns
Properties
attributes
ts
readonly attributes: readonly InstancedBufferAttribute[];capacity
ts
readonly capacity: number;data
ts
readonly data: readonly Float32Array<ArrayBufferLike>[];Methods
alive()
ts
alive(now): number;Counts living rows using the shader clock precision; called only for explicit statistics.
Parameters
| Parameter | Type | Description |
|---|---|---|
now | number | Host time in seconds. |
Returns
number
The number of living particles.
flush()
ts
flush(): void;Marks only changed components; a wrapped interval becomes two ranges.
Returns
void
spawn()
ts
spawn(row): number;Copies a0 through a5 into the next ring slot.
Parameters
| Parameter | Type | Description |
|---|---|---|
row | ArrayLike<number> | Caller-owned 24-float birth record. |
Returns
number
The overwritten ring slot.
Interfaces
EffectCommand
Structural copy of jco's effect-cmd; retained fields are copied on receipt.
Example
ts
import type { EffectCommand } from 'gameable/vfx';
const command: EffectCommand = {
action: 'play', effect: 0, preset: 'hit-sparks',
position: { x: 0, y: 0, z: 0 }, direction: { x: 0, y: 1, z: 0 },
scale: 1, intensity: 1,
};
console.log(command.preset);Properties
action
ts
action: "stop" | "play" | "move";direction
ts
direction: ParticleVector;effect
ts
effect: number;entity?
ts
optional entity?: number;intensity
ts
intensity: number;position
ts
position: ParticleVector;preset
ts
preset: string;scale
ts
scale: number;tint?
ts
optional tint?: ParticleVector;Particles
Host particle service. Reading stats.alive explicitly scans the two pools.
Example
ts
import { Scene } from 'three/webgpu';
import { createParticles, type Particles } from 'gameable/vfx';
const particles: Particles = createParticles(new Scene());
particles.update(0, 0);
console.log(particles.stats.alive); // 0
particles.dispose();Properties
objects
ts
readonly objects: readonly Object3D<Object3DEventMap>[];stats
ts
readonly stats: object;alive
ts
readonly alive: number;births
ts
readonly births: number;Births in the last update.
emitters
ts
readonly emitters: number;total
ts
readonly total: number;Births since the system was made.
Methods
dispose()
ts
dispose(): void;Returns
void
handle()
ts
handle(cmd): void;Parameters
| Parameter | Type |
|---|---|
cmd | EffectCommand |
Returns
void
setCapacity()
ts
setCapacity(capacity): void;Resize both pools at a quality change, preserving running emitters. Existing particles are released; subsequent births use the new capacity.
Parameters
| Parameter | Type | Description |
|---|---|---|
capacity | number | Particles per blend pool. |
Returns
void
update()
ts
update(now, dt): void;Parameters
| Parameter | Type |
|---|---|
now | number |
dt | number |
Returns
void
ParticlesOptions
Fixed capacities and optional entity lookup; RNG state is private to this system.
Example
ts
import { Scene } from 'three/webgpu';
import { createParticles, type ParticlesOptions } from 'gameable/vfx';
const options: ParticlesOptions = { capacity: 64, seed: 42 };
const particles = createParticles(new Scene(), options);
particles.dispose();Properties
capacity?
ts
optional capacity?: number;resolveEntity?
ts
optional resolveEntity?: ResolveEntity;seed?
ts
optional seed?: number;PostCommand
Structural copy of jco's post-cmd: omitted fields retain their current value.
Example
ts
import type { PostCommand } from 'gameable/vfx';
const command: PostCommand = { reset: false, exposure: 2 };
console.log(command.exposure);Properties
bloom?
ts
optional bloom?: number;bloomRadius?
ts
optional bloomRadius?: number;bloomThreshold?
ts
optional bloomThreshold?: number;contrast?
ts
optional contrast?: number;exposure?
ts
optional exposure?: number;fogColor?
ts
optional fogColor?: object;x
ts
x: number;y
ts
y: number;z
ts
z: number;fogDensity?
ts
optional fogDensity?: number;reset
ts
reset: boolean;saturation?
ts
optional saturation?: number;tint?
ts
optional tint?: object;x
ts
x: number;y
ts
y: number;z
ts
z: number;toneMapping?
ts
optional toneMapping?: "none" | "aces" | "agx" | "neutral";vignette?
ts
optional vignette?: number;PostSettings
Resolved post settings. Colours are linear RGB.
Example
ts
import { NEUTRAL_POST, type PostSettings } from 'gameable/vfx';
const look: PostSettings = { ...NEUTRAL_POST, exposure: 2 };
console.log(look.exposure);Properties
bloom
ts
readonly bloom: number;bloomRadius
ts
readonly bloomRadius: number;bloomThreshold
ts
readonly bloomThreshold: number;contrast
ts
readonly contrast: number;exposure
ts
readonly exposure: number;fogColor
ts
readonly fogColor: readonly [number, number, number];fogDensity
ts
readonly fogDensity: number;saturation
ts
readonly saturation: number;tint
ts
readonly tint: readonly [number, number, number];toneMapping
ts
readonly toneMapping: NonNullable<"none" | "aces" | "agx" | "neutral" | undefined>;vignette
ts
readonly vignette: number;PostStack
A caller-owned post pass; detach it from the engine before disposal.
Example
ts
import { createEngine } from 'gameable/core';
import { createPostStack, type PostStack } from 'gameable/vfx';
const engine = await createEngine({ canvas: document.createElement('canvas') });
const post: PostStack = createPostStack(engine.renderer, engine.scene);
engine.setPost(post);
post.set({ reset: false, exposure: 1.2 });Extends
Properties
active
ts
readonly active: boolean;Whether a frame target and presentation pass are needed.
settings
ts
readonly settings: Readonly<PostSettings>;The immutable resolved look.
Methods
dispose()
ts
dispose(): void;Release owned targets, materials and fog.
Returns
void
present()
ts
present(renderer): void;Present the target returned for this frame to the screen. Called only when target was non-null.
Parameters
| Parameter | Type |
|---|---|
renderer | WebGPURenderer |
Returns
void
Inherited from
set()
ts
set(cmd): void;Apply a partial look, resetting to neutral first when requested.
Parameters
| Parameter | Type |
|---|---|
cmd | PostCommand |
Returns
void
target()
ts
target(renderer):
| RenderTarget<Texture<unknown, TextureEventMap>, RenderTargetEventMap>
| null;Return a linear half-float target sized to the drawing buffer, or null to bypass processing.
Parameters
| Parameter | Type |
|---|---|
renderer | WebGPURenderer |
Returns
| RenderTarget<Texture<unknown, TextureEventMap>, RenderTargetEventMap> | null
Inherited from
Vfx
The engine's effects: what the adapter's vfx option takes, plus the per-frame step.
Properties
particles
ts
readonly particles: Particles;The particle system.
post
ts
readonly post: PostStack;The post stack, installed on the engine with setPost.
Methods
dispose()
ts
dispose(): void;Take the particles off the scene and the post stack off the engine.
Returns
void
effect()
ts
effect(command): void;Play, move or stop an effect. The command is the guest's pooled payload: it is copied, never kept.
Parameters
| Parameter | Type |
|---|---|
command | EffectCommand |
Returns
void
setPost()
ts
setPost(command): void;Change the post settings (partial; reset starts from neutral).
Parameters
| Parameter | Type |
|---|---|
command | PostCommand |
Returns
void
update()
ts
update(dt): void;Advance the particles one rendered frame.
Parameters
| Parameter | Type | Description |
|---|---|---|
dt | number | Seconds since the previous frame. |
Returns
void
Nothing.
VfxOptions
Options for createVfx.
Properties
capacity?
ts
readonly optional capacity?: number;Particles per pool (additive and alpha each). Default 4096.
post?
ts
readonly optional post?: Omit<PostCommand, "reset">;The base look, applied before the first frame (the game's declared features.vfx.post).
seed?
ts
readonly optional seed?: number;The host particle RNG's seed. Presentation only: it never reaches the guest.
Type Aliases
VfxPresetName
ts
type VfxPresetName = keyof typeof PRESETS;Names accepted by the built-in particle emitter.
Example
ts
import { PRESETS, type VfxPresetName } from 'gameable/vfx';
const name: VfxPresetName = 'fire';
console.log(PRESETS[name].pool); // additiveVariables
NEUTRAL_POST
ts
const NEUTRAL_POST: PostSettings;Neutral look: no target, no full-screen pass, no fog.
Example
ts
import { NEUTRAL_POST } from 'gameable/vfx';
console.log(NEUTRAL_POST.exposure); // 1PRESETS
ts
const PRESETS: object;Built-in particle presets in linear light, metres and seconds.
Type Declaration
dust
ts
readonly dust: object;dust.alpha
ts
readonly alpha: readonly [0.4, 0];dust.burst
ts
readonly burst: 22 = 22;dust.colourEnd
ts
readonly colourEnd: readonly [0.23, 0.18, 0.12];dust.colourStart
ts
readonly colourStart: readonly [0.3, 0.21, 0.12];dust.drag
ts
readonly drag: 2 = 2;dust.duration
ts
readonly duration: 0 = 0;dust.gravity
ts
readonly gravity: 0.8 = 0.8;dust.inherit
ts
readonly inherit: 0 = 0;dust.life
ts
readonly life: readonly [0.4, 1];dust.pool
ts
readonly pool: "alpha" = 'alpha';dust.rate
ts
readonly rate: 0 = 0;dust.size
ts
readonly size: readonly [0.1, 0.45];dust.speed
ts
readonly speed: readonly [0.2, 1.4];dust.spin
ts
readonly spin: readonly [-2, 2];dust.spread
ts
readonly spread: 1.3 = 1.3;dust.up
ts
readonly up: readonly [0, 1, 0];explosion
ts
readonly explosion: object;explosion.alpha
ts
readonly alpha: readonly [1, 0];explosion.burst
ts
readonly burst: 100 = 100;explosion.colourEnd
ts
readonly colourEnd: readonly [1.5, 0.08, 0.01];explosion.colourStart
ts
readonly colourStart: readonly [9, 5, 1.5];explosion.drag
ts
readonly drag: 3 = 3;explosion.duration
ts
readonly duration: 0.3 = 0.3;explosion.gravity
ts
readonly gravity: 3 = 3;explosion.inherit
ts
readonly inherit: 0 = 0;explosion.life
ts
readonly life: readonly [0.3, 1.2];explosion.pool
ts
readonly pool: "additive" = 'additive';explosion.rate
ts
readonly rate: 24 = 24;explosion.size
ts
readonly size: readonly [0.55, 0.06];explosion.speed
ts
readonly speed: readonly [0.5, 8];explosion.spin
ts
readonly spin: readonly [-4, 4];explosion.spread
ts
readonly spread: number = Math.PI;explosion.up
ts
readonly up: readonly [0, 1, 0];fire
ts
readonly fire: object;fire.alpha
ts
readonly alpha: readonly [0.8, 0];fire.burst
ts
readonly burst: 6 = 6;fire.colourEnd
ts
readonly colourEnd: readonly [1.5, 0.025, 0.005];fire.colourStart
ts
readonly colourStart: readonly [5, 1.5, 0.1];fire.drag
ts
readonly drag: 0.8 = 0.8;fire.duration
ts
readonly duration: number = Infinity;fire.gravity
ts
readonly gravity: -1 = -1;fire.inherit
ts
readonly inherit: 0 = 0;fire.life
ts
readonly life: readonly [0.25, 0.65];fire.pool
ts
readonly pool: "additive" = 'additive';fire.rate
ts
readonly rate: 55 = 55;fire.size
ts
readonly size: readonly [0.28, 0.025];fire.speed
ts
readonly speed: readonly [0.8, 2];fire.spin
ts
readonly spin: readonly [-3, 3];fire.spread
ts
readonly spread: 0.35 = 0.35;fire.up
ts
readonly up: readonly [0, 1, 0];hit-sparks
ts
readonly hit-sparks: object;hit-sparks.alpha
ts
readonly alpha: readonly [1, 0];hit-sparks.burst
ts
readonly burst: 26 = 26;hit-sparks.colourEnd
ts
readonly colourEnd: readonly [2, 0.2, 0.01];hit-sparks.colourStart
ts
readonly colourStart: readonly [7, 4, 1.5];hit-sparks.drag
ts
readonly drag: 2 = 2;hit-sparks.duration
ts
readonly duration: 0 = 0;hit-sparks.gravity
ts
readonly gravity: 9.8 = 9.8;hit-sparks.inherit
ts
readonly inherit: 0 = 0;hit-sparks.life
ts
readonly life: readonly [0.15, 0.4];hit-sparks.pool
ts
readonly pool: "additive" = 'additive';hit-sparks.rate
ts
readonly rate: 0 = 0;hit-sparks.size
ts
readonly size: readonly [0.055, 0.005];hit-sparks.speed
ts
readonly speed: readonly [3, 9];hit-sparks.spin
ts
readonly spin: readonly [-9, 9];hit-sparks.spread
ts
readonly spread: 1.1 = 1.1;hit-sparks.up
ts
readonly up: readonly [0, 1, 0];magic
ts
readonly magic: object;magic.alpha
ts
readonly alpha: readonly [1, 0];magic.burst
ts
readonly burst: 12 = 12;magic.colourEnd
ts
readonly colourEnd: readonly [0.1, 2, 3];magic.colourStart
ts
readonly colourStart: readonly [2.5, 0.3, 5];magic.drag
ts
readonly drag: 0.6 = 0.6;magic.duration
ts
readonly duration: number = Infinity;magic.gravity
ts
readonly gravity: -0.15 = -0.15;magic.inherit
ts
readonly inherit: 0 = 0;magic.life
ts
readonly life: readonly [0.6, 1.5];magic.pool
ts
readonly pool: "additive" = 'additive';magic.rate
ts
readonly rate: 28 = 28;magic.size
ts
readonly size: readonly [0.075, 0.015];magic.speed
ts
readonly speed: readonly [0.15, 0.65];magic.spin
ts
readonly spin: readonly [-6, 6];magic.spread
ts
readonly spread: 1.5 = 1.5;magic.up
ts
readonly up: readonly [0, 1, 0];smoke
ts
readonly smoke: object;smoke.alpha
ts
readonly alpha: readonly [0.35, 0];smoke.burst
ts
readonly burst: 8 = 8;smoke.colourEnd
ts
readonly colourEnd: readonly [0.18, 0.19, 0.2];smoke.colourStart
ts
readonly colourStart: readonly [0.3, 0.32, 0.34];smoke.drag
ts
readonly drag: 0.5 = 0.5;smoke.duration
ts
readonly duration: number = Infinity;smoke.gravity
ts
readonly gravity: -0.3 = -0.3;smoke.inherit
ts
readonly inherit: 0 = 0;smoke.life
ts
readonly life: readonly [1.5, 3];smoke.pool
ts
readonly pool: "alpha" = 'alpha';smoke.rate
ts
readonly rate: 18 = 18;smoke.size
ts
readonly size: readonly [0.18, 1.2];smoke.speed
ts
readonly speed: readonly [0.2, 0.6];smoke.spin
ts
readonly spin: readonly [-0.5, 0.5];smoke.spread
ts
readonly spread: 0.6 = 0.6;smoke.up
ts
readonly up: readonly [0, 1, 0];trail
ts
readonly trail: object;trail.alpha
ts
readonly alpha: readonly [1, 0];trail.burst
ts
readonly burst: 0 = 0;trail.colourEnd
ts
readonly colourEnd: readonly [0.1, 0.3, 1];trail.colourStart
ts
readonly colourStart: readonly [0.5, 2.5, 4];trail.drag
ts
readonly drag: 0 = 0;trail.duration
ts
readonly duration: number = Infinity;trail.gravity
ts
readonly gravity: 0 = 0;trail.inherit
ts
readonly inherit: 0.05 = 0.05;trail.life
ts
readonly life: readonly [0.12, 0.3];trail.pool
ts
readonly pool: "additive" = 'additive';trail.rate
ts
readonly rate: 120 = 120;trail.size
ts
readonly size: readonly [0.09, 0];trail.speed
ts
readonly speed: readonly [0, 0];trail.spin
ts
readonly spin: readonly [0, 0];trail.spread
ts
readonly spread: 0 = 0;trail.up
ts
readonly up: readonly [0, 1, 0];Example
ts
import { PRESETS } from 'gameable/vfx';
console.log(PRESETS.trail.rate); // 120Functions
createParticles()
ts
function createParticles(scene, options?): Particles;Creates two scene-owned instanced billboard meshes without requiring a renderer.
Parameters
| Parameter | Type | Description |
|---|---|---|
scene | Scene | Scene that owns the particle meshes. |
options | ParticlesOptions | Pool capacity, seed and entity resolver. |
Returns
A particle service with idempotent disposal.
Example
ts
import { Scene } from 'three/webgpu';
import { createParticles } from 'gameable/vfx';
const particles = createParticles(new Scene(), { seed: 42 });
particles.update(0, 0);
particles.dispose();createPostStack()
ts
function createPostStack(renderer, scene): PostStack;Grade the finished linear frame and apply the canvas sRGB output transform once. Uses RenderPipeline to supply BloomNode's render context and suppress the renderer's automatic tone mapping/output conversion. Bloom has its own internal blur passes; the final bloom addition, grade and output transform share one full-screen pass. Contrast pivots around 0.18 in linear light after tone mapping, matching gradePixel. No target or pipeline is created until a non-neutral look is requested.
Parameters
| Parameter | Type | Description |
|---|---|---|
renderer | WebGPURenderer | Engine renderer used to present the frame. |
scene | Scene | Scene receiving the optional exponential fog. |
Returns
A caller-owned post stack; detach it before disposal.
Example
ts
import { createEngine } from 'gameable/core';
import { createPostStack } from 'gameable/vfx';
const engine = await createEngine({ canvas: document.createElement('canvas') });
const post = createPostStack(engine.renderer, engine.scene);
engine.setPost(post);
post.set({ reset: false, bloom: 0.5, toneMapping: 'aces' });
// Later: engine.setPost(null); post.dispose();createVfx()
ts
function createVfx(engine, options?): Vfx;Bind particles and the post stack to a booted engine.
Parameters
| Parameter | Type | Description |
|---|---|---|
engine | Engine | A booted engine with a renderer. |
options | VfxOptions | Pool capacity, RNG seed and the base look. |
Returns
The sink to hand the engine adapter as vfx, with update to call once per rendered frame.
Example
ts
import { createVfx } from 'gameable/vfx';
const vfx = createVfx(engine, { post: { bloom: 0.6 } });
const adapter = createEngineAdapter(engine, { vfx });gradePixel()
ts
function gradePixel(
rgbLinear,
uv,
s,
out
): void;CPU reference of the linear grade (bloom and canvas encoding are separate). Order: exposure, r186 tone mapping, contrast around linear 0.18, Rec.709 saturation, tint, then radial vignette (squared centre distance, normalized to one at corners). Writes into the caller's output, which may alias rgbLinear. Neutral is an exact identity.
Parameters
| Parameter | Type | Description |
|---|---|---|
rgbLinear | ArrayLike<number> | Input linear RGB channels. |
uv | ArrayLike<number> | Normalized screen coordinates. |
s | PostSettings | Resolved grade settings. |
out | { [n: number]: number; } | Caller-owned output RGB storage, optionally aliasing the input. |
Returns
void
Example
ts
import { gradePixel, NEUTRAL_POST } from 'gameable/vfx';
const out = new Float32Array(3);
gradePixel([0.2, 0.3, 0.4], [0.5, 0.5], NEUTRAL_POST, out);
console.log(out[0]); // approximately 0.2isPreset()
ts
function isPreset(name): name is "hit-sparks" | "explosion" | "smoke" | "fire" | "magic" | "dust" | "trail";Narrows a string to a built-in preset name.
Parameters
| Parameter | Type | Description |
|---|---|---|
name | string | Preset name to check. |
Returns
name is "hit-sparks" | "explosion" | "smoke" | "fire" | "magic" | "dust" | "trail"
Whether the name belongs to the built-in table.
Example
ts
import { isPreset, PRESETS } from 'gameable/vfx';
const name: string = 'smoke';
if (isPreset(name)) console.log(PRESETS[name].rate); // 18needsPass()
ts
function needsPass(s): boolean;Whether a full-screen pass is needed. Fog is handled by the scene alone. Bloom threshold and radius only affect a pass when bloom strength is nonzero.
Parameters
| Parameter | Type | Description |
|---|---|---|
s | PostSettings | Resolved settings to inspect. |
Returns
boolean
Whether bloom or grading needs a full-screen pass.
Example
ts
import { needsPass, NEUTRAL_POST } from 'gameable/vfx';
console.log(needsPass(NEUTRAL_POST)); // falseparticleAt()
ts
function particleAt(
row,
age,
out
): void;Evaluates a flat a0..a5 row at age in seconds, matching the vertex shader.
Parameters
| Parameter | Type | Description |
|---|---|---|
row | ArrayLike<number> | Flat 24-float particle birth record. |
age | number | Elapsed seconds since birth. |
out | ParticleSample | Caller-owned sample to populate. |
Returns
void
Example
ts
import { particleAt } from 'gameable/vfx';
const row = new Float32Array(24);
row[7] = 1; row[16] = 0.1;
const sample = { position: { x: 0, y: 0, z: 0 }, colour: { x: 0, y: 0, z: 0 },
size: 0, alpha: 0, rotation: 0 };
particleAt(row, 0.5, sample);
console.log(sample.size); // approximately 0.05resolvePost()
ts
function resolvePost(prev, cmd): PostSettings;Reset first, then apply present fields. Exposure is floored to 1e-6.
Parameters
| Parameter | Type | Description |
|---|---|---|
prev | PostSettings | Previously resolved settings. |
cmd | PostCommand | Partial settings to apply after an optional reset. |
Returns
Immutable settings with bounded fields clamped.
Example
ts
import { resolvePost, NEUTRAL_POST } from 'gameable/vfx';
const look = resolvePost(NEUTRAL_POST, { reset: false, exposure: 2 });
console.log(look.exposure); // 2