動的キューブマッピング

実行結果

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

キューブマップ系の集大成

前回はキューブ環境マッピングを用いたテクニックの一つである屈折マッピングを解説しました。屈折マッピングを用いると、ガラスのような透明感のある質感を演出できるのでしたね。

今回は、今まで解説してきたキューブマッピング系の処理の集大成とも言える、動的キューブ環境マッピングを解説します。動的キューブマッピングでは、動的にキューブマップテクスチャを生成することによってリアルタイムに映り込みを表現します。

このテクニックを実現させるためには、キューブ環境マッピングのテクニックのほかにフレームバッファを扱える知識が必要です。もしフレームバッファの使い方がわからないようであれば、以前のテキストを参照してしっかりと予習しておきましょう。

また、ご存知の通りキューブマッピングには 6 枚のテクスチャが必要になります。それを動的に生成しながらレンダリングするということは、最低限、事前にこの六つのシーンをレンダリングしておかなくてはいけないことになります。つまり、動的キューブマッピングでは、どうあがいても 7 パスのレンダリングを行なって初めて一つのシーンが完成します。

今回のサンプルのような小規模なものであればそれほど付加は高くなりませんが、場合によっては非常に高負荷な処理になることもあります。その点には注意しましょう。

複数のシェーダプログラム

今回の動的キューブマッピングでは、キューブマップを適用してレンダリングするためのシェーダのほか、普通にライティングを行なうシェーダも用意します。今回のサンプルではワールド空間の原点にキューブマップを適用した球体をレンダリングします。この球体には、動的に生成したキューブマップが適用されるようにします。一方、この球体の周囲には、普通にライティングを行なったトーラスをレンダリングします。ここで複数のシェーダを切り替えながらレンダリングする必要性が生じます。

まずは、トーラスをレンダリングする際に利用するライティングシェーダのソースから見てみましょう。

ライティング用の頂点シェーダ

attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
uniform   mat4 mvpMatrix;
uniform   mat4 invMatrix;
uniform   vec3 lightDirection;
uniform   vec3 eyeDirection;
uniform   vec4 ambientColor;
varying   vec4 vColor;

void main(void){
	vec3  invLight = normalize(invMatrix * vec4(lightDirection, 0.0)).xyz;
	vec3  invEye   = normalize(invMatrix * vec4(eyeDirection, 0.0)).xyz;
	vec3  halfLE   = normalize(invLight + invEye);
	float diffuse  = clamp(dot(normal, invLight), 0.0, 1.0);
	float specular = pow(clamp(dot(normal, halfLE), 0.0, 1.0), 50.0);
	vec4  amb      = color * ambientColor;
	vColor         = amb * vec4(vec3(diffuse), 1.0) + vec4(vec3(specular), 1.0);
	gl_Position    = mvpMatrix * vec4(position, 1.0);
}

ライティング用のフラグメントシェーダ

precision mediump float;

varying vec4 vColor;

void main(void){
	gl_FragColor = vColor;
}

ご覧のとおり、今回は頂点シェーダで全てのライティングを計算しています。フラグメントシェーダでライティング計算を行なうのに比べ、頂点単位で計算を行なうことで計算量を軽減できるからです。

やっていることは拡散光と反射光による照明の計算です。また、シェーダ内でアンビエント、つまり環境光に関係するような変数名を使っていますが、今回は単純にモデルに色をつけるために環境光に相当するデータを javascript プログラムのほうから受け取って処理するようにしています。

ライティングに関する詳細な解説は以前のテキストで既に行なっていますので、今回は割愛します。必要に応じて過去のテキストを参照してください。

さて、続いては環境マップを適用する際に利用するシェーダのソースです。

こちらは基本的なキューブマッピングを行なっているだけですので、特別なことはあまりありません。

キューブマッピング用の頂点シェーダ

attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
uniform   mat4 mMatrix;
uniform   mat4 mvpMatrix;
varying   vec3 vPosition;
varying   vec3 vNormal;
varying   vec4 vColor;

void main(void){
	vPosition   = (mMatrix * vec4(position, 1.0)).xyz;
	vNormal     = (mMatrix * vec4(normal, 0.0)).xyz;
	vColor      = color;
	gl_Position = mvpMatrix * vec4(position, 1.0);
}

キューブマッピング用のフラグメントシェーダ

