要获得看起来很酷的实心纹理,大多数人使用某种形式的Perlin噪声。Perlin噪声返回类似下图的噪声。
Perlin噪声的一个关键部分是它是可重复的:它接受一个3D点作为输入,并总是返回相同的随机数字。附近的点返回相似的数字。Perlin噪声的另一个重要部分是它要简单快速,因此通常作为一种技巧来实现。
Using Blocks of Random Numbers
使用随机数块返回albedo。文章中对于该噪声的实现如下:
- 首先定义了一个2的整数倍的数组大小L(文章中为255)
- 然后定义一个随机数组随机获得L个【0~1)的随机数。
- 分别计算perm_x数组和对应的y,z数组。
- 混乱数组中的(0~i)的下标所指向的(0~i)的数
- 最后按照以下方式得到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
处的噪声值,我们会:
- 确定
p
所在的单位立方体,即找到p
周围最近的 8 个格点。 - 对每个格点,查找其对应的随机浮点数值。
- 使用三线性插值对这 8 个随机浮点数值进行插值,得到
p
处的噪声值。
在这个过程中,插值是基于整数的 x、y、z 格点上进行的。因为这些格点上的值是随机的浮点数,所以当插值函数在这些随机数之间进行插值时,产生的噪声值会在这些随机数值之间跳跃,导致噪声图案的块状效果。具体来说:
- 当
p
的坐标接近某个整数格点时,插值函数会更多地依赖于该格点的随机浮点数值。 - 由于每个整数格点上的随机数值可能有很大的差异,这会导致在整数格点处的噪声值变化剧烈。
- 这种变化在整个空间中重复出现,导致噪声图案呈现出块状效果。
Ken Perlin 的改进方法是使用随机单位向量而不是随机浮点数。这种方法能有效避免块状效果,原因如下:
- 随机单位向量:在每个格点上放置一个随机单位向量,而不是一个随机浮点数。这些单位向量可以代表不同的方向。
- 点积计算:对于每个输入点
p
,计算p
相对于每个格点的相对位置向量,然后与该格点的随机单位向量进行点积。这会生成一个标量值。 - 平滑插值:使用这些点积的结果进行插值,生成最终的噪声值。
通过这种方法,插值是在相对位置向量与随机单位向量的点积之间进行的,而不是直接在随机浮点数之间进行。这样一来,噪声值的变化不再局限于整数格点上,而是可以在整个单位立方体内平滑过渡,从而生成更加自然的噪声图案
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,是对噪声重复调用的总和。
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;
};