アルゴリズム
WebGL
GLSL
WebGLDay 19

WebGLでSimplex NoiseのGLSLを使ってグラフィックパターンを生成する

More than 1 year has passed since last update.

突然の宣伝ですが、「WebGL Bits」という実験サイトをやってまして、ここではWebGLで作ったちょっとした小ネタをいくつか載せています。

ちょっと前に「Simplex Noise」というGLSLで実装されたSimplex Noiseというアルゴリズムを使ってグラフィックパターンを生成するコンテンツを作りました。
今回はそのGLSL版Simplex Noiseの使い方やパターンの生成方法の考え方について説明したいと思います。

特にライブラリは使わず、直接WebGLのAPIを使用する前提の話になってしまいますが、基本的にGLSLの話しかしないのでもしかしたら何らかのライブラリで独自のシェーダーを使う場合にも応用が利くかもしれません(?)

Simplex Noiseコンテンツの概要

Simplex Noise - WebGL Bits

Simplex Noise」で作られるグラフィックパターンがどんな感じなのかは、上の画像やサイトを見てもらえればと思います。

このコンテンツでは7種類のパターンを載せてますが、基本的にすべて同じSimplex Noiseのアルゴリズムを使って描画していて、パラメータを弄るだけで様々な色や模様のアニメーションが作れるようになっています。
隠しコマンドのCTRL+SHIFT+SPACEでパラメータをスライダーで操作できるので、興味がありましたら是非試してみてください。

3Dプログラミングの作りとしてはとても単純です。
頂点シェーダーは画面いっぱいに平面を表示するだけで3D的な事は特にしてません。
そして、断片シェーダーの中で平面の塗り方をピクセルごとにSimplex Noiseの値を使って処理している感じです。

最近知ったんですが、こういうの「シェーダーアート」とかって呼ばれてるみたいですね。

Simplex Noiseアルゴリズムとは

Simplex NoiseはPerlin Noiseの改良版と言われるアルゴリズムで、Perlin Noise同様に雲や煙や炎などを表現するのに使われたり、物体のテクスチャに使うことでより自然な見栄えを表現するのに使われるそうです。

アルゴリズム的な観点で言うと、完全にランダムな値を毎回生成するのではなく、不規則かつ徐々に値が変化し、全体としては一定の周期で増えたり減ったりが繰り返されるような値を生成します。
また、渡した引数が同じであれば毎回必ず同じ値が生成されるのも特徴のひとつです。

Simplex NoiseのGLSLの入手方法

Simplex NoiseのGLSLは「webgl-noise」を使っています。

ここにはSimplex NoiseとPerlin Noiseを使ったものがあり、それぞれに引数が2~4個のバリエーションが用意されていますので、全部で6種類になります。

今回は引数3個のSimplex Noiseが利用できるnoise3D.glslを使用しました。

実際に使う場合は、noise3D.glslのソースをコピペして自分が書くGLSLの先頭の方に持ってきます。
GLSLでは関数は先に記述しておかないとコンパイルエラーになるので要注意です。

noise3Dのsnoise関数の使い方

noise3Dの使い方は簡単で、snoise関数にvec3型の引数をひとつ渡すだけです。

// 3つの値a, b, cを渡して、1つの値を取得する。
float value = snoise(vec3(a, b, c)) 

戻り値はおそらくですが-1から1までの値になります。
ですので、1を足して2で割って0から1までの範囲に変換したり、0以下を切り捨てて色が出る範囲を半分ぐらいに設定したりしても良いと思います。

渡す引数についてですが、3つの値が全て同じなら毎回必ず同じ値が返ってきます。
もし、3つのうち1つをちょっとだけ変更したら前回に近い値が返り、大幅に変更していたら前回から遠い値が返ってくるでしょう。(たまたま一周して同じような値が返ってくる場合もあります。)

複数を同時に変更すると変化が想像しにくくなりますが、1つよりも2つ、2つよりも3つの方が変化が大きくなると言えると思います。

