Wednesday, February 12, 2020

Creating a Pencil Effect in SVG

Dragons Abound produces maps in SVG, which is a vector graphics format.  Vector graphics have a lot of features (such as lossless zoom) that are nice for maps.  And vector graphics are good for producing crisp, well-defined lines like an ink stroke:
On the other hand, vector graphics are not very good at producing textures with a lot of non-repeating fine detail.  In vector graphics, each drawn element is represented with a description of its size, shape, location, color and so on.  To represent a lot of fine, non-repeating detail, you need to describe lots and lots of elements.  Something like this pencil stroke
would require many tens of thousands of different elements.  Basically each little blob of gray in that image would be separately defined.  Other things, like blurred images, are even more problematic.

This is a pretty big limitation on vector graphics, so SVG has added some workarounds to let you reproduce some of these texture effects more efficiently.  I'm going to explore using some of those SVG features to create something like a pencil line.  Of course, there are many other more sophisticated approaches to replicating pencil strokes.  Academic papers have been written on the subject.  But I'm hoping to create a fairly simple filter that has an acceptable result.

As always, my preference would be to borrow or build on a filter that someone more talented than myself has already created, but in this case I find that there's a surprising paucity of vector pencil filters to use for inspiration.  (Cue ominous foreshadowing music: There may be a reason for this.)  So I'm pretty much on my own.

I've posted previously about Dragons Abound's capabilities to create “hand-drawn" lines.  This was concerned primarily with avoiding mathematically straight lines and instead creating lines with subtle variations that look more like they were drawn by a human hand.  You can see some of this in the mountain illustration above.  This works fairly well to create an ink illustration style, but it doesn't work very well to create a pencil line, because it lacks the pencil texture.

If you look at some pencil strokes, like the example above, or these:
you'll see a number of features that distinguish pencil strokes from ink lines (or computer-drawn lines).  Most importantly, they have a texture caused by the way a pencil interacts with paper.  Paper has a grain and the pencil tends to deposit graphite onto the high parts of the grain and leave the lower parts white.  Rough-grained paper results in more obvious texture; something like illustration board is so fine-grained that it imparts almost no texture.  Second, the edges of pencil lines are somewhat indistinct.  Again, this is largely caused by the irregularities in the paper and in the pencil point itself, which result in varying amounts of graphite being deposited along the edge of the line.

(Of course, there are other effects as well.  Pencil builds up on itself and so can appear darker where strokes cross.  Strokes themselves vary in pressure and this can change the darkness of a stroke along its length.  But for the purposes of this posting, I'm going to be focused primarily on trying to recreate the revealed paper texture.)

To get started, I will set up some plain gray lines:
Here I've “hand-drawn" lines that are 4, 2 and 1 pixel wide.  Unfortunately, SVG effects tend to look different when applied to different width lines, so I want to compare the effect on different sizes.

The primary capability SVG offers for adding texture effects is called filters.  Filters are applied after a vector object is drawn, and they change the way the object is presented -- like looking at the object through a filter, hence the name.  Typical filters can do things like change the color of the object, add noise to the object, etc.  Filters are a fairly complicated topic with a complex syntax, so I won't be offering a full tutorial here on how to use them, but I'll try to give enough detail to make it clear what I've done, and at the bottom of this posting I'll provide a link to a Codepen with the filter so you can play around with it yourself.

To begin, I'll try to change the edges of the lines so that they are not smooth but have irregular edges similar to paper grain.  I'll do this by moving the pixels in the line around.  The filter element that does this is called “feDisplacementMap" and it moves each pixel around based upon the values in a different image.  Since we want each pixel to move around in a random but coherent way, we want to feed noise into feDisplacementMap to control the movement.  Fortunately, SVG provides another filter element call “feTurbulence" that is specifically for producing noise.  So we can combine those two filters to roughen up the edges of the lines.
The amount and roughness of the line can be controlled with parameters to the displacement map and the noise generation.  Unfortunately, the displacement is in absolute units, rather than relative to the line size.  Here I've tweaked these to find something that looks reasonable for all the line widths, but you can see a problem when I increase the scale of displacement:
Now the displacement is big enough to start moving the entire line around, rather than just tweaking the edges.  This effect is magnified on thinner lines.  In this example, the 4 pixel line still looks mostly like the edge is rough, while the two pixel line has had obvious distortions added to it.  So I have to pick a value that doesn't create distortion in the thinner lines.

When I zoom out to regular magnification, this is what the effect looks like (after some tweaking to improve the effect):
At this scale, a lot of the rough edges become spots and speckles.  It's not an entirely unpleasing effect and is somewhat reminiscent of a pencil line.  (However, in general SVG filters seem to have scaling problems -- in many cases they look good when zoomed in but look like they've been put through a poor resizing algorithm when zoomed out.)

