Ray tracing: next week: Perlin noise

Ray tracing: next week中的柏林噪声

前序知识-柏林噪声

前言

其实之前那篇文章已经把Perlin Noise讲的很详细了,但是在读RT: next week,我看到作者这么一句话,瞬间来了兴趣

I’ll build that hack up incrementally based on Andrew Kensler’s description.

Andrew Kensler,你可能不知道他是谁,但是如果你在简中互联网搜他的名字,排序第一的可能是个知乎的回答 你见过最美的程序是什么?-知乎
然后我去翻了他的博客,找到了这么一篇文章Building Up Perlin Noise,我本想把其用中文转述一遍的...但是原文写的真的太好了。
文章中给了另一种理解柏林噪声的方法-把Perlin noise看作是几个surflet的和。文章非常直观,能看到Gradient vector的方向和大小对perlin noise产生的影响....真的真的推荐去看看。

surflet这个词我没找到中文翻译QAQ..

perlin类

在书里作者循序渐进的把一个白噪声产生器变成了柏林噪声发生器。
我会跳过这步直接分析代码。
注意:代码相比原版有改动,我以一种自认为方便理解的方式重写了部分作者的代码。

比如说我把原来的perlin类类名改成了perlin3D

类的结构

class perlin3D {
public:
  perlin3D();
  ~perlin3D();
  //[-1,1]
  double noise(const vec3 &p)const;
  //[0,1]
  double noise01(const vec3&p)const;
private:
  vec3 *ranVec;
  // Hash seed in x,y,z
  int *perm_x;
  int *perm_y;
  int *perm_z;
  static constexpr int point_count = 256;
  static constexpr int mask = point_count - 1;  

  // ease curve function
  static double easeCurve(float t) {
    return t * t * t * (t * (t * 6 - 15) + 10);
  }
  static vec3 easeCurve(const vec3 &p) {
    return vec3(easeCurve(p.x()), easeCurve(p.y()), easeCurve(p.z()));
  }

  static double trilinear_interp(double c[2][2][2], const vec3 &);

  // Generate hash seed array
  static int *perlin_generate_perm();

  // Shuffle the array
  static void permute(int *p, int n);
};

noise函数产生在目标点的噪声,不用多说。
这里让人疑惑的可能是permute,perlin_generate_perm函数和那几个int数组(指针)和vec3数组。
perlin_generate_perm函数是用来生成一个包含了经过重排序的0-255数字的数组,其中permute就是负责重排序的。
利用perlin_generate_perm生成三个数组,perm_x,perm_y,perm_z。这三个数组将会在之后成为计算hash时的种子。
ranVec里存放256个归一化后的随机向量,这些向量后面会作为Gradient Vector使用。
trilinear_interp是个三线性插值(因为三维空间)。

函数实现

permute函数,负责打乱数组

inline void perlin3D::permute(int *p, int n) {
  for (int i = n - 1; i > 0; i--) {
    int target = rand_i(0, i);
    int tmp = p[i];
    p[i] = p[target];
    p[target] = tmp;
  }
}

在Andrew Kensler的文章里有个很神奇的打乱函数,可以看看

perlin_generate_perm函数,生成一个数组并且调用permute将其打乱

inline int *perlin3D::perlin_generate_perm() {
  auto p = new int[point_count];
  for (int i = 0; i < perlin3D::point_count; i++)
    p[i] = i;
  permute(p, point_count);
  return p;
}

构造函数和构析函数

inline perlin3D::perlin3D() {
  perm_x = perlin_generate_perm();
  perm_y = perlin_generate_perm();
  perm_z = perlin_generate_perm();
  ranVec = new vec3[point_count];
  for (int i = 0; i < point_count; ++i) {
    ranVec[i] = normalize(random_vec());
  }
}
inline perlin3D::~perlin3D() {
  delete[] ranVec;
  delete[] perm_x;
  delete[] perm_y;
  delete[] perm_z;
}

三次插值函数trilinear_interp,这东西当时让我目瞪口呆,我思考了了好半天都没想明白,直到看了这篇文章 线性插值与双/三线性插值

inline double perlin3D::trilinear_interp(double c[2][2][2], const vec3 &weight) {
  auto accum = 0.0;
  for (int i = 0; i < 2; i++)
    for (int j = 0; j < 2; j++)
      for (int k = 0; k < 2; k++)
        accum += (i * weight.x() + (1 - i) * (1 - weight.x())) *
                 (j * weight.y() + (1 - j) * (1 - weight.y())) *
                 (k * weight.z() + (1 - k) * (1 - weight.z())) * c[i][j][k];

  return accum;
}

这个函数等同与7个lerp套一起,就是类似我上篇文章的写法

最后的重点,噪声发生函数

inline double perlin3D::noise(const vec3 &p)const {
  vec3 pi = vec3(floorf(p.x()), floorf(p.y()), floorf(p.z()));
  vec3 pf = p - pi;
  vec3 weight = easeCurve(pf);
  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++) {
        vec3 corner(di, dj, dk);
        vec3 c2p = pf - corner;
        int hash_ = perm_x[((int)pi.x()+di) & 255] ^ perm_y[((int)pi.y()+dj) & 255] ^
                    perm_z[((int)pi.z()+dk) & 255];
        auto &grad = ranVec[hash_];
        c[di][dj][dk] = dot(c2p, grad);
      }
  return  trilinear_interp(c, weight);
}

这里需要讲解一下,变量hash_是利用了之前生成的三个随机数组来得到一个随机值,接着用这个随机值取到gradient vector。三维数组c里储存的时gradient vector与 从顶点指向目标点的向量的点乘结果,与之前文章的代码在本质上一摸一样。最后调用三次插值函数得到结果。
注意,这里的结果大小在[-1,1]里,可以再包装一层用来返回一个[0,1]之间的大小。

噪声材质

class noise_texture :public texture{
public:
  noise_texture() {}
  
  //use scale to control frequent
  noise_texture(double _scale):scale(_scale){};
  virtual vec3 value(double u, double v, const vec3 &point) const override;
protected:
    perlin3D noise;
    double scale = 1;
};

vec3 noise_texture::value(double u, double v, const vec3 &point)const{
    return vec3(noise.noise01(point*scale));
}

用scale参数可以控制噪声的频率,比如对一个小小的球,你想让它上面的噪声看着清楚点,就可以给个大的倍数....

利用Perlin noise产生其他材质

湍流

这东西Perlin本人称之为turbulence,也就是湍流,其效果很惊艳。生成公式如下
$\sum|\frac{2^inoise(V)}{2^i}|$
代码

//in class perlin3D
inline double perlin3D::turb_noise(vec3 p,int layer = 8)const{
  float amp = 1;
  double res = 0;
  for (int i = 0; i < layer; i++) {
    res += noise(p)*amp;
    amp *=0.5;
    p = p*2;//double frequency
  }
  return fabs(res);
}
--------------------
//in turb_texture
class turb_texture :public noise_texture {
public:
  turb_texture() {}
  turb_texture(double _scale) : noise_texture(_scale) {}
  virtual vec3 value(double u, double v, const vec3 &point) const override;
};
inline vec3 turb_texture::value(double u, double v, const vec3 &point)const{
    return vec3(noise.turb_noise(scale * point));
}

渲染效果

大理石材质

公式如下
$0.5*(1+sin(x+\sum|\frac{2^inoise(V)}{2^i}))|$

代码

class marble_texture : public noise_texture {
public:
  marble_texture() {}
  marble_texture(double _scale) : noise_texture(_scale) {}
  vec3 value(double u, double v, const vec3 &point) const {
    return 0.5 * (1 + sin(scale * point.z() + 10 * noise.turb_noise(point)));
  }
};

参考资料:

Ray tracing: next week - Peter Shirley
Building Up Perlin Noise - Andrew Kensler
Perlin noise -Wikipedia
【图形学】谈谈噪声 -冯乐乐大佬的博客
Improved Noise reference implementation -Ken Perlin
GPU Gems - charpter 5 Implementing Improved Perlin Noise

后记

在一开始我尝试用Andrew Kensler的方式重写这个PerlinNoise,但是失败了....并且得到了个很奇怪的图

是不是很好玩啊哈哈哈哈哈

END

留言