150
\$\begingroup\$

Related:

I'd like to generate tileable Perlin noise. I'm working from Paul Bourke's PerlinNoise*() functions, which are like this:

// alpha is the "division factor" (how much to damp subsequent octaves with (usually 2))
// beta is the factor that multiplies your "jump" into the noise (usually 2)
// n is the number of "octaves" to add in
double PerlinNoise2D(double x,double y,double alpha,double beta,int n)
{
   int i;
   double val,sum = 0;
   double p[2],scale = 1;

   p[0] = x;
   p[1] = y;
   for (i=0;i<n;i++) {
      val = noise2(p);
      sum += val / scale;
      scale *= alpha;
      p[0] *= beta;
      p[1] *= beta;
   }
   return(sum);
}

Using code like:

real val = PerlinNoise2D( x,y, 2, 2, 12 ) ; // test

return val*val*skyColor + 2*val*(1-val)*gray + (1-val)*(1-val)*cloudColor ;

Gives sky like

nontileable

Which isn't tileable.

The pixel values are 0->256 (width and height), and pixel (0,0) uses (x,y)=(0,0) and pixel (256,256) uses (x,y)=(1,1)

How can I make it tileable?

\$\endgroup\$
1
  • 20
    \$\begingroup\$ Just FYI, what you have there isn't Perlin noise; it's fractal noise. Perlin noise is likely the "noise2" function generating each octave of the fractal noise. \$\endgroup\$ Commented Feb 10, 2012 at 6:49

10 Answers 10

101
\$\begingroup\$

There's two parts to making seamlessly tileable fBm noise like this. First, you need to make the Perlin noise function itself tileable. Here's some Python code for a simple Perlin noise function that works with any period up to 256 (you can trivially extend it as much as you like by modifying the first section):

import random
import math
from PIL import Image

perm = range(256)
random.shuffle(perm)
perm += perm
dirs = [(math.cos(a * 2.0 * math.pi / 256),
         math.sin(a * 2.0 * math.pi / 256))
         for a in range(256)]

def noise(x, y, per):
    def surflet(gridX, gridY):
        distX, distY = abs(x-gridX), abs(y-gridY)
        polyX = 1 - 6*distX**5 + 15*distX**4 - 10*distX**3
        polyY = 1 - 6*distY**5 + 15*distY**4 - 10*distY**3
        hashed = perm[perm[int(gridX)%per] + int(gridY)%per]
        grad = (x-gridX)*dirs[hashed][0] + (y-gridY)*dirs[hashed][1]
        return polyX * polyY * grad
    intX, intY = int(x), int(y)
    return (surflet(intX+0, intY+0) + surflet(intX+1, intY+0) +
            surflet(intX+0, intY+1) + surflet(intX+1, intY+1))

Perlin noise is generated from a summation of little "surflets" which are the product of a randomly oriented gradient and a separable polynomial falloff function. This gives a positive region (yellow) and negative region (blue)

Kernel

The surflets have a 2x2 extent and are centered on the integer lattice points, so the value of Perlin noise at each point in space is produced by summing the surflets at the corners of the cell that it occupies.

Summation

If you make the gradient directions wrap with some period, the noise itself will then wrap seamlessly with the same period. This is why the code above takes the lattice coordinate modulo the period before hashing it through the permutation table.

The other step, is that when summing the octaves you will want to scale the period with the frequency of the octave. Essentially, you will want each octave to tile the entire just image once, rather than multiple times:

def fBm(x, y, per, octs):
    val = 0
    for o in range(octs):
        val += 0.5**o * noise(x*2**o, y*2**o, per*2**o)
    return val

Put that together and you get something like this:

size, freq, octs, data = 128, 1/32.0, 5, []
for y in range(size):
    for x in range(size):
        data.append(fBm(x*freq, y*freq, int(size*freq), octs))
im = Image.new("L", (size, size))
im.putdata(data, 128, 128)
im.save("noise.png")

Tileable fBm Noise

As you can see, this does indeed tile seamlessly:

fBm Noise, Tiled

With some small tweaking and color mapping, here's a cloud image tiled 2x2:

Clouds!

Hope this helps!

\$\endgroup\$
11
  • 5
    \$\begingroup\$ im not a python guy, so i ask, how does x*2**o convert to C? is it: x*pow(2,o) or pow(x*2,o) ? \$\endgroup\$
    – idev
    Commented Feb 11, 2012 at 14:49
  • 11
    \$\begingroup\$ x*pow(2, o), since exponentiation has higher precedence than multiplication. \$\endgroup\$ Commented Feb 11, 2012 at 19:05
  • 1
    \$\begingroup\$ could someone convert this to C? i have huge problems understanding this code, as i have never done anything with python. for example what is a value? and im not sure how the functions convert to C... i get straight lines in output only. \$\endgroup\$
    – idev
    Commented Feb 12, 2012 at 15:06
  • 1
    \$\begingroup\$ This is definitely the best solution so long as you're fine with the domain of your noise being tied to the shape of your tile. For example, this doesn't allow arbitrary rotations. But if you don't need any such thing, this is the ideal answer. \$\endgroup\$ Commented Feb 12, 2012 at 22:49
  • 1
    \$\begingroup\$ Note: if you want to generate other size than 128, DO NOT change the numeric values on the line im.putdata(data, 128, 128). (For those unfamiliar with python or PIL: they mean scale and offset, not image size.) \$\endgroup\$ Commented Aug 30, 2013 at 3:05
102
\$\begingroup\$

Here's one rather clever way that uses 4D Perlin noise.

Basically, map the X coordinate of your pixel to a 2D circle, and the Y coordinate of your pixel to a second 2D circle, and place those two circles orthogonal to each other in 4D space. The resulting texture is tileable, has no obvious distortion, and doesn't repeat in the way that a mirrored texture would.

Copy-pasting code from the article:

for x=0,bufferwidth-1,1 do
    for y=0,bufferheight-1,1 do
        local s=x/bufferwidth
        local t=y/bufferheight
        local dx=x2-x1
        local dy=y2-y1

        local nx=x1+cos(s*2*pi)*dx/(2*pi)
        local ny=y1+cos(t*2*pi)*dy/(2*pi)
        local nz=x1+sin(s*2*pi)*dx/(2*pi)
        local nw=y1+sin(t*2*pi)*dy/(2*pi)

        buffer:set(x,y,Noise4D(nx,ny,nz,nw))
    end
end
\$\endgroup\$
11
  • 3
    \$\begingroup\$ This is definitely the right answer. Add dimensions is an old mathematician trick. Olinde Rodrigues docet (Sir W.R. Hamilton docet too but slightly less) \$\endgroup\$
    – FxIII
    Commented Feb 10, 2012 at 15:18
  • 5
    \$\begingroup\$ You can use any 4D noise function. Simplex noise would be my recommendation. webstaff.itn.liu.se/~stegu/simplexnoise/simplexnoise.pdf \$\endgroup\$ Commented Feb 10, 2012 at 20:20
  • 3
    \$\begingroup\$ thanks john! got it working, sweet! nobody said it, but: the x1,y1,x2,y2 seems to be some sort of scaling, the larger distance, the detailed noise. if this helps anyone. \$\endgroup\$
    – idev
    Commented Feb 10, 2012 at 21:02
  • 7
    \$\begingroup\$ Note that this is topologically equivalent to bobobobo's answer: your mapping embeds a 2-torus into an ℝ⁴, which is possible without the metric distortions you invevitably get when embedding it into ℝ³. \$\endgroup\$ Commented Feb 11, 2012 at 13:21
  • 1
    \$\begingroup\$ @idev: no, it just means that the two methods are more similar than it may seem at first sight. But John Calsbeek's version is quite arguably better, since it avoids the distortions. \$\endgroup\$ Commented Feb 11, 2012 at 14:46
27
\$\begingroup\$

Ok, I got it. The answer is to walk in a torus in 3D noise, generating a 2D texture out of it.

torus wraps 2 dirs

Code:

Color Sky( double x, double y, double z )
{
  // Calling PerlinNoise3( x,y,z ),
  // x, y, z _Must be_ between 0 and 1
  // for this to tile correctly
  double c=4, a=1; // torus parameters (controlling size)
  double xt = (c+a*cos(2*PI*y))*cos(2*PI*x);
  double yt = (c+a*cos(2*PI*y))*sin(2*PI*x);
  double zt = a*sin(2*PI*y);
  double val = PerlinNoise3D( xt,yt,zt, 1.5, 2, 12 ) ; // torus

  return val*val*cloudWhite + 2*val*(1-val)*gray + (1-val)*(1-val)*skyBlue ;
}

