柏林噪声(Perlin Noise)

要获得看起来很酷的实心纹理,大多数人使用某种形式的Perlin噪声。Perlin噪声返回类似下图的噪声。

Perlin噪声的一个关键部分是它是可重复的:它接受一个3D点作为输入,并总是返回相同的随机数字。附近的点返回相似的数字。Perlin噪声的另一个重要部分是它要简单快速,因此通常作为一种技巧来实现。

Using Blocks of Random Numbers

使用随机数块返回albedo。文章中对于该噪声的实现如下:

  1. 首先定义了一个2的整数倍的数组大小L(文章中为255)
  2. 然后定义一个随机数组随机获得L个【0~1)的随机数。
  3. 分别计算perm_x数组和对应的y,z数组。
  4. 混乱数组中的(0~i)的下标所指向的(0~i)的数
  5. 最后按照以下方式得到albedo(其实也就是256个噪声点)
double noise(const point3& p) const {
    auto i = int(4*p.x()) & 255;
    auto j = int(4*p.y()) & 255;
    auto k = int(4*p.z()) & 255;

    return randfloat[perm_x[i] ^ perm_y[j] ^ perm_z[k]];
}

首先是随机选取256个噪声点

perlin(){
    for(int i=0;i<point_count;i++){
        randfloat[i] = random_double();
    }

}

然后对于perm数组,得到像素点的打乱的值。

private:
static const int point_count = 256;
double randfloat[point_count];
int perm_x[point_count];
int perm_y[point_count];
int perm_z[point_count];
static void perlin_generate_perm(int* p){
    for(int i=0;i<point_count;i++){
        p[i] = i;
    }
    permute(p,point_count);
}
static void permute(int* p,int n){
    for(int i=n-1;i>0;i--){
        int target = random_int(0,i);
        int temp = p[i];
        p[i] = target;
        p[target] = temp;
    }
}

最后在初始化阶段就需要去这样初始化。

perlin(){
    for(int i=0;i<point_count;i++){
        randfloat[i] = random_double();
    }
    perlin_generate_perm(perm_x);
    perlin_generate_perm(perm_y);
    perlin_generate_perm(perm_z);
}

以及对于传入一个点,返回其albedo.这里文章中是进行了缩放,将坐标乘4以至于实现表现的快速缩放。并且防止数组溢出,与255取模(&)下面的取反也是这个原因。

double noise(const Point3& p) const{
    int i = int(4*p.x()) & 255;
    int j = int(4*p.y()) & 255;
    int z = int(4*p.z()) & 255;

    return randfloat[perm_x[i]^perm_y[j]^perm_z[z]];
}

Perlin材质

实现了这样的Perlin方法了以后,我们就需要去根据这个Perlin类创建它的材质,也就是给定顶点后返回其albedo

class noise_texture : public texture{
public:
noise_texture(){}
color value(double u,double v,const Point3& p) const override{
    return color(1.0,1.0,1.0) * noise.noise(p);
}
private:
perlin noise;
};

在main方法中进行调用。

void perlin_spheres(){
    hittable_list world;
    shared_ptr<noise_texture> pertex = make_shared<noise_texture>();
    world.add(make_shared<sphere>(Point3(0,-1000,0),1000,make_shared<lambertian>(pertex)));
    world.add(make_shared<sphere>(Point3(0,2,0),2,make_shared<lambertian>(pertex)));
    camera cam;

    cam.aspect_ratio      = 16.0 / 9.0;
    cam.image_width       = 400;
    cam.samples_per_pixel = 100;
    cam.max_depth         = 50;

    cam.vfov     = 20;
    cam.lookfrom = Point3(13,2,3);
    cam.lookat   = Point3(0,0,0);
    cam.vup      = vec3(0,1,0);

    cam.defocus_angle = 0;

    cam.render(world);
}

看起来是比较粗糙的

Smoothing out the Result(平滑)

首先我们知道,我们的randfloat随机值是在一开始就固定好了。那么我们如何能够实现立体空间上的平滑,自然也就是属性的插值,得到一个立方体上每个点都平滑的效果。

这里使用了三线性插值。

最开始的时候,我们是对坐标直接取整。现在,我们设定我们的击中点在一个立方体内,对于这个立方体,其顶点的属性就是我们的randfloat中的顶点。

这里分别得到点在立方体内部的位置,和立方体的起始位置。

