Creating a Simple 2D Noise

We have explained most of the techniques used for creating noise in the previous chapter. Creating higher dimensional noise should be from now on much more straightforward task, as they are all based on the same concepts and techniques. Be sure to have mastered and truly understood what we have described in the previous chapter before you move on. If things are not quite clear yet, read it again or drop us an email if you have questions.

我们在前面一篇已经解释了创建一个noise function 所需的大部分技术。创建一个多维的noise function 现在应该不是什么特别困难的事情了(PS:我只流泪不说话T_T...),多维noise函数是建立在相同的思路和技术上的。(PS:只是采样、样本分布方式不一样)确保你完全理解了前一章的所有知识点,如果没有,就再读一次。

Remember that all noise functions return a float but that its input can be a float, a 2D point or a 3D point. The name given to the noise function is related to the dimension of its input value. A 2D noise is therefore a noise function that takes a 2D point as input. A 3D noise takes a 3D point and we even mentioned a 4D noise in the first chapter of this lesson where the fourth dimension accounts in fact for time (it will produce a noise pattern based on a 3D point but animated through time).

记住所有的noise函数都返回一个float , 但是它们的输入可以是 float, 或 2d, 3d 的点。 noise function的名字和它接受的输入参数的维度相关。

一个2D Noise Function 表示它接受2D Point作为输入参数。3DNoise 就接受Vector3咯。我们在序章也提到过有4Dnoise 函数。 第4维是时间。(它基本上是3DNoise,但是会根据时间改变整个结果集的相位)。

PS: 有真正的4D输入,貌似是从数学角度做的多维度定义,感兴趣的同学可以找一下Simplex扩展到4D,5D的情况。

Figure 23: for a 2D noise the random values are created at the vertex positions of a regular grid.


图23:一个4*4的坐标系

