npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nachi-vfx/core

v0.2.3

Published

GPU-first deterministic particle simulation and VFX runtime for Three.js

Readme

@nachi-vfx/core

Renderer-independent authoring, compilation, scheduling, deterministic lifecycle, scalability, simulation caches, debugging, and data interfaces for nachi VFX.

Install the Three.js runtime adapter with core to create a renderable system:

pnpm add @nachi-vfx/core @nachi-vfx/three [email protected]
# TypeScript projects also need Three's separately published declarations:
pnpm add -D @types/[email protected]

@nachi-vfx/core deliberately does not import Three.js. Use createThreeKernelAdapter() and createThreeRuntimeRenderer() from @nachi-vfx/three, then materialize each spawned emitter's draw and add it to your Three scene. See the repository Quick start and the @nachi-vfx/three README for a complete rendering example.

The public authoring surface includes emitter/effect definitions, built-in Init/Update/Render modules, typed User.* parameters, VFXSystem, JSON-compatible registries, fixed-step execution, quality and significance controls, simulation bake/replay, debug snapshots, Grid2D/Grid3D, neighbor grids, boids, and PBD constraints. Backend capability failures are explicit diagnostics; core does not silently replace WebGPU behavior with CPU simulation.

Debug attribute snapshots preserve backend compaction order by default. Pass { order: 'physical-slot', offset, limit } to sort the full alive membership before pagination when you need pages independent of compaction order for the same physical membership; each row's aliveIndex still identifies its original compact membership index. This is physical identity, not persistent lineage, and does not promise identical slot allocation between backends. Only omission or undefined selects the default order; null and other non-enum values are invalid. Capture rejects a returned compaction row whose physical slot is outside capacity with NACHI_DEBUG_PHYSICAL_SLOT_OUT_OF_RANGE; physical-slot order validates the full membership before sorting and pagination.

Simulation-cache format

Simulation caches use format version 2. This is a breaking cache-file boundary: every cached emitter carries lossless u32 spawnOrder lineage for alive births (and the next spawn-order state when that attribute is materialized), so interpolation and loop continuity cannot confuse a reused physical slot with the particle that previously occupied it. Version-1 caches are not inferred or migrated; re-bake them with the current runtime. Loading an old cache fails explicitly with NACHI_SIM_CACHE_VERSION_UNSUPPORTED.

Input validation

Built-in module factories reject malformed ordinary ValueInput constants, ranges, parameter fallbacks, and curve values synchronously; direct/JSON module data runs the same checks at compile time. Required ValueInput fields cannot be missing or undefined, while optional field omission retains its documented default. Parameter generators require a string path even when their fallback is omitted. Every scalar or vec3 field, including nested positionSphere.arc.thetaMax, validates finite shape and the declared type of both User.* and materialized built-in parameters from the shared uniform definition. turbulence.octaves is an integer from 1 through 4. Collision modes are exactly bounce, kill, or stick; plane, sphere, box, and SDF collisions require one, while scene-depth collision may omit it. A normalized-age reader without age+lifetime writers or an explicit normalized-age writer receives NACHI_NORMALIZED_AGE_WITHOUT_LIFETIME.

Spawn position/rotation, instance.setTransform(), and attachment transforms are checked before uniform writes. Spawn may omit position, but live transforms and attachment samples require one. Positions are finite vec3 values; rotations are finite Euler vec3 or quaternion vec4 values. Invalid untyped JavaScript input never consumes an instance ID, replaces an attachment, or partially changes the live transform. Each operation reads the position/rotation properties, tuple length and components, or object x/y/z components once into an owned frozen snapshot. Validation, matrix construction, and uniform writes use only that snapshot, so mutable accessors cannot pass validation and then supply a different value during commit.

System spawn reads timeScale and priority once, validates those primitive snapshots before ID allocation, and passes the same values to the instance clock and significance calculation. An accessor cannot return a valid value for validation and a different value for construction; an invalid first value does not consume an ID. Direct VfxEffectInstance construction still validates its clock value through EffectClock.

Attachment sampling is guarded by an operation revision at both direct attachTo() and scheduled update boundaries. If a transform getter reentrantly attaches again, detaches, releases, or attempts an invalid attachment that it catches, that nested operation invalidates the outer sample. This also applies when the nested attachment uses the same source object, so stale poses cannot overwrite the newer operation. Runtime checks the revision both after the source getter and again after snapshotting the returned transform, preserving quiet release/replacement while also catching reentry from transform property or component accessors.

Runtime diagnostic delivery

VFXSystem reports runtime failures and warnings when they occur. Omit onRuntimeDiagnostic for one-line console.error/console.warn output, pass a function to replace that reporter, or pass null to silence delivery. The diagnostic is still retained on its owning effect instance when delivery is replaced or disabled.

const system = new VFXSystem(runtimeRenderer, scene, {
  onRuntimeDiagnostic: (diagnostic) => telemetry.record(diagnostic),
});

// Explicitly silent delivery; instance.state and instance.diagnostics still update.
const quietSystem = new VFXSystem(runtimeRenderer, scene, { onRuntimeDiagnostic: null });

