A downloadable asset pack


In this tutorial I will show you how to generate Perlin noise using a DS Grid.

(Version 2.0 of the Perlin Noise generator is available here)

Prologue

This tutorial is designed for people with moderate familiarity with GML that understand for-loops, ds-grids, and the lerp function. If you don't know any of these things this introduction into creating Perlin maps could be difficult to understand. I have included a link to a version that I spent quite a lot of time on perfecting and looks noticeably better.

Part 1: Creating Noise

To create Perlin noise, we must first generate simple noise. The simple noise is what we will operate on to create a smooth transition using a lerp function later

function perlin_grid(_w, _h, _steps, seed, values) {
    random_set_seed(seed);
    var _ww = floor(_w/_steps)+2;
    var _hh = floor(_h/_steps)+2;
    var _randArray = [];
    /// Fill array with random values from 0 to values
    for(var j = 0; j < _hh; j++){
        for(var i = 0; i < _ww; i++){
            _randArray[i][j] = irandom(values-1);    
        }
    }
    return _randArray;
}

This code defines a function called perlin_grid that takes in five arguments: _w, _h, _steps, seed, and values. The function generates a two-dimensional array of random values based on the _w, _h, _steps, and seed arguments and returns the array.

The random_set_seed function is called with the seed argument to set the seed for the random number generator. This ensures that the random numbers generated will be the same every time the function is called with the same seed.

The variables _ww and _hh are calculated by flooring _w/_steps and _h/_steps and adding 2. These variables represent the number of elements in the width and height of the resulting two-dimensional array, respectively.

The _randArray variable is initialized as an empty array. A nested loop iterates over the indices of _randArray, and for each iteration, a random integer between 0 and values-1 is generated using the irandom function and assigned to the current element of _randArray.

Finally, the function returns the _randArray array. This will merely return an array with random values between 0 and values so we will have to modify it to generate Perlin noise

Part 2: Smoothing The Noise

Lets create a function that can smoothly interpolate between each point in the noise map.

function perlin_lerp(_dx, _dy, _v1, _v2, _v3, _v4) {
    /// find midpoint values
    /*
        To find the value at point X we will use lerp
        to first get the points closest to it on the edges.
        Then, we will use lerp those values to get the
        X and Y values
    
          V1.|......V2
          ---X--------
          ...|........
          ...|........
          V4.|......V3
    */
    var m1,m2,m3,m4;
    m1 = lerp(_v1,_v2,_dx);    /// top
    m2 = lerp(_v2,_v3,_dy);    /// right
    m3 = lerp(_v4,_v3,_dx);    /// bottom
    m4 = lerp(_v1,_v4,_dy);    /// left
    /// find both center values
    var c1, c2;
    c1 = lerp(m1,m3,_dy);
    c2 = lerp(m4,m2,_dx);
    /// average c1 and c2 and return
    return((c1+c2)*0.5);
}

This function will take in the change in x, y position. (For example, getting point X of 3.4 and Y of 5.6 will interpolate between 3 and 4 on the X axis and 5 to 6 on the Y axis by 0.4 on the X and 0.6 on the Y, so we input 0.4, and 0.6 into the function) This will allow us to find points in between the discrete points in the noise map and smoothly interpolate between them. When we pick a point between our discrete random points it will have four adjacent values that we will want to interpolate between. We first get the values along the edges, then we interpolate between the edge and the point inside. We end up with two values, so we simply take the average.

Part 3: The Perlin Map Generator

We will need to modify the code in the perlin map function so we can generate a DS Grid, and use the lerp function to transform the noise into perlin noise. 

    
var _grid = ds_grid_create(_w,_h);
/// populate ds_grid
for(var j = 0; j < _h; j++){
   for(var i = 0; i < _w; i++){
        /// partial step into noise map
        var _dx = frac(i/_steps);
        var _dy = frac(j/_steps);
        /// discrete step into noise map
        var _x = floor(i/_steps);
        var _y = floor(j/_steps);
        /// get values from discrete step
        var _v1 = _randArray[_x][_y];
        var _v2 = _randArray[_x+1][_y];
        var _v3 = _randArray[_x+1][_y+1];
        var _v4 = _randArray[_x][_y+1];
        /// interpolated value
        var _val = floor(perlin_lerp(_dx, _dy, _v1, _v2, _v3, _v4));
        ds_grid_set(_grid,i,j,_val);
   }
}

Conclusion

After just a few lines of code it is not too difficult to get some reliable and decent looking Perlin noise. I ended up tweaking this code a little bit to generate some pretty good looking Perlin noise. Here is the final version of the code and the settings I used.  (Updated the Perlin lerp to create a much more natural looking smooth resulting in a less blocky texture) It uses the inverse square of the distance which results in spherical falloff.

/// more natural looking Perlin noise
function perlin_lerp(_dx, _dy, _v1, _v2, _v3, _v4, pwr = 2) {
    /// find midpoint values
    /*
        Inverse Falloff
    */
    
    // get weighted average
    var _l1 = 1/power(point_distance(0,0,_dx,_dy) + 0.001, pwr);
    var _l2 = 1/power(point_distance(1,0,_dx,_dy) + 0.001, pwr);
    var _l3 = 1/power(point_distance(1,1,_dx,_dy) + 0.001, pwr);
    var _l4 = 1/power(point_distance(0,1,_dx,_dy) + 0.001, pwr);
    var _total = _l1+_l2+_l3+_l4;
    _l1 /= _total;
    _l2 /= _total;
    _l3 /= _total;
    _l4 /= _total;
    
    var out = _l1*_v1 + _l2*_v2 + _l3*_v3 + _l4*_v4;
    /// average c1 and c2 and return
    return(out);
}
function perlin_map(_w, _h, _steps, seed, values) {
    random_set_seed(seed);
    /// get width and height of the random noise array
    var _ww = floor(_w/_steps)+2;
    var _hh = floor(_h/_steps)+2;
    var _randArray = [];
    /// Fill array with random values from 0 to values
    for(var j = 0; j < _hh; j++){
        for(var i = 0; i < _ww; i++){
            _randArray[i][j] = irandom(values-1);    
        }
    }
    /// setup grid structure
    var _grid = ds_grid_create(_w,_h);
    /// populate ds_grid
    for(var j = 0; j < _h; j++){
        for(var i = 0; i < _w; i++){
            /// partial step into noise map
            var _dx = frac(i/_steps);
            var _dy = frac(j/_steps);
            /// discrete step into noise map
            var _x = floor(i/_steps);
            var _y = floor(j/_steps);
            /// get values from discrete step
            var _v1 = _randArray[_x][_y];
            var _v2 = _randArray[_x+1][_y];
            var _v3 = _randArray[_x+1][_y+1];
            var _v4 = _randArray[_x][_y+1];
            /// interpolated value
            var _val = floor(perlin_lerp(_dx, _dy, _v1, _v2, _v3, _v4));
            ds_grid_set(_grid,i,j,_val);
        }
    }
    return(_grid);
}
/// code for game object
function create_perlin_map(){
  randomize();
  var seed = random_get_seed();
  values = 100;
  ww = display_get_gui_width() div 4;
  hh = display_get_gui_height() div 4;
  map = perlin_map(ww, hh, 5, seed, values);
}
/// code for rendering
function draw_perlin_map(){
  for(var j = 0; j < ww; j++){
    for(var i = 0; i < hh; i++){
        var val = map[# j,i];
        var x1 = i*4;
        var y1 = j*4;
        var x2 = x1+4;
        var y2 = y1+4;
        var cc = merge_color(c_white, c_black, val/values);
        draw_rectangle_color(x1,y1,x2,y2,cc,cc,cc,cc,0);
    }
  }
}

 

Leave a comment

Log in with itch.io to leave a comment.