River systems

Now it’s a good time to draw some rivers. As we already have a heightmap we do not need to fabricate rivers out of nothing and will calculate river systems based on precipitation drainage.

The first step is to build a precipitation model. Frankly speaking, we can omit this step and consider that each cell has the same precipitation. It will be enough to create plausible rivers, but having a precipitation model is useful not only for rivers calculation, but also for biomes and erosion modeling.

The most interesting precipitation model I know is the Wind model presented by Scott Turner. Here he shows how it can be easily used for biomes definition and here disclose a few info regarding rivers. Despite this model is cool, it’s a bit over-complicated for a landscapes created via blobs approach and requires more detailed map.

Blocage_air_froid_animation

Let’s keep it simple. Sun evaporates water from ocean into atmosphere. Driven by winds water vapor precipitates out on saturation. As a result of orographic lift vapor cannot pass uplifted areas, so the greatest precipitation falls on the windward slopes of the hills as shown on the .gif (it’s so cool that I had to copy it from Commons). Leeward side on the side usually remain dry and form a rain shadow. This is especially important given the fact we are going to set a prevailing winds and not allow wind to significantly change its direction during precipitation modelling.

The code I use is pretty straightforward. I select the narrow line of sites along the map border to represent a rain clouds. Selected border depends on prevailing winds, so for westerlies I will select the left side. Then I move the ‘clouds’ one by one towards the opposite border with a random side walk. Passing over the land ‘clouds’ precipitates out losing some of their initial precipitation. Facing with mountains, ‘clouds’ disappear giving away all moisture to the land. Move each ‘cloud’ while precipitation is more than zero or opposite border is not reached. In the case there are two or more prevailing winds, just divide the initial precipitation by the prevailing winds count and repeat the logic for each particular wind.

fluxmap - west winds
Precipitation map with west prevailing winds. Bluish cells are more moist

The resultant precipitation map is rough, but it points on both humid and arid zones and ready to be used for further calculations. We can smooth it by taking the cell’s precipitation as the average of the values of its neighbors.

The next step is river paths calculation. For each cell we need to define a lowest neighboring cell. This will allow us to trace the flux routes over the map and drain all precipitation into an ocean or lakes.

Depressed cells
Depressed cells marked black

There is a problem with the cells that are lower than all of its neighbors. We cannot just ignore this problem and definitely need to solve it, i.e. to apply depression filling algorithm. There are a lot of filling algorithms, including ones used by professional hydrologists, but, as usual, I am going to use the easiest one I can imagine. The idea is to loop through the cells, detect depressed ones and add a bit of height to them. Then sort the cell to start from the highest and repeat the depression fill cycle until all depressions are resolved.

function resolveDepressions() {
  console.time('resolve depression');
  // Filter only land cells
  land = $.grep(polygons, function(e) {
    return (e.height >= 0.2);
  });
  var depression = 1, minCell, minHigh;
    while (depression > 0) {
      depression = 0;
      for (var i = 0; i < land.length; i++) {
      minHigh = 10; // overestimated value
      land[i].neighbors.forEach(function(e) {
        if (polygons[e].height <= minHigh) {
          minHigh = polygons[e].height;
          minCell = e;
        }
      });
      if (land[i].height <= polygons[minCell].height) {
        depression += 1;
        land[i].height = polygons[minCell].height + 0.01;
      }
    }
  }
  land.sort(compareHeight);
}

function compareHeight(a, b) {
  if (a.height < b.height) return 1;
  if (a.height > b.height) return -1;
  return 0;
}

Does it effective? No. Does it work? Yes.

Precipitation routes
Water flux

Now we are ready to calculate water flux. The cells array is already sorted, so highest cell will be taken first. Pour the precipitation to the lowest neighbor, then do the same for all of the cells. Following lowest cells collect more and more precipitation. When precipitation exceeds the threshold value (e.g. 3) we consider the cell as river. Without threshold all the map will be covered by rivers and this looks messy.

To store the river cells we use array, pushing to it not only cell coordinate, but also river id (just an integer) and type. For newly stated river segments type will be “source”, for lasting it will be “course”.

At the end of calculation for each cell we got not only precipitation, but also a flux value. Precipitation shows initial amount of water (moisture) considering its spread by rivers. I’m going to use it for biomes definition. Flux shows collected amount of water and could be used to draw rivers more precisely.

Interesting moment is when precipitation pours into the cell that already has a stated river. In this case we re-write existing river, but only if new river is longer than current one. So, we can separately track both the main stream and the tributaries.

One more interesting moment is pouring the water into ocean and lakes. In case there are two or more possible “pour” cells and flux is rather big, we will generate river delta. If no, mark the pour element in array as simple “estuary”. Deltas are just furcations of the pour element for each possible “pour” cell. Maybe they look not very good in my implementation, but… I like it.

function flux() {
  var riversData = []; // array to store river elements
  var id, oposite, edge, ea, xDiff, yDiff, riverNext = 0;
  for (var i = 0; i < land.length; i++) {
    var index = [],
      peak = [],
      pour = [],
      id = land[i].index;
    cell = diagram.cells[id];
    cell.halfedges.forEach(function(e) {
      edge = diagram.edges[e];
      ea = edge.left.index;
      if (ea === id || !ea) {
        ea = edge.right.index;
      }
      if (ea) {
        index.push(ea);
        peak.push(polygons[ea].height);
        // Define neighbor ocean cells for Deltas
        if (polygons[ea].height < 0.2) {
          xDiff = (edge[0][0] + edge[1][0]) / 2;
          yDiff = (edge[0][1] + edge[1][1]) / 2;
          pour.push({
            x: xDiff,
            y: yDiff,
            cell: ea
          });
        }
      }
    }) min = peak.indexOf(Math.min(...peak));
    min = index[min]; // Define river number
    if (land[i].flux > 3) {
      if (!land[i].river) {
        // State new River
        land[i].river = riverNext;
        riverNext += 1;
        riversData.push({
          river: land[i].river,
          cell: id,
          x: land[i].data[0],
          y: land[i].data[1],
          type: "source"
        });
      }
      if ((land[i].flux >= polygons[min].flux) && land[i].flux > 3) {
        // Assing existing River to the downhill cell
        polygons[min].river = land[i].river;
      }
    }
    polygons[min].flux += land[i].flux;
    if (land[i].flux > 3) {
      if (polygons[min].height < 0.2) {
        // pour water into the Ocean
        if (land[i].flux >= 30 && pour.length > 1) {
          // River Delta
          for (var c = 0; c < pour.length; c++) {
            if (c == 0) {
              riversData.push({
                river: land[i].river,
                cell: id,
                x: pour[0].x,
                y: pour[0].y,
                type: "delta",
                pour: pour[0].cell
              });
            } else {
              riversData.push({
                river: riverNext,
                cell: id,
                x: land[i].data[0],
                y: land[i].data[1],
                type: "course"
              });
              riversData.push({
                river: riverNext,
                cell: id,
                x: pour[c].x,
                y: pour[c].y,
                type: "delta",
                pour: pour[0].cell
              });
            }
            riverNext += 1;
          }
        } else {
          // River Estuary
          riversData.push({
            river: land[i].river,
            cell: id,
            x: pour[0].x,
            y: pour[0].y,
            type: "estuary",
            pour: pour[0].cell
          });
        }
      } else {
        // add next River segment
        riversData.push({
          river: land[i].river,
          cell: id,
          x: polygons[min].data[0],
          y: polygons[min].data[1],
          type: "course"
        });
      }
    }
  }
}

When all precipitation is poured, we can restore river paths from array and draw the rivers.

function drawRivers(riversCount) {
  var dataRiver, x, y, line;
  x = d3.scaleLinear().domain([0, mapWidth]).range([0, mapWidth]);
  y = d3.scaleLinear().domain([0, mapHeight]).range([0, mapHeight]);
  for (var i = 0; i & amp; lt; riversCount; i++) {
    dataRiver = $.grep(riversData, function(e) {
      return (e.river == i);
    });
    if (dataRiver.length > 1) {
      if (dataRiver.length > 2 || dataRiver[1].type == "delta") {
        line = d3.line().x(function(d) {
          return x(d.x);
        }).y(function(d) {
          return y(d.y);
        }).curve(d3.curveCatmullRom);
        riversShade.append("path").attr("d", line(dataRiver));
        rivers.append("path").attr("d", line(dataRiver));
      }
    }
  }
}
River system 1
Close look to a river system

Let’s take a look on the result. Not bad, but there are two weak points. The first, rivers should be more meandering. The second, rivers have the same width throughout course. We usually thing that river should have increasing width from source to mouth.

As meander is a bend in a sinuous river, we need to add some sinuous bends. We parse points of all rivers and add new intermediate points between the existing ones. To make rivers plausible we randomize new points a bit.

curved rivers comparation
Comparison between initial (red) and meandered (blue) rivers

After some testing I decided to add two intermediate points to each river segment, each point with a significant deviation from a straight line. If first added point has ‘left’ shift, the second will be shifted ‘right’, but with the same deviation as the first point. Please also note that all points, both existing and new, should be pushed to a new array in a correct sequence. The resultant line looks like sine wave, but not so regular and hence more plausible.

The second problem is more complex. SVG doesn’t allow to vary path’s stroke-width. The only acceptable variant I know is to split river path into small segments and draw each segment separately with different stroke-widths. To split the river I draw river path with D3 curve interpolation into svg defs first. Then I get its path via getPathData(). As it’s new API we have to include the path-data-polyfill.js as external resource.

Technically it’s all, we just need to draw segments applying different stroke-widths. But there is a big dilemma on how to calculate the segment width. There are some variants: set stroke-width as a square root of segment’s flux (Amit Patel’s variant), vary river width depending on both flux and slope (Scott Turner’s variant) or on its length only (my variant).

Close look to amended river
Close look to an amended river system

Why I decided to use this third variant? It’s easy and it guarantees that width will increase smoothly, which is critical in case of interactive scalable map. We cannot allow rivers to change their width drastically as they will be looking ugly on a big scales. Of course, we can split each segment into even a smaller ones, but this requires more resources and map will lag on dragging.

That’s all for today. The next post will (I hope) cover map styling. As always please fill free to use the JSFiddle playground, ask questions, amend the code and suggest changes. Please also do not hesitate to point me on spelling mistakes.

Advertisements

2 thoughts on “River systems

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s