オーブ(光の玉)のレンダリング

実行結果

今回のサンプルの実行結果

美しい光の表現

前回はシェーダに送られてくる経過時間やマウスカーソルの情報を活用して、輪のような模様を GLSL のフラグメントシェーダだけでレンダリングしてみました。

マウスカーソルの座標を中心として、そこから各処理ピクセルへの距離を測ってやり、サイン波に変換して色として出力していたのでしたね。見た目はちょっと地味なものでしたが、GLSL だけで時間の経過やマウス操作に連動して動くグラフィックを生成する基本は理解できたのではないかと思います。

さて、今回は少し見た目にもこだわってみましょう。

前回よりは多少見栄えのするオーブ(光の玉)を GLSL でレンダリングしてみましょう。

やり方は一つではない

オーブのレンダリング方法を解説する前に、もう少しだけ前置きします。

どのようなプログラミングでも同じことが言えると思いますが、今回のサンプルと同じようなレンダリング結果を得られるシェーダソースには、いろいろな実装方法が考えられます。あくまでも、今回のテキストの内容は一例です。これから先も、様々なシェーダのソースを当サイトで公開することになるでしょうが、それらについてもまったく同じことが言えます。

自由にシェーダを記述できる GLSL だからこそ、それだけ創意工夫のしがいがあるというもの。

当サイトではそのヒントや、なにかのきっかけになるようなひらめきを見つけていただければと思います。

また、フラグメントシェーダはその性質上、できる限りコードを最適化しようとする場合が多いです。これは、考えてみれば当たり前ですよね。スクリーンを覆うすべてのピクセルに対して処理を行うシェーダのソースですから、その負荷の大きさはバカになりません。

ただでさえ重い処理となるフラグメントシェーダに、複雑なものをたくさん詰め込もうとするわけですから嫌でも最適化しなくてはならなくなる場面が多いのです。

ただし勘違いしないでいただきたいのは、コードを記述している最中には無理に最適化を意識しすぎないことが大切です。断片的なコードや、これから形になっていく過程のコードを無理に最適化したところで、単にコードが読みづらくなるだけです。GLSL には、コードを最適化したり短くしたりするための小技というか、テクニックがたくさんあります。それを駆使して書かれているシェーダのソースに、インターネット上で出会うこともあるでしょう。

それでも、まず慣れないうちはわかりやすくコードを書くようにしたほうがいいでしょう。

ロジックが理解でき、なおかつ、自分が無理せず最適化しながらコードを記述していけるようになるまでは、無理にはじめから最適なコードを書こうとする必要はありません。それに、いずれ自然とそれはできるようになります。単に慣れの問題だからです。

当サイトのシェーダのコードも、最適化の余地が残っていたり、わかりやすさを優先して若干非効率な書き方をしている場合があります。これは、理解を助けるためでもあり、より汎用的なコードとして掲載したいからでもあるのです。

オーブを描く

ちょっと前置きが長くなりました。

それでは、オーブ(光の玉)をレンダリングするためのシェーダのソースを考えてみましょう。

冒頭にあった画像のように、光る玉のようなレンダリング結果を得るためにはどうしたらいいでしょうか。

まずは、ものは試しに、前回のテキストでやったのと同じように、マウスカーソルからの距離を単純に length 関数で測り、それをそのまま色として出力してみましょう。

するとこうなります。

vec2 m = vec2(mouse.x * 2.0 - 1.0, -mouse.y * 2.0 + 1.0);
vec2 p = (gl_FragCoord.xy * 2.0 - resolution) / min(resolution.x, resolution.y);

// length
float t = length(m - p);

gl_FragColor = vec4(vec3(t), 1.0);

length

この画像では、マウスカーソルがスクリーンの中心にある場合を表しています。単純に、中心から遠いところほど値が大きくなり、白が強くなっていますね。

しかしこれでは全然オーブに見えないどころか、白い紙の上に薄墨を落としたようになってしまっています。

そこで、これをそのまま 1 から引いて、値を反転させてみましょう。

vec2 m = vec2(mouse.x * 2.0 - 1.0, -mouse.y * 2.0 + 1.0);
vec2 p = (gl_FragCoord.xy * 2.0 - resolution) / min(resolution.x, resolution.y);

// one minus length
float t = 1.0 - length(m - p);

gl_FragColor = vec4(vec3(t), 1.0);

one minus length

見事に明暗が反転しました。

こうなると、だいぶ暗闇の中で光が溢れているような感じにはなりましたね。

しかし、光の玉を描いているというよりは、なんとなく光源が遠くにあり、強い光がぼんやりと漏れ出しているような感じにしかなっていませんね。

オーブのように見せるためには、強く光る部分がもう少し広がればよさそうな気がします。

そこで length の戻り値を、1 から引くのではなく、もう少し大きな数字から引くように変えてみます。

vec2 m = vec2(mouse.x * 2.0 - 1.0, -mouse.y * 2.0 + 1.0);
vec2 p = (gl_FragCoord.xy * 2.0 - resolution) / min(resolution.x, resolution.y);

// one point one minus length
float t = 1.1 - length(m - p);

gl_FragColor = vec4(vec3(t), 1.0);

one point one minus length

だいぶそれっぽくなってきましたね。

しかし、この状態でも溢れている光の量が多すぎて、中心にあるオーブがあまりくっきり見えませんね。

こういうときは、累乗(2 乗、3 乗などの計算方法)を使いましょう。

累乗は、ビルトイン関数 pow を使うことで計算できます。スペキュラなどを計算するライティングでも登場したあれですね。

vec2 m = vec2(mouse.x * 2.0 - 1.0, -mouse.y * 2.0 + 1.0);
vec2 p = (gl_FragCoord.xy * 2.0 - resolution) / min(resolution.x, resolution.y);

// one point one minus length + pow
float t = 1.1 - length(m - p);
t = pow(t, 5.0);

gl_FragColor = vec4(vec3(t), 1.0);

one point one minus length + pow

ビルトイン関数 length の戻り値を 1.1 から減算、さらに 5 乗してあげたことでだいぶそれらしい仕上がりになりました。

ここまでくると、完全に光の玉のような状態になりましたね。

しかし、ここまで説明しておいてなんですが、他にも割り算を使って似たような結果を得る方法があったりします。

それが以下のコードです。

vec2 m = vec2(mouse.x * 2.0 - 1.0, -mouse.y * 2.0 + 1.0);
vec2 p = (gl_FragCoord.xy * 2.0 - resolution) / min(resolution.x, resolution.y);

// division
float t = 0.1 / length(m - p);

gl_FragColor = vec4(vec3(t), 1.0);

division

一番最初、冒頭で掲載した画像は、このシェーダのレンダリング結果だったんですね。

わざわざ累乗を使うことをしなくても、0.1 に対して割り算で計算すると、こんなふうにいかにも光の玉っぽいレンダリング結果が得られます。一見すると、ちょっと不思議ですね。

よくよく仕組みを考えてみれば、理屈は簡単です。

除算を行うということは、 length の結果が小さければ小さいほど、変数 t に入る数値は大きくなることになりますね。ここで 0.1 をもっと大きな値にすれば、オーブとして認識できる白い球体部分はもっと大きくなるでしょう。逆に、0.1 という数値をもっと小さいものにすれば、オーブも小さくレンダリングされるはずです。

ちょっと不思議に感じるかもしれませんが、冷静になって計算してみれば、なるほど然りという感じになるはずです。

まとめ

途中でも書きましたが、同じような結果を導き出す上で、実装方法は必ずしも一つではありません。気軽に、そして自由にシェーダを修正しながら、自分の望むレンダリング結果に近づけていく作業は非常に楽しいものです。

今回はオーブ(光の玉)というものを例にとって、いくつか順を追ってフラグメントシェーダのコードを掲載しました。自分でゼロからシェーダのソースを書く場合でも、まずは確実に動く簡単なコードから試していき、少しずつ修正を加えながら記述していくスタイルが個人的な経験から言ってもいいのではないかと思います。

ある程度ソースが煮詰まってきたら、最適化をほどほどに考えてみたり、あるいはもっと軽い実装で同じことができないか考えてみたり、試行錯誤を繰り返してみてください。

ときには、思いもよらなかった効果がスクリーン上に現われて驚くこともあるかもしれません。

今回も、実際に動作するサンプルを以下のリンクより参照できます。

entry

PR

press Z key