Search code examples
javanormal-distributionperlin-noise

How to increase the standard deviation of a Perlin noise distribution


I have been following a guide to Perlin noise here: http://flafla2.github.io/2014/08/09/perlinnoise.html

The code I use is exactly the same as what he describes, although my rendering implementation is different. However, when I get noise from this distribution, I tend to get a very large fraction of values near the 0.5 range and very few on the fringes. You can see this from a sample which I turned into a quick graphic in the image below. I scaled my noise function by the width of the screen and had it increment an array at index = noise by 1 every time it generated a noise value. As you can see, the distribution is hardly Normal, as the fringes just drop out entirely. (It's technically a histogram without labels and bar lines)

Distribution

What is the best way to make this distribution wider towards the bottom, but not towards the top? I want most of the values to lie in a Gaussian distribution, but I can't just use a Gaussian distributed random number because I need values selected close to each other to be close to each other (Perlin noise).

I guess a better way of asking would be: Why do I get no noise values in the upper and lower quarters of my range? Is there a way to determine a good scaling factor to widen the graph by?

Here is the code I'm using to get the image:

/**
 * Simple linear interpolation
 * @param a Start
 * @param b End
 * @param weight weighting
 * @return A linear interpolation between points a and b 
 */
public double lerp(double a, double b, double weight) {
    return a + weight*(b-a);
}

/**
 * Calculates a dot product between a distance vector and a pseudorandom
 * "gradient" vector which gets picked using the hash
 * @param hash
 * @param x distance vector x component
 * @param y distance vector y component
 * @param z distance vector z component (z = 0 for 2D map)
 * @return dot product of <x, y, z> and a pseudorandom gradient vector
 */
public double grad(int hash, double x, double y, double z) {
    switch(hash & 0xF)
    {
        case 0x0: return  x + y;
        case 0x1: return -x + y;
        case 0x2: return  x - y;
        case 0x3: return -x - y;
        case 0x4: return  x + z;
        case 0x5: return -x + z;
        case 0x6: return  x - z;
        case 0x7: return -x - z;
        case 0x8: return  y + z;
        case 0x9: return -y + z;
        case 0xA: return  y - z;
        case 0xB: return -y - z;
        case 0xC: return  y + x;
        case 0xD: return -y + z;
        case 0xE: return  y - x;
        case 0xF: return -y - z;
        default: return 0; // never happens
    }
}

/**
 * A fifth order fade function: 6t^5 - 15t^4 + 10t^3
 * @param t The x-value along the function, t is in [0, 1]
 * @return The y-value for the fade function
 */
public double fade(double t) {
    return t * t * t * (t * (t * 6 - 15) + 10);
}

//repeat is set to 0 so this method is just a regular "increment by 1"
public int inc(int num) {
    num++;
    if(repeat > 0) num %= repeat;
    return num;
}

/**
 * Generates a noise value in the range [0,1].
 * Each coordinate is a given distance from a pseudorandomly picked set of
 * gradient vectors. The vectors are determined by an array of 256 indexes,
 * so the noise pattern inevitably repeats at a scale greater than 255,
 * which is bigger than we should need.
 * @param x
 * @param y
 * @param z
 * @return 
 */
public double perlin(double x, double y, double z) {
    if(repeat > 0) {
        x = x%repeat;
        y = y%repeat;
        z = z%repeat;
    }

    int xi = (int)x & 255;
    int yi = (int)y & 255;
    int zi = (int)z & 255;
    double xf = x-(int)x;
    double yf = y-(int)y;
    double zf = z-(int)z;


    double u = fade(xf);
    double v = fade(yf);
    double w = fade(zf);

    int aaa, aba, aab, abb, baa, bba, bab, bbb;
    aaa = p[p[p[    xi ]+    yi ]+    zi ];
    aba = p[p[p[    xi ]+inc(yi)]+    zi ];
    aab = p[p[p[    xi ]+    yi ]+inc(zi)];
    abb = p[p[p[    xi ]+inc(yi)]+inc(zi)];
    baa = p[p[p[inc(xi)]+    yi ]+    zi ];
    bba = p[p[p[inc(xi)]+inc(yi)]+    zi ];
    bab = p[p[p[inc(xi)]+    yi ]+inc(zi)];
    bbb = p[p[p[inc(xi)]+inc(yi)]+inc(zi)];


    double x1, x2, y1, y2;

    /*
    Box has corners:
    ____
    |ab|
    |cd|
    ----
    Interpolate a-b, then c-d then both of those together, then repeat on the z-1 level
    */
    x1 = lerp(grad (aaa, xf  , yf  , zf),           // The gradient function calculates the dot product between a pseudorandom
              grad (baa, xf-1, yf  , zf),             // gradient vector and the vector from the input coordinate to the 8
              u);                                     // surrounding points in its unit cube.
    x2 = lerp(grad (aba, xf  , yf-1, zf),
              grad (bba, xf-1, yf-1, zf),
              u);
    y1 = lerp(x1, x2, v);

    x1 = lerp(grad (aab, xf  , yf  , zf-1),
              grad (bab, xf-1, yf  , zf-1),
              u);
    x2 = lerp(grad (abb, xf  , yf-1, zf-1),
              grad (bbb, xf-1, yf-1, zf-1),
              u);
    y2 = lerp (x1, x2, v);

    return (lerp (y1, y2, w)+1)/2; //Interpolate everything again and move the range from [-1, 1] to [0, 1]
}

/**
 * Layers levels of noise, each with decreasing amplitudes and persistence
 * @param x
 * @param y
 * @param z
 * @param octaves
 * @param persistence how much each layer impacts the layer below it
 * @return
 */
public double octave(double x, double y, double z, int octaves, double persistence) {
    double total = 0, frequency = 1, amplitude = 1, maxValue = 0;
    for(int i = 0; i < octaves; i++) {
        total += perlin(x * frequency, y * frequency, z * frequency) * amplitude;
        maxValue += amplitude;
        amplitude *= persistence;
        frequency *= 2;
    }

    return total/maxValue;
}

Here is the start method:

public void enter() {
    eOffsetX = r.nextInt(10000);
    eOffsetY = r.nextInt(10000); //This will "randomize" the seed of the noise
    p = new int[512];
    for(int x = 0; x < 512; x++) {
        p[x] = permutation[x%256]; //Fill twice
    }
    eNoise = new float[(int)(1280/tile)][(int)(800/tile)];
    gauss = new float[1280];
    Arrays.fill(gauss, 0);

    for(int i = 0; i < eNoise.length; i++) {
        for(int j = 0; j < eNoise[0].length; j++) {
            eNoise[i][j] = 1f * 100 * (float) octave(((double)i*zoom+eOffsetX)/1280, ((double)j*zoom+eOffsetY)/800, 0, 7, 0.60);
            gauss[(int)(((eNoise[i][j]/100)-.5)*1280*2.5+640)] += 1f;
        }
    }}

The render method:

public void render(Graphics g) {
        g.setBackground(Color.white);
        g.setColor(Color.black);
        for(int k = 0; k < 1280; k++) {
            g.fillRect(k, 800-(gauss[k]/10), 5, 5);
        }
}

And finally, the permutation set alluded to in enter():

private static final int[] permutation = { 151,160,137,91,90,15,
    131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
    190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
    88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
    77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
    102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
    135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
    5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
    223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
    129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
    251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
    49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
    138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180
};

It's all the numbers from 0 to 255 in random order.

Again, to restate the problem, the function generates a huge proportion of numbers between 0.45 and 0.55, and almost no numbers outside that range. I want to rescale the function so that I get more numbers in those lower and upper ranges. This might occur somewhere in the last line of the perlin() function, but I'm not sure how to do it. Thanks for your help.


Solution

  • I have fixed this problem through the following method.

    First, while generating noise values, I kept track of the minimum and maximum values.

    Second, when storing the noise values into my array, I normalized the data: (value - min)/(max - min). This way, all my values are between 0 and 1, but are better distributed. The equation maps the minimum value to (min - min)/(max - min) or 0, and maps the largest value to (max - min)/(max - min) or 1, and it scales all the values in between.