Creating a Simple 1D Noise

来源:互联网 发布:fft算法c语言 编辑:程序博客网 时间:2024/05/16 17:53


Noise is a function that returns a float in the range [0:1] for a certain input x (x can be a float, a 2D, 3D or 4D point but in this chapter we will only be looking at the one dimensional case. Our input position can also be positive and negative and extend from 0 to infinity or minus infinity). We could generate these floats using a random number generator but we have explained in the first chapter that it returns values which are very different from each other each time the function is called. This creates a pattern known as white noise which is not suitable for texture generation (white noise patterns are not smooth while most natural patterns in nature are).

Figure 8: white noise (left) vs noise (right). White noise is not suitable for texture generation. Noise is better because it has a smooth appearance which is better for faking natural patterns.
















Noise 是这样一个函数:它返回一个值域范围在[0:1]之间的值,函数的输入参数x可以是一个 float ,或者 vector2, vector3, vector4.

在本章节,我们只讨论一维的情况. 作为输入的值(也可以称为坐标)的范围没有限制,从负无穷到正无穷。我们可以用一个纯随机函数来生成输入数据。但是这个函数就如同第一章所分析的那样,每次调用它返回的结果是完全没有相关性的(样本平均分布)。具有此特性的数据集合我们称之为white noise (白噪音),white noise不适合用来生成贴图,因为各数据之间没有平滑性,而自然界大多数模式是有平滑性的。


What we will do instead is create a series of random points (using drand48()) spaced at regular intervals. If you work in 2D we create these random values on the vertices of a regular grid (lattice) and if you work in 1D this grid can be seen as a ruler. To make things simple, we will assume that the vertices of that grid or the ticks on that ruler are created at coordinates along the x and y axis which have integer values (0, 1, 2, 3, etc.). These random numbers are generated at these positions only once (when the noise function is initialised).

接下来我们创建一系列随机的值(用 drand48() ),这些值在一维坐标上的分布是等间距的。(*这也能叫一维...)

如果你是在处理2D输入(来自x,y两个维度的值),你可以为一个二维网格上每个点生成一个随机值。

为了简化,我们只在整数坐标上生成随机数(如x=1,2,3); 这些随机数只在noise函数初始化的时候生成一次。

PS:在生成原始噪点时,加上不同的约束,可以影响最终生成的图像。

 

Figure 9: if we create a 2D noise we need to assign random values to the vertices of regular 2D grid (top). For 1D noise, we assign random values at regular interval on the x axis (bottom). In both cases, coordinates of the vertices are integer values. In our example, we only do that for the 10 first numbers, starting from 0 up to 9. The numbers in the boxes below the x axis represents the random values stored in a float array. To make it easier to read the picture, we have scaled up the y axis.












































图9: 如果我们创建一个2Dnoise函数,我们需要给网格(如上面那张图)上的顶点赋初始噪点值.对于1D noise函数,我们给x的整数坐标分别指定初始值。 在这里,我们对于两种情况都只处理整数坐标。在这个例子里,我们只随了10个值,分别对应坐标上的0-9。 格子里就是随机的值。为了方便看,我们把表示noise值的轴放大了。

ps: 绿点是原始噪点值.

If we continue with our 1D example, we can see that we are now left with a series of dots or values defined at the integer position on the ruler. For example the result of the function for x = 0 is 0.36, the result for x = 1 is 0.68, etc. But what's the result of that function when x is not an integer ? To compute a value for any point on the x axis all we need to do is to find out the two nearest integer positions on the x axis for this input value (the minimum and the maximum) and use the random values that are associated with these two positions. For instance if x equals 0.5, the two points with integer values surrounding x are 0 and 1. And these two points have the associated random values 0.36 and 0.68. The result of the function for x when x = 0.5, is a mix of the value 0.36 defined at point 1 and the value 0.68 defined at point 2. To compute this number we can use a simple interpolation technique called linear interpolation. Linear interpolation is a simple function that returns a mix of the values a and b, for a certain value t, where t is in the range [0:1]:


现在 x = 0时, noise = 0.36 , x = 1 , noise = 0.68。对于 x = 0.5怎么办? 我们的做法是:做插值.

