Perlin noise(二)

16 篇文章 4 订阅
16 篇文章 9 订阅

Perlin noise(二)

  通过前面两篇文章,我们现在对噪声已经有了初步的了解,对Perlin噪声生成原理也进行了阐述,现在是时候开始学习噪声生成了,即将进入程序纹理这个神秘而又让人觊觎的国度了。

(一)、1D Perlin Noise

  前面我们是以2D Perlin 噪声来讲解Perlin噪声的原理的,对于1D Perlin噪声,我们首先要明白,1D Perlin噪声就是点,通过插值我们可以得到线。对1D Perlin噪声来说,我们不需要晶格,或者说我们只需要一个点就可以了,但Perlin噪声生成的流程我们都得过一遍。回顾一下2D Perlin噪声生成,第一步我们得到晶格的顶点梯度向量,第二步得到距离向量并将距离向量与梯度向量作点积,第三步我们对得到的点做插值从而得到二维纹理。对1D Perlin噪声也是一样。
  第一步:生成伪随机梯度值(Pseudo-Random Number Generator(PRNG ))。对1D Perlin噪声来说,在晶格上他只有一个点,所以只需要一个随机梯度值,根据我们之前说的,伪随机梯度值其实并不是完全随机的值,他需要保持一定的稳定性,对于给定的输入要求得到确实的输出。因此对我们来说Math.random()并不是最好的选择,在前面的文章中,我们学习了LCG(Linear Congruential Generator),正好在这里派上用场。在这里,我们用一个随机数初始化Z,但在下一次计算的时候就会用上一次生成的Z来取代这个随机Z,同时,我们将返回值限制在[0,1]这个区间内,为方便使用,我们将PRNG单独写成一个类。

    public class PRNG
    {
        static long M = 4294967296;
        static long A = 1664525;
        static long C = 1;
        static long Z;
        public PRNG()
        {
            Random rdm = new Random();
            Z = (long)Math.Floor((rdm.NextDouble() * M));
        }
        public double Next()
        {
            Z = (A * Z + C) % M;
            return (double)Z / (double)M;
        }
    }

  我们知道LCG输出其实是跟上一个输出有关系的,在这里我们使用了Math.random(),在参数选择上,我们选择了最大的 uint 值作为M的值,M值越大,生成随机数重复的可能性就越小。好了,现在梯度向量我们已经生成了。
  第二步:生成距离向量,在1D perlin噪声中,使用的晶格只有一个点,因此没有办法计算这个距离向量,这里我们将距离向量看成是1,点积后不改变梯度向量即可。
  第三步:插值。根据前面所学,插值方法多种多样,这里我们选择Cosine插值算法进行插值处理,兼顾计算量与平滑度。

       private double Cos_Interpolate(double a, double b, double t)
        {
            double ft = t * Math.PI;
            t = (1 - Math.Cos(ft)) * 0.5;
            return a * (1 - t) + t * b;
        }

  至此,所有准备工作都已准备了,剩下的就是把这些整合起来得到最终的1D perlin 噪声了。

        public int Perlin1D(int x)
        {
            Wavelength = (int)(1.0f / Frequency);
            double y;
            if (x % Wavelength == 0)
            {
                pointA = pointB;
                pointB = myPRNG.Next();
                y = pointA * Amplitude;
            }
            else
            {
                y = Cos_Interpolate(pointA, pointB, (double)(x % Wavelength) / Wavelength) * Amplitude;
            }
            return (int)y;
        }

  简单解释一下代码,首先通过频率得到波长,得到波长的目的是在合适的点生成Perlin噪声点,其他点则使用插值法进行计算,现在1D perlin噪声我们就生成了,其余的工作就是将生成的曲线在界面上表现出来,不再赘述(代码在本文末,界面我们使用GDI+来画,代码在VS2015+WIN10上测试通过,以下同)。生成的1D Perlin噪声如下图所示:

这里写图片描述

  这张图是完全用离散的点来画的,当然,我们也可以在点与点之间用直线连接起来形成连续的曲线。上面这张图看起来很平滑,也就是说缺乏细节,我们可以使用分形布朗运动方法生成带有丰富细节的曲线,下面这张图就是octaves = 4时的分形曲线。
这里写图片描述

  1D Perlin噪声可以用来扭曲曲线,增加曲线或图形的随机感,稍后我们将会学习到。1D Perlin噪声生成相对简单,因为没有距离向量的参与让事情简单不少,1D Perlin噪声生成的关键是PRNG,使用LCG保证了PRNG的相关性,所以生成的点不是完全随机而是有一定规律的点。

(二)、2D Perlin Noise

  1D Perlin噪声因为没有距离的参与让事情简单不少,在学完1D Perlin噪声之后,现在是时候学习2D Perlin噪声了,我们遵照Perlin噪声生成原理,一步一步的实现之。
  第一步:生成伪随机梯度值PRNG ,在1D中,我们使用了LCG来生成PRNG,在2D中,我们不再使用这种方法,我们采用在单位圆上分布均匀的单位向量作为PRNG值,对2D Perlin噪声来说,8或16个梯度值就基本能满足要求了。这里我们使用8个梯度值。我们让这8个梯度向量在圆上均匀分布。

这里写图片描述

