Skip to content

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.

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:

ImplementationUsed 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.

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).

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.

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.

AreaWhereNotes
Townssrc/world/, src/structures/generated from the world seed, then baked into public/baked/towns/ (npm run bake:towns); TOWN_GEN_VERSION invalidates the bake
Propspublic/props/, src/props/prepared packages (LODs, colliders, sockets) placed by the furnishing planner
Treessrc/trees/TurboTwig procedural trees, templates baked in public/baked/trees/
Grass and plantssrc/gpu/grass-*, src/vegetation/GPU grass and flora swayed by one shared wind field
Mega objectssrc/mega-objects/the world tree and skyscrapers with interiors
Asteroidssrc/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.

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.

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.

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

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.