21
\$\begingroup\$


I'm searching for a nice and easy way to generate a mask for an island map with C#.


Basically I'm using with a random heightmap generated with perlin noise, where the terrain is NOT surrounded by water.

enter image description here

The next step would be to generate a mask, to ensure the corners and borders are just water.

enter image description here

Then I can just subtract the mask from the perlin noise image to get an island.

enter image description here

and playing around with the contrast..

enter image description here

and the gradient curve, I can get an island heightmap just like I want it..

enter image description here

(these are just examples of course)

so as you can see, the "edges" of the island are just cut off, which is not a big problem if the colour value is not too white, because I will just divide the grayscale into 4 layers (water, sand, grass and rock).

My question is, how can I generate a good looking mask like in the second image?


UPDATE

I've found this technique, it seems to be a nice starting point for me, but I'm not sure how exectly I can implement it to get the desired output. http://mrl.nyu.edu/~perlin/experiments/puff/


UPDATE 2

this is my final solution.

I've implemented the makeMask() function inside my normalisation loop like this:

        //normalisation
        for( int i = 0; i < width; i++ ) {
            for( int j = 0; j < height; j++ ) {
                perlinNoise[ i ][ j ] /= totalAmplitude;
                perlinNoise[ i ][ j ] = makeMask( width, height, i, j, perlinNoise[ i ][ j ] );
            }
        }

and this is the final function:

    public static float makeMask( int width, int height, int posX, int posY, float oldValue ) {
        int minVal = ( ( ( height + width ) / 2 ) / 100 * 2 );
        int maxVal = ( ( ( height + width ) / 2 ) / 100 * 10 );
        if( getDistanceToEdge( posX, posY, width, height ) <= minVal ) {
            return 0;
        } else if( getDistanceToEdge( posX, posY, width, height ) >= maxVal ) {
            return oldValue;
        } else {
            float factor = getFactor( getDistanceToEdge( posX, posY, width, height ), minVal, maxVal );
            return oldValue * factor;
        }
    }

    private static float getFactor( int val, int min, int max ) {
        int full = max - min;
        int part = val - min;
        float factor = (float)part / (float)full;
        return factor;
    }

    public static int getDistanceToEdge( int x, int y, int width, int height ) {
        int[] distances = new int[]{ y, x, ( width - x ), ( height - y ) };
        int min = distances[ 0 ];
        foreach( var val in distances ) {
            if( val < min ) {
                min = val;
            }
        }
        return min;
    }

this will give an output like in the image #3.

with a little bit of change in the code, you can get the originally wanted output like in image #2 ->

    public static float makeMask( int width, int height, int posX, int posY, float oldValue ) {
        int minVal = ( ( ( height + width ) / 2 ) / 100 * 2 );
        int maxVal = ( ( ( height + width ) / 2 ) / 100 * 20 );
        if( getDistanceToEdge( posX, posY, width, height ) <= minVal ) {
            return 0;
        } else if( getDistanceToEdge( posX, posY, width, height ) >= maxVal ) {
            return 1;
        } else {
            float factor = getFactor( getDistanceToEdge( posX, posY, width, height ), minVal, maxVal );
            return ( oldValue + oldValue ) * factor;
        }
    }
\$\endgroup\$
1
  • \$\begingroup\$ any chance you can link to youur full source? \$\endgroup\$
    – stoic
    Commented May 17, 2017 at 15:24

4 Answers 4

12
\$\begingroup\$

Generate regular noise with a bias for higher values towards the center. If you're wanting square-ish island shapes like you show in your example, I would use the distance to the closest edge as your factor.

enter image description here

With that factor, you can use something like the following when generating the mask noise:

maxDVal = (minIslandSolid - maxIslandSolid)

getMaskValueAt(x, y)
    d = distanceToNearestEdge(x,y)
    if(d < maxIslandSolid)
        return isSolid(NonSolidValue) //always non-solid for outside edges
    else if(d < minIslandSolid)
        //noisy edges
        return isSolid((1 - (d/maxDVal)) * noiseAt(x,y))
    else
        return isSolid(SolidValue) //always return solid for center of island

Where distanceToNearestEdge returns the distance to the nearest edge of the map from that position. And isSolid decides if a value between 0 and 1 is solid (whatever your cut off is). It's a very simple function, it might look like this:

isSolid(float value) return value < solidCutOffValue

Where solidCutOffValue is whatever value you're using to decide between solid or not. It could be .5 for even split, or .75 for more solid or .25 for less solid.

Finally, this little bit (1 - (d/maxDVal)) * noiseAt(x,y). First, we get a factor between 0 and 1 with this:

(1 - (d/maxDVal))

enter image description here

Where 0 is on the outside edge and 1 is on the inside edge. This means our noise is more likely to be solid at the inside and non-solid at the outside. This is the factor we apply to the noise we get from noiseAt(x,y).

Here is a more visual representation of what the values are, as the names might be misleading to the actual values:

enter image description here

\$\endgroup\$
11
  • \$\begingroup\$ thx for that quick answer, i'll try to implement this technique. hope to get the desired output with this. \$\endgroup\$
    – Ace
    Commented Apr 22, 2013 at 17:05
  • \$\begingroup\$ It'll likely take some tweaking to get the noisy edges the way you want them. But this should be a solid basis for you. Good luck! \$\endgroup\$
    – House
    Commented Apr 22, 2013 at 17:12
  • \$\begingroup\$ so far i got the base implementation to work, can you just give me an example of the IsSolid function ? i dont know how i can get a value between 0 and 1 based on the min and max distance from the edge. see my update for my code so far. \$\endgroup\$
    – Ace
    Commented Apr 22, 2013 at 18:12
  • \$\begingroup\$ I had some mixed up logic in there. I've fixed it to make more sense. And provided an example of isSolid \$\endgroup\$
    – House
    Commented Apr 22, 2013 at 18:36
  • \$\begingroup\$ To get a value between 0 and 1, you just find out what the maximum value can be and divide your current value by that. Then I subtracted that from one, because I wanted zero to be on the outside edge and 1 to be on the inside edge. \$\endgroup\$
    – House
    Commented Apr 22, 2013 at 18:47
14
\$\begingroup\$

If you're willing to spare some computational power for this, then you could use a similar technique to what the author of this blog did. (NB: If you wish to directly copy his code, it's in ActionScript). Basically, he generates quasi-random points (i.e. looks relatively uniform) and then uses these to create Voronoi polygons.

Voronoi Polygons

He then sets the outside polygons to water and iterates through the rest of the polygons, making them water if a certain percentage of the adjacent polygons are water. You're then left with a polygon mask roughly representing an island.

Polygon Map

From this you can apply noise to the edges, resulting in something resembling this (the colours are from another, unrelated step):

Polygon Map with Noisy Edges

You are then left with a (quite) realistic-looking island-shaped mask, which would serve your purposes. You could choose to use it as a mask for your Perlin noise, or you could then generate height values based on distance to the sea and add noise (although that seems unnecessary).

\$\endgroup\$
4
  • \$\begingroup\$ thx for your reply, but this woth the first ( an pretty much only ) solution i got from searching the web. this solution seemy to be very nice, but i would like to try the "simple" way. \$\endgroup\$
    – Ace
    Commented Apr 22, 2013 at 18:58
  • \$\begingroup\$ @Ace Fair enough, it probably is a bit overkill for whatever you're going to do :P Still, it's worth keeping in mind should you ever need it. \$\endgroup\$
    – Polar
    Commented Apr 22, 2013 at 20:08
  • \$\begingroup\$ Glad someone linked to this - that page is always on my list of "really fantastic posts about how someone implemented something." \$\endgroup\$
    – Tim Holt
    Commented Apr 22, 2013 at 21:52
  • \$\begingroup\$ +1. This is so cool. Thank you for this, it'll definitely be helpful for me! \$\endgroup\$
    – Andre
    Commented Apr 22, 2013 at 22:59
1
\$\begingroup\$

One very simple method is to create inverse radial or spherical gradient with center at the width / 2 and height /2. For masking you want to subtract the gradient from the noise rather than multiply it. This gives you more realistic looking shores with the drawback that the islands are not necessarily connected.

You can see the difference between subtracting and multiplying the noise with the gradient here: http://www.vfxpedia.com/index.php?title=Tips_and_Techniques/Natural_Phenomena/Smoke

If you are unsure how to create radial gradient, you can use this as a start point:

    public static float[] CreateInverseRadialGradient(int size, float heightScale = 1)
    {
        float radius = size / 2;

        float[] heightMap = new float[size * size];

        for (int iy = 0; iy < size; iy++)
        {
            int stride = iy * size;
            for (int ix = 0; ix < size; ix++)
            {
                float centerToX = ix - radius;
                float centerToY = iy - radius;

                float distanceToCenter = (float)Math.Sqrt(centerToX * centerToX + centerToY * centerToY);
                heightMap[iy * size + ix] = distanceToCenter / radius * heightScale;
            }
        }

        return heightMap;
    }

Don't forget to scale your gradient to the same heightas your height map and you still need to factor in your water line somehow.

The problem with this method is that your height field will be centered around the center of the map. This method, however, should get you started with adding features and making your landscape more diverse as you can use addition to add features to your height map.

\$\endgroup\$
1
  • \$\begingroup\$ thx for the reply. i'm not sure if i understand you right, but did you notice that the mask doesnt affect the original heightmap data at all, it is just affected in the negative, so it just defines the pixes that are displayed ( in% ) or not. but also i've tryed it with simple gradiants, and i was not happy with the result. \$\endgroup\$
    – Ace
    Commented Apr 22, 2013 at 20:10
1
\$\begingroup\$

I second ollipekka's suggestion: what you want to do is subtract a suitable bias function from your heightmap, so that the edges are guaranteed to be underwater.

There are plenty of suitable bias functions, but one fairly simple one is:

f(x, y) = 1 / (x * (1-x) * y * (1-y)) - 16

where x and y are the coordinate values, scaled to lie between 0 and 1. This function takes the value 0 at the center of the map (at x = y = 0.5) and tends to infinity at the edges. Thus, subtracting it (scaled by a suitable constant factor) from your height map ensures that the height values will also tend to minus infinity near the map edges. The just pick any arbitrary height you want and call it the sea level.

As ollipekka notes, this approach will not guarantee that the island will be contiguous. However, scaling the bias function by a fairly small scale factor should make it mostly flat in the middle area of the map (thus not affecting your terrain much), with a significant bias appearing only near the edges. Thus, doing so should give you a squarish, mostly contiguous island with, at most, possible a few tiny sub-islands near the edges.

Of course, if you don't mind the possibility of disconnected terrain, a somewhat larger scaling factor should give you more water and a more natural-looking island shape. Adjusting the sea level and/or the scale of your original heightmap can also be used to vary the size and shape of the resulting island(s).

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .