Welcome to the first article in my series on how the Blood of the Mage map bot works. If you haven’t seen it yet, you can find the bot on twitter at @BloodOfTheMage. Every day at 7am PST, this bot tweets out a new procedurally generated fantasy world. The project underlying this bot is called Fantasy Generator, or FanGen for short. I’ve been working on FanGen since April 2017, and it remains a work in progress.
For the first article in this series, I thought I’d focus on a single, seemingly simple aspect of the generator: Mountains. There’s a lot going on when a FanGen map is generated, from height maps generation, erosion, water basin detection, and a whole host of other processes. Mountain rendering, however, is a straightforward example of how things work, and is illustrative of the design and goals behind the entire project, so let’s start there.
This article deals specifically with how I render individual mountains. Future articles will deal with their placement on the map, but let’s keep it simple for now.
Before getting into the details of drawing the mountains, let’s first take a look at the guiding principles of the FanGen project. These principles inform all of the design decisions made when adding a new feature.
- Deterministic: While FanGen creates a random world each time, it should generate the exact same output if re-run with the same random seed.
- Hand-drawn style: Maps generated should not look realistic or 3D rendered. Instead, the goal is to produce something that would look at home in a J.R.R. Tolkein or George R.R. Martin book.
- Highly Scalable: Maps should look good as the resolution scales up. Glyphs and icons should look good when they are made much bigger.
- Generation and Rendering Decoupled: The generation of the map data (height maps, water flow, lakes, etc) should be decoupled from the rendering of the map. In this way, different rendering techniques can later be applied to the same generation algorithm.
The first step in generating mountains, or really anything in FanGen, is to find suitable reference to provide a visual target. I have been using Jared Blando’s excellent book How to Draw Fantasy Art and RPG Maps: Step by Step Cartography for Gamers and Fans as my goto reference for this project. This book is great because it has a very nice, simple style, and it provides instructions and insight on how to create the various glyphs and icons. I highly recommend this book, whether you’re interested in drawing maps yourself, or in generating them.
With Mr. Blando’s permission, I’ll be using this image from the book as my main point of reference for these articles.
Now that we have our reference, let’s try to figure out how to break the generation of these mountain glyphs down into discrete parts. Decomposing the glyphs above, we can see:
- Mountains are generally upside-down V’s
- The lines that make up the V’s are darkest at the point, and lightest at the base
- The sides of the V are subtly rounded
- Sometimes one or both of the sides of the V has a small bend in it
- The peak of the V is near, but not exactly at the center of the mountain
- The interior of each mountain consists of a set of lines which follow the contours of the V’s near the outside, blending into a straight vertical line beneath the peak
With these rules established, we can create an algorithm to draw a mountain.
Drawing the Outline
In an attempt to decouple the logic which places glyphs from the logic which renders them, FanGen determines the position, width, and height of each feature before rendering. This means that the most basic parameters provided for every mountain is its position, and a bounding rectangle. Given this rectangle, we can trivially meet condition #1 and render an upside-down V.
The gray rectangle shows the bounding box, and the mountain is rendered on top of it. The red dot represents the position of the mountain as determined by the placement algorithm. Given our basic parameters (position, height and width), we’ve rendered ourselves a mountain. Unfortunately, it’s a pretty boring mountain. Let’s start adding the other conditions to make it more interesting.
The next condition states that the mountain stroke should be a little lighter at the the base than it is at the peak. The FanGen tool has a concept of a mathematical function which can be applied in a variety of contexts. A “function” is really just an x-y mapping that will return a y value for a given x. The line drawing procedures can use a function to alter the width of a line along its length. This imitates an artist applying variable pressure to their pen/digital stylus as they draw a line, and should give us the results we want.
Functions can be defined in a few different ways, but for this we will use a simple x-y mapping. We define a few points, and the code interpolates between them. What we want here is to start the line at 75% of its full width, and then increase to the full width halfway along.
Alright, still boring but at least it looks a little better. Let’s get to the fun part: making the sides actually interesting. Our next condition requires us to round the sides of the mountain. Studying the reference material shows us that the sides are usually concave, but sometimes a little convex. Additionally, the perfect 90 degree point at the top of our mountain doesn’t look quite right. It seems like it should be rounded, even if only slightly.
So how do we accomplish this? Well the FanGen project has Bezier curves in its line drawing library. Bezier curves are parameterized smooth lines that are constructed with end points and control points. Depending on their placement, you can make most curves you want to draw using Beziers. See the Wikipedia page for a complete explanation. The short version is this: We can use a cubic bezier curve on each side, using one point to round the side and the other to round the peak.
Where should we place our points? We want the sides to be slightly rounded. We want them to usually be concave, but sometimes convex. We want the peak to always be slightly rounded. We also want to randomly place the control points such that all of our mountains look good, but no two look the same. The peak rounding can be accomplished by placing the peak control point at the same y coordinate as the peak endpoint, and moving it horizontally towards the edge a small amount. The side curve can be accomplished by selecting an end edge point using the following algorithm:
- Select a point along the edge at a random point between 30% and 70% of the way from the base of the edge to the peak
- Calculate the vector from the position of the mountain (bottom center) to the selected point
- Scale this vector by a random value between 0.8 and 1.1
- Place the base control point at the position plus the scaled vector
If the random scaling is less than 1, the side will be concave. If it is greater than 1, the side will be convex. The resulting mountains will have nice curved sides and rounded peaks.
The above image shows a mountain with both a convex and concave side. The green dots represent the control points of our beziers.
Ok, so our mountains are curvy, but even with our random sides they are all going to look very similar. Going back to conditions, we see that sometimes one or both sides have bends. This should really punch up our mountains! But how do we add these bends? FanGen takes the following approach:
- Choose a random point to the right of the peak roughly half the horizontal distance from the peak to the edge, and about a quarter of the vertical distance from the peak to the base.
- Create 2 new Bezier curves:
- One from the peak to the chosen point
- One from the chosen point to the base
- For the upper curve, set the control points to be horizontal, similar to the peak itself
- For the lower curve, round it just like we would a normal side, but with a lower peak
- Replace the right side of the mountain with these two curves
- Repeat this process for the left side, but choose a point lower than the first
This creates a nicely bumpy mountain with a low ridge on the left side and a higher ridge on the right.
With this algorithm we can now generate a wide variety of mountains. We simply decide which side algorithm to use based on the following probabilities:
- 25% chance the mountain has no bumps
- 50% chance the mountain has 1 bump
- 25% chance the mountain has 2 bumps
The only remaining issue is that the lower bump will always be on the left. This is trivially solved by assigning a 50% chance that the mountain is mirrored.
For the outline, our final condition is that the peak is near, but not necessarily at the center of the mountain. This is easy enough to deal with by applying a random, small translation to the peak at the beginning of the render. The final result is a wide variety of good looking mountain shapes.
Now that we’ve got nice outlines, we need to add some shading. Again, going back to our reference we see that the shading is accomplished by adding vertical lines which follow the contours of the mountain sides. The lines don’t extend all the way to the peak, but rather stop at a curved boundary just below, giving the impression of a round mountain. They also extend beyond the base of the mountain along another curved line.
To add these lines, we create a new curve between the two sides at 60% of the distance between the base and the peak. This curve is the top of our shading lines. We create another new curve between the two base points for the bottom of our shading lines.
Next we chose a large set of numbers between 0 and 1. This number represents the distance, from left to right, along the top and bottom curves to position our shading lines. If the number 0.1 is chosen, then we will draw a line from the point 10% along the top curve to 10% along the bottom curve. Instead of drawing a straight line, we will use the contour of the side, relaxed by an amount equal to the distance from the mountain center. That is to say, if the shading line is drawn at the 20% point, then its curves will be relaxed by 2 x 20% – 40%. In this way, a line drawn directly in the middle the mountain would be straight up and down.
We repeat this for each number in our large set, but we draw the lines with a low alpha value. This means overlapping lines will result in darker shading. We will also apply a more aggressive function to these lines so that they are invisible when they start, and quite thick at the bottom. The end result is a nicely, randomly shaded mountain.
While the mountains generated by FanGen don’t match the reference exactly, I am pretty happy with how they turned out. They look pretty nice, and they have a reasonably “hand drawn” feel. There is, however, still a lot of room for improvement. On the list for mountains is the ability to create “composite” mountains with more than one peak, and to experiment with different shading techniques.