6

I've been trying to create a generalized Gradient Noise generator (which doesn't use the hash method to get gradients). The code is below:

class GradientNoise {
    std::uint64_t m_seed;
    std::uniform_int_distribution<std::uint8_t> distribution;
    const std::array<glm::vec2, 4> vector_choice = {glm::vec2(1.0, 1.0), glm::vec2(-1.0, 1.0), glm::vec2(1.0, -1.0),
                                                    glm::vec2(-1.0, -1.0)};

public:
    GradientNoise(uint64_t seed) {
        m_seed = seed;
        distribution = std::uniform_int_distribution<std::uint8_t>(0, 3);
    }

    // 0 -> 1
    // just passes the value through, origionally was perlin noise activation
    double nonLinearActivationFunction(double value) {
        //return value * value * value * (value * (value * 6.0 - 15.0) + 10.0);
        return value;
    }

    // 0 -> 1
    //cosine interpolation
    double interpolate(double a, double b, double t) {
        double mu2 = (1 - cos(t * M_PI)) / 2;
        return (a * (1 - mu2) + b * mu2);
    }

    double noise(double x, double y) {
        std::mt19937_64 rng;
        //first get the bottom left corner associated
        // with these coordinates
        int corner_x = std::floor(x);
        int corner_y = std::floor(y);

        // then get the respective distance from that corner
        double dist_x = x - corner_x;
        double dist_y = y - corner_y;

        double corner_0_contrib; // bottom left
        double corner_1_contrib; // top left
        double corner_2_contrib; // top right
        double corner_3_contrib; // bottom right

        std::uint64_t s1 = ((std::uint64_t(corner_x) << 32) + std::uint64_t(corner_y) + m_seed);
        std::uint64_t s2 = ((std::uint64_t(corner_x) << 32) + std::uint64_t(corner_y + 1) + m_seed);
        std::uint64_t s3 = ((std::uint64_t(corner_x + 1) << 32) + std::uint64_t(corner_y + 1) + m_seed);
        std::uint64_t s4 = ((std::uint64_t(corner_x + 1) << 32) + std::uint64_t(corner_y) + m_seed);


        // each xy pair turns into distance vector from respective corner, corner zero is our starting corner (bottom
        // left)
        rng.seed(s1);
        corner_0_contrib = glm::dot(vector_choice[distribution(rng)], {dist_x, dist_y});

        rng.seed(s2);
        corner_1_contrib = glm::dot(vector_choice[distribution(rng)], {dist_x, dist_y - 1});


        rng.seed(s3);
        corner_2_contrib = glm::dot(vector_choice[distribution(rng)], {dist_x - 1, dist_y - 1});


        rng.seed(s4);
        corner_3_contrib = glm::dot(vector_choice[distribution(rng)], {dist_x - 1, dist_y});


        double u = nonLinearActivationFunction(dist_x);
        double v = nonLinearActivationFunction(dist_y);


        double x_bottom = interpolate(corner_0_contrib, corner_3_contrib, u);
        double x_top = interpolate(corner_1_contrib, corner_2_contrib, u);
        double total_xy = interpolate(x_bottom, x_top, v);
        return total_xy;
    }
};

I then generate an OpenGL texture to display with like this:

int width = 1024;
int height = 1024;
unsigned char *temp_texture = new unsigned char[width*height * 4];
double octaves[5] = {2,4,8,16,32};

for( int i = 0; i < height; i++){
    for(int j = 0; j < width; j++){
        double d_noise = 0;
        d_noise += temp_1.noise(j/octaves[0], i/octaves[0]);
        d_noise += temp_1.noise(j/octaves[1], i/octaves[1]);
        d_noise += temp_1.noise(j/octaves[2], i/octaves[2]);
        d_noise += temp_1.noise(j/octaves[3], i/octaves[3]);
        d_noise += temp_1.noise(j/octaves[4], i/octaves[4]);
        d_noise/=5;
        uint8_t noise = static_cast<uint8_t>(((d_noise * 128.0) + 128.0));
        temp_texture[j*4 + (i * width * 4) + 0] = (noise);
        temp_texture[j*4 + (i * width * 4) + 1] = (noise);
        temp_texture[j*4 + (i * width * 4) + 2] = (noise);
        temp_texture[j*4 + (i * width * 4) + 3] = (255);
    }
}

