Perlin Noise - seamless cube maps

Started by
9 comments, last by Aardvajk 7 years, 8 months ago
I'm trying to generate some Perlin Noise cube maps and don't seem to be able to get them to tile. I'm not sure if my noise function is at fault or if it is the way I'm mapping to the image.

The code for generating the map is very slap-dash, just thrown together in a Qt application I was using to generate 2D noise maps.

Here's the noise method first. This is heavily based on Sean O'Neil's Sandbox project, just reformatted to my own taste and explicitly three dimensional:

class Noise
{
public:
    explicit Noise(unsigned int seed);

    float noise(float f[3]) const;

protected:
    float lattice(int ix, float fx, int iy=0, float fy=0, int iz=0, float fz=0, int iw=0, float fw=0) const;

    unsigned char perm[256];
    float buffer[256][3];
};

Noise::Noise(unsigned int seed)
{
    std::srand(seed);

    for(int i = 0; i < 256; ++i)
    {
        perm[i] = i;
        for(int j = 0; j < 3; ++j)
        {
            buffer[i][j] = (float)random(-0.5, 0.5);
        }

        float magnitude = 0;
        for(int j = 0; j < 3; ++j)
        {
            magnitude += buffer[i][j] * buffer[i][j];
        }

        magnitude = 1 / sqrtf(magnitude);
        for(int j = 0; j < 3; ++j)
        {
            buffer[i][j] *= magnitude;
        }
    }

    for(int i = 0; i < 256; ++i)
    {
        int j = static_cast<int>(random(0, 255));
        std::swap(perm[i], perm[j]);
    }
}

float Noise::noise(float f[3]) const
{
    int n[3];
    float r[3];
    float w[3];

    for(int i = 0; i < 3; ++i)
    {
        n[i] = floor(f[i]);
        r[i] = f[i] - n[i];
        w[i] = cubic(r[i]);
    }

    float value = lerp(lerp(lerp(lattice(n[0], r[0], n[1], r[1], n[2], r[2]), lattice(n[0]+1, r[0]-1, n[1], r[1], n[2], r[2]), w[0]),
                       lerp(lattice(n[0], r[0], n[1]+1, r[1]-1, n[2], r[2]), lattice(n[0]+1, r[0]-1, n[1]+1, r[1]-1, n[2], r[2]), w[0]),
                       w[1]),
                  lerp(lerp(lattice(n[0], r[0], n[1], r[1], n[2]+1, r[2]-1), lattice(n[0]+1, r[0]-1, n[1], r[1], n[2]+1, r[2]-1), w[0]),
                       lerp(lattice(n[0], r[0], n[1]+1, r[1]-1, n[2]+1, r[2]-1), lattice(n[0]+1, r[0]-1, n[1]+1, r[1]-1, n[2]+1, r[2]-1), w[0]),
                       w[1]),
                  w[2]);

    return clamp(value * 2.0f, -0.99999f, 0.99999f);
}

float Noise::lattice(int ix, float fx, int iy, float fy, int iz, float fz, int iw, float fw) const
{
    int n[4] = { ix, iy, iz, iw };
    float f[4] = { fx, fy, fz, fw };

    int index = 0;
    for(int i = 0; i < 3; ++i)
    {
        index = perm[(index + n[i]) & 0xFF];
    }

    float value = 0;
    for(int i = 0; i < 3; ++i)
    {
        value += buffer[index][i] * f[i];
    }

    return value;
}
Now I believe I should be able to pass 3D direction vectors to this to map the noise onto the surface of a sphere. Perhaps I'm wrong here.

In my temporary application, I'm currently generating the Back and Right faces. Essentially I do this (dim is a global dimension of each side of each face of the cube map).

QImage generate(Face face, Noise &noise)
{
    QImage image(dim, dim, QImage::Format_ARGB32);

    for(int iy = 0; iy < dim; ++iy)
    {
        for(int ix = 0; ix < dim; ++ix)
        {
            float v[3];
            createDirection(face, v, ix, iy);

            float n = generateNoise(noise, v);

            image.setPixel(ix, iy, color(n));
        }
    }

    return image;
}

float generateNoise(Noise &noise, float v[3])
{
    float n = noise.noise(v);
    return (n + 1.0f) / 2.0f;
Create direction is implemented like this:

void createDirection(Face face, float *v, int ix, int iy)
{
    float x = static_cast<float>(ix);
    float y = static_cast<float>(iy);

    QVector3D vec;

    switch(face)
    {
        case Face::Back: vec = QVector3D(x, y, 1); break;
        case Face::Right: vec = QVector3D(1, y, x); break;

        default: break;
    }

    float hd = static_cast<float>(dim) / 2.0f;
    QVector3D centroid(hd, hd, hd);

    QVector3D to = vec - centroid;
    to = to.normalized();

    v[0] = to.x();
    v[1] = to.y();
    v[2] = to.z();
}
So first of all, construct the position of the pixel in an imaginary cube map space, then form a vector from the middle of the cube to this point, to produce a directional vector, normalise and return. Forgive all the shenanigans converting from one type to another, just rough code here.

The outputs are nice, smooth noise maps, but they refuse to tile.

Back:
[attachment=33011:back.png]

Right:
[attachment=33012:right.png]

I'm not sure if I am getting the 3D noise function wrong, or if there is some mistake in the way I am mapping pixels to directional vectors, or what the problem is. I was under the impression that as I walked around the noise functions with directional vectors around a sphere, I'd get tiling happening.

Appreciate this is a big ask, but can anyone help me out here? Thanks in advance.
Advertisement

Did you test your 3d noise function separately?

Just generate a 3d noise bitmap and draw it a slice at a time and see if you get a fluent animation.


    switch(face)
    {
        case Face::Back: vec = QVector3D(x, y, 1); break;
        case Face::Right: vec = QVector3D(1, y, x); break;

        default: break;
    }

I'm not sure about this (maybe I'm not seeing it right) - shouldn't you be using dim/2 here instead of the 1 constant?

