柏林噪声(Perlin Noise)

什么是柏林噪声?

说起噪声大家可能会想起这个:
在这里插入图片描述
但是这个噪声看起来很不自然,而且现实中的自然噪声并不长这个样子,比如起伏的山脉,大理石的纹理,天空的云彩,这些噪声虽然看起来杂乱无章,其实它们都有着内在规律。柏林噪声的用处就在这里:生成看似杂乱但是有序的内容。
在这里插入图片描述
那么如何生成它们呢?
在这里插入图片描述
主要有三个步骤:

1.固定一部分点的颜色。

2.“平滑”这些固定点之间的颜色。

3.用上面的方法生成几个不同频率的平滑噪音然后相加。

如何确定噪音的生成频率?只要改变固定点的个数就可以了,固定点多的频率就高,固定点少的频率就低。那么怎么平滑固定点之间的颜色?

perlin noise

其实上面所说的固定一部分点的颜色然后进行平滑的方法是一个效果比较差的函数,它其实叫value 噪声,百度柏林噪声第一页就有几篇博客,标题是perlin noise,而实现的是value noise,o(╥﹏╥)o。
现在就到了perlin噪声的关键环节,我们不固定点的颜色,而是固定点的梯度(gradient)。

步骤就变成了:

1.固定一部分点的gradient。

2"平滑"这些固定点中间的gradient。

3.用上面的方法生成几个不同频率的平滑噪音, 然后相加(准确来说这一步是属于分形噪声的部分)。

先上代码,之后一点点讲,这个代码是简化版的,只是为了帮助理解,真正用的时候还是要用官方的那篇代码,我把我写的代码的C++版放在了文章的末尾:

#include "pch.h"
#include <iostream>
#include <ctime>
#include<fstream>
using namespace std;
struct vec3
{
	double x;
	double y;
	double z;
	vec3() {};
	vec3(double x, double y,double z) :x(x), y(y),z(z) {}
	double operator*(vec3 t)
	{
		return x * t.x + y * t.y+z*t.z;
	}
	vec3 operator+(vec3 t)
	{
		return vec3(x + t.x, y + t.y,z+t.z);
	}
};
class perlinNoise {
public:
	vec3 g[12] = { vec3(1,1,0),vec3(-1,1,0),vec3(1,-1,0),vec3(-1,-1,0),
		vec3(1,0,1),vec3(-1,0,1),vec3(1,0,-1),vec3(-1,0,-1), vec3(0,1,1),
		vec3(0,-1,1),vec3(0,1,-1),vec3(0,-1,-1) };
	vec3 vertex[25][25][25];
	perlinNoise()
	{
		srand((int)time(0));
		for (int i = 0; i < 25; i++)
		{
			for (int j = 0; j < 25; j++)
			{
				for (int k = 0; k < 25; k++)
				{
					int mrand = rand() % 12;
					vertex[i][j][k] = g[mrand];
				}
			}
		}
	}
	double generateNoise(double x, double y,double z)
	{
		int X = (int)floor(x);
		int Y = (int)floor(y);
		int Z = (int)floor(z);
 
		double u = x - X;
		double v = y - Y;
		double w = z - Z;
 
		vec3 vec000 = vec3(u, v,w);
		vec3 vec010 = vec3(u, v, w) + vec3(0, -1, 0);
		vec3 vec100 = vec3(u, v, w) + vec3(-1, 0, 0);
		vec3 vec110 = vec3(u, v, w) + vec3(-1, -1, 0);
		vec3 vec001 = vec3(u, v, w) + vec3(0, 0, -1);
		vec3 vec011 = vec3(u, v, w) + vec3(0, -1, -1);
		vec3 vec101 = vec3(u, v, w) + vec3(-1, 0, -1);
		vec3 vec111 = vec3(u, v, w) + vec3(-1, -1, -1);
 
		double g000 = vec000 * vertex[X][Y][Z];
		double g010 = vec010 * vertex[X][Y + 1][Z];
		double g100 = vec100 * vertex[X + 1][Y][Z];
		double g110 = vec110 * vertex[X + 1][Y + 1][Z];
		double g001 = vec001 * vertex[X][Y][Z + 1];
		double g011 = vec011 * vertex[X][Y + 1][Z + 1];
		double g101 = vec101 * vertex[X + 1][Y][Z + 1];
		double g111 = vec111 * vertex[X + 1][Y + 1][Z + 1];
 
		u = fade(u);
		v = fade(v);
		w = fade(w);
 
		double lerpx1 = lerp(g000, g100, u);
		double lerpx2 = lerp(g010, g110, u);
		double lerpy1 = lerp(lerpx1, lerpx2, v);
 
		double lerpx3 = lerp(g001, g101, u);
		double lerpx4 = lerp(g011, g111, u);
		double lerpy2 = lerp(lerpx3, lerpx4, v);
 
		double lerpz = lerp(lerpy1, lerpy2, w);
 
		return lerpz;
	}
	double lerp(double a, double b, double t)
	{
		return a + t * (b - a);
	}
	double fade(double t)
	{
		return t * t * t * (t * (t * 6 - 15) + 10);
	}
};
int main()
{
	perlinNoise a;
	ofstream outfile("perlinNoise.ppm");
	int X = 400, Y = 400;
	outfile << "P3" << endl << X << " " << Y << endl << "255" << endl;
	for (double i = 0; i < 20; i += 0.05)
	{
		for (double j = 0; j < 20; j += 0.05)
		{
			int temp = (a.generateNoise(i, j,7.856413) + 1.0) / 2.0*255.0;
			outfile << temp << " " << temp << " " << temp << " ";
		}
	}
}