Results:

Once:

tilable sky

And tiled:

showing it tiles

\$\endgroup\$
7
  • 8
    \$\begingroup\$ It kinda works, but it looks like you're getting a bunch of distortion due to the curvature of the torus. \$\endgroup\$ Commented Feb 10, 2012 at 21:51
  • 1
    \$\begingroup\$ you can really just modulo the position, but I love all the awesome/creative answers to this question. So many different ways to do the same thing. \$\endgroup\$
    – user10968
    Commented Feb 11, 2012 at 8:57
  • \$\begingroup\$ i noticed you actually dont want to use 0-1 values, but 0-0.9999... values! so you would use: x/width, y/height etc. otherwise the seams doesnt match (makes the opposite edges exact same pixels). also it looks like the PerlinNoise3D() function needs clamping for the result value too, or some pixel values overflow. \$\endgroup\$
    – idev
    Commented Feb 11, 2012 at 15:52
  • 3
    \$\begingroup\$ @idev I believe the way to fix the distortion is to use the 4D method in the top answer of this question. ;) \$\endgroup\$ Commented Feb 11, 2012 at 16:24
  • 2
    \$\begingroup\$ Note that a 2D slice through 3D (or 4D) noise has different properties than native 2D noise. It's less band-limited, which can make it harder to get the right balance between feature detail and aliasing artifacts. See this paper for a more thorough explanation \$\endgroup\$
    – DMGregory
    Commented Aug 22, 2018 at 17:21
17
\$\begingroup\$

One simple way I can think of would be to take the output of the noise function and mirror/flip it into an image that's twice the size. It's difficult to explain so here's an image: enter image description here