Your code isn't doing exactly what you expect it to. You'd probably get better results if you divide x and y by dim before you subtract the centroid.

Any box in 3D space is going to map from the coords (x1,y1,z1) to (x2,y2,z2). The mapping ranges for each of the faces will derive from these extents. For example, the back face will map from (x1,y1,z2) to (x2,y2,z2) while the right face will map from (x2,y1,z1) to (x2,y2,z2). And thus, the back and the right faces will share an edge, delineated by the line segment from (x2,y1,z2) to (x2,y2,z2). So, say you have a cube of dim=512. You want to center it at 0, so your cube extents would be (-256,-256,-256) to (256,256,256). However, in your case (assuming image dim=512) you would be mapping from (0,0,1) to (512,512,1) on the back face (before the centroid is subtracted). But the right face maps from (1,0,0) to (1,512,512). The back and right faces in this case don't share an edge.

So you could either divide x and y by dim before subtracting the centroid, so that your cube extents become (0,0,0)->(1,1,1), or you could use dim for the extents so that your cube becomes (0,0,0)->(dim,dim,dim). It doesn't really matter, since you simply normalize the coordinates to unit length.
Ah, indeed, good spot. Thanks, guys. Will investigate this and see what happens.
Hmm. Changed to:

void createDirection(Face face, float *v, int ix, int iy)
{
    float x = static_cast<float>(ix);
    float y = static_cast<float>(iy);

    QVector3D vec;

    switch(face)
    {
        case Face::Back: vec = QVector3D(x, y, dim); break;
        case Face::Right: vec = QVector3D(dim, y, x); break;

        default: break;
    }

    float hd = static_cast<float>(dim) / 2.0f;
    QVector3D centroid(hd, hd, hd);

    QVector3D to = vec - centroid;
    to = to.normalized();

    v[0] = to.x();
    v[1] = to.y();
    v[2] = to.z();
}
But same issue - it still doesn't tile. I do get different noise patterns to before, but there is still an obvious seam between the two.

I've just been inspecting the output vectors and seem to be correct. E.g. with a dim of 16, 8,8 for Back is (0, 0, 1) and 8, 8 for Right i s(1, 0, 0).

Do you think my noise function is at fault instead here? Does anyone have a sample noise function that is confirmed to work correctly with 3D unit vectors around a sphere?

[EDIT] I tried replacing the noise function with a completely different one and same issue. Good individual noise generated, but the tiling doesn't work. I'm starting to wonder if I have the wrong end of the stick here. I'm assuming with a 3D noise function, I can pass in normalized unit vectors and get seamless noise around the surface of a sphere. I'm sure my vectors are correct now, after the help above. Am I perhaps misunderstanding how to use a 3D noise function?

Did you test your 3d noise function separately?
Just generate a 3d noise bitmap and draw it a slice at a time and see if you get a fluent animation.


That's a good idea. Will try that later. Thanks.

Shouldn't you flip certain coords, e.g. for back (dim - 1 - x, y, dim) or something ?

Woo hoo. Just figured it out, came in to share the good news and see unbird figured it out in the meantime too :)

case Face::Right: vec = QVector3D(dim, y, dim - x); break;
dim - x. I was using x as the z coord for the right face, so it was iterating across the face from front to back instead of the other way around.

I ended up moving the createDirection method into an old game and drawing the vectors on the screen. Noticed then that what should have been the same two vectors (Back: dim, 0 and Right: 0, 0) had reversed Z coords which led me to the answer.

I now have two perfectly tiling Back and Right textures.

Thanks so much for all the help guys :)

Do you think my noise function is at fault instead here? Does anyone have a sample noise function that is confirmed to work correctly with 3D unit vectors around a sphere?

[EDIT] I tried replacing the noise function with a completely different one and same issue. Good individual noise generated, but the tiling doesn't work. I'm starting to wonder if I have the wrong end of the stick here. I'm assuming with a 3D noise function, I can pass in normalized unit vectors and get seamless noise around the surface of a sphere. I'm sure my vectors are correct now, after the help above. Am I perhaps misunderstanding how to use a 3D noise function?


I am not so sure that the noise function is at fault here. In fact (if I'm not mistaken) you should be able to replace your noise function with some simple mapping of xyz -> rgb and the result should be a continuous colour gradient. If it is *not* continuous then the issue must be with your sampling vector.

My suspicion is that you're still not generating your sampling vector correctly compared with where you render the resultant pixel after you sample.

If we think about the common vertices for the shared edge between the back face and the right face. Your createDirection function needs to return the same sampling vector for these vertices for both faces - otherwise they won't tile.

Let us say that the top-right corner of the back face is (ix=dim, iy=dim) and it coincides with the top-left corner of the right face at (ix=0, iy=dim). This means that these two calls should produce the same sampling vector:

createDirection(Face::Back, v, dim, dim)
createDirection(Face::Right, v, 0, dim)

Right now, I don't believe they will.

You might be rotating or inverting the images (effectively using a different coordinate space to the one I've used here), but I don't see evidence of that currently.

Edit: Waaaay too slow

This topic is closed to new replies.

Advertisement