インデックスバッファによる描画

実行結果

今回のサンプルの実行結果

複雑化するモデルに対応するために

前回はモデル座標変換行列を操作して、複数のモデルを移動したり回転したり、あるいは拡大縮小させたりしてレンダリングしてみました。ただし、レンダリングしたのはあくまでも単純な三角形ポリゴンであって、構造は極端すぎるほどシンプルなモデルでした。

ただし実際問題として、WebGL を使う場合にただの三角形ポリゴン一枚をレンダリングする機会はそんなに多くはないでしょう。せめて、四角形ポリゴンを使うことになるでしょうし、そもそも目指すのはもっと複雑なモデルをレンダリングすることだと思います。

モデルが複雑化すると、それに伴って頂点の数もどんどん増えます。精密で滑らかなモデルを描画しようと思えば思うほどに、必要な頂点データの量も肥大化していきます。javascript に限らず、あらゆるプログラムは省メモリ・省リソースであることに越したことはありません。巨大な頂点データを力技でガリガリ描画するのもいいですが、できる限りデータの量を減らしたいと考えるのがプログラマの性です。

WebGL には頂点データの肥大化を抑えるために、ある手段が提供されています。その一端がインデックスバッファを用いた頂点データの管理です。インデックスバッファは別名 IBO ( index buffer object )とも呼ばれるバッファの一つの種類です。そして、今まで使ってきた似たようなバッファに VBO がありますね。IBO は、この VBO とセットで使います。

頂点が増えれば増えるほど

先ほども書いたように、頂点が増えれば増えるほどそれを管理するための頂点データは肥大化していきます。しかし、ポリゴンを形成する際にはいくつかの頂点を使いまわすことができるケースが出てきます。

具体的な例を挙げて考えてみましょう。

ポリゴンは基本的に三角形なので、たとえば四角形を表現したいと考えた場合には三角形ポリゴンが二つ必要になります。

四角形ポリゴンの頂点

さて、それではこのとき、頂点はいくつ必要になるでしょうか。

先ほどの図を見ながら考えると、普通に頂点が四つあればいいような気がしますね。しかし、ここで考えてみてください。三角形ポリゴンを一つ定義するためには、頂点が三つ必要でしたね。ということは、二つのポリゴンを定義するためには、単純に計算すると頂点が六つ必要となってしまうはずではないでしょうか。

頂点の座標情報が全く同じなのに、重複する頂点をいくつも定義するのは明らかに無駄ですね。できることなら、頂点を使いまわしたいと考えてしまいます。

重複する頂点を再利用する

重複する頂点データを再利用し、できる限り頂点データを圧縮したい――こんなときこそ、インデックスバッファの出番です。インデックスバッファはその名が示すとおり、頂点のインデックス情報を格納するために使われます。先ほどの図を例にとれば、一つ目のポリゴンは[ 1, 2, 3 ]の三つの頂点で定義できますね。同様に、二つ目のポリゴンは[ 2, 3, 4 ]の三つの頂点で定義できます。

このように、ポリゴンなどを描画する際に、どの頂点を使えばいいのか WebGL に教えてくれるのがインデックスバッファです。今回のように単なる四角形一枚であればその効果はたいしたことはありませんが、巨大で複雑なモデルになればなるほど、インデックスバッファによる省リソースの恩恵は大きなものになります。

また、インデックスバッファには省リソースであること以外にも、非常に大きなメリットがあります。

インデックスバッファを用いると、そのデータは直接 GPU 上のメモリ領域にデータとして確保されます。描画のたびに毎回 GPU にデータを転送することはせず、GPU 上のメモリ空間からデータを引っ張ってくるわけですから、これは毎回データを転送することに比べて非常に高速です。

省リソースなうえに、動作も高速とあれば IBO を使わない手はありませんね。

IBO の生成

インデックスバッファを生成する手順は、頂点バッファのときと似ています。いくつかの引数などが変化しますが、基本的には同じような流れでインデックスバッファを生成できます。

今回のサンプルで新しく定義するオリジナル関数、 create_ibo を見ながら考えてみましょう。

オリジナル関数 create_ibo

// IBOを生成する関数
function create_ibo(data){
	// バッファオブジェクトの生成
	var ibo = gl.createBuffer();
	
	// バッファをバインドする
	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
	
	// バッファにデータをセット
	gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Int16Array(data), gl.STATIC_DRAW);
	
	// バッファのバインドを無効化
	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
	
	// 生成したIBOを返して終了
	return ibo;
}

VBO を生成したときと同様、まずは createBuffer メソッドを使って空のバッファオブジェクトを生成します。その後、そのバッファをバインドするわけですが bindBuffer メソッドの第一引数には VBO のときとは違う組み込み定数を指定します。それが gl.ELEMENT_ARRAY_BUFFER という組み込み定数ですね。この定数を指定するとそのバッファは IBO として扱われます。

バッファがバインドできたらインデックスデータをバッファに割り当てます。ここでも若干 VBO のときとは違う部分があるのですがわかるでしょうか。それは第二引数に指定されているデータの変換方法です。VBO のときには Float32Array としてデータを渡しましたが、インデックスデータというのは浮動小数点数ではなく整数なので、 Int16Array を使ってデータをセットするのですね。

このオリジナル関数である create_ibo という関数は、引数に配列を受け取ります。これは VBO を生成するオリジナル関数 create_vbo と同じ仕様です。IBO 用の配列データは VBO 用の配列データと同じようにあらかじめ定義しておきます。

インデックスデータ配列の例

