Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
75
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

updated at

[連載]やってみれば超簡単! WebGL と GLSL で始める、はじめてのシェーダコーディング(6)

まずは挑戦してみよう

シェーダを自分でコーディングするなんて……

きっとお難しいんでしょ……

と、お思いの奥様方。そんなことはないんです。コツをつかめば意外と楽しめます。当連載では、シェーダというものに対して抱かれてしまいがちな、漠然とした 難しそう感 を払拭すべく、簡単なシェーダの記述とその基本について解説したいと思います。

前回:[連載]やってみれば超簡単! WebGL と GLSL で始める、はじめてのシェーダコーディング(5) - Qiita

想定する読者

当連載では、シェーダってなんか難しそう……とか、シェーダプログラミング始めてみたいけど……とか、なんとなく興味を持ってるけどシェーダを記述したことがない方を読者に想定しています。

たとえば Unity などのツール、あるいはマインクラフトのようなゲーム、またはモデリングソフトなどでもシェーダを自分で記述することができるような世の中です。きっとシェーダに触れた経験は無駄にはならないでしょう。

すぐに業務で活かすとか、そういう壮大な話はさておいてまずは気軽にシェーダに触れてみましょう。

難しい 3D の数学的知識はとりあえず要りません。もちろん難しいことをやろうとする場合は話が変わってきますが、当連載ではそのあたりの知識は求めません。ちょっとくらい、三角関数とかは出てきますがそんなに難しくないですから安心してください。

対象のシェーダ記述言語は GLSL

「シェーダ」という言葉には非常に広い意味が含まれるので、世の中にはシェーダといってもいろいろなものが存在しています。また、それを記述するための専用の言語にも、いくつか種類があったりして非常に初心者には敷居が高いのかなと思います。

当連載では GLSL というシェーダ専用言語を用います。

GLSL は 昨今話題の WebGL でも採用されているので、特別な開発環境の準備などをしなくても、ブラウザとテキストエディタさえあれば簡単に始められます。

また、当連載では著者自作の GLSL editor というオンラインでシェーダが記述できるエディタを使いますので、もはやブラウザとネット環境さえあればシェーダが書けます。気軽ですね。

それでは早速、前回に引き続き、シェーダプログラミングについて考えていきましょう。

複数のオーブが宙を舞う

前回の連載記事では、光のオーブを複雑に移動させる方法について扱いました。

サインやコサインといった三角関数を利用することで、かなり面白い動きを付けることができるのでしたね。

今回は、前回の内容を踏襲しつつ、まずは、より見た目にインパクトのある物量感を出してみましょう。前回まではオーブの個数が一個だけでしたが、これを複数個出せるようにしてみます。

今回はいきなりコードを掲載してしまいましょう。

前回もそうでしたが、まずはコードを見て、どんなふうにレンダリングされるのか想像してみるといいかもしれません。

precision mediump float;
uniform float t; // time
uniform vec2  r; // resolution

void main(void){
    vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
    vec3 destColor = vec3(0.0);
    for(float i = 0.0; i < 5.0; i++){
        float j = i + 1.0;
        vec2 q = p + vec2(cos(t * j), sin(t * j)) * 0.5;
        destColor += 0.05 / length(q);
    }
    gl_FragColor = vec4(destColor, 1.0);
}

さて、このコードを実行するとどんな見た目になるんでしょうかね。

気になるところですが、まずは今回のコードで初めて登場した概念である for 文による繰り返し処理について触れておきます。

GLSL の for ステートメント

javascript などと基本的には同じ文法で書くことができる for 文ですが、GLSL 特有の注意事項がいくつかあります。

まず、初期化の時点で次のように書くと、これがエラーになるんですがどうしてだか想像つくでしょうか。

float j = 5.0;
for(float i = 0.0; i < j; i++){
    // 何かの処理
}

普通に考えると何もおかしなところは無いように見えますよね……

でもこれはコンパイルが通りません。

どうしてかというと、ループの継続を判断する条件式の部分に、変数(上記の場合でいうと j)を使っているから NG なんですね。

GLSL における for 文の初期化では、継続判定の条件式にはコンパイル前に不定の値は利用できません。つまり、ここに関してはハードコーディングが必要ということです。

ですから次のようにすればエラーは起こりません。