Here's what this looks like used to draw mountains (pencil effect to the left):

That's not terrible, but it does have some sharp artifacts that look a little odd.  And when this is applied to thin lines close together, they end up overlapping and merging:
Again, this is not entirely terrible, and the shadows in the pencil mountains end up looking very much like they'd been shaded in with pencil, if that's an effect you want.

This approach adds roughness to the outline of the pencil stroke, but it doesn't change the uniform color of the stroke.  Real pencil strokes have texture inside the stroke as well, as the graphite colors some spots on the paper more than others.

To add texture inside the stroke, I will use an SVG filter that generates noise and then combines the noise with the stroke:
Here in close up you can see how the inside of each stroke is now filled with a pseudo-graphite texture.  Here's what it looks like at normal scale:
This doesn't look too bad, particularly on the thicker lines.  Here's what this looks like used on mountains:
It is not as good at this scale.  Note that this filter also reduces the darkness of the lines; this is a natural result of adding white noise to the lines.  To some extent this can be mitigated by increasing the contrast of the noise, so that some parts of the line are made darker to compensate:
But since the line colors are already pretty dark to start with, this eliminates much of the “pencil" effect.  So if I use this, it will have to be tweaked to produce a pleasing balance.

Obviously, both of these can be combined.  This looks pretty good close up:
It has both the rough edge and the interior texture we expect in a pencil line.  At normal scale it is not quite so good:
because of the sharp artifacts introduced at this scale.

A good trick to make this texture look better is to add a paper texture to the background:
The eye now sees the texture consistently across the image.  Even at a pretty subtle level, this helps trick the eye into believe that the texture in the line comes from the paper.

Here's an example of using this filter on a map (with a paper texture):
Overall this isn't too bad, although on detailed inspection it looks as if there's simply been noise added everywhere.  At 200% artifacts are more evident:

Another way I might create a rough edge for a pencil line is to draw the line multiple times with slightly different perturbations and a lowered opacity.  In the center of the line where the multiple versions overlap, the density will be something like the original line; at the outside edges where sometimes only some of the lines are showing there will be lower opacity and less distinct edges.

