I promise, I have read through the 100 other posts concerning "adding rivers to procedurally generated terrain" but I have a different problem than they typically address.
I've been reading through some literature on the subject of generating watercourses such as rivers, particularly this:
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.18.8451&rep=rep1&type=pdf
(If you can't view the link, it isn't that important to the question, but it helps you see where I'm starting)
I've developed the following method of generating rivers:
- Create a height map (standard x-y coordinates with a (double) z-height.
- Generate a moisture map (this is just done through noise with a bit of tweaking).
- Create "drip paths":
- A drip path of tile "t" is the path a drop of water will flow when it is placed on "t". This is a pretty typical way people generate procedural rivers.
- Water flows from a tile to its lowest neighbor.
- A drip path ends at water or a local minimum.
- Create a (double) variable for each tile called "watershed".
- For each point in the landmass, iterate through its drip path, adding the moisture level of the beginning tile to each tile in the path. This ensures that rivers won't ever spawn in deserts, but can certainly run through them.
- Rivers can be generated using these watershed values.
The last step is the step I'm struggling with. The only method I've been able to think of to use these values is to just define a river as anything with a watershed greater than X, where X can be a number I define. This works, but creates very straight, boring rivers. It also occasionally generates big blocky patches on things like cliffs, where the watershed is large because every point on the cliff is included in each watershed value.
Below, I will (try) to post two images I have created. The first is a generated world with rivers in black corresponding to tiles with sufficiently high watershed values. The second is a gradient map of (normalized) watershed values, where "black" is high values and "white" is low values. You will notice that it is mostly white, because a few tiles have extremely large watershed values (which I hope I can attribute to wider rivers).
My specific question is how, if it is possible, can I generate realistic rivers from these values? I'm pretty sure it can be done, but if there is some fundamental problem with the way I'm proceeding, let me know. Additionally, I have posted the code to my GenRiver class, which contains all the basics of the generation process described above. My actual world generation class is probably too large and complicated to post here, which I know is probably bad programming on my part. If you want any of that code I will gladly try to share it.
My final concern is that the way I'm actually generating terrain is what's accounting for these boring rivers (the terrain isn't bumpy enough). If this is the case, can someone recommend a way I add small detail to my terrain without changing the overall map shape? I'm using a basic 3D Perlin noise generator for the height map, with a frequency of 0.8, persistence of 0.5, lacunarity of 2.0, and I believe 6 or 8 octaves. Additionally, it is tiled along the x-axis.
Thank you for taking the time to read through my question. If you need more clarification, feel free to ask.
GenRiver.java
public class GenRiver extends Generator {
@Override
public String getName() {
return "River";
}
@Override
public void generate() {
MapGenerator.landGroups.stream().forEach((TileGroup tg) -> {
tg.tiles.stream().forEach((t) -> {
Watershed.drip(t);
});
});
MapGenerator.landGroups.stream().forEach((TileGroup tg) -> {
tg.tiles.stream().forEach((Tile t) -> {
if (t.watershed > 20) {
GenRiver.this.tiles.put(t, new BiomeRiver(0)); //Ignore the BiomeRiver part, it's just putting the tiles in a list for now.
}
});
});
}
public static class Watershed {
public static void drip(Tile t) {
boolean minimum = false;
double m = t.moisture;
do {
t.watershed += m;
int i = getRiverDirection(t);
switch(i) {
case 0:
minimum = true;
break;
case 1:
t = t.Right;
break;
case 2:
t = t.Top;
break;
case 3:
t = t.Left;
break;
case 4:
t = t.Bottom;
break;
default:
break;
}
} while(!t.isWater && !minimum);
}
private static int getRiverDirection(Tile t) {
double a = t.z-t.Right.z;
double b = t.z-t.Top.z;
double c = t.z-t.Left.z;
double d = t.z-t.Bottom.z;
//Higher positive numbers mean a sharper drop
double e = Math.max(Math.max(a, b), Math.max(c, d));
if(e < 0) { return 0; }
if(e == a) { return 1; }
if(e == b) { return 2; }
if(e == c) { return 3; }
if(e == d) { return 4; }
else { System.out.println("PROBLEM CHILD"); return 5; }
}
}}
Note: The thicker dark lines are just topographical separations
UPDATE:
I have multiplied my height map by a ridged multi-fractal (Frequency 0.8 with 8 octaves) and achieved the following image. As it turns out, what I technically want is shorter river blocks, since that means my terrain has more variation. I believe my question has been answered, but for anyone who sees this post and wants to know what my plan is, I will be generating rivers by finding the biggest watershed values and eroding paths that take me to a specified height level. The artificial erosion will be based on the moisture of the area (its watershed value). I will essentially be following a path until it reaches a minimum, determine if that minimum should end in a lake, and if it does/doesn't, if that river should continue past the lake, at which point I will erode a path that leads me to the highest watershed. I believe that by doing this I can generate lakes above sea level that will either hold (if they are in landlocked or mountainous terrain) or break into another river.
I will continue to play with the distortion values until I get pretty terrain, but for now this will do quite well.