WebGLの能力を引き出すプログラマブルシェーダー:Webグラフィックをハックする(最終回)(2/5 ページ)
プログラマブルシェーダーの基本的な書き方と、Three.jsの各機能に組み込む方法を解説します
シェーダー言語(GLSL)の書式
前ページではThree.jsからカスタムシェーダーを利用する方法を取り上げました。ここからはシェーダー言語(GLSL)の具体的な記述方法を解説していきます。
GLSLの文法は、C言語を単純化してベクトルや配列などの型を追加したものになっています。JavaScriptもC言語の影響を強く受けているので、共通する部分が多くあります。以降では、JavaScriptと異なる部分を中心にして、GLSLの主な機能・文法を説明します。
一般的に、GLSLのコードは以下のような構成になります。前のページのサンプルにあるコードと見比べてみてください。
//グローバル変数の定義 void main() { // シェーダーの処理 }
コードの最初の部分では、プログラム中で使用するグローバル変数の定義を記述します。前ページで出てきたuniform変数やattribute変数の定義もここで行います(詳細は後述)。JavaScriptと異なり、この部分にコードを書くことはできません。
プログラマブルシェーダーの実行は、main()関数が呼び出されることで始まります。main()関数内ではシェーダー外部から渡された各種パラメータを参照しながら計算を行い、その結果を出力用の変数に格納します。多くのシェーダーはmain()関数のみで完結する場合が多いのですが、処理が複雑な場合は他の関数を定義して呼び出すことも可能です。
変数の定義
変数は、以下の書式で定義します。GLSLの変数には型があるので、JavaScriptのvarキーワードの代わりに型名を書くと考えれば分かりやすいでしょう。
GLSLで利用できる主な型を以下にまとめます。ベクトルやマトリクスといった構造がネイティブサポートされているのが特徴的です。また、整数と浮動小数点値が厳密に区別されるのはJavaScriptと大きく異なる点です。型の違う値を変数に代入しようとするとエラーになるので注意してください。
型名 | 説明 |
---|---|
int | 整数 |
ivec2 | 整数の2次元ベクトル |
ivec3 | 整数の3次元ベクトル |
ivec4 | 整数の4次元ベクトル |
bool | 真偽値 |
bvec2 | 真偽値の2次元ベクトル |
bvec3 | 真偽値の3次元ベクトル |
bvec4 | 真偽値の4次元ベクトル |
float | 浮動小数点値 |
vec2 | 浮動小数点値の2次元ベクトル |
vec3 | 浮動小数点値の3次元ベクトル |
vec4 | 浮動小数点値の4次元ベクトル |
mat2 | 浮動小数点値の2x2行列 |
mat3 | 浮動小数点値の3x3行列 |
mat4 | 浮動小数点値の4x4行列 |
sampler2D | 2次元テクスチャ |
samplerCube | キューブマップテクスチャ |
Bool、 int、 floatの値はJavaScriptと同様に記述できます。ただし、暗黙的な型変換は行われないので、例えばfloatの変数に1を代入するときは、「1.0」と書かねばなりません。ベクトル型や行列型の値は、関数呼び出しのような書式で要素値を並べる「型コンストラクタ」を使って指定できます。
int i = 1; bool b = true; float f = 1.0; // 1ではエラーになる ivec4 iv4 = ivec4(1, 2, 3, 4); vec3 v3 = vec3(1.0, 0.5, 3.0); mat2 m2 = mat2(1.0, 0.0, 0.0, 1.0);
型コンストラクタは、型の変換にも使えます。特にベクトルや行列の型コンストラクタには他のベクトルを含めることも可能で、長さの違うベクトルや行列への変換が行えます。
int i = int(true); // -> 1 float f = float(4); // -> 4.0 vec2 v2 = vec2(f, 0.5); // -> vec2(4.0, 0.5); vec4 v4 = vec3(0.0, v2, 1.0); // -> vec4(0.0, 4.0, 0.5, 1.0); mat2 m2 = mat2(v2, v2); // -> mat2(4.0, 0.5, 4.0, 0.5);
上記のすべての型は配列も定義できます。定義方法はC言語と同じで、変数名の後に要素数を大括弧でくくって記述します。各要素へのアクセス方法もC言語(およびJavaScript)と同じです。
int intArray[2]; // 要素数2の整数配列を定義 intArray[0] = 0; // 最初の要素に値を代入 intArray[1] = 1; // 2番目の要素に値を代入 int a = intArray[0] + intArray[1];
ただし、プログラマブルシェーダー内で使用できる記憶領域は限定されているので、あまり長い配列は使えません。また、uniform変数以外の配列のインデックスとして変数を使うことには強い制約があり、基本的にforループのループカウンタのみと考えるのが無難です。
演算
GLSLでは、JavaScriptとほぼ同じ演算子による数値演算が可能です。原則的に型の異なる演算はエラーになりますが、行列とベクトルの乗算、およびベクトル・行列とfloatの四則演算は例外的に認められます。また、一部の演算子は特定の型にしか適用できません。
float f = 1.0 + 2.0; bool b = 1.0 < 2.0; vec2 v2 = vec2(1.0, 0.0) + vec2(0.0, 1.0); v2 = mat2(1.0, 0.0, 0.0, 2.0) * v2; vec3 v3 = vec3(v2, 0.5) * 2.0; vec4 v4 = vec4(1.0, 0.5, 0.3, 0.0) + v3; // エラー : 型の異なる値の演算はできない bool b2 = v3 > vec3(0.0, 0.0, 0.0); // エラー : 比較演算子はベクトル・行列には適用できない bool b3 = b && f; // エラー : 論理演算子はbool値にしか適用できない bool b4 = b && f > 0.0; // これはOK
ベクトル型には、各要素を取り出すためのドット演算子が適用できます。ドットに続けてx、 y、 z、 wを記述することで、対応する要素にアクセスできます。それらの文字を最大4文字まで並べて、要素を並べ替えた新しいベクトルを作ることも可能です。
vec3 v3 = vec3(1.0, 2.0, 3.0); float f = v3.x; // -> 1.0 vec2 v2 = v3.yx; // vec2(2.0, 1.0); vec4 v4 = v3.xyzz; // vec4(1.0, 2.0, 3.0, 3.0)
パラメータを受け取るための変数
前ページのサンプルのシェーダーで、変数定義の前に「uniform」「attribute」「varying」といった修飾子が付いているものがあります。これらはシェーダーの外部から渡されるパラメータ値を受け取るための変数です。
uniform修飾子が付いた変数(uniform変数)は、ShaderMaterialのuniformsパラメータで指定された値を受け取ります。uniform(一定)という名前の通り、uniform変数の値は(同じマテリアルならば)全頂点、全ピクセルで共通になります。
attribute修飾子が付いた変数(attribute変数)は、ShaderMaterialのattributesパラメータで指定された値を受け取ります。頂点シェーダー専用で、フラグメントシェーダーでは定義できません。前ページで説明した通り、attributesの値は頂点ごとに別々に指定されるので、attribute修飾子の値は頂点ごとに変化します。
uniform変数、attribute変数ともに、型名や変数名はShaderMaterialでの定義と一致していなければなりません。ShaderMaterialで定義した変数をシェーダーで定義しないことは問題ありませんが、逆にShaderMaterialで定義していない(もしくは型が異なる)変数をシェーダーで定義するとエラーになります。
varying変数は他の2つとは異なり、頂点シェーダーからフラグメントシェーダーへの値の受け渡しに使用されます。従って、頂点シェーダーでは読み書きできますが、フラグメントシェーダーでは読み込みのみです。頂点シェーダーとフラグメントシェーダーで同じ名前のvarying変数を定義するだけで値が引き渡されますが、頂点シェーダーでの値がそのまま設定されるわけではなく、三角形の各頂点での値がピクセル位置を基に補間されます。
制御構造
GLSLでは、ifやfor、whileなどの制御構造が利用できます。JavaScriptと異なり、これらの文で使用する条件式は、必ずbool値(bool型の変数、もしくは比較演算子・論理演算子の結果)でなければなりません。また、ループ構造はGPUによっては直接サポートされないので、あまり複雑なものは使えません。単純なインクリメントによる定数回ループにとどめるのが無難です。
以下はforループを使って複数のライトの計算を行うフラグメントシェーダーの例です。このように、uniformの配列を処理するのが、ループの一般的な使用方法です。
uniform vec4 lights[4]; varying vec3 normal; void main() { float l = 0.0; for(int i = 0 ; i < 4 ; i++) { l += max(dot(normal, lights[i].xyz) * lights[i].w, 0.0); } gl_FragColor = vec4(l, l, l, 1.0); }
関数を定義することも可能です。書式はJavaScriptの関数定義の先頭のfunctionキーワードを返り値の型に置き換え、各引数にも型を記述したものになります。以下は、上の例の光源計算を関数化したものです。
uniform vec4 lights[4]; varying vec3 normal; float lighting(vec3 normal, vec4 light) { return max(dot(normal, light.xyz) * light.w, 0.0); } void main() { float l = 0.0; for(int i = 0 ; i < 4 ; i++) { l += lighting(normal, lights[i]); } gl_FragColor = vec4(l, l, l, 1.0); }
関数から複数の値を返す必要があるときは、引数リストの型名の前に「out」や「inout」というキーワードを記述します。すると、参照渡しのような状態になり、呼び出し元の値を変更できます。
Copyright © ITmedia, Inc. All Rights Reserved.