precision mediump float;

uniform vec3        eyePosition;
uniform samplerCube cubeTexture;
uniform bool        reflection;
varying vec3        vPosition;
varying vec3        vNormal;
varying vec4        vColor;

void main(void){
	vec3 ref;
	if(reflection){
		ref = reflect(vPosition - eyePosition, vNormal);
	}else{
		ref = vNormal;
	}
	vec4 envColor  = textureCube(cubeTexture, ref);
	vec4 destColor = vColor * envColor;
	gl_FragColor   = destColor;
}

以前のテキストで解説した基本的なキューブ環境マッピングのソースとほとんど同じものですので、わからない場合にはそちらを参照してみてください。

今回は複数のシェーダを切り替えながら処理しますが、シェーダのソース自体は既出の技術を使いまわしているだけのものです。今までのテキストを順に読み進めてきていれば、概念的な部分に関しては新しいことは何も出てきていません。

今回のサンプルで難関となるのは、シェーダよりもむしろメインプログラムとなる javascript プログラムのほうです。

キューブマップ生成と座標変換行列

動的にキューブマップを生成するには、前述のとおりフレームバッファを使う必要があります。バックグラウンドでオフスクリーンにレンダリングした結果を、キューブマップテクスチャとして利用するわけですね。

今回のサンプルでは、以下のような手順で動的キューブマッピングを実現します。

  • シェーダのコンパイルなど諸々の初期化処理
  • 動的キューブマップの生成(フレームバッファへの描画)
    • キューブマップで背景をレンダリング
    • トーラスをレンダリング
  • 上記を六つの方角でそれぞれ行なう
  • フレームバッファのバインドを解除
  • キューブマップで背景をレンダリング
  • 動的に生成したキューブマップを使って球体を描画

こんな感じですね。

まず前提として、今までのサンプルでやってきたのと同様に、普通にキューブマップを一つ用意しておきます。これはいずれのシーンでも背景として使われます。動的キューブマップテクスチャを生成するためにフレームバッファに描き込む際にも、背景のレンダリングはどのみち行なわなければなりません。

また、動的キューブマップの生成を行なう際、つまりフレームバッファに対してレンダリングを行なう際には、ビュー座標変換行列とプロジェクション座標変換行列を生成する際に注意が必要です。

キューブマップテクスチャはご存知の通り、原点から 6 方向を継ぎ目無くきれいに繋がる形で撮影した状態でなければなりません。動的にキューブマップテクスチャを生成するためには、そのレンダリングを行なう際にこのことを十分に考慮してやる必要があります。

具体的には、カメラに関するパラメータを決定するビュー座標変換行列の生成と、レンダリングされるシーンに関するパラメータを決定するプロジェクション座標変換行列の生成で、[ カメラの位置 ]を原点に、[ カメラの向き ]をそれぞれ 6 方向に、[ カメラの上方向 ]を撮影方向に応じて適切に設定してやる必要があります。また、スクリーンのアスペクト比が 1.0 になるように調整した上で画角を 90 度に設定します。

ビュー座標変換行列生成時の注意点

図に表すと上記のような状態ですね。

このことは、このあと解説するフレームバッファへのレンダリングの際に非常に重要になりますので、しっかり覚えおいてください。

フレームバッファの生成

さて、それではキューブマップテクスチャとして利用するフレームバッファの生成を見てみます。

以前、フレームバッファに関する解説を行なった際には、あくまでも二次元のレンダリングを行なう対象としてフレームバッファを使っていました。ですから、フレームバッファにアタッチするテクスチャは gl.TEXTURE_2D という組み込み定数を指定して生成していましたね。

今回の場合、キューブマップテクスチャとしてフレームバッファを使いますので、当然ながらフレームバッファにアタッチするカラーバッファは gl.TEXTURE_CUBE_MAP として生成されたテクスチャである必要があります。

以前のテキストで利用した当サイトの自作フレームバッファ生成関数を、キューブマップ対応バージョンに改造しましたので、そのソースを見てみましょう。

フレームバッファの生成

