読者です 読者をやめる 読者になる 読者になる

ノイズの話

これはレイトレ合宿2!!アドベントカレンダーの4週目の記事です。
梅雨真っ盛りでぱっとしない天気が続きますがレイの追跡の具合はいかがでしょうか。
7月に入ったとはいえアドベントカレンダーはまだまだ序盤。 後のほうにガチな人がいらっしゃるのでレイトレと関係あるのか微妙なネタでもまだ許されると信じていきます。

さて、レンダリングしているとテクスチャが欲しくなって、とりあえずチェッカーをつくってみたりします。 それに飽きてきたらノイズです。ノイズですよねッ!?
もうノイズがあるだけでそれはそれはCGらしくなります。 しかも模様だけにとどまらず、バンプやディスプレイス、プロシージャルなオブジェクトと使い道もたくさんある素敵なやつです。 そんなわけでちょっとノイズでも作ってみようかと思います。

とりあえず2Dの画像をつくることにして、深いことを考えずに乱数で埋め尽くすとこんな感じになります。

float noise(float x, float y) {
    return random_generator_next_float(); // 適当な乱数生成器
}

f:id:c5h12:20140706080140j:plain

いわゆる ホワイトノイズ というやつです。
これは全ての周波数を含むという重要なノイズで、どれだけ拡大縮小してもほとんど見た目が変わりません。 もちろんここにあるのは画像になっているので、拡大するとドットが大きく見えるだけですが。
でもほしいノイズはこういうのじゃないよ!という声が聞こえてきます。

ところでノイズはCGでは基本的に物体の表面に貼付けるテクスチャとして使われます。 レンダリング中でも反射や屈折、マルチサンプリングなどで何回も参照されます。 ましてやデザイン的な部分でも毎回違う値が出てきては困ります。 ノイズはランダムなのがいいのですが、これと決めたら同じものが出てこないといけません。

そこで本当の乱数ではなく、座標を元にして適当な値を返すハッシュ関数を用意します。 とりあえずお手軽にハッシュテーブルで行くことにします。
今時はLL言語というやつで配列をシャッフルしてくれたりしますので例えばPython

import random
a = range(0, 256)
random.shuffle(a)
print(a)

"""
出力 : [36, 102, 45, 194, 188, 241, 32, 141, 115, 97, 117, 82, 143, 209, 1, 112, 158, 169,
 213, 77, 223, 253, 43, 133, 238, 76, 40, 90, 222, 177, 139, 95, 83, 219, 55, 191, 144, 26,
 203, 37, 232, 221, 0, 17, 100, 59, 138, 11, 204, 134, 38, 71, 207, 84, 114, 235, 210, 23,
 248, 251, 130, 81, 183, 201, 145, 93, 31, 151, 9, 6, 152, 94, 127, 99, 176, 61, 54, 212,
 51, 22, 142, 192, 33, 19, 208, 189, 74, 157, 88, 24, 60, 147, 64, 50, 202, 181, 53, 250,
 215, 186, 228, 150, 105, 30, 69, 140, 35, 200, 224, 107, 27, 57, 185, 225, 92, 155, 226,
 220, 78, 164, 87, 66, 172, 132, 116, 67, 126, 42, 246, 217, 146, 70, 108, 171, 2, 242,
 166, 96, 52, 62, 44, 121, 240, 167, 89, 214, 16, 124, 129, 197, 41, 216, 49, 8, 211, 72,
 120, 46, 170, 48, 122, 174, 153, 104, 68, 5, 125, 101, 230, 205, 187, 179, 58, 182, 21,
 65, 249, 137, 12, 243, 252, 165, 85, 245, 86, 254, 123, 7, 154, 47, 4, 28, 136, 34, 14,
 15, 161, 135, 79, 218, 29, 25, 131, 10, 56, 156, 234, 119, 63, 229, 233, 91, 103, 39, 190,
 118, 3, 198, 113, 75, 244, 163, 80, 178, 160, 173, 227, 106, 196, 149, 148, 175, 255, 236,
 18, 206, 168, 128, 231, 247, 111, 13, 110, 180, 73, 109, 162, 193, 199, 98, 184, 195, 237,
 20, 239, 159]
"""

なんて感じでお手軽に調達できます。 256で繰り返してしまうのがいやなら、もっとサイズを大きくするなり計算だけで値を出せる関数を使うなりしましょう。 余談ですがOpen Shading Languageの中の人は http://burtleburtle.net/bob/c/lookup3.c というハッシュが好きだとソースに書いています。 そしてこのテーブルを使って

