Create seamless noise maps

image

Creating a seamless image in Photoshop is easy: crop the image, grab the trimmed right and bottom parts, and then glue them to the left and top using the Fade tool. But for the proper implementation of seamless noise maps, you have to think carefully.

If you have a basic understanding of Perlin noise , then you know that it consists of interpolated random numbers. It is mainly used in two dimensions. But it is also useful in one dimension (for example, when moving), in three dimensions (cylindrical and spherical transformation of 3D objects), and even in four or five dimensions.

Four-dimensional noise can be used to create a seamless 2D image. It is not very common for us to think in four dimensions, so we will take one dimension at a time.

In my examples, I used two-octave simplex noise . Simplex noise is faster in large dimensions, and due to its triangular nature, it looks better.

I wrote a small function drawNoiseto create a canvas and process a pixel array in a loop.

One-dimensional seamless noise


In one dimension, noise is an infinite smooth line (my implementation of noise starts with two, so I use a constant as the second parameter). Here we see that these are just interpolated random numbers.

// one dimensional line
fNoiseScale = .02;
drawNoise(function(i,x,y){
    var v = Simplex.noise(
         123+x*fNoiseScale
        ,137 // we just need one dimension so this parameter is a constant
    );
    return v*iSize>y?255:0;
}).img();


One-dimensional noise

You can use this in animation, recalculating the noise value every millisecond, but you can also create a loop and calculate all the values โ€‹โ€‹in advance. The values โ€‹โ€‹in the image above do not loop around the edges. But implementing repeatability is quite simple, just one more dimension and loop ... or circle, for that.

One-dimensional loop


For most of you, Perlinโ€™s noise looks something like the image below.


If we drew a circle here and counted the noise values โ€‹โ€‹on this circle, we would get a one-dimensional loop.


Noise with a circle to create a one-dimensional loop.

In code, it looks like this:

// one dimensional loop
drawNoise(function(i,x,y){
    var fNX = x/iSize // we let the x-offset define the circle
        ,fRdx = fNX*2*Math.PI // a full circle is two pi radians
        ,a = fRdsSin*Math.sin(fRdx)
        ,b = fRdsSin*Math.cos(fRdx)
        ,v = Simplex.noise(
             123+a*fNoiseScale
            ,132+b*fNoiseScale
        )
    ;
    return v*iSize>y?255:0;
}).img().div(2);


You probably already understood what we're going to. To loop a two-dimensional image, we need a three-dimensional (at least) noise map.

Cylindrical card


Noise Perlin was originally created for continuous 3D-texturing (the film "Tron"). The image map is not a sheet of paper wrapped around an object, but is calculated by its location in a three-dimensional noise field. Therefore, when cutting the object, we can still calculate the map for the newly created surface.

Before we reach our ultimate goal of a seamless image, we first create an image that seamlessly joins left and right. This is similar to a two-dimensional circle for a one-dimensional loop, but with one additional dimension: a cylinder.

// three dimensional cylindrical map
drawNoise(function(i,x,y){
    var fNX = x/iSize
        ,fRdx = fNX*2*Math.PI
        ,a = fRdsSin*Math.sin(fRdx)
        ,b = fRdsSin*Math.cos(fRdx)
        ,v = Simplex.noise(
             123+a*fNoiseScale
            ,132+b*fNoiseScale
            ,312+y*fNoiseScale // similar to the one dimensional loop but we add a third dimension defined by the image y-offset
        )
    ;
    return v*255<<0;
}).img().div(2);


Cylindrical noise map

Spherical map image


You might think that it would be convenient to use a sphere to create a seamless image, but you are mistaken.

I will make a small digression and show how the spherical image map is calculated and what it looks like.

// three dimensional spherical map
document.body.addChild('h2').innerText = 'three dimensional spherical map';
fNoiseScale = .1;
var oSpherical = drawNoise(function(i,x,y){
    var  fNX = (x+.5)/iSize // added half a pixel to get the center of the pixel instead of the top-left
        ,fNY = (y+.5)/iSize
        ,fRdx = fNX*2*Math.PI
        ,fRdy = fNY*Math.PI // the vertical offset of a 3D sphere spans only half a circle, so that is one Pi radians
        ,fYSin = Math.sin(fRdy+Math.PI) // a 3D sphere can be seen as a bunch of cicles stacked onto each other, the radius of each of these is defined by the vertical position (again one Pi radians)
        ,a = fRdsSin*Math.sin(fRdx)*fYSin
        ,b = fRdsSin*Math.cos(fRdx)*fYSin
        ,c = fRdsSin*Math.cos(fRdy)
        ,v = Simplex.noise(
             123+a*fNoiseScale
            ,132+b*fNoiseScale
            ,312+c*fNoiseScale
        )
    ;
    return v*255<<0;
}).img();


Spherical noise map


Sphere with noise

Cubic panoramic map


The sphere we created can also be used as a panorama if you place a camera in the center of the sphere. But the best way would be to use a cubic panorama, because it has much fewer faces. The sphere is projected onto the six sides of the cube, as shown in this sketch.


