World Generation Techniques: Domain Warping
Domain Warping, domain distortion, or noise distortion is an effective technique to breathe additional life into noise maps by stretching and twisting them about, adding some excitement to the otherwise fairly uniform underlying noise.
We’ll demonstrate this technique by starting with a vanilla Perlin noise map as the base, and displacing its positions along two sets of Perlin noise maps to achieve a sum greater than its parts. Let’s dive right in:
The Base Map
Start with a Perlin noise map that fits your needs. Play around with the map size and noise parameters until you are satisfied. Due to the nature of the displacement, it is useful to start with a map that is tileable both vertically and horizontally, since points will almost certainly be displaced off of the map boundaries and will need to be wrapped around.
Warp 20: Engage
We’ll need to generate two additional Perlin noise maps for displacement: one for the X values, and one for the Y. These should also be tileable to avoid any obvious seams across map boundaries, and each cell should have a value ranging from -1 to 1. If they have a value from 0 to 255, as in the ones in the demo, map them to the -1 to 1 range with the necessary arithmetic (x / 255 * 2 - 1, in this case).
The one missing ingredient is the amplitude. This describes the maximum displacement that will occur for a value of -1 or 1. Pick a reasonable value for your purposes: from 1 to twice your map dimensions. For this demo I’ve chosen 20.
Now we warp:
- Iterate through each x/y value.
- For each position, grab the corresponding value from the X displacement map and multiply it times the amplitude, and do the same for the Y displacement map.
- You now have two values, one for X, and one for Y, in the range (-amplitude, amplitude).
- These describe the x/y offset, from the current position, of the cell that will be replacing this cell in the result. So if we’re working with cell 5,10, and the offset values are -2, 9, we’ll write the value in cell 3, 19 to the result, at position 5, 10.
- We must also ensure that we handle offsets that reach across map boundaries: for our purposes, wrapping works well, though clamping may also give you reasonable results.
Note: I’ve chosen ranges for most values such that the demo produces satisfactory results. I encourage you to tinker with the source code such that you may explore what lies beyond these bounds!
Tidying Things Up
Depending on your particular needs, you may have all that you require as of this point. However, for our world generation purposes, we’ll take some additional steps to round out the edges of the map, pick a water line, and finally, assign some color values to different elevations so that we can better appreciate the difference domain warping makes.
For a quick rounding of the map’s edges, I recommend a simple gradient circle map, which multiplies values within an inner radius times 1, then linearly interpolates to 0 at the outer radius. (The keen observer may have noticed that I have moved away from the more interesting yet radically more expensive particle deposition map from World Generation Breakdown.)
To pick a water line, you may be inclined to take your max elevation after you’ve normalized your maps and multiply it times the desired water cover. However, collecting all of the map’s values in a sorted list, then indexing the value at water_line_percent * list.length will provide more accurate results, since the distribution of the noise values is unlikely to be linear. In the source code, the Map class provides a handy getElevationByPercentage() function for this purpose.
I certainly hope to see more procedurally-generated worlds taking advantage of simple-yet-effective techniques such as domain warping. The source code for all of the demos, and of course, the underlying domain warp logic, can be found on GitHub. Enjoy!