snoise関数を使ってRGBの各色を生成

断片シェーダー内で色を出力する基本的な形として、RGB各色をそれぞれsnoise関数で求めるパターンを紹介します。

// 3×3の値をどうするか?
vec3 red = ...
vec3 green = ...
vec3 blue = ...

// RGBをそれぞれSimplex Noiseで生成
float r = snoise(vec3(red.x, red.y, red.z));
float g = snoise(vec3(green.x, green.y, green.z));
float b = snoise(vec3(blue.x, blue.y, blue.z));

// 生成したRGBを出力、アルファはとりあえず1
gl_FragColor = vec4(r, g, b, 1.0);

問題は上記の3×3の値をどう変化させていくかです。
特にsnoiseに渡す3つの値のルール決めが重要で、これがグラフィックパターンの色や模様を決める大事な要素になります。

snoise関数に渡す3つの値の役割

ここからは話を単純にするため、赤色1色に絞って説明します。

snoise関数に渡す3つの値の役割を次のようにします。

// red.x 画面横方向の色の変化量、時間経過に関わらず基本固定
// red.y 画面縦方向の色の変化量、時間経過に関わらず基本固定
// red.z 画面全体の色の変化量、時間経過と共に増える
vec3 red = ...

// UV座標が入っている
vec2 uv = ...

// 1つ目はUの変化によって変わる
// 2つ目はVの変化によって変わる
// 3つ目は時間の変化によって変わる
float r = snoise(vec3(red.x * uv.x, red.y * uv.y, red.z));

ここでUV座標はそれぞれ0から1の値が入っているものとします。
つまり、Uが左から右に向かって0から1、Vが下から上に向かって0から1となります。

1つ目と2つ目はred.xred.yにUV座標を掛けているため、画面の描画する場所によって渡す値が変化します。
その代わりに元の値red.xred.yは常に固定とするので、同じ描画位置では渡す値も常に同じになります。
描画の位置によって色が変わるので、濃淡によってグラフィックの模様を表現することができます。

3つ目はred.zをそのまま使用し、UV座標を掛けていないので画面の描画位置に影響を受けません。
全ての描画位置で共通の値になりますが、時間の経過に伴って値を増やすようにします。
これによって画面全体の色を変化させてアニメーションさせる事ができます。

位置に応じた色の変化

1つ目と2つ目の値についてもう少し詳しく説明します。

uv.xuv.yは描画位置によって一意に決まるため、後はred.xred.yをあらかじめどういう値にするかでグラフィックのパターンが決まります。

例えば描画位置が横に移動した場合、2つ目と3つ目の値に変化はなく1つ目の値がred.xに比例して増えます。
その結果、左右の隣の色との違いがred.xに比例して強くなります。

逆に縦に移動した場合、1つ目と3つ目の値に変化はなく2つ目の値がred.yに比例して増えます。
横の移動と同様に、上下の隣の色との違いがred.yに比例して強くなります。

実際に見てもらった方が早いと思うので、これから例を元に説明します。

横にしか色が変化しないパターン

tate.png

上記の画像は、red.yを0.0とした場合のパターンになります。
描画位置が縦に移動した時は、元々1つ目と3つ目の値に変化はなく、2つ目の値も常に0になるので結果的に色が変化しません。
そのため縦の線が並んでるように見えます。

真ん中の画像の状態でred.xの値を増やすと、右側の画像のように縦の線が細くなり線が増えたように見えます。
逆にred.xの値を減らしていくと、左側の画像のように縦の線が太くなり滑らかなグラデーションになります。

縦にしか色が変化しないパターン

yoko.png

想像に容易いと思いますが、red.xred.yの関係を逆にすれば、縦横が逆になります。

縦横に色が変化する色々なパターン

others.png

左の画像は横の変化が強く、縦の変化をほんの少し設定したものです。
真ん中は縦横の変化が強く、右は縦横の変化が弱い例になります。