对于二维来说,要实现perlin噪声需要:
在这里插入图片描述
1.定义一个晶格结构(就当成一个单位正方形),每个晶格的顶点有一个“伪随机”的梯度向量(说它是伪随机的是因为:1.梯度向量是我们随机的选择我们自定义的向量,而不是随机的选择随机的向量。2.一旦确定了,固定的点就有了固定的梯度向量)。这个梯度向量就当做向量理解吧。对于二维的Perlin噪声来说,晶格结构就是一个平面网格,三维的就是一个立方体网格。
2.输入一个点(二维的话就是二维坐标,三维就是三维坐标,n维的就是n个坐标),我们找到和它相邻的那些晶格顶点(二维下有4个,三维下有8个,n维下有个),计算该点到各个晶格顶点的距离向量,再分别与顶点上的梯度向量做点乘,得到个点乘结果。
3.使用缓和曲线(ease curves)来计算它们的权重和

下面这些就是已经定义好的随机向量,它们是由立方体12条棱的中点决定的,至于为什么这样做,《GPU精粹》上说这样生成的图像比随机产生固定向量的“污点”更少:

	vec3 g[12] = { vec3(1,1,0),vec3(-1,1,0),vec3(1,-1,0),vec3(-1,-1,0),
		vec3(1,0,1),vec3(-1,0,1),vec3(1,0,-1),vec3(-1,0,-1), vec3(0,1,1),
		vec3(0,-1,1),vec3(0,1,-1),vec3(0,-1,-1) };

这个构造函数就是为每个点固定一个梯度,由生成的随机数来确定 使用g[12]中的哪一个梯度,由于空间原因只定义了(0,0,0)到(25,25,25)的点,所以最后输入的点的大小不能超过25,不过官方的代码可以输入更大的值。

	vec3 vertex[25][25][25];
	perlinNoise()
	{
		srand((int)time(0));
		for (int i = 0; i < 25; i++)
		{
			for (int j = 0; j < 25; j++)
			{
				for (int k = 0; k < 25; k++)
				{
					int mrand = rand() % 12;
					vertex[i][j][k] = g[mrand];
				}
			}
		}
	}

我们需要求出另外4个向量(在3维空间则是8个),它们分别从各顶点指向输入点(蓝色点)。下面有个2维空间下的例子:
在这里插入图片描述
二维空间下:

		int X = (int)floor(x);
		int Y = (int)floor(y);

		double u = x - X;
		double v = y - Y;
		
		vec2 vec00 = vec2(u, v);
		vec2 vec01 = vec2(u, v) + vec2(0, -1);
		vec2 vec10 = vec2(u, v) + vec2(-1, 0);
		vec2 vec11 = vec2(u, v) + vec2(-1, -1);

