Perlin ノイズ


~年輪の重ね方~






■はじめに

今回はPerlin ノイズを使った年輪のテクスチャを作成します。 Perlin ノイズはそれを題材にしたコンテストが行われたり、 Cg のコーディングコンテストやATIのデモでリアルタイム生成が試みられていて、 ひそかに(?)盛り上がっています。
有用なランダム関数は使い出があるので、勉強しておきましょう。

まぁ、いつものように適当にファイルが入っています。

draw.cppメインの描画部分。
draw.h描画の各関数の定義。
bg.cpp背景の描画。
load.cppメッシュのロード。
load.hロードのインターフェイス。
main.h基本的な定数など。
main.cpp描画に関係しないシステム的な部分。
sky.bmp (空)
tile.bmp (地面)

あと、モデルと実行ファイル及び、プロジェクトファイルが入っています。

■Perlin ノイズとは

Perlin ノイズとは、ニューヨーク大学の Ken Perlin 氏が開発したノイズです。
Ken Perlin 氏は、「T2」等で使用された 「Noise & Turblence アルゴリズム」の開発で1997年にアカデミー技術賞を受賞しているつわものです。
SIGGRAPH でも毎年のように発表されており、この分野では非常に有名な方です。

Perlin ノイズは、複数のノイズを合成して1つのノイズを作ります。

合成するノイズは、サンプリング周波数が2のべき乗で増加します。
また、このとき乱数のゆれ(振幅)を小さくしていきます。 この「小さくしていく度合い」を Persistance といいます。 Persistance は、ほとんどの場合1以下の小数で、Persistance が小さいほどなだらかなノイズを作ることができます。
なだらかさは、合成するノイズの個数(オクターブ数)でも調整することができます。 なぜ、オクターブというかは、ノイズのサンプリング周波数が2のべき乗であがるから名づけられました。 周波数が2倍高い音は1オクターブ上の音です。 その音楽用語との連想で、オクターブという言葉が使われています。
高いオクターブのノイズは、細かな部分を表現できるので、より細かいノイズを作ることができます。
図に出ている「Persistance 小」ないし、「オクターブ大」の画像が今回のタイトル絵で使っているノイズです。
作成したノイズを10倍して、さらに小数部を採用してギザギザの模様を作ります。

なお、サンプリングする中間では、値の補間をします。

今回はCatmull-Rom スプラインを。補間に用いますが、線形補間や三角関数も補間に用いられます (あとで気づいたのですが、三角関数はもっとなだらかにつながるように調整されます)。
計算のしやすさや結果の違いの好みで選べばいいでしょう。

■ソースファイル

それではプログラムを見ていきましょう。
今回は、特別なシェーダは全くありません。

テクスチャーの生成は初期化時に行います。
D3DXFillTexture を使って、算出的に生成します。

draw.c
0275:     if( FAILED(hr = lpD3DDev->CreateTexture(512, 512, 1
0276:                                  , D3DUSAGE_RENDERTARGET, D3DFMT_X8R8G8B8
0277:                                  , D3DPOOL_DEFAULT, &pTexture))) return hr;
0278:     if( FAILED(hr = D3DXFillTexture(pTexture, MakTexture, NULL))) return hr;

実際のテクスチャの生成は次のようになります。

draw.c
0227: // ----------------------------------------------------------------------------
0228: // テクスチャーを作成
0229: // ----------------------------------------------------------------------------
0230: VOID MakTexture(D3DXVECTOR4* pOut, D3DXVECTOR2 *pTexCoord, D3DXVECTOR2 *pTexelSize, LPVOID pData)
0231: {
0232:     const float PERSISTANCE = 1.0f/4;
0233:     const int   OCTAVES     = 4;
0234: 
0235:     float shade = 0.0f;
0236:     
0237:     // 最大の振幅の大きさ
0238:     float amplitude = 1.0f/(((float)pow( PERSISTANCE, OCTAVES ) - 1.f)/(PERSISTANCE - 1.f));
0239:     float frequency = 4.0;
0240: 
0241:     for(int i=0; i<OCTAVES ; i++){
0242:         shade += amplitude * GetNoiseOctave(i, frequency, pTexCoord->x, pTexCoord->y);
0243: 
0244:         amplitude *= PERSISTANCE;   // 振幅は小さく
0245:         frequency *= 2;             // 周波数は高くなる
0246:     }
0247:     
0248:     // 年輪ぽく加工する
0249:     shade = 10*shade;           // 10年分の輪
0250:     shade = shade - (int)shade; // 小数部を取り出す
0251: 
0252:     // 色をつける
0253:     pOut->x = 0.80f*shade;  // 茶色っぽい色にする
0254:     pOut->y = 0.36f*shade;
0255:     pOut->z = 0.08f*shade;
0256:     pOut->w = 1.0f;
0257: }