The runtime path covers post-spawn GPU submission and attachment failures, device loss, preparation failures, runtime camera/quality/capacity warnings, NeighborGrid warnings, and exact spawn/event overflow when alive-count readback is enabled. Spawn-time compile, kernel-build, and resource materialization failures remain on onBuildDiagnostic; this includes NACHI_RUNTIME_MATERIALIZATION_FAILED despite its historical name. Runtime delivery does not add a readback: with aliveCountReadbackInterval omitted, exact free-list and event overflow remain unreported. Device loss is delivered once per system occurrence while every affected instance stores it. A throwing replacement handler is contained, records NACHI_RUNTIME_DIAGNOSTIC_HANDLER_FAILED on an owning instance, and is retried for later diagnostics. An ownerless system source has no instance storage target, so its failure fallback is console-only and limited to once per system. Build diagnostics remain exclusively owned by onBuildDiagnostic.

Measured update deltas

system.update() without an argument measures elapsed wall time. The first omitted call advances zero; later calls are capped at 0.25 seconds by default so a suspended tab or RAF does not send one unbounded variable step into lifetime, spawning, or integration. Configure a positive maxMeasuredDeltaSeconds, or pass Infinity to restore uncapped measurement. An explicit system.update(deltaSeconds) is never capped and does not reset the measured clock.

The cumulative measuredDeltaDroppedSeconds, fixedStepDroppedSeconds, and droppedSeconds getters expose the measured discard, the existing fixed-step backlog discard, and their sum. The measured cap runs before fixed partitioning, so the components do not double-count. See RFC 001 for queue ordering, mixed explicit/measured calls, and clock validation. fixedTimeStep.stepSeconds must be strictly greater than 1e-10 seconds; 0, 1e-12, and 1e-10 throw RangeError: stepSeconds must be greater than 1e-10 seconds. The product stepSeconds * maxSubSteps must also remain finite or construction throws RangeError: stepSeconds * maxSubSteps must be a finite number. Fixed accumulation computes retained time from the finite remaining capacity, so adding a huge finite delta cannot turn the frame-local drop or accumulator into NaN; only the cumulative drop may eventually overflow to Infinity.

Coordinate-space selectors

Particle position and velocity are stored in world space. velocityCone() and linearForce() accept space: 'world' | 'emitter' and default to world, preserving their v1 behavior. Select emitter for a cone or thruster direction that rotates with the effect instance. gravity() is always world-space.

velocityCone({ angle: 12, direction: [0, 1, 0], space: 'emitter', speed: 4 });
linearForce({ force: [0, 6, 0], space: 'emitter' });
gravity([0, -9.8, 0]);

vortex(), pointAttractor(), and the analytic collidePlane()/collideSphere()/collideBox() selectors default to emitter; their explicit world option remains available. killVolume() is fixed emitter-local. Moving emitter-space Update consumers sample one transform at exact phase 0.5 between the preceding and current simulation endpoints (translation lerp plus shortest-path quaternion slerp). This midpoint is a one-sample temporal approximation, not CCD, so use fixed substeps when a thin volume must not be skipped.

NeighborGrid is the intentional exception: its emitter-local origin and all bucket/visitor cell lookups use the current endpoint, while particle snapshots, velocities, and distances remain world-space. Grid2D/Grid3D injection coordinates are normalized grid coordinates and their velocity channels are measured in cells per second. The public emitter transform has no scale. See RFC 004 for the exhaustive built-in table, unit rules, and API-addition checklist.

The eight H2-6 helpers (velocityCone, linearForce, vortex, pointAttractor, the three analytic colliders, and killVolume) emit module version 2. Module v1 remains executable with its old world/current-endpoint meaning, and format round trips never upgrade it implicitly. An older core without the v2 registrations rejects type@2 with NACHI_MODULE_UNKNOWN instead of silently misreading a selector.

Transparent ordering

The current billboard(), meshRenderer(), and decalRenderer() helpers emit renderer module v2. Alpha and premultiplied draws default to WebGPU particle sorting; use sorted: false for the faster, explicitly unordered compact-alive path. Additive and multiply billboard/mesh draws default to unsorted. Low/medium quality tiers gate sorting off, while high/epic retain it. Module-v1 definitions keep their historical behavior.

renderOrderOffset is a signed-integer core bucket offset and sortCenter is an emitter-local point for draw-level coarse ordering. VFXSystem ranks participating compiled draws far-to-near and sends draw-index assignments to the renderer. A system accepts at most 2^20 - 1 automatic transparent draw entries and rejects an overflowing spawn before retaining its resources. Version-2 decals also capture the emitter's interpolated rotation at birth; authored Init rotation remains a later world-space override. See RFC 006 for exact v1/v2, quality, camera, pooling, and numeric contracts.

Resource preparation

await system.prepare(effect, { signal, onProgress, preparer }) compiles the current quality tier's emitter and grid pipelines without advancing system.time or publishing an effect instance. Work scales with authored resources, not lifecycle or timeline duration. Successful emitter and grid resources enter the normal effect pool, so call prepare() before the first spawn() when the backend needs the exact prepared node objects. Preparation requires maxPoolSize > 0; it rejects explicitly when pooling is disabled. One call reserves one sequential first-use bundle—it does not reserve additional copies for overlapping instances. preparer is an optional renderer hook; without it, core prepares compute resources only. Abort and backend failures reject the promise, roll renderer hooks back, and release partial resources.