auto u = p.x() - std::floor(p.x());
auto v = p.y() - std::floor(p.y());
auto w = p.z() - std::floor(p.z());

auto i = int(std::floor(p.x()));
auto j = int(std::floor(p.y()));
auto k = int(std::floor(p.z()));

然后定义一个数组去记录立方体的每个属性

for(int di=0;di<2;di++){
    for(int dj=0;dj<2;dj++){
        for(int dk=0;dk<2;dk++){
            c[di][dj][dk] = randfloat[
                perm_x[(i+di) & 255] ^
                perm_y[(j+dj) & 255] ^
                perm_z[(k+dk) & 255]
                ];
        }
    }
}
return trilinear_interp(c,u,v,w);

最后进行三线性插值

static double trilinear_interp(double c[2][2][2],double u, double v, double w){
    double x0_y0_z0_1 = c[0][0][0] * (1-w) + c[0][0][1] * w;
    double x0_y1_z0_1 = c[0][1][0] * (1-w) + c[0][1][1] * w;
    double x1_y0_z0_1 = c[1][0][0] * (1-w) + c[1][0][1] * w;
    double x1_y1_z0_1 = c[1][1][0] * (1-w) + c[1][1][1] * w;

    double x0_1_y1 = x0_y1_z0_1 * (1-u) + x1_y1_z0_1 * u;
    double x0_1_y0 = x0_y0_z0_1 * (1-u) + x1_y0_z0_1 * u;

    double accum = x0_1_y0 * (1-v) + x0_1_y1 * v;
    return accum;
}

文章中是这样

double accum =0.0;
for(int dx=0;dx<2;dx++){
    for(int dy=0;dy<2;dy++){
        for(int dz=0;dz<2;dz++){
            accum += ((dx*u + (1-dx) * (1-u)) * 
                (dy*v + (1-dy) * (1-v)) *
                (dz*w + (1-dz) * (1-w)))
                *c[dx][dy][dz];
        }
    }
}
return accum;

这两种方式是等价的

文章的平滑操作

平滑处理可以带来改进的结果,但其中仍然存在明显的网格特征。其中一些是马赫带效应,这是线性插值颜色的一个已知感知伪影。一个常用的技巧是使用赫尔曼三次样条来平滑插值。

double noise(const Point3& p) const{
    auto u = p.x() - std::floor(p.x());
    auto v = p.y() - std::floor(p.y());
    auto w = p.z() - std::floor(p.z());
    u = u * u * (3-2*u);
    v = v * v * (3-2*v);
    w = w * w * (3-2*w);
    auto i = int(std::floor(p.x()));
    auto j = int(std::floor(p.y()));
    auto k = int(std::floor(p.z()));
    double c[2][2][2];
    for(int di=0;di<2;di++){
        for(int dj=0;dj<2;dj++){
            for(int dk=0;dk<2;dk++){
                c[di][dj][dk] = randfloat[
                    perm_x[(i+di) & 255] ^
                    perm_y[(j+dj) & 255] ^
                    perm_z[(k+dk) & 255]
                    ];
            }
        }
    }
    return trilinear_interp(c,u,v,w);
}

Tweaking The Frequency(增大采样频率)

我们可以增大采样频率,让效果更加明显。其实就是加快它到达下一个状态的频率。

class noise_texture : public texture{
public:
    noise_texture(double scale) : scale(scale){}
    color value(double u,double v,const Point3& p) const override{
        return color(1.0,1.0,1.0) * noise.noise(scale*p);
    }
private:
    perlin noise;
    double scale;
};

Using Random Vectors on the Lattice Points

上面的结果看起来仍然有有一点块状。

在原始的 Perlin 噪声中,如果每个格点上的值只是一个随机浮点数,那么在进行插值计算时,会导致噪声图案显得比较块状。这是因为插值函数会直接在这些随机浮点数之间进行线性插值,而这些随机浮点数在整数的 x、y、z 格点上定义。因此,插值函数无法生成平滑过渡的噪声图案。我们来详细解释这一过程。

假设我们在三维空间中有一个输入点 p,它的坐标是 (x, y, z)。为了计算 p 处的噪声值,我们会:

  1. 确定 p 所在的单位立方体,即找到 p 周围最近的 8 个格点。
  2. 对每个格点,查找其对应的随机浮点数值。
  3. 使用三线性插值对这 8 个随机浮点数值进行插值,得到 p 处的噪声值。

