Procedural Universe Generator
Deterministic star system generator in C++ that builds any 3D sector from seeded Perlin noise.
5 min read
Query any 3D “sector” coordinate and get a deterministically-generated handful of star systems back. It is an infinite, explorable universe with nothing precomputed or stored. The same coordinate always regenerates the same systems. Different coordinates give different, statistically-plausible content, because a spectral-type frequency table pulled from real published astrophysics figures drives what gets placed.
Two connected pieces:
- Python (
python/) encodes the spectral-type distribution and per-class mass/radius/temperature ranges into a pandas table, exports it todata/spectral_distribution.csv, and sanity-checks it by sampling 100,000 synthetic stars and comparing the observed class breakdown to the input frequencies. - C++ (
cpp/) is the actual generator: a from-scratch Perlin noise implementation seeded per-sector, plus the CSV table loaded at startup, producing star systems on demand for any queried sector coordinate.
Build / run
# 1. Python: (re)generate the distribution table + run the sanity check
cd python
python3 spectral_data.py
python3 analyze_distribution.py
# 2. C++: build the generator
cd ../cpp
make
# 3. Query a sector (from the project root, so the default data path resolves)
cd ..
cpp/universe_gen 1024 -37 8842 --data data/spectral_distribution.csv
--data defaults to data/spectral_distribution.csv (relative to the
working directory), so either run from the project root or pass the flag
explicitly.
Architecture
Noise (cpp/noise.hpp, ~95 lines). Classic improved Perlin 3D noise,
hand-written from the standard reference algorithm (Ken Perlin, 2002):
fade/lerp/grad, no library. The 256-entry permutation table is built by
shuffling 0..255 with a seeded splitmix64 PRNG, so PerlinNoise(seed) is
fully determined by seed.
Sector -> content pipeline (cpp/main.cpp).
sectorSeed(x, y, z)FNV-1a-hashes the three sector integers into one 64-bit seed. Same coordinates -> same seed, always.- That seed drives a
PerlinNoiseinstance. One noise sample decides how many systems (3-8) live in the sector. - For each system, further noise samples (distinct coordinate “channels” per system index) pick its position within the sector and, as a second octave, its planet count and each planet’s orbital distance (log-scaled 0.1-40 AU).
- The spectral class itself is not drawn from raw noise (see the bug note below). It is drawn from a hash-based uniform value and looked up against the cumulative frequency table loaded from the CSV, then mass / radius / temperature are interpolated within that class’s published range using more noise samples.
- Everything is printed as JSON. Nothing is written to disk or kept in memory beyond the single query being served. Run the process again with the same coordinates and it recomputes the identical answer from scratch.
A bug worth documenting. The first working version used the Perlin
noise value directly as the uniform draw for picking a spectral class.
That’s wrong: Perlin noise output clusters near the middle of its range
(it isn’t uniformly distributed), so low values needed to hit the O/B/A/F/G
tail of the cumulative table almost never occurred. A 300-sector batch test
came back ~98.5% M-class stars instead of the correct ~76.5%. Fix: spectral
class is now picked from hashUnit(), a small splitmix64-based hash that
produces a genuinely uniform [0,1) value from (seed, system index). A
re-run of the same 300-sector batch landed at M 76.7%, K 11.3%, G 8.0%,
F 3.2%, A 0.65%, B 0.12%, which matches the published table within normal
sampling noise. Perlin noise is still used for position/count/planet-orbit
“flavor” data, where exact uniformity was never required.
Verified output
Determinism: same coordinates queried twice, byte-identical:
$ cpp/universe_gen 1024 -37 8842 --data data/spectral_distribution.csv > run1.json
$ cpp/universe_gen 1024 -37 8842 --data data/spectral_distribution.csv > run2.json
$ diff run1.json run2.json && echo "IDENTICAL - deterministic"
IDENTICAL - deterministic
Variety: three widely-separated sectors, distinct seeds/positions/class
mixes, captured under output/:
output/sector_1024_-37_8842_run1.json/run2.json: 6 systems, M/G mixoutput/sector_0_0_0.json: 6 systems, all M-class (plausible: P(6/6 M) is ~20% given the 76.5% M frequency)output/sector_-500000_999999_-12345.json: 6 systems, M/K mix
Python sanity check output (100,000 samples):
class expected % observed % observed n
O 0.0000% 0.0000% 0
B 0.1309% 0.1340% 134
A 0.6040% 0.6490% 649
F 3.0202% 3.0200% 3,020
G 7.6513% 7.5580% 7,558
K 12.0809% 12.0310% 12,031
M 76.5126% 76.6080% 76,608
max |observed - expected| = 0.0954 percentage points
OK: sampled distribution matches input table within tolerance.
Files
python/spectral_data.py distribution table -> data/spectral_distribution.csv
python/analyze_distribution.py 100k-sample sanity check against the table
data/spectral_distribution.csv exported table the C++ side loads at startup
cpp/noise.hpp from-scratch seeded Perlin 3D noise
cpp/main.cpp CLI: sector coords -> deterministic star systems (JSON)
cpp/Makefile `make` builds cpp/universe_gen
output/*.json captured proof runs (determinism + variety)
Simplified vs full spec
- Embedded published distributions, not a live NASA API pull. The
spectral-type frequencies and mass/radius/temperature ranges are
hardcoded from standard textbook main-sequence figures (see
python/spectral_data.py), not fetched from a live catalog. Add when a real-time or larger dataset (e.g. Gaia DR3) is needed for statistical fidelity beyond broad class averages. - Single noise-based generator, not a full N-body/galactic-structure simulation. Systems are placed independently per sector with no inter-sector gravity, galactic arm density, or stellar evolution over time. Add when spatial clustering (galaxy shape, star-forming regions) or time-dependent behavior matters.
- Text/JSON output, not a 3D renderer. The CLI prints/exports a description of a sector. There’s no viewer. Add when the output needs to be seen rather than read, e.g. a WebGL or Unity front end consuming the same JSON.
Support
Submit issues, questions, or bug reports to the GitHub issue tracker: https://github.com/ADM1SH/procedural-universe-generator/issues
Roadmap
- Done: Core architecture and baseline implementation.
- Done: Functional verification and test coverage.
- To do: Implement 3D starfield visualization using Raylib
- To do: Add planetary surface biome climate modeling
Contributing
Contributions are welcome.
- Fork the repository.
- Create a feature branch:
git checkout -b feature/improvement. - Commit your changes:
git commit -m "feat: enhance functionality". - Push to the branch:
git push origin feature/improvement. - Open a Pull Request.
Authors and Acknowledgment
- Adam Anwar (ADM1SH) - Lead architect and developer.
- Engineered by Adam Anwar using seeded Perlin noise and published astronomical stellar distributions.
License
Licensed under the MIT License. See LICENSE for details.
Project Status
Seeded deterministic galactic universe generator.