分形柏林噪声

· · 科技·工程

分形柏林噪声

刚刚写专栏的蒟蒻还不知道如何分类文章,也不知道什么才是好文章,勿喷

原理

前言

为了更好理解分形柏林噪声,我们需要理解先单层柏林噪声。

正如你所知的,分形柏林噪声的主要特点是“分形”,分形柏林噪声是由多个不同振幅、不同晶格大小的单层柏林噪声混合而成的。

在完成了单层柏林噪声的学习后,只需要多生成几个类似的噪声值,并叠加即可。

接下来,我们将不会先利用简单噪声生成算法加强理解,而是直接开始介绍。

晶格

正如上面提到的所谓“不同晶格大小”,在生成柏林噪声时,会将整个类似于“地图”的部分拆解为许多个“晶格”。也就是说,每个将会被取值的点,一定会属于至少1个晶格。 如图,一根根黑线划分出的方框就是“晶格”。

梯度向量

接下来,准备工作已经做完了。(没错,就是这么快)

我们要开始根据种子计算数据了。

柏林噪声,需要在每个晶格的顶点构建一条梯度向量。(方向根据种子随机,长度为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;

方向向量

我们只区分了不同的晶格,还没有区分不同的取值位置。我们可以使用当前取值位置于晶格右上顶点的位置相减,产生方向向量的x方向值与y方向值。(为了后续计算方便,我们将其限制在了0和1之间,即/ size部分)

    double xu = (x - chunk_x * size) / size;
    double yu = (y - chunk_y * size) / size;

点积

现在,所有前置内容已经完成,我们开始真正的重头戏吧。

我们已经获得了梯度向量和方向向量,为了获得我们的值,我们需要对梯度向量和方向向量进行点积操作。

敏锐的你可能已经发现了,我们现在还不能进行点积,因为梯度向量还只是一个方向啊!为了将其转换为一个x方向值与y方向值,我们需要使用三角函数,

当然,因为一个晶格里有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));

现在,点积操作变得异常方便,只需要将两者的xx相乘,yy相乘,再相加即可。即:

value=x_{1}x_{2}+y_{1}y_{2}

这一点,在代码中的实现见下:

    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个结果)

我们不妨先不考虑这个问题。我们先考虑别的问题,那就是我们该怎么在晶格边缘保持值的平滑?

我们可以使得取值的点在靠近某条边时,受与该条边的两个端点的梯度向量的的点积结果的影响增加。这样,在边上(也就是晶格交界处)时,只会受到该边的两个端点影响,便保证了边缘不会出现“悬崖”。

一个简单的方法,我们可以利用一种简单的算法(加权平均)基本实现我们要的插值效果。 但是事实证明,这样的插值在值为01是的导数不是0,会导致“地图”的头顶尖尖的。 我们可以使用由柏林噪声发明者设计的插值函数——fade函数:y=6x^{5}-15x^{4}+10x^{3},在值为01时都十分平滑。

于是我们可以将上述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时的预期效果: (因为洛谷图床限制,我们压缩了图片,实际可能稍有不同)

第一次写,肯定有纰漏。要是有什么表达不清楚,或者知识性错误,还请多多提出