Edward Loveall

Blender Shader Contour Maps

A top-down, cartoony map of blue water, green land, and white-capped mountains. At even height levels the area is outlined.

For a few years now I’ve admired contour lines. Used topographic maps they cleanly show 3D terrain in 2D space. The idea is to draw a line at certain height intervals: 0ft, 10ft, 20ft, etc, to outline the 3D space. Not only are they useful, but I think they look amazing. I wanted to create some, and I figure out how in Blender.

First attempts

Unsurprisingly, there are techniques and software out there to do this. The Geospatial Data Abstraction Library (or GDAL) has a tool called gdal_contour that can take a raster height data from the space shuttle and create vector lines from them. This uses the marching squares algorithm which more-or-less samples small squares of the height data and creates a dense polygon tracing a height level. d3’s contour library uses this too. It can create some surprisingly good looking maps! However, it does create some not great looking jagged lines if you don’t sample small enough portions.

There’s also the conrec algorithm which seems pretty similar to marching squares but uses triangles instead of squares. It seems cool and I’m not sure what software might use it.

But then I wondered if I could make this in Blender.

Turns Out, Yes

Specifically, I was wondering if I could create contour lines in Blender’s shader node editor. Making a height map is easy enough. The Noise Texture node creates Gradient noise which looks like mildly interesting terrain, if not a bit repetitive. It creates random points between 0 and 1 and smooths them to make this:

A gradient noise field. It kind of looks like a blurry, black and white picture of cotton batting.

A blender shader node setup. A noise texture is plugged into an Emission shader

We can take take the noise and make it look a bit more like terrain:

A gradient noise field, but distinct levels have been combined to make shapes that look like flat levels of terrain on a map.

Blender nodes, where the original noise has been multiplied by 20, run through a floor node, then multiplied by 0.02. The output is then put through a Map Range node that takes a min of 0.2 and a max of 0.8 and maps it to an output of 0 to 1.

Not bad! Already, we have some basic terraced terrain that looks pretty good. We can even control the amount of terrace levels with a single number (the Value node on the left). Now how can we turn this into contours?

Edge Detection

The technique I used was to to take the image, shift if very slightly, then subtract the shifted image from the original. The pixels that become 0 are on the same level, which means whatever pixels are not 0 must be on a different level. These are the pieces of the image right on the edge!

To illustrate, I’ll make a circle and show how edge detection could work there.

A plain white circle on a black background.

Blender nodes showing a Gemometry node's position output plugged into a Distance node with a vector at 0, 0, 0. This is then put into a less than node with a threshold of 0.5 and output to the Emission shader.

If we make a copy of that circle, shift if slightly to the left (positive X), then subtract it from the original, we end up with this:

A sliver of the remaining circle that looks like a very thin crescent moon arcing on the left side.

The previous Blender nodes, but the original Distnace and Less than nodes have been duplicated. The duplicated distance now has a base vector of 0.02, 0, 0 (a slight shift on the x-axis). The two Less Than nodes are then subracted before going in to the Emission shader.

In as much as circles can have a left edge, we’ve isolated it. If we did this in four directions, we could approximate an outline. We can see this subtraction effect a little better if we superimpose the two circles together, making the original red and the shifted copy green:

Two circles almost exactly on top of each other but slightly shifted like a Venn diagram. There is a sliver of red on the left and green on the right. The majority in the middle is yellow.

The previous Blender node setup, but after each Less than node is a Combine XYZ node. The top is going into the X input, and the bottom is going into the Y to create red and green respectively. The two are then summed with a vector Add node then sent to the Emission shader.

Contoured Terrain

Now let’s apply this to our terrain. To make things a little easier to work with, since we’re going to use the terraced terrain multiple times, I’m going to put those nodes into a group with an input for the levels, and one for a vector so we can shift the noise around.

The blender nodes from the last terrace terrain step, but most nodes have been collected into a group called Terraced terrain. A value of 20 is entering in to the group. The group's output is attached directly to the Emission node.

Now we make a copy, shift it, and subtract it from the original like we did with the circle.

The terraced terrain from before, but the black areas are a lot bigger, weirdly.

The Terraced terrain node group setup, with a duplicated group. The new group has a vector Add entering into it adding 0, 0, 0 and 0.02, 0, 0 vectors. The two groups are then subtracted from one another before going to the Emission node.

Hmmm… It’s interesting, but not what we wanted. Turns out there are two problems.

First, we need to use the same vector space for both terrains if we’re going to shift one. Currently the second terrain just used a static vector of (0, 0, 0) and subtracted 0.2 from x. This effectively makes the whole noise all the same color because the input vector never changes. To fix this, we can use the Texture Coordinate node and its Generated socket to get what we originally had.

A bunch of wavy, blobby, vertical-ish lines that hint at the underlying terrain we had before.

The exact same node setup from the previous step, but with a Texture Coordinate's Generated outlet going into both the original Terraced terrain group and vector Add nodes respecively.

That’s most of it. The second thing is that subtraction worked great for a simple circle since it was only black or white; 0 or 1. However, we are dealing with lots of in-between values for the different levels of our terrain. Each level will still subtract from itself to get to 0, but if the levels are close enough they can overlap which looks weird and slightly transparent.

The solution is to add in a Math node set to Greater Than 0. That means any value that isn’t 0 will show up as white. This also combines overlapping levels to be one solid color.

The same wavy, vertical-ish lines but more solid.

The exact same node setup from the previous step, but with a Greater Than node set to a 0 threshold after the Subrtract node.

This isn’t exactly a problem, but the lines look a little thick to me. I’ll lower the amount we’re shifting by 10x from 0.02 to 0.002 to make the lines sharper.

The same wavy, vertical-ish lines but much thiner.

Now the only remaining steps are to create and subtract the three other sides, and add them all together. This can start to look a little overwhelming. Take comfort in that it’s only because there are lots of nodes and connections between them, not because this is complex math.

The process is:

Like I said, complex looking, but not complex math. Here’s a picture of the final result:

A black and white contour map, outlining hills and valleys.

The original terraced terrain, subtracted by a copy of itself shifted left, right, up, and down. Each successive pair are added together (left plus right, left/right plus up, left/right/up plus down). The final result is put into a Greater than node which feeds the Emission shader and then Material Output.

That looks pretty good to me! Now that you have this, you could then take these lines and superimpose them on the original terrain, add colors, shift the properties of the terrain to make it look different, or even add some underlying noise to the terrain itself to make different shapes. I myself made colorful terrain which you can see at the top. Enjoy!