Which give good results:

enter image description here

But gprof is telling me that the Mersenne twister is taking up 62.4% of my time and growing with larger textures. Nothing else individual takes any where near as much time. While the Mersenne twister is fast after initialization, the fact that I initialize it every time I use it seems to make it pretty slow.

This initialization is 100% required for this to make sure that the same x and y generates the same gradient at each integer point (so you need either a hash function or seed the RNG each time).

I attempted to change the PRNG to both the linear congruential generator and Xorshiftplus, and while both ran orders of magnitude faster, they gave odd results:

LCG (one time, then running 5 times before using) enter image description here

enter image description here

Xorshiftplus

After one iteration enter image description here

After 10,000 iterations. enter image description here

I've tried:

Running the generator several times before utilizing output, this results in slow execution or simply different artifacts.

Using the output of two consecutive runs after initial seed to seed the PRNG again and use the value after wards. No difference in result.

What is happening? What can i do to get faster results that are of the same quality as the mersenne twister?

OK BIG UPDATE:

I don't know why this works, I know it has something to do with the prime number utilized, but after messing around a bit, it appears that the following works:

Step 1, incorporate the x and y values as seeds separately (and incorporate some other offset value or additional seed value with them, this number should be a prime/non trivial factor)

Step 2, Use those two seed results into seeding the generator again back into the function (so like geza said, the seeds made were bad)

Step 3, when getting the result, instead of using modulo number of items (4) trying to get, or & 3, modulo the result by a prime number first then apply & 3. I'm not sure if the prime being a mersenne prime matters or not.

Here is the result with prime = 257 and xorshiftplus being used! (note I used 2048 by 2048 for this one, the others were 256 by 256)

enter image description here

Krupip
  • 4,404
  • 2
  • 32
  • 54
  • 1
    As an aside, why is the rng a class-member, instead of an automatic variable? – Deduplicator Jul 15 '17 at 16:32
  • 1
    You are using PRNG as a very expensive hash function. Try using actual (cryptographic?) hash function instead. – yuri kilochek Jul 15 '17 at 17:48
  • @yurikilochek How would I do that? – Krupip Jul 15 '17 at 17:50
  • @snb what is unclear? Just pass your seeds (or the coordinats directly) though the the hash function, pick two bits from the result to select your vectors – yuri kilochek Jul 15 '17 at 17:55
  • @snb: what does your last (10000 iterations) picture show? Do you mean, that you seeded xorshift, then you generated and ignored 10000 numbers, then you used the 10001st? And then, you even get this picture with these patterns? – geza Jul 15 '17 at 17:55
  • @snb: why did you leave hash method, what is the problem with them? – geza Jul 15 '17 at 17:56
  • @snb why are you using only 4 directions anyway? – yuri kilochek Jul 15 '17 at 17:57
  • @geza Yeah you got it, its kind of cryptic since I didn't show the code, but it is the equivalent of calling `distribution(rng)` 10,000 times before actually using the value from `distribution(rng)` note xorshiftplus doesn't exist in the standard library, but I made a functor for it all the same. – Krupip Jul 15 '17 at 18:06
  • @geza also to your second question I didn't use the Perlin noise hash because I didn't understand how it was derived, I didn't understand why it was done the way it was or how you would even take the steps to create such a function. in comparison I understand the ideas behind PRNGs much better and its much easier to see what my function is actually doing this way. – Krupip Jul 15 '17 at 18:08
  • @snb: Which xorshift rnd is this exactly? Maybe you've found something, that the creators of xorshift could be interested in. Newer xorshift generators accepted as very good. – geza Jul 15 '17 at 18:12
  • @snb: hmm. I'd be much worried about RNGs than a hash function, actually. You say, that you better understand how MT works, than a hash? :) I'd say that you should try hash based noise generation instead. Random generators are bad at this kind of things – geza Jul 15 '17 at 18:14
  • @geza first off, this is what I used (xorshiftplus, at the bottom) https://en.wikipedia.org/wiki/Xorshift#xorshift.2A. Second, when I say I understand RNGs better that doesn't mean I understand what MT does internally, but rather I understand the idea behind using the function. What I don't understand is how perlin derived his odd bit hash for either simplex or perlin noise, I don't understand how he crafted it. I can look up how MT works, inpts and outputs, why it works, I can't really do that with perlins junk – Krupip Jul 15 '17 at 18:17
  • @snb: okay, I don't want to argue with you:) but actually, understanding how a good random generator works, and **why** it is good, is hard. Actually, I think no-one knows why a random generator is good exactly. All we have is tests, which we use to test generators. PRNG is black art. You should check out Bob Jenkins [integer hash page](http://burtleburtle.net/bob/hash/integer.html). And you can find information at Bob's site, how a hash function designed. – geza Jul 15 '17 at 18:25
  • @yurikilochek its making/finding a hash function that works that is the issue, I'm fine with using one, just as long as its actually explained. Also I'm using four directions because that is what Perlin used. I would be fine generating the vectors themselves or using more, but for experimentation purposes I've been using four. – Krupip Jul 15 '17 at 18:39