こんな感じで、この縦横のバランスを調整する事で色んな模様のグラフィックパターンを作ることができます。
これがred.xred.yが持つ模様を表現する役割になります。

時間に応じた色の変化

続いて、3つ目の値の話をします。

red.zはUV座標の影響を受けないので、描画位置に関わらず常に固定でした。
そのため、**この値を変更すると画面の全ての色に影響を及ぼし、全体の模様が変化します。

この時のred.zの変化量が多いほど画面全体の模様が大きく変わるため、スピードが速くなったように見えます。
逆に変化量が少なければ模様が徐々にしか変わらないため、スピードがゆっくりに見えます。

※アニメーションのサンプルが用意できなかったので、代わりに「Simplex Noise」でCTRL+SHIFT+SPACEを押して、「SPEED」のスライダーを操作してもらえればと思います。スピード感が掴めると思います。

RGB各色に割り当ててみる

snoise関数に渡す3つの値の役割がはっきりしたので、後はこれを赤だけでなく緑と青にも適用していけば、基本形が完成となります。

layer.png

例えば、左の画像のように赤と緑の色を近い模様にすれば重なった部分が黄色く見えます。
逆に赤と緑の色を正反対の模様にするとそれぞれの色がほぼ原色のままになるでしょう。

さらに色々と組み合わせる

基本形が完成しましたが、残念ながらこれだけだとあまり面白い色を出す事ができません。

色のバリエーションを増やそうとすると、3色の重なる場所を増やさないといけないので、模様が3つとも似たような形になってしまいます。
逆に模様を複雑にしようとすると、色の重なりが減るため、3原色そのまんまの色ばかりになってしまいます。

カラーマトリックス変換

そこで、カラーマトリックス変換を使います。

カラーマトリックス変換のアルゴリズムについては説明を省略しますが、3×3の行列で表す事ができますので、GLSLのソースコードは以下のようになります。

// 3×3のカラーマトリックス
mat3 colorMatrix = ...

// 3×3の値を調整
vec3 red = ...
vec3 green = ...
vec3 blue = ...

// RGBをそれぞれSimplex Noiseで生成
float r = snoise(vec3(red.x, red.y, red.z));
float g = snoise(vec3(green.x, green.y, green.z));
float b = snoise(vec3(blue.x, blue.y, blue.z));

// 生成したRGBにカラーマトリックスを掛けて出力、アルファはとりあえず1
gl_FragColor = vec4(colorMatrix * vec3(r, g, b), 1.0);

カラーマトリックス変換を使ったサンプルも載せておきます。

matrix.png

画面左の画像を元にして、右2つの画像のように色のバリエーションを簡単に作る事ができます。

その他の色変換

他にも、色相/彩度/輝度などの調整によっても色のバリエーションを増やす事ができます。
これらの処理もマトリックス変換で実現できますので、カラーマトリックス同様に行列を掛け合わせていくだけで実現できます。

おまけ

こういった方法でグラフィックパターンを生成する場合、後はひたすら好みのパターンになるようにパラメータを調整するのですが、スライダーなどのGUIを使って効率的にパラメータを変更できるようにしておくと良いです。

最近自分は、GLSLの実験環境としてMax(Max/MSP/Jitter)を取り入れてみました。
MaxはスライダーやダイアルなどのGUIが豊富でMIDIコントローラーとの連携もすぐにできるので、物理的なツマミで弄りながら調整できるのがとても楽しいんですよね。

それと、Jitterを使うと画像の読み込みやテクスチャの生成、プリミティブの描画などをソースコードを書かずにパッチを組むだけでやってくれるのがとても楽です。
GLSLのコードを書く事とパラメータを調整するためのパッチを組むだけの作業に集中できるのでお気に入りです。


あ、明日は、「WebGL 総本山」や「WebGL 開発支援サイト wgld.org」の管理人、@h_doxasさんですね。
WebGLの調べ物する時はいつも大変お世話になってます。

ではでは。