4
\$\begingroup\$

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:

  1. Create a height map (standard x-y coordinates with a (double) z-height.
  2. Generate a moisture map (this is just done through noise with a bit of tweaking).
  3. 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.
  4. 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.
  5. 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; }
    }

}}

Pictures: Watersheds greater than 20

Note: The thicker dark lines are just topographical separations

Gradient of normalized watersheds

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.

My distorted map

\$\endgroup\$
  • \$\begingroup\$ Off topic but, cool maps! If this is is for some kind of strategy game, I love the choke-points the generator seems to create \$\endgroup\$ – Niels Aug 4 '16 at 10:53
  • 1
    \$\begingroup\$ @Niels Yeah I am planning on using it for a strategy game. If you want to make maps like it, I generated it with Perlin noise multiplied by a weak Ridged Multifractal using the Flowpowered Noise library. However, in the second version I'm working on now, I'm looking into other libraries to use, since Flow didn't give me all the options I wanted (difficult to make a heat gradient and other things). I'm planning on eventually posting a tutorial on how I went about the whole process, so stay tuned! \$\endgroup\$ – Nathaniel D. Hoffman Aug 16 '16 at 17:10
4
\$\begingroup\$

I think your problem is mostly a matter of scale. Your height maps are way too undetailed to do anything but shoot straight out to sea. They are more like small islands than entire vast continents.

You would have to make them many times more detailed, then use some noise to distort them a bit more, to get some interesting rivers.

Then I would take the final result and also distort that. IE artificially add in more bends and intentionally raise a few terrain areas to force the river around.

I think if have a good start you just need to have more data and more code tweaking to get results that make sense for vast tracts of land.

\$\endgroup\$
  • \$\begingroup\$ How would you recommend scaling the noise? I've been playing with the idea a bit, and I'll try to post some pictures of what the results have been. \$\endgroup\$ – Nathaniel D. Hoffman Aug 1 '16 at 3:59
  • \$\begingroup\$ I think the important thing is to have more detailed starting maps. You can use perlin noise at any level you want after that. I'd do it on both the initial height map data til you have something that seems "interesting" and then on the resultant river data. \$\endgroup\$ – Yudrist Aug 1 '16 at 4:02
  • \$\begingroup\$ I think what I need to do then is to add a level of distortion. Simply scaling the map and raising the frequency to match still just produces very bland hills. I'm going to try a fractal distortion and see what that gives me. \$\endgroup\$ – Nathaniel D. Hoffman Aug 1 '16 at 4:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.