まずは挑戦してみよう
シェーダを自分でコーディングするなんて……
きっとお難しいんでしょ……
と、お思いの奥様方。そんなことはないんです。コツをつかめば意外と楽しめます。当連載では、シェーダというものに対して抱かれてしまいがちな、漠然とした 難しそう感 を払拭すべく、簡単なシェーダの記述とその基本について解説したいと思います。
前回:[連載]やってみれば超簡単! WebGL と GLSL で始める、はじめてのシェーダコーディング(1) - Qiita
想定する読者
当連載では、シェーダってなんか難しそう……とか、シェーダプログラミング始めてみたいけど……とか、なんとなく興味を持ってるけどシェーダを記述したことがない方を読者に想定しています。
たとえば Unity などのツール、あるいはマインクラフトのようなゲーム、またはモデリングソフトなどでもシェーダを自分で記述することができるような世の中です。きっとシェーダに触れた経験は無駄にはならないでしょう。
すぐに業務で活かすとか、そういう壮大な話はさておいてまずは気軽にシェーダに触れてみましょう。
難しい 3D の数学的知識はとりあえず要りません。もちろん難しいことをやろうとする場合は話が変わってきますが、当連載ではそのあたりの知識は求めません。ちょっとくらい、三角関数とかは出てきますがそんなに難しくないですから安心してください。
対象のシェーダ記述言語は GLSL
「シェーダ」という言葉には非常に広い意味が含まれるので、世の中にはシェーダといってもいろいろなものが存在しています。また、それを記述するための専用の言語にも、いくつか種類があったりして非常に初心者には敷居が高いのかなと思います。
当連載では GLSL というシェーダ専用言語を用います。
GLSL は 昨今話題の WebGL でも採用されているので、特別な開発環境の準備などをしなくても、ブラウザとテキストエディタさえあれば簡単に始められます。
また、当連載では著者自作の GLSL editor というオンラインでシェーダが記述できるエディタを使いますので、もはやブラウザとネット環境さえあればシェーダが書けます。気軽ですね。
それでは早速、前回に引き続き、シェーダプログラミングについて考えていきましょう。
フラグメントシェーダ
さて、前回の連載ではシェーダを用いると画面上に出力する色を自在に操ることができる、というところまで説明しました。
参考までに、前回のコードを載せておきましょう。
precision mediump float;
uniform float t; // time
uniform vec2 r; // resolution
void main(void){
gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
はい、これですね。
前回の最後に、gl_FragColor
は画面に最終的に出力される色を表していると説明しました。
ですから、上記のようなシェーダを実行すると、画面全体が RGBA で指定された色で塗られるのでしたね。
しかし、せっかくシェーダを使っているのに、画面にべた塗りで色を付けることしかできないのでは面白くありません。それに、画面を一色で塗りつぶすことは確かに簡単かもしれませんが、個別に、場所によって色を変えたりしたい場合にはどうしたらいいのか、これではわかりませんね。
前回から、シェーダというキーワードが何度も登場してきましたが、正確には、GLSL editor で記述しているのは一般に フラグメントシェーダ と呼ばれているシェーダです。シェーダにはいくつか種類がありますが、そのうちのフラグメントシェーダには、前回から言っているように画面に出力する色を決定するという役割があります。
そして、フラグメントシェーダに書かれているシェーダのコードは、原則として画面全体の 各ピクセル に対してすべて同じように実行されます。
ちょっとわかりにくいですね。
もう少し別の言い方をすると、スクリーン上のすべてのピクセルの色は 同じシェーダのコード によって決められるのです。
補足コラム :
正確には、フラグメントシェーダはポリゴンなどが描かれるすべてのピクセルで実行されます。画面上に描くべきオブジェクトが一切無い場合には、フラグメントシェーダが何かを画面上に描き出すことはありません。
GLSL editor では、パッと見では全然わかりませんがスクリーン全体を覆う一枚の大きなポリゴンがレンダリングされています。
これにより、記述されたフラグメントシェーダのコードが必ずスクリーン上のすべてのピクセル上で動くようになっているんですね。
グラデーション
さて、すべてのピクセルに対してシェーダのコードが実行されるということは理解できましたか。
しかし、GLSL editor のスクリーンサイズは一辺の長さが 512px あります。ということは、スクリーン全体は 512 x 512 = 262144px もあるわけです。このすべてのピクセルに対して計算が行われるというふうに考えると、いかにも大変な負荷が掛かってしまいそうに思ってしまいますね。
フラグメントシェーダは GPU の力を借りて高速に動作するので、よほど複雑なことをしなければ、それほど高負荷にはなりません。近年のハードウェアの進化には目を見張るばかりです。
さて、それでは話を戻しましょう。
フラグメントシェーダで、好きな場所に好きなように色を描き出すためには、「 今から処理しようとしているピクセルとはどれなのか 」がわからないと困ってしまいますね。GLSL には、これをシェーダ内で参照するための手順もちゃんと用意されています。
シェーダが今まさに処理しようとしているピクセルの座標は、gl_FragCoord
という組み込み変数を参照することで取得できるようになっています。
何度か登場している gl_FragColor
と綴りが似ているで気を付けてくださいね。
GLSL editor は先ほどから書いている通り一辺が 512px 四方の正方形です。となると、gl_FragCoord
にも最大で 512 より小さい範囲までの数値が格納されているはず……ということになります。
gl_FragCoord
は vec4
型のデータです。x 要素にはこれから処理しようとしているピクセルの横位置が入っています。同様に、y 要素には縦位置ですね。
これらを踏まえつつ、以下のようなシェーダコードを記述して実行すると、どのような映像がスクリーンに出るのか考えてみてください。
void main(void){
float a = gl_FragCoord.x / 512.0;
gl_FragColor = vec4(vec3(a), 1.0);
}
main 関数の中身だけ書きましたが、どうでしょう。コードだけを見てどのような結果になるのか想像できたでしょうか。
main 関数の中では、まず冒頭で float
型の変数に gl_FragCoord.x
を 512.0 で割った数値を代入しています。
焦らず、考えてみましょう。
gl_FragCoord
には、どんな情報が入っていたんでしたっけ。この組み込みの変数には これから処理しようとしているピクセルの位置 が入っていたんでしたよね。
ということは、gl_FragCoord
の中身の範囲は、0 ~ スクリーンの横幅(もしくは縦幅)の最大値、という範囲になっているはずです。
つまり、gl_FragCoord.x
を 512.0 で割ると、0 ~ 1 の範囲の数値が得られることになります。
GLSL では、X の値は右に行くほど増えていきます。左の端は 0 で、右の端は 512 になるわけです。
となると、先ほどのコードのが実行されると、変数 a
の中身は 0 ~ 1 の範囲で値が入り、その値は 左側ほど小さく、右側ほど大きい ということになるのがわかりますね。
ですから、先ほどのコードを実行すると次のような結果になります。
おお~ なんかグラデーションになってます。想定していた通りの結果になっていたのがわかりますね。
ちなみに、色を指定している部分で以下のようにコードが書かれていました。
gl_FragColor = vec4(vec3(a), 1.0);
これはちょっとわかりにくかったかもしれませんが、変数 a
の値を vec3
型に変換して使っていると思っていただけるとわかりやすいかと思います。
ちなみに、以下のように書いた場合と意味としては全く同じです。
gl_FragColor = vec4(vec3(a, a, a), 1.0);
それぞれの要素に同じ内容を再利用するときは、途中を省略して書いてもいいことになっているわけですね。
X と Y の扱い
さて、先ほどのコードで横方向にグラデーションを掛けることができました。
それなら、同じような考え方で、縦方向にグラデーションを掛けることもできるはずです。たとえば、次のようなコードを書けばいいですね。
void main(void){
float a = gl_FragCoord.y / 512.0;
gl_FragColor = vec4(vec3(a), 1.0);
}
さっきと違うのは gl_FragCoord
の X ではなく Y を使っている点だけです。
これを実行すると次のようになります。
どうでしょう。
想像していたのと同じ結果になったでしょうか。
勘のいい人ならわかったかもしれませんが、GLSL では Y 座標の扱いは 左下 が原点です。
下に行けば行くほど gl_FragCoord
の値が小さく、上に行けば行くほど大きくなるわけですね。
通常、CANVAS の 2D コンテキストなどでは、左上が原点になります。GLSL の中身ではこれが上下反転しているので、最初はちょっと紛らわしく感じるかもしれません。
しかし、将来的にはこの座標系の扱いのほうが都合がいいということがわかる日がくると思います。現時点では。とりあえず深くは考えずに、GLSL の原点位置は左下であると一応覚えておきましょう。
型の暗黙の変換はされない点に注意
さて、今回は最後に GLSL の型のキャストについて触れておきます。
今回登場したシェーダのコードですが、非常に簡素なものでありながら、初心者のうちに陥りやすい失敗として次のようなコードの記述をしてしまう場合が考えられます。
どこがおかしいのか、わかるでしょうか。
void main(void){
float a = gl_FragCoord.x / 512;
gl_FragColor = vec4(vec3(a), 1.0);
}
はい、これはエラーになります。シェーダのコンパイルが通らずに、GLSL editor であればエラーのアラートが出るはずです。
おかしい部分というのは main 関数の中の一行目、割り算をしているところですね。
普通に数学的に考えると何もおかしなところはないのですが、GLSL は数値の扱いが非常に厳密です。先ほどのコードの場合、浮動小数点数の型である vec2
型の gl_FragCoord
の値を、整数型の数値で割り算しようとしているので、異なる型で計算しようとしている点が引っかかってしまうのです。
GLSL では、スカラの値(数値のハードコーディング)をする時にも、整数なのか浮動小数点数なのかを厳密に区別して書く必要があります。
たとえば、次のような記述は間違いです。
float a = 1 + 0.5;
この場合、整数と浮動小数点数とで足し算をしようとしているのでエラーになってしまうのです。
次のように書けばエラーは起こりません。
float a = 1.0 + 0.5;
ちょっと紛らわしく感じるかもしれませんが、最初のうちは全部を浮動小数点数として記述するようにしておくのが一番安心です。
なぜなら、vec
系の変数型は、内部的には全て float
として扱われるので、特別な理由がない限りは整数を利用しないほうがうっかりミスによるエラーが起こりにくいからです。
もし、GLSL でコーディングを行っていて、数式的には全く間違っていないはずなのにエラーが起こるときには、こういった点にも気を付けてみるといいかもしれませんね。
さて、次回はもう少し具体的なシェーダのコーディングをしていきます。今回までは単色での実行結果ばかりでしたが、次回からはもう少しカラフルな見た目になるように、値を調整してみることにしましょう。
お楽しみに。
次回:[連載]やってみれば超簡単! WebGL と GLSL で始める、はじめてのシェーダコーディング(3) - Qiita