在这个过程中,插值是基于整数的 x、y、z 格点上进行的。因为这些格点上的值是随机的浮点数,所以当插值函数在这些随机数之间进行插值时,产生的噪声值会在这些随机数值之间跳跃,导致噪声图案的块状效果。具体来说:

  • p 的坐标接近某个整数格点时,插值函数会更多地依赖于该格点的随机浮点数值。
  • 由于每个整数格点上的随机数值可能有很大的差异,这会导致在整数格点处的噪声值变化剧烈。
  • 这种变化在整个空间中重复出现,导致噪声图案呈现出块状效果。

Ken Perlin 的改进方法是使用随机单位向量而不是随机浮点数。这种方法能有效避免块状效果,原因如下:

  1. 随机单位向量:在每个格点上放置一个随机单位向量,而不是一个随机浮点数。这些单位向量可以代表不同的方向。
  2. 点积计算:对于每个输入点 p,计算 p 相对于每个格点的相对位置向量,然后与该格点的随机单位向量进行点积。这会生成一个标量值。
  3. 平滑插值:使用这些点积的结果进行插值,生成最终的噪声值。

通过这种方法,插值是在相对位置向量与随机单位向量的点积之间进行的,而不是直接在随机浮点数之间进行。这样一来,噪声值的变化不再局限于整数格点上,而是可以在整个单位立方体内平滑过渡,从而生成更加自然的噪声图案

perlin(){
    for(int i=0;i<point_count;i++){
        randvec[i] = unit_vector(vec3::random(-1,1));
    }
    perlin_generate_perm(perm_x);
    perlin_generate_perm(perm_y);
    perlin_generate_perm(perm_z);
}
static double perlin_interp(const vec3 c[2][2][2],double v,double u,double w){
    double uu = u * u * (3 - 2 * u);
    double vv = v * v * (3 - 2 * v);
    double ww = w * w * (3 - 2 * w);
    double accum = 0.0;
    for(int i=0;i<2;i++){
        for(int j=0;j<2;j++){
            for(int k=0;k<2;k++){
                vec3 weight(u-i,v-j,w-k);//相对位置
                accum += (i*u + (1-i)*(1-u)) *
                    (j*v + (1-j)*(1-v)) *
                    (k*w + (1-k)*(1-w)) *
                    dot(c[i][j][k],weight);
            }
        }
    }
    return accum;
}
class noise_texture : public texture{
public:
    noise_texture(double scale) : scale(scale){}
    color value(double u,double v,const Point3& p) const override{
        return color(1.0,1.0,1.0) * 0.5 * (1.0+noise.noise(scale*p));
    }
private:
    perlin noise;
    double scale;
};

Introducing Turbulence

通常使用具有多个叠加频率的复合噪声。这通常被称为Turbulence,是对噪声重复调用的总和。 Turbulence(湍流噪声)是对基本噪声函数的多次调用,通过不同频率和振幅的叠加来生成更复杂和自然的噪声图案。通常使用多层噪声叠加,称为 "频率分层"(octaves)。这种方法可以生成类似云朵、烟雾和地形的自然现象。 (这里的weight是振幅的增加,而temp_p是频率的增加)

double turb(const Point3& p, int depth) const{
        double accum = 0.0;
        Point3 temp_p = p;
        double weight = 1.0;
        for(int i=0;i<depth;i++){
            accum+=weight*noise(temp_p);
            weight *= 0.5;
            temp_p *= 2;
        }
        return std::fabs(accum);
    }
class noise_texture : public texture{
public:
noise_texture(double scale) : scale(scale){}
color value(double u,double v,const Point3& p) const override{
    return color(1.0,1.0,1.0)  * noise.turb(p,7);
}
private:
perlin noise;
double scale;
};

Adjusting the Phase

然而,通常Turbulence是间接使用的。例如,程序化实体纹理的“Hello World”是一个简单的类似大理石的纹理。基本思想是将颜色与类似正弦函数的东西成比例,并使用Turbulence来调整相位(使其在sin(x)中沿x方向移动),这使得条纹波动。注释掉直接的噪声和Turbulence,并给出类似大理石的效果是:

class noise_texture : public texture{
public:
noise_texture(double scale) : scale(scale){}
color value(double u,double v,const Point3& p) const override{
    return color(.5, .5, .5) * (1 + std::sin(scale * p.z() + 10 * noise.turb(p, 7)));
}
private:
perlin noise;
double scale;
};

  • 30
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值