static float[,] gradients;
gradients = new float[8,2];
private void GenerateGradients()
{
   for (int i = 0; i < 8; ++i)
      {
         gradients[i,0] = (float)Math.Cos(0.785398163f * (float)i);  // ( 2 * PI / 8) * i
         gradients[i,1] = (float)Math.Sin(0.785398163f * (float)i); 
      }
 } 

   gradients[i,0] = (float)Math.Cos(0.785398163f * (float)i) 这行代码就是计算梯度向量的x分量值,0.785398163f = ( 2 * PI / 8) ,这是把圆平均分成了8份。然后我们依次取各向量的值作为梯度向量。好了,现在梯度向量有了,如何选择他们呢?也就是说对于每一个顶点,我们该选择哪一个值作为他的梯度向量呢?根据前文叙述我们知道这个梯度值不能随便选择,得保持一定的稳定性,对于给定的值,梯度向量应该选择同一个值,这样才能保证生成的Perlin噪声的连续性。对此,我们先初始化一个排列permutation,然后我们初始的时候我们打乱这个排列表以得到不同的结果。这个排列表permutation也是Perlin本人给出的,这里我们只是拿过来用。

    private static readonly int[] permutation = { 151,160,137,91,90,15,               
    131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,    
    190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
    88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
    77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
    102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
    135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
    5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
    223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
    129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
    251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
    49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
    138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180
    };
        private static  int[] p;
        p = new int[256];
        Random rdm = new Random();
        int j;
        for (int x = 0; x < 256; x++)
        {
            j = (int)rdm.Next(256);
            p[x] = permutation[j];
        }

   有了排列表,我们就可以根据输入值所在晶格选择相应的梯度值了。选择梯度值的代码如下:

grad11 = p[(Xi + p[Yi & 255]) & 255] & 7;
grad12 = p[(Xi + 1 + p[Yi & 255]) & 255] & 7;
grad21 = p[(Xi + p[(Yi + 1) & 255]) & 255] & 7;
grad22 = p[(Xi + 1 + p[(Yi + 1) & 255]) & 255] & 7;

   grad11,grad12,grad21,grad22分别代表输入点所在晶格四个顶点的梯度值索引,得到排列值后我们让他与7相与,保证梯度值索引小于等7(因为我们只有8个梯度值)。有了梯度值索引后,我们通过gradients就可以得到梯度值了。
  第二步:生成距离向量。根据前文的生成原理,我们要得到距离向量,我们首先要得到输入点所在的晶格四个点的索引值,这个通过取输入点的Foor值就可以得到。输入点在所在晶格(四边形)的位置其实就是输入点的小数部分,得到位置用他去减晶格四个索引值就得一距离向量了。

Xi = GetFloor((float)x * Frequency);   //x,y为输入值
Yi = GetFloor((float)y * Frequency);
fracX = (float)x * Frequency - (float)Xi;  //x * Frequency是为了将输入值与频率关联起来。
fracY = (float)y * Frequency - (float)Yi;

  得到了距离向量和梯度值,我们将这两个值作点积。

noise11 = DotProduct(gradients[grad11,0], gradients[grad11, 1], fracX, fracY);
noise12 = DotProduct(gradients[grad12,0], gradients[grad12, 1], fracX - 1.0f, fracY);
noise21 = DotProduct(gradients[grad21,0], gradients[grad21, 1], fracX, fracY - 1.0f);
noise22 = DotProduct(gradients[grad22,0], gradients[grad22, 1], fracX - 1.0f, fracY - 1.0f);

  因为是2D,总共有四个点,所以点积后得到四个值,得到四个值后我们对这四个值进行双向性插值,这就是第三步了。
  第三步:插值。我们先得到插值参数,这个利用改进的Perlin算法缓和曲线就可以实现,不再赘述。

fracX = Fade(fracX);   //复用了变量fracX ,fracY 
fracY = Fade(fracY);
interpolatedx1 = (float)Cos_Interpolate(noise11, noise12, fracX);
interpolatedx2 = (float)Cos_Interpolate(noise21, noise22, fracX);
return (float)Cos_Interpolate(interpolatedx1, interpolatedx2, fracY) * Amplitude;

  好了,至此,2D Perlin噪声生成完毕,剩下的工作就是将其显示出来(或者保存为文件),这里不再赘述。生成图片的时候我们对颜色进行了一些处理,所以看起来跟之前看到的2D Perlin噪声图像有一点不一样。

这里写图片描述

  同样,我们可以使用分形布朗运动方法生成带有丰富细节的图像,下面这张图就是octaves = 4时的分形图像。
这里写图片描述

  我们采用了两种方式来展示生成的噪声图像,一种采用VS2015+C#,另一种采用UNIYT2017.1.1f1+Shader,这两种方式一种使用CPU来计算,另一种则使用GPU来计算,因为底层架构不同,实现方式也不一样。

(三)、小结

  这篇文章我们主要讲解了1D和2D Perlin噪声的生成算法,1D Perlin生成中距离向量没有参与或者说是以单位向量的形式参与运算,所以大大简化了复杂性,2D Perlin噪声生成中完全遵照了Perlin论文中生成噪声的算法来生成,排列表permutation主要是保证选择的梯度向量有很强的随机性,但同时他还保证对于同样的输入有同样的输出,这个对生成Perlin噪声至关重要,否则生成的噪声可能就是白噪声了。

(四)、代码下载

  一、VS2015+C#版的1D和2D 噪声生成代码:代码
  二、Unity+Shader版本: 代码

参考文献:
1、Understanding Perlin Noise https://flafla2.github.io/2014/08/09/perlinnoise.html

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_DavidWang_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值