The general way to do this with SVG filters is to use feTurbulence and feDisplacementMap together to create a perturbed version of the line, but to do this multiple times and then combine them all at the end with a series of feBlend primitives.  If we blend (say) three copies, we have to also lower the opacity of the lines appropriately.  (I'm not entirely certain how to calculate the appropriate opacity, but I think it might be the cube root of the luminosity of the line.)

That produces an effect like this (with three lines, closeup):
There are a couple of drawbacks with this approach.  The filter has a fixed perturbation, so it has more impact on narrower lines, and you can see that the 1 pixel line is completely separated at points.  Secondly, this is a fairly complex filter that creates three separate perturbations and merges them; this might be very slow on a big complex image like one of Dragons Abound's maps.

Here's what this looks like at normal scale:
This doesn't look exactly like a pencil outline to my eye, but it does do a better job of eliminating the sharp artifacts from the other approach.

This can be combined with the interior texture filter from above to add texture to the inside of the pencil lines, and the paper texture:
Here's what this looks like used on mountains:
This filter tends to spread out the lines more than the other filter -- effectively making them thicker.  Sometimes this generates a sketch-like effect with multiple lines that isn't entirely displeasing.

Here's an example of using this filter on a map:
This maintains the original darkness better than the first filter, and while it doesn't look exactly like pencil to my eye, it isn't entirely unpleasing.  Zoomed in to 200%:
This filter largely avoids the sharp artifacts of the first filter when zoomed in.  The overlapping lines effect on narrower lines (such as the forest) starts to look pretty artificial, but broader lines like the mountain and rivers still look good.  However, the internal noise is almost entirely lost within black lines like the river.

I have put both of these filters up on Codepen so you can try them out yourself.  You can find the first filter here and the second filter here.  I encourage you to play around with them and try to improve them -- and if you do come up with something better, please let me know!  I'd love to have a really good pencil filter!

Monday, February 3, 2020

New Mountain Style (Part 9)

In previous posts I described drawing a new style of mountains and using them to fill mountain areas on a map with a mountain chain and some surrounding mountains:
Creating the mountain chains requires (smartly) connecting the ridgeline of one mountain to the topline of the next mountain.  I realized while implementing this that I could apply the same idea to my existing mountains, which can look quite different but also have ridgelines and toplines:
So in theory I should be able to use the same approach to filling mountain areas using my old-style mountains.

To do this requires some serious re-factoring.  The mountain chaining functions need to be modified so that they take some kind of mountain object which provides all the required functionality -- including things like drawing a new mountain, moving the mountain, getting the topline and ridgeline and so on.  This is a minor refactor for the new mountain codes, but substantially more involved for the old mountain code.  The old mountain code doesn't organize the mountain pieces in quite the same way -- for example, there's no specific ridgeline (there can be none, or many).  It also doesn't support moving mountains around in the way that is required for chaining.  In the old mountain code, a mountain is moved by applying an SVG transformation.  This moves the mountain on the screen, but lines that make up the mountain don't actually change.  I need them to change so I can check for intersections with other mountains and so on.

Both the old and the new code represent mountains as Javscript structures.  I probably should refactor them both to create real Javascript objects, but for various reasons I stopped somewhat short of that level of refactoring.  Instead I have three functions I pass into the mountain chaining code (make a mountain, move a mountain and draw a mountain) and I make sure that the mountain structure itself has all the required parts.

With these changes in place I can draw the example map from above using the old style mountains.  Here's the first working image:
That's a surprisingly good/interesting result (although it looks a bit Dr. Seuss-ish).  The mountain chains are very evident, and in the parts of the mountain chains where the angle is conducive they look pretty good.  These mountains are smaller than the new style mountains, so there are many more in the mountain chain, which is most evident in the long nearly vertical stretch in the upper mountain chain.  Making the mountains bigger will help with that.  There's also a problem with how the mountains are overlapped -- the algorithm is overlapping them based upon the mountain centers, when it really should be the lowest point on the mountain.  This is making the mountains overlap too much.

Here's a revised version with those two fixes:
This is still a little busy in the vertical stretch but otherwise looks pretty good.  Here's an example in color with a different style of shading:
Note that by default the old-style mountains don't always have a ridgeline, or have one that fades out before it reaches the next mountain in the chain.  This somewhat dilutes the visual impact of the continuous ridgeline throughout the chain.  The mountain chaining code actually indicates to the mountain drawing code that a mountain is in (or not in) a chain, so I can use this to force the ridgeline to be drawn for all the mountains that are in the chain:
This makes the chains (versus the surrounding mountains) a little more visually obvious.

Here's another example with different parameters:
One thing to experiment with is the density and overlap of the mountains surrounding the chains.  This can create quite a different feel:
That's probably closer to the density that you see on a lot of fantasy maps, but it tends to conceal the mountain chains.  Something in-between might be better.
Or I can keep them all for different styles.

It occurs to me also that since the new approach draws mountain chains and then fills in around them with random mountains, if I don't draw a mountain chain and just fill in randomly, it's equivalent to the old style of mountain fill.  So I can collapse both styles into one implementation.

This completes the new mountain style.  The map that inspired this style was done in pencil:
Next time I'll make some attempts to create a pencil filter effect in SVG.

Monday, January 20, 2020

New Mountain Style (Part 8)

I've found a few problems during the previous development stages that I didn't address, so I'll try to take care of them now.

The first problem is that sometimes the range of drawn mountain sizes is too large; the largest mountains are so big they look odd on the map.  An example:
The largest mountain in the left chain is much bigger than the others and looks odd as a result.  (Although admittedly this is something of a matter of taste.  Maybe you like the look of a huge mountain anchoring the mountain chain.)  The real problem here might be that the very big mountain is mostly surrounded by much smaller mountains, but detecting and correct that situation would be difficult.  The easier way to address this is to tune the parameter that controls the maximum mountain size.  This compresses the range of mountain sizes that will be drawn.  After playing around a bit with the parameters:
This seems better to my eye.

A second problem can be seen in this screen shot:
The red lines are drawn to connect the ridgeline of one mountain to the topline of the next mountain.  But they've obviously gone wrong in the upper-left mountain chain.

The reason is that I wrote the mountain chain code to draw mountain chains top down.  In the upper-left mountain chain you can see that the green line the mountain chain is following turns to go upward where the problematic red lines begin.  You can see another problem with the last mountain in the chain -- it doesn't actually connect to the previous mountain.  In contrast the other mountain chain -- which goes downward -- is fine.

I won't run through the whole logic of drawing a mountain chain upwards, but the general idea is that instead of intersecting the topline of the new mountain with the ridgeline of the old mountain, you have to do the opposite -- intersect the topline of the old mountain with the ridgeline of the new mountain.  This turns out to be a surprisingly small code change (at least once I quit confusing the X and Y axes in the code):
Now you can see the right side of the mountain chain makes more sense, and only short red lines are needed to connect adjacent mountains.

Another problem becomes apparent when I add a land texture:
Each mountain blocks out a chunk of the map below its baseline.  I added this because when tightly packed, the ridgeline of the back mountain often peeks out below the front mountain.  But this seems to be less of a problem with this method of filling the mountain areas, so I can probably reduce the size of the masking.
That looks pretty good; no peeking problems, at least on this map.

The last problem (at least for now) that I want to address is the straight lines on the mountain sides.  In my analysis of the original map I noted that almost all the mountains had straight sides:
But straight sides don't look that good on my maps.  Reviewing the original map I think that the sides tend to have subtle curves that break up the monotony.  I can add slight curves and some more hand-drawn jitter to my mountains to try to make the mountains look less mechanical:
I've kept the effect pretty subtle here and may increase it later, but I think it helps to keep the mountains from looking so mechanical and repetitive.

Next time I'll work on developing an SVG filter to make these look more like they were drawn with a pencil.


Monday, January 13, 2020

New Mountain Style (Part 7)

In the previous posting, I created the code to fill a polygon with mountains by putting down some mountain chains and then filling in around them:
I'll now take this routine and use it to fill in mountain areas on a map.  (Small areas will still be filled in with random mountains.)

Here I'm using a black & white map style like the original map.   I've filled the main mountain areas with two mountain chains and filled in around them with some random mountains.  Overall this isn't too bad, but it does have some problems that I need to address.

Looking specifically at the mountain chains I think they're pretty good overall, but I see a couple of problems.  ( I always have outsized expectations for new features, and I'm disappointed when they don't look indistinguishable from the work of a human artist.)  First of all, the mountains in those chains are all fairly close to the same size, and that's not particularly appealing.  Partially this happens because the underlying terrain looks that way; partially it happens because very small mountains don't work well in mountain chains so they've been suppressed.  One possibility to address this is to force the chain to peak in the middle:
I experimented with this during testing, and I think it looks pretty good and helps to better define the mountain chain.  I'm a little concerned that this might become repetitive on a large map with many mountain areas, so that's something to watch out for.

Another problem with the chains is that on the left side of the main mountain chain the slope of the chain is almost the same as the slopes on the right side of the toplines and it creates a clumsy looking progression where all the mountain sides align:
A human artist wouldn't make this mistake.  One way a human would avoid this problem would be not to draw adjacent mountains with the same slopes (so that they couldn't line up).  I can implement something like this by forcing the slopes of the mountains along the chain to alternate between wide and narrow:
I was worried that strict alternation would be obvious, but it doesn't seem to be a problem.  This fix isn't a panacea.  It worked pretty well for the lower mountain chain, but note that the top mountain chain now has some similar problems.