在三维空间中求出这8个向量:

int X = (int)floor(x);
int Y = (int)floor(y);
int Z = (int)floor(z);
 
double u = x - X;
double v = y - Y;
double w = z - Z;
 
vec3 vec000 = vec3(u, v,w);
vec3 vec010 = vec3(u, v, w) + vec3(0, -1, 0);
vec3 vec100 = vec3(u, v, w) + vec3(-1, 0, 0);
vec3 vec110 = vec3(u, v, w) + vec3(-1, -1, 0);
vec3 vec001 = vec3(u, v, w) + vec3(0, 0, -1);
vec3 vec011 = vec3(u, v, w) + vec3(0, -1, -1);
vec3 vec101 = vec3(u, v, w) + vec3(-1, 0, -1);
vec3 vec111 = vec3(u, v, w) + vec3(-1, -1, -1);

接着,对每个顶点的梯度向量和距离向量做点积运算,我们就可以得出每个顶点的影响值:

double g000 = vec000 * vertex[X][Y][Z];
double g010 = vec010 * vertex[X][Y + 1][Z];
double g100 = vec100 * vertex[X + 1][Y][Z];
double g110 = vec110 * vertex[X + 1][Y + 1][Z];
double g001 = vec001 * vertex[X][Y][Z + 1];
double g011 = vec011 * vertex[X][Y + 1][Z + 1];
double g101 = vec101 * vertex[X + 1][Y][Z + 1];
double g111 = vec111 * vertex[X + 1][Y + 1][Z + 1];

下面是平滑曲线的函数,最初平滑曲线是这样的 w(t)=3t² - 2t³ ,它保证了w(0)=0, w(1)=1.和w’(0) = 0和w’(1)=0,而新版的这个函数还保证了w’’(0) = 0和w’’(1)=0,总之就是更平滑了

	double fade(double t)
	{
		return t * t * t * (t * (t * 6 - 15) + 10);
	}

三次方插值和五次方插值对比:在这里插入图片描述
我们需要用fade函数来变换u,v,w的值来让插值更加平滑:

u = fade(u);
v = fade(v);
w = fade(w);

根据各顶点对该点的影响值确定数值大小,总共需要7次插值(二维需要3次):
在这里插入图片描述

		double lerpx1 = lerp(g000, g100, u);
		double lerpx2 = lerp(g010, g110, u);
		double lerpy1 = lerp(lerpx1, lerpx2, v);

		double lerpx3 = lerp(g001, g101, u);
		double lerpx4 = lerp(g011, g111, u);
		double lerpy2 = lerp(lerpx3, lerpx4, v);

		double lerpz = lerp(lerpy1, lerpy2, w);

		return lerpz;

还有一个内存问题
要想获得更大的频率,我们就必须要有更多的固定点,所以高频率的噪音绝对是有必要的. 而为了储存固定点的gradient, 要建立一个数组才行. 而当噪音的频率非常高的时候, 由于需要的固定点会很多, 25X25X25显然是不够的,这个数组也会非常的大. 为了降低内存的使用, perlin使用了1个256个元素的哈希表. 也就是说, 预先找出合理的, 足够随机的256个gradient, 存在一个表里. 然后每次需要某个固定点的gradient值的时候, 通过这个这个点的坐标, 伪随机的在表里选出一个值. 对于3d的情况, 如果我们想要坐标(i,j,k)的gradient g(i,j,k),而P里预存储了256个gradient, 那么:

g(i, j, k) = P[ ( i + P[ (j + P[k]) mod 256 ] ) mod 256 ]

