Language: English (this page) / 日本語
Warning
HEAVY EXPERIMENTAL — v0.2.2 is not production-ready. APIs, behavior, performance, compatibility, and package boundaries may change significantly between releases.
Code-first, TSL/WebGPU-native VFX for Three.js, designed around Niagara's staged simulation model. Nachi v0.2.2 is an experimental preview; M12 includes JSON assets, advanced simulation, React Three Fiber bindings, release automation, and a buildable documentation gallery.
| Package | Purpose |
|---|---|
@nachi-vfx/core |
Definitions, particle modules, compiler, GPU scheduler, scalability, sim cache, debugger, Grid2D/3D, and neighbor grids |
@nachi-vfx/three |
Three.js WebGPU kernel/runtime adapter, resource resolvers, and particle draw materializers |
@nachi-vfx/format |
Strict nachi-effect v1 JSON schema, serializer/loader, migrations, and asset inheritance |
@nachi-vfx/react |
Thin R3F provider, hook, component lifecycle, and Object3D attachment |
@nachi-vfx/timeline |
Effect-local sequencing, camera shake, hit stop, markers, and mesh-fx lifecycle |
@nachi-vfx/trails |
GPU ribbons and trails |
@nachi-vfx/mesh-fx |
Procedural effect geometry, fxMaterial, and Blender VAT playback |
@nachi-vfx/post |
RenderPipeline distortion, radial blur, bloom presets, and WebGPU WBOIT |
@nachi-vfx/tsl-kit |
Standalone Three.js TSL shader building blocks |
The repository also contains the Vite playground and static documentation site.
Bug reports, feature requests, and detailed use-case feedback are welcome through GitHub Issues. External pull requests are not accepted; accepted changes are implemented and reviewed by the maintainers, with opt-in co-author credit for substantive issue contributions. See CONTRIBUTING.md. Report vulnerabilities privately according to SECURITY.md.
Core Three.js usage:
pnpm add @nachi-vfx/core @nachi-vfx/three three@0.185.1React Three Fiber usage keeps React, R3F, and Three as peers:
pnpm add @nachi-vfx/core @nachi-vfx/three @nachi-vfx/react react@^19 @react-three/fiber@^9 three@0.185.1Packages that expose Three.js types (@nachi-vfx/three, @nachi-vfx/tsl-kit, @nachi-vfx/mesh-fx, @nachi-vfx/trails,
@nachi-vfx/timeline, @nachi-vfx/post, and @nachi-vfx/react) also expect the separately published matching
declarations in TypeScript projects:
pnpm add -D @types/three@0.185.0three@0.185.1 is the supported and tested runtime. Do not deduplicate these integrations onto a
different Three minor version.
VFXSystemProvider synchronizes the active R3F camera and pixel viewport with core before each
update by default. Pass syncCamera={false} only when the application calls system.setCamera()
itself.
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 renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.append(renderer.domElement);
await renderer.init();
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 1, 5);
const kernelAdapter = createThreeKernelAdapter({ backend: 'webgpu' });
const runtimeRenderer = createThreeRuntimeRenderer(renderer, kernelAdapter);
const sparks = defineEmitter({
capacity: 512,
spawn: burst({ count: 120 }),
init: [positionSphere({ radius: 0.2 }), lifetime(0.8)],
update: [gravity(-9.8), drag(0.35)],
render: billboard({ blending: 'additive' }),
});
const effect = defineEffect({ elements: { sparks } });
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('The sparks emitter was not created.');
const draw = materializeThreeSpriteDraw(emitter.program, emitter.kernels);
scene.add(draw);
let previousTime = performance.now();
async function frame(time: number) {
const deltaSeconds = Math.min((time - previousTime) / 1000, 0.1);
previousTime = time;
await system.update(deltaSeconds);
renderer.render(scene, camera);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
// When the effect is no longer needed:
// scene.remove(draw); instance.release(); draw.geometry.dispose(); draw.material.dispose();In R3F, create the same adapter once and let the binding own instance cleanup. The complete
materialization example is in @nachi-vfx/react.
useEffectInstance() is the hook form. Live parameters, transform, time scale, and attachment are
forwarded to core; changing seed or priority creates a fresh instance. Keep definition at module
scope (or otherwise referentially stable), because changing its reference respawns the instance.
attachTo owns the complete v1 live position/rotation and overwrites spawn/prop values on each
scheduled step. EffectWorldTransform has no scale; Three attachment world scale is not forwarded.
import { loadEffect, serializeEffect } from '@nachi-vfx/format';
const document = serializeEffect(effect);
const loaded = loadEffect(JSON.stringify(document));Only the declarative subset is serializable. Inline callbacks, functions, live Three.js resources,
class instances, and cycles fail with path-specific NACHI_ASSET_* diagnostics. Grid2D/3D stages,
neighbor-grid declarations, built-in fluid stages, boids, and PBD constraints are part of the v1
declarative model; inline custom grid/neighbor TSL remains code-only.
Simulation caches use bakeSimulation() and replaySimulation(). Runtime debugging uses
instance.debug.captureAttributes() and system.debug.captureProfile(). Attribute capture keeps
backend compaction order by default; use { order: 'physical-slot', offset, limit } for stable
physical-identity pagination when comparing snapshots with the same slot membership. It removes
compaction-order differences, not backend allocation differences; aliveIndex remains the original
compact index. order defaults only when omitted or undefined; invalid values and out-of-capacity
membership are structured diagnostics rather than rows containing fabricated zero attributes.
Three draw objects are lifetime-bound to their emitter kernels. Reuse an existing materialized mesh or dispose/unmaterialize it before replacement; releasing an instance cleans registered draws before its kernels enter the pool, and a respawn must attach the newly materialized draw.
A practical authoring guide lives in skills/nachi-effect-authoring/:
the library usage catalog (library-usage.md)
and scale-based skill-effect recipes (effect-recipes.md).
It is packaged in the Agent Skills format, so coding agents (Claude Code,
Codex, Cursor, GitHub Copilot, …) can install it into a consuming project with the
skills CLI — no extra tooling beyond Node.js:
npx skills add reifier-k/nachi --skill nachi-effect-authoringUse -a <agent> to target specific agents and -g for a user-wide install. The skills/*/SKILL.md
layout is also compatible with gh skill install (GitHub CLI v2.90.0+).
pnpm typecheck
pnpm lint
pnpm test
pnpm format:check
pnpm build
pnpm docs:build
pnpm esm-all
pnpm release:dry # build + every package ESM import gate + publish-shaped pnpm pack checks
pnpm golden:regress # with the playground dev server running; headless SwiftShader golden suite
node tools/bundle-size.mjs
node tools/license-report.mjsBiome is the workspace linter and formatter for JavaScript, TypeScript, JSON, CSS, and HTML. Markdown and YAML are not automatically formatted.
Changesets are independently versioned: run pnpm changeset, then pnpm version-packages when a
release versioning pass is intended. release:dry never publishes.
The maintainer release procedure, including the one-time npm bootstrap and token-free trusted
publishing migration, is documented in docs/releasing.md.
Design and status: PLAN.md, ROADMAP.md, normative API RFC, and the Effekseer compatibility study. Release compatibility is defined by RFC 003; FA evidence is summarized by the parity, bundle, and license reports.