0

I'm having difficulties with the Midpoint Displacement Algorithm using Haxe. I am implementing this by following the steps found here.

First, create an array that represents a blank map. You begin by giving the four corners a random value.

In this square, create the middle point by averaging the four corners and adding a small 'error', or random value. Then create the midpoints of the 4 sides by averaging the two corners each is between. After these steps, you are left with 4 squares. Repeat the steps:

  1. Create the middle point by averaging the four corners and adding a small 'error'.

  2. Create the midpoint of each side by averaging the two corners each point is between.

Each iteration, make the range of the RNG smaller. That way the original few points can have pretty large variation, but the later points only get tiny adjustments. This ensures the right amount of detail in an image.

Here is the function I've written to perform these steps and then normalize the values:

public static function generateFloatMatrix(Columns:Int, Rows:Int, RangeModifier:Float = 0.65):Array<Array<Float>>
{
    //Blank 2D Array
    var matrix:Array<Array<Float>> = InitFloatMatrix(Columns, Rows);
    var range:Float = 1;
    
    //Set Values for all four corners
    matrix[0][0] = Math.random() * range;
    matrix[Rows-1][0] = Math.random() * range;
    matrix[0][Columns-1] = Math.random() * range;
    matrix[Rows - 1][Columns - 1] = Math.random() * range;
    
    //Calculates the amount of segments in base 2
    var length = Math.sqrt((Columns * Columns) + (Rows * Rows));
    var power:Int = Std.int(Math.pow(2, Math.ceil(Math.log(length) / Math.log(2))));
    
    //Stores largest calculated value for normalization
    var max:Float = 0;
    
    var width:Int = Std.int(Columns);
    var height:Int = Std.int(Rows);
    
    var i:Int = 1;
    while (i < power)
    {
        //Segment Size
        width = Std.int(Columns / i);
        height = Std.int(Rows / i);
        
        for (y in 0...i)
        {
            for (x in 0...i)
            {
                //Top Left Coordinates per segment
                var left = width * x;
                var top = height * y;
                
                //Find Midpoint
                var xMid = Math.ceil(left + (width / 2));
                var yMid = Math.ceil(top + (height / 2));
                
                //Make sure right and bottom do not go out of bounds
                var right:Int = (left + width < Columns ? left + width : Columns - 1);
                var bottom:Int = (top + height < Rows ? top + height : Rows - 1);
                
                //Sets midpoint value to average of all four corners.
                matrix[yMid][xMid] = 
                    (matrix[top][left] + 
                        matrix[bottom][left] + 
                        matrix[bottom][right] + 
                        matrix[top][right]) / 4;
                        
                //trace ("Top: " + top + " - Left: " + left + " - Bottom: " + bottom + " - Right: " + right);
                
                //Adds random value to midpoint
                matrix[yMid][xMid] += Math.random() * range;
                
                //Set side values to average of adjacent corners
                matrix[top][xMid] = (matrix[top][left] + matrix[top][right]) / 2;
                matrix[bottom][xMid] = (matrix[bottom][left] + matrix[bottom][right]) / 2;
                matrix[yMid][left] = (matrix[top][left] + matrix[bottom][left]) / 2;
                matrix[yMid][right] = (matrix[top][right] + matrix[bottom][right]) / 2;
                
                max = Math.max(matrix[top][left], max);
            }
        }
        
        //Reduces range
        range *= RangeModifier;
        i *= 2;
    }
    
    //Normalizes all values in matrix
    for (y in 0...Rows)
    {
        for (x in 0...Columns)
        {
            matrix[y][x] /= max;
        }
    }
    
    return matrix;
}

These are the images it is producing if I use each value to render each pixel to the specified coordinate. All the pixels that are rendered white have the value 0, black is value 1.

Rendered as 8x8 pixels

Rendered as 1x1 pixels

Community
  • 1
  • 1
Tim Stoddard
  • 95
  • 1
  • 11

1 Answers1

1

Your problem is that you don't necessarily hit the already populated pixels with your calculations if your map dimensions are not a power of two. For example if your map is 30 units wide, your grid width is 15 in the first pass and 7 in the second pass, where it bases its calculations on the yet untouched unit 14.

A solution is to do all calculations with floating-point arithmetic until you determine the unit indices, which must of course be integer:

while (i < power)
{
    var width:Float = Columns / i;    // Floating-point division
    var height:Float = Rows / i;

    for (y in 0...i)
    {
        for (x in 0...i)
        {
            var left:Int = Math.floor(width * x);
            var top:Int = Math.floor(height * y);

            var xMid:Int = Math.floor(width * (x + 0.5));
            var yMid:Int = Math.floor(height * (y + 0.5));

            var right:Int = Math.floor(width * (x +1));
            var bottom:Int = Math.floor(height * (y + 1));

            //Make sure right and bottom do not go out of bounds
            if (right > Columns - 1) right = Columns - 1;
            if (bottom > Rows - 1) bottom = Rows - 1;

            // Do offset and interpolation stuff
        }
    }
}

This should give you a random map, graph-paper effect and all.

