Polygonal rivers and river editor

Even I’ve already wrote two posts about rivers and got a positive feedback, rivers are still a weak point and need to be re-worked. First thing you notice when you start to edit maps in vector graphics editor is that rivers are just a bunch of separate curved segments with different width. I had to use this trick as currently svg does not allow lines to have variable width. But there is a better way to do it — consider rivers not as strokes, but as polygons filled with color. Unlike lines, polygons can have any shape and it’s not a problem to make polygons looking like narrowed rivers.

Polygonal rivers

Segmented river
Segmented river

Sounds like a complex change and I did not dare to try it until the recent time even having a perfectly understandable description by Scott Turner. But in reality it’s pretty easy and took just about an hour to implement a new algorithm. The general idea is to loop through the the river points, calculate a normal for each point to place two new points with a desired offset along the river course, add these points into two separate arrays, merge the arrays and draw a polygon. Sounds confusing, so I will add some explanations below.

First steps remain almost the same — we have to get a set of points for each river and then amend it in order to add more points and make rivers more meandering. Then we interpolate the points into a curve using D3 curve interpolation. Now we can get a river length using getTotalLength() method.

polygonal rivers 1
River points

Having the total length and basic river path we can loop through the length to get point along the river, calculate a normal for this point and place new point on each side of the river with the same offset. The offset value depends on the current length and hereby river is getting wider over its length. To make it looks natural I advise to use a hyperbolic tangent function.

polygonal rivers 2
Offset calculated for each point
 var riverLength = river.node().getTotalLength();
 var riverPointsLeft = [], riverPointsRight = [];
 var widening = 200; // default value
 for (var l=0; l < riverLength; l++) {
   var point = river.node().getPointAtLength(l);
   var from = river.node().getPointAtLength(l - 0.1);
   var to = river.node().getPointAtLength(l + 0.1);

   var angle = Math.atan2(from.y - to.y, from.x - to.x);
   var offset = Math.atan(l / widening);

   var xLeft = point.x + -Math.sin(angle) * offset;
   var yLeft = point.y + Math.cos(angle) * offset;
   riverPointsLeft.push([xLeft, yLeft]);

   var xRight = point.x + Math.sin(angle) * offset;
   var yRight = point.y + -Math.cos(angle) * offset; 
   riverPointsRight.unshift([xRight, yRight]);
 }

At the end of the loop we should get 2 new arrays: riverPointsLeft containing points for the left side of the river from its source to mouth and riverPointsRight containing points for the right side in opposite order (from river mouth to its source). This will simplify polygon creation.

One more thing to notice. Let’s say the river TotalLength is 90.9. In this case the last loop iteration will at the point at length 90, so the rest of the river course will be lost. It’s not good as the last river segment is usually pretty important one. To fix this we always need to add this last point into both arrays. To do this we can just add an extra loop iteration for the point at river.node().getPointAtLength(riverLength).

polygonal rivers 3
Left and Right arrays rendered separately

The next step is to interpolate each array into a curve separately and then unite two curves into a single path. And that is all. A result is a good-looking single-segmented river:

polygonal rivers 4
Completed polygonal rivers

As you may remember I had a separate routine to deal with river confluences. It was not ideal and effect was really subtle, so I’m not going to add it to a polygonal rivers implementation. The only thing I have added is a confluence check. For each river point we check whether the point is a river confluence. If so, we add an extra-width to the river depending on the confluence “volume”.

River Editor

To make my maps interactive I’m implementing tools that allow user to edit map elements. On any river click River Editor is getting opened and red dots showing the river core points are getting appended. You can either drag a river entirely or move a selected point. River’s path will be recalculated automatically.

River editor
River Editor interface

There are some tools available:

  • Resize — ability to rotate and re-scale the river
  • Regenerate — regenerate river based on core points, the river width will be randomized so the button could be used to change the river width
  • Add point — click on the map to add new river point and re-draw a river.
  • Remove point — click on a river point to remove it and re-draw a river
  • Copy — copy river and place near the selected one
  • Remove — remove the river (the action cannot be canceled)
  • New River — click on the map to create a new river and define a river course. Click on added point or river course to finish the river creation

So user can edit existing or create new rivers. I does not affect graph structure, just a visual change and hence there are no any restrictions here.

Thank you for your attention, that’s all for today. I hope I will manage to deploy some new tools soon.

9 thoughts on “Polygonal rivers and river editor

  1. Really nice work! Ready for the next challenge … waterfalls … Highly strategic points in a map (can’t cross with a boat) and dependent on the steepness of the height map. Also waterfalls generally occur in smaller rivers / streams, where a big river hasn’t existed long enough to erode the surroundings. Which leads to another river / mountain related feature … canyons …

    All in all, have fun 😉

    Liked by 1 person

    1. Hi! Thanks for the response!

      Really good idea, I like it. Generally I have a separate section in my to-do list for that kind of ideas, called “Points of Interest”. Each POI will be marked with specific icon. It’s not a secret, so I can list main POIs I’m planning to add soon: lighthouse (place near harbors on ocean routes), mine (place in mountains, need to invent a resource system before adding that), tavern (place on crossroads). I will add waterfalls to the list, but I need to say that POIs is not a priority task.

      Like

  2. This is fascinating.

    I remember an old fractal landscape generator had a one-click river creator; you specified where the river rose and it used the heightmap to find out where it went, even creating upland lakes if necessary. I wonder how feasible a similar approach would be here. In theory it could even choose how the river width changes based on some sort of catchment basin heuristic also using the heightmap.

    Like

    1. Hi! This is pretty easy to implement, but without lakes as of now. River width is based mainly on river length, it’s the best way to render rivers as some complex calculations looks not so good.

      Like

      1. Yeah, I started thinking about the biome and rainfall calculations and how they could be used to generate runoff water on a per-polygon basis, propagating downhill until one poly had enough to start a river and then using those flow figures to dictate width, but that’s a lot of deep calculations that would need fine-tuning and the output probably wouldn’t as be aesthetically pleasing as the current heuristic. Not to mention that flow is not the only factor in river width; slope, rock/soil type etc. also affect it.

        Although I guess a smaller change might be to adjust how deep the bends are based on elevation, on the assumption that higher up is rockier and the river faster.

        Like

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 )

Google photo

You are commenting using your Google 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 )

Connecting to %s