// フレームバッファをオブジェクトとして生成する関数(キューブマップ仕様)
function create_framebuffer(width, height, target){
	// フレームバッファの生成
	var frameBuffer = gl.createFramebuffer();

	// フレームバッファをWebGLにバインド
	gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer);

	// 深度バッファ用レンダーバッファの生成とバインド
	var depthRenderBuffer = gl.createRenderbuffer();
	gl.bindRenderbuffer(gl.RENDERBUFFER, depthRenderBuffer);

	// レンダーバッファを深度バッファとして設定
	gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height);

	// フレームバッファにレンダーバッファを関連付ける
	gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthRenderBuffer);

	// フレームバッファ用テクスチャの生成
	var fTexture = gl.createTexture();

	// フレームバッファ用のテクスチャをキューブマップテクスチャとしてバインド
	gl.bindTexture(gl.TEXTURE_CUBE_MAP, fTexture);

	// フレームバッファ用のテクスチャにカラー用のメモリ領域を 6 面分確保
	for(var i = 0; i < target.length; i++){
		gl.texImage2D(target[i], 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
	}

	// テクスチャパラメータ
	gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
	gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
	gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
	gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

	// 各種オブジェクトのバインドを解除
	gl.bindTexture(gl.TEXTURE_CUBE_MAP, null);
	gl.bindRenderbuffer(gl.RENDERBUFFER, null);
	gl.bindFramebuffer(gl.FRAMEBUFFER, null);

	// オブジェクトを返して終了
	return {f : frameBuffer, d : depthRenderBuffer, t : fTexture};
}

以前のフレームバッファ生成では、フレームバッファの幅と高さだけを引数として受け取っていました。今回の改造で、あらたに target という第三引数が増えています。これはキューブマップテクスチャのどの面を表すのか(ターゲット)を指定するために使われる配列です。

レンダーバッファを深度バッファとしてアタッチするところまでは、以前となんら変わりません。その後、テクスチャを生成してカラーバッファとしてアタッチするわけですが、そこが以前とは違います。

まず、カラーバッファ用の空のテクスチャを生成したあと、それをキューブマップ用テクスチャとして WebGL にバインドします。そこからループを回して、6 方向分それぞれに、メモリ領域を確保する処理を行ないます。以前はここで texImage2D メソッドの第一引数に gl.TEXTURE_2D を指定していました。今回はキューブマップ用にメモリ領域を確保するため、この自作関数の第三引数として入ってきたターゲット(例: gl.TEXTURE_CUBE_MAP_POSITIVE_X など)を texImage2D メソッドの第一引数に渡してやります。

このような処理を行うことで、フレームバッファにアタッチするための 6 面分のメモリ領域を持つキューブマップテクスチャが生成されます。あとは実際にフレームバッファにレンダリングを行なう際に、どの面に対してのレンダリングなのかを明確に通知してアタッチしてやれば、動的なキューブマップの生成が行なえます。

二次元のテクスチャをフレームバッファにアタッチする場合には、この関数内部でテクスチャのアタッチまで行なっていました。しかしフレームバッファにキューブマップテクスチャをアタッチする場合には、実際にレンダリングが行なわれる段階でアタッチを行ないます。

フレームバッファへのレンダリング

さて、それではいよいよフレームバッファへのレンダリングです。

ここでは先述の通り、座標変換行列の生成やテクスチャのアタッチ処理が重要になります。サンプルのコードのうち、関連する部分だけを抜粋します。

フレームバッファへのレンダリング

// 注視点
var eye = new Array();

// カメラの上方向
var camUp = new Array();

// モデルの座標位置
var pos = new Array();

// モデルに適用するカラー値
var amb = new Array();

// フレームバッファをバインド
gl.bindFramebuffer(gl.FRAMEBUFFER, fBuffer.f);

