ポリゴンのレンダリング
今回のサンプルの実行結果
レンダリングへの一連の流れ
今回はいよいよポリゴンをレンダリングするところまで実際にやってみます。以前のテキスト(シェーダのコンパイルとリンク)では HTML と 頂点シェーダ・フラグメントシェーダまでを記述しましたね。今回は、javascript の処理を最初から最後まで見ていきます。
前回、また前々回の内容をしっかり理解できていれば、ある程度すんなり理解できるはずです。少しわかりにくい場面も出てくると思いますが、焦らず取り組んでいただければと思います。
まずは、全てのコードを掲載します。そのあと、ポイントを絞って解説していきます。
script.js のコード全文
onload = function(){
// canvasエレメントを取得
var c = document.getElementById('canvas');
c.width = 300;
c.height = 300;
// webglコンテキストを取得
var gl = c.getContext('webgl') || c.getContext('experimental-webgl');
// canvasを初期化する色を設定する
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// canvasを初期化する際の深度を設定する
gl.clearDepth(1.0);
// canvasを初期化
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 頂点シェーダとフラグメントシェーダの生成
var v_shader = create_shader('vs');
var f_shader = create_shader('fs');
// プログラムオブジェクトの生成とリンク
var prg = create_program(v_shader, f_shader);
// attributeLocationの取得
var attLocation = gl.getAttribLocation(prg, 'position');
// attributeの要素数(この場合は xyz の3要素)
var attStride = 3;
// モデル(頂点)データ
var vertex_position = [
0.0, 1.0, 0.0,
1.0, 0.0, 0.0,
-1.0, 0.0, 0.0
];
// VBOの生成
var vbo = create_vbo(vertex_position);
// VBOをバインド
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
// attribute属性を有効にする
gl.enableVertexAttribArray(attLocation);
// attribute属性を登録
gl.vertexAttribPointer(attLocation, attStride, gl.FLOAT, false, 0, 0);
// minMatrix.js を用いた行列関連処理
// matIVオブジェクトを生成
var m = new matIV();
// 各種行列の生成と初期化
var mMatrix = m.identity(m.create());
var vMatrix = m.identity(m.create());
var pMatrix = m.identity(m.create());
var mvpMatrix = m.identity(m.create());
// ビュー座標変換行列
m.lookAt([0.0, 1.0, 3.0], [0, 0, 0], [0, 1, 0], vMatrix);
// プロジェクション座標変換行列
m.perspective(90, c.width / c.height, 0.1, 100, pMatrix);
// 各行列を掛け合わせ座標変換行列を完成させる
m.multiply(pMatrix, vMatrix, mvpMatrix);
m.multiply(mvpMatrix, mMatrix, mvpMatrix);
// uniformLocationの取得
var uniLocation = gl.getUniformLocation(prg, 'mvpMatrix');
// uniformLocationへ座標変換行列を登録
gl.uniformMatrix4fv(uniLocation, false, mvpMatrix);
// モデルの描画
gl.drawArrays(gl.TRIANGLES, 0, 3);
// コンテキストの再描画
gl.flush();
// シェーダを生成する関数
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));
}
}
// プログラムオブジェクトを生成しシェーダをリンクする関数
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));
}
}
// VBOを生成する関数
function create_vbo(data){
// バッファオブジェクトの生成
var vbo = gl.createBuffer();
// バッファをバインドする
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
// バッファにデータをセット
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
// バッファのバインドを無効化
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// 生成した VBO を返して終了
return vbo;
}
};
初期化処理
さて、それでは上から順番に見ていきましょう。
まず前提として script.js の全てのコードはウェブページの読み込みと同時に自動的に実行されるようにします。ですから onload
に関数として全ての処理を渡しています。その後、まずは canvas エレメントの取得から処理がスタートします。
エレメントの取得と初期化までを抜粋
// canvasエレメントを取得
var c = document.getElementById('canvas');
c.width = 300;
c.height = 300;
// webglコンテキストを取得
var gl = c.getContext('webgl') || c.getContext('experimental-webgl');
// canvasを初期化する色を設定する
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// canvasを初期化する際の深度を設定する
gl.clearDepth(1.0);
// canvasを初期化
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
一番最初にやっていることは canvas への参照を取得し、canvas のサイズを縦横 300 px に設定することですね。その後、WebGL コンテキストを取得し、クリアする色を設定しています。
次に出てくるのがクリアする深度の設定です。 clearDepth
メソッドはコンテキストをクリアする際の深度を設定するために使います。以前のサンプルでは色を初期化するだけだったので clearColor
メソッドしか使いませんでしたが、実際に三次元空間を扱う場合には、奥行きに関する情報もクリアする必要があるので、 clearDepth
メソッドも併せて実行します。
同様の理由により clear
メソッドに与える引数も変化します。色だけでなく深度に関してもクリアするようにするために、 gl.DEPTH_BUFFER_BIT
という組み込み定数を引数に追加しています。
シェーダとプログラムオブジェクトの生成
頂点シェーダとフラグメントシェーダを生成し、プログラムオブジェクトでリンクさせます。以前のテキスト(シェーダのコンパイルとリンク)でこのあたりの処理の詳細については解説していますので、そちらも参考にしてみるといいでしょう。
シェーダとプログラムオブジェクトによる処理の抜粋
// 頂点シェーダとフラグメントシェーダの生成
var v_shader = create_shader('vs');
var f_shader = create_shader('fs');
// プログラムオブジェクトの生成とリンク
var prg = create_program(v_shader, f_shader);
// attributeLocationの取得
var attLocation = gl.getAttribLocation(prg, 'position');
// attributeの要素数(この場合は xyz の3要素)
var attStride = 3;
ここで登場しているいくつかのメソッドは、WebGL の組み込みのメソッドではなく、自前の関数ですので注意しましょう。具体的には、 create_shader
と create_program
は組み込みメソッドではなく、オリジナル関数です。
create_shader
関数は引数に HTML の id を文字列として受け取り、対象となる script タグからシェーダのソースを参照し、シェーダオブジェクトを生成して返します。上記の例で言えば、id が vs と fs の二つの script タグから頂点シェーダとフラグメントシェーダを生成するわけですね。
シェーダを生成することができたら、次は create_program
関数にシェーダを引数として与えて呼び出します。戻り値として返されるデータがプログラムオブジェクトですので、これも変数に保持しておきます。 create_program
関数の内部では、プログラムオブジェクトの生成と、シェーダのリンクを行なっています。
続いて、二つの変数を定義しています。ここで定義している attLocation
と attStride
という二つの変数は、このあと頂点シェーダにデータを渡す際に必要となる情報を保持するのに使います。
ここで少し話が戻りますが、以前のテキストで示した今回使用する頂点シェーダのソースを見てみましょう。そうすると、二つの変数を定義した理由が少しだけ見えてきます。
今回使用する頂点シェーダのソース
attribute vec3 position;
uniform mat4 mvpMatrix;
void main(void){
gl_Position = mvpMatrix * vec4(position, 1.0);
}
今回使用する頂点シェーダでは、利用する attribute
変数は一つだけです。 position
がそれにあたりますね。この変数 position
は、ご覧の通り vec3
として宣言されていますね。これは、三つの要素を持つベクトルであることを意味します。
ここでのポイントは[ 利用する attribute
変数は一つでその名前は position
である ]ということと、[ その変数は vec3
型の変数である ]という二つの事実です。実は attribute
変数としてシェーダにデータを渡す際には、そのデータが何番目の attribute 変数なのかということと、その変数はいくつの要素から成るのかということの二つの情報が必要になります。
つまり変数 attLocation
は、そのデータが何番目のデータなのかということを保持するために使い、変数 attStride
はデータがいくつの要素から成るのかを保持するために使うわけです。
WebGL コンテキストのメソッドである getAttribLocation
には、第一引数に対象となるプログラムオブジェクトを、第二引数に取得したい attribute
変数の名前を指定します。戻り値として返される数値が、頂点シェーダにデータを渡す際のインデックスとしての役割を果たします。つまり何番目の変数なのかということを教えてくれるわけですね。ここで取得できた数値はあとあと使うことになるので覚えておきましょう。
変数 attStride
は、頂点シェーダ内の attribute
変数である position
が、三つの要素を持つ vec3
型の変数であることを示すために使う変数です。これもあとで使うことになるので変数に保持しておきます。
頂点バッファ( VBO )の生成と通知
さぁ、どんどんいきましょう。続いては、頂点データ(モデルデータ)を定義して VBO を生成します。その後、VBO を頂点シェーダに関連付けるためにバインドし登録します。
VBO の生成と通知に関する処理の抜粋
// モデル(頂点)データ
var vertex_position = [
0.0, 1.0, 0.0,
1.0, 0.0, 0.0,
-1.0, 0.0, 0.0
];
// VBOの生成
var vbo = create_vbo(vertex_position);
// VBOをバインド
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
// attribute属性を有効にする
gl.enableVertexAttribArray(attLocation);
// attribute属性を登録
gl.vertexAttribPointer(attLocation, attStride, gl.FLOAT, false, 0, 0);
以前のテキスト(モデルデータと頂点属性)でも詳しく解説したとおり、頂点のデータは単純な配列として定義します。その配列データから、オリジナル関数である create_vbo
を使って頂点バッファ( VBO )を生成します。
頂点バッファと頂点シェーダ内の attribute
変数を紐付けするために、まずは WebGL に VBO をバインドします。その後、先ほど取得した attribute
属性のインデックスを使って、その attribute
属性を有効にします。WebGL のメソッドである enableVertexAttribArray
を使うことで、目的の属性を有効にできます。
次に、WebGL のメソッドである vertexAttribPointer
を使いシェーダにデータを登録します。先に定義してあった二つの変数 attLocation
と attStride
はここでも使うのですね。 vertexAttribPointer
の第一引数には attribute
の何番目であるのかを示すインデックスを、第二引数にはその要素数を、第三引数にはそのデータがどんなデータ型であるかを示す組み込み定数を指定します。 gl.FLOAT
は浮動小数点数を示す定数です。第四~第六引数は基本的にはほとんど変更することはありませんが、メモリ上の参照を駆使して特殊なデータの受け渡し方をしたい場合には使うこともあります。
注意点としては、 vertexAttribPointer
を実行する際には、対象となる VBO を必ずバインドしておくようにします。どの VBO をどの attribute
属性に関連付けるかが重要なわけですから、あらかじめ WebGL に VBO を忘れずにバインドしておきましょう。
座標変換行列の生成と通知
続いてはレンダリングのための座標変換行列を準備します。これには当サイトオリジナルの行列演算用ライブラリ minMatrix.js を使います。minMatrix.js の基本的な使い方については前回のテキスト(minMatrix.js と座標変換行列)で解説しています。
座標変換行列の生成と通知に関する処理を抜粋
// matIVオブジェクトを生成
var m = new matIV();
// 各種行列の生成と初期化
var mMatrix = m.identity(m.create());
var vMatrix = m.identity(m.create());
var pMatrix = m.identity(m.create());
var mvpMatrix = m.identity(m.create());
// ビュー座標変換行列
m.lookAt([0.0, 1.0, 3.0], [0, 0, 0], [0, 1, 0], vMatrix);
// プロジェクション座標変換行列
m.perspective(90, c.width / c.height, 0.1, 100, pMatrix);
// 各行列を掛け合わせ座標変換行列を完成させる
m.multiply(pMatrix, vMatrix, mvpMatrix);
m.multiply(mvpMatrix, mMatrix, mvpMatrix);
// uniformLocationの取得
var uniLocation = gl.getUniformLocation(prg, 'mvpMatrix');
// uniformLocationへ座標変換行列を登録
gl.uniformMatrix4fv(uniLocation, false, mvpMatrix);
今回の場合は、モデル変換行列については初期化してそのまま何もせずに使っています。モデル変換行列になにか操作を加えることももちろんできないわけではありませんが、最初ということでそのまま使っています。
ビュー変換行列は、minMatrix.js で定義されている matIV.lookAt
を使うことで用意できます。上記のようにすると、三次元空間を映し出すカメラは、原点から上に 1.0 、後ろに 3.0 移動した状態で置かれ、原点を注視点として見つめている状態になります。カメラの上方向は Y 軸の方向に指定してあります。
プロジェクション変換行列は、視野角を 90 度に、アスペクト比は canvas のサイズをそのまま、ニアクリップとファークリップをそれぞれ指定して matIV.perspective
を呼び出し生成しています。
モデル・ビュー・プロジェクションの各変換行列を掛け合わせて、最終的には mvpMatrix
という座標変換行列を完成させます。あとは、これを WebGL に通知すればいいわけですね。
WebGL に uniform
変数として登録するには、 attribute
変数のときと同じ要領でまずはロケーションを取得します。WebGL のメソッドである getUniformLocation
にプログラムオブジェクトと変数の名称を与えて呼び出すと、何番目の uniform
変数なのかというインデックスが得られます。
得られたインデックスを元に、今度は頂点シェーダにデータが正しく受け渡されるように登録する作業を行ないます。これには、WebGL のメソッドである uniformMatrix4fv
を使います。第一引数にはロケーションのインデックスを、第二引数には行列を転置するのかどうかを真偽値で(ただし true にするとクラッシュする場合があります)、第三引数には実際に登録する行列を指定します。
uniform 系メソッドについて
今回登場した uniformMatrix4fv に代表される uniform 系のメソッドには、たくさんの種類があります。大別すると、次のような種類があります。
uniform
系
uniform 系には、uniform1 ~ uniform4 までがあり、それぞれ一つから四つの要素を頂点シェーダに登録するために使います。渡されるデータのタイプが整数なのか浮動小数点数なのかによって、数字の後ろに i (int)や f (float)などの小文字アルファベットを付加します。例としては、二つの要素を持つ浮動小数点数のデータを渡したい場合には、uniform2f を使えばいいということになります。
記述例 : gl.uniform2f(uniformLocation, date1, data2);
uniform v
系
この系統は、基本的には前述の uniform 系と大差ありませんが、データを配列として渡す場合に使います。uniform 系と同様、1 ~ 4 までがあり、扱うデータタイプに応じて i もしくは f を付加します。
記述例 : gl.uniform3iv(uniformLocation, Array);
uniformMatrix
系
その名の通り、行列を扱う場合にはこちらの系統を用います。当然のことながら行列を扱うわけですから 1 はなく、2 ~ 4 までが存在します。また、行列のデータは基本的に浮動小数点数のデータですので、i も存在しませんし、データは配列として扱うことを前提としているため、無印と v 付きの区別もありません。
記述例 : gl.uniformMatrix4fv(uniformLocation, false, Matrix);
モデルの描画とコンテキストの再描画
シェーダ、頂点データ、座標変換行列と、いろいろな作業を経てきましたが、いよいよ描画命令です。
描画命令とリフレッシュに関する処理の抜粋
// モデルの描画
gl.drawArrays(gl.TRIANGLES, 0, 3);
// コンテキストの再描画
gl.flush();
WebGL のメソッド drawArrays
が呼び出されるとモデルがバッファ上に描画されます。ここであえて[ バッファ上に ]と書いたのには理由があって、 drawArrays
が呼び出された時点では、画面上にはポリゴンはまだ描画されません。
画面上にレンダリングされたモデルを描画するためには、コンテキストをリフレッシュする必要があり、WebGL のメソッドである flush
が実行されて初めて、レンダリング結果が画面上に反映されます。
ここで登場した drawArrays
メソッドには、第一引数に頂点をどのように利用して描画するのかを指定するための組み込み定数を、第二引数には何番目の頂点から利用するかのオフセットを表す数値を、第三引数にいくつの頂点を描画するのかを数値で指定します。
今回の場合は gl.TRAIANGLES
を指定しているので、頂点は純粋な三角形ポリゴンとして三つの頂点を利用して描画されます。
まとめ
かなり長いテキストになりました。掲載したコードの量も多いので、正直面食らった人もいるかもしれません。
ただ、今回のコードで描画されるのは単なる板ポリ一枚だけです。それだけのためにこんな大量のコードを書くわけですから、3D プログラミングが難しいと言われていることにも納得できるのではないでしょうか。
しかし個人的には、これでも DirectX なんかと比べると、かなり簡単で、かなり簡素なように思います。特別な開発環境を必要としないという意味でも、WebGL は相当お手軽と言っていいでしょう。今回のテキストの内容が理解できれば、あとは少しずつカスタマイズして調整するだけで、いろんなことができるようになります。次回以降は、今回の内容がベースになりますので、しっかり理解しておいてください。
当テキストの最後に、今回の内容をそのまま実行できるサンプルへのリンクを貼っておきます。対応ブラウザを利用しているなら実際に見てみるといいでしょう。
さて、次回はポリゴンに色をつけてみます。お楽しみに。