クォータニオンとビルボード
今回のサンプルの実行結果
ビルボードとは
前回はクォータニオンを用いた球面線形補間を行ないました。二つの異なる回転から、時間値を指定して補間した新しい回転を得られる球面線形補間は、クォータニオンの活用例として代表的なものです。
今回は、クォータニオン関連の処理の集大成として、クォータニオンによるビルボード処理を行なってみます。
ビルボード(billboard)は、直訳すると[ 看板 ]などとなる言葉です。ビルボードの歴史は結構古く、昔からよく使われてきましたテクニックなのですが、まずはビルボードとはなんなのかから解説しましょう。
ビルボードとは、カメラの視線ベクトルに対し常に垂直な姿勢を持つようにモデルをレンダリングすることを言います。言い換えると、常にスクリーンに対して平行であるとも言えます。
たとえば、板状のポリゴンにテクスチャを貼っている状況を考えます。
レンダリングされているのはあくまでも板状のポリゴンですから、カメラがポリゴンの正面にあるときはテクスチャがちゃんと見えますね。
しかしカメラがポリゴンを真横から見つめていた場合はどうでしょうか。この場合は、当然ポリゴンに貼り付けられたテクスチャ(というかポリゴンそのもの)は見えなくなってしまいます。
カメラのある方角に向かって、常に板ポリゴンが向きを変えてくれるようにすれば、カメラがどの位置にあっても常にテクスチャが見える状態を作ることができます。これが、ビルボードです。
ビルボードを活用すると、あたかもそこにモデルがレンダリングされているかのように見せることができたり、炎や光のエフェクトを簡単に実装することができたりします。
たとえば三次元空間上に、地面と、一本の木をレンダリングしようとしているとします。このとき、木を真面目にレンダリングしようとすれば、まず木のモデルデータをモデリングツールなどで用意して、そこから VBO や IBO を準備して……となかなか面倒です。また、木のモデルデータが大量の頂点からなる場合には、レンダリングの際の負荷も高くなってしまいます。
しかし、ここでビルボードを活用するとすれば、必要なのは木が描かれたテクスチャと、板ポリゴンのための四つの頂点だけで済みます。
ビルボードは常にカメラの方角を向くため、テクスチャに描かれた木の姿が、あたかもそこにあるかのようにカメラからは見えるわけですね。
ビルボードにはもちろんデメリットもあって、カメラがビルボードに近づきすぎると、それが板ポリゴンであることがバレバレになってしまいますし、なにより同じテクスチャを眺め続けることになるので、それなりに不自然ではあります。
しかし、その処理の軽さや実装の簡単さから、ビルボードは今も昔もよく使われる技術です。工夫次第で、それがビルボードであるということを感じさせないような、非常に自然なレンダリング結果を得ることもできるはずです。
どうビルボードを活用するかはさておき、まずはその実装方法をしっかりと習得しましょう。
ビルボードの概念
ビルボードを実装するには、レンダリングされるモデルが常にカメラの視線ベクトルに対して垂直な姿勢を保つようにする必要があります。
カメラに関する一切を決めているのは、各座標変換のうち、ビュー座標変換でしたね。このビュー座標変換をうまく活用することができれば、ビルボードが実現できそうです。
ビュー座標変換行列には、カメラをどのような姿勢で、どのような場所に置かれているのか、という情報が含まれています。これらの情報のうち必要な物だけを抜き出してモデル座標変換に流用します。
たとえば、カメラが座標(x, y, z)にあり、原点(0, 0, 0)を見つめている場合を考えます。
上記の図のような状況のとき、ビルボードはカメラの方を向かなければならないわけですから、ピンク色の矢印で表されているカメラの視線ベクトルと、まるっきり逆のベクトルを適用しなければなりませんね。
そこで、通常ビュー座標変換行列を生成するのに使っている minMatrix.js (もしくは minMatrixb.js)に実装されている matIV.lookAt
メソッドを使って、カメラの視線ベクトルの真逆のベクトルを持つ行列を生成します。
カメラの視線ベクトルと逆の視線ベクトルを持つ行列
// カメラの座標位置
var camPosition = [0.0, 5.0, 10.0];
// ビュー座標変換行列
m.lookAt(camPosition, [0, 0, 0], [0, 1, 0], vMatrix);
// ビルボード用のビュー座標変換行列
m.lookAt([0, 0, 0], camPosition, [0, 1, 0], invMatrix);
このように、カメラの座標とカメラの注視点を入れ替えた形で matIV.lookAt
を呼び出せば、逆の視線ベクトルを持つ行列が生成できます。下の図で言うところの、青い矢印のベクトルを持つ行列が取得できるわけですね。
しかし、これだけではまだビルボードは実現できません。なぜなら、カメラに与えられている回転を加味できていないからです。ビルボードの向くべき方角は既にわかっていますので、後はビルボードを実際に回転させてやる行列が必要です。そこで、先ほど取得したビルボード用のビュー座標変換行列から逆行列を生成して、これをビルボードのモデル座標変換に使います。
逆行列の生成は、ライト関連の処理でも使った matIV.inverse
メソッドで簡単に行なえます。
逆行列の生成
// ビルボード用ビュー行列の逆行列を取得
m.inverse(invMatrix, invMatrix);
これで逆行列が生成されたので、あとはこれをビルボードとしてレンダリングするモデルの、モデル座標変換に適用してやれば OK です。
どうして逆行列を使うのかを疑問に思うかもしれませんが、たとえば、カメラが Y 軸を中心として 45 度回転しているとします。このとき、ビルボードとしてレンダリングされるモデルは、逆方向に 45 度回転した状態になっていなければなりません。
カメラの回転を相殺するために、逆行列が必要なんですね。
サンプルに関する補足
今回のサンプルは、当サイトのここまでのテキストで扱ってきた様々なテクニックを、いろいろと織り交ぜた内容となっています。
概要をざっと並べてみます。
- ライティングは使わない
- テクスチャを二枚使う
- アルファブレンディングを有効にする
- 一つの VBO を使いまわして二つのモデルを描画
- マウス座標を取得してカメラを回転
- カメラの回転にはクォータニオンを利用
- そのカメラの回転を加味してビルボードを描画
だいたいこんな感じです。
また、サンプルではビルボードをオン・オフできるように、HTML 内にチェックボックスを設けています。
まずはシェーダですが、これは以前テクスチャを扱ったときのものをほとんどそのまま使います。一応、ソースを載せておきます。
サンプルの HTML ソース
<html>
<head>
<title>wgld.org WebGL sample 022</title>
<script src="minMatrixb.js" type="text/javascript"></script>
<script src="script.js" type="text/javascript"></script>
<script id="vs" type="x-shader/x-vertex">
attribute vec3 position;
attribute vec4 color;
attribute vec2 textureCoord;
uniform mat4 mvpMatrix;
varying vec4 vColor;
varying vec2 vTextureCoord;
void main(void){
vColor = color;
vTextureCoord = textureCoord;
gl_Position = mvpMatrix * vec4(position, 1.0);
}
</script>
<script id="fs" type="x-shader/x-fragment">
precision mediump float;
uniform sampler2D texture;
varying vec4 vColor;
varying vec2 vTextureCoord;
void main(void){
vec4 smpColor = texture2D(texture, vTextureCoord);
gl_FragColor = vColor * smpColor;
}
</script>
</head>
<body>
<canvas id="canvas"></canvas>
<p>
<input id="check" type="checkbox" checked> billboard
</p>
</body>
</html>
頂点情報としては、頂点の位置と色、そしてテクスチャ座標を attribute
変数として受け取ります。そのほか uniform
変数としては座標変換行列と、テクスチャ周辺の情報を受け取ります。まぁ、そんなに難しいことはやってませんね。
さて、続いては javascript のほうですが、ここは要点を絞ってコードを掲載しながら解説します。
まず、今回のサンプルはではマウス座標を取得して、そこからクォータニオンを生成、さらにそのクォータニオンを使ってカメラを操作、という手順を行ないます。ですから、canvas エレメントやクォータニオンをグローバルな変数として宣言しておきます。
参考:(wgld.org | WebGL: マウス座標による回転 |)
また、今回使用するテクスチャ用の画像は、アルファ付き PNG 画像フォーマットです。画像に含まれているアルファ値を使って、そのままアルファブレンディングを行ないますので、ブレンディングを有効にし、ブレンドファクターを設定しておきます。
アルファブレンディングのブレンドファクター
// ブレンドファクター
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE);
尚、ブレンドファクターについては以前のテキストで詳しく解説していますので、よくわからないという人は復習しておきましょう。
参考:(wgld.org | WebGL: ブレンドファクター |)
メインプログラムの中では、二つのテクスチャをあらかじめ生成しておき、ループ内でテクスチャを切り替えながらレンダリングします。テクスチャに関する処理も以前のテキストで詳細を扱っていますのでそちらを参考に。
参考:(wgld.org | WebGL: マルチテクスチャ |)
尚、今回利用するテクスチャは以下の二枚。いずれも PNG フォーマットで、球体が描かれているテクスチャは背景が透過処理されているアルファ付き PNG です。
texture0.png
texture1.png
さて、それでは恒常ループのなかの処理を見てみます。
恒常ループの内部では、とにかく行列関連の処理が複雑になっています。大雑把にですが流れを列挙すると次のような手順です。
- クォータニオンから回転行列を生成
- 通常のビュー座標変換行列を生成
- ビルボード用のビュー座標変換行列を生成
- 通常のビュー座標変換行列を回転
- ビルボード用のビュー座標変換行列を回転
- ビルボード用の行列から逆行列を生成
- テクスチャや行列を切り替えつつレンダリング
クォータニオンを使って生成した回転行列を、二つのビュー座標変換行列に適用しているわけですね。
だいたいの流れを掴んだら、以下のループ内処理のコードを見てみましょう。
恒常ループ
// 恒常ループ
(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);
// クォータニオンを行列に適用
var qMatrix = m.identity(m.create());
q.toMatIV(qt, qMatrix);
// カメラの座標位置
var camPosition = [0.0, 5.0, 10.0];
// ビュー座標変換行列
m.lookAt(camPosition, [0, 0, 0], [0, 1, 0], vMatrix);
// ビルボード用のビュー座標変換行列
m.lookAt([0, 0, 0], camPosition, [0, 1, 0], invMatrix);
// ビュー座標変換行列にクォータニオンの回転を適用
m.multiply(vMatrix, qMatrix, vMatrix);
m.multiply(invMatrix, qMatrix, invMatrix);
// ビルボード用ビュー行列の逆行列を取得
m.inverse(invMatrix, invMatrix);
// ビュー×プロジェクション座標変換行列
m.perspective(45, c.width / c.height, 0.1, 100, pMatrix);
m.multiply(pMatrix, vMatrix, tmpMatrix);
// フロア用テクスチャをバインド
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texture1);
gl.uniform1i(uniLocation[1], 1);
// フロアのレンダリング
m.identity(mMatrix);
m.rotate(mMatrix, Math.PI / 2, [1, 0, 0], mMatrix);
m.scale(mMatrix, [3.0, 3.0, 1.0], mMatrix);
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
gl.uniformMatrix4fv(uniLocation[0], false, mvpMatrix);
gl.drawElements(gl.TRIANGLES, index.length, gl.UNSIGNED_SHORT, 0);
// ビルボード用テクスチャをバインド
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture0);
gl.uniform1i(uniLocation[1], 0);
// ビルボードのレンダリング
m.identity(mMatrix);
m.translate(mMatrix, [0.0, 1.0, 0.0], mMatrix);
if(eCheck.checked){m.multiply(mMatrix, invMatrix, mMatrix);}
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
gl.uniformMatrix4fv(uniLocation[0], false, mvpMatrix);
gl.drawElements(gl.TRIANGLES, index.length, gl.UNSIGNED_SHORT, 0);
// コンテキストの再描画
gl.flush();
// ループのために再帰呼び出し
setTimeout(arguments.callee, 1000 / 30);
})();
各種行列の処理、テクスチャの処理などを経て、まずは床となる板ポリゴンを先にレンダリングしていますね。
その後、その床の少し上のあたりに、ビルボード用の逆行列を適用した板ポリゴンをレンダリングしています。ビルボード処理をオン・オフできるようにしているため、ビルボード用の逆行列を適用するかどうかは if
文で分岐しています。
まとめ
ビルボードは少ない頂点数で処理できる点や、ある程度実装が簡単である点がメリットとなり、様々なシーンで利用されている技術です。
ただし、行列に関わる処理が冗長になりがちなので、大量のモデルをビルボードとして処理してしまうと、逆に負荷が高くなってしまう可能性もあります。また、使う場面を間違えると、それがビルボードであることがバレバレになってしまい、シーンをぶち壊してしまう可能性もあります。
用途を正しく選んで、適宜利用するというのがポイントになるでしょう。
また、OpenGL にはポイントスプライトという概念があり、これがビルボードの代替として広く利用されているという背景もあります。今後のテキストではこのポイントスプライトについても扱っていければと思っています。
サンプルへのリンクはいつものようにテキストの最後にあります。実際に動作するものを見るとなかなか面白いので、是非ご覧になってみてください。