|
| 1 | +# World generation |
| 2 | +World generation in Hypermine is constrained the following principles: |
| 3 | +* The generated contents of each chunk must depend only on the parameters of the world generation algorithm, not on gameplay. |
| 4 | +* Everything must be generated in its settled state to ensure that unmodified terrain does not need to be stored on disk, and to help with immersion. |
| 5 | +* World generation should be as isotropic as reasonably possible, so that it is not obvious which direction leads to the origin. |
| 6 | + |
| 7 | +These constraints help inspire the details of the world generation algorithm, which are explained further in the sections below. |
| 8 | + |
| 9 | +## Noise |
| 10 | +Every procedural world generation algorithm needs to start with some way of generating noise. This noise is useful for determining properties like temperature and rainfall (which affects what material the ground is made of), and it is also used in determining the shape of the terrain. While Perlin noise is often used as a standard approach, it is not clear how it would be adapted to a large hyperbolic world. Instead, Hypermine uses a different approach specifically designed for a hyperbolic tiling, inspired by Hyperrogue. |
| 11 | + |
| 12 | +### Coarse noise function output |
| 13 | +The first step is to form a coarse approximation of the noise function, deciding on one value for each node. To do this, we take advantage of the tiling itself. For a 2D analogy, the pentagonal tiling of the hyperbolic plane can be thought of as a set of lines dividing the hyperbolic plane instead of individual pentagons. |
| 14 | + |
| 15 | +TODO: Picture of pentagonal tiling with lines colored to distinguish them from each other |
| 16 | + |
| 17 | +Similarly, in 3D, the dodecahedral tiling can be thought of as a set of planes dividing hyperbolic space. This interpretation of the dodecahedral tiling is important for understanding how the noise function works between nodes. |
| 18 | + |
| 19 | +To decide on a noise value for each node, we break the dodecahedral tiling up into this planes. We associate each plane with a randomly chosen noise value offset, such that crossing a specific plane in one direction increases or decreases the noise value by a specific amount, and crossing the same plane from the other side has the opposite effect. Once we decide on a noise value for the root node, this definition fully determines the noise value of every other node. |
| 20 | + |
| 21 | +The following diagram shows an example of the 2D equivalent of this algorithm. |
| 22 | + |
| 23 | +TODO: Picture of pentagonal tiling with each line labeled with the noise value offset, using arrows or something similar to show how this offset applies. The center of each pentagon is also labeled with a number with its current noise value. Integers are used everywhere to allow the reader to verify the math easily in their head. |
| 24 | + |
| 25 | +In this diagram, the randomly-chosen noise value offset of each line, along with the derived noise value of each node, is shown. Note how the difference in noise values between any two adjacent nodes matches the noise value offset of the line dividing them. |
| 26 | + |
| 27 | +This algorithm allows for random variation while keeping nearby nodes similar to each other, which is what we need from a noise function. One notable quirk worth mentioning is that the noise value is unbounded, which currently means that hills and valleys in Hypermine can become arbitrarily high and deep, respectively. |
| 28 | + |
| 29 | +### Fine noise function output |
| 30 | +The next step is to use this coarse approximation of the noise function to produce the actual noise function. To begin, set the noise value at the center of each node to the coarse output we computed earlier. |
| 31 | + |
| 32 | +TODO: Picture of pentagonal tiling with a color at the center of each node to represent the noise value. |
| 33 | + |
| 34 | +Then, trilinearly interpolate these values based on voxel coordinates to create a continuous function across the world. Note that in 2D, this would be bilinear interpolation. |
| 35 | + |
| 36 | +TODO: Picture of the same pentagonal tiling with gradients added. Decorate the center of each node with a dot to highlight the control points of the interpolation. |
| 37 | + |
| 38 | +Finally, for each voxel, add a random offset to its noise value, drawn independently from some distribution. |
| 39 | + |
| 40 | +TODO: Picture of same pentagonal tiling with the final noise value of each voxel. |
| 41 | + |
| 42 | +## Terrain shape |
| 43 | +Hypermine uses the 3D noise function to determine the shape of the terrain. This may seem surprising, as it is arguably simpler to use a 2D noise function instead to form a heightmap of the world. However, using 3D noise instead is a useful way of generating more interesting terrain with overhangs, and more importantly, a 2D heightmap works less well in hyperbolic space because of the way that space expands as you move away from the ground plane. The naive approach would cause hills and valleys to have significantly less detail than terrain near the ground plane. |
| 44 | + |
| 45 | +Instead, the basic algorithm is as follows: Using a 3D noise function, we determine the hypothetical elevation of the terrain at each voxel. We then subtract this elevation by the actual height of the voxel above the ground plane to determine a value (denoted `dist` in code) that can be roughly translated to "the voxel's depth relative to the terrain's surface". If this value is above zero, we are inside the terrain, and the voxel should be solid, and otherwise, it should be void. If the value is above zero but close to zero, one of the surface materials (like dirt) will be used, while if the value is far above zero, a material like stone will be used instead. |
| 46 | + |
| 47 | +Note that the above is a simplification of the actual algorithm. It is recommended to read the implementation of `worldgen::ChunkParams::generate_terrain` to understand all the details. For instance, terracing is used to add flatter terrain layers with steeper hills between them, and the strength of this terracing effect is controlled by another parameter affected by noise called `blockiness`. In addition, some measures are taken to make the terrain surface smoother than the interface between the different terrain layers. |
| 48 | + |
| 49 | +## Terrain material |
| 50 | +The terrain material depends entirely on the following four factors: |
| 51 | +* Elevation above the ground plane |
| 52 | +* Estimated distance below the terrain surface |
| 53 | +* Temperature |
| 54 | +* Rainfall |
| 55 | + |
| 56 | +Note that temperature and rainfall are noise functions set up for the purpose of allowing varying terrain materials. How these are used is described in detail in `terraingen.rs`, so the details are omitted here. |
| 57 | + |
| 58 | +## The road |
| 59 | +Currently, the only megastructure in Hypermine is a single infinite straight road. This megastructure works by using the state machine `worldgen::NodeStateRoad`, which is much like `worldgen::NodeStateKind`, but instead of defining the ground plane, it defines the plane that divides the road into its "east" and "west" sides (with this terminology assuming that the road runs "north" and "south"). Based on this state, the `worldgen::ChunkParams::generate_road` function will be called for chunks right above the ground plane, generating the road's surface and carving out any terrain that is in the way. The `worldgen::ChunkParams::generate_road_support` function will be called for chunks below the ground plane, generating the wooden truss if the road is above the terrain, acting as a bridge. |
| 60 | + |
| 61 | +## Trees |
| 62 | +For decoration, tiny two-block trees are scattered throughout the terrain, with their density depending on the amount of rainfall. Each tree is generated by placing a wood block next to a dirt or grass block, followed by placing a "leaves" block on that wood block, away from the dirt or grass block. While this algorithm is unaware of gravity, it often generates trees upright because the wood blocks are placed on relatively flat ground. See `worldgen::ChunkParams::generate_trees` for more details. |
| 63 | + |
| 64 | +Note that generating larger trees requires a more complicated algorithm that has not yet been planned out or implemented. |
| 65 | + |
| 66 | +## Implementation details |
| 67 | +Hypermine generates worlds in three main stages: |
| 68 | +* Generate a `NodeState` for each node |
| 69 | +* Generate a `ChunkParams` for each chunk, based on the `NodeState` of all 8 nodes adjacent to the chunk's origin |
| 70 | +* Using the information in `ChunkParams`, asynchronously generate the voxel data for each chunk |
| 71 | + |
| 72 | +The reasoning behind these three phases can be seen most clearly in the noise generation algorithm. The coarse noise function created at the beginning of the algorithm is stored in the `NodeState`. Since the noise needs to be interpolated between adjacent `NodeState`s, the `ChunkParams` needs all 8 adjancent nodes to be constructed. The actual interpolation and the inclusion of additional noise is done in the final, asynchronous phase of the algorithm, which depends entirely on `ChunkParams`. |
| 73 | + |
| 74 | +## Additional information |
| 75 | +For additional information related to world generation, it is recommended to read the code and its documentation in `worldgen.rs`, as it has many details not covered here. |
0 commit comments