分形柏林噪声
b9113fced86a32cad0d8 · · 科技·工程
分形柏林噪声
刚刚写专栏的蒟蒻还不知道如何分类文章,也不知道什么才是好文章,勿喷
原理
前言
为了更好理解分形柏林噪声,我们需要理解先单层柏林噪声。
正如你所知的,分形柏林噪声的主要特点是“分形”,分形柏林噪声是由多个不同振幅、不同晶格大小的单层柏林噪声混合而成的。
在完成了单层柏林噪声的学习后,只需要多生成几个类似的噪声值,并叠加即可。
接下来,我们将不会先利用简单噪声生成算法加强理解,而是直接开始介绍。
晶格
正如上面提到的所谓“不同晶格大小”,在生成柏林噪声时,会将整个类似于“地图”的部分拆解为许多个“晶格”。也就是说,每个将会被取值的点,一定会属于至少1个晶格。 如图,一根根黑线划分出的方框就是“晶格”。
梯度向量
接下来,准备工作已经做完了。(没错,就是这么快)
我们要开始根据种子计算数据了。
柏林噪声,需要在每个晶格的顶点构建一条梯度向量。(方向根据种子随机,长度为
对于每个晶格,我们需要确切的知道它四个顶点梯度向量的方向。
因为在我的代码中,也在大部分地方,柏林噪声是直接调用函数求值的,所以我们需要利用取值位置直接获得当前晶格的位置,这很简单,除以晶格边长即可:
int chunk_x = floor(1.0 * x / size);
int chunk_y = floor(1.0 * y / size);
或许你会疑惑:“为什么需要晶格位置?”,你马上就要知道了。
显然,对于相邻的晶格来说,它们的某一顶点是重合的。然而,直接进行随机的话,会导致同一个顶点在不同的晶格中会出现不同的梯度向量,这不是我们想要的。
你可能已经意识到了什么,我们可以通过利用晶格位置的加减,赋予同一个随机种子,产生相同的梯度向量。
然而,另外设置种子并随机太麻烦了,我们使用了哈希。(我们还对结果取模了)
int h_ul = _hash_(seed + _hash(chunk_x) + hash_(chunk_y + 1));
int h_ur = _hash_(seed + _hash(chunk_x + 1) + hash_(chunk_y + 1));
int h_dl = _hash_(seed + _hash(chunk_x) + hash_(chunk_y));
int h_dr = _hash_(seed + _hash(chunk_x + 1) + hash_(chunk_y));
int ul = h_ul % 360;
int ur = h_ur % 360;
int dl = h_dl % 360;
int dr = h_dr % 360;
方向向量
我们只区分了不同的晶格,还没有区分不同的取值位置。我们可以使用当前取值位置于晶格右上顶点的位置相减,产生方向向量的/ size
部分)
double xu = (x - chunk_x * size) / size;
double yu = (y - chunk_y * size) / size;
点积
现在,所有前置内容已经完成,我们开始真正的重头戏吧。
我们已经获得了梯度向量和方向向量,为了获得我们的值,我们需要对梯度向量和方向向量进行点积操作。
敏锐的你可能已经发现了,我们现在还不能进行点积,因为梯度向量还只是一个方向啊!为了将其转换为一个
当然,因为一个晶格里有4个梯度向量,我们就产生了8个取值。
double grad_ul_x = cos(ul * (pi / 180));
double grad_ul_y = sin(ul * (pi / 180));
double grad_ur_x = cos(ur * (pi / 180));
double grad_ur_y = sin(ur * (pi / 180));
double grad_dl_x = cos(dl * (pi / 180));
double grad_dl_y = sin(dl * (pi / 180));
double grad_dr_x = cos(dr * (pi / 180));
double grad_dr_y = sin(dr * (pi / 180));
现在,点积操作变得异常方便,只需要将两者的
这一点,在代码中的实现见下:
double vul = (xu) * grad_ul_x + (yu - 1.0) * grad_ul_y;
double vur = (xu - 1.0) * grad_ur_x + (yu - 1.0) * grad_ur_y;
double vdl = (xu) * grad_dl_x + (yu) * grad_dl_y;
double vdr = (xu - 1.0) * grad_dr_x + (yu) * grad_dr_y;
你可能发现了,其中有部分情况存在将方向向量的某一方向值减去1.0,这是为了将右上角的方向向量转化为其它方向的方向向量。
插值
问题又来了,上述过程中,产生了4个数值,我们应该怎么做?(显然,我们的结果要同时包含上述4个结果)
我们不妨先不考虑这个问题。我们先考虑别的问题,那就是我们该怎么在晶格边缘保持值的平滑?
我们可以使得取值的点在靠近某条边时,受与该条边的两个端点的梯度向量的的点积结果的影响增加。这样,在边上(也就是晶格交界处)时,只会受到该边的两个端点影响,便保证了边缘不会出现“悬崖”。
一个简单的方法,我们可以利用一种简单的算法(加权平均)基本实现我们要的插值效果。
但是事实证明,这样的插值在值为,会导致“地图”的头顶尖尖的。
我们可以使用由柏林噪声发明者设计的插值函数——fade函数:
于是我们可以将上述4个取值全部纳入我们插值的范围,产生了如下代码:
double vu = vul + fade(xu) * (vur - vul);
double vd = vdl + fade(xu) * (vdr - vdl);
return vd + fade(yu) * (vu - vd);
上面的vd + fade(yu) * (vu - vd)
就是我们要的单层单点取值。
分形
还记得我们的目标是分形柏林噪声吗?
现在,我们已经完成了单层柏林噪声,到了最简单的一步。
我们只需要提供给perlin
函数相同的种子、相同的坐标、不同的晶格大小,再直接相加,归一化后直接输出,就完成了。
示例代码
如何使用?
再刚刚运行代码时,程序会要求你输入一个种子,接着,程序会计算指定范围内每个点的取值(因为涉及大量三角学运算,耗时会较长),接着转换为RGB后存储入指定.bmp
文件中再打开。
源代码
#include<bits/stdc++.h>
using namespace std;
const int w = 2048;
const int h = 2048;
const double pi = 3.141592653589793;
char img[w*h*3];
int _hash(int x){//普通的哈希函数
x = (x << 2) ^ 0xb9113fce;
x *= 0xd86a32ca;
x = (x << 2) ^ 0xd0d8958b;
x *= 0x1d7539e1;
x = (x << 2) ^ 0xab63ec6c;
x *= 0x95b08412;
x = (x << 2) ^ 0xe7d7ae4e;
x *= 0xd22ee12b;
return x;
}
int hash_(int x){
x *= 0xd22ee12b;
x = (x << 2) ^ 0xe7d7ae4e;
x *= 0x95b08412;
x = (x << 2) ^ 0xab63ec6c;
x *= 0x1d7539e1;
x = (x << 2) ^ 0xd0d8958b;
x *= 0xd86a32ca;
x = (x << 2) ^ 0xb9113fce;
return x;
}
int _hash_(int x){
x *= 0xba8956ac;
x = (x << 2) ^ 0x295ff4a6;
x *= 0xbf351d6c;
x = (x << 2) ^ 0xbb1ea812;
x *= 0x3e119203;
x = (x << 2) ^ 0x2c3bd591;
x *= 0x114b0c94;
x = (x << 2) ^ 0x5b126d49;
return x;
}
double fade(double x){
return 6.0 * pow(x,5) - 15.0 * pow(x,4) + 10.0 * pow(x,3);
}
double perlin(int x, int y, int seed, double size){
int chunk_x = floor(1.0 * x / size);
int chunk_y = floor(1.0 * y / size);
int h_ul = _hash_(seed + _hash(chunk_x) + hash_(chunk_y + 1));
int h_ur = _hash_(seed + _hash(chunk_x + 1) + hash_(chunk_y + 1));
int h_dl = _hash_(seed + _hash(chunk_x) + hash_(chunk_y));
int h_dr = _hash_(seed + _hash(chunk_x + 1) + hash_(chunk_y));
int ul = h_ul % 360;
int ur = h_ur % 360;
int dl = h_dl % 360;
int dr = h_dr % 360;
double xu = (x - chunk_x * size) / size;
double yu = (y - chunk_y * size) / size;
// 梯度向量计算
double grad_ul_x = cos(ul * (pi / 180));
double grad_ul_y = sin(ul * (pi / 180));
double grad_ur_x = cos(ur * (pi / 180));
double grad_ur_y = sin(ur * (pi / 180));
double grad_dl_x = cos(dl * (pi / 180));
double grad_dl_y = sin(dl * (pi / 180));
double grad_dr_x = cos(dr * (pi / 180));
double grad_dr_y = sin(dr * (pi / 180));
// 点积计算
double vul = (xu) * grad_ul_x + (yu - 1.0) * grad_ul_y;
double vur = (xu - 1.0) * grad_ur_x + (yu - 1.0) * grad_ur_y;
double vdl = (xu) * grad_dl_x + (yu) * grad_dl_y;
double vdr = (xu - 1.0) * grad_dr_x + (yu) * grad_dr_y;
// 插值计算
double vu = vul + fade(xu) * (vur - vul);
double vd = vdl + fade(xu) * (vdr - vdl);
return vd + fade(yu) * (vu - vd);
}
void WriteBMP(char* img,const char* filename){
int l = (w * 3 + 3) / 4 * 4;
int bmi[]= {l * h + 54,0,54,40,w,h,1 | 3 * 8 << 16,0,l * h,0,0,100,0};
FILE *fp = fopen(filename,"wb");
fprintf(fp,"BM");
fwrite(&bmi,52,1,fp);
fwrite(img,1,l * h,fp);
fclose(fp);
}
int main(){
int seed;
cin >> seed;
// 分形噪声参数配置
const vector<double> sizes = {128,64,32,16,8,4,2,1,0.5,0.25,0.125};//晶格尺寸
const vector<double> amps = {128,64,32,16,8,4,2,1,0.5,0.25,0.125};//对应振幅
const double amp_sum = (128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 + 0.5 + 0.25 + 0.125) / 1.8;//振幅总和
for(int i = 0; i < h; i++){
for(int j = 0; j < w; j++){
// 分形噪声叠加
double total = 0.0;
for(int k = 0;k < sizes.size();k++){
double noise = perlin(j, i, seed, sizes[k]);
total += noise * amps[k];
}
// 归一化处理
double value = total / amp_sum;
// 颜色映射
if(0.75 >= value && value > 0.5){
double t = (value - 0.5) / 0.25;
img[3 * (i * w + j) + 0] = 255 * (1 - t);
img[3 * (i * w + j) + 1] = 0;
img[3 * (i * w + j) + 2] = 0;
}else if(0.5 >= value && value > 0.25){
double t = (value - 0.25) / 0.25;
img[3 * (i * w + j) + 0] = 255;
img[3 * (i * w + j) + 1] = 255 * (1 - t);
img[3 * (i * w + j) + 2] = 0;
}else if(0.25 >= value && value > 0){
double t = (value - 0) / 0.25;
img[3 * (i * w + j) + 0] = 255 * t;
img[3 * (i * w + j) + 1] = 255;
img[3 * (i * w + j) + 2] = 0;
}else if(0 >= value && value > -0.25){
double t = (value - -0.25) / 0.25;
img[3 * (i * w + j) + 0] = 0;
img[3 * (i * w + j) + 1] = 255;
img[3 * (i * w + j) + 2] = 255 * (1 - t);
}else if(-0.25 >= value && value > -0.5){
double t = (value - -0.5) / 0.25;
img[3 * (i * w + j) + 0] = 0;
img[3 * (i * w + j) + 1] = 255 * t;
img[3 * (i * w + j) + 2] = 255;
}else if(-0.5 >= value && value > -0.75){
double t = (value - -0.75) / 0.25;
img[3 * (i * w + j) + 0] = 255 * (1 - t);
img[3 * (i * w + j) + 1] = 0;
img[3 * (i * w + j) + 2] = 255;
}else if(-0.75 >= value && value > -1){
double t = (value - -1) / 0.25;
img[3 * (i * w + j) + 0] = 255;
img[3 * (i * w + j) + 1] = 255 * (1 - t);
img[3 * (i * w + j) + 2] = 255;
}else{
}
}
}
WriteBMP(img,"result.bmp");//可自由修改
system("result.bmp");
}
种子为0
时的预期效果:
(因为洛谷图床限制,我们压缩了图片,实际可能稍有不同)
第一次写,肯定有纰漏。要是有什么表达不清楚,或者知识性错误,还请多多提出