シェーダのコンパイルとリンク
積み重ねてきたもの
当サイトのテキストも、ナンバリングで言えば 11 番目のテキストです。散々基本的な部分ばかりやってきたので、いまだにポリゴン一枚すら描画できていませんね。あはは……
まぁ、何事も基本が大事ということで、ここまで積み上げてきた知識を総動員して、WebGL でポリゴンをレンダリングするための最終ステップに入りましょう。やるべきことの予備知識は既に解説してきました。あとはその手順を知るだけでポリゴンは描画できます。まずは、レンダリングに至る手順を大まかに確認しておきましょう。
- HTML から canvas エレメントを取得
- canvas から WebGL コンテキストの取得
- シェーダのコンパイル
- モデルデータを用意
- 頂点バッファ( VBO )の生成と通知
- 座標変換行列の生成と通知
- 描画命令の発行
- canvas を更新してレンダリング
ここに挙げたなかで、現時点で概念を全く解説していないのは最後の二つ、描画命令に関する部分と canvas の更新だけです。あとは、基本的な概念については既に説明しています。一見すると非常に複雑で冗長な手順を踏んでいるように感じるかもしれませんが、これが WebGL におけるレンダリングの基本的な流れになります。
今回のテキストでは、上から順番に消化していき、シェーダのコンパイルまでを解説します。
※一部の手順はその順序を多少変えても問題ありませんが、最初は一応この順序でやってみることにします
HTML と canvas の処理
HTML と canvas に関する処理は、以前のテキスト(コンテキストの初期化)でも行ないましたね。基本的には、このときと変わらないのですが、一応各種ソースと共に解説します。
html ソース
<html>
<head>
<title>WebGL TEST</title>
<script src="script.js" type="text/javascript"></script>
<script src="minMatrix.js" type="text/javascript"></script>
<script id="vs" type="x-shader/x-vertex">
attribute vec3 position;
uniform mat4 mvpMatrix;
void main(void){
gl_Position = mvpMatrix * vec4(position, 1.0);
}
</script>
<script id="fs" type="x-shader/x-fragment">
void main(void){
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
</script>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>
HTML ソースは上記のようになっており、 head
タグ内部で二つの javascript ファイルを読み込んでいます。一つは WebGL の処理を記述した script.js、そしてもう一つが当サイトのオリジナル行列演算ライブラリである minMatrix.js です。
頂点シェーダの処理
続いて、頂点シェーダとフラグメントシェーダのソースが出てきます。まずは type
属性が x-shader/x-vertex
となっている頂点シェーダから。以下、頂点シェーダのソースだけを抜粋。
頂点シェーダのソース
attribute vec3 position;
uniform mat4 mvpMatrix;
void main(void){
gl_Position = mvpMatrix * vec4(position, 1.0);
}
ここでは一つの attribute
変数と、一つの uniform
変数が出てきていますね。 position
は変数の型が vec3
で宣言されていることからもわかるとおり、三つの要素を持つベクトルです。要はこれが頂点の位置情報であり、ベクトルの各要素には X ・ Y ・ Z のそれぞれの座標が入ってきます。
一方 uniform
宣言されている変数 mvpMatrix
は、変数の型が mat4
であることから 4 x 4 の正方行列だということがわかります。ここには、モデル・ビュー・プロジェクションの各変換行列を掛け合わせた座標変換行列が入ってきます。
今回の頂点シェーダでは、座標変換行列を用いて頂点の座標位置を変換するだけですので、双方を掛け合わせます。この際、 position
を正しく行列と掛け合わせるために、組み込みの vec4
関数で四つの要素を持つベクトルに変換して掛け合わせています。最終的な計算結果が gl_Position
に代入されて、頂点シェーダは完結します。
フラグメントシェーダの処理
さて次はフラグメントシェーダです。
今回の場合は、描画するモデル(単なる三角形ポリゴンですが)には特に色をつけるわけでもなく、ただ真っ白に塗り潰します。なので、フラグメントシェーダ内で行うことは、 gl_FragColor
に白の色情報を与えることだけです。以下、フラグメントシェーダのソースだけを抜粋。
フラグメントシェーダのソース
void main(void){
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
色に関する情報は、基本的に vec3
か、もしくは vec4
で表されることが多いです。これは単純に RGB や RGBA が、3 ~ 4 の要素数だからです。今回の場合 vec4
として全ての要素が 1.0 のベクトルを渡しているので、[ 赤・緑・青・不透明度 の各要素がそれぞれ最大 = 白 ]がモデルの色として適用されます。
ソースをコンパイルしてシェーダを生成
さて、続いては頂点シェーダとフラグメントシェーダのコンパイルについて見てみます。
コンパイルとは言っても、特別なコンパイラなどは必要ありません。コンパイル自体は WebGL 内でメソッドを呼び出すだけで実行できます。シェーダのソースからコンパイルされ、実際にシェーダが生成されるまでの一連の流れを一つの関数にしてみます。以下、その関数のソース。
※以下の関数では gl にあらかじめ WebGL コンテキストが取得できている状態を想定しています
シェーダを生成・コンパイルする関数
function create_shader(id){
// シェーダを格納する変数
var shader;
// HTMLからscriptタグへの参照を取得
var scriptElement = document.getElementById(id);
// scriptタグが存在しない場合は抜ける
if(!scriptElement){return;}
// scriptタグのtype属性をチェック
switch(scriptElement.type){
// 頂点シェーダの場合
case 'x-shader/x-vertex':
shader = gl.createShader(gl.VERTEX_SHADER);
break;
// フラグメントシェーダの場合
case 'x-shader/x-fragment':
shader = gl.createShader(gl.FRAGMENT_SHADER);
break;
default :
return;
}
// 生成されたシェーダにソースを割り当てる
gl.shaderSource(shader, scriptElement.text);
// シェーダをコンパイルする
gl.compileShader(shader);
// シェーダが正しくコンパイルされたかチェック
if(gl.getShaderParameter(shader, gl.COMPILE_STATUS)){
// 成功していたらシェーダを返して終了
return shader;
}else{
// 失敗していたらエラーログをアラートする
alert(gl.getShaderInfoLog(shader));
}
}
この関数を利用する際には、引数に script
タグの id 属性を文字列で渡します。関数内部で id
属性を元にエレメントを参照します。
シェーダを生成するには、WebGL のメソッドである createShader
を使います。このメソッドによってシェーダを生成する際に、それが頂点シェーダなのかフラグメントシェーダなのかによって与える引数が変わってきます。 gl.VERTEX_SHADER
を指定した場合には頂点シェーダが、 gl.FRAGMENT_SHADER
を指定した場合にはフラグメントシェーダが生成されます。
生成されたシェーダに、まずはソースを割り当てます。 shaderSource
メソッドを使って、第一引数に対象となるシェーダオブジェクト、第二引数にシェーダのソースを渡します。この時点ではシェーダにソースが割り当てられただけなので、まだコンパイルはされていません。実際にコンパイルを行なっているのが、 compileShader
メソッドですね。このメソッドの引数に対象となるシェーダを渡して実行することで、シェーダがコンパイルされます。
シェーダが正しくコンパイルされたのかどうかは、シェーダが持つパラメータを参照することで確認できます。パラメータを参照するには、 getShaderParameter
メソッドに、WebGL の組み込み定数である COMPILE_STATUS
を指定します。もしここで false
が返ってくるようなら、何かしらの原因でコンパイルに失敗しています。原因を特定するためには、 getShaderInfoLog
に対象となるシェーダを渡して実行することで、ログを確認できます。
この自作の関数では、頂点シェーダでもフラグメントシェーダでも、どちらでもコンパイルが可能です。実際問題、頂点シェーダとフラグメントシェーダで処理を分岐しなければならないのは createShader
メソッドを実行するときだけです。そこからは全て同様の手順で行うことができるわけですね。
プログラムオブジェクトの生成とリンク
さて、シェーダを生成できたら、次はプログラムオブジェクトを生成します。ここで唐突に登場したプログラムオブジェクトとは、そもそもいったいどんな働きをするオブジェクトなのでしょうか。
以前のテキスト(シェーダの記述と基礎)でも触れましたが、頂点シェーダからフラグメントシェーダへデータを渡すために、 varying
修飾子付きの変数を使うことができます。実は、このシェーダからシェーダへのデータの橋渡しを実現してくれるのが、他ならぬプログラムオブジェクトなのですね。プログラムオブジェクトは、頂点シェーダとフラグメントシェーダ、また WebGL プログラムと各シェーダとのデータのやり取りを管理してくれる重要なオブジェクトなのです。
さて、それでは早速、プログラムオブジェクトの生成と、シェーダをプログラムオブジェクトに渡してシェーダをリンクするところまでを関数化してみます。
プログラムオブジェクトの生成とシェーダのリンクを行なう関数
function create_program(vs, fs){
// プログラムオブジェクトの生成
var program = gl.createProgram();
// プログラムオブジェクトにシェーダを割り当てる
gl.attachShader(program, vs);
gl.attachShader(program, fs);
// シェーダをリンク
gl.linkProgram(program);
// シェーダのリンクが正しく行なわれたかチェック
if(gl.getProgramParameter(program, gl.LINK_STATUS)){
// 成功していたらプログラムオブジェクトを有効にする
gl.useProgram(program);
// プログラムオブジェクトを返して終了
return program;
}else{
// 失敗していたらエラーログをアラートする
alert(gl.getProgramInfoLog(program));
}
}
この関数は、引数に頂点シェーダとフラグメントシェーダを受け取ります。その後、まずプログラムオブジェクトを生成し、割り当てます。プログラムオブジェクトの生成には、WebGL のメソッドである createProgram
メソッドを使います。さらに、そこで生成されたプログラムオブジェクトにシェーダを割り当てるのですが、ここでは attachShader
メソッドが使われます。 attachShader
メソッドの第一引数にはプログラムオブジェクトを、第二引数にはシェーダを渡します。
シェーダの割り当てが済んだら、プログラムオブジェクトによって二つのシェーダをリンクします。これには linkProgram
メソッドを使います。引数には対象となるプログラムオブジェクトを渡します。
シェーダを生成したときと同じように、正しくシェーダのリンクが行なわれたのかどうかをチェックします。その際は getProgramParameter
メソッドにプログラムオブジェクトと、組み込み定数 LINK_STATUS
を与えて実行します。問題がなければ、プログラムオブジェクトを有効にするために useProgram
メソッドを実行します。このメソッドを実行しておかないとプログラムオブジェクトが WebGL に正しく認識されませんので注意しましょう。
リンクに失敗していた場合にはログをアラートするようにしておきます。 getProgramInfoLog
を実行することでログが得られます。
まとめ
ここまでの内容を簡単にまとめましょう。
HTML ソースには、必要になる javascript ファイルへの参照と、シェーダのソースを記述しておきます。
シェーダをコンパイルする関数と、そのシェーダをリンクし管理してくれるプログラムオブジェクトに関する関数を用意しましたね。いずれも、エラー処理として正しく希望通りの処理が行なわれたのかどうかチェックする機構を持たせています。
次回は、頂点データ、つまりモデルデータを用意して、それを VBO に変換する処理をやってみます。少しずつやることが増えていきますが一つ一つ順に理解していけば問題ないはずです。がんばりましょう。