int P[256 * 2] = {
    ..., // 先の値
    ...  // 同じものをもう一回
}

int hash(int x, int y) {
    x &= 0xff;
    y &= 0xff;
    return P[x + P[y]]; // Pで同じものを繰り返しておくとここが簡潔になる
}

float value(float x, float y) {
    return hash(x, y) / 255.0;
}

こんな感じでできあがりです。 これで毎回同じノイズが生成できるようになりました。 ついでにハッシュ関数の入力が整数になっているので、与えられた座標の値を整数に丸めてしまいましょう。

float noise(float x, float y) {
    int ix = floor(x); // 整数部を取り出す
    int iy = floor(y);
    return value(ix, iy);
}

f:id:c5h12:20140706080237j:plain

座標値として0から8の範囲を入力しています。 整数に丸めたことで、副産物的に大きさも好きにできるようにもなりました。

さて、これではただホワイトノイズの画像を拡大したようなものなので、隣の値とブレンドして滑らかにします。 これも何でも良いと言えばいいのですが、かのパーリン先生推奨の関数を使います。

// f(t) = 6.0*t^5 - 15*t^4 + 10*t^3
float interpolate(float t) {
    return t * t * t * (10.0 + t * (-15.0 + 6.0 * t));
}

float mix(float a, float b, float t) {
    return a * (1.0 - t) + b * t;
}

float noise(float x, float y) {
    ...
    
    // 注目する点を囲む格子の頂点の値
    float v00 = value(ix    , iy);
    float v10 = value(ix + 1, iy);
    float v01 = value(ix    , iy + 1);
    float v11 = value(ix + 1, iy + 1);
    
    float tx = x - ix; // 入力から整数部を引いて少数部を取り出す
    float tx = y - iy;
    
    tx = interpolate(tx); // 滑らかになるように曲線に変換
    ty = interpolate(ty);
    
    float v0010 = mix(v00, v10, tx);
    float v0111 = mix(v01, v11, tx);
    return mix(v0010, v0111, ty);
}

f:id:c5h12:20140706080259j:plain

いいですね!このノイズは バリューノイズ(Value Noise) と呼ばれます。
ご覧の通りとても単純なノイズで、3次元に拡張するのも簡単です。 GLSL SnadboxShaderToyで見かけるノイズはたいていこれです。
パーリン先生推奨とさらっと流した補間の式 f(t) = 6t5 - 15t4 + 10t3 ですが、これは f(t) = -2t3 + 3t2 という滑らかにつなぎたい時によく使われる曲線に似せて作られたものです。 なんでも後者の関数は2階導関数が0と1の境界部分で不連続になるため、これで作ったノイズをバンプマップに使うとグリッドの継ぎ目が目立ってしまうんだそうです。

f:id:c5h12:20140706080358j:plain

左が3次の関数で、右が5次の関数で全く同じノイズを補間したものです。 細かい理屈は置いておいて確かにグリッドの線が目立ちますね。

さてここまでは良くある話なので、折角なのでもう一歩進んでみましょう。
格子の点から単純に値を取り出すのではなく、グラデーションになるような関数を通してみます。 計算も簡単で方向も指定できるのでベクトルの内積なんか手軽でしょう。 格子の点に割り当てたベクトルを V、求めたい位置の点を P とすると

f:id:c5h12:20140708023844j:plain

割り当てたベクトルと格子点から求めたい点へと向かうベクトルとの内積はこんな感じです。 ちなみにベクトルVの後ろ側は真っ黒ですが実際には負の値です。

// 適当なベクトル
float G[4][2] = {
    { 1.0,  1.0},
    {-1.0,  1.0},
    {-1.0, -1.0},
    { 1.0, -1.0}
};

float gradient(int ix, int iy, float x, float y) {
    float *gv = G[ hash(ix, iy) % 4 ]; // ハッシュでベクトルを選ぶ
    float vx = x - ix; // 格子の頂点から求めたい点へのベクトル
    float vy = y - iy;
    return vx * gv[0] + vy * gv[1]; // 内積をとる
}

float noise(float x, float y) {
    ...
    // 注目する点を囲む格子の頂点からの値を求める
    float v00 = gradient(ix    , iy    , x, y);
    float v10 = gradient(ix + 1, iy    , x, y);
    float v01 = gradient(ix    , iy + 1, x, y);
    float v11 = gradient(ix + 1, iy + 1, x, y);
    ...
}

f:id:c5h12:20140706080456j:plain

見覚えのあるノイズができました。素敵です!
内積を取っているため負の値も出てきますので、画像は1を足して1/2しています。
このノイズがかの有名な パーリンノイズ(Perlin Noise) です。
またこのような関数値を補間するタイプのノイズを グラディエントノイズ と呼びます。 格子点に割り当てるベクトルですが、格子の中で取りうる値が同じになるように選ぶと、ノイズの値に偏りが少なくなって良いそうです。 3Dの場合は立方体の中心から各辺へ向かう12個のベクトルを選ぶのが良いとか。

ここまでできればあとはこれらを使って、大きさを2倍にして明るさを1/2倍にして合成していく方のパーリンノイズも作れます。 あれをやろうと思ってみたものの元になるノイズはどうすれば良いの?という服を買いにいく服が無いみたいなことにはもうなりません。
2倍と1/2倍にこだわらないのは fBm (Fractional Brownian Motion) とも呼ばれるようですが確証はありません。
なおもうお気付きかもしれませんが、パーリンノイズはベクトルの内積を使っているため、格子の頂点の上は必ず0になっています。

f:id:c5h12:20140706080529j:plain

赤い部分は負の値、緑の部分が正の値です。格子の交点が全て黒いところにあるのがわかると思います。
fBmを作る時には整数倍にしないとか回転や移動を入れるなどして、格子が重ならないようにするとより美しくなります。

ついでなのでさらっと他のノイズを紹介します。

シンプレックスノイズ (Simplex Noise)

f:id:c5h12:20140706080543j:plain

2次元なら三角形、3次元なら四面体というように、ある次元での最小の頂点を持つ立体のことをシンプレックス(単体)といいます。 N次元ならその頂点数はN+1個です。 さて、なぜいきなりこんなものがでてきたかというと、今までの格子を使う方法ではノイズの計算に使われる頂点の数は、2次元なら4個、3次元なら8個というようにN次元では2N個の頂点数になります。 なら単体を使えば計算量が少なくできるんじゃないか、というのがシンプレックスノイズです。 次元が高くなるほど計算量が少なくなります。

ウェーブレットノイズ (Wevelet Noise)

f:id:c5h12:20140706080556j:plain

最初にタイルと呼ばれるデータをつくり、必要になった時にそこから取り出してつくられるノイズです。 タイルはノイズで埋め尽くしたバッファを半分に縮小、再度拡大し、元から減算することで作られます。 縮小拡大、取り出す時の補間法などがミソなんだとか。
この方法で作ると『完璧に、特定の帯域の周波数しか含まないノイズ』を作ることができるのだそうです。 それが何かというと、画素より小さくなるディテールはチラツキや色のつぶれの原因になるので、fBmを作る段階でそのようなサイズになっているノイズだけを除外できる、ということです。 なおタイルが必要なので、テクスチャのメモリがいらないというノイズのメリットがそがれてしまっていることが難点です。

ガボールノイズ (Gabor Noise)

f:id:c5h12:20140706080610j:plain

この2つはパラメーターの違いだけでできるパターンです。 このように単体での表現力が高く、さらにウェーブレットノイズのような特性も持っているという期待の新星です。 スパースコンボリュージョンノイズと呼ばれるタイプで、ホワイトノイズにぼかしフィルタをかけるような原理で作られます。 そのときにガボールフィルターというフィルターを使うのがガボールノイズです。 設定できるパラメーターが多く直感的に使いづらいのと、ぼかしフィルタの原理なので重いことが難点です。

とりあえずこんなところでしょうか。
最後に、手軽にノイズで遊ぶならBlenderOpen Shading Language (OSL)を使うのもお勧めです。
OSLには補間の無いノイズのセルノイズ、スムースノイズ(パーリンノイズ)、シンプレックスノイズ、ガボールノイズとそろっていて、さらにスクリプトとして書けるので高い自由度があります。 しかもBlenderテキストエディタのOSLのテンプレートにノイズだけを集めたものが用意されていたりします。

それでは合宿まであと丁度2ヶ月間。
ハッピーレンダリング

*7/8 誤字修正、内積のグラディエントの図を追加。