#+title: Elevation Control
#+date: <2017-07-17 Mon>
#+begin_export html

Note: the /x/ series of posts on my site are mostly unpolished mini posts. I'm trying out a few different ideas for procedural map generation; this is one of them.

#+end_export
In my [[../1725-procedural-elevation/][previous blog post]] I experimented with elevation based on river drainage basins. It was cool, but it didn't match what we needed for our game, so I stepped back a bit and decided to work on something else. We want the game/level designer to have some control over mountains, coastlines, and rivers. The designer should be able to draw a rough sketch, and then the procedural generator will fill in the rest. We're intentionally making /unrealistic terrain/ (likely something low-poly and cartoony), but that's something for a future blog post. This week's experiment is about designer control of mountains and coastlines.
Draw @@html:⯀@@mountain ridges or @@html:⯀@@coastlines: @@html: @@
#+begin_export html

Noise:

#+end_export
Things to try:
- Draw the mountain ridges, and the system figures out the coastline. Adjust the coastline by drawing your own.
- Draw the coastline, and the system figures out the mountain ridges once you completely enclose an area. Draw your own mountain ranges to replace the default.
- Draw a partial coastline and also some mountain ridges to see how the water/land "spills" in/out of the partially enclosed area.
#+begin_export html
#+end_export
The implementation is surprisingly simple.
** Distance fields
I had tried several approaches, including medial axis, before I found something I like quite a lot. The idea is to use /distance fields/. For each point on the mesh, calculate the (graph) distance from a set of /seed points/. Here are the three distance fields for the map you drew above:
#+begin_export html

A = Mountains

B = Boundary

C = Coastline

