Wednesday, November 9, 2016

All Good Things Must Come to An End

In my previous posting about mountain ranges along fault lines, I simplified things by assuming that the fault line and the mountain range atop it ran from one edge of the map to the other edge.  So there was no need to end the mountain range on the map.  But of course, that's something we might want to do, so in this post I'll describe my approach to that problem.

Recall that we placed the mountain range on the map by using a sort of tent-shaped mask along the fault line:
This mask was created by measuring the distance from the fault line, and making the mask high close to the line and dropping to zero at some distance away from the line.  So the mountains show through fully where the mask is high and disappear where the mask is low.

To make the mountains end on the map, we need to modify the mask so that it stops where we want the mountains to end.  And in fact, the function I provided in the previous posting had an option to treat the mask as a line segment, so that past the end of the line segment the "distance to the line" becomes the distance to the end point of the line segment, and the mask gets a curved end like this:

So we can use this version of the mask to end the mountains at the end of the line segment.  Here's an example:
This works, but in practice it's too abrupt.  The round end of the mountain range is also a little odd looking.  A map view makes this more apparent:
A more natural-looking termination is for the mountain range to trail off with a long, gradually narrowing end:
To do this, we need to end our tent mask with a long wedge shape rather than the half-circle of the simple mask.  We can achieve this adding ends to our tent mask that look like this:
(And now our mask really looks like a pup tent!)  Now when we draw our mountains inside this mask, they'll diminish to a pointy end:
With the addition of some perturbation to avoid arrow-straight mountain chains, the result can be very convincingly realistic:
The technique of using masks to control noise-based terrain is very powerful, because it permits you to create natural-looking terrain while simultaneously having exact control of how that terrain is placed on the map.

So how do you calculate the tent mask?  This is more complicated than the simpler mask used in the earlier posting.  In the version of the tent mask below, I also permit the main part of the tent to vary in width and height from one end to the other.  Altogether, the mask code looks like this:

//
//  Tent Mask
//
//  (x,y) -- the point we are masking
//  (x0, y0) -- one end of the main part of the tent
//   w0, h0  -- the width and height of the x0 end of the tent
//  taper0   -- a boolean that indicates whether the tent should taper past x0
//  l0       -- the length of the taper
//  (x1, y1) -- the other end of the main part of the tent
//   w1, h1  -- the width and height of the x1 end of the tent
//  taper1   -- a boolean that indicates whether the tent should taper past x1
//  l1       -- the length of the taper
//  
//
function tentMask(x, y, x0, y0, w0, h0, x1, y1, w1, h1, taper0, l0, taper1, l1) {
    // The math breaks down if x0 is the same as x1; rather than add that special
    // case, I simply tweak x1 to avoid the problem.
    if (x0 == x1) {
       x1 += 0.0001;
    };
    // x0 should be less than x1.  If not, swap the two points.
    if (x0 > x1) {
       var xt = x1, yt = y1, ht = h1, wt = w1;
       x1 = x0; y1 = y0; h1 = h0; w1 = w0;
       x0 = xt; y0 = yt; h0 = ht; w0 = wt;
    };
    //  (xp, yp) is going to be the point on the line (x0,y0)->(x1,y1) where the
    //  the perpendicular from (x, y) intersects.  If "t" is between 0 and 1, then
    //  the perpendicular hits between (x0,y0) and (x1,y1).  If not, then the
    //  perpendicular is off one end or the other.
    var d = (x1-x0)*(x1-x0)+(y1-y0)*(y1-y0);
    var d01 = Math.sqrt(d);
    var t = ((x-x0)*(x1-x0)+(y-y0)*(y1-y0))/d;
    var xp = x0 + t*(x1-x0);
    var yp = y0 + t*(y1-y0);
    //  If we're in the main part of the tent, or we're off and end without a
    //  taper, then our mask is just proportional to the distance of (x,y) from
    //  the line.
    if ((t >= 0 && t <= 1) || (t < 0 && !taper0) || (t > 1 && !taper1)) {
       // Distance from (x1,y1) to the point where the perpendicular meets the line
       var dp1 = Math.sqrt((xp-x1)*(xp-x1)+(yp-y1)*(yp-y1));
       // Height at the line
       var hp = (dp1/d01)*(h0-h1) + h1;
       // Width at this point of the line
       var wp = ((x1-xp)/(x1-x0))*(w0-w1)+w1;
       // Distance from the (x,y) to the line
       var lp = Math.sqrt((xp-x)*(xp-x)+(yp-y)*(yp-y));
       // if lp > wp then the point is outside the width of the
       // trapezoid, and the mask is 0
       if (lp > wp) return 0;
       // This is the height of the mask at (x,y)
       return (1-(lp/wp))*hp;
    };
    // Less than x0.  In this case, we want to use the distance away
    // from x0 and the taper length l0 to figure out the taper.
    if (t < 0 && taper0) {
       var dp0 = Math.sqrt((xp-x0)*(xp-x0)+(yp-y0)*(yp-y0));
       // If dp0 > l0 then we're off the end of the taper
       if (dp0 >= l0 || h0 == 0) return 0;
       // Height goes linear from h0 to 0
       var hp = (1-(dp0/l0))*h0/2 + h0/2;
       // Width goes linear from w0 to 0
       var wp = (1-(dp0/l0))*w0;
       // What's the width of x,y?
       var lp = Math.sqrt((xp-x)*(xp-x)+(yp-y)*(yp-y));
       // If lp > wp we're outside the arrow head
       if (lp > wp) return 0;
       return (1-(lp/wp))*hp;
    };
    // More than x1 -- the reverse case.
    if (t > 1 && taper1) {
       var dp1 = Math.sqrt((xp-x1)*(xp-x1)+(yp-y1)*(yp-y1));
       // If dp1 > l1 then we're off the end of the taper
       if (dp1 >= l1 || h0 == 0) return 0;
       // Height goes linear from h0 to 0
       var hp = (1-(dp1/l1))*h1;
       // Width goes linear from w0 to 0
       var wp = (1-(dp1/l1))*w1;
       // What's the width of x,y?
       var lp = Math.sqrt((xp-x)*(xp-x)+(yp-y)*(yp-y));
       // If lp > wp we're outside the arrow head
       if (lp > wp) return 0;
       return (1-(lp/wp))*hp;
    };
    // Otherwise masked out.
    return 0;
};

(Let me know if you like seeing code on the blog or not.)  The ends of the tent are really special cases of the middle of the tent, so this can probably be written more compactly as a conjunction of three masks, but it may be more understandable this way. 

This tent mask uses linear interpolation (the sides of the tent are straight lines).  A more complex mask might use a different easing function to create a more natural profile.  For map drawing this difference would probably not be apparent, but if you were using mountains as part of a 3D environment, this might be worth pursuing.

1 comment: