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.
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.
Enjoying the code, enjoying the rest too.
ReplyDelete