フラグメントシェーダーとGLSLの基礎

graphics

フラグメントシェーダーとGLSLの基礎

クロスプラットフォームな標準的グラフィック用APIであるOpenGLにおいて、シェーダーを記述する言語がGLSLである。

今回は、画面の効果(グレースケール等)に使いたいだけなので頂点シェーダーは無視してフラグメントシェーダーについてがメイン。

頂点シェーダーとフラグメントシェーダー

シェーダーにはいくつか種類が存在し、html5のcanvas領域に描写をする時に使われるレンダラーであるWebGLの場合、頂点シェーダーフラグメントシェーダーが扱える。

頂点シェーダー

3Dグラフィックにおける頂点の情報を操作する。基本3Dグラフィックを構成する基本的な要素は"点”、“線分”、“三角形"の3つであり、そられは頂点を持つ。これを描写処理の一過程として処理を加えたい時に使われるのが頂点シェーダーである。

頂点の座標を変換すると書いたが、一般的にどこまで頂点シェーダーが仕事をするのか。

3Dグラフィックが描写される時のことを考えてみると、

  1. 描写候補のオブジェクトはワールド(xyzで表現される3D空間)の原点を{x: 0, y: 0, z: 0}として、どの位置にあるのか
  2. カメラ(視界)はどの位置に合ってどの方向を向いているのか
  3. カメラはどの角度に収まる範囲のものを描写し、どの距離に収まる範囲のものを描写するのか

などを決めてあげないと最終的にどの位置に何を描写するのか(または描写しないのか)が分からない。

これら全て頂点シェーダー内で書かないといけないのか。

実際のところ頂点シェーダーがする仕事の範囲はそのプログラムの設計に一任されるが、基本的に全く加工されていないデータを全て頂点シェーダーに渡して計算するよりは、アプリケーション側で基本的な座標変換は計算しておいて、加工後の座標変換にさらに処理を加えるのに頂点シェーダーが使われるという方が一般的らしい。(詳しくは知らないけど)

頂点シェーダーで処理されたデータは次の過程でカリングラスタライズ等の処理が加えられた後、次のフラグメントシェーダーに渡されて更に加工される。

カリング キャラクターがカメラに対して正面を向いている時に描写が不要になる部分、つまり背中の部分を描写しないようにするのがカリング

ラスタライズ 最終的には画像はピクセル(正方形の集まり)で表現されるので、生データをピクセルで表現される空間に変換しないといけない。これをするのがラスタライズ。

フラグメントシェーダー

全ての画像はドットの集まりである。例えば100px * 100pxの画像であればその画像は10000個のドットによって表現されている。これら一つ一つのドットに対して描写を指示するのがフラグメントシェーダーである。

頂点シェーダーが必要な点のみを操作するのに対し、フラグメントシェーダーはイメージを構成する全ての要素に対して指示を行うので、一般的に頂点シェーダーよりもフラグメントシェーダーの方が処理が重くなりやすい。

練習環境

とりあえず練習用の環境がないと始まらない。幸い、簡単に記述できてその場で実行して結果を見ることが出来るWebアプリがある。

http://jp.wgld.org/js4kintro/editor/

フラグメントシェーダーHelloWorld

javaで言うところの、以下のような"最初は脳死で覚えようね"的な呪文がGLSLにもある。

class HelloWorld {
  public static void main(String[] args]) {
    System.out.println("Hello World"); 
  }
}

それが下記。

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

