An open-source, browser-based 6-DoF simulation of the Milky Way — from galactic distances down to inch-scale interaction on a planetary surface, with no loading screens and no scale seams.
Quick start · What it does · Architecture · Development · Documentation · Contributing
The first milestone, a vertical architectural proof, is complete. The current build adds measured Solar System bodies, streamed terrain, a planetarium, portable photographs, cinematic playback and a calibrated stellar population with diffuse sky and dust. Flight and on-foot surface movement are playable; the exploration reward loop and ship subsystems are still design work.
The hard problems in a game at this scale are precision, determinism and identity, and all three are solved and demonstrated here rather than asserted. You can fly from the galactic center to a mountainside, resolve an inch, save the session as compact references and dynamic state, and get the same answer twice when the seed, catalog and generation versions agree.
Status: pre-alpha, single maintainer, no release. Flight and surface traversal work; the exploration reward loop is unimplemented.
docs/roadmap.mdsays what is deliberately not built and where each piece will attach.
| Requirement | Version | Why |
|---|---|---|
| Node.js | 26 or newer | The headless runner executes the TypeScript sources directly through type stripping — that is how pnpm sim works with no build step |
| pnpm | 12 (12.3.4 pinned) | The lockfile is pnpm's, and packages/* are source-only workspace links |
| A browser with WebGPU | Chrome, Edge or Safari 26+ | The client renders through WebGPURenderer with TSL. WebGL 2 is a retained fallback, so Firefox runs — without extended-range HDR output, which it cannot do at all |
| git | any |
Nothing else. There is no native toolchain, no Python, no database, and
packages/* have zero third-party runtime dependencies — a rule the build
enforces rather than documents.
Getting Node 26 and pnpm
The version of pnpm is pinned in package.json's packageManager field, and
any pnpm from 10 on reads that pin and runs the pinned version in this tree, so
which pnpm is installed globally does not matter. Node 26 does not bundle
Corepack, so install one directly:
mise use -g npm:pnpm # or: npm install -g pnpmFor Node itself, any version manager works. With mise:
mise use -g node@26or with nvm:
nvm install 26 && nvm use 26Check both:
node --version # v26.x or newer
pnpm --version # 12.xgit clone git@github.com:jonjaques/inertialref.git
cd inertialref
pnpm install --frozen-lockfile
pnpm build # creates the assets directory the Worker serves
pnpm dev # → http://localhost:5173One command starts both halves — Astro on 5173 and the Cloudflare Worker on 8787,
with /api and /ws proxied to it. pnpm dev:client and pnpm dev:server are
the halves if you want one without the other. After a fresh install,
pnpm dev:client starts the client without a full build, generating the
documentation manifest on its first start; combined development also needs the Worker assets produced by pnpm build.
pnpm preview builds and then
serves the result through the real Worker, which is the closest thing to
production that runs locally.
That is the whole setup. No environment variables you have to set, no services to start, no API keys — the universe is generated from a seed, and solo simulation does not need a live server. A warmed production build can also start offline, subject to its cached assets.
Two things this repository deliberately does not contain
Neither is needed to run the game, the tests or the build.
- The cutscene's reference audio. The scene is cut against a piece of music
that is somebody else's; it is served from the site's own R2 bucket and pulled
in at build time (
pnpm media:pull), never committed. Without it the cutscene plays silent, which is what a fork gets and is a supported outcome rather than a failure.scripts/media.mjshas the reasoning. - Raw catalog downloads. 34 MB of HYG to produce 907 KB of assets, and the
assets are committed.
pnpm catalog:fetchre-downloads them if you want to rebuild.
Open /play/solo, then the browser console. The whole simulation is scriptable
from there once the scene is ready:
ir.help() // everything the harness can do
ir.targets() // everywhere you can go, nearest first — start here
ir.goTo('b:2') // system or body; accepts the forms a human types
await ir.selfTest() // the twelve milestone capabilities, executed
await ir.scenario('surface') // land on the first solid world
ir.status() // full structured stateStart with ir.targets() — every other verb takes an address and none of them
will tell you one. The same verbs are on the author's instruments, opened from
the IR menu at the bottom center, so anything you can do by clicking is
reproducible in a test.
The harness object also drives the headless runner, so a scenario that reproduces a bug in Chrome can be replayed without a browser:
pnpm sim --self-test # no browser, no DOM, no WebGL
pnpm sim --targets --goto b:2 # the same navigation, from a terminal
pnpm sim --help # all flagsOne gotcha. Chrome throttles
requestAnimationFramein backgrounded tabs, so a freshly reloaded page that is not focused sits at tick 0 until you click it. That is the browser, not the clock.
- A galaxy centered on the real galactic center, with 7,123 real star systems out to 150 light-years — HYG v4.4 converted through ICRS → galactic coordinates, plus 7,514 distant bright catalog stars. Nine luminosity levels fill the catalog's completeness gaps. Resolved stars and diffuse light share one calibrated population and dust field; ADR-0038 records the physical model and its limits.
- 702 confirmed exoplanets around 443 of them, with their published orbits,
masses and radii, plus the eight planets of the Solar System. Every body says
whether it is
observedorprojected; the game never claims a generated planet is real. - Stars named the way people name them —
Sirius,Alpha Centauri,Tau Ceti,61 Cygni— with every alternate designation searchable, and one stable address per system whatever the catalog calls it next year. - The real Solar System — 129 bodies: eight planets, nine dwarf planets, fifty asteroids and comets, and sixty-two moons, with measured radii, oblateness, axial tilts, rotation periods, albedos and ring geometry, drawn from NASA and USGS surface, elevation, cloud and ring maps. Earth has its clouds, its city lights and sun-glint on its oceans; Saturn is visibly oblate and casts its shadow across its own rings; Pluto has its heart.
- Bodies that are not spheres — twenty-five measured shape models from the NASA Planetary Data System, resampled to radius grids that reconstruct their sources' volume to between 99.8% and 100.6%. Phobos has Stickney in its silhouette, Bennu has the equatorial ridge its own rotation raised, and 216 Kleopatra is a dog bone. Everything else that gravity never rounded off — in Sol and in every generated system — gets a figure from its own seed, on its published half-extents, drawn from the distribution those twenty-five bodies actually have.
- Deterministic star systems everywhere else — planets, moons, orbits, atmospheres and terrain, all a pure function of a global seed and an address — and a belt of six to eighteen small bodies in each, sized by Dohnanyi's collisional cascade and spinning no faster than the 2.2-hour rotation barrier a rubble pile comes apart at. A planet answers to the same kind of limit: no day short enough to fling its own equator off, and the oblateness left over comes from Darwin–Radau with a moment of inertia factor, because the uniform-fluid relation draws a fast giant as a lens. One giant in six wears rings, and its strip is generated from a character — a bright sheet, a system of dark threads, or a sheet with threads shepherded outside it — so two ringed worlds are not one picture at two radii.
- A debug spacecraft with 6-DoF flight, patched-conic gravity, atmospheric drag, sphere-of-influence frame transitions, and landing.
- Streamed cube-sphere terrain — a restricted, morphing quadtree covering
the whole disk, seamless from orbit to standing height, generated on the GPU
by a compute kernel held to the CPU's canonical field —
pnpm test:gpuon a physical adapter is what proves that bound; the twelve checks prove the pool — with the worker pool as fallback, and shaded from the body's own geology: seven deposits laid down in order, a sea drawn as a refracting sheet over its own seabed, crater rays, and the archive's published photograph on the bodies that have one. It refines to cells of a third of a meter, with boulders addressed aso:objects lying on it — and the ground the ship lands on and the ground you can see are two functions 1.25 m apart, measured and written down. - On-foot movement on solid planetary terrain, with walking, sprinting, strafing, crouching, and local-gravity jumps in first or third person, in a heat-weighted EVA suit whose gait is locked to the ground it covers. Lock to walk or the default Shift + L activates pointer capture after landing; the planetarium has no walker and its stance flies with the keys. A local solo owner can double-tap the jump binding to fly. Contact covers terrain and the walkable relief a structure declares — the pad's deck, aprons, ramp and equipment. On foot lists the controls and limits.
- Save and load to IndexedDB, storing references and dynamic state rather than generated content. The self-test's flown session is about 1 KB; save size grows with entities, loaded-system references and mutations.
- Offline after caching: a warmed production build loads, streams terrain from its GPU and workers, and passes the twelve capability checks with the server stopped. Unvisited documents and uncached models or textures still need a connection.
- Dockable panels in the browser — the catalog, the object record, the
camera and the presets in the planetarium, and the author's
controls,telemetry,perfandgraphicsbehind a disclosure — that call the harness and nothing else, so anything you can do by clicking is reproducible in a test.
The first milestone had twelve things to demonstrate (the full list, with what
each measures). They are executable rather
than described — await ir.selfTest() in the browser, or pnpm sim --self-test
in Node:
12/12 capabilities proven
PASS 1. Deterministic generation — HIP 38 identical across runs, differs by seed
PASS 2. Stable addressing — 129 bodies addressed and round-tripped
PASS 3. Astronomical distances — Sol to Alpha Centauri: 4.3209 ly
PASS 4. Movement within a system — 6.81 km under thrust in 10 s
PASS 5. Approach a planet — fell 16.65 m in 60 s at 0.0092 m/s², within 0.03% of free fall
PASS 6. Frame transitions — entered b:g:milky-way/s:SOL/b:0 after traveling 8 Mm
PASS 7. Precision near the surface — 1 inch resolved to 9.4 µm, 8.18 kpc from the galactic center
PASS 8. Meter-scale rendering — 1 m separation survives float32 at 8.18 kpc
PASS 9. Origin rebasing — 500 rebases, 2560 km of origin travel, zero drift
PASS 10. Worker task — 4761 elevations and 33800 cover bytes generated in a worker, identical to local generation
PASS 11. Save round trip — 1041 bytes restored to an identical state hash
PASS 12. Frame-rate independence — identical state hash 55d4dba1 at tick 513
This example is the Node run from 9 Sep 2026. The twelve checks cover the
architectural foundation; they do not establish visual quality, galaxy
photometry, GPU performance or every offline asset. Dedicated suites and
recorded measurements cover those areas. CI runs the self-test on every pull
request, alongside pnpm check.
UniverseVector (sector + offset) canonical position, sub-millimeter anywhere
↓ reference frames semantics of motion; identity-preserving
frame-local Vec3 what gameplay and physics work in
↓ floating origin rebased on a 1024 m grid, exactly
render space small numbers, float32-safe
↓ LOD + depth compression angular size preserved, depth compressed
Three.js
Five decisions carry most of the weight:
- Positions are sectorized, not doubles — an int32 sector index per axis plus a double offset inside a 2⁴⁰ m sector. Sub-millimeter everywhere within 249,000 ly of the origin, and crossing a sector boundary is exact.
- Frames are not a precision mechanism; the coordinates already are. They carry the semantics of motion, and re-framing provably does not move anything.
- Seeds derive down a path of labels, never along a shared stream, so generation order, worker count and load state cannot change the universe.
- 64 Hz fixed tick, because 1/64 is exact in binary. Wall clock decides only how many ticks to run — and a ship coasting through them is propagated from an epoch rather than stepped, so the frame jumps and every warp detent arrives.
- A save is a reference, not a copy: seed, tick, and the handful of things that could not be regenerated.
Full reasoning, alternatives and consequences are in docs/adr/.
apps/
game Astro shell, React + R3F runtime, WebGPU/TSL renderer
headless Node runner — no DOM, no React, no WebGL
ingest turns published catalogs into the packed star asset
server the Cloudflare Worker — the only place Cloudflare appears
packages/
shared units, invariants, structured logging (layer 0)
spatial UniverseVector, frame graph, floating origin (1)
procedural PRNG, hierarchical seeds, noise (1)
physics Kepler, rigid body, atmosphere, thrusters (2)
universe addressing, catalog, generation, terrain (3)
simulation clock, entities, flight, streaming, snapshots (4)
protocol versioned, validated wire and save schemas (4)
workers typed tasks, transport ports, job pool (5)
persistence save/load, migrations, store port (5)
net authority port, local authority (5)
rendering canonical→render bridge, LOD, terrain meshing (5)
devtools inspection, capability checks, harness (6)
data/catalog/ the packed star catalog, committed, CC BY-SA 4.0
docs/ concepts, ADRs, guides, and the design bible — published
design/ the working half: plans, tooling reports, brand sources
scripts/ the graph check, the dev orchestrator, the browser driver,
the brand build, the media sync and the documentation build
Every package below apps/ runs unchanged in the browser main thread, a Web
Worker and Node. A package may depend only on strictly lower layers, and
pnpm graph enforces it — along with rejecting any third-party runtime dependency
in packages/*.
| Command | What it does |
|---|---|
pnpm dev |
Astro on :5173 and the Worker on :8787, in one terminal |
pnpm dev:client |
Just the Astro development server on :5173 |
pnpm dev:server |
Just cf dev, the Worker on :8787 |
pnpm preview |
Build, then serve it through the real Worker on :8787 |
pnpm test |
Vitest, Node environment only — no DOM is ever registered |
pnpm test:gpu |
The shader suite, on the real GPU through Dawn — not in pnpm check |
pnpm typecheck |
The six type projects, in parallel — a group of pnpm check; see below |
pnpm lint |
oxlint, not eslint (oxlint --fix applies autofixes) |
pnpm graph |
Dependency layering + cycle check, and prints the graph |
pnpm brand |
Re-render every icon, the share card and the crawler files |
pnpm docs:build |
Render docs/ and every export of packages/* into the site's /docs |
pnpm build |
Optional media pull, docs:build, typecheck, then Astro and emitted HTML checks |
pnpm check |
The gate, as one graph: graph, spelling, brand, presets, format, lint, typecheck, test, test:slow, sim, build. |
pnpm sim --self-test |
Headless run plus the twelve capability checks |
pnpm vitest run <substring> |
A single test file |
pnpm drive --help |
Drive Chrome over the DevTools Protocol — --js, --shot, --sample |
Do not report a task complete without pnpm check passing. CI runs exactly
that command, so there is no separate list of CI stages to drift out of step.
- There are five tsconfig projects, not one, and no project references. They
type-check the real environments:
tsconfig.jsoncoverspackages/*with no DOM lib (the core must run in a browser, a worker, and Node),apps/game/tsconfig.jsonadds DOM/WebWorker/JSX,apps/headless/tsconfig.jsonadds Node types, plus the Worker and the catalog ingest. Why references were rejected is in development. strict,noUncheckedIndexedAccess,erasableSyntaxOnlyandverbatimModuleSyntaxare all on. So: noenum, no parameter properties,import typefor type-only imports, and local imports carry their.tsextension — Node runs the sources directly.- Astro 7 with React, Vite 8, and React Compiler. Do not hand-write
useMemo/useCallbackmemoization. (useMemofor a stable Three.js object is a different thing and is fine.) - Tests live beside the code and run in plain Node. That is the check that the
core stays free of DOM, React and WebGL. Reach for
fast-checkproperty tests for anything mathematical — several real bugs here were found that way and would not have been found otherwise. packages/*are source-only workspace links. There is no build step between an edit and a test.
AGENTS.md is the working card. Each rule in it exists because violating it is a rewrite later rather than a refactor — for example:
- Never put an absolute position in a
Vec3.UniverseVectoris the only thing that may claim to be one. - Never use
Math.random(),Date.now()orperformance.now()in anything canonical. Wall clock enters at exactly one call. - Never make generation depend on order. Derive a seed from the address.
- Never persist anything regenerable. If you want to store generated content, you want a cache, and it is not a save.
Read it before changing anything. Agents should continue in docs/agents/. Humans should continue in docs/.
docs/ is the map. Voice and where each audience should look
are in STYLE.md.
Every page below is also readable at
/docs — the same markdown, rendered
by pnpm docs:build alongside a generated reference for every export of
packages/*, and read over a live scene rather than beside a screenshot of one.
The markdown in this repository is the source; the site has no copy of its own.
| Getting started | Run it, drive it, and five things to try |
| Vision and scope | What this is for, and the principles behind it |
| Architecture | The system in one sitting |
| Concepts | How each mechanism works, and why |
| ADRs | Twenty-six decisions that are expensive to reverse |
| Development | Commands, toolchain, conventions |
| The harness | The scriptable API, in full |
| Testing | Property tests, golden vectors, state hashes |
| Extending | Adding a body type, a task, a save field |
| Roadmap | What is deliberately not built yet, and the seam for each |
| Spikes | Five questions that needed a measurement, and the numbers that came back |
| Design bible | What the game is, and why each mechanic is shaped that way |
| Agent handbook | How coding agents should work here |
| CONTEXT.md | Build log: what exists, what was decided, which bugs must not return |
Stated plainly, because discovering these by surprise is worse than reading them:
- Multiplayer is not implemented and is a later phase. The seams exist (partition mapping, no vendor imports) and the design sketch is ADR-0008.
- Multiple-star systems are modeled as single stars. The catalog records the true component count, so the simplification is visible rather than hidden.
- Gravity is patched-conic — no n-body perturbation.
- Collision covers terrain and a structure's declared relief. Characters step, slide along a wall and reject steep uphill movement. Rock scatter, hull walls, ceilings, and entity-to-entity contact remain unimplemented. The rendered shape mesh of an irregular body is not its canonical contact field.
- On-foot survival and interaction are unimplemented. There are no suit resources, injuries, inventory, tools, or walkable ship interiors. Flight permission is a trusted session capability; online admin authentication is not implemented.
- Terrain costs more than its budget. The quadtree covers the whole disk —
morphed, seamless, measured to the ground rather than the datum — and it
refines craters, plates, volcanism and ice from each body's own facts and
shades them from a palette built out of the same facts. Above the eight-pixel
gate a body is drawn as its archive sphere, and a generated world's sphere
wears six baked faces of its own ground — the reflectance, and a relief record
in the layout and encoding the archive's normal maps use, so one decode path
serves a bake and a photograph — and its maria, its lakes and its mountains
all survive the switch. A mapped body's ground wears the same photograph its
sphere does. Generating a bordered 65×65 patch costs 24 to 69 ms across the
zoo — 24 on a world young enough to have no crater population, 69 on a
battered icy one whose crater ladder runs all fourteen rungs — against a
documented ≤ 8 ms budget, and a whole-disk selection peaks at 1,077 patches,
which is 255 MB of vertex buffers at the flight lens. That cost binds only where the pool draws the ground. On a
WebGPU page the tiles come from a TSL compute kernel
(ADR-0023), and a two-meter stance on
Luna converges in 4.4 s at 1600×900 and 7.5 s on a retina window, against
25–33 s and 61 s from a pool of eight on the same descents; what binds there
is the main-thread mesh build at eight patches a frame. WebGL 2 has no
producer but the pool and pays its figure in full.
pnpm sim --terrain-baselineprints all of it; the roadmap has the seams. - Target-laptop acceptance remains open. The Perf panel and timeline record frame, engine, worker, memory and GPU work. Measurements include M5 runs at 1080p and retina resolutions and cold/warm startup; they do not establish the 2023-class laptop budget. Technical requirements separates budgets from historical operating points.
- Rendering has explicit approximation limits. Measured maps and figures, modeled hulls, GPU terrain, instanced stars and rock scatter are implemented. The galaxy has calibrated V-band light, illustrative RGB stellar colors and absorption-only dust. Scattering by interstellar dust, H II emission, globular clusters and neighboring galaxies remain separate additions. ADR-0038.
- Atmospheric scattering uses precomputed tables. The shared CPU bake and client worker path provide transmittance and multiple-scattering tables. Cloud maps are thin weather shells, and weather dynamics remain unbuilt. ADR-0028 records task ownership.
Contributions are welcome, and the design's shape makes some kinds much easier than others.
- Read AGENTS.md first. It is the working card, and its rules are not stylistic. The rest of the agent handbook is docs/agents/.
- Read the ADR for the area you are touching. They are short, and they exist because those decisions are expensive to reverse.
- Find the test that covers the behavior you are changing. If there is not one, that is the first thing to write.
pnpm checkmust be green, and any meaningful architectural change should be reflected in the ADRs and in CONTEXT.md.
Good first areas: terrain and LOD work, physics and orbital mechanics, the
procedural generators, and anything in docs/spikes.md marked
as still needing hardware. Harder: anything touching coordinates, determinism or
addressing — not because contributions are unwelcome there, but because the
invariants are subtle and the ADRs should be read carefully first.
There is no CONTRIBUTING.md or CODE_OF_CONDUCT.md yet; both are
acknowledged gaps.
The project is non-commercial, but it is deliberately not licensed with a non-commercial clause — such a clause is not an open source license under the OSI definition and would make the project ineligible for most package ecosystems. The right way to be non-commercial is a genuine open license and simply not commercializing it. The reasoning is in sustainability.
data/catalog/ is a derived database and is not covered by the Apache
license. It is CC BY-SA 4.0. The terms were verified rather than assumed
(spike 4):
| Source | Terms |
|---|---|
| HYG database v4.4 | CC BY-SA 4.0. Share-alike reaches the packed catalog, which ships as its own asset with its own notice |
| NASA Exoplanet Archive | No license stated; operated by Caltech under NASA contract. Its requested acknowledgment is carried |
| Gaia (ESA) | CC BY-NC 3.0 IGO — non-commercial. Deliberately unused, for exactly that reason |
Share-alike attaches to the database and not to the software that reads it —
CC BY-SA 4.0 § 4(b) says "but not its individual contents" — so Apache-2.0 on
packages/* and CC BY-SA 4.0 on the catalog cover different works and do not
conflict. See NOTICE, data/catalog/LICENSE.md, and
the catalog guide.
Built by Jon Jaques · Issues and discussion at jonjaques/inertialref