-
Notifications
You must be signed in to change notification settings - Fork 18
Add documentation summarizing the world generation algorithm #461
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
It looks like checks are still needed for this PR to be merged. I did find https://github.com/orgs/community/discussions/129235 after some googling, which seems related. I had forgotten that, while workflows are defined in the Please let me know if you have any thoughts. It seems like one option could be to make these jobs a no-op if only the |
docs/world_generation.md
Outdated
| @@ -0,0 +1,75 @@ | |||
| # World generation | |||
| World generation in Hypermine is constrained the following principles: | |||
| * The generated contents of each chunk must depend only on the parameters of the world generation algorithm, not on gameplay. | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not obvious to me what point this is trying to make. How could initial chunk contents depend on gameplay?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would probably be good for me to add examples to the docs of things that don't fulfill the principles to make them less confusing. There are two examples I can think of here:
- In Hyperrogue, what biomes you encounter depend on what you have unlocked.
- If we're not careful, the world generation could depend on the order in which nodes and chunks are generated. Minecraft has this issue, as described in https://minecraft.wiki/w/World_generation#Generation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah; maybe something like "The contents of a chunk when it is first visited are determined only by the chunk's location and the world generation parameters, which never change within a world."
docs/world_generation.md
Outdated
| # World generation | ||
| World generation in Hypermine is constrained the following principles: | ||
| * The generated contents of each chunk must depend only on the parameters of the world generation algorithm, not on gameplay. | ||
| * 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does it mean for state to be settled?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should probably just omit this, as we don't have any naturally moving voxels currently, making this requirement fulfilled automatically. It could be useful to keep in as a design guideline, but it's likely more confusing than helpful.
Minecraft is an example of a game that does not fulfill this, as mountains are generated with water source blocks that start flowing and creating waterfalls after they're generated, so if you move quickly enough, you can see waterfalls forming.
If this happened in Hypermine, it would break the immersion of you being just a visitor to a vast world, and it would potentially make exploring without placing blocks more costly on storage space.
docs/world_generation.md
Outdated
| These constraints help inspire the details of the world generation algorithm, which are explained further in the sections below. | ||
|
|
||
| ## Noise | ||
| 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Every procedural world generation algorithm needs to start with some way of generating noise
I don't think this is true. It's a popular approach for voxel games, but even there it's important not to over-focus on it -- it's an awkward, wasteful way to produce large scale structures or local, self-contained effects like trees, for example.
While Perlin noise is often used as a standard approach, it is not clear how it would be adapted to a large hyperbolic world
Perlin noise (aka gradient noise) could be readily adapted. In space of any curvature, you generate gradient noise by defining a regular grid of points, placing a randomly chosen gradient kernel at each point, and then summing all overlapping gradients to produce a scalar field. The nuance is in how you deterministically randomize each grid point, but we have a solution for that.
Instead, Hypermine uses a different approach
Hypermine's approach resembles value noise, which is like gradient noise except you assign each point a value instead of a gradient and interpolate rather than summing kernels. However, normal value noise selects the value of neighboring vertices independently, whereas we're building them by iteratively perturbing the values from the origin.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had originally learned about Perlin noise from https://web.archive.org/web/20080724063449/http://freespace.virgin.net/hugo.elias/models/m_perlin.htm, which is linked to from https://en.wikipedia.org/wiki/Value_noise as an example of a misinterpretation of Perlin noise. My whole world is turned upside-down (EDIT: Although, based on https://en.wikipedia.org/wiki/Talk:Perlin_noise#Common_mistakes, it seems like this mistake was hard to avoid, and that perhaps Perlin noise is ill-defined. Either way, I have never gotten familiar with gradient noise).
Given this revelation, I might not be qualified to make general statements like this, and I might need to simplify this part of the docs to just say what Hypermine does. However, it would be nice to have some kind of motivation to make these docs more memorable. Perhaps this is worth a TODO that someone more knowledgeable than me can fill out (or I can fill out after doing more research).
Thanks for the call-out!
That being said, I'm still not sure whether Perlin (or value) noise could be readily adapted. We would need a regular grid of points, which is not so easy in hyperbolic space, especially if we want mountains larger than a handful of nodes in size. The high frequency components are doable, but the low frequency components would need something else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gradient noise is, by design, band-limited. You're thinking of wideband noise that's usually achieved by composing many layers of differently scaled gradient noise (often termed "fractal brownian motion" for some reason), which indeed would be difficult to adapt.
docs/world_generation.md
Outdated
|
|
||
| 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. | ||
|
|
||
| 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
editorial:
- "into this planes" is probably not what you intended.
- "value offset" feels like reinventing the term "delta" more obscurely, but maybe that's just me
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First bullet point: Whoops. Good catch. This is why peer review is important (I proofread it, but I think I just read it as "into these planes", which is what I should have written)
Second bullet point: I like the idea of "delta". In an earlier draft, I had thought of using "temperature" as a concrete example of the noise function, but after I wrote out more of the docs, I found that it wasn't as helpful as I expected, so I switched from the word "temperature" to "noise value". However, I do like the brevity of "noise delta" instead of "noise value offset".
|
|
||
| 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. | ||
|
|
||
| Finally, for each voxel, add a random offset to its noise value, drawn independently from some distribution. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might bear mention how we seed that randomness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I decided to create a separate section for this.
docs/world_generation.md
Outdated
|
|
||
| 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. | ||
|
|
||
| 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do we seed that randomness in a way that doesn't depend on traversal order?
docs/world_generation.md
Outdated
| * Generate a `ChunkParams` for each chunk, based on the `NodeState` of all 8 nodes adjacent to the chunk's origin | ||
| * Using the information in `ChunkParams`, asynchronously generate the voxel data for each chunk | ||
|
|
||
| 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`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe mention why we bother making this async?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is definitely a tricky one. I'm still trying to figure out the exact scope of this particular page, and some detail like this could be put into its own section, one that talks about how the graph is managed and maintained. It probably can't hurt to add some redundancy, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively, we could avoid talking about how/when the computation takes place entirely in this section, and focus on the theoretical model.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll get rid of the "Implementation details" section. Once other pages of documentation are added, it should hopefully be clearer where this information should belong.
|
The required checks should be in the repository settings, which you should have access to. Disabling that would be a fine expediency, IMO. |
For some reason, when I go to https://github.com/Ralith/hypermine/settings, I just get a 404. I believe it might be because I'm a "contributor" and not an "owner". |
|
Thank you for the feedback! I will work on updating the wording based on all your suggestions. |
aafc5a5 to
60c51ad
Compare
Rather than writing documentation in order, my plan is to prioritize what I believe to be the least-well-understood parts of Hypermine that I am familiar with. I also plan to write it in Markdown with TODO placeholders for images, as I believe that this would be the most efficient way for me to write the most essential parts of the documentation with the fewest distractions.
If/when it's time to add visualizations, I (or whoever adds them) should hopefully be able to focus entirely on the "how" rather than the "what".
As this documentation is meant to be understandable, please let me know if any parts of it are confusing, and I can try to reword it (or if it is fundamentally difficult to understand, I can try to make sure that there are enough placeholder images).