在1D数组保存3D数据
index=x+(nx)y+(nx)(ny)*z
这样, 在生成perlin noise的时候, 内存的使用限定在了1个256大小的哈希表.,下面是这一部分的代码,由于很多容易混的问题都堆在了这块,所以可能不是很容易理解

		int A = p[X] + Y, AA = p[A] + Z, AB = p[A + 1] + Z,     
			B = p[X + 1] + Y, BA = p[B] + Z, BB = p[B + 1] + Z; 

		return lerp(w, lerp(v, lerp(u, grad(p[AA], x, y, z),
									   grad(p[BA], x - 1, y, z)), 
								lerp(u,grad(p[AB], x, y - 1, z),  
									   grad(p[BB], x - 1, y - 1, z))),
			lerp(v, lerp(u, grad(p[AA + 1], x, y, z - 1), 
							grad(p[BA + 1], x - 1, y, z - 1)), 
				lerp(u, grad(p[AB + 1], x, y - 1, z - 1),
						grad(p[BB + 1], x - 1, y - 1, z - 1))));

接下来是fbm的介绍(后面所有代码均以文末代码为基础写的

现在想一些问题,
如何增大频率?增加更多采样点。
如何增加更多采样点?覆盖更多固定点。
如何覆盖更多固定点?增大输入数之间的间隔。
如果频率一直增大,而振幅一直减小会变成什么样子?
在这里插入图片描述
正如你所看到的,如果频率增加而振幅减少,那么这些函数的“特性”会越来越小,而且越来越近。如果两个频率值的比例正好是2:1,那么就叫做倍程。如果将不同频率的函数加和在一起,那么我们会看到更有意思的着色器效果。

这样做的结果是一个包含不同大小特性的函数。低频率函数的大幅度振动构成基本的形状,而高频率函数的小幅度振动构成小范围的细节信息。Perlin将不同倍程的噪声相加后的函数(每个噪声都比前一个倍程的噪声振幅减半)叫做1/f噪声,不过如今通常会使用分形布朗运动(fraction Brownian motion)或者fbm这个名词来描述它。

下面是perlin noise+fbm的代码

ImprovedNoise pNoise;
int octaves = 4;
ofstream outfile("分形噪声.ppm");
int xx=400, yy=400;
outfile << "P3" << endl << xx << " " << yy << " " << endl << "255" << endl;
for (double i = 0; i < 20; i+=0.05)
{
	for (double j = 0; j < 20; j+=0.05)
	{
//*************************************************************************************
		double sum = 0,maxValue=0,frequency = 1, amplitude = 1;
		for (int k = 0; k < octaves; k++, frequency *= 2.0, amplitude *= 0.5)
		{
			sum += pNoise.noise(i*frequency, j*frequency, 7.89101112131415)*amplitude;
			maxValue += amplitude;
		}
		sum /= maxValue;
		int b = (sum+1)*255.0/2.0;
		outfile << b << " " << b << " " << b << endl;
//*************************************************************************************
	}
}

下面是效果图,第一个是上面教学代码产生的Perlin噪声图,第二个是Perlin+fbm产生的图:
在这里插入图片描述
在这里插入图片描述
1.我们可以使用噪声函数的绝对值来产生其他一些有趣的效果。这一方法导致导数产生了不连续性,因为函数在0分界线上会发生明显的偏折。如果噪声函数在不同频率的结果都发生了偏折,并且将结果叠加在一起的话,那么得到的纹理结果将会在各个尺度上都出现皱痕效果。Perlin将这类噪声称作湍流,因为它看起来像是湍急水流产生的效果。这种效果可以模拟不同的自然现象,比如用这类噪声来模拟火焰或者熔岩。

sum += abs(pNoise.noise(i*frequency, j*frequency, 7.89101112131415))*amplitude;

湍流效果:
在这里插入图片描述
2.另一类噪声函数的形式就是将噪声函数作为正弦等周期函数的一部分使用。将噪声作为正弦函数的输入值加入,我们就可以得到一个“随机”振荡的函数。使用这个函数可以创建一些类似颜色脉络变化的效果。
s i n ( x + ∣ n o i s e ( p ) ∣ + 12 ∣ n o i s e ( 2 p ) ∣ + 14 ∣ n o i s e ( 4 p ) ∣ + . . . ) sin(x+|noise(p)|+12|noise(2p)|+14|noise(4p)|+...) sin(x+noise(p)+12noise(2p)+14noise(4p)+...)
这个公式可以让表面沿着x方向形成一个条纹状的结构,我们可以通过改变x分量前面的系数来控制条纹的疏密。

double sum = 0frequency = 1, amplitude = 1;
for (int k = 0; k < octaves; k++, frequency *= 2.0, amplitude *= 0.5)
{
	sum += abs(pNoise.noise(i*frequency, j*frequency, 7.89101112131415))*amplitude;
}
sum = sin(sum + i * frequency / 16.0);
int b = (sum+1)*255.0/2.0;
outfile << b << " " << b << " " << b << endl;

大理石效果:
在这里插入图片描述
3.(真·瞎搞)给不同的值赋予不同的颜色

if (b < 50)
{
	outfile << 255 << " " << b << " " << b << endl;
}
else if(b<100)
{
	outfile << b << " " << 255 << " " << b << endl;
}
else if (b < 150)
{
	outfile << 175 << " " << 175 << " " << b << endl;
}
	else if (b < 200)
{
	outfile << b << " " << 125 << " " << 125 << endl;
}
else
{
	outfile << b << " " << b << " " << 255 << endl;
}

在这里插入图片描述
一些Perlin噪声产生美图,这些作品基本上都来自这位大神的文章
1.nimitz发明了一种对每层噪音添加旋转的方法,得到的图形看起来像翻滚的岩浆
在这里插入图片描述
2.除了操作噪音本身之外,还可以操作噪音所在的空间(坐标系)。
在这里插入图片描述
3.1/z变换,最简单的一种共形变换,1/z再进展到把模长也除掉就会得到星光状的图像。
在这里插入图片描述
Perlin Noise代码的C++版

#include "pch.h"
#include <iostream>
#include <cmath>
#include<fstream>
using namespace std;

class ImprovedNoise {
public :
	ImprovedNoise()
	{
		int permutation[512] = { 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
		};
		for (int i = 0; i < 256; i++) p[256 + i] = p[i] = permutation[i];
	}
	double noise(double x, double y, double z) {
		int X = (int)floor(x) & 255,             
			Y = (int)floor(y) & 255,             
			Z = (int)floor(z) & 255;

			x -=floor(x);                            
			y -=floor(y);                            
			z -=floor(z);
		double  u = fade(x),                      
				v = fade(y),                         
				w = fade(z);
		int A = p[X] + Y, AA = p[A] + Z, AB = p[A + 1] + Z,     
			B = p[X + 1] + Y, BA = p[B] + Z, BB = p[B + 1] + Z; 

		return lerp(w, lerp(v, lerp(u, grad(p[AA], x, y, z),
									   grad(p[BA], x - 1, y, z)), 
								lerp(u,grad(p[AB], x, y - 1, z),  
									   grad(p[BB], x - 1, y - 1, z))),
			lerp(v, lerp(u, grad(p[AA + 1], x, y, z - 1), 
							grad(p[BA + 1], x - 1, y, z - 1)), 
				lerp(u, grad(p[AB + 1], x, y - 1, z - 1),
						grad(p[BB + 1], x - 1, y - 1, z - 1))));
	}
	double fade(double t) { return t * t * t * (t * (t * 6 - 15) + 10); }
	double lerp(double t, double a, double b) { return a + t * (b - a); }
	double grad(int hash, double x, double y, double z) {
		int h = hash & 15;                     
		double u = h < 8 ? x : y,              
			v = h < 4 ? y : h == 12 || h == 14 ? x : z;
		return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
	}
	int p[512];

};
int main()
{
	ImprovedNoise pNoise;
	int octaves = 4;
	ofstream outfile("noise.ppm");
	int xx=400, yy=400;
	outfile << "P3" << endl << xx << " " << yy << " " << endl << "255" << endl;
	for (double i = 0; i < 20; i+=0.05)
	{
		for (double j = 0; j < 20; j+=0.05)
		{
			double sum = pNoise.noise(i, j, 7.89101112131415);
			int b = (sum + 1)*255.0 / 2.0;
			outfile << b << " " << b << " " << b << endl;
	
		}
	}
	//cout << count;
	return 0;
}
  • 58
    点赞
  • 129
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值