#+end_export
The land and water were spilling over the coastlines. To fix that, I made the distance fields for the boundary and mountains /not cross the coastline/.
Distance fields are easy to calculate on a graph, using breadth first search (around 20 lines of code).
** Combining distance fields
Given ~A~ = distance-from-mountain and ~B~ = distance-from-boundary, how should I combine them? My first thought was to interpolate the nearest constraint points. Between +1 and 0 I would interpolate linearly. But I draw an enclosed "O" shape for mountains, the interpolation would be between 1 and 1, and it'd end up as a plateau. I'd prefer it to be a crater. So I searched for some useful interpolation functions.
I discovered that ~B/(A+B)~ works really well! Near the mountain ridges, ~A~ is small, so ~B/(A+B)~ is close to ~B/B~ = 1. Near the boundary, ~B~ is small, so ~B/(A+B)~ is close to ~0/A~ = 0. In between, it interpolates.
It's scale-free: if I replace distances by ten times the distance, I get the same answer. This is an important property. Without it, I have much lower confidence in the results.
*** Pairwise combination
The mountains and boundary aren't enough. I wanted to be able to draw a coastline ~C~ as well. I extended the two-point interpolation:
- If ~C~ is furthest away, use the previous interpolation with only ~A~ and ~B~ but modify it for -1 (boundary) to +1 (mountains): ~2*B/(A+B)-1~
- If ~B~ is furthest away, interpolate between coastline (0) and mountains (+1): ~C/(A+C)~
- If ~A~ is furthest away, interpolate between coastline (0) and boundary (-1): ~B/(B+C)-1~
There are sometimes weird effects with partially drawn coastlines. The boundary–mountain interpolation doesn't always match up with the boundary–coastline + coastline–mountain interpolations. The problem is that any time I introduce /if-then/ into the equation, there's a potential for discontinuity. Are there other functions that don't lead to discontinuities?
*** Harmonic mean
I looked at the [[https://en.wikipedia.org/wiki/Weighted_geometric_mean][weighted geometric mean]] and the [[https://en.wikipedia.org/wiki/Harmonic_mean#Weighted_harmonic_mean][weighted harmonic mean]]. The weighted geometric mean would return positive elevations only, so it's not useful here. The weighted harmonic mean though looks nice. For two values with elevation 0.0 and 1.0, it becomes ~B/(A+B)~, which is exactly what I had started with. For three values:
~(+1 * 1/A + 0 * 1/C + -1 * 1/B) / (1/A + 1/C + 1/B)~
- If there's no coastline, ~C~ is infinity, so ~1/C~ is 0, and this becomes ~(1/A-1/B)/(1/A+1/B)~ which turns out to be equivalent to the previous rule of ~2*B/(A+B)-1~.
- If the land is surrounded by coastline, in the interior ~B~ is infinity, so ~1/B~ is 0, and this becomes ~(1/A)/(1/A+1/C)~ which turns out to be equivalent to the previous rule of ~C/(A+C)~.
- If the land is surrounded by coastline, in the exterior ~A~ is infinity, so ~1/A~ is 0, and this becomes ~(-1/B)/(1/C+1/B)~ which turns out to be equivalent to the previous rule of ~B/(B+C)-1~.
Hey, that's pretty cool — when there's no coastline or a complete coastline, it is the same as my previous set of rules! Where it differs is when there's a partial coastline. I had discontinuities with partial coastlines, but the harmonic mean eliminates all the discontinuities. It's less code too. Nice!
*** Weighting functions
With harmonic means, there's also the potential to use different weighting other than linear: ~(+1 * 1/Aⁿ + 0 * 1/Cⁿ + -1 * 1/Bⁿ) / (1/Aⁿ + 1/Cⁿ + 1/Bⁿ)~.
#+begin_src python :exports results :results output :wrap export html
#!/usr/bin/env python3
import math
def interpolation_line(f):
points = []
points2 = []
epsilon = 1e-3
for i in range(101):
a = max(epsilon, i/100.0)
b = max(epsilon, 1-i/100.0)
e = (1/f(b)) / (1/f(a) + 1/f(b))
x = round(250 * a, 2)
y = 90 - round(90 * e, 2)
points.append((x,y))
points2.append((500-x,y))
return points + list(reversed(points2))
def diagram(label, points):
d = 'M' + 'L'.join(map(lambda p: "{:.2f},{:.2f}".format(*p), points))
print("""
""".format(d, label))
diagram("Linear: n=1", interpolation_line(lambda x: x))
diagram("Square: n=2", interpolation_line(lambda x: x*x))
diagram("Square root: n=sqrt(2)", interpolation_line(lambda x: math.sqrt(x)))
#+end_src
#+results:
#+BEGIN_export html
#+END_export
For the demo on this page I chose harmonic means with sqrt weighting. On land, it should produce more valleys and sharper mountain peaks. In the water, it should produce more shallow water and occasional deep water trenches.
*However* it doesn't actually produce that effect for the three-valued harmonic mean. :-( The sqrt weighting puts a lot more of the values in the middle, which is 0 for the -1:+1 range. That's what I want — a lot of values near 0. But for the three-valued harmonic mean, it's separately putting a lot of values in the middle of the -1:0 range (lots of -0.5) and the 0:+1 range (lots of +0.5). So it's a cute trick but it has issues.
** Features
*** Noise
One of our goals is to let the designer specify constraints, but then fill in the unconstrained areas procedurally. I tried a little bit of here with elevation. I mixed in simplex noise for unconstrained areas. Try going to the demo at the top and changing the noise slider.
I'm not happy with the results though. It may need higher resolution maps to make it work well.
*** Resolution
The lines drawn by the designer are stored as vectors, so it should be possible to apply those same vectors to a more detailed mesh to get similar landscapes. The idea would be to use the low resolution mesh to give interactive speeds for the designer to explore possibilities, then apply a high resolution mesh for the final terrain. However I haven't implemented this yet.
*** Non-islands
In this demo I've constrained map boundaries to be deep water (field ~B~), but the algorithm isn't limited to island shapes. I can generate other types of maps by setting the boundary conditions to include both land and water. Try drawing mountains over the edges of the map.
*** Different elevation constraints
Right now the algorithm only works with mountain, boundary, coastline constraints. It would be useful to be able to set /any/ elevation instead of only +1. The algorithm does *not* support that, and I'm not sure what to do about it.
There are papers listed at the end that do support more complex constraints, not only arbitrary elevations, but also constraints on slopes and second derivatives. There are some pretty cool effects from these types of constraints, but the algorithms to set elevations is much more complicated than what I wanted to implement.
** More
There are other projects that have done far more advanced things than what I'm doing here. Take a look at these:
- [[https://pdfs.semanticscholar.org/6a7e/8a731dc35fb6c9d740a9f43eb2e84672cad6.pdf][Feature based terrain generation using diffusion equation]] [PDF]
- [[https://hal.archives-ouvertes.fr/hal-01517343][Sculpting Mountains: Interactive Terrain Modeling Based on Subsurface Geology]]
- [[https://hal.archives-ouvertes.fr/hal-01519852][EcoBrush: Interactive Control of Visually Consistent Large-Scale Ecosystems]]
- [[https://people.cs.uct.ac.za/~jgain/publications/terrsketch.pdf][Terrain Sketching]] [PDF]
- [[http://hpcg.purdue.edu/pubs/2013/Genevaux2013sigg.pdf][Terrain Generation Using Procedural Models Based on Hydrology]] [PDF]
I'm looking for the /simplest/ thing that could work for our project's needs. Our maps will be low-poly cartoonish, so realism is not the goal. Previous blog posts in this series:
- [[../1721-voronoi-alternative/][Triangle dual meshes]]
- [[../1722-b-rep-triangle-meshes/][Triangle mesh representation]]
- [[../1722-delaunay-map-import/][Importing map data from an image]] (designer control)
- [[../1723-procedural-river-growing/][Generating river drainage basins]]
- [[../1724-elevation-from-rivers/][Generating elevation from rivers]] part 1
- [[../1725-procedural-elevation/][Generating elevation from rivers]] part 2
My current plan is to use the method on this page to generate /coarse/ elevation. I'll then use the algorithms I developed in the previous blog posts: coarse elevation to generate rivers, then both coarse elevation and rivers to generate detailed elevation. The level designer will sketch out some ideas and the system will procedurally generate a landscape that matches the designer's input.
#+begin_export html