for(float i = 0.0; i < 5.0; i++){
    // 何かの処理
}

これだったら大丈夫です。

あとは、次のような書き方もダメです。

for(float i = 0.0; 5.0 < i; i++){
    // 何かの処理
}

些細な違いなんですけどね……

とにかく書き方に厳密なルールがあるということは確かです。for 文による繰り返し処理を記述する際は、このあたり注意しましょう。

また、今回は掲載したコードの性質的に、繰り返し処理のカウンタとして一般的な int 型の整数ではなく float を使っていますが、ここで float でなくてはいけないということはありません。

むしろ、普通は int で繰り返し処理を記述したほうがいいでしょう。

ただ、たとえばそのカウンタの値によって係数が変化したりするような処理を書く場合、型のキャストが問題になることがあります。

以前の連載テキストでも書きましたが、整数型と浮動小数点型のデータを同時に計算に使おうとすると、GLSL ではエラーが発生してコンパイルが通りません。

for(int i = 0; i < 5; i++{
    float f = i + 1;
}

たとえば上記のような記述は、整数と浮動小数点数データとが混在しているためエラーになってしまいます。

カウンタの役割を持つ変数 i が計算に利用できないのは不便ですよね。そこで、こういった場合には意図的に型をキャストするための文法を利用します。

明示的にキャストを行う場合には次のように書きます。

float f = float(i + 1);

C 言語などの場合は、キャストするのに (float)i といった書き方をしますね。GLSL は文法が C 言語とよく似ている割に、このような型のキャストはできません。イメージとしては、上記のコードで示したように型キャスト用の関数があって、その引数に値を渡すような感じになるんですね。

複数の光のオーブ

さて、for 文について理解できたら、冒頭で掲載したコードの実行結果がどのようになるのか、その様子を見てみましょう。

precision mediump float;
uniform float t; // time
uniform vec2  r; // resolution

void main(void){
    vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
    vec3 destColor = vec3(0.0);
    for(float i = 0.0; i < 5.0; i++){
        float j = i + 1.0;
        vec2 q = p + vec2(cos(t * j), sin(t * j)) * 0.5;
        destColor += 0.05 / length(q);
    }
    gl_FragColor = vec4(destColor, 1.0);
}

js4kintro010.jpg

こんな感じになります。

コードをよく見るとわかると思いますが、for 文によって繰り返し処理することで、複数のオーブをレンダリングさせています。

それぞれのオーブは、いずれも円を描くような軌道で移動し続けますが、移動する量を繰り返し処理のカウンタの大きさに応じて変えているので、それぞれが異なる速さでスクリーン内を移動するようになっています。

実際に GLSL editor で実行してみればわかると思いますが、光の強さは単純に for 文内では加算するように処理しているため、オーブ同士が座標的に重なっている場合にはかなり光の表現が強くなります。

これを工夫して改造することができたら、メタボールのような表現をすることもできそうですよね。

オーブ以外の光の形状

さて、光のオーブ、そしてオーブを複数描く、といったことができるようになりました。

これだけでも、オーブの移動する軌道や速さを変えるだけでかなり遊べます。また現状は完全に色を白黒で表現していますが、RGB をうまく調整すると非常にキレイな見た目にできるのでいろいろ調整してみると楽しいです。

ですか、オーブしか描けないのはちょっと味気ないので、比較的概念の簡単なリング状の光の輪にもチャレンジしてみましょう。

光の輪を描く仕組みは、実はオーブの概念をほんの少し工夫して修正すればいいのです。

今度はこれまでとは逆に、コードはまだ掲載しませんが結果だけを見てみます。

どのようなコードを書いたら、こんな結果が得られるのでしょうね。

js4kintro011.jpg

よーーーく見たらコード見えちゃいそうですけどそこはノリで!

このように、同じような光の表現を用いつつ、輪を描くように光を分散させるにはどうしたらいいか、想像がつくでしょうか。

ポイントになるのは、オーブをレンダリングするときに、どのような計算をして光の強さを調整していたのかをよく考えることですね。

まずは、オーブの場合のコードを再度掲載してみましょう。

precision mediump float;
uniform float t; // time
uniform vec2  r; // resolution

void main(void){
    vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
    float f = 0.1 / length(p);
    gl_FragColor = vec4(vec3(f), 1.0);
}

はい、これがオーブを描いた場合の GLSL のコードです。

ポイントは float 型の変数 f に値を代入している部分でした。

length 関数は原点からどのくらい離れているのか、その距離を返してくれる関数でした。原点に近ければ近いほど小さい値を、遠ければ遠いほど大きい値を返すわけです。

その数値を使って 0.1 を除算すると、結果的に原点に近い場所ほど光が強くなるという仕組みでしたね。

輪のような形に光を広げたいとすると、原点から 一定の距離にある場所ほど小さい値になる ようにしてやり、その数値を使って除算すればいいということになります。

ちょっとわかりにくいですね。

でも焦らずに考えてみましょう。

たとえば、原点(0.0, 0.0)から 0.5 離れた距離にある場所にリングを出したいとします。

その場合、length の戻り値が 0.5 になる場所ほど、計算結果が 0.0 に近づくようにすればいいわけです。

もう一度言います。length の戻り値が、0.5 に近づけば近づくほど、計算結果が 0.0 に近くなるような計算の方法を考えればいいんです!

はい、それではそろそろ正解のコードを掲載してみます。

precision mediump float;
uniform float t; // time
uniform vec2  r; // resolution

void main(void){
    vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
    float f = 0.01 / abs(length(p) - 0.5);
    gl_FragColor = vec4(vec3(f), 1.0);
}

さて、どうでしょう。

先ほどと同様に length が使われているコードをよく見てみてください。

length の戻り値から 0.5 を減算して、さらにそれを abs で包んでいますね。先ほども書いたように、輪のような模様を出すためには length の戻り値が 0.5 に近い場所ほど計算結果が 0.0 に近づくようにすればいいわけですから、上記のように単純に 0.5 を減算してやればいいのですね。

abs で包んでいるのにもちゃんと理由があって、もしこれを abs なしでやった場合には次のような感じになっちゃいます。

js4kintro012.jpg

これ、画像でわかりますかね。

リングの内側だけ、ぼんやりとしたブラーが掛かっておらず、バッサリと黒一色になってしまっています。

よく考えてみればわかると思いますが、length はあくまでも原点からの距離を返してくるだけなので、abs を併用しない場合には当然、0.5 を引いたときに負の数値になってしまう領域が出てきます。

負の数値を計算に利用してしまったら、当然変数 f に入る値も負の数値になってしまうので、結果的に色が真っ黒になってしまうのですね。

輪の内側にも外側にも、ぼんやりと光のあふれたような表現をするためには、abs の存在が実は重要だったわけですね。

さあここからは応用だ!

さて、光の輪を出すためのコードがわかりました。

となると、ここまでの連載で扱ってきた for 文や、時間の経過によるアニメーションを駆使すると、かなりいろいろなことができるようになるはずです。

参考までに、ここまで紹介してきたテクニックを駆使して、もう少し見た目にインパクトのあるシェーダを書いてみました。

precision mediump float;
uniform float t; // time
uniform vec2  r; // resolution

void main(void){
    vec2 p = (gl_FragCoord.xy * 2.0 - r) / min(r.x, r.y);
    vec3 destColor = vec3(1.0, 0.3, 0.7);
    float f = 0.0;
    for(float i = 0.0; i < 10.0; i++){
        float s = sin(t + i * 0.628318) * 0.5;
        float c = cos(t + i * 0.628318) * 0.5;
        f += 0.0025 / abs(length(p + vec2(c, s)) - 0.5);
    }
    gl_FragColor = vec4(vec3(destColor * f), 1.0);
}

ちょっと複雑に見えますか?

でも、今まで解説してきたことの組み合わせだけでほとんどできています。

しいて言うなら、コードの中に出てきている謎の数字、0.628318 ですが、これは円周率を五分の一にした数値です。どうして円周率を五分の一にした数値を使って今回のコードを書いたのかは、次回以降、簡単に解説しようと思います。

実際に GLSL editor に貼りつけて実行してみると、面白いものが見れると思いますよ。

それでは第六回はここまで。

次回もお楽しみに!

次回:[連載]やってみれば超簡単! WebGL と GLSL で始める、はじめてのシェーダコーディング(7) - Qiita

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
75
Help us understand the problem. What are the problem?