Bootstrap

[图形学] 柏林噪声 (perlin noise)

 

参考论文:《An Image Synthesizer》 Ken Perlin

 

        如果你是游戏玩家,你也许曾在游戏风景中驻足,并仔细观察草木和岩石的贴图,并感叹于它那看似杂乱而又自然的纹脉。你未必会知道,这里面的很多纹理,并不是美工一个个像素填出来的,也不是相机拍摄出来的,而是由代码自动生成的。同样,波澜起伏的地面或海洋,也不一定完全是手工制作的高度图,而同样依赖于代码的力量,也就是我们接下来要介绍的柏林噪声。

 

        柏林噪声旨在描述自然中的随机效果,它创建的纹理可以直接运用于顶点着色器,而不是生成一张纹理图,然后用传统的纹理映射技术把贴图附加到一个三维物体上。

        这也就相当于,纹理将不需要适应表面,我们只需要提供每个顶点的(x,y,z)的坐标,传入柏林噪声函数Noise(),计算得到一个随机数,然后与原颜色运算,得到新的颜色,如同直接在物体表面绘制纹理一样。

        这意味着我们应用柏林噪声的代码最好采用着色器,直接把颜色运算之后传给顶点着色器。

        那么,问题的关键就在于这个Noise()应该是怎样的,perlin认为,噪声函数应当满足以下性质:

 

        1. 旋转统计不变性。(不管我们怎么旋转它的域,它都有同样的统计特性)

        2. 频率带通有一定界限。(它没有明显的大或者小的特征,而是在一定范围内)

        3. 平移统计不变性。(不管我们如何平移它的域,它都有同样的统计特性)

 

         这也就意味着,我们可以在不同的视觉尺度上创建所需表面的随机特性。

 

        噪声的生成步骤:

 

        

        1.预处理:考虑x,y,z空间中所有点的集合(坐标为整数),我们称这个集合为整数格。现在我们为整数格的每个点附一个(x,y,z)上的伪随机梯度值,也就是一个向量,并且要将其 处理成单位向量。

           perlin给出的计算方法如下 : 

          对于每个顶点先随机生成一个向量(比如调用C库中的rand()),然后将这个下标固定地映射到另外一个下标上,取另外一个下标对应的顶点向量。如果是二维或三维,这个过程就要重复2次,具体过程可以看之后的代码。

        2.如果(x,y,z)处在整数格上,那么此处的噪声就是d。

        3.如果(x,y,z)不在整数格上,我们计算光滑插值系数。

           具体的方法如下,以二维为例,我们要找到这个点周围的四个整数点,然后我们得到四个值,横坐标为bx0 ~ bx1,纵坐标为by0 ~ by1。点到bx0在x轴上的距离为rx0,到by0在y轴上的距离为ry0。

           perlin给出了一个缓和曲线使得线性插值连贯。

           能使得一阶导数连续的缓和曲线函数 (最初的版本) : s_curve(t) ( t * t * (3. - 2. * t) )

           能使得二阶导数连续的缓和曲线函数 (更新的版本) : s_curve(t) ( t * t * t * (6 * t * t - 15 * t + 10) )

           之后分别将rx0和ry0传入缓和曲线,得到一个新的值sx和sy。

           接下来做一个双线性插值,sx和sy就是第一步线性插值的系数,但是计算得到系数之后,我们还需要插值的起点和终点。他们的计算方法如下:

          (1)求(rx0,ry0)与左上角点的梯度b00的点乘,得到起点u,求(rx0 + 1,ry0)与右上角点的梯度b10的点乘,得到终点v,以uv为两端,sx为插值系数,做线性插值,得到a

          (2)求(rx0,ry0 + 1)与左下角点的梯度b01的点乘,得到起点u,求(rx0 + 1,ry0 + 1)与右下角点的梯度b11的点乘,得到终点v,以uv为两端,sx为插值系数,做线性插值,得到b

           ( 3 ) 最终,对a和b进行线性插值,插值系数为sy,得到最终的结果。

           以上算法步骤中,先对x轴还是先对y轴插值其实是无所谓的。

 

           最终得到的柏林噪声分布在 -1 ~ 1之间,我们可以把它映射到我们需要的颜色区别(比如0 ~ 255 或 0 ~ 1)得到对应的颜色。

 

         应用

         计算得到噪声之后,我们可以做一些简单的应用,比如创建一个简单的随机表面纹理:

         color = white * Noise(point)

         上式中的point也就是点的坐标。

         这样的效果如下,这张图具有较窄的带宽,它的细节仅仅分布特定区间内,也就是说,纹理的频谱没有一些中心峰的频率。关于生成这些图的代码会在后面给出。两张图仅仅是参数不同的区别,它们都是由柏林噪声生成的。

 

               

                                                   Perlin给出的官方例程的噪声生成结果

 

        生成以上基础柏林噪声纹理的代码会在后面给出,之后的一些变形都是在Noise()函数的基础上得到的,可以根据perlin给定的一些公式自己尝试实现这些效果。当然也可以自己试着模拟一些效果。

 

 

         当然,我们还可以做点别的。比如当我们可能想要不同范围的值,映射到不同的颜色上:

         color = Colorful(Noise(k*point)

         我们通过乘以一个常量k,就可以得到不同尺度上的纹理,这么描述似乎有些晦涩,我们可以看一下最终的效果:

         

         可以看到这个脉络和最简单的柏林噪声图是类似的,只不过我们把特定区间的rgb映射到了一个颜色上。(通过Colorful函数)

 

        我们把描述Noise()在x,y,z上各自的变化率(梯度)的函数为DNoise()。

        假如我们想制作一个凹凸纹理贴图,我们也可以在原有法线(normal)上加一些噪声的扰动,以做出凹凸的效果。在有了DNoise()之后,这样的行为变得非常方便:

        normal = normal + Dnoise(point)

        最终的效果图如下:

        

 

        我们来一些更有意思的。

        我们可以加入一个1/f波动,通过以下公式来模拟这个信号(把不同频率的octave叠加以得到更为真实的效果):

        

        以上1/f频谱函数的微分是一个单调频谱的函数,因此我们可以同时在所有的octave内创建类似的法线扰动,它的伪代码如下:

        f = 1

        while f < pixel_freq

                normal += Dnoise ( f * point )

                f *= 2

      这个算法在计算到像素级别后停止。以上这些算法都是逐像素独立的,也就是说计算一个像素颜色不需要依赖于其它像素,所以我们可以并行地计算每个像素,然后大幅度提升运行速度。

        

 

       大理石纹理

 

       我们注意到大理石的有着各种各样的层次,我们可以用正弦波来做一个简单的色彩滤镜:

       function boring_marble(point)

              x = point[1]

              return marble_color(sin(x))

       上式中,Point[1]是point的第一个分量(x),而marble_color()是一个样条曲线函数,它可以把一个数映射到一个色彩向量上。

       想要模拟更真实的大理石,可以加一个扰动:

      function marble(point)

              x = point[1] + turbulence(point)

              return marble_color(sin(x))

        

         而这个扰动turbluence()函数,可以通过Noise()得到,它的具体算法如下:

         function turbulence(p)

                 t = 0

                 scale = 1

                 while( scale > pixelsize)

                         t += abs(Noise(p/scale) * scale)

                        scale /= 2

                 return t

 

       水纹理

 

        假设我们想要模拟波浪的表面,为了简单起见,我们使用法线扰动,而不是修改表面点的值。

        我们使用球面波来做这件事,对于每个波浪中心,我们将会通过一个表面到中心的圆的函数来扰动表面法线。

        normal += wave ( point - center)

        function wave (v)

                return direction(v) * cycloid (norm (v))

        我们可以使用某一集合的点的Doise()的方向,产生随机分布的单位球体,来创建多个中心。

        function makewaves(n)

                for i in [ 1 .. n ]

                        center [i] = direction (Dnoise ( i * [100 0 0] ))

                return center

        比如如果我们想要创建一个有20个源的波浪,我们可以这样调用:

        if begin_frame

                center = makewaves(20)

        for c in center

                normal += wave (point - c)

       用20的参数做出的效果:

       

        如果想要做出更逼真的波浪效果,可以加入一个1/f扰动。如果对每个中心加一个随机的频率f,那么这个程序的最后一行将替换为:

        normal += wave ((point - c) * f ) / f

        此外,我们还需要让波浪随着时间动起来:
        function moving_wave (v ,Dphase)

                return direction(v) * cycloid (norm(v) - frame * Dphase)

       Dphase就是相变的速度,一个可以让效果比较真实的参数是f^(1/2)。

       做出的波浪效果:

       

        perlin的个人主页中给出了基本Noise()函数的代码,以下只截取了二维部分,加入绘制部分,更详细的代码可以参考以下链接,点击打开链接

#include <stdlib.h>
#include "gl/glut.h"
#include <stdio.h>
#include <time.h>
#include <math.h>

#define B 0x100        //256  
#define BM 0xff         //255  

#define N 0x1000        //4096  
#define NP 12           // 2^N   
#define NM 0xfff  

static int p[B + B + 2];
static float g2[B + B + 2][2];


int start = 1;

static void init(void);

//#define s_curve(t) ( t * t * (3. - 2. * t) )  
#define s_curve(t) ( t * t * t * (t * (t * 6. - 15.) + 10.) )  
#define lerp(t, a, b) ( a + t * (b - a) )  
#define setup(i,b0,b1,r0,r1)\
	t = vec[i] + N; \
	b0 = ((int)t) & BM; \
	b1 = (b0 + 1) & BM; \
	r0 = t - (int)t; \
	r1 = r0 - 1.;

float noise2(float vec[2])  
{
	int bx0, bx1, by0, by1, b00, b10, b01, b11;
	float rx0, rx1, ry0, ry1, *q, sx, sy, a, b, u, v, t;
	register int i, j;
	if (start){
		start = 0;
	    init();
    }
	setup(0, bx0, bx1, rx0, rx1);
	setup(1, by0, by1, ry0, ry1);
	i = p[bx0];
	j = p[bx1];

	b00 = p[i + by0];
	b10 = p[j + by0];
	b01 = p[i + by1];
	b11 = p[j + by1];

	sx = s_curve(rx0);  
	sy = s_curve(ry0);

#define at2(rx,ry) ( rx * q[0] + ry * q[1] )  
	q = g2[b00]; u = at2(rx0, ry0);
	q = g2[b10]; v = at2(rx1, ry0);  
	a = lerp(sx, u, v);

	q = g2[b01]; u = at2(rx0, ry1);
	q = g2[b11]; v = at2(rx1, ry1);
	b = lerp(sx, u, v);

	return lerp(sy, a, b);

}
static void normalize2(float v[2])
{
	float s;
	s = sqrt(v[0] * v[0] + v[1] * v[1]);
	v[0] = v[0] / s;
	v[1] = v[1] / s;
}

static void init(void)
{
	int i, j, k;
	for (i = 0; i < B; i++) {
		p[i] = i;
		for (j = 0; j < 2; j++)
			g2[i][j] = (float)((rand() % (B + B)) - B) / B;
	}  

	while (--i) {
		k = p[i];
		p[i] = p[j = rand() % B];
		p[j] = k;
	}
	for (i = 0; i < B + 2; i++){
		p[B + i] = p[i];
		for (j = 0; j < 2; j++)
			g2[B + i][j] = g2[i][j];
	}
}

void drawScene()
{
	float d1 = -300.0, d2 = -300.0;
	const float size = 0.1f;
	for (int i = 0; i < 100; i++) {
		d1 = -300;
		for (int j = 0; j < 100; j++) {
			float vec[2] = { d1,d2 };
			float color = (noise2(vec) + 1)/4;
			glColor3f(color,color,color);
			glRectf(i*size, j*size, i*size + size, j*size + size);
			d1 += 0.1;		
		}
		d2 += 0.1;
	}
}

void reshape(int width, int height)
{
	if (height == 0)height = 1;
	glViewport(0, 0, width, height);
	glMatrixMode(GL_PROJECTION);//设置矩阵模式为投影         
	glLoadIdentity();   //初始化矩阵为单位矩阵            
	glOrtho(0, 10,0,10, -100, 100); //正投影  
	glMatrixMode(GL_MODELVIEW);  //设置矩阵模式为模型  
}

void redraw()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);//清除颜色和深度缓存      
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();   //初始化矩阵为单位矩阵        
	drawScene();//绘制场景     
	glutSwapBuffers();//交换缓冲区    
}

int main(int argc,char* argv[])
{
	srand(time(nullptr));
	glutInit(&argc, argv);//对glut的初始化           
	glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH | GLUT_DOUBLE);      
	glutInitWindowSize(400, 400);//设置窗口大小           
	int windowHandle = glutCreateWindow("Perlin Noise");//设置窗口标题             
	glutDisplayFunc(redraw); //注册绘制回调函数           
	glutReshapeFunc(reshape);   //注册重绘回调函数           

	glutMainLoop();  // glut事件处理循环        
}

 

 

 

;