// フレームバッファへの 6 方向レンダリング
for(var i = 0; i < cubeTarget.length; i++){
	// フレームバッファにテクスチャを関連付ける
	gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, cubeTarget[i], fBuffer.t, 0);

	// フレームバッファを初期化
	gl.clearColor(0.0, 0.0, 0.0, 1.0);
	gl.clearDepth(1.0);
	gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

	// ライトベクトル
	var lightDirection = [-1.0, 1.0, 1.0];

	// 方角を判別して処理する
	switch(cubeTarget[i]){
		case gl.TEXTURE_CUBE_MAP_POSITIVE_X:
			eye[i]   = [ 1,  0,  0];
			camUp[i] = [ 0, -1,  0];
			pos[i]   = [ 6,  0,  0];
			amb[i]   = [1.0, 0.5, 0.5, 1.0];
			break;
		case gl.TEXTURE_CUBE_MAP_POSITIVE_Y:
			eye[i]   = [ 0,  1,  0];
			camUp[i] = [ 0,  0,  1];
			pos[i]   = [ 0,  6,  0];
			amb[i]   = [0.5, 1.0, 0.5, 1.0];
			break;
		case gl.TEXTURE_CUBE_MAP_POSITIVE_Z:
			eye[i]   = [ 0,  0,  1];
			camUp[i] = [ 0, -1,  0];
			pos[i]   = [ 0,  0,  6];
			amb[i]   = [0.5, 0.5, 1.0, 1.0];
			break;
		case gl.TEXTURE_CUBE_MAP_NEGATIVE_X:
			eye[i]   = [-1,  0,  0];
			camUp[i] = [ 0, -1,  0];
			pos[i]   = [-6,  0,  0];
			amb[i]   = [0.5, 0.0, 0.0, 1.0];
			break;
		case gl.TEXTURE_CUBE_MAP_NEGATIVE_Y:
			eye[i]   = [ 0, -1,  0];
			camUp[i] = [ 0,  0, -1];
			pos[i]   = [ 0, -6,  0];
			amb[i]   = [0.0, 0.5, 0.0, 1.0];
			break;
		case gl.TEXTURE_CUBE_MAP_NEGATIVE_Z:
			eye[i]   = [ 0,  0, -1];
			camUp[i] = [ 0, -1,  0];
			pos[i]   = [ 0,  0, -6];
			amb[i]   = [0.0, 0.0, 0.5, 1.0];
			break;
		default :
			break;
	}

	// ビュー×プロジェクション座標変換行列
	m.lookAt([0, 0, 0], eye[i], camUp[i], vMatrix);
	m.perspective(90, 1.0, 0.1, 200, pMatrix);
	m.multiply(pMatrix, vMatrix, tmpMatrix);

	// キューブマップテクスチャで背景用キューブをレンダリング
	gl.useProgram(cPrg);
	set_attribute(cVBOList, cAttLocation, cAttStride);
	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cIndex);
	m.identity(mMatrix);
	m.scale(mMatrix, [100.0, 100.0, 100.0], mMatrix);
	m.multiply(tmpMatrix, mMatrix, mvpMatrix);
	gl.uniformMatrix4fv(cUniLocation[0], false, mMatrix);
	gl.uniformMatrix4fv(cUniLocation[1], false, mvpMatrix);
	gl.uniform3fv(cUniLocation[2], [0, 0, 0]);
	gl.activeTexture(gl.TEXTURE0);
	gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubeTexture);
	gl.uniform1i(cUniLocation[3], 0);
	gl.uniform1i(cUniLocation[4], false);
	gl.drawElements(gl.TRIANGLES, cubeData.i.length, gl.UNSIGNED_SHORT, 0);

	// 視線ベクトルの変換
	var invEye = new Array();
	invEye[0] = -eye[i][0];
	invEye[1] = -eye[i][1];
	invEye[2] = -eye[i][2];

	// スペキュラライティングシェーダでトーラスモデルをレンダリング
	gl.useProgram(sPrg);
	set_attribute(tVBOList, sAttLocation, sAttStride);
	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, tIndex);
	m.identity(mMatrix);
	m.translate(mMatrix, pos[i], mMatrix);
	m.rotate(mMatrix, rad, eye[i], mMatrix);
	m.multiply(tmpMatrix, mMatrix, mvpMatrix);
	m.inverse(mMatrix, invMatrix);
	gl.uniformMatrix4fv(sUniLocation[0], false, mvpMatrix);
	gl.uniformMatrix4fv(sUniLocation[1], false, invMatrix);
	gl.uniform3fv(sUniLocation[2], lightDirection);
	gl.uniform3fv(sUniLocation[3], invEye);
	gl.uniform4fv(sUniLocation[4], amb[i]);
	gl.drawElements(gl.TRIANGLES, torusData.i.length, gl.UNSIGNED_SHORT, 0);
}

ここに抜粋したコードは、恒常ループの中に記述されています。つまり、ループが回るたびに実行されるコードということですね。※一部ループ外で実行しても問題ない処理も含まれていますが、これは理解を助けるための実装です。