振幅の減少度を示す PERSISTANCE と、重ね合わせ回数 OCTAVES を適当に決めておきます。
オクターブの回数だけノイズを足しこんでいきます。
オクターブがあがるごとに、振幅は小さく、周波数は高くしていきます。

あとは、年輪のようにするように、繰り替えしを付けます。具体的には、10倍して、その小数部をとります。 こうすれば、元のノイズが0から1に変化した段階で10個の年輪ができます。
最後に色をつけて出力すれば、1つのテクセルに関する色が求まります。

問題は、ノイズを与える GetNoiseOctave 関数です。この関数は、0から1の大きさで各オクターブに応じたノイズを生成します。

draw.c
0187: float GetNoiseOctave(int octave, float frequency, float x, float y)
0188: {
0189:     int  ifrequency = (int)frequency;
0190:     float t[2] = {frequency * x, frequency * y};
0191:     int  no[2] = {(int)t[0],     (int)t[1]};
0192: 
0193:     int cp[2][4] = {
0194:         { (RAND_ARRAY*((no[0]+0)%ifrequency)/ifrequency+octave)%RAND_ARRAY
0195:         , (RAND_ARRAY*((no[0]+1)%ifrequency)/ifrequency+octave)%RAND_ARRAY
0196:         , (RAND_ARRAY*((no[0]+2)%ifrequency)/ifrequency+octave)%RAND_ARRAY
0197:         , (RAND_ARRAY*((no[0]+3)%ifrequency)/ifrequency+octave)%RAND_ARRAY},
0198:         { (RAND_ARRAY*((no[1]+0)%ifrequency)/ifrequency+octave)%RAND_ARRAY
0199:         , (RAND_ARRAY*((no[1]+1)%ifrequency)/ifrequency+octave)%RAND_ARRAY
0200:         , (RAND_ARRAY*((no[1]+2)%ifrequency)/ifrequency+octave)%RAND_ARRAY
0201:         , (RAND_ARRAY*((no[1]+3)%ifrequency)/ifrequency+octave)%RAND_ARRAY},
0202:     };
0203: 
0204:     D3DXMATRIX mHeight(
0205:             rand_tbl[cp[0][0]][cp[1][0]],
0206:             rand_tbl[cp[0][1]][cp[1][0]],
0207:             rand_tbl[cp[0][2]][cp[1][0]],
0208:             rand_tbl[cp[0][3]][cp[1][0]],
0209: 
0210:             rand_tbl[cp[0][0]][cp[1][1]],
0211:             rand_tbl[cp[0][1]][cp[1][1]],
0212:             rand_tbl[cp[0][2]][cp[1][1]],
0213:             rand_tbl[cp[0][3]][cp[1][1]],
0214: 
0215:             rand_tbl[cp[0][0]][cp[1][2]],
0216:             rand_tbl[cp[0][1]][cp[1][2]],
0217:             rand_tbl[cp[0][2]][cp[1][2]],
0218:             rand_tbl[cp[0][3]][cp[1][2]],
0219: 
0220:             rand_tbl[cp[0][0]][cp[1][3]],
0221:             rand_tbl[cp[0][1]][cp[1][3]],
0222:             rand_tbl[cp[0][2]][cp[1][3]],
0223:             rand_tbl[cp[0][3]][cp[1][3]]);
0224:     
0225:     return CatmullRomPatch( mHeight, t[0]-(float)no[0], t[1]-(float)no[1]);
0226: }

