TSL · WebGPU · Three.js

VFX authored as code,
scheduled like Niagara.

Composable particle modules, timelines, fluid grids, neighbor simulation, and debugging — without an editor runtime.

Getting started

From install to first burst

01

Install

pnpm add @nachi-vfx/core @nachi-vfx/three three@0.185.1
pnpm add -D @types/three@0.185.0 # TypeScript
02

Import and define

import {
  VFXSystem, billboard, burst, defineEffect,
  defineEmitter, drag, gravity, lifetime,
  positionSphere,
} from '@nachi-vfx/core'
import {
  createThreeKernelAdapter,
  createThreeRuntimeRenderer,
  materializeThreeSpriteDraw,
} from '@nachi-vfx/three'
import * as THREE from 'three/webgpu'

const sparks = defineEmitter({
  capacity: 512,
  spawn: burst({ count: 120 }),
  init: [positionSphere({ radius: .2 }), lifetime(.8)],
  update: [gravity(-9.8), drag(.35)],
  render: billboard({ blending: 'additive' }),
})

const effect = defineEffect({ elements: { sparks } })
03

Materialize and play

const renderer = new THREE.WebGPURenderer()
renderer.setSize(innerWidth, innerHeight)
document.body.append(renderer.domElement)
await renderer.init()

const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(
  50, innerWidth / innerHeight, .1, 100,
)
camera.position.set(0, 1, 5)

const adapter = createThreeKernelAdapter({ backend: 'webgpu' })
const runtimeRenderer = createThreeRuntimeRenderer(renderer, adapter)
const system = new VFXSystem(runtimeRenderer, scene)
const instance = system.spawn(effect, { position: [0, 1, 0], seed: 42 })
const emitter = instance.getEmitter('sparks')
if (!emitter) throw new Error('Missing sparks emitter')
const draw = materializeThreeSpriteDraw(emitter.program, emitter.kernels)
scene.add(draw)

let previous = performance.now()
async function frame(now) {
  await system.update(Math.min((now - previous) / 1000, .1))
  previous = now
  renderer.render(scene, camera)
  requestAnimationFrame(frame)
}
requestAnimationFrame(frame)

The repository Pages deployment serves the playground and showcase below this documentation root. A standalone docs deployment must configure or omit those demo links; the npm Quick Start above has no playground dependency.

Core guides

The complete authoring path

Emitters

Choose capacity and deterministic burst, rate, or distance spawning. Lifecycle, prewarm, looping, bounds, and quality stay declarative.

Modules

Compose Init and Update behavior in author order. Access manifests make dynamic attributes and custom TSL scratch modules inspectable.

Timeline

Sequence play, stop, markers, camera shake, and hit stop on an effect-local clock with deterministic loop boundaries.

Mesh effects

Compose scrolling maps, independent dissolve UVs, runtime opacity, and edge emission with fxMaterial. Put hold thresholds below the noise texture's minimum value, and tune noise frequency to the mesh scale.

Scalability

Use four quality tiers, distance/frustum culling, significance scores, particle budgets, and bounded resource pools.

Simulation cache

Bake renderer-read attributes to Float32 or quantized binary snapshots and replay without scheduling simulation kernels.

Debugger

Capture logical particle rows and frame-local profiles through the serialized system queue; unavailable GPU timing remains explicit.

Grid fluids

Run packed Grid2D/3D stages for injection, advection, buoyancy, Jacobi pressure, projection, and particle transfer.

Neighbors

Build a bounded GPU neighbor grid for flocking, custom neighbor TSL, and iterative Jacobi PBD distance constraints.

API reference

Contracts before convenience.

The normative RFC documents stage semantics, runtime ownership, diagnostics, serialization, and package boundaries. Package READMEs provide focused entry points.