Now, in this case, it's pretty obvious what you did when you look at this. I can think of two ways to (possibly :-) ) resolve this:

  1. You could take that larger image and then generate some more noise on top of it but (and I'm not sure if this is possible) focused towards the middle (so the edges stay the same). It could add the extra bit of difference that would make your brain think it's not just mirror images.

  2. (I'm also not sure if this is possible) You could try fiddling with the inputs to the noise function to generate the initial image differently. You'd have to do this by trial and error, but look for features that draw your eye when you tile/mirror it and then try and get it not to generate those.

Hope this helps.

\$\endgroup\$
6
  • 5
    \$\begingroup\$ Very nice but too symmetrical! \$\endgroup\$
    – bobobobo
    Commented Feb 10, 2012 at 18:18
  • 1
    \$\begingroup\$ @bobobobo That's what I was thinking the other steps would alleviate. So, you could generate a "base" using this method, and then add some more details over the whole thing to make it look like it's not (so) mirrored. \$\endgroup\$ Commented Feb 10, 2012 at 18:29
  • \$\begingroup\$ You start to get some weird patterns when you do this kind of thing. This one in particular looks kind of like a butterfly. Easy solution, though. \$\endgroup\$
    – notlesh
    Commented Feb 10, 2012 at 21:04
  • \$\begingroup\$ This was my first approach too, but it has a problem, visible here: dl.dropbox.com/u/6620757/noise_seam.png As you cross a flip boundary you cause a disjoint in the noise function by instantly inverting the slope of the function. Even if you apply a second noise function on top, that may still be visible in the output. \$\endgroup\$
    – Jherico
    Commented Nov 25, 2012 at 22:57
  • \$\begingroup\$ Great idea. This can be easily done in a pixel shader using triangle wave function : tex2d(abs(abs(uv.x)%2.0-1.0), abs(abs(uv.y)%2.0-1.0)) \$\endgroup\$
    – tigrou
    Commented May 16, 2016 at 12:13
13
\$\begingroup\$

First version of this answer was actually wrong, I've updated it

A method I used successfully is make noise domain tiled. In other words, make your base noise2() function periodical. If noise2() is periodic and beta is integer, resulting noise will have the same period as noise2().

How can we make noise2() periodic? In most implementations, this function uses some kind of lattice noise. That is, it gets random numbers at integer coordinates, and interpolates them. For example:

function InterpolatedNoise_1D(float x)

  integer_X    = int(x)
  fractional_X = x - integer_X

  v1 = SmoothedNoise1(integer_X)
  v2 = SmoothedNoise1(integer_X + 1)

  return Interpolate(v1 , v2 , fractional_X)

end function

This function can be trivially modified to become periodic with integer period. Simply add one line:

integer_X = integer_X % Period

before calculating v1 and v2. This way, values at integer coordinates will repeat every Period units, and interpolation will ensure that resulting function is smooth.

Note, however, that this only works when Period is more than 1. So, to actually use this in making seamless textures, you'd have to sample a Period x Period square, not 1x1.

\$\endgroup\$
5
  • \$\begingroup\$ But how do you make noise2 periodic (with a short period such as 1 unit)? I think that's what the question is ultimately asking. Standard Perlin noise is periodic with a period of 256 on each axis but you want a modified noise with a smaller period. \$\endgroup\$ Commented Feb 10, 2012 at 6:52
  • \$\begingroup\$ @Nathan Reed If you call noise2 as suggested, you will get periodic results, whether the function itself is periodic or not. Because the arguments wrap around every 1 unit. \$\endgroup\$
    – Nevermind
    Commented Feb 10, 2012 at 7:01
  • 1
    \$\begingroup\$ But then you get seams at the grid lines, don't you? Since there's no guarantee that noise2(0, 0.999) is anything near noise2(0, 0), unless I've missed something. \$\endgroup\$ Commented Feb 10, 2012 at 17:30
  • 1
    \$\begingroup\$ @Nathan Reed That's a good point. In fact, I just re-checked my old code and it turns out I was wrong. I'll edit the answer now. \$\endgroup\$
    – Nevermind
    Commented Feb 10, 2012 at 18:57
  • \$\begingroup\$ Great! This is actually a good answer now. +1 :) \$\endgroup\$ Commented Feb 10, 2012 at 19:31
9
\$\begingroup\$

Though there are some answers here that would work, most of them are complicated, slow and problematic.

All you really need to do is use a periodic noise generation function. That's it!

An excellent public domain implementation based on Perlin's "advanced" noise algorithm can be found here. The function you need is pnoise2. The code was written by Stefan Gustavson, who has made a pointed comment here about exactly this issue, and how others have taken the wrong approach. Listen to Gustavson, he knows what he's talking about.

Regarding the various spherical projections some here have suggested: well, they in essence work (slowly), but they also produce a 2D texture that is a flattened sphere, so that the edges would more condensed, likely producing an undesired effect. Of course, if you intend for your 2D texture to be projected onto a sphere, that's the way to go, but that's not what was being asked for.

\$\endgroup\$
6
\$\begingroup\$

Another alternative is to generate noise using libnoise libraries. You can generate noise over a theoretical infinite amount of space, seamlessly.

Take a look at the following: http://libnoise.sourceforge.net/tutorials/tutorial3.html#tile

There is also an XNA port of the above at: http://bigblackblock.com/tools/libnoisexna

If you end up using the XNA port, you can do something like this:

Perlin perlin = new Perlin();
perlin.Frequency = 0.5f;                //height
perlin.Lacunarity = 2f;                 //frequency increase between octaves
perlin.OctaveCount = 5;                 //Number of passes
perlin.Persistence = 0.45f;             //
perlin.Quality = QualityMode.High;
perlin.Seed = 8;

//Create our 2d map
Noise2D _map = new Noise2D(CHUNKSIZE_WIDTH, CHUNKSIZE_HEIGHT, perlin);

//Get a section
_map.GeneratePlanar(left, right, top, down);

GeneratePlanar is the function to call to get the sections in each direction that will connect seamlessly with the rest of the textures.

Of course, this method is more costly than simply having a single texture that can be used across multiple surfaces. If you are looking to create some random tileable textures, this may be something that interests you.

\$\endgroup\$
5
\$\begingroup\$

Here's a much much simpler way to do tiled noise:

tiling perlin noise from shadertoy code

You use a modular wrap around for each scale of the noise. These fit the edges of the area no matter what frequency scale you use. So you only have to use normal 2D noise which is a lot faster. Here is the live WebGL code which can be found at ShaderToy: https://www.shadertoy.com/view/4dlGW2

The top three functions do all the work, and fBM is passed a vector x/y in a 0.0 to 1.0 range.

// Tileable noise, for creating useful textures. By David Hoskins, Sept. 2013.
// It can be extrapolated to other types of randomised texture.

#define SHOW_TILING
#define TILES 2.0

//----------------------------------------------------------------------------------------
float Hash(in vec2 p, in float scale)
{
    // This is tiling part, adjusts with the scale...
    p = mod(p, scale);
    return fract(sin(dot(p, vec2(35.6898, 24.3563))) * 353753.373453);
}

//----------------------------------------------------------------------------------------
float Noise(in vec2 x, in float scale )
{
    x *= scale;

    vec2 p = floor(x);
    vec2 f = fract(x);
    f = f*f*(3.0-2.0*f);
    //f = (1.0-cos(f*3.1415927)) * .5;
    float res = mix(mix(Hash(p,                  scale),
        Hash(p + vec2(1.0, 0.0), scale), f.x),
        mix(Hash(p + vec2(0.0, 1.0), scale),
        Hash(p + vec2(1.0, 1.0), scale), f.x), f.y);
    return res;
}

//----------------------------------------------------------------------------------------
float fBm(in vec2 p)
{
    float f = 0.4;
    // Change starting scale to any integer value...
    float scale = 14.0;
    float amp = 0.55;
    for (int i = 0; i < 8; i++)
    {
        f += Noise(p, scale) * amp;
        amp *= -.65;
        // Scale must be multiplied by an integer value...
        scale *= 2.0;
    }
    return f;
}

//----------------------------------------------------------------------------------------
void main(void)
{
    vec2 uv = gl_FragCoord.xy / iResolution.xy;

#ifdef SHOW_TILING
    uv *= TILES;
#endif

    // Do the noise cloud (fractal Brownian motion)
    float bri = fBm(uv);

    bri = min(bri * bri, 1.0); // ...cranked up the contrast for no reason.
    vec3 col = vec3(bri);

#ifdef SHOW_TILING
    vec2 pixel = (TILES / iResolution.xy);
    // Flash borders...
    if (uv.x > pixel.x && uv.y > pixel.y                                        // Not first pixel
    && (fract(uv.x) < pixel.x || fract(uv.y) < pixel.y) // Is it on a border?
    && mod(iGlobalTime-2.0, 4.0) < 2.0)                 // Flash every 2 seconds
    {
        col = vec3(1.0, 1.0, 0.0);
    }
#endif
    gl_FragColor = vec4(col,1.0);
}
\$\endgroup\$
1
  • 2
    \$\begingroup\$ Your image link went dead. I took a best guess & replaced it with a screen shot of the output from the shadertoy code you posted. If that's not correct, please re-upload your intended image directly to the Stack Exchange server. \$\endgroup\$
    – Pikalek
    Commented Aug 22, 2018 at 14:26
3
\$\begingroup\$

I had some not-bad results interpolating near the edges of the tile (edge-wrapped), but it depends on what effect you're trying to achieve and the exact noise parameters. Works great for somewhat blurry noise, not so good with spikey/fine-grained ones.

\$\endgroup\$
1
\$\begingroup\$

I was checking this thread in search for an answer to a similar issue, then I got a clean and compact solution from the developer of this python code to generate fractal noise out of perlin/simplex noise. The updated code is provided in this (closed) issue and can be resumed in setting the gradients for the right side of the "generator" equal to those on the left side (and same for top and bottom), such as in

# Gradients
angles = 2*np.pi*np.random.rand(res[0]+1, res[1]+1)
gradients = np.dstack((np.cos(angles), np.sin(angles)))
# Make the noise tileable
gradients[-1,:] = gradients[0,:]
gradients[:,-1] = gradients[:,0]

Seems like an elegant and clean solution, I avoid copying the whole code here (since it's not my own solution), but it's available at the link given above. Hope this can be useful to someone looking to produce a tileable fractal 2d image like this one I needed, free of artifacts or distorsions.

tileable fractal terrain

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .