フラグメントシェーダ ノイズ
今回のサンプルの実行結果
シェーダのみで描き出すノイズ
前回はジュリア集合と呼ばれるフラクタルの一種を GLSL を使ってレンダリングしました。マンデルブロ集合と並ぶ有名なフラクタルですが、GLSL を用いれば GPU の力で高速にレンダリングが行われ非常に美しい模様が次々と姿を変えながら描き出されます。
その様子を眺めているだけでも本当に面白いサンプルだったのではないでしょうか。
さて今回は、前回と比べて見た目に派手さはありませんが、3D プログラミングにおける非常に重要な要素のひとつであるノイズの生成を GLSL だけでやってみたいと思います。今回のポイントは、GPU を用いて高速に処理することでリアルタイムなノイズの生成に挑戦するというところでしょうか。
以前、当サイトのオリジナルライブラリで noiseX.js というものを作りました。これは動的に canvas 上にノイズを生成して描き出すものですが、レンダリングに canvas の 2D コンテキストを用いていること、加えて乱数生成が若干適当なこともありあまり速度も出ない感じで、ちょっと残念な仕上がりになっています。※自分で残念とか言っちゃいけませんね!
今回はこのノイズ生成を GLSL を使って行うことで、リアルタイムに、高速に、ノイズを生成します。当然、その速度は canvas2d を用いた場合とは比較になりません。がんばって習得していただければと思います。
ノイズの必要性
さて、解説に入る前に、ノイズがどうして役に立つのかを再度考えてみましょう。
GLSL だけでレンダリングを行う本章に限らず、3D プログラミングにはノイズは欠かせない存在です。ノイズというより、乱数が非常に重要な役割を果たす場面がとても多く存在します。代表的なところでは雲や岩などの自然界にある物体をリアルに表現するためであったり、レイトレーシングと呼ばれる手法によるレンダリングでは、レイ(視線などのベクトル)を乱数によって得られた無数の方向にバラバラに飛ばしたりなど、非常に利用頻度が高いのですね。
動的に、そしてランダムに、いかにもそれらしい模様をした雲を空に浮かべようと思ったら、まず必要になるのが乱数であり、その乱数から生成されたノイズが自然な情緒を演出するための基礎となります。
プロシージャルに、つまり動的にノイズを生成する方法を用いることができない場合、事前にペイントソフトなどを使って雲の模様を画像ファイルにしておき、それを読み込むといった下準備が必要になります。ですから、プロシージャルにノイズを生成することができればリソースの軽減などの恩恵が受けられるわけです。
これまた以前の話になりますが、パーティクルフォグを WebGL で実装したときにもプロシージャルに生成したノイズを利用して霧をレンダリングしました。GLSL だけでレンダリングを行う本章においても、動的にノイズを生成する仕組みを習得しておくことは後々必ず役に立つときがくると思います。
参考:パーティクルフォグ
また、上記で説明したように、分野にもよりますが乱数というのは登場する場面が多いこともあり、非常に質が重要になるものの一つでもあります。高性能な乱数ジェネレータは、高度な分野になればなるほど重宝されます。今回紹介する GLSL ノイズは、お世辞にも高性能とは言い難いものですが、最低限必要な性質は十分に満たしていると思います。ちょっとしたノイズを用いた処理であれば、十分に実用になるでしょう。
乱数に関しては非常に奥が深いので、もし潜ってみたい方は自分で資料を見つけて探究してみるのもいいかもしれません。
フラグメントシェーダの記述
さて、少々前置きが長くなりましたが、フラグメントシェーダのコードを見ながら考えていきましょう。
今回のサンプルを理解するためには、当サイトのテキストとしては今まであまり取り上げてこなかった GLSL に関するルールをひとつ知っておく必要があります。
GLSL も javascript などと同様に、シェーダ内で利用するための自前の関数を自由に記述することができます。
ただしユーザー定義の関数は、その関数の呼び出しよりも先(コードの記述位置としては上)に定義されていなくてはいけません。これは C 言語などではお馴染のルールですね。javascript の場合、最近のブラウザでは関数の記述箇所が問題になることはほぼないでしょう。しかし GLSL では関数のコールが行われる前の段階で、関数の記述が先に完結していなくてはいけません。これはまあ、慣れれば特に問題にはならないでしょう。
また当り前のことですが、ビルトイン関数の名前を始めとする予約語と同じ名称の関数は記述できません。
このあたりのルールに気をつけつつ、シェーダのコードを見てみてください。
ノイズ生成フラグメントシェーダ
precision mediump float;
uniform float time;
uniform vec2 mouse;
uniform vec2 resolution;
const int oct = 8;
const float per = 0.5;
const float PI = 3.1415926;
const float cCorners = 1.0 / 16.0;
const float cSides = 1.0 / 8.0;
const float cCenter = 1.0 / 4.0;
// 補間関数
float interpolate(float a, float b, float x){
float f = (1.0 - cos(x * PI)) * 0.5;
return a * (1.0 - f) + b * f;
}
// 乱数生成
float rnd(vec2 p){
return fract(sin(dot(p ,vec2(12.9898,78.233))) * 43758.5453);
}
// 補間乱数
float irnd(vec2 p){
vec2 i = floor(p);
vec2 f = fract(p);
vec4 v = vec4(rnd(vec2(i.x, i.y )),
rnd(vec2(i.x + 1.0, i.y )),
rnd(vec2(i.x, i.y + 1.0)),
rnd(vec2(i.x + 1.0, i.y + 1.0)));
return interpolate(interpolate(v.x, v.y, f.x), interpolate(v.z, v.w, f.x), f.y);
}
// ノイズ生成
float noise(vec2 p){
float t = 0.0;
for(int i = 0; i < oct; i++){
float freq = pow(2.0, float(i));
float amp = pow(per, float(oct - i));
t += irnd(vec2(p.x / freq, p.y / freq)) * amp;
}
return t;
}
// シームレスノイズ生成
float snoise(vec2 p, vec2 q, vec2 r){
return noise(vec2(p.x, p.y )) * q.x * q.y +
noise(vec2(p.x, p.y + r.y)) * q.x * (1.0 - q.y) +
noise(vec2(p.x + r.x, p.y )) * (1.0 - q.x) * q.y +
noise(vec2(p.x + r.x, p.y + r.y)) * (1.0 - q.x) * (1.0 - q.y);
}
void main(void){
// noise
vec2 t = gl_FragCoord.xy + vec2(time * 10.0);
float n = noise(t);
// seamless noise
// const float map = 256.0;
// vec2 t = mod(gl_FragCoord.xy + vec2(time * 10.0), map);
// float n = snoise(t, t / map, vec2(map));
gl_FragColor = vec4(vec3(n), 1.0);
}
だいぶコードの量が多いので、面喰ってしまうかもしれませんね。
しかし実際には、ユーザー定義の関数がいくつか含まれているので長文に見えるだけで、切り分けて考えればそれほど複雑な構造にはなっていません。
ノイズを生成するための基本的なロジックについては、以前のテキストや noiseX.js の解説ページである程度詳しく触れています。要は、それらと同じロジックを用いてシェーダ内ですべてが完結するように記述してあるわけです。もしノイズ生成のロジックそのものが気になるという場合には、別途テキストを参照してもらったほうがわかりやすいと思います。
細分化して見ていく上では main
関数のなかで行われていることを逆に遡っていくとわかりやすいのではないでしょうか。
main
関数の中では、まず noise
というユーザー定義関数が呼び出されています。この関数はノイズを生成するための関数ですが、その関数の中では補間された乱数を取得して処理しています。関数 irnd
を呼び出している部分がそうですね。
ユーザー定義関数 irnd
は、自身の中で乱数を生成する rnd
を呼び出して値を取得した後、同じくユーザー定義の関数である interpolate
を複数回呼び出して補間を行っています。
つまり処理の順番をまとめて並べると次のようになります。
rnd
で乱数が生成されるirnd
内でinterpolate
によって乱数が補間されるnoise
内で補間された乱数が合成される- 乱数がすべて合成されてノイズの出来上がり!
いくつものユーザー定義関数が繰り返し呼び出されているのでちょっと紛らわしい部分もあると思いますが、落ち着いて見ていけば大丈夫なはずです。
また、シェーダの main
関数の内部でコメントアウトされている部分、ここはシームレスなノイズを生成するためのコードです。
シームレスノイズの生成では、一辺の長さをどのくらいに設定してシームレスなタイル状にするか、引数で指定できるようになっています。
シームレスノイズを生成する場合
const float map = 256.0;
vec2 t = mod(gl_FragCoord.xy + vec2(time * 10.0), map);
float n = snoise(t, t / map, vec2(map));
シームレスノイズの生成には snoise
というユーザー定義関数を使います。
これも、ユーザー定義関数の中身を見てみればわかると思いますが、自身の中で noise
関数を複数回呼び出して値を決定しています。当然、通常の noise
と比較すると snoise
のほうが負荷の高い処理になります。
通常のノイズの場合も、シームレスノイズの場合も、今回のサンプルでは uniform 変数 time
の値を利用して座標が少しずつずれていくようにしています。サンプルを実行すると、ノイズが斜めの方向にスライドしてアニメーションしていくように見えるはずです。
もし、何かしらのノイズを用いた処理のために一度レンダリングするだけであれば、このような時間経過によるアニメーション処理は必要ありませんので外してしまって大丈夫です。
まとめ
さて、GLSL だけを用いたプロシージャルノイズのレンダリングについて見てきましたが、いかがでしたでしょうか。
GLSL 内でユーザー定義関数を用いて、複雑な処理を効率よく記述する今回のようなシェーダの書き方は、今後頻繁に登場するようになります。C 言語などに少しでも馴染みがあればわかりやすいと思うのですが、javascript しかわからないという人にはちょっと苦しい部分もあるかもしれません。
しかし、コードをよく見れば意味は理解できると思いますし、焦らずじっくり取り組めば必ず習得できると思います。
それと勘のいい方であれば気がついたかもしれませんが、今回は uniform 変数として入ってくる情報のうち、 time
以外の情報を使っていません。その唯一処理に組み込まれている time
でさえ、アニメーションする必要がないなら使わなくても処理できます。
GLSL カテゴリのテキストなので、前回までのテキストとの親和性の意味でそのまま uniform 変数を残してありますが、今回のように uniform を必ず使わないといけないということでは全くもってありませんので、勘違いしてしまわないように注意してください。
ノイズが生成される仕組み自体は過去のテキストを参照したほうが詳細に説明されています。もしそちらに興味のある方は過去のテキストを参照してみてください。
実際に動作するサンプルはいつものように以下のリンクから。