Architecture
A tour of how Pinionfall is put together, for someone about to change it. Every path is
relative to the repository root. docs/claude-memory/ holds the long form: campaign and
subsystem files, with the traps that shaped them.
Terrain
Section titled “Terrain”Every solid body is a density function: positive inside the solid, negative in the air,
zero at the surface. The GPU evaluates it in compute shaders and extracts the surface with marching
tetrahedra (src/gpu/density-mc-shader.ts, src/gpu/gpu-chunk-generator.ts; the mc in
older file names is historical). Chunks are streamed through a per-body octree LOD
(src/chunking/), welded, and packed into shared vertex and index pools, so the frame draws
terrain from a handful of large buffers instead of one buffer per chunk.
The density function exists three times and the three are kept in lockstep, layer order included:
| Implementation | Used by |
|---|---|
WGSL (src/gpu/density-function.ts and the shaders built from it) | the terrain you see |
TypeScript (src/chunking/density.ts) | CPU queries: placement, agents, bakes |
Rust (parry/src/shape/density_field.rs in the physics workspace) | the terrain collider |
Per-body parameters live once, in src/gpu/body-density-params.ts, and reach all three.
npm run test:densityparity pins the Rust output bit for bit against a committed golden and
bounds the TypeScript/Rust difference. The lattice, hash and gate decisions agree exactly;
transcendental functions may differ by a few f32 ULPs, by design, which keeps the surfaces
continuous. Change one implementation and you change all three in the same commit.
Towns, roads and structures carve the field through footprints and a road mask, so a building’s floor and a road’s bed are part of the terrain rather than objects laid on it.
Physics
Section titled “Physics”vendor/rapier3d/ is a prebuilt WASM of forked Rapier and parry (sources in the
pinionfall/physics workspace; vendor/rapier3d/provenance.json records the commits it was
built from). The fork adds a DensityField collider shape: collision queries fault in tiles
that are meshed from the same density function (density evaluation, marching tetrahedra,
weld, then a TriMesh with its BVH). A worker (src/physics/tile-builder-worker.ts) builds
tiles ahead of the player and ships them to the main thread serialised; a query that reaches
a tile the worker has not delivered yet falls back to work on the main thread. Nothing about
terrain collision is baked.
The player and every NPC share one CharacterController over Rapier’s kinematic character
controller (src/character/). Wildlife has no Rapier body: creatures find the ground by
marching the density function through surfaceRadiusAt (src/wildlife/creature-context.ts). Gravity is a field
with a sphere of influence per body (src/physics/gravity-field.ts).
Rendering
Section titled “Rendering”WebGPU only. The main path is a visibility buffer resolved into a G-buffer and lit in a
deferred pass (src/gpu/visibility-*, src/gpu/deferred-lighting-pipeline.ts), with
clustered punctual lights, cascaded sun shadows packed into one atlas, a separate punctual
shadow atlas (src/lights/), Hi-Z occlusion culling of terrain chunks, TAA, SSAO, bloom and an ACES tonemap. The atmosphere
is LUT-based scattering with aerial perspective; clouds are volumetric and cast shadows over
the whole sphere. Water has its own pipeline with waves, foam and screen-space reflections.
Glass and other transparent surfaces are drawn after the opaque frame.
src/gpu/webgpu-renderer.ts is the frame orchestrator and is very large; most techniques
live in their own *-pipeline.ts and *-shader.ts pair next to it.
Worlds and the solar system
Section titled “Worlds and the solar system”src/celestial/celestial-frame.ts defines the bodies (DEFAULT_SYSTEM): the sun Naliay,
the home planet Meroros (id planet) and its moon Merys (luna), further landable worlds and
moons, two terrain-less giants (goliath and frost), and asteroid rings owned by some of them. Each body
carries its own settings (terrain parameters, octree depth, gravity, atmosphere, weather,
ecosystem) and moves on a circular orbit. One body is active at a time: it sits at the world
origin and owns physics. Flying into another body’s sphere of influence swaps the active
body, and the target is prepared ahead so the swap happens without a loading screen.
Content
Section titled “Content”| Area | Where | Notes |
|---|---|---|
| Towns | src/world/, src/structures/ | generated from the world seed, then baked into public/baked/towns/ (npm run bake:towns); TOWN_GEN_VERSION invalidates the bake |
| Props | public/props/, src/props/ | prepared packages (LODs, colliders, sockets) placed by the furnishing planner |
| Trees | src/trees/ | TurboTwig procedural trees, templates baked in public/baked/trees/ |
| Grass and plants | src/gpu/grass-*, src/vegetation/ | GPU grass and flora swayed by one shared wind field |
| Mega objects | src/mega-objects/ | the world tree and skyscrapers with interiors |
| Asteroids | src/asteroids/ | belts with impostors and proximity-activated physics |
Anything expensive and deterministic is baked ahead of time (npm run prebake), shipped in
the asset bundles, and checked by scripts/verify-prebake.ts during npm run build.
Characters and animation
Section titled “Characters and animation”src/animation/ holds the skeletal animation runtime (retargeting, blending, root motion,
foot and aim IK, procedural gaits; derived from ossos, see THIRD-PARTY-NOTICES.md) and the
character assembly; src/character/ the controller, camera, creator and ragdoll. Skinning
runs on the GPU; capes and hair use an XPBD cloth solver with a GPU compute path. Clips come from a runtime motion library whose every clip records
its source; scripts/check-animation-provenance.mjs is part of npm run build.
The game layer
Section titled “The game layer”src/game/ is the RPG: combat, dialogue, events and reputation, identity and races, the
intro, items and loot, lore, naming, progression, quests, saves, selection and stealth. NPCs (src/npc/) follow
schedules, sleep, work and sit. Speech is synthesised on the player’s device with Kokoro; an
NPC dialogue model (Qwen2.5 0.5B, run on the CPU through wllama) is on by default, downloads
in the background at launch, and can be switched off in Settings or in the talk window. Both
download their weights from Hugging Face on first use.
Saves live in the browser: the save in IndexedDB, settings in localStorage. SAVE_VERSION
migrates saves forward; BUILD_EPOCH in src/boot-first.ts wipes stale client state when a
release cannot migrate it.
Layout
Section titled “Layout”| Path | |
|---|---|
src/gpu/ | renderer, terrain generation, atmosphere, clouds, water, grass, sun shadows |
src/lights/ | punctual lights and their shadow atlas |
src/chunking/ | density function (TypeScript), octree LOD, chunk classification |
src/physics/ | Rapier integration, collision tiles, gravity |
src/celestial/ | bodies, orbits, frames |
src/world/, src/structures/, src/props/ | towns, roads, walls, buildings, furnishing |
src/trees/, src/vegetation/, src/mega-objects/, src/asteroids/ | scattered and landmark content |
src/character/, src/animation/ | characters, controller, animation, IK, cloth |
src/npc/, src/wildlife/ | NPCs and creatures |
src/game/ | the RPG layer |
src/audio/ | spatial audio, generative music, procedural synths, speech |
src/ui/ | HUD, windows and panels |
scripts/ | bakes, workshops, audits and the setup scripts |
vendor/rapier3d/ | the prebuilt physics WASM |
docs/claude-memory/ | the Claude Code memory the engine was built with |
Diagnostics
Section titled “Diagnostics”A production build prints nothing to the console. Add ?diag to the URL to turn logging and
the debug keys on; many subsystems also expose window.__* switches in the console. Most
subsystems have a window in the in-game sidebar (SYSTEM, SUN SHADOWS, OCCLUSION, PHYSICS,
NPCS and so on) whose settings change the running engine.
Written by release:site-sync from ARCHITECTURE.md at v2026.09.22. Edit the engine document, not this page.