複数モデルのレンダリング
今回のサンプルの実行結果
頂点バッファの再利用
前回のテキストでは、ポリゴンを形成する三つの頂点に、色という新しい頂点属性を付加することで、ポリゴンに着色してレンダリングしました。VBO を新たに準備することで、頂点属性を自由に拡張することができるということがわかっていただけたと思います。
さて、今回は複数モデルのレンダリングに挑戦します。ただし、新しく VBO を用意するということはしません。VBO は前回用意したものをそのまま利用します。つまり、VBO は再利用し、座標変換行列を操作することで複数モデルの描画を実現するわけです。
というわけで、今回はシェーダに関しては一切変更なしです。前回使った頂点シェーダとフラグメントシェーダをそのまま使用します。javascript のソースに関しても、それほど変更点は多くありません。抜粋して、変更した部分を中心に解説します。
座標変換行列の再利用
今回の複数モデルレンダリングでは、座標変換行列に関しても再利用します。
というのも、実際問題として、複数のモデルを違う位置にレンダリングしようとする場合、操作しなければならない座標変換行列はモデル変換行列のみです。カメラの位置を決めるビュー変換行列、スクリーンに映し出される領域などを決めるプロジェクション変換行列の二つの座標変換行列に関しては、どちらのモデルに対しても同じものを適用します。
手順としては、以下のように行列を操作すればいいでしょう。
- ビュー・プロジェクションの両座標変換行列を用意する
- あらかじめ二つの行列を掛け合わせ保持しておく(以下、pv)
- 一つ目のモデル座標変換行列を用意(以下、m1)
- m1 に pv を掛け合わせて uniform 登録
- 一つ目のモデルを描画
- 二つ目のモデル座標変換行列を用意(以下、m2)
- m2 に pv を掛け合わせて uniform 登録
- 二つ目のモデルを描画
- コンテキストのリフレッシュして再描画
要するに、ビュー座標変換行列とプロジェクション座標変換行列に関してはあらかじめ掛け合わせておき、それを保持しておきます。モデル座標変換行列が用意できたら、その都度、ビュー・プロジェクション座標変換行列を掛け合わせ、uniform 登録をした後、描画するわけですね。
それでは、実際のコードを見てみましょう。
script.js の一部を抜粋
// 各種行列の生成と初期化
var mMatrix = m.identity(m.create());
var vMatrix = m.identity(m.create());
var pMatrix = m.identity(m.create());
var tmpMatrix = m.identity(m.create());
var mvpMatrix = m.identity(m.create());
// ビュー×プロジェクション座標変換行列
m.lookAt([0.0, 0.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, tmpMatrix);
// 一つ目のモデルを移動するためのモデル座標変換行列
m.translate(mMatrix, [1.5, 0.0, 0.0], mMatrix);
// モデル×ビュー×プロジェクション(一つ目のモデル)
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
// uniformLocationへ座標変換行列を登録し描画する(一つ目のモデル)
gl.uniformMatrix4fv(uniLocation, false, mvpMatrix);
gl.drawArrays(gl.TRIANGLES, 0, 3);
// 二つ目のモデルを移動するためのモデル座標変換行列
m.identity(mMatrix);
m.translate(mMatrix, [-1.5, 0.0, 0.0], mMatrix);
// モデル×ビュー×プロジェクション(二つ目のモデル)
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
// uniformLocationへ座標変換行列を登録し描画する(二つ目のモデル)
gl.uniformMatrix4fv(uniLocation, false, mvpMatrix);
gl.drawArrays(gl.TRIANGLES, 0, 3);
// コンテキストの再描画
gl.flush();
ポイントとなるのは、ビュー・プロジェクション座標変換行列を一時的に保持しておくために、新たに宣言されている tmpMatrix
の使い方ですね。ビュー座標変換行列とプロジェクション座標変換行列を matIV.multiply
で掛け合わせた際、その結果をいったん tmpMatrix
に保持します。
その後、 matIV.translate
を使って、モデル座標変換行列を操作していますが、一つ目のモデルは X 方向へ 1.5 移動するモデル座標変換となっています。このモデル座標変換行列と tmpMatrix
を掛け合わせてから uniform 登録して描画します。
二つ目のモデル座標変換行列は、先ほどとは逆に、X 方向へ -1.5 移動させた行列ですね。これも一つ目のときと同様に tmpMatrix
と掛け合わせ、uniform 登録して描画しています。
最後にコンテキストをリフレッシュして再描画させると、canvas 上には二つのポリゴンが描画されます。このように、VBO や、一部の座標変換行列を再利用することで、無駄なリソースを省いて効率的に複数のモデルをレンダリングすることができます。
注意点としては、二つ目のモデルに対するモデル座標変換行列を用意する際、まず最初に matIV.identity
を使って行列を初期化していることでしょうか。仮に、初期化を行なわずに mMatrix
にそのまま matIV.translate
をもう一度適応してしまうと、前回の移動成分が影響してしまうので、結果が変わってしまいます。一度初期化してから操作することで、そのようなイージーミスを回避できます。
オマケ( attribute 登録の関数化 )
前回のテキストでも触れましたが、頂点属性の登録を行なう作業は、関数化することでかなり効率化できます。今回のサンプルの主題(複数モデルレンダリング)とは直接関係ありませんが、サンプル内では関数化して利用していますので、簡単に解説しておきます。
VBO をバインドし登録する関数
// VBOをバインドし登録する関数
function set_attribute(vbo, attL, attS){
// 引数として受け取った配列を処理する
for(var i in vbo){
// バッファをバインドする
gl.bindBuffer(gl.ARRAY_BUFFER, vbo[i]);
// attributeLocationを有効にする
gl.enableVertexAttribArray(attL[i]);
// attributeLocationを通知し登録する
gl.vertexAttribPointer(attL[i], attS[i], gl.FLOAT, false, 0, 0);
}
}
この関数は三つの引数を取りますが、いずれも配列として受け取ることを前提としています。 for ~ in
ステートメントを使って、VBO の数だけ属性をバインドして登録します。普通、VBO はたくさん用意しなければならなくなりますので、このように関数化しておくことで、先々だいぶ楽にコードが記述できるようになるでしょう。
まとめ
今回はモデル座標変換行列を操作することで、VBO やビュー・プロジェクション座標変換行列はそのまま再利用しながら、複数のモデルをレンダリングする方法を紹介しました。
単純なモデルや図形をたくさん描画しなければならないような場面では、今回のようなやり方を活用することで、簡潔に処理を記述できるでしょう。無駄にリソースを消費することもありません。
今回のサンプルでは、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 = new Array(2);
attLocation[0] = gl.getAttribLocation(prg, 'position');
attLocation[1] = gl.getAttribLocation(prg, 'color');
// attributeの要素数を配列に格納
var attStride = new Array(2);
attStride[0] = 3;
attStride[1] = 4;
// 頂点属性を格納する配列
var position = [
0.0, 1.0, 0.0,
1.0, 0.0, 0.0,
-1.0, 0.0, 0.0
];
var color = [
1.0, 0.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 0.0, 1.0, 1.0
];
// VBOの生成
var pos_vbo = create_vbo(position);
var col_vbo = create_vbo(color);
// VBO を登録する
set_attribute([pos_vbo, col_vbo], attLocation, attStride);
// uniformLocationの取得
var uniLocation = gl.getUniformLocation(prg, 'mvpMatrix');
// 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 tmpMatrix = m.identity(m.create());
var mvpMatrix = m.identity(m.create());
// ビュー×プロジェクション座標変換行列
m.lookAt([0.0, 0.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, tmpMatrix);
// 一つ目のモデルを移動するためのモデル座標変換行列
m.translate(mMatrix, [1.5, 0.0, 0.0], mMatrix);
// モデル×ビュー×プロジェクション(一つ目のモデル)
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
// uniformLocationへ座標変換行列を登録し描画する(一つ目のモデル)
gl.uniformMatrix4fv(uniLocation, false, mvpMatrix);
gl.drawArrays(gl.TRIANGLES, 0, 3);
// 二つ目のモデルを移動するためのモデル座標変換行列
m.identity(mMatrix);
m.translate(mMatrix, [-1.5, 0.0, 0.0], mMatrix);
// モデル×ビュー×プロジェクション(二つ目のモデル)
m.multiply(tmpMatrix, mMatrix, 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;
}
// VBOをバインドし登録する関数
function set_attribute(vbo, attL, attS){
// 引数として受け取った配列を処理する
for(var i in vbo){
// バッファをバインドする
gl.bindBuffer(gl.ARRAY_BUFFER, vbo[i]);
// attributeLocationを有効にする
gl.enableVertexAttribArray(attL[i]);
// attributeLocationを通知し登録する
gl.vertexAttribPointer(attL[i], attS[i], gl.FLOAT, false, 0, 0);
}
}
};