5 Answers5

4

LCG is known to be inadequate for your purpose.

Xorshift128+'s results are bad, because it needs good seeding. And providing good seeding defeats the whole purpose of using it. I don't recommend this.

However, I recommend using an integer hash. For example, one from Bob's page.

Here's a result of the first hash of that page, it looks OK to me, and it is fast (I think it is much faster than Mersenne Twister): enter image description here

Here's the code I've written to generate this:

#include <cmath>
#include <stdio.h>

unsigned int hash(unsigned int a) {
    a = (a ^ 61) ^ (a >> 16);
    a = a + (a << 3);
    a = a ^ (a >> 4);
    a = a * 0x27d4eb2d;
    a = a ^ (a >> 15);
    return a;
}

unsigned int ivalue(int x, int y) {
    return hash(y<<16|x)&0xff;
}

float smooth(float x) {
    return 6*x*x*x*x*x - 15*x*x*x*x + 10*x*x*x;
}

float value(float x, float y) {
    int ix = floor(x);
    int iy = floor(y);
    float fx = smooth(x-ix);
    float fy = smooth(y-iy);

    int v00 = ivalue(iy+0, ix+0);
    int v01 = ivalue(iy+0, ix+1);
    int v10 = ivalue(iy+1, ix+0);
    int v11 = ivalue(iy+1, ix+1);
    float v0 = v00*(1-fx) + v01*fx;
    float v1 = v10*(1-fx) + v11*fx;
    return v0*(1-fy) + v1*fy;
}

unsigned char pic[1024*1024];

int main() {
    for (int y=0; y<1024; y++) {
        for (int x=0; x<1024; x++) {
            float v = 0;

            for (int o=0; o<=9; o++) {
                v += value(x/64.0f*(1<<o), y/64.0f*(1<<o))/(1<<o);
            }

            int r = rint(v*0.5f);

            pic[y*1024+x] = r;
        }
    }

    FILE *f = fopen("x.pnm", "wb");
    fprintf(f, "P5\n1024 1024\n255\n");
    fwrite(pic, 1, 1024*1024, f);
    fclose(f);
}

If you want to understand, how a hash function work (or better yet, which properties a good hash have), check out Bob's page, for example this.