Superimposing a sphere on a cube

For each pixel on the surface of the cube, we need to calculate the intersection between the viewpoint C in the center and the sphere. It may seem complicated, but it's actually pretty simple.

We can consider the CA line as a vector. And vectors can be normalized so that their direction does not change, but the length decreases to 1. Due to this, all vectors together will look like a sphere.

Normalization is also quite simple, we just need to divide the values โ€‹โ€‹of the vector by xyz by the total length of the vector. The length of the vector can be calculated using the Pythagorean theorem.

In the code below, the normalization calculation is first performed on one face. Then the noise is computed simultaneously for all six edges, because to get the position of the next face, you just need to flip the values โ€‹โ€‹along xyz.

// 3D panoramical cube map
document.body.addChild('h2').innerText = '3D panoramical cube map';
// we're not using the drawNoise function because our canvas is rectangular
var mCubemap = document.createElement('canvas')
    ,iW = 6*iSize;
mCubemap.width = iW;
mCubemap.height = iSize;
var  iHSize = iSize/2 // half the size of the cube
    ,oCtx = mCubemap.getContext('2d')
    ,oImgData = oCtx.getImageData(0,0,iW,iSize)
    ,aPixels = oImgData.data
    ,aa = 123
    ,bb = 231
    ,cc = 321
;
for (var i=0,l=iSize*iSize;i<l;i++) {
    var  x = i%iSize        // x position in image
        ,y = (i/iSize)<<0    // y position in image
        ,a = -iHSize + x+.5    // x position on the cube plane, the added .5 is to get the center of the pixel
        ,b = -iHSize + y+.5 // y position on the cube plane
        ,c = -iHSize        // z position of the cube plane
        ,fDistanceAB = Math.sqrt(a*a+b*b) // to calculate the vectors length we use Pythagoras twice
        ,fDistanceABC = Math.sqrt(fDistanceAB*fDistanceAB+c*c)
        ,fDrds = .5*fDistanceABC // adjust the distance a bit to get a better radius in the noise field
        ,v = 1
    ;
    a /= fDrds; // normalize the vector
    b /= fDrds; // normalize the vector
    c /= fDrds; // normalize the vector
    //
    // since we now know the spherical position for one plane we can derive the positions for the other five planes simply by switching the x, y and z values (the a, b and c variables)
    var aNoisePositions = [
         [a,b,c]    // back
        ,[-c,b,a]    // right
        ,[-a,b,-c]    // front
        ,[c,b,-a]    // left
        ,[a,c,-b]    // top
        ,[a,-c,b]    // bottom
    ];
    for (var j=0;j<6;j++) {
        v = Simplex.noise(
             aa + aNoisePositions[j][0]
            ,bb + aNoisePositions[j][1]
            ,cc + aNoisePositions[j][2]
        );
        var pos = 4*(y*iW+j*iSize+x); // the final position of the rgba pixel
        aPixels[pos] = aPixels[pos+1] = aPixels[pos+2] = v*255<<0;
        aPixels[pos+3] = 255;
    }
}
oCtx.putImageData(oImgData,0,0);
document.body.addChild('img',{src:mCubemap.toDataURL("image/jpeg")});

Here are six sides in one image, plus a screenshot of how it looks when viewed from a cube. The source code has a 3D example written in threejs .


Cubic panoramic map


Seamless 2D Image


It may seem that a seamless 2D image is easy to implement, but it seems to me that this is the most difficult of the described in the article, because to understand it you need to think in four dimensions. The closest thing to this was a cylindrical map (with horizontal repetition), so we will take it as a basis. In the cylindrical map, we used the horizontal position of the image for the circle; that is, the horizontal position of the image gives us two coordinates x and y in the noise field xyz. The vertical position of the image corresponds to z in the noise field.

We want the image to be seamless and vertical, so if we add another dimension, we can use it to create a second circle and replace the linear value of the z field. This is similar to creating two cylinders in a four-dimensional field. I tried to visualize this on a sketch, it is inaccurate, but I tried to convey the general principle, and not draw a four-dimensional cylinder.


A sketch of two cylinders in four dimensions

The code is quite simple: these are just two circles in a four-dimensional noise space.

// four dimensional tile
fNoiseScale = .003;
drawNoise(function(i,x,y){
    var  fNX = x/iSize
        ,fNY = y/iSize
        ,fRdx = fNX*2*Math.PI
        ,fRdy = fNY*2*Math.PI
        ,a = fRds*Math.sin(fRdx)
        ,b = fRds*Math.cos(fRdx)
        ,c = fRds*Math.sin(fRdy)
        ,d = fRds*Math.cos(fRdy)
        ,v = Simplex.noise(
             123+a*fNoiseScale
            ,231+b*fNoiseScale
            ,312+c*fNoiseScale
            ,273+d*fNoiseScale
        )
    ;
    return (Math.min(Math.max(2*(v -.5)+.5,0),1)*255)<<0;
}).img().div(2,2);

And here is the result:

image

All Articles