Vimでパーリンノイズ
あけましておめでとうございます.これは Vim Advent Calendar 34日目の記事です.
今日はパーリンノイズの話をしようと思います.パーリンノイズはCGのテクスチャ生成等に利用される乱数*1です.普通の乱数だと,あまりにも各々の値がばらばらで,自然界にあるものを表すのには不適合な場合があります.そのときに使われるのがこのパーリンノイズです.
このパーリンノイズをvimで利用できるプラグインを作成しました.
GitHub - mmisono/vim-perlin: perlin noise function implemented in vimscript
なお,動作にはynkdirさんによるvim-funlibが必要です.
GitHub - ynkdir/vim-funlib
今回はこのプラグインについての解説をしようと思います.なお,パーリンノイズのアルゴリズムそのものについては,既に詳しい文献がたくさんあるので,そちらを参考にしてください.以下にいくつかサイトを挙げておきます.
・We are currently working on our website
パーリンノイズの作成者,Ken Perlin氏による解説のスライド.パーリンノイズ自体の説明は14-16あたりです.
・ Ken's Academy Award
Ken Perlin氏によるオリジナルのソースコード
・ http://webstaff.itn.liu.se/~stegu/TNM022-2005/perlinnoiselinks/perlin-noise-math-faq.html
パーリンノイズのアルゴリズムの解説.分かりやすいです.
・ http://www.programmersheaven.com/2/perlin
パーリンノイズの実装についての解説.オリジナルのものと違うように一瞬見えますが,同じことをしてます.
・ http://scratchapixel.com/lessons/3d-advanced-lessons/noise-part-1/
ノイズ生成についての一般的な話.丁寧で分かりやすいですが,パーリンノイズそのものの説明ではありません.
一応簡単に説明すると,パーリンノイズの基本にある考え方は,飛び飛びの点ごとに乱数で値を求めて,その値を適当な関数で補間すればそれなりに滑らかな変化があるものが得られるよね,というものです.ただし,パーリンノイズはただ乱数を補間するのではなく,各点の乱数がその点での勾配を示していると考え,それに基づいて補間をおこないます.これにより,ただ乱数を補間して得られるものより,より一層自然な変化をする値が得られます.
パーリンノイズはその原理上,何次元のものでも作成できます.一般に(x,y)の2次元のもの,(x,y,z)もしくは(x,y,t)の3次元のもの,そして(x,y,z,t)の4次元ものがよく使われるようですが,今回は2次元のものしか作成していません.
実際のソースコードを以下に示します(autoload/perlin.vim).
let s:save_cpo = &cpo set cpo&vim " ---------------------------------------------------------------------------- let s:Perlin = {'SIZE':256} function! s:Perlin.new(...) let obj = deepcopy(self) if a:0 >= 1 let obj.SIZE = a:1 endif call obj.init() call remove(obj,"new") call remove(obj,"init") lockvar obj.SIZE lockvar obj.p lockvar obj.gx lockvar obj.gy return obj endfunction " lenear inter poration function! s:Perlin.lerp(t,a,b) return a:a + a:t*(a:b - a:a) endfunction " smoothing function function! s:Perlin.s_curve(t) return a:t*a:t*(3.0-2.0*a:t) endfunction function! s:Perlin.init() let self.p = [] let self.gx = [] let self.gy = [] for i in range(self.SIZE) call add(self.p,i) endfor for i in range(self.SIZE) let j = random#randint(0,self.SIZE-1) let tmp = self.p[i] let self.p[i] = self.p[j] let self.p[j] = tmp endfor for i in range(self.SIZE) call add(self.gx,1 - 2*random#random()) " gx[i] = 1-2*random#random() call add(self.gy,1 - 2*random#random()) " gy[i] = 1-2*random#random() endfor endfunction function! s:Perlin.noise2(x,y) " the 4 grid points bounding (x,y) let qx0 = float2nr(floor(a:x)) let qx1 = qx0 + 1 let qy0 = float2nr(floor(a:y)) let qy1 = qy0 + 1 " index of gradient vectors let q00 = self.p[(qy0 + self.p[qx0 % self.SIZE]) % self.SIZE] let q01 = self.p[(qy0 + self.p[qx1 % self.SIZE]) % self.SIZE] let q10 = self.p[(qy1 + self.p[qx0 % self.SIZE]) % self.SIZE] let q11 = self.p[(qy1 + self.p[qx1 % self.SIZE]) % self.SIZE] " calculate vectors from the grid points to (x,y) let tx0 = a:x - floor(a:x) let tx1 = tx0 - 1 let ty0 = a:y - floor(a:y) let ty1 = ty0 - 1 " calculate influences from the grid points let v00 = self.gx[q00]*tx0 + self.gy[q00]*ty0 let v01 = self.gx[q01]*tx1 + self.gy[q01]*ty0 let v10 = self.gx[q10]*tx0 + self.gy[q10]*ty1 let v11 = self.gx[q11]*tx1 + self.gy[q11]*ty1 let wx = self.s_curve(tx0) let v0 = self.lerp(wx,v00,v01) let v1 = self.lerp(wx,v10,v11) let wy = self.s_curve(ty0) let v = self.lerp(wy,v0,v1) return v endfunction function! perlin#perlin(...) return call(s:Perlin.new,a:000,s:Perlin) endfunction " ---------------------------------------------------------------------------- let &cpo = s:save_cpo unlet s:save_cpo
ざーっと見てもらうと,2種類の関数宣言があることに気づくと思います.
(1) function! s:Perlin.new() (2) function! perlin#perlin()
まず(1)ですが,これは辞書関数と呼ばれるものです(:h dictionary-function).先頭についているs:はスコープを表し,この場合はスクリプトローカル,つまりこのスクリプト内のみで参照可能となります*2.一番先頭部分で s:Perlin という辞書を作成していますが,このように関数を定義することで,
call s:Perlin.new()
というように関数が呼べるようになります*3.次に(2)ですが,これは autoload の仕組みを利用したものです(:h autoload).autoloadは主にライブラリ関数のためにあります.runtimepathのautoload以下に含まれるスクリプトで, (autoloadからスクリプトの相対パス)#(関数名) という形で関数を宣言すると,この関数は call path#to#hoge() という感じで呼び出すことができます.autoloadを利用することで,関数の多重読み込みなどを防止することができますし.また,vimの起動時ではなく関数が一番初めに呼び出されるときにスクリプトが読み込まれるので,vimの起動が早くなるという利点もあります.
このスクリプトは autoload/perlin.vim という名前でしたよね.autoload/perlin.vim 内で perlin#perlin() という関数を定義してあるので,この関数は call perlin#perlin() として呼ぶことができます(この関数はグローバルに,つまりどこからでも呼びだせます).
さて,この perlin#perlin() の本体を見てみます.
function! perlin#perlin(...) return call(s:Perlin.new,a:000,s:Perlin) endfunction
... というのは可変長引数をを受け取ることを表していて,a:000は全引数を持つリストです.今回は内部で使用する配列の大きさを指定できるようにするため,こうしています*4.リストの要素を引数として他の関数を呼び出す場合にはcall()を使いますが,辞書関数の場合は第3引数に自分自身を指定する必要があります(後述のようにselfで参照される)*5.
結局のところ,s:Perlin.new() を呼びだしているだけなので,そちらをみてみます.
function! s:Perlin.new(...) let obj = deepcopy(self) if a:0 >= 1 let obj.SIZE = a:1 endif call obj.init() call remove(obj,"new") call remove(obj,"init") lockvar obj.SIZE lockvar obj.p lockvar obj.gx lockvar obj.gy return obj endfunction
辞書関数では自分自身をselfとして参照できます.この関数ではまず,自分自身のコピーを作り,内部で使用する配列の大きさを決めます.そして,init()を呼び出し初期化します.そして,いらなくなった関数(これから使うことがないであろう関数,initとnew)を削除します.最後に,内部のリストや変数を勝手に変更されては困るので,lockvarによって変更禁止にして返します.インスタンスの生成みたいなことしてる訳ですね.
ということで,このパーリンノイズのライブラリは以下のようにして利用できます.
let perlin = perlin#perlin() echo perlin.noise2(1.2,0.7)
さて,こうして作ったパーリンノイズ関数ですが,使わないと意味がないので(w , 使ってみようと思います.
100x50 の領域内に関数を用いて印字可能なASCII文字およそ70文字を適当に配置します.そして,それを0-255の白黒の範囲に対応づけてvimのシンタックス機能により色付けさせます.
まずは,パーリンノイズではなく,普通の乱数でやってみましょう.gvimでsample/white_noise.vimを開いて, :source % します.以下のような結果が得られます(なるべくフォントサイズを小さくすると良いです).
そして,次がパーリンノズによる出力結果です(sample/perlin_noise.vim).(0,0)-(5,5)までのノイズの生成に対応させています.
一目瞭然ですね!
このパーリンノイズを利用することで,他にもさまざまな模様が生成できます*6.以下はマーブル模様を生成してみた様子です(sample/marble.vim).
そして次が木目模様を生成してみた様子です(sample/wood.vim).
なかなかおもしろいですね.
ということで何に使えるのかさっぱり分かりませんが,よければ使ってみてください.
補足
googleで "perlin noise" として検索するとトップに出てくる http://freespace.virgin.net/hugo.elias/models/m_perlin.htm ですが,これは,パーリンノイズの説明ではありません(な、なんだってー!! ΩΩΩ ) .まぁ,僕はこの辺の話に全く詳しくないのではっきりとしたことは言えませんが,少なくとも,この記事はKen Perlin氏が作成したオリジナルのパーリンノイズの解説ではありません.ここで取られている方法は点ごとの乱数を補間することによって作成したノイズを,パラメータを変えていくつか生成し重ね合わせる,というものです.ただ,これによってオリジナルのパーリンノイズに近い出力が得られるようです.なんかこういう出力が得られるものをパーリンノイズって呼んでるような感じですね.また,パーリンノイズ自体もいくつか重ね合わせたりすることもあります(marble.vimでは重ね合わせてます).まぁこのあたりはいろいろとやってみていいのを選ぶ,みたいな感じなんでしょうか.