Another approach to eliminating the collinear sides is to detect when this is happening and perturb mountains left or right to break up the visual line.  To detect collinear sides is pretty difficult, but it's fairly easy to tell when the peak of the front mountain is close to the right topline of the back mountain, and this is a good enough indicator.  When this happens, I can move the mountain away from the topline.  Moving the mountain randomly doesn't work very well; it's better to move it along the normal of the topline.
You can see that this has broken up the visual line in the top mountain chain.  The drawback of this approach is that it also tends to break up the visual continuity of the mountain chain (and may in fact require drawing an extra line to connect the ridgeline of the back mountain to the topline of the front mountain) but that's probably better than leaving the toplines aligned.

In the maps above, the angle for the mountain chains has been selected to be the same as the longest chord in the polygon that defines the mountains.  One way to have better mountain chains is to force the mountain ranges to lie at one of the angles that works well for drawing mountain chains.
This creates nice mountain chains, but they tend to be shorter than the other approach because they're not going the long way across the polygon.

The drawback with both of these approaches is that drawing the mountain chains as an angled line doesn't do a good job of defining the shape of the polygon, and creates a bunch of parallel valleys, which is unrealistic.  A better approach might be to draw a mountain chain down the “spine" of the region, and then fill in around it.

The spine of a polygon is usually called the “centerline."  It's hard to find an exact definition for the centerline of a polygon, but as the name suggests it's intended to be the longest line that runs roughly in the center of the polygon.  This page has a nice interactive example of generating the centerline of a polygon for use in labeling.  The short version is that you create a Voronoi diagram based on the points in the polygon and then find the longest path through the Voronoi edges.  This is the result for the map I'm currently using:
This works but you can see that in both polygons the line curves back around at the ends to be longer.  For the purposes of drawing mountain chains I prefer straighter lines, even if they're not the longest possible.  To select straighter lines, I have to incorporate that in the metric for picking the path.  I introduced the idea of sinuosity when placing path labels, so I'll combine that with length to pick a path:
This helps find a more reasonable backbone for the mountains.  Here's what it looks like to put down the centerline as a backbone and then fill in with random mountains:
There's something of a glitch where the line takes a sharp turn in the middle of the larger region.  In general it is difficult for the mountain ranges to follow abrupt changes of direction.  Smoothing the centerline helps with this:
The drawback is that aggressively smoothing the line means it is no longer a very faithful spine for the mountains area.  I like that this is thematically more interesting than a mass of random mountains, or than parallel rows of mountains, but I wonder if it will be problematic for larger mountain areas.

Here are some example maps (reduced in size to fit in the blog) to see how this looks on a full map:
Both of these mountain ranges happen to be almost vertical, an orientation that works well for mountain ranges because the ridgelines can connect to the peaks.
Here we have one long mountain chain with a capital city on the slopes of one of the biggest mountains -- a pretty nice combination.  The chain itself looks pretty good as well.
This is the least successful of the example maps, although all the chains are generally at good angles for mountain chains.

Placing a chain of mountains down the centerline and then filling the region sparsely with other mountains seems to work pretty well.  The centerline gives the mountain area an identifiable structure, and the random mountains define the total area.  At least right at the moment I like this better than filling the area completely with random mountains or filling the area with parallel mountain chains.

Next time I'll take a look at fixing a few lingering problems.  (One of these can be seen in the white spaces under mountains in the last example map.)