var index = [
	0, 1, 2,
	1, 2, 3
];

頂点が四つある場合、上記のようにすることで一つ目の三角形ポリゴンを[ 0, 1, 2 ]の頂点で、二つ目の三角形ポリゴンを[ 1, 2, 3 ]の頂点で描画するという意味になります。頂点のインデックスは 0 から始まるということと、配列は一次元の配列で作成するということに注意しましょう。

描画まわりの変更点

さて、インデックスバッファを生成するところまでは理解できたでしょうか。続いては実際にインデックスバッファを使う場合の手順について解説しましょう。

IBO を使ったレンダリングを行なう場合には、事前に IBO を WebGL にバインドしておく必要があります。これは以下のようなコードで実現できます。

IBO をバインドするコード

gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);

変数 ibo には、先ほど生成した IBO を渡します。これだけで IBO のバインドは完了です。ただしバインドしただけでは IBO を使った描画は行なわれません。もう一つ、描画命令にも修正を加える必要があります。

今までは、ポリゴンのレンダリングには drawArrays メソッドを使っていましたね。IBO を使って描画を行なうためには、 drawElements メソッドを使います。このメソッドを使って描画を行なう際の一例を示すと、以下のようになります。

drawElements を用いた描画命令の例

gl.drawElements(gl.TRIANGLES, index.length, gl.UNSIGNED_SHORT, 0);

この drawElements メソッドは四つの引数を取ります。第一引数にはどのように頂点をレンダリングするのかを表す組み込み定数を、第二引数にはインデックスデータの要素数を、第三引数にはインデックスデータのデータサイズを、第四引数には使うインデックスデータのオフセットを指定します。

わかりにくいのは第二引数の部分だと思います。ここには、インデックスデータを格納した配列の要素数をそのまま渡せば大丈夫です。他の引数については特別なことをしない限りは、そのまま使えば大丈夫でしょう。

まとめ

インデックスバッファを使うことで、データを節約しながら効率よく頂点をレンダリングすることが可能になります。そして、それはモデルが複雑になればなるほど大きな影響を持つようになります。

また、IBO を用いることで、データの転送を効率化でき、高速化を図ることもできるのでしたね。これはプログラマにとって非常に嬉しいことです。

インデックスバッファは頂点バッファと同様に、純粋な一次元の配列データから生成することが可能であり、生成された IBO をバインドすることによって利用できます。IBO を利用する場合には描画メソッドに drawElements を使うということも大事なポイントです。

今回のサンプルでは、頂点シェーダとフラグメントシェーダに変更はありません。ですので、必然的に HTML ソースは以前のものをそのまま使います。javascript の内容が多少変更になっているので、最後にコード全文を掲載しておきます。また、実際にサンプルを実行できるページへのリンクをテキストの最後に貼っておきますので、参考にしてみてください。

次回はカリングと深度テストについてやる予定です。

script.js ソース全文

onload = function(){
	// canvasエレメントを取得
	var c = document.getElementById('canvas');
	c.width = 500;
	c.height = 300;
	
	// webglコンテキストを取得
	var gl = c.getContext('webgl') || c.getContext('experimental-webgl');
	
	// 頂点シェーダとフラグメントシェーダの生成
	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,
		 0.0, -1.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,
		1.0, 1.0, 1.0, 1.0
	];
	
	// 頂点のインデックスを格納する配列
	var index = [
		0, 1, 2,
		1, 2, 3
	];
	
	// VBOの生成
	var pos_vbo = create_vbo(position);
	var col_vbo = create_vbo(color);
	
	// VBO を登録する
	set_attribute([pos_vbo, col_vbo], attLocation, attStride);
	
	// IBOの生成
	var ibo = create_ibo(index);
	
	// IBOをバインドして登録する
	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
	
	// 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, 5.0], [0, 0, 0], [0, 1, 0], vMatrix);
	m.perspective(45, c.width / c.height, 0.1, 100, pMatrix);
	m.multiply(pMatrix, vMatrix, tmpMatrix);
	
	// カウンタの宣言
	var count = 0;
	
	// 恒常ループ
	(function(){
		// canvasを初期化
		gl.clearColor(0.0, 0.0, 0.0, 1.0);
		gl.clearDepth(1.0);
		gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
		
		// カウンタをインクリメントする
		count++;
		
		// カウンタを元にラジアンを算出
		var rad = (count % 360) * Math.PI / 180;
		
		// モデル座標変換行列の生成(Y軸による回転)
		m.identity(mMatrix);
		m.rotate(mMatrix, rad, [0, 1, 0], mMatrix);
		m.multiply(tmpMatrix, mMatrix, mvpMatrix);
		gl.uniformMatrix4fv(uniLocation, false, mvpMatrix);
		
		// インデックスを用いた描画命令
		gl.drawElements(gl.TRIANGLES, index.length, gl.UNSIGNED_SHORT, 0);
		
		// コンテキストの再描画
		gl.flush();
		
		// ループのために再帰呼び出し
		setTimeout(arguments.callee, 1000 / 30);
	})();
	
	// シェーダを生成する関数
	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);
		}
	}
	
	// IBOを生成する関数
	function create_ibo(data){
		// バッファオブジェクトの生成
		var ibo = gl.createBuffer();
		
		// バッファをバインドする
		gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
		
		// バッファにデータをセット
		gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Int16Array(data), gl.STATIC_DRAW);
		
		// バッファのバインドを無効化
		gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
		
		// 生成したIBOを返して終了
		return ibo;
	}

};

entry

PR

press Z key