まず、上記コードのなかで登場する cubeTarget という配列には、キューブマップのどの面を表すのかを指定するための定数である gl.TEXTURE_CUBE_MAP_POSITIVE_X などが順番に格納されています。

フレームバッファを WebGL にバインドしたあと、6 方向それぞれに対して処理を行なうために for ステートメントによる繰り返し処理を行ないますが、ここで cubeTarget の中身を参照しながら処理を行なうわけですね。

繰り返し処理のなかではまず、フレームバッファへのテクスチャのアタッチが行なわれています。先ほども書いたとおり、キューブマップテクスチャをカラーバッファとして利用するためには、レンダリングが行なわれる都度アタッチを行なう必要があるからです。

// フレームバッファにテクスチャを関連付ける
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, cubeTarget[i], fBuffer.t, 0);

ここで出てくる fBuffer は先述のフレームバッファ生成関数によって生成したオブジェクトで、そのメンバである t には関数内部で生成したフレームバッファ用のキューブマップテクスチャが入っています。

また、繰り返し処理が始まる前に定義されている配列 eye はカメラの注視点を指定するために使います。同様に camUp はカメラの上方向を表し pos はレンダリングされるトーラスのワールド空間上の座標位置を表します。トーラスには 6 方向それぞれで違った色を塗りたいので配列 amb を使って色を指定できるようにしています。

ビュー座標変換とプロジェクション座標変換の行列を生成する際には、先に指定した eye camUp の中身を参照しながら、適切にカメラやスクリーンの設定をしていきます。

座標変換行列の生成

// ビュー×プロジェクション座標変換行列
m.lookAt([0, 0, 0], eye[i], camUp[i], vMatrix);
m.perspective(90, 1.0, 0.1, 200, pMatrix);
m.multiply(pMatrix, vMatrix, tmpMatrix);

カメラは原点に置き、注視点やカメラの上方向は事前に格納してあった配列の値を使います。また、画角を 90 度に設定して、スクリーンのアスペクト比は 1.0 にします。

また、コードをよく観察するとわかるのですが、前後と左右の方向をレンダリングする際の、カメラの上方向の設定には注意してください。一見するとカメラが逆さまになっているように見えると思いますが、このように設定してやることで正しくキューブマップ用の風景を撮影できます。

また、背景のレンダリングやトーラスのレンダリングでは、シェーダを切り替えながら処理していることにも注意しましょう。利用するプログラムオブジェクトを指定する useProgram メソッドが実行されている部分で、シェーダの切り替えが行なわれています。

また途中で視線ベクトルを反転させる処理が入っていますが、これは原点から見たときの正しい照明効果を得るために行なっているもので、シェーダへ送る視線ベクトルとして使われます。

まとめ

フレームバッファへの各面のレンダリングが済むと、あとは通常のキューブ環境マッピングと同様のレンダリングを行なうだけになります。

ここでは、まず普通に生成しておいたキューブマップテクスチャを使って背景をレンダリングします。その後、六つのトーラスと、動的キューブマップを適用した球体をレンダリングします。

今回の動的キューブマッピングでは、キューブマップを動的に生成するために背景を 6 回、トーラスを 6 回レンダリングしますね。さらに、最終的なシーンをレンダリングするために背景を 1 回とトーラスを 6 回、さらに球体を 1 回レンダリングします。

たった一つのシーンをレンダリングするために、これほど多くの描画命令を発行しなければならないとなると、やはりあまりオブジェクトの多いシーンでは使いにくいのかなぁという印象を持ちます。ただ、少なくとも今回のサンプルはそれほど重いとは感じませんし、その割に見ごたえはあると思います。

かなり長くなるのでソースコードの全文掲載は行ないませんが、なにをやっているのかさえわかれば、十分に解析できる範囲だと思います。実際に動作するサンプルを見ながら、分析してみてください。

ちなみに、今回のサンプルでレンダリングされるトーラスは、POSITIVE 方向のものが明るい色で、NEGATIVE 方向のものが暗い色でレンダリングされるようになっています。さらに、X 方向が赤系、Y 方向が緑系、Z 方向が青系で着色されていますので、それを踏まえてサンプルを見ていただけるとわかりやすいかと思います。

サンプルへのリンクはいつものように以下にあります。

entry

PR

press Z key