パーリンノイズってのはゲーム画面で「もくもく」してるアレだ。 雲とか霧とか。 実はちょっと加工すれば地形とかにも使えるスグレモノだ。
イカしたソースを紹介するぜ!
- ケン・パーリン先生によるリファレンス実装 https://mrl.cs.nyu.edu/~perlin/noise/
- 英語版Wikipediaの解説つき実装 https://en.wikipedia.org/wiki/Perlin_noise
問題はこの2つが、書き方の違いのせいで大きく違って見えることだ。重要な違いはそれほど無い。 解りやすくなるように、パーリン先生のロジックをWikipediaスタイルで書いてみる。
#include <math.h>
static int p[] = {
151,160,137,91,90,15,
131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180
};
double interpolate(double a0, double a1, double w) {
return (a1 - a0) * w + a0;
}
double fade(double t) {
return t * t * t * (t * (t * 6 - 15) + 10);
}
typedef struct {
double x, y, z;
} vector3;
vector3 randomGradient(int ix, int iy, int iz) {
switch (p[(p[(p[ix & 255] + iy) & 255] + iz) & 255] & 15) {
case 0: return (vector3) { .x = +1, .y = +1, .z = 0 };
case 1: return (vector3) { .x = -1, .y = +1, .z = 0 };
case 2: return (vector3) { .x = +1, .y = -1, .z = 0 };
case 3: return (vector3) { .x = -1, .y = -1, .z = 0 };
case 4: return (vector3) { .x = +1, .y = 0, .z = +1 };
case 5: return (vector3) { .x = -1, .y = 0, .z = +1 };
case 6: return (vector3) { .x = +1, .y = 0, .z = -1 };
case 7: return (vector3) { .x = -1, .y = 0, .z = -1 };
case 8: return (vector3) { .x = 0, .y = +1, .z = +1 };
case 9: return (vector3) { .x = 0, .y = -1, .z = +1 };
case 10: return (vector3) { .x = 0, .y = +1, .z = -1 };
case 11: return (vector3) { .x = 0, .y = -1, .z = -1 };
case 12: return (vector3) { .x = +1, .y = +1, .z = 0 };
case 13: return (vector3) { .x = 0, .y = -1, .z = +1 };
case 14: return (vector3) { .x = -1, .y = +1, .z = 0 };
default: return (vector3) { .x = 0, .y = -1, .z = -1 };
}
}
double dotGridGradient(int ix, int iy, int iz, double x, double y, double z) {
vector3 gradient = randomGradient(ix, iy, iz);
double dx = x - (double)ix;
double dy = y - (double)iy;
double dz = z - (double)iz;
return (dx*gradient.x + dy*gradient.y + dz*gradient.z);
}
double perlin(double x, double y, double z) {
int x0 = (int)x;
int x1 = x0 + 1;
int y0 = (int)y;
int y1 = y0 + 1;
int z0 = (int)z;
int z1 = z0 + 1;
double sx = fade(x - x0);
double sy = fade(y - y0);
double sz = fade(z - z0);
double n0, n1, ix0, ix1, iy0, iy1, value;
n0 = dotGridGradient(x0, y0, z0, x, y, z);
n1 = dotGridGradient(x1, y0, z0, x, y, z);
ix0 = interpolate(n0, n1, sx);
n0 = dotGridGradient(x0, y1, z0, x, y, z);
n1 = dotGridGradient(x1, y1, z0, x, y, z);
ix1 = interpolate(n0, n1, sx);
iy0 = interpolate(ix0, ix1, sy);
n0 = dotGridGradient(x0, y0, z1, x, y, z);
n1 = dotGridGradient(x1, y0, z1, x, y, z);
ix0 = interpolate(n0, n1, sx);
n0 = dotGridGradient(x0, y1, z1, x, y, z);
n1 = dotGridGradient(x1, y1, z1, x, y, z);
ix1 = interpolate(n0, n1, sx);
iy1 = interpolate(ix0, ix1, sy);
value = interpolate(iy0, iy1, sz);
return value;
}
要するにパーリンノイズは
- キリ番の座標毎に単位ベクトル(gx, gy,...)をランダムな向きで生成する。これは勾配ベクトルと呼ばれる。
- 各座標の最寄りのキリ番座標4つに対して、勾配ベクトルと相対座標(dx, dy,...)のドット積(dx*gx + dy*gy +...)を求める。
- 4つのドット積を補間して値を求める。
というのが基本で、アルゴリズムの違いは
- 先生のは値がdouble、Wikipediaのは値がfloat。
- 先生のは3次元、Wikipediaのは2次元。
- 疑似乱数の作り方が違う。
- 先生のはfade()とかいう関数を用意して補間に使ってるけど、Wikipediaのはリニアに補間。
パーリン先生のは擬似乱数下位4bitを12方向に変換してるから、ちょっと偏りがある。そんなんでいいのか。
Wikipediaのはプログラマブルシェーダにパーリンノイズを生成させることを意識してるのかもしれない。特に疑似乱数の作り方とか。