ホーム < ゲームつくろー! < DirectX技術編 < クォータニオンを学んでみよう!
その10 クォータニオンを学んでみよう!
@ What is Quaternion ?
クォータニオン(Quaternion)とは日本語で「4元数」と訳します(アルク:http://www.alc.co.jp/)。数字が4つ集まったもので、言ってみれば4次元ベクトルです。3次元ベクトルであれば縦横高さで何となく想像ができますが、4次元となるともうドラえもんしかわかりません(笑)。この原稿を書いている私も、実は何のことやらさっぱり。そこで、私と同じような境遇にいる皆さんにも理解できるように、このクォータニオンを1から学んでみようと思います。
クォータニオンについてマイクロソフトのHPに一通りの説明がありました(http://www.microsoft.com/japan/msdn/academic/Articles/DirectX/01/)。より理解を深めるためにも一読をお勧めします。
DirectX9のマニュアル(Direct3Dの基礎知識→3D座標系とジオメトリ)からクォータニオンについて抜粋してみます。
クォータニオンは、3 成分ベクトルを定義する [x, y, z] 値に第 4 の成分を追加する。クォータニオンは、3D 回転で一般的に使われる行列手法に代わるものである。クォータニオンは、3D 空間内の軸と、その軸を中心とする回転を表す。たとえば、1 つのクォータニオンで軸 (1,1,2) と 1 ラジアンの回転を表すことができる。クォータニオンは重要な情報も伝えるが、その真価は、クォータニオン上で実行できる 2 つの処理、合成と補間で発揮される。 |
太文字の部分が超重要です。これによるとクォータニオンを用いると、原点から任意の方向に引っ張った回転軸を中心としたオブジェクトの回転を表現できるようです。これはすばらしい!下の太文字も魅力的な事を言っています。想像するに、あるベクトルから別のベクトルへ変換する事ができそうです。これは例えば、ある方向に飛んでいる飛行機を別の方向に向かせたい時などに使えそうです。また、ある位置にある物体の方向を見るためにどうベクトルを変化させるかという悩ましい問題を解決するのにも使えそうですね。面白くなってきました!
A クォータニオンの基礎の基礎
DirectXのマニュアルからわかることはこの位でして、魅力的ですが使い方は教えてくれません。ここはWebを辿るとしましょう。わかりやすいページがありました(http://staff.aist.go.jp/toru-nakata/quaternion.html)。筆者様、非常に勉強になりました。ありがとうございます。このページによりますと、クォータニオンQとは1つの実部(実数)と3つの虚部(虚数)で構成されていて、3次元空間の座標は、
Q = (0; x, y, z)
と表すようです。ここでセミコロンの左が実部、x,y,zは虚部です。虚部なんか出てきてうわ〜っと思ってしまいそうですが、中身の細かい事や証明はこの際抜きにしましょう。
クォータニオンである点を回転させるには、クォータニオン同士の掛け算が必要になるそうです。クォータニオンQ1とQ2を、
Q1 = (t1; x1, y1, z1)=(t1; V1)
Q2 = (t2; x2, y2, z2)=(t2; V2)
と表すとすると、その掛け算は、
Q1*Q2 = (t1t2-V1・V2; t1V2+t2V1+V1×V2)
と表されるのだそうです。ここでV1、V2は3D空間の座標で、t1やt2は何らかの実数です。この掛け算が実は任意軸による回転に必要なのですが、注目するのはクォータニオン同士の掛け算が、ベクトルの内積と外積のみで計算が出来るという点です。
B クォータニオンによる任意軸回転
この知識だけでもう任意軸回転が出来ます!まず、回転させたい点を、
P = (0; xp, yp, zp)=(0; Vp)
と置きます。次に、回転軸の方向を表すベクトルを、
v = (xv, yv, zv)
とし、回転させたい角度をθと表すことにします。ただし|v|=1です。このvから次に2つのクォータニオンを作成します。
Q = (cos(θ/2); xv・sin(θ/2), yv・sin(θ/2), zv・sin(θ/2))
R = (cos(θ/2); -xv・sin(θ/2), -yv・sin(θ/2), -zv・sin(θ/2))
そして次のような掛け算を実行します。
R*P*Q = (0; x, y, z)
これだけで、点Pをベクトルvを向いた軸周りにθ回転させた点の座標が得られるのです!式を見るとcos(θ/2)とsin(θ/2)しかないですし、後は3次元の内積と外積を2回ずつやるだけです。これは非常に簡単で高速です。
C Direct3DXヘルパー関数でクォータニオンからワールド変換行列を作成
この段階で、クォータニオンによる回転を実際に使用する事を考えた時、「おや?」っと思いました。Direct3Dで点の座標変換を行うのはワールド変換行列です。D3Dによる描画は、ワールド変換→ビュー変換→射影変換という王道を辿るはず。では上の回転軸による頂点変換はどう組み込めば良いのでしょうか?
「頂点を回転させた後、回転をしないワールド変換行列で残りのスケーリングと平行移動をしたらいいのでは?」と考えるかもしれません。しかしこれはNG。なぜなら、ローカル座標にある頂点を直接変更する事になるからです。ローカル座標の頂点はアニメーションをしないのであれば不動であるべきで、これが変更されると元に戻すのが大変なのです。
実は、Direct3DXにはクォータニオンからワールド変換行列を作成する涙が出るほどうれしいヘルパー関数がいくつかあります。まずごっつい方から行きましょう。D3DXMatrixTransformation関数です。この関数の中身を見てみましょう。
D3DXMatrixTransformation関数 D3DXMATRIX *D3DXMatrixTransformation(
D3DXMATRIX *pOut,
CONST D3DXVECTOR3 *pScalingCenter,
CONST D3DXQUATERNION *pScalingRotation,
CONST D3DXVECTOR3 *pScaling,
CONST D3DXVECTOR3 *pRotationCenter,
CONST D3DXQUATERNION *pRotation,
CONST D3DXVECTOR3 *pTranslation
);
引数がたくさんあって目が回りそうですが頑張ってみましょう。
pOutに取得したいワールド変換行列が返ります。
pScallingCenterというのはスケーリングの中心点です。スケーリングは原点以外でもできるんですね。ただ、ワールド変換行列においては通常原点中心のスケーリングとなりますから、ここはNULLにします。
pScalingRotationはスケーリングの回転を指定します。スケーリングの回転というのも変な表現です。通常のワールド変換行列はXYZ軸に対してスケール値が設定されますね。このXYZ軸自体がぐるぐると回転している状態を想像して頂くとわかりやすいかと思います。変な事をする時以外はここもNULLで結構。
pScallingは軸ごとのスケール値を設定します。これはわかりますね。
pRotationCenterは回転の中心を指定します。回転の中心・・・マニュアル何とかしてほしいです。これは、回転軸の原点の平行移動を意味しています。Bで説明した回転はあくまでも原点を通る直線なのですが、この原点を別の点にしたいときにこの引数を設定します。NULLにすると原点が使用されます。
pRotationには回転軸と回転角度を指定します。ここが肝なんですが、指定方法が独特なので後述します。
pTranslationはスケーリングと回転が終わった頂点をさらにオフセット(平行移動)する時に使います。ワールド座標への配置はこれで行います。
つまり、この関数はスケーリングの座標、回転の座標、オフセットの座標をまったく別物として設定できるわけです。恐ろしきかな行列・・・。ただ、一般的な使用に関して言えば、スケーリングは原点を中心に軸平行、回転軸の中心も原点という場合が殆どですから、設定するところはpScallingとpRotationとpTranslationだけです。
クォータニオンによる回転軸と回転角度の指定はちょっと面倒でして、ここはヘルパー関数に任せてしまいます。D3DXQuaternionRotationAxis関数を用います。この関数は指定のクォータニオンを回転した結果を返してくれます。
D3DXQuaternionRotationAxis関数 D3DXQUATERNION *D3DXQuaternionRotationAxis(
D3DXQUATERNION *pOut,
CONST D3DXVECTOR3 *pV,
FLOAT Angle
);
pOutには回転させたいクォータニオンを入れます。すると演算結果がここに返ってきます。InとOutが一緒になっているわけです。
pVには回転軸の方向ベクトルを与えます。
Angleには回転軸に対する回転角度を与えます。
pOutに単位クォータニオン(x,y,z,w)=(0,0,0,1)を入れて軸と角度を設定すると、欲しいクォータニオンが取得できます。
これでクォータニオンによる回転を用いたワールド変換行列が一気に作成できます。
余談ですが、D3DXMatrixTransformation関数についてDirectX9の日本語マニュアルは酷いですね。例えばpScallingRotationについての説明はこうなっています。 「Pointer to a D3DXQUATERNION structure that specifies the scaling rotation. If this argument is NULL, an identity Msr matrix is applied to the formula in Remarks.」 これを素直に訳すと、 「スケーリングの回転を指定する D3DXQUATERNION構造体へのポインタ。この引数がNULLの場合、(後述の)注意内の式にあるMsrには単位行列が適用される」 です。単位行列(identity matrix)が指定されると言う事はその回転行列はなんら作用しない、あっても無くても同じと言う事でして、決して「スケールされない」というわけではありません。日本語訳ですとpScallingRotationを指定しないとスケールが無効になるかのような表現になっています。最初これを見たときにはこの関数が理解不能に思えてしまってえらく当惑しました。しかし、英語版を見て納得です。素直に上のような訳にしてもらった方がユーザとしては誤解を招かないと思うのですが・・・。 |
さて、D3DXMatrixTransformation関数を用いても良いのですが、明らかに計算量が多そうです。もっとシンプルに作成したい場合はD3DXMatrixRotationQuaternion関数を用います。まずはこの関数を見てみます。
D3DXMatrixRotationQuaternion関数 D3DXMATRIX *D3DXMatrixRotationQuaternion(
D3DXMATRIX *pOut,
CONST D3DXQUATERNION *pQ
);
えらくシンプルです。
pOutにはクォータニオンによって定義される回転行列が返ります。
pQが回転を定義するクォータニオンです。
まずpQを与えると回転行列が算出されてきます。後はワールド変換行列を作成するときに、この行列を掛け算してあげればよいのです。こちらの方がはるかに簡単ですし、無駄な計算がない分高速です。
D 軸回転指定か任意軸回転指定か
Cまでで通常の軸回転とクォータニオンによる任意軸回転という2つの回転方法でワールド変換行列を作成できる事がわかりました。どちらが使いやすいか?それは状況によって異なると思われますが、私はクォータニオンによる任意軸回転が良いかと思います。これまで軸回転を扱ってきて、正直「わかりにくい」という感覚がありました。想像してみてください。歩いていて鳥が空を飛んでいる。そちらへ目線を向けたいとき、どの軸をどう回転させれば良いのか?普通簡単には思いつきません。でもクォータニオンによる任意軸回転なら鳥の方向を指すベクトルさえ計算すればそちらを向くワールド変換行列をすぐに作成できます。直感的でわかりやすいのです。
もう1つ重要なのは、任意軸回転には「ジンバルロックがない」という利点があります。ジンバルロックとは回転軸が重なってしまうことによって特定の向きにしかオブジェクトを回せなくなる現象です。これ、私は始め何ぼWebサイトを検索しまくって頭で考えても理解が出来ませんでした。「回転軸が重なる」という意味がわからなかったんです。だって、3軸がどれだけ回転しようとも、各軸は直行するんだから・・・と考えてしまったわけです。答えがはっきりしたのが、「GAME CODING Vol.1 Direct3D/COM編(鎌田茂雄著)」を読んだ時で、この本によりますと、ジンバルロックはジャイロスコープ(姿勢安定装置)のジンバルという部品がロックしてしまう現象から来ているようです。ジャイロスコープとはこんな感じです。
3つの輪が軸で回転できる仕組みになっていまして、真ん中の赤い輪を色々な方向に回すと、緑や青の輪が一緒に動いてくれて、任意の姿勢を表現できる仕組みになっています。ところが、右の図にあるように、X軸が90度回転して青い輪と緑の輪が同一平面上に来てしまうと、Z軸とY軸が重なってしまうため、赤い輪をドアノブのように回すことが出来なくなってしまいます(わかるかなぁ(^-^;)。これがジンバルロックです。軸が直行した状態から始めれば任意の姿勢にできますが、「軸の回転を保存」するとジンバルロックになる可能性が生じてきます。角度を保存しても任意の別の姿勢に綺麗に変化できるクォータニオンは軸回転にはないアドバンテージがあります。
E 球面線形補間
クォータニオンを調べているとあちらこちらに「球面線形補間」という用語が出てきます。とあるページによりますと、これは「ある回転軸v1でθ1回っている状態を回転軸v2でθ2回っている状態に滑らかに変換する方法」なのだそうです。これだけだとイメージが難しいので気合を入れて図を作りました。
回転軸Aから回転軸Bへ移動させようと思ったとき、一番近いのはオレンジの矢印で示されている経路をたどることです。しかも、回転軸Aによって回転しているある一点(緑色の点)を回転軸Bの点に移動させるには、A自体を回転させてBの回転角度に滑らかに合わせる必要があります。これを軸回転で行おうと思っても非常に難しいのですが、球面線形補間はこの両方についてA→B間のクォータニオンを算出してくれるのです。驚くほど使える補間方法です!
「計算が難しくねぇか?」と思うかもしれませんが、実はそれほど面倒ではありません。しかもうれしい事に、Direct3DXにはちゃんとこれを実現するヘルパー関数があります。D3DXQuaternionSlerp関数です。この関数を見てみましょう。
D3DXQuaternionSlerp関数 D3DXQUATERNION *D3DXQuaternionSlerp(
D3DXQUATERNION *pOut,
CONST D3DXQUATERNION *pQ1,
CONST D3DXQUATERNION *pQ2,
FLOAT t
);
pOutに補間されたクォータニオンが返ってきます。
pQ1が始点のクォータニオン、pQ2が終点のクォータニオンです。
tは補間位置を表す数値で、0〜1を指定します。0がpQ1で1に近づくほどpQ2で示したクォータニオンに近くなっていきます。
たったこれだけでこのすばらしい補間が使えるなんて、本当にありがたいものです。pOutに戻ってきた補間クォータニオンはそのままD3DXMatrixTransformation関数に入れてワールド変換座標を作成できますから、さらに使い勝手が良いですね。
球面線形補間の使用例として、例えばアクションゲームや3DのSTGで自分の方に敵の銃口を向けさせる場合などがあります。これは軸座標でも可能かもしれませんが、球面線形補間を用いた方がはるかに簡単です。敵が今向いている方向ベクトルをA、敵と自分を結ぶ方向ベクトルをB(それぞれ正規化します)としてクォータニオンを作成してD3DXQuaternionSlerp関数に入れてtを0から1に変化させていけば良いだけです。この時この変化量が問題になりますが、これは2つの方向ベクトルの角度Wを内積で計算して、適当に決める変化量の単位(角速度)Uからt = U / Wと計算します。t>1ならt=1とすればOKです。
F クォータニオンによるビルボードのワールド変換行列作成
ビルボード(看板)はうまく使うと少ないポリゴンで画面に様々な効果を与える事が出来ます。例えばパーティクルによる火花。点ポリゴンで火花を作成する事も出来ますが、あのまぶしさがうまく出なかったりします。かといってライトを使うわけにも行きません(そもそもライト自体は発光しません)。こういう時に小さな板ポリゴンに光る「絵」を書いて加算合成を行うと美しく表現が出来ます。しかしここで問題になるのはカメラの視点が変わった時です。正面から見ると美しい火花も、側面から見ると板が丸出し。そこで、カメラの視線にいつもビルボードが向くようにすると、丸い火花のように人は錯覚して感じる事になります。
カメラの目線のベクトルは通常わかります。ということはビルボードの法線はカメラの目線と真逆になればよいわけです。ビルボードの法線をvn=(nx, ny, nz)、カメラの向いている方向をvc=(cx, cy, cz)とすると、
vn = (nx, ny, nz) = (-cx, -cy, -cz)
とカメラのベクトルにマイナスをつけるだけで法線の計算は終わりです。「じゃ、あとはこの法線に向けて板を回転させればよいわけだ」・・・となりますが、例えば、
vn = (-5, 8, 13)
のときに、XYZ軸を何度動かせば良いかわかりますでしょうか?三角関数を駆使しますか?違いますよね。ここまで読んで下さった方なら回転には「クォータニオンを使う」と返答するでしょう。
まずイメージ図を見てください。
オレンジ色の板ポリゴンをカメラの方向に向かせるには、2つのベクトルが含まれる平面に垂直なベクトル、すなわち法線ベクトルを軸としてベクトルがなす角度分だけ回転させればよいわけです。2つのベクトルに垂直な法線は外積で、成す角度は内積でそれぞれ計算できますね。
クォータニオンを作成して回転行列を作ります。
D3DXVECTOR3 LookAt(-cx, -cy, -cz); // カメラの目線の反対ベクトル(標準化済)
D3DXMATRIX TurnMat; // 回転行列
D3DXQUATERNION q; // 回転クォータニオン
D3DXVECTOR3 NAxis( 0, 1, 0); // ローカル座標でのビルボードの法線
D3DXVECTOR3 Normal;
// 回転軸を計算
D3DXVec3Cross(&Normal, &NAxis, &LookAt); // 法線を計算
// 回転角度を計算
float angle = acos(D3DXVec3Dot(&LookAt, &NAxis));
D3DXVec3Normalize(&Normal, &Normal); // 法線を正規化
q.x = q.y = q.z = 0.0f; q.w = 1.0f; // 単位クォータニオンに初期化
D3DXQuaternionRotationAxis( &q, &NAxis, angle); // クォータニオン作成
D3DXMatrixRotationQuaternion( &TurnMat, &q);
単位クォータニオンを作る事がポイントですね。後は、通常のワールド変換行列を作成するのとまったく同じように行列の掛け算を行えば、いつもカメラの方向を向くビルボードが完成です。ただ、実は上の実装だと確かにビルボードは正面を向くのですが、カメラの角度によってビルボードが目線軸を中心にくるくると回転してしまいます。これは3次元のベクトルだけで考えたためで、「カメラの上方向」と「ビルボードの上方向」が合っていないためです。よって、上の実装は正面に見えるビルボードが回転しても問題ない場合に使えます。
どうでしたでしょうか?クォータニオン、実に使えます。色々なサイトを調べていると、それなりの問題点もあるようなのですが、基本的な使い方をしていれば大きな問題は発生しません。何よりその利点が大きい。扱いもそれほど面倒ではありませんし、回転行列を作成できるので、ワールド変換行列への相性もばっちり。これまで足踏みしていた皆さんも、一緒にガシガシ使って行きましょう!