Most of my projects work by executing a piece of code for every single pixel on the canvas. This includes my Perlin noise terrains, where I use a noise function to find the exact terrain height at every single spot. This leads to quite long render times, but it also places some limitations on the sorts of graphics one can use. For instance, it is very difficult to draw contours with a specific width.
In the above picture, contours are highlighted within a specific band, say the band of 0.5 being highlighted by values between 0.49 and 0.51. However, you can see that the width of such bands vary quite a bit, such as with flat mesas.
Today, I want to share a pseudo-code approach to finding these contours as a set of points without having to iterate through every pixel of the canvas. The result looks a bit like this:
Compared to before, the contours are very clean and keep the same width. Furthermore, it is easy to colour in the area within a certain contour line. This is because we get a list of points making up the contour instead of the area itself.
The algorithm works with the following steps:
- Pick a random point
- Move this point until you find a position in the noise with the desired value. In this example, we move directly up until we find a value sufficiently close to 0.6
- Since we know that down is away from 0.6 and up is towards 0.6, the given point must be part of a contour line seperating "not 0.6" from "actually 0.6". This contour line cannot be vertical. Thus, there must be a point somewhere to the right that lies on the same contour line, as well as one to the left.
- We will find this point to the left by checking positions if we were to move in X direction until we find this point which lies on the contour line. This X direction starts at -90° (downwards, towards "not 0.6") and is increased slowly towards 90° (upwards, towards "actually 0.6") until we find the next point with close to the same value.
- We jump to the next point, keeping track of where we have gone.
- Repeat step 4-5, only now instead of starting at -90°, we start at the last direction used minus 90°. This allows us to step by step moving along the contour line until we return to where we started.
- Terminate as you once again are within step-length of the first point.
- Draw some shape using the list of points.
So all in all, it's not too complicated. There are however a few complications when you want to draw several contours in the same picture, though these have less to do with the method and more to do with general problems of geometry.
- As you trace out a shape, it usually should be filled on the inside, but sometimes on the outside.
- There is no telling how large a shape might be. It might be a tiny island or a gigantic ocean much larger than the screen.
- It is easy to draw a contour that then obscures other contours inside of it.
- It is easy to accidentally draw the same contour twice with slightly different paths.
- Actually finding out what places contours are to be rendered and in what order is a bit of a issue.
As usual, code can be found on OpenProcessing, though it's not particularly clean.
I have the same problematic, I created terrain height maps from perlin noise and wanted to create that old-timey look with simply a red line for contour.
ReplyDeleteI found it easier to color white everything above a value (let's say 0.6 as per your example) and black everything under it; then use gimp to detect contour, and Bob's yer uncle.
But thit seems really nice and I can embed it in my app. I love it. Thanks!
Good tech. I have often done this by looping over each pixel and coloring it if any of its neighbors cross a contour boundary, but employing random points+searching is an interesting approach! I wonder what the performance differences would be, and how many points are needed to capture the desired contours with confidence.
ReplyDeleteI do encourage readers to see my article for why we shouldn't default to Perlin in our usage (and teachings!) of gradient/coherent noise: https://noiseposti.ng/posts/2022-01-16-The-Perlin-Problem-Moving-Past-Square-Noise.html#breaking-the-cycle
Indeed Perlin has shape-rectangularity issues in its canonical form, and this technique here should really be seen as working on any noise. Simplex-type noise and specifically Domain-rotated 3D+ Perlin are better choices for noise in a general technical sense.