One seed to rule them all

Oh now it happens! A while ago I wrote that I will never write about something work related, even if this would be a private project… so, here we go.

I am a sucker for procedural generation of any kind and a while ago, I wrote a galaxy PCG system which allows to generate galaxies of different types and shapes like elliptical, spiral, lenticular and irregular. The generation starts by selecting a fixed or random seed. Please note that all the following generators always work in the same way and the results are completely reproducible. For my seeds to initialze the random number generators with, I like to take universally unique identifier (Version 4 UUID) with 122 bits of random thingies. That makes up to 21222¹²² variants, that’s 5.3×10365.3×10³⁶ … according to the absolutely reliable and fail-safe Wikipedia, that’s 5.3 undecillion. This UUID stored as a string representation looks something like this: a1244d3f-740f-4beb-974b-c504380f7948

So, my system generates the galaxy and a point cloud corresponding to the distribution of star systems per galaxy type. The galaxy is internally divided by a k-d tree (k-dimensional tree) where each chunk receives another UUID to generate its children from. Walking down the tree, the last child node generates the contained star systems and assigns another UUID to each of them. That makes the generation process very simple and you only have to determine where in the tree your point of reference currently is. The star system PCG is then generating a representation for each body.

In general I tried to be as close to the actual physics as possible. I spend weeks and months to read about cosmology and astrophysics. The result was not comparable to Space Engine, but… quite close. The clumsy, ugly little sister, so to say. Space Engine is being developed for what? Nearly two decades if I am not mistaken. What to do now? Expand the system outwards and implement the distribution of galaxies or inward generating the worlds? And that’s were my latest project starts.

The last UUID my galaxy generator passes is the one for each celestial body. I used the seed to generate a heightmap for the topology of the body. This heightmap is now replaced by a node-graph that contains rules like combining noise functions for several different types of planets. In this case only the habitable planets are relevant.

Let’s start with the climate. According to the distance from the main star and the axial tilt, I generated a temperature layer using a shifted gradient for the axial tilt and a base temperature that simulated the overall temperature relative to the distance from the planets main star. This is implemented as a single float value.

Base temperature -0.5 left, 0.0 center, 0.5 right

As you can see, the climate zones (or biomes) are depending on the base temperature and the bi-polar gradient that simulates the equator and poles. I later implemented a simulation of those climates which allowed me to melt the ice away, let the sea level rise, the heat dries out the landmasses, desertification et all. All those unrealistic things that won’t happen at all when temperatures are rising. /s

Next up I implemented wind and precipitation layers. Oh, I forgot to mention that all “layers” correspond to noise functions like Perlin, Voronoi etc. In the simluation mode, wind and percipitaion data will erode the elvetation data (i.e. heightmap) further and the sediment data layer is generated. In simulation mode I used the percipitation and erosion data to simulate the river generation, without simulation the rivers are generated using a Dijkstra map from a random point in a higher region of the elevation data towards the nearest basin at sea level.

I really like to play open-world games where the focus is also on exploration so I wanted to put a little efford in implementing mechanics for that. Imagine walking around the map, no clue where you are and you start to plot a map of the land. You reach a body of water and say “Well… let your name be… Tommy!” and you are marking it as “Lake Tommy”. Some weeks later you come by a body of water and you find a note saying: “Lake Tommy”. “Great! I know exactly where I am!” you think. But Lake Tommy is a little (or huge) SOAB and you can be literally anywhere on the map because Tommy spans from north to south. I was searching for a way to split biomes/water bodies if they are getting to big to make naming and orientation easier. I decided to split those regions when there is a very narrow section separating larger parts, like the Straight of Gibraltar separates the Mediterranean Sea from the Atlantic Ocean.

That was quite a daunting task. There are so many ways to do that and the biggest problem was: the better the result looked, the higher were the calculation costs. I ended up collecting all points that were marked as a certain biome (let’s say water) and then flood fill them, separating the resulting bodies until all collected points were processed. Then I analyize each of the resulting bodies of X if there are very narrow parts and splitting them there. Because that was done via flood-fill, too, the resulting debug data was quite unattractive. Straight lines parting the regions were not exactly what I was looking for.

Biome view, first interation of flood-filling the biome, initial separation, processed edges.

In the next step I took all the resulting edges that were separating the regions and applied a jitter to them. That helped a lot and the result is quite nice for the calculation costs, I think. Now I was able to split large forests, lakes, oceans etc.

I then wanted to include civilizations to my world. Heavily inspired by Dwarf Fortress, I started working on a history generator buuuuut… let’s be honest, that would be too much and would take me too long. I decided to give each planet a random number of initial civilizations, a birth factor for new civilizations to emerge and a simulation time span to work with. Each civilization comes with a set of properties like government types (I am looking at you Traveller!), relgion types etc. Each government type has a agression value from 0 to 1 and each religion has a fanatism value from 0 to 1. Combined this will result in the overall agreession the civilization expands with. Civilizations with contrary government or religion types are more likely to wage war on another than civilizations that are more similar. The civilization PCG selects a random point from all points not equal to water and assigns it as the capital or origin of the civilization. From there on, the civilizations are eating up unclaimed points bordering their current patches of land. If a civilization reaches a point that is already claimed, a war emerges and this will be decided by rolling on the overall agression factor. Remember the sediment data layer? Sediment is equal to fertility in this situation and data points with high amount of sediment are more attractive than deserted ones. If there was a war on a point in the data structure, it will be marked as such for later use. This continues until all points are claimed by a civilization. This way I was able to let civilizations rise and fall… great!

Next, another data layer was introducted: Points of Interest. I tried several ways to randomize them and ended up by just generating a POI on a data point when the random number generator returned a 0.5 or higher. Easy! I prepared some POI types like capital, metropolis, city, town, village, ruin, monument, oddity etc. When data point X has a POI of type town and the point is marked as “war”, there is a chance of 50% that the town once belonged to the opposing civilization, has been destroyed and is now a ruin. If POIs are generated below sea level, they are automatically marked as submerged and another POI type is introduced: vessle. We can now dive for ship wrecks! Yay! All POIs are now assigned to the current landholders and voilà.

I am currently working on some kind of language generator for the civilizations which is heavily inspired by Vulgarlang.com. This way I can automatically generate names in different procedural “languages” for lakes, seas, rivers, forests, mountains etc.

Please remember that I am working on this system for about 5 weeks now and that it is highly, highly unoptimized. Up until this point I stored the whole data as JSON chunks to easily save and debug the output. A generated planet was about 118 MB worth of data at an array size of 1024×512, after compression about 20MB. Stripping the JSON and writing only binary data ended up in 7MB. A whole planet with all the sub-generators takes about 11.7 seconds on a desktop machine, 34.8 seconds on a current-gen mobile phone and 125 seconds on a Raspberry PI 5. None of the generators are using compute shaders, so this would be an optimization for the future to speed things up on systems that can take make use of those shaders.

So, now you know! I have no clue what to do with this system, but it is fun working on it.