void main(void){
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

実際は参考サイトによって最低限書かれている内容が違うことがあるが、とりあえずこの時点で認識しておくべきことは以下の4つ。

  1. mainは必須。これが呼ばれることによってシェーダーが動き出す。
  2. 2,3行目にあるのは変数の宣言。C言語のように上から下に、宣言すべきものは最初に宣言する。
  3. コメント//time,resolutionとあるのから察するに、実行時に外から変数が与えられるようになっている。
  4. gl_FragColorとある通り、ビルトインの変数があるようだ。

上記のHelloWorld的な内容を練習環境にコピペしてCtr-sすると、描写範囲が一色で塗られる。

それがシェーダーが実行されて描写範囲を操作した結果だ。

基本的な構文

1.変数の宣言

変数の宣言は修飾子(必要に応じて) + 変数型 + 変数名によって行う。

以下の例だと、修飾子の無し、型がfloatxという変数をmain関数の外に、修飾子の無し、型がfloatyという変数をmain関数の中に宣言している。変数は一時的なものであればその場で宣言してしまって良い。

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

float x;

void main(void){
  x = 1.0;

  float y = 0.2;

  gl_FragColor = vec4(x, y, 0.0, 1.0);
}

変数に修飾子を付けることで単にその場で値を格納することで、glslの外、WebGlの場合だとシェーダーを実行するjavascriptからglsl内へ変数を渡すことなどが出来るようになる。

以下の3つが代表的な修飾子

上記の例だと、trにはuniform修飾子がついており、コメントにあるように時間と描写する領域の情報を受け取る"予定となっている”(実際にどの変数にどういった値が送られてくるかはシェーダーを動かしているプログラム側が決める)。

繰り返しになるが変数名は常に時間がtで描写領域の情報がrではなく、どの変数にどういった値を送るのかはアプリケーション側の実装によって決まる。(WebGLであればjavascript側が決める)

今回は画面の描写効果に使いたいというだけなので、使う修飾子はuniformがメインになる。

時間などの他にも、マウスポインタの位置を送ることもよくある。マウスポインタに追随するキラキラもシェーダーで作れるということ。

2.ビルトインの変数

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という変数に値vec4を代入しているのが分かる。vec4rgbaのような4つの要素からなるベクターだが(vec2, vec3も要素数が違うだけで同じ意味)、代入先のgl_FragColorは何か。

GLSLにはビルトインの変数が用意されており、代表的なものは以下の通り

prefixにgl_が付く特徴が一致している。

フラグメントシェーダーは領域を構成する一つ一つの要素(ピクセル)について処理を行っているが、gl_FragColorにはその時処理しているピクセルの色を代入できる。100px100pxであれば1000個のピクセルがあり、それぞれのピクセルについてmainが呼ばれ(つまり10000回繰り返し呼ばれているイメージ)、その時その時のピクセルの色への参照をgl_FragColorが持つと思えば良い。 (100px100pxで愚直に10000回mainが呼ばれているかどうかは詳しく知らない)

ここまでの説明でgl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);が全ての要素に対して{R: 1.0, G: 0.0, B: 0.0, A: 1.0}の色を指定していたということが分かる(だから領域が一色に染まる)。

3.暗黙の型変化がない厳密な型

例えばjsであれば1 + 1.0のような計算をしても普通に計算できるだろう。他の言語でも同様に計算できることが多い。

しかしGLSLには暗黙の型変換が無く、intであれば1, 2, 3のような書き方をしなくてはならないし、floatであれば1.0, 2.0, 3.0のようにfloatであることが分かるように書かなくてはならない。

また、違う型同士を計算することは出来ないので、1 + 1.0のような計算は出来ずにコンパイル時にエラーとなる。

// OK
float f = 1.0 + 2.0;
int i = 1 + 2;

// NG
float f = 1 + 1; // float変数にintは入らない

float f = 1 + 1.0; // 違う型同士の値の計算は出来ない(int + float)

扱われる値の多くがfloatなので、特に必要が無ければfloatで書いていくと思って良い。

4.プロパティへのアクセス

先程vec4RGBAのような4つの要素を持った値のベクターであることに触れたが、ではそれぞれの要素へのアクセスはどうするのか。

GLSLにはスウィズル演算子と変数に呼ばれる演算子が用意されており、これを使う。スウィズル演算子というと特別なものに聞こえるが見た目はjsや他の言語でもおなじみのドット演算子によるアクセスと同じ。

vec4 v = vec4(1.0, 0.9, 0.8, 0.7);

// xyzw
// 下記の場合 x = 1.0
float x = v.x;

// rgba (表現が違うだけで値はxyzwと同じ)
// 下記の場合 r = 1.0
float x = v.r;

スウィズル演算子にはxyzwrgbaが用意されており、vec4であればxが最初、2番目がy…となっていく。

表現が違うだけでrgbaも同じ。xyzwrgbaはよく使われるので標準的な機能としてアクセスできるように機能が提供されているだけ。

また、rgbaを表すvec4からrgb部分だけを抜き出したvec3を取りたい時に、v.rgbとすればvec3が抜き出せる。便利。

5.ビルトインの関数

四則演算に加えて、mod(%はない)などの標準的な関数も用意されている。必要な時に調べる。

デバッグについて

残念なことに、GLSLではコンソールデバッグは出来ない。計算中の値の中身を読むことは出来ない。

そのため、デバッグが必要になったら指定の場所に色を出力してその色からデバッグしたい変数の値を読まないといけない。

参考サイト

勉強に使えるサイト

https://thebookofshaders.com/ https://wgld.org/d/webgl/