If you read the lesson on Interpolation (we suggest you do it now if you have not) you may already have an idea of how things work for 2D noise. For the 2D case we will distribute random values at the vertex position of a regular 2D grids (figure xx). The 2D version of the noise function will take a 2D point as input. Lets call this point P. Similarly to the 1D version we will need to find the position of that point on the grid. Like for the 1D case, our 2D grid has a predefined resolution (the grid is square so the resolution is the same along the x and y axis). Lets say 4 (which is a power of two) for the sake of this explanation. We will use the same modulo trick to remap the position of P on the grid if the point is outside the grid boundaries (if P's coordinates are lower than 0 or greater than 4). We will perform the modulo operation on P's x and y coordinates. That will give us a new coordinates for the point on the grid (lets call this new point Pnoise).

如果你已经阅读了”插值“相关的课程,你可能已经知道如何处理2DNoise了。

首先我们确保任何x,y的输入都通过取模运算折算到我们规定的4*4周期内。

Figure 24:XX REDO THIS ILLUSTRATION XX


图24:取模运算在2D时的效果

As you can see on figure 25, the point is surrounded by the four vertices of a cell. We will use the same technique described in the lesson on interpolation to find a value for that point by linearly interpolating the values from the cell corners. To do this, we will first compute tx and ty which are the counterpart of t in our 1D noise.

Figure 25: bilinear interpolation


图25:双线性插值

We can now interpolate the values from the two corners on the left with the values from the two corners on the right using tx. That would give us two values nx0 and nx1 which corresponds to the linear interpolation along the x axis of c01/c10 (nx0) and c01/c11 (nx1). We have now two interpolated values on the lower and upper edge of the cell vertically aligned on Pnoise's x coordinate that we will in turn linearly interpolate using ty. The result of this interpolation along the y axis, is our final noise value for P.


1、在 y = floor( input.y) 时, 对x 做插值 得到nx1。 (a,b可以通过查询原始噪点得到)

2、在 y = ceiling( input.y) 时,对x 做插值, 得到nx2。( a,b可以通过查询原始噪点得到)

3、对y做插值,a = nx1 , b = nx2, 得到最终 noise(x,y)


Here is the code of our simple 2D value noise. The size of the grid is 256 on each side. Note that like for the 1D noise, it is possible to change the remapping function for t. In this version we have chosen the Smoothstep function but you could experiment by ignoring the function (using tx and ty directly), using the Cosine smooth function or any alternative smooth function you know about.


这里有一段代码,不过我们这里用的是256*256的周期。在这里我们使用了 smoothstep插值函数来处理 tx,和ty。作为实验目的,你可以试着不使用平滑函数、consine或者其他你知道的平滑函数。

inline int floor( float x )
{
    return (int)x - ( x < 0 && (int)x != x );
}
     
inline float Smoothstep( const float & t )
{
    return t * t * ( 3 - 2 * t );
}
     
///
/// Basic implementation of a 2D value noise
///
class Simple2DNoiseA
{
public:
    Simple2DNoiseA( unsigned seed = 2011 )
    {
        srand48( seed );
        for ( unsigned i = 0; i < kMaxVertices * kMaxVertices; ++i )
        {
            r[ i ] = drand48();
        }
    }
     
    /// Evaluate the noise function at position x
    float eval( const Point2f &pt ) const
    {
        int xi = floor( pt.x );
        int yi = floor( pt.y );
     
        float tx = pt.x - xi;
        float ty = pt.y - yi;
     
        int rx0 = xi & kMaxVerticesMask;
        int rx1 = ( rx0 + 1 ) & kMaxVerticesMask;
        int ry0 = yi & kMaxVerticesMask;
        int ry1 = ( ry0 + 1 ) & kMaxVerticesMask;
     
        /// Random values at the corners of the cell
        const float & c00 = r[ ry0 * kMaxVerticesMask + rx0 ];
        const float & c10 = r[ ry0 * kMaxVerticesMask + rx1 ];
        const float & c01 = r[ ry1 * kMaxVerticesMask + rx0 ];
        const float & c11 = r[ ry1 * kMaxVerticesMask + rx1 ];
     
        /// Remapping of tx and ty using the Smoothstep function
        float sx = Smoothstep( tx );
        float sy = Smoothstep( ty );
     
        /// Linearly interpolate values along the x axis
        float nx0 = Mix( c00, c10, sx );
        float nx1 = Mix( c01, c11, sx );
     
        /// Linearly interpolate the nx0/nx1 along they y axis
        return Mix( nx0, nx1, sy );
    }
    static const unsigned kMaxVertices = 256;
    static const unsigned kMaxVerticesMask = kMaxVertices - 1;
    float r[ kMaxVertices * kMaxVertices ];
};
     
     
void NoiseImage()
{
    Simple2DNoiseA simpleNoise2D;
    std::ofstream ofs( "./noise2D.ppm" );
    unsigned imageWidth = 512, imageHeight = 512;
    float invImageWidth = 1.f / imageWidth, imvImageHeight = 1.f / imageHeight;
    ofs << "P6\n" << imageWidth << " " << imageHeight << "\n255\n";
    for ( unsigned j = 0; j < imageHeight; ++j )
    {
        for ( unsigned i = 0; i < imageWidth; ++i )
        {
            Point2f pnoise = Point2f( i * invImageWidth, j * imvImageHeight ) * 10;
            float n = simpleNoise2D.eval( pnoise );
            unsigned char c = static_cast( n * 255 );
            ofs << c << c << c;
        }
    }
    ofs.close();
}


If you you run the code it will produce the following image (left):

如果你运行上述代码,你会生成下面左边边这张图

Figure 26: on the left, the result of our noise function. On the right, the result of the Ken Perlin version of the noise function. His version looks much more convincing that ours, but in this lesson we are just learning about the concepts upon which it is built and its properties. To learn about Perlin's noise, check the lesson Noise part 2.


图26: 左图是我们的noise函数生成的图。右边是Ken Perlin的noise函数生成的图。PerlinNoise的结果更加自然,我们在这里只需要确保自己学会Noise是如何工作的。如果想了解PerlinNoise ,可以查阅Noise Part 2(PS:已经确认没有这个栏目 T_T )


Note that the result is probably not as good as we had hoped for. The resulting noise is quite blocky. Noise from professional applications produces much smoother results. Remember that this lesson's goal is to teach you the basics concepts used to build a noise function. To improve the results we will need to use more elaborate techniques but it will be hard to study them unless you can build a simple function first. Don't worry too much about the result of the noise function for now, and just try to grasp how it works, learn and understand its properties. You can still have fun playing around with this code and create some interesting effects/images as we will see in the next chapter. You will learn about creating a better looking noise in the second lesson.

注意,结果可能并不是我们所期待的那样。noise 集合看起来方块感十足。 专业的应用程序生成的结果会比我们这个平滑的多。不过我们这个课程的目的首先在于告诉你创建一个noise函数所需的全部思路,而结果目前并不是最重要的。后面会学到如何完善我们的结果集。


1、在 y = floor( input.y) 时, 对x 做插值 得到nx1

2、在 y = ceiling( input.y) 时,对x 做插值, 得到nx2

3、对y做插值,a = nx1 , b = nx2, 得到最终 noise(x,y)

3D Noise and Beyond

In this lesson we will just give an example of 1D and 2D noise. We will provide code for 3D and 4D noise is the next lesson on noise. However, if you have read the lesson on interpolation and understood the concept explained in the chapter you should easily be able to extend the code to write a 3D noise. All you need to do is to interpolate the random values along the x axis (resulting in 4 values), interpolate the result of this interpolation along the y axis (2 values). You will be left with two values that you will need to interpolate along the third dimension (the z axis) to get a final result (the second lesson on noise contains an example of 3D noise).

Now there is a problem with our current code. We mentioned in first chapter that noise was a compact function which didn't use a lot of memory. However in the 2D case we already have a allocated a 256x256 array of random values. If we use the same technique for the 3D version, we will need to allocate a 256x256x256 array of floats which starts to be quite a significant chunk of memory for a function that is supposed to use very little of it. Ken Perlin solved this problem with a simple and elegant solution in the form of a permutation table which will explain now.


3D 和更高维度的 Noise Function

这个教程目前给出了1D、2D Noise的例子,稍后会有3D、4D的代码范例。如果你阅读过“插值”这个课程,你应该能很容易的将2DNoise扩展为3DNoise。 你只需要做下列的事情:对X插值,得到4个结果;对Y插值,得到2个结果;对Z插值,得到最终结果。

现在我们的函数出现一个问题。我们提到过noise function应该是一个小函数,它不应该使用大量的内存。但是在2DNoise的实现中,我们已经使用了256*256个随机值了。如果我们用同样的思路来处理3DNoise, 就需要使用 256 * 256 *256 这么多的值。而且我们申请了一大票内存,却在函数执行的时候只使用其中一小部分数据。Ken Perlin用了一个简单而优雅的方式解决了此问题:一个permutation table 置换表.


Introducing the Concept of Permutation

Assuming we want the noise function to stay memory efficient, we will limit ourselves with using an array of 256 random values to compute our noise, whether we deal with the 1D, 2D or 3D version of the function. To deal with this limitation, all we need to do is to be sure that any input value to the noise function (a float, a 2D or 3D point) can map to one and only one value in this array of random values. As you already know, each input point coordinates to our noise function is converted into an integer. And this integer is used to look up into the array of random values. Ken Perlin stored the numbers 0 to 255 in an additional array of 256 integers and shuffled (permuted) the entries of this array, randomly swapping each one of them. In fact Perlin extended the size of this array to 512 and copied the values for the indices in the range 0 to 255 to the indices 256 to 511. In other words copying the first half of the array into the second half.


permutation table 的概念

假设我们希望noise function 高效的使用内存。我们会限制随机数最多不超过256,不论我们是在处理1D,2D,3D的输入。 你已经知道我们的随机数都对应着坐标系的整数。而这个整数是可以用来当做数组的下标索引的。Ken Perlin为随机数建立了一个索引数组,并随机交换其中的元素。事实上Ken Perlin还额外扩展了一倍,变成了512的长度,将随机的内容完全复制到了[256-511]的数组元素中。

PS:
随机数组 = [r1,r2,r3]
索引数组 = [ 1, 2 , 3]
扩展索引数组 =>  [ 1 ,2 , 3 ]  = > [ 1, 2, 3, 0 ,0 ,0]

随机交换第一个元素(一步) [3 ,2, 1] = > [ 3, 2 , 1 , 3, 0, 0]

随机交换第N个元素 [ ... N ...] = > [  ... N ... , .... N ....]

最后Permutation table 的内容: [ 3, 1, 2, 3, 1 ,2]


for ( unsigned i = 0; i < kMaxVertices; ++i )
{
    r[ i ] = drand48();
    /// assign value to permutation array
    perm[ i ] = i;
}
/// randomly swap values in permutationa array
for ( unsigned i = 0; i < kMaxVertices; ++i )
{
    int swapIndex = random() & kMaxVerticesMask;
    int temp = perm[ swapIndex ];
    perm[ swapIndex ] = perm[ i ];
    perm[ i ] = temp;
    perm[ i + kMaxVertices ] = perm[ i ];
}

Instead of directly looking up into the array of random values, we will first do a lookup using our integer position into this permutation array. As we know, the integer value for our input point is necessarily a multiple of 255. Lets say that this number is 201. Using this value to do a lookup in the permutation array will return the value which is stored in the table at the index position 201. The result of this lookup is an integer in the range 0 to 255 which we can now use as an index in the array of random values.

之前我们是接通过下标访问随机数的数组,取得某个随机数。现在我们先访问索引数组,取出一个下标值,再去得到随机数。我们知道输入数据X的值最多是255,就假设x = 201 吧。 我们先通过 permutation array 得到一个代表着下标的值,然后再用它当做下标访问 random values ,得到一个随机数。

PS: RandomValue = RandomValues[ PermutationArray[201] ]


You may wonder why the permutation array is twice the size of the function period (512 in size instead of 256). If we deal with a 2D noise, we will first do a lookup in the permutation table using the integer value for the x coordinate of our input point (as described). This will return an integer value in the range [0:255]. We will add the result of this lookup to the integer value for the y coordinate of the input point and use the sum of these two numbers as an index in the permutation table again. Since the result of the first permutation lookup is in the range [0:255] and that the integer value for the point's y coordinate is also in the range [0:255], it means that the range of possible index value for the permutation table is [0:511]. Hence the size of the permutation array (512). This code might help to clear things up for you:

你可能会想为什么索引数组的长度是512呢?观察Perlin的算法:

1、查找permutation table 来得到下标值,这个值只可能是[0:255]。

2、 然后加上第N行,这个值只可能是[0:255] ~ [1:256]  (上取整的情况.)

所以最终结果只可能是 [0:511], permutation [ permutation[ intX ]  + intY ] 就不会越界了。

int xi = floor( pt.x );
int yi = floor( pt.y );
     
float tx = pt.x - xi;
float ty = pt.y - yi;
     
int rx0 = xi & kMaxVerticesMask;
int rx1 = ( rx0 + 1 ) & kMaxVerticesMask;
int ry0 = yi & kMaxVerticesMask;
int ry1 = ( ry0 + 1 ) & kMaxVerticesMask;
     
/// random values at the corners of the cell using permutation table
const float & c00 = r[ perm[ perm[ rx0 ] + ry0 ] ];
const float & c10 = r[ perm[ perm[ rx1 ] + ry0 ] ];
const float & c01 = r[ perm[ perm[ rx0 ] + ry1 ] ];
const float & c11 = r[ perm[ perm[ rx1 ] + ry1 ] ];


This technique is very neat because it continues to work as you add more dimensions to your noise function. It provides us with a technique to map any input point to a unique location in the random value array. All we do is using the input point's coordinates as indices in the permutation table. In programming we call this technique a  hash table , or a  hash function . These techniques were quite popular at the time (and still are but is not as well known by the younger developers). They are useful to map a particular key (a point location for example) to some data stored in a memory block making the lookup process efficient (check the lesson on programming techniques to learn more about hash functions).

这项技术非常规整,所以它能很容易的扩展到多维的情况。Perlin的算法提供一种特性:任意的输入都有一个与之一一对应的下标值。只要输入确定,结果就确定,输入一致,结果就一致。这种技术在编程角度称为 hash table 或者 hash function。这个技术能优化noise function的内存使用效率。

PS:从另外一个角度看,PerlinNoise的原始结果是有一定相关性的。
PS2: 这里没有解释为什么 p[p[x] + y] 总能得到不重复的值,相关理论可以看 hash functions 的课程。


Here is finally the latest memory efficient version of our 2D noise function using the permutation table:

这里是我们在应用内存压缩技术后的noise函数代码:

///
/// Basic implementation of a 2D value noise using permutation table
///
class Simple2DNoiseB
{
public:
    Simple2DNoiseB( unsigned seed = 2011 )
    {
        srand48( seed );
        for ( unsigned i = 0; i < kMaxVertices; ++i )
        {
            r[ i ] = drand48();
            /// assign value to permutation array
            perm[ i ] = i;
        }
        /// randomly swap values in permutationa array
        for ( unsigned i = 0; i < kMaxVertices; ++i )
        {
            int swapIndex = random() & kMaxVerticesMask;
            int temp = perm[ swapIndex ];
            perm[ swapIndex ] = perm[ i ];
            perm[ i ] = temp;
            perm[ i + kMaxVertices ] = perm[ i ];
        }
    }
     
    /// Evaluate the noise function at position x
    float eval( const Point2f &pt ) const
    {
        int xi = floor( pt.x );
        int yi = floor( pt.y );
     
        float tx = pt.x - xi;
        float ty = pt.y - yi;
     
        int rx0 = xi & kMaxVerticesMask;
        int rx1 = ( rx0 + 1 ) & kMaxVerticesMask;
        int ry0 = yi & kMaxVerticesMask;
        int ry1 = ( ry0 + 1 ) & kMaxVerticesMask;
     
        /// random values at the corners of the cell using permutation table
        const float & c00 = r[ perm[ perm[ rx0 ] + ry0 ] ];
        const float & c10 = r[ perm[ perm[ rx1 ] + ry0 ] ];
        const float & c01 = r[ perm[ perm[ rx0 ] + ry1 ] ];
        const float & c11 = r[ perm[ perm[ rx1 ] + ry1 ] ];
     
        /// remapping of tx and ty using the Smoothstep function
        float sx = Smoothstep( tx );
        float sy = Smoothstep( ty );
     
        /// linearly interpolate values along the x axis
        float nx0 = Mix( c00, c10, sx );
        float nx1 = Mix( c01, c11, sx );
     
        /// linearly interpolate the nx0/nx1 along they y axis
        return Mix( nx0, nx1, sy );
    }
    static const unsigned kMaxVertices = 256;
    static const unsigned kMaxVerticesMask = kMaxVertices - 1;
    float r[ kMaxVertices  ];        /// array of random values
    int perm[ kMaxVertices * 2 ];    /// permutation table
};


你可能感兴趣的:(Creating a Simple 2D Noise)