パーリンノイズ
クールな見た目のソリッドテクスチャはパーリンノイズ (Perlin noise) の一種を使って作られることが多い。考案者のケン・パーリン (Ken Perlin) の名前が付いたこのノイズを使ったテクスチャは、次のホワイトノイズのようにはならない:
パーリンノイズによって計算されるテクスチャは、次のぼかしたホワイトノイズに似ている:
パーリンノイズで重要なのが、反復できることである。パーリンノイズは三次元の点を入力として受け取り、乱数らしき値を返す。同じ入力に対する出力はいつでも同じであり、近い点に対しては近い値が出力される。さらにパーリンノイズに関してもう一つ重要なのが、単純で高速なことだ。そのためこのノイズはハックとして実装されることが多い。ここでは Andrew Kensler の説明に沿ってインクリメンタルに実装していく。
乱数のブロック
ノイズの生成方法として、適当な大きさの乱数を三次元配列に格納し、それを一つのブロックとして空間内に敷き詰めるという方法がまず考えられる。こうして得られるのは角ばったテクスチャであり、パターンも明らかである:
乱数のブロックを敷き詰める代わりに、ハッシュのような処理を使って添え字をかき混ぜてみよう。この処理には少しコードが必要になる:
class perlin {
public:
perlin() {
ranfloat = new double[point_count];
for (int i = 0; i < point_count; ++i) {
ranfloat[i] = random_double();
}
perm_x = perlin_generate_perm();
perm_y = perlin_generate_perm();
perm_z = perlin_generate_perm();
}
~perlin() {
delete[] ranfloat;
delete[] perm_x;
delete[] perm_y;
delete[] perm_z;
}
double noise(const point3& p) const {
auto i = static_cast<int>(4*p.x()) & 255;
auto j = static_cast<int>(4*p.y()) & 255;
auto k = static_cast<int>(4*p.z()) & 255;
return ranfloat[perm_x[i] ^ perm_y[j] ^ perm_z[k]];
}
private:
static const int point_count = 256;
double* ranfloat;
int* perm_x;
int* perm_y;
int* perm_z;
static int* perlin_generate_perm() {
auto p = new int[point_count];
for (int i = 0; i < perlin::point_count; i++)
p[i] = i;
permute(p, point_count);
return p;
}
static void permute(int* p, int n) {
for (int i = n-1; i > 0; i--) {
int target = random_int(0, i);
int tmp = p[i];
p[i] = p[target];
p[target] = tmp;
}
}
};
perline.h
] パーリンノイズを表すクラス
このクラスを使って \(0\) から \(1\) までの float
を生成し、それを使ってグレーの色を作成するテクスチャ noise_texture
を実装する:
#include "perlin.h"
class noise_texture : public texture {
public:
noise_texture() {}
virtual color value(double u, double v, const point3& p) const {
return color(1,1,1) * noise.noise(p);
}
public:
perlin noise;
};
texture.h
] ノイズテクスチャ
このテクスチャを球に割り当てよう:
hittable_list two_perlin_spheres() {
hittable_list objects;
auto pertext = make_shared<noise_texture>();
objects.add(make_shared<sphere>(
point3(0,-1000,0), 1000, make_shared<lambertian>(pertext))
);
objects.add(make_shared<sphere>(
point3(0, 2, 0), 2, make_shared<lambertian>(pertext))
);
return objects;
}
main.cc
] パーリンテクスチャを持つ球を二つ配置したシーン
カメラは前と同じとする:
const auto aspect_ratio = double(image_width) / image_height;
...
point3 lookfrom(13,2,3);
point3 lookat(0,0,0);
vec3 vup(0,1,0);
auto dist_to_focus = 10.0;
auto aperture = 0.0;
camera cam(
lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus, 0.0, 1.0
);
main.cc
] 視点のパラメータ
レンダリング結果からは、期待通りハッシュによってテクスチャがかき混ざったことが分かる:
ノイズのスムージング
線形補間を行えば値をスムーズにできる:
inline double trilinear_interp(
double c[2][2][2], double u, double v, double w
) {
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*u + (1-i)*(1-u))*
(j*v + (1-j)*(1-v))*
(k*w + (1-k)*(1-w))*c[i][j][k];
return accum;
}
class perlin {
public:
...
double noise(point3 vec3& p) const {
auto u = p.x() - floor(p.x());
auto v = p.y() - floor(p.y());
auto w = p.z() - floor(p.z());
int i = floor(p.x());
int j = floor(p.y());
int k = 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] = ranfloat[
perm_x[(i+di) & 255] ^
perm_y[(j+dj) & 255] ^
perm_z[(k+dk) & 255]
];
return trilinear_interp(c, u, v, w);
}
...
}
perlin.h
] 三次元線形補間を使ったパーリンノイズ
次の画像が得られる:
エルミート補間によるノイズの改善
線形補間により見た目は改善されたが、まだ元のグリッドが残って見える。色の線形補間で起こるマッハバンド (Mach band) と呼ばれる錯覚が原因の一つである。ここでよく使われるのが、三次エルミート補間を使って値の変化を滑らかにするというテクニックだ:
class perlin {
public:
...
double noise(const point3& p) const {
auto u = p.x() - floor(p.x());
auto v = p.y() - floor(p.y());
auto w = p.z() - floor(p.z());
u = u*u*(3-2*u);
v = v*v*(3-2*v);
w = w*w*(3-2*w);
int i = floor(p.x());
int j = floor(p.y());
int k = floor(p.z());
...
perlin.h
] パーリンノイズを滑らかにする
さらにスムーズな画像 (図 2.13) が得られる。
周波数の調整
このテクスチャは周波数が低すぎる (値の変化が遅すぎる) ように見える。入力点をスケールすれば、ノイズがもっと速く変化させることができる:
class noise_texture : public texture {
public:
noise_texture() {}
noise_texture(double sc) : scale(sc) {}
virtual color value(double u, double v, const point3& p) const {
return color(1,1,1) * noise.noise(scale * p);
}
public:
perlin noise;
double scale;
};
perlin.h
] 周波数を追加した noise_texture
scale
を 5
とすれば 図 2.14 を得る。
格子点にランダムなベクトルを配置する
これでもまだ角張って見える。模様の最大値と最小値が常に \(x\), \(y\), \(z\) が整数の部分にあることが理由として考えられる。Ken Perlin が考案した非常に賢いトリックは、格子点に (float
ではなく) ランダムな単位ベクトルを割り当てて、内積を使って最大値と最小値を格子点から動かすというものである。格子点に割り当てるベクトルはパターンが明らかでなければ完全にランダムでなくても構わないので、ここでは前もって計算したベクトルを用いる:
class perlin {
public:
perlin() {
ranvec = new vec3[point_count];
for (int i = 0; i < point_count; ++i) {
ranvec[i] = unit_vector(vec3::random(-1,1));
}
perm_x = perlin_generate_perm();
perm_y = perlin_generate_perm();
perm_z = perlin_generate_perm();
}
~perlin() {
delete[] ranvec;
delete[] perm_x;
delete[] perm_y;
delete[] perm_z;
}
...
private:
vec3* ranvec;
int* perm_x;
int* perm_y;
int* perm_z;
...
}
perlin.h
] ランダムな単位長の変位を追加した perlin
perlin::noise()
は次のようになる:
class perlin {
public:
...
double noise(const point3& p) const {
auto u = p.x() - floor(p.x());
auto v = p.y() - floor(p.y());
auto w = p.z() - floor(p.z());
int i = floor(p.x());
int j = floor(p.y());
int k = floor(p.z());
vec3 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] = ranvec[
perm_x[(i+di) & 255] ^
perm_y[(j+dj) & 255] ^
perm_z[(k+dk) & 255]
];
return perlin_interp(c, u, v, w);
}
...
}
perlin.h
] ランダムなベクトルを使う perlin::noise()
補間 perlin_interp()
はさらに複雑になる:
class perlin {
...
private:
...
inline double perlin_interp(vec3 c[2][2][2], double u, double v, double w) {
auto uu = u*u*(3-2*u);
auto vv = v*v*(3-2*v);
auto ww = w*w*(3-2*w);
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++) {
vec3 weight_v(u-i, v-j, w-k);
accum += (i*uu + (1-i)*(1-uu)) *
(j*vv + (1-j)*(1-vv)) *
(k*ww + (1-k)*(1-ww)) * dot(c[i][j][k], weight_v);
}
return accum;
}
...
}
perlin.h
] パーリンノイズの補間関数
この補間関数の出力は負になる可能性がある。負の値がガンマ補正関数の sqrt()
に渡されると NaN
が発生するので、perlin_texture
ではパーリンノイズからの出力を \(0\) から \(1\) に変換する:
class noise_texture : public texture {
public:
noise_texture() {}
noise_texture(double sc) : scale(sc) {}
virtual color value(double u, double v, const point3& p) const {
return color(1,1,1) * 0.5 * (1.0 + noise.noise(scale * p));
}
public:
perlin noise;
double scale;
};
perlin.h
] 変更したパーリンノイズを使う noise_texture
これでようやくノイズらしい見た目が得られる:
乱流ノイズ
異なる周波数のノイズを組み合わせて新しくノイズを作る方法も非常によく使われる。この方法は乱流 (turbulence) と呼ばれ、複数回呼び出したノイズの和として計算する:
class perlin {
...
public:
...
double turb(const point3& p, int depth=7) const {
auto accum = 0.0;
auto temp_p = p;
auto weight = 1.0;
for (int i = 0; i < depth; i++) {
accum += weight*noise(temp_p);
weight *= 0.5;
temp_p *= 2;
}
return fabs(accum);
}
...
perlin.h
] 乱流関数
ここで使われている fabs()
は絶対値を計算する関数であり、<cmath>
で定義される。
乱流をそのまま使うとカモフラージュネットのような見た目が得られる:
位相の調整
乱流は間接的に使われることが多い。例えばプロシージャルソリッドテクスチャにおける "hello world" である大理石模様のテクスチャは乱流を間接的に使うと作成できる。基本的な考え方は、色の濃さをサイン関数に比例させ、位相 (\(\sin x\) における \(x\)) に乱流で変化を付けて値をうねらせるというものだ。ノイズをそのまま使うコードをコメントアウトし、大理石模様のエフェクトを追加しよう:
class noise_texture : public texture {
public:
noise_texture() {}
noise_texture(double sc) : scale(sc) {}
virtual color value(double u, double v, const point3& p) const {
return color(1,1,1) * 0.5 * (1 + sin(scale*p.z() + 10*noise.turb(p)));
}
public:
perlin noise;
double scale;
};
perlin.h
] パーリンノイズと乱流を使ったノイズテクスチャ
次の画像が得られる: