This document explains the internal architecture of the core library for contributors. It covers code organization, the two runtimes (CPU and WebGPU), the module system, and major subsystems like the spatial grid, pipelines, and oscillators.
packages/core/src/engine.ts: facade that selects runtime (cpu/webgpu/auto) and delegates the fullIEngineAPIinterfaces.ts:IEngine,IParticle,AbstractEnginecommon logic (view, modules, config, oscillators, export/import, FPS)module.ts:Modulebase class,ModuleRole,DataType, and uniform plumbingmodules/forces/*: built-in forces (environment, boundary, collisions, behavior, fluids, sensors, interaction, joints, grab)modules/render/*: built-in render modules (particles, trails, lines)runtimes/cpu/*: CPU engine and helpers (Canvas2D rendering, neighbor queries, descriptors)runtimes/webgpu/*: WebGPU engine and builders (GPU resources, program/pipeline builders, spatial grid, shaders)
- Top-level
Engineconstructs eitherWebGPUEngineorCPUEnginebased onruntime. - When
runtime === "auto", initialization attempts WebGPU first and falls back to CPU if device/adapter creation fails (cleanup is handled, and the CPU engine is re-initialized with the same options). - The selected concrete engine provides all
IEnginemethods; the facade also exposes helpers like pin/unpin andisSupported(module).
Note on particle readbacks
- On WebGPU,
getParticles()requires a GPU → CPU readback of the full particle buffer and can be expensive for large scenes. - Prefer local queries like
getParticlesInRadius(center, radius, { maxResults })for tool-like occupancy checks. getParticlesInRadius(...)is implemented in WebGPU via a small compute compaction pass (seeruntimes/webgpu/local-query.ts) that only reads back a bounded result buffer.
Shared functionality across both runtimes:
- Animation control:
play()/pause()/stop()/toggle(), dt clamping, FPS smoothing - View:
Viewtracks camera, zoom, and canvas size; view changes trigger runtime hooks - Configuration:
cellSize,maxNeighbors,maxParticles,constrainIterations,clearColor - Modules: array of
Moduleinstances;export()/import()serialize module inputs, includingenabled - Oscillators:
OscillatorManagerwrites into module inputs each frame viamodule.write()and triggersonModuleSettingsChanged()
Key components (see runtimes/webgpu/):
GPUResources: device/context acquisition, swapchain, shared bind groups, uniform buffers, scene texturesModuleRegistry: collects modules; attaches uniform writers/readers; materializes pass/phase requirementsSimulationPipeline: builds the compute program by concatenating module-provided WGSL snippets across phases (global,state,apply,constrain,correct); dispatches with configuredworkgroupSizeRenderPipeline: executes module render passes (Fullscreen/Compute/Instanced) in sequence, ping-ponging the scene texture as neededSpacialGrid: grid uniforms/buffers and neighbor iterators used by simulation WGSLParticleStore: GPU storage for particle arrays (positions, velocities, size, mass, color, etc.) with known strideLocalQuery: compact local particle queries (getParticlesInRadius) without full-scene readback
Execution order (per frame):
- Update oscillators and inputs; flush uniform buffers when settings changed
- Simulation
statepass (optional), thenapply(forces), thenconstrain(iterated), thencorrect - Rendering: render passes run in declared order; compute passes may read/write the scene texture; fullscreen passes composite
- Present
Performance considerations:
workgroupSize(default 64) andmaxParticlesare configurable- dt is clamped to improve stability (
<= 100ms) - Neighbor queries depend on
cellSizeandmaxNeighbors; tune for density
Parallels the WebGPU phases with pure TypeScript:
- Simulation phases implemented via
CPUDescriptorcallbacks (state,apply,constrain,correct) - Neighbor queries via a spatial grid with
getNeighbors(position, radius)(seespatial-grid.ts) - Rendering via Canvas2D
- Composition: modules declare how to interact with the canvas clear/draw order
- Effects like Trails use immediate-mode approximations (decay fill, canvas blur)
- Each module declares
name,role, andinputs(NUMBER/ARRAY); the engine binds them as uniforms/buffers - The base
Moduleexposeswrite()to update inputs andread()to snapshot them;setEnabled()toggles an implicitenabledinput - For force modules, both runtimes support the lifecycle hooks; render modules contribute render passes
- Arrays are supported and surfaced in WGSL via
getLength()and indexedgetUniform()access
- Environment: gravity/inertia/friction/damping; inward/outward/custom directions use grid/view transforms per runtime
- Boundary: bounce/warp/kill/none modes; optional repel with inside/outside scaling; tangential friction
- Collisions: position correction + impulse; handles identical-position separation
- Behavior: separation/alignment/cohesion/wander; consistent FOV checks; pseudo-random jitter to reduce bias
- Fluids: SPH density (
state) and pressure/viscosity (apply); near-pressure for dense packs; force clamping - Sensors: trail/color sampling; consistent world↔UV mapping and CPU sampling
- Interaction: falloff-based point force; attract/repel
- Joints: CSR incident lists; momentum preservation; optional particle↔joint and joint↔joint CCD
- Grab: single-particle override applied in
correct - Render: Particles (instanced soft-discs; ring for pinned), Trails (decay+diffuse compute), Lines (instanced quads)
export()iterates modules and collects current input values plusenabledimport()writes values back and togglesenabled, then triggersonModuleSettingsChanged()- Oscillators update via a centralized manager; input writes also flow through the same mechanism
- Add new modules under
modules/forces/*ormodules/render/* - For WebGPU: extend builders if the WGSL DSL needs new helpers
- For CPU: ensure compositing and sampling utilities cover your pass
- Update the playground to expose controls for new inputs
- Authoring:
module-author-guide.md - User Guide:
user-guide.md