Noise(x) = Lerp( Noise(floor(x),  Noise(ceiling(x)),  x - floor(x)) 。

线性插值公式: f(t) = a*(1-t) + b*(t); t的取值范围是 [0,1]。

Figure 10: we want to evaluate the noise function for x = 0.5 and x = 1.2. In the first case, we interpolate the random value for x = 0 (0.36) and x = 1 (0.68) with t = 0.5. The result is 0.52. In the second case, we interpolate the random value at x = 1 (0.68) and x = 2 (0.11) with t = 0.2. The result is 0.566.















x=1.2
a = floor(x) = 1
b = ceiling(x) = 2
dt = 1.2 - floor(x) = 0.2
noise(x) = noise(1) * (1 - dt) + noise(2) * dt
      = 0.68 * 0.8 + 0.11 * 0.2 = 0.566

这里我用了上取整 下取整函数。
事实上,比如我们的随机样本长度是10.那么可以将任意坐标映射到x=[0,10]

例如,size是输入数据实际的个数,那么在处理第i个元素的时候,其坐标可以表示为 (i/size) * size ,一般我们称 i/size 这个为uniform coordinate(坐标), 用这个coor 再去乘以noise函数定义的处理范围,比如这里的10,就可以得到一个映射后的x值,然后x前后的原始噪点值也确定了,就可以插值了。


In the case of our noise function, we will replace a and b with the random values defined at the integer positions surrounding x (if x = 1.2, the random values for x = 1 and x = 2), and we will compute t from x, by simply subtracting the minimum integer value found for x from x itself (t = 1.2 - 1 = 0.2).

没啥要说的...

int xMin = (int)x;float t = x - xMin;

And here is the code to compute a value using linear interpolation:

float Mix( const float &a, const float &b, const float &t ){    return a * ( 1 - t ) + b * t;}

The mix function is usually known as the Lerp (for linear interpolation) function by CG programmers. If you find a Lerp function in the source code of a renderer or mentioned in a book you should know that it is the same thing as the mix function used here.

如果你在其他书里看到 Lerp 这个函数,其实就是线性插值函数,和这里的 Mix函数是一回事。

Computing a value for any x in the range [0:1] using linear interpolation is similar to drawing a line from point 1 to point 2 (figure xx, left). If we repeat this process for all the points in the range [1:2], [2:3] and so on, we get the curve from figure 11. You may understand now why we call this type of noise value noise (noise can be created in a few different other ways). The idea is to create some values at regular intervals on a ruler (1D) or a grid (2D) and interpolate them using linear interpolation.

Figure 11: result of the noise function when we use the technique of linear interpolation described above. We have 10 random values, one for each integer position on the x axis of the ruler (from 0 to 9). For any value of x lower or equal to 9, we can compute a noise value by linearly interpolating the random values stored at these positions.

















如果得到所有在[0,1]之间的x对应的noise值,就像是将  noise(0) 和 noise(1)之间连一条线。同样的[1:2], [2:3],也是连线。这样我们就可以连出一条线序曲线。
现在你也许已经理解到我们为什么叫这种 noise 为 value noise (PS:因为noise代表了一个值)( noise 有时会用来代指一些不常用的含义)(PS:也许是说不是数字或标量,可能是别的什么玩意.)
生成受控noise的 主要思路 就是(1D:等间距坐标点上, 2D:网格顶点上) 先预先生成噪点,然后利用插值来完成任意点的噪声值计算。


We now have all the bits and pieces we need for creating a very simple noise function. When initialising the function, we will create a series of random values which we will store in a float array (the numbers written in boxes at the bottom of figure 9). As you can see in the code, the length of the array can easily be changed. This will be important later, but for now, to keep the demonstration simple, we will only create 10 values (from the origin 0, to 9). The index of a number in the array corresponds to its position on the ruler. The first number in the array correspond to x = 0, the second to x = 1, etc. To compute a noise value for x, we will first compute the integer boundaries for x (the minimum and the maximum integer value for x). We can then use these two integer values as index positions in the array storing the random numbers. The two numbers we get, a and b, are the two random values stored at these index positions. We also need to find t from x using the technique described above (subtract the minimum integer for x from x). The final step is to perform a linear interpolation of a and b using t by calling the mix function. And you get the result of your noise for x.

有了上述知识点,我们可以实现一个非常简单的noise生成函数了。在初始化阶段,我们人为指定一组数,保存在一个数组里(数值如图9所示)。 本来这个数组的长度可以随意指定,为了概念上简单,我们只使用10个数。数组下标对应坐标刻度。我们使用之前提到的方法计算任意x,[0:10]的噪音值。

class Simple1DNoiseA{public:    Simple1DNoiseA( unsigned seed = 2011 )    {        srand48( seed );        for ( unsigned i = 0; i < kMaxVertices; ++i )        {            r[ i ] = drand48();        }    }         /// Evaluate the noise function at position x    float eval( const float &x )    {        int xMin = (int)x;        assert( xMin <= kMaxVertices - 1 );        float t = x - xMin;        return Mix( r[ xMin ], r[ xMin + 1 ], t );    }    static const unsigned kMaxVertices = 10;    float r[ kMaxVertices ];};     int main( int argc, char **argv ){    Simple1DNoiseA simple1DNoiseA;         static const int numSteps = 200;         for ( int i = 0; i < numSteps; ++i )    {        float x = i / float( numSteps - 1 ) * 9;        std::cout << "Noise at " << x << ": " << simple1DNoiseA.eval( x ) << std::endl;    }         return 0;}

We only have 10 random values defined at each integer position on the x axis starting at x = 0 so we can only compute a value for any x in the range [0:10]. Why [0:10] instead of [0:9] ? When x is in the range [9:10] we will use the random value at index 9 and at index 0 to compute a noise value. As you can see in figure 13, if you do this, the beginning and the end of the curve are the same. In other words noise when x = 0 and when x = 10 is the same (in our example 0.36). Lets make a copy of the curve and move it to the left or to the right of the existing one. The existing curve (curve 1) is defined over the range [0:10] and the new copy (curve 2) is defined over the range [10:20].

我们只使用[0:10],为什么不用[0:9]呢?如果x in [9:10],我们就使用 a = noise(9) , b = noise(0) 来进行插值. 这样 noise(0) == noise(10) ,从曲线角度上看,[0:10] 和 [10:20] 就首尾相连了。这种模式可以推广到整个一维坐标。

Figure 12: to make our noise function periodic we need to make the first and the last value of the function identical. When you make copies of this curve they now join without any discontinuities making it possible to extend the function to infinity.









图12:为了让我们的noise函数呈现周期性变化,我们需要让曲线的首尾值一致,这样计算任意位置的噪音值的函数就是连续的。如果是不连续的函数,会在一个值上产生二义性,甚至会导致某些插值方法产生无穷大的值(PS:或者说截断,这在地形生成中会产生极不自然的断崖。)

You can see that there is no discontinuities where the curves join (for x = 10). Why ? because the noise value at the end of curve 1 is the same as the noise value at the start of curve 2. And that's because when x is in the range [9:10] we interpolate the random value for x = 9 and x = 0. Because there is no discontinuities between successful copies of the curve, we can make as many copies as we want and extend our noise function to infinity. The value for x is not limited anymore to the range [0:10]. It can take any positive or negative values going from 0 to infinity (or minus infinity. The noise function should work for negative values).

How do we make that possible in the code ? We know how to compute a noise value when x is in the range [0:9]. However when x is greater than 9 (and the same thing applies for the case where x is negative), lets say 9.35, we want to interpolate the random position at x = 9 and x = 0 as explained above. If we take the minimum and maximum integer for x we get 9 and 10. But instead of using 10 we want to use 0. What we need here is the modulo operator. The modulo operator gives theremainder of a number divided by another number (in our case the reminder of x divided by 10). If you do the math, the remainder of 9 divided by 10 is 9. And the remainder of 10 divided by 10 is 0. In other words using the modulo operator on the minimum and maximum integer value for x = 9.35, gives us 9 and 0 which is exactly what we want. And if you do the test you will see that using this operator always returns the right integer boundaries for any x greater than 10 or lower than 0 (special care muse be taken for negative values but the principle is the same).

Using this technique we can repeatedly cycle over the noise functions as we move along the x axis (which is similar to making copies of the original curve). We mentioned in the first section that the noise function was periodic. In this case, the period of the function is 10 (it repeats itself every 10 units on each side of the origin for negative and positive values of x). We will be using the modulo operator for now (% in C++) but later on we will see how this can be simplified and made more efficient (be aware that the following code does not work for values of x lower than 0. This restriction will be removed in the last version of this code):

Figure 13: for values of x in the range [9:10] we interpolate the random values at index 9 and index 0. As you can see with the plot of the curve, the noise value is now the same when x = 0 and x = 10 which makes it possible to duplicate the curve and use it as a periodic function. The first row of number (top) indicates the values along the x axis. The second row are the index positions used to compute the noise. For x = 10 we use index 0. 


我们让噪点值生成函数(其实就是个分段函数)在任意点上保持连续,只要保持函数连续性,我们就可以处理从负无穷到正无穷的输入,noise函数应该能处理输入负数为了做到这一点,我们需要分别处理x in [0:9]和[9:10]的情况.在代码上,我们使用%求模运算符来完成对x值的归一化. 代码如下:

class Simple1DNoiseB{public:    Simple1DNoiseB( unsigned seed = 2011 )    {        srand48( seed );        for ( unsigned i = 0; i < kMaxVertices; ++i )        {            r[ i ] = drand48();        }    }         /// Evaluate the noise function at position x    float eval( const float &x )    {        int xi = (int)x;        int xMin = xi % (int)kMaxVertices;        float t = x - xi;        int xMax = ( xMin == kMaxVertices - 1 ) ? 0 : xMin + 1;        return Mix( r[ xMin ], r[ xMax ], t );    }    static const unsigned kMaxVertices = 10;    float r[ kMaxVertices ];};     int main( int argc, char **argv ){    Simple1DNoiseB simple1DNoiseB;         static const int numSteps = 200;         for ( int i = 0; i < numSteps; ++i )    {        float x = i / float( numSteps - 1 ) * 10;        std::cout << "Noise at " << x << ": " << simple1DNoiseB.eval( x ) << std::endl;    }         return 0;}


There is nothing wrong with this result (technically it does the right thing) except that it looks like a saw-toothed curve which is not very natural. If you look at patterns in nature that seem to be random such as the profile of an ocean surface (waves), they usually do not have this saw-toothed profile. Their profile is smooth (rounded). What we can do to change the appearance of this curve is to remap our input t value, using another function that has a smooth profile (a function that produces a curve having an "S" shape). Two S curve functions that are commonly used to remap t are the cosineand the smoothstep function. It is important to understand that the code for interpolating the random values do not change. What we do is simply remap the value of t before we use it in the mix function. Once again, we do not replace the mix function with an "S" curve function. We remap t using a smooth function followed by a linear interpolation of a and b using this remapped value of t. Here is what we get in pseudo code:

这个函数原理上来说没啥问题,但是它产生的noise分布看起来是锯齿状的,这不太自然。你可以观察自然界里的一些带有随机性的轮廓,比如海洋的表面(波浪),它们通常都不会呈现锯齿状。自然界的随机轮廓通常都是光滑的(圆形)。 我们可以换一种映射方式:利用具有平滑曲线的函数(比如"S"形曲线),将输入数据x 处理下,从而得到更加平滑的分布曲线。经常使用的两个S Curve函数是 consineand (cos函数) 和 smoothstep函数( Returns a smooth Hermite interpolation between 0 and 1(使用埃尔米特插值方式对x在0和1之间插值))  。有一点很重要,我们不是要改进对value的插值,而是改进插值时使用的比率 t 的计算方式。下面是伪代码:

PS:我个人的理解:常见的插值方式还有贝塞尔插值.这些繁杂的插值公式,最终效果都是得到更加平滑的结果分布,不同的插值在结果上略有不同。在例子中,我们使用的是分段的线性函数,在段与段的连接点会产生很明显的拐点,如之前的图所示。从数学上分析,我们使用的分段函数虽然是连续的,但是其导数是不连续的。(若干常数函数的组合)。别的插值方式在这方面做了改进,所以可以获得更平滑的分布曲线。


float SmoothNoiseProfile( const float &a, const float &b, const float &t ){    assert( t >= 0 && t <= 1 );     // t should be in the range [0:1]    float tRemap = smoothFunc( t ); // remap t input value     return Mix( a, b, tRemap );     // return interpolation of a-b using new t}

Cosine

Figure 14: plot of the cosine function (left). We are only interested in the range [0:PI]. On the right, plot of the remapped cosine function (with r1=0 and r2=1) that we will be using to smoothly interpolate random values. Notice the "S" shape of the curve profile.


图14:  consine函数的图像(左)。 我们只对值域范围在 [0 : PI]进行分析。 在右面的图中,我们可以看到当输入在[0:1]变化时,我们可以得到更加平滑的插值结果。可以看到曲线呈“S”形分布。


Lets start to plot the cosine function in the range [0:2PI] (a complete turn around the unit circle). You are certainly familiar with the profile of this curve (figure 7, left image). As you can see the curve goes from 1 to 0 when x is in the range [0:PI/2], 0 to -1 when x is the range [PI/2:PI], -1 to 0 when x is in the range [PI:3/2PI] and finally (last quadrant) from 0 to 1 when x is in the range [3/2PI:2PI]. The section of the curve where the function varies from 1 to -1 (when x is in the interval [0:PI]) could be used to interpolate two values. Because t is in the interval [0:1] we will remap it before using it in the cosine function by simply multiplying it by PI (remember that we want x to be in the interval [0:PI]). However we want the result of our remapping function to go from 0 to 1 but the cosine function when x is the range [0:PI] goes from 1 to -1. The trick is to write 1-cos(t*PI) which gives us now a value that ranges from 0 (1-cos(0)=0) to 2 (1-cos(1*PI)=1--1=2). If we divide this result by 2 (or multiply it by 0.5) our remapping function finally returns values in the range [0:1] (figure 14, right image). To complete the code, we will interpolate r1 and r2 with a mix function by using the value of t which was remapped with the cos function. Here is the code we will be using:


我们绘制出[0:2*PI]的图像,结果会形成一个圆。 这个和图7(左侧)里表现的曲线很像。(PS:图7是另外的文章里的图)。

剩下的是在解释cosine的周期性了,大家查阅三角函数吧,需要理解的概念有:函数曲线,值域范围,周期性,相位,振幅,频率。

通过控制振幅、相位,可以控制最终输出结果在[0:1]这个范围,使用cosine函数的插值代码如下:

float Cosine( const float &a, const float &b, const float &t ){    assert( t >= 0 && t <= 1 );    float tRemapCosine = ( 1 - cos( t * M_PI ) ) * 0.5;    return Mix( a, b, tRemapCosine );}

If we use this code, the profile of the curve stays the same (since we are interpolating the same predefined values at the same positions) but the transition between two successive values is much smoother (Figure 15).

如果我们使用这段代码来进行插值,曲线的轮廓和之前大体一致,但是连续两个x得到的noise的值会更加平滑。(PS:一级导数连续)

Figure 15: the same 10 points are interpolated using a cosine remapping function for t. The interpolation is now smooth. Note how the curve passes exactly through the control points. The red curve is shown underneath for comparison. The two curves pass through the exact same control points but our cosine curve is smoother.


图15:红线是未经处理的输入,黑线是处理过的输入。


Smoothstep

The smoothstep function is commonly used in implementation of noise functions (it is used in the popular implementation of the noise written by Ken Perlin which is known as Perlin noise). There is nothing much to say about the function itself apart from the fact that its profile (within the range of interest [0:1]) has exactly the kind of shape we are interested in. Here is the equation of the function and its plot (Figure 16, right image. Notice that the Cosine remapping equation ((1-cos(t*PI))*0.5) has almost the same profile).


smoothstep 函数通常用于实现noise函数.(Ken Perlin在创造 Perlin Noise算法时使用了smoothstep函数来处理插值)。对于输入范围在[0:1]的情况,smoothstep形成的曲线如图16所示,它看起来和 ((1- cos(t*PI)) * 0.5) 很像。

PS:其实任何需要“平滑的插值” + “一定经过目标点”的地方都可以使用这类插值函数,比如网络同步多对象的移动时,影子跟踪算法会需要平滑路径,有些插值算法得到的曲线不会经过控制点。

Figure 16: on the left, a plot of the mix function for r1=0 and r2=1. On the right a plot of the smoothstep function which has an the "S" shape.


Care must be taken when you translate the smoothstep function into code because t needs to be raised to the power of 2 and 3. It is possible to optimise these operations slightly with the following implementation:

注意在实现 smoothstep函数的时候,需要计算 t 的2次方 和三次方,在实现的时候可以这样进行优化:

float Smoothstep( const float &a, const float &b, const float &t ){    float tRemapSmoothstep = t * t * ( 3 - 2 * t );    return Mix( a, b, tRemapSmoothstep );}

Figure 17: the same 10 points are interpolated using a smoothstep remapping function for t. This curve is almost identical to the curve obtained with the cosine remapping function.


图17展示了使用 smoothstep函数处理输入x 之后的结果。


A Simple 1D Noise Function

In this section of the lesson we will quickly show different ways to change the result of the noise function. First we will show what our function looks likes. For now we are still using 10 random number for generating our noise pattern (like in figure 13). Which means that our function will vary from 0 to 10 on the x-axis (for values in the range [9:10] we are using the lattice 9 and 0). After that it will repeat itself with a period of 10 units. We mentioned before that the noise function is a periodic function (figure 12 illustrates this idea). In real applications, such a short period is not satisfying. Our final version of the noise function will deal with a much larger period (256 and we will explain why we use this number). We will also have to write the code so that it works with negative values for x. We have used the modulo operator so far to cycle over the period of the noise function but this will not work when x is negative which is making it more complicated than it should (and slower). In the final version of the code you will learn how you can avoid using it. Finally note how the noise results still vary in the range [0:1] along the y axis.


在这个章节里,我们会示例多种方法来控制噪点生成。 首先我们来看看 noise function 长啥样。 目前我们使用了10个节点的随机噪点来代表我们的噪点分布模式。如图13所示。 这意味着我们的函数可以在 [0:10] 上连续产生噪点。([9:10] 使用 noise(9) 和 noise(0)进行插值)。然后会周期性的使用这长度为10的模式。之前我们提过 nosie function 是一个周期函数,如图12所示。在实际应用中,不会使用10这么短的周期。在我们的最终版本,会使用范围大得多的周期(256,稍后会解释为什么用这个长度)。我们也会让函数支持负数输入。我们已经使用取模运算来解决输入的周期性问题,但是对负数取模会很复杂,这个复杂度甚至超过了noise function应有的复杂度。在最终版本里,你会学到如何避免对负数取模。最后注意 nosie function 产生的输出y 总是在[0:1]的范围上做周期性的变化。


Scaling

It is possible to change the shape of the function quite easily by either scaling the input value x or multiplying the result of the function itself. The first operation will change the periodicity of the function. Multiplying x with a value greater than 1 will increase the periodicity of the function (you change the noise frequency). In short you will compress the curve (making it to appear shorter). This will be more obvious in the next chapter with examples of 2D noise. If we multiply x by a value lower than 1, this has the effect to stretch the curve out along the x axis, making it to appear larger (figure 18). This operation is very useful to control the frequency of the noise pattern.


如果给 x 乘以一个大于1 的数,就会让模式的周期延长了,小于1会让模式的周期缩短。这就是改变noise模式的频率方法。

在做2D noise的时候,你能更明显的看到对频率的控制。


float frequency = 0.5;float freqNoise = simple1DNoiseC.eval( x * frequency );

Figure 18: it is possible to scale the x variable which is the input to the noise function. By doing so we can stretch the period of the function. Red curve is our reference (frequency = 1). Green curve: we have halved the frequency (frequency = 0.5). Blue curve: the curve is twice as long as the green curve. The frequency (period) of the this curve is twice the frequency of the red curve.


图18,: 可以缩放 noise函数的输入参数x. 我们通过这样的操作来缩放函数的周期长度。 红线表示的是频率为1的情况, 绿线为0.5 , 蓝线为2

PS: 频率一般代表相同的变化里模式重复出现的次数大小,在这里,文章中的 frequency 实际上代表的是对周期的缩放系数,如果转化为实际频率所代表的意思,应该1/缩放系数。


The second most basic operation is simply to multiply the result of the noise function itself, which results in changing its amplitude. If you use a 1D noise to animate a curve the effect might be too strong and can attenuate it by scaling its amplitude down (figure 19).

第二种基础操作是对 noise function的结果做乘法,这个叫做调整振幅。如果你发现noise的变化过快,你可以通过降低振幅来整体调整变化量。

float amplitude = 0.5;float ampNoise = simple1DNoiseC.eval( x ) * amplitude;

Figure 19: it is also possible to multiply the result of the noise function itself, changing its amplitude. The red curve is our reference. Blue curve: we have multiplied the amplitude by 2. Green curve: we have divided it by 2.

图19 : 红:标准振幅。 绿:1/2 振幅。  蓝:2倍振幅。


Offsetting

It can also be very useful to add a certain value to the input of the noise function. This has the effect to shift the curve either to the left (if we add a positive number to x) or to the right (if we add a negative number to x). This technique of shifting the result of the noise function by adding an offset to x is very useful to animate the function over time (by increasing the offset value at each frame).


位移

我们可以通过给 x 加上一个数,从而让图像向右移动。同理,减去一个数会让图像向左移动。 我们可以很方便的利用这种特性,来和时间结合,做出noise function 随时间变化时的动态效果。(让函数随时间进行位移)

Figure 20: adding a offset to x. Here the curve gives the impression to move to the right (with have added a negative values to x). The curve is moving all together but if you focus your attention on the x = 0 and observe how the noise value moves up and down you will get a sense of wavy motion.

图20: 通过控制位移,可以让x = 0时, noise(0) 得到不同的值。这会让noise(0) 在Y轴上有一个类似波浪的运动效果。

PS: 这个效果只是值随时间变化时的形象表示,和生成地形没有直接关系。

PS2: 这个offset,更专业的做法叫做 phase (相位)



Signed Noise

Usually noise functions return values in the range [0:1]. But it doesn't have to be. It depends of how they are implemented. For instance, you can assign random numbers to the integer lattice point which are in the range [-1:1] which would result in a noise function returning values in the same range. However, as we said, the convention is usually to return a value in the range [0:1] but it easy enough to remap that value in the range [-1:1]. The resulting noise is sometimes called a signed noise. Here is the code and a plot of our noise function with its result remapped in the range [-1:1] (figure 21). This can be useful again when you use noise to procedurally animate objects or want to vary a parameter. If the position of an object in y is 3 then and that you apply a noise to it, you might want the object to vary within the range 0 to 6. In which case you apply a signed noise to y position of the object which you multiply also by 3 so that it varies within the range [-3:3] which added to 3 will make the object vary with the range [0:6] along the y axis.


有信号特征的噪音

通常 noise function 返回一个分布在[0:1]上的结果。但是它并不是必须这样。返回什么样的分布结果取决于你的实现。比如你可以调整输出[-1,1]的值域范围内的随机值。正如刚才说的,通常做法是返回[0:1],也不限制你返回[-1,1]。这样产生的noise值,有时被称为 signed noise (不知道中文叫啥)。 图12是将输出范围映射到[-1,1]的noise function 的图像表示。 如果你希望有一个 [0:6] 的输出,你可以通过给3倍振幅,得到[-3,3]的输出,然后再对结果+3 得到 [0:6] 的输出。

代码和图表示的是 [-1,1]的输出

float signedNoise = 2 * simple1DNoiseC.eval( x ) - 1;


Figure 21: example of signed noise. It is just a noise function which result is remapped in the range [-1:1].


A Complete 1D Noise Function

Using 10 random number results in a very short period. This means that the function will repeat itself quite often if we use it over a large range of x values (say from 0 to 100 it will repeat 10 times). Visually there is a strong probability that you will be able to spot this pattern which is not going to make our result look very natural. We mentioned in the first chapter, that one of the noise properties is that it shouldn't look periodic. To achieve this we will simply be cheating and make the period of the noise function very large.

In the previous chapter (in the paragraph on the noise properties), we have mentioned that when you zoom out enough the noise becomes so small that a repetition would become hardly visible. This is in fact more a trick than anything else. If you look at the bottom curve from figure xx, we drew the noise function from 0 to 256 (one period). As you can see if we were to duplicate this curve to the right and the left and zoom out even more to have a look at these three curves side by side, it's very likely that you could only see a long slightly jagged line. You wouldn't be able to tell that this line is made of three copies of the same curve. The trick is to find a size for the period of the noise that is long enough to make it impossible to recognise the pattern as you zoom out but short enough that the random value array stays reasonably small (keeping the memory usage consumption low). And 256 seems to be that number.

Figure 22: top: our final noise function plotted for x in the range -10 to 10. As you can see we can't notice any repetition simply because now the period is large (256 units). Bottom: we have zoomed out to plot the noise function over the range [0:256]. If we were to zoom out even more it would be hard to notice the variation in the curve making it difficult to spot the repetition of the pattern.



使用10个随机值节点的周期很短。这意味着如果我们使用一个远超函数周期范围的输入时,函数会非常快的产生重复值。(比如输入0到100,函数周期会重复出现10次)。从视觉上看这会让我们定义的模式显得极不自然。我们在前一章里提到过,噪音的特点之一就是看起来不那么周期性。为了达成这个目的,我们只需要简单的将nosie function的周期设计的很大。

在前一章,我们提到过当你缩小了看noise的结果集,你很难将其中的周期性识别出来。这实际上是利用了技巧。
下图有两条曲线,底部的曲线是由3条上面的曲线首尾相连而组成的,但是你很难发现其中的模式。
我们在实现noise function的时候,设计周期的标准就是长到你很难发现模式,又不会长的离谱。所以看起来256是个挺好的长度。


Most modern implementation of the noise function use a lattice grid of 256 points (or 512). The 256 value hasn't been chosen by chance. Why 256 and not 255 or 257 ? Because this number is a power of two (2^8). When we deal with such numbers, we can replace the modulo C operator (%) by a bit operator (&). In the past programmers preferred arithmetic operations rather than using the % operator which was slower. Modern processors/compilers though handle this very well so there's no particular need to switch to the & operator apart from the fact that it makes things simpler when x is negative.

目前noise function 的实现大多使用256或512个原始噪点。256不是偶然选择的,因为它是2的幂函数(2^8)。 这能在C或C++实现层面优化算法的效率。我们可以通过位运算来代替取模操作,这样对负数取模也就没有效率问题了。


QUESTION FROM A READER: can you explain why & is better and how it works ?
In C/C++ -10 % 256 is not the same thing than -10 & MASK (with MASK = 255). One returns -10 when the other returns 246 which is the value we want to use in our noise function when x = -10 (if our noise has a 256 period, the first element in the array of random values (index 0) corresponds to x = -256. If x = -10, the index position is 246). Even if in theory the compiler optimizes the code when the modulo is a power of two, it doesn't work for negative values of x while & does.
How it works is bit trickier and we advise you to read the lesson on Bitwise Arithmetic for an in depth explanation. However if you are already familiar with this topic, the following code should help you finding the answer:

#include <bitset>
using namespace std;
#define MASK 256 - 1
int a = -10;
int b = a & MASK;
bitset<16> ab( a );
bitset<16> bb( b );
bitset<16> mb( MASK );
cout << ab << "=" << a << "\n" << mb << "=" << MASK << "\n" << bb << "=" << b << "\n";
1111111111110110=-10
0000000011111111=255
0000000011110110=246
上面在讲取模的优化


template<InterpolationFunc F>class Simple1DNoise{public:    Simple1DNoise( unsigned seed = 2011 )    {        srand48( seed );        for ( unsigned i = 0; i < kMaxVertices; ++i )        {            r[ i ] = drand48();        }    }         /// Evaluate the noise function at position x    float eval( const float &x )    {        /// Floor        int xi = (int)x - ( x < 0 && x != (int)x );        float t = x - xi;        /// Modulo using &        int xMin = xi & kMaxVerticesMask;        int xMax = ( xMin + 1 ) & kMaxVerticesMask;        return (*F)( r[ xMin ], r[ xMax ], t );    }    static const unsigned kMaxVertices = 256;    static const unsigned kMaxVerticesMask = kMaxVertices - 1;    float r[ kMaxVertices ];};     int main( int argc, char **argv ){    Simple1DNoise<Smoothstep> simple1DNoise;         static const int numSteps = 200;         for ( int i = 0; i < numSteps; ++i )    {        // x varies from -10 to 10        float x = ( 2 * ( i / float( numSteps - 1 ) ) - 1 ) * 10;        std::cout << "Noise at " << x << ": " << simple1DNoise.eval( x ) << std::endl;    }         return 0;}



Note how at line 18 we take care of rounding x to the minimum integer value when x is negative (int(-1.2) is -1 while we want -2. So if the boolean test on the right inside is true, then it will add -1 to the result: -1 + -1 = -2). If you are curious and want to check that your function is really periodic, then you can write the following code:

std::cout << simple1DNoise.eval( -512 ) << std::endl;std::cout << simple1DNoise.eval( -256 ) << std::endl;std::cout << simple1DNoise.eval( 0 ) << std::endl;std::cout << simple1DNoise.eval( 256 ) << std::endl;std::cout << simple1DNoise.eval( 512 ) << std::endl;    0.3542710.3542710.3542710.3542710.354271


As you can see calling the noise function with a value for x which is a multiple of its period (256) always returns the same value (which is value of the random number stored in the array at index 0). You can also write the following code which is another neat way to understand the concept of periodicity:

cout << simple1DNoise.eval( x       ) << " " <<        simple1DNoise.eval( x + 256 ) << " " <<        simple1DNoise.eval( x - 256 ) << " " <<        simple1DNoise.eval( x + 512 ) << "\n";

上面在验证周期性的正确性。

One Last Very Important Thing to Know

The implementation of the noise function relies on creating an array of random values where each one of these values is considered to be positioned at integer positions on the ruler. This is a very important observation which will be extremely useful when we will come to filter the noise function later on. In the first chapter of this lesson we have mentioned that when the noise pattern was too small it becomes white noise again and create a visual artifact known as aliasing. It is possible to get rid of this aliasing by filtering the noise function when its frequency becomes too high. The question is to know when is "too high". The answer to this question is precisely related to the distance that separates each pre-defined random values on the ruler: two consecutive random numbers are 1 unit apart. It is very important that you remember this property of the noise function (a chapter on filtering noise can be found in the second lesson).


最后一件非常重要的事情

nosie function 在实现的时候依赖一组人为指定的、和整数坐标对应的、随机的值。这些值本身的特性对于我们之后对 noise 结果集做滤波时十分总要。 在前一节,我们说如果周期太小,noise function 就退化成白噪音了。在图形学上,这会引起由于精度问题导致的“闪烁”现象发生(PS:由于精度不足,微小的浮点数变化最终会导致光栅化时同一个像素点在连续的两帧里计算出不同的颜色值)。解决这个问题的方法是过滤掉一些“频率太高"的noise function. 问题的重点是如何界定”太高“,而方法就是测量之前定义的随机值,连续两个之间在坐标上的间隔被缩放到何种程度了。两个预定义噪点在坐标上的间隔长度称为1个单位间隔长度,你需要记住这个定量分析noise function 频率大小的方法。(在第二课里可以看到对noise的过滤)

在前一章,我们提到过当你缩小了看noise的结果集,你很难将其中的周期性识别出来。这实际上是利用了技巧。
下图有两条曲线,底部的曲线是由3条上面的曲线首尾相连而组成的,但是你很难发现其中的模式。
我们在实现noise function的时候,设计周期的标准就是长到你很难发现模式,又不会长的离谱。所以看起来256是个挺好的长度。

0 0
原创粉丝点击