(Caveat: I'm not familiar with Haxe, but have tested this in Javascript, which doesn't have an integer type. I've used Math-floor throughout, where you'll want to do it the Haxe way.)

Finally, it looks to me that you do too many passes. I'd base the power on the maximum of the two dimensions instead of the diagonal. You can also skip the last step where wthe width is near one.

M Oehm
  • 28,726
  • 3
  • 31
  • 42
  • Applied the suggestions you made, it looks much better now, thanks! [Result](http://i.imgur.com/32fGTDk.png) Although is there a way to remove the graph paper effect? Like maybe a second interpolation? – Tim Stoddard Nov 13 '14 at 17:48
  • Thanks for sharing the result. The graph paper effect is a typical artefact of the midpoint displacement algorithm. It occurs because you only add noise to the midpoint of each square, not to the edge midpoints. The algorithm is also sensitive to the range modifier; maybe calibrating that gives better results. That's a common problem, have a look at the related questions in the right sidebar. – M Oehm Nov 13 '14 at 18:49
  • You could also add noise in two stages: First, add noise to the midpoint of each square. Then add noise to the edges of the squares by treating them as midpoints of a square that is rotated by 45° whose corners are the end points of the edges and the midpoints of the two squares connected by the edges. So you average and add noise in two grids, a rectilinear one and a diagonal one, alternately. After each stage, you should multiply the range by the square root of the range modifier. This will relieve the effect, but it will probably not get entirely rid of it. – M Oehm Nov 13 '14 at 18:55
  • Having played a bit with midpoint displacement, I find your graph-paper effect especially pronounced. It is present in most cases, like in the examples on the [Wikipedia page](http://en.wikipedia.org/wiki/Diamond-square_algorithm), but much weaker. I guess that your creases come from normalising the data where max is much greater than 1. You only add to the terrain and your corners start with a value between 0 and 1. A better strategy might be to start with values around 0.5 (e.g. `0.25 + random()`) and allow the midpoint to go both ways: `m += range * (random() - 0.5)`. – M Oehm Nov 13 '14 at 20:15
  • I've created a little [toy webpage](http://martin-oehm.de/data/diamond.html), where you can fiddle with some settings. The default settings allow the terrain to raise or lower at modpoints; they give a reasonable terrain with occasional weak creases. If you set the range decay to 0.3 and tick "normalise", you get a smooth terrain with strong, starburst-like creases. Tick "two-stage algo" and the creases vanish. (There are still peaks, however.) So the solutions are to chose sensible value ranges first and to use a two-pass algorithm for fine-tuning. – M Oehm Nov 13 '14 at 21:26
  • Okay, I did your first suggestion of adding noise to the edge midpoints which got [this](http://i.imgur.com/7NAm9oS.png). I wasn't entirely sure of your two stage method, I thought you'd refer to changing which points to average the end points from but that ended up giving me some really dark and blocky patterns. Finally I tried your strategy for dealing with normalization by starting the four corners at (0.25 + random()) and allowing the midpoint to go both ways, which gave [this](http://i.imgur.com/dXri9vu.png) which looks like it has greatly reduced the graph paper effect, but still blocky. – Tim Stoddard Nov 13 '14 at 22:28
  • Looks like MPD is a very sensitive algorithm. Your blockiness comes from the difference in height and width. Because you increase the step uniformly and you have to cater for the largest dimension, the noise will apply to the same line repeatedly in the other dimension. Most example implementations use square grids and usually also uses dimensions that are `2^k + 1`, which will save you all the floating-point arithmetic. The example you've linked to does that as does my code. This yields reasonably realistic results. – M Oehm Nov 14 '14 at 07:13
  • 1
    So what you could do is: Expand you map to a square and work with that. Obviously you are going to throw away a portion of the map after generation. Alternatively, you could subdivide your map into squares, e.g. 3x4 tiles of 160x160 pixels each and displace these. You must create seamless transitions between the tiles, though, so you could consider the tiles map as an advanced state in the algorithm with equal step width in each direction, but with different dimension. – M Oehm Nov 14 '14 at 07:16
  • I see, I remember the steps mentioning that its limited to fixed sizes, saying that extra steps need to be made to handle rectangles. Annoyingly it didn't say what the extra steps would be. I guess I could calculate the greatest common divider of both values and use that to determine the grid size? – Tim Stoddard Nov 14 '14 at 15:05
  • Yes, GCD seems like a good option, but that will only work if your dimensions are not relatively prime or when the GCD is small. I think it is sufficient to have the grid size roughly identical in both directions. You probably know your map sizes, so you could also estimate a good starting grid size and make that a function parameter. – M Oehm Nov 14 '14 at 15:31
  • I see. My intention was to use this for a general function, but that'll probably make my situation more difficult. – Tim Stoddard Nov 14 '14 at 17:57
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/64965/discussion-between-tim-stoddard-and-m-oehm). – Tim Stoddard Nov 14 '14 at 19:26
  • Looks like the clipping method gives the best results, despite reducing the speed of the algorithm exponentially. Here are the results: http://i.imgur.com/hXsez9q.gif – Tim Stoddard Nov 14 '14 at 20:42