geza
  • 28,403
  • 6
  • 61
  • 135
  • wheres your gradients? – Krupip Jul 15 '17 at 19:18
  • I didn't want to complicate this code further. It only have octave summation. For checking hash quality, it is enough. – geza Jul 15 '17 at 19:28
  • I'm confused, is this the exact code you used to create the above image? I'm confused how this works with out gradients:/ – Krupip Jul 15 '17 at 19:46
  • @snb: Yes, this is (you can try it, it is compilable. The resulting x.pnm can be viewed with gimp, for example). You don't need gradients to have a picture like this, only octave summing is needed. – geza Jul 15 '17 at 19:51
  • I thought you needed gradients; how do gradients actually help then? It looks like this result is fine. The code makes it look like random value noise. – Krupip Jul 15 '17 at 19:56
  • @snb: I'd say it is a different method to create a noise texture. The results look similar. But perlin noise (and this noise) mostly looks like this because of octave summation. If you remove the summation, the noise becomes a lot "simpler". – geza Jul 15 '17 at 20:01
  • It took me a while, but you are 100% correct, the nature of those random number generators makes consecutive inputs hard to use and generate uniform random results, they were not made for this purpose, and their avalanche effect is bad, so consecutive seeds will often share some correlation, unless you apply separate operations (ie mersenne primes or re-utilizing the output values for x and y as seeds to "randomize" it). Hash numbers are the use case for this application, we are trying to get very different results from separate numbers, not generate new numbers with no input. – Krupip Jul 17 '17 at 20:42
2

You (unknowingly?) implemented a visualization of PRNG non-random patterns. That looks very cool!

Except Mersenne Twister, all your tested PRNGs do not seem fit for your purpose. As I have not done further tests myself, I can only suggest to try out and measure further PRNGs.

Peter G.
  • 14,786
  • 7
  • 57
  • 75
  • See my updated post, apparently they *do* satisfy, with some finagling with primes, I just don't understand why! – Krupip Jul 15 '17 at 19:16
1

The randomness of LCGs are known to be sensitive to the choice of their parameters. In particular, the period of a LCG is relative to the m parameter - at most it will be m (your prime factor) & for many values it can be less.

Similarly, the careful parameters selection is required to get a long period from Xorshift PRNGs.

You've noted that some PRNGs give good procedural generation results while other do not. In order to isolate the cause, I would factor out the proc gen stuff & examine the PRNG output directly. An easy way to visualize the data is to build a grey scale image where each pixel value is a (possibly scaled) random value. For image based stuff, I find this to be an easy way to find stuff that may lead to visual artifacts. Any artifacts you see with this are likely to cause issues with your proc gen output.

Another option is to try something like the Diehard tests. If the aforementioned image test failed to reveal any problems, I might use this just to be sure my PRNG techniques were trustworthy.

Pikalek
  • 926
  • 7
  • 21
1

Note that your code seeds the PRNG, then generates one pseudorandom number from the PRNG. The reason for the nonrandomness in xorshift128+ that you discovered is that xorshift128+ simply adds the two halves of the seed (and uses the result mod 264 as the generated number) before changing its state (review its source code). This makes that PRNG considerably different from a hash function.

Peter O.
  • 32,158
  • 14
  • 82
  • 96
0

What you see is the practical demonstration of quality of PRNG. Mersenne Twister is one of the best PRNGs with good performance, it passes DIEHARD tests. One should know that generating a random numbers is not an easy computational task, so looking for a better performance will inevitably result in poor quality. LCG is known to be simplest and worst PRNG ever designed and it clearly shows two-dimensional correlation as in your picture. The quality of Xorshift generators largely depend on bitness and parameters. They are definitely worse than Mersenne Twister, but some (xorshift128+) may work good enough to pass BigCrush battery of TestU01 tests.

In other words, if you are making an important physical modelling numerical experiment, you better continue to use Mersenne Twister as known to be a good trade-off between speed and quality and it comes in many standard libraries. On a less important case you may try to use xorshift128+ generator. For an ultimate results you need to use cryptographical-quality PRNG (none of mentioned here may be used for cryptographical purposes).

Serge Goncharov
  • 488
  • 1
  • 6
  • 13
  • 3
    Don't overstate your case, LCG is ***not*** the simplest and worst PRNG ever designed. Von Neumann really stepped in it with the [middle-square method](https://en.wikipedia.org/wiki/Middle-square_method), which has fixed-point behaviors and very short cycles for some seed values. – pjs Jul 15 '17 at 17:44