オクターブごとの違いを表現しましょう。
オクターブが違えば、周波数が高くなります。
周波数は、サンプリングの個数、すなわち繰り返しの回数です。 (no[0], no[1]) は、現在の座標(x, y)が、frequency の繰り返しの中の何番目かを示します。

rand_tbl は、テクセル座標に依存した(同じ座標値を与えた時に、常に同じ乱数を返すような)乱数です。
配列として乱数を用意しました。
また、乱数の値は、使いやすいように、0~1の範囲にしました。

draw.c
0031: const int RAND_ARRAY = 64;
0032: float rand_tbl[RAND_ARRAY][RAND_ARRAY];

0272:     for(i=0;i<RAND_ARRAY;i++)
0273:         for(j=0;j<RAND_ARRAY;j++)
0274:             rand_tbl[i][j] = (float)rand()/(float)RAND_MAX;

配列の大きさは適当です。小さいと周期性が見えてしまうので、テクスチャサイズとオクターブ数で割った程度の大きさを用意します。

ノイズをなだらかにする関数としては、Catmull-Rom 曲線のパッチを使いました。
引き渡すパラメータの値は、繰り返しの中での0から1の間のパラメータになります。
テクセル座標に応じて、周期的になるように乱数のコントロールポイントを決めます。
但し、オクターブが違えば別の乱数を得るように調整しています。

draw.c
0152: // ----------------------------------------------------------------------------
0153: // CatmullRom スプライン
0154: //
0155: //          1            ┌ -1  3 -3  1┐┌ Pi-1 ┐
0156: // xi(t) = ― [t3 t2 t 1]│  2 -5  4 -1││ Pi   │
0157: //          2            │ -1  0  1  0││ Pi+1 │
0158: //                       └  0  2  0  0┘└ Pi+2 ┘
0159: // ----------------------------------------------------------------------------
0160: float CatmullRom(float x0, float x1, float x2, float x3, float t)
0161: {
0162:     const D3DXMATRIX m(
0163:         -1.0f/2.0f,  3.0f/2.0f, -3.0f/2.0f,  1.0f/2.0f, 
0164:          2.0f/2.0f, -5.0f/2.0f,  4.0f/2.0f, -1.0f/2.0f, 
0165:         -1.0f/2.0f,  0.0f/2.0f,  1.0f/2.0f,  0.0f/2.0f, 
0166:          0.0f/2.0f,  2.0f/2.0f,  0.0f/2.0f,  0.0f/2.0f
0167:     );
0168: 
0169:     float t2 = t*t;
0170:     D3DXVECTOR4 vt(t2*t, t2, t, 1.f);
0171: 
0172:     D3DXVec4Transform(&vt, &vt, &m);
0173: 
0174:     return x0 * vt.x + x1 * vt.y + x2 * vt.z + x3 * vt.w;
0175: }
0176: // ----------------------------------------------------------------------------
0177: float CatmullRomPatch(D3DXMATRIX &cp, float s, float t)
0178: {
0179:     return CatmullRom(
0180:             CatmullRom( cp._11, cp._12, cp._13, cp._14, s),
0181:             CatmullRom( cp._21, cp._22, cp._23, cp._24, s),
0182:             CatmullRom( cp._31, cp._32, cp._33, cp._34, s),
0183:             CatmullRom( cp._41, cp._42, cp._43, cp._44, s),
0184:             t);
0185: }

CatmullRomPatch 関数は、Catmull-Rom 曲線を2重に使用することによって、2次元的に補間をします。
パラメータ (s,t) が0から1の間で動くと、コントロールポイントの中央の4点の値が補完されます。

テクスチャを実際に張るのは円柱座標を使って張っていますが、枝葉の部分なので説明は省略します。

■最後に

今回は、シェーダとは全く関係なしで、普通にテクスチャを生成しましたが、 リアルタイムで実現していきたいですね。





もどる

imagire@gmail.com