ホーム < ゲームつくろー! < DirectX技術編

その47 カメラのようにキャラクタを向かせたい!


 Direct3DXの関数の中にD3DXMatrixLookAtLH関数があります。これはカメラに関係する関数で、カメラの位置とカメラが見つめたい位置からビュー行列を作成してくれます。大変便利です。

 この「位置」と「視点」という考え方は、キャラクタの向きを変える方法論として大変重宝しますが、D3DXMatrixLookAtLH関数を直接使う事はできません。それはD3DXMatrixLookAtLH関数が「原点にいるZ軸方向を向くカメラの前にオブジェクトを移動させる」という「俺様」な行列を作ってしまうためです。

 ここではD3DXMatrixLookAtLH関数の考え方を流用して、キャラクタがちゃんとある点に向く関数を作成してみましょう。D3DXMatrixLookAtLH関数を何となく使っている人は、この章を見れば彼がしている事が良くわかると思いますよ(^-^)



@ キャラクタの視線は常にZ軸

 Direct3Dは「左手系」です(DirectX技術編その15参照)。画面の右方向をX軸、上方向をY軸とした時に、Z軸は手前から画面の奥に向かいます。これはゲームとして矛盾を無くすための賢い選択です。FPS(一人称視点STG)やRPGなどでフィールドを移動する時、プレイヤーはキャラクタの背中を見ています。そして、パッドの上ボタンを押せば「奥」に進みます。左手系だと矛盾が無いんです。この考え方を踏襲するために、キャラクタモデルを作る時にはZ軸方向がそのキャラクタの正面であると作るのが大切です。

 この前提を元に、キャラクタをある点に向ける方法を考えてみましょう。



A キャラクタが向く理屈

 わかってしまえばとても単純な話です。キャラクタの現在の位置を(Px,Py,Pz)としておきます。そして、キャラクタを(Ax,Ay,Az)を見るように向かせたいとします。@で説明したように、これがキャラクタのZ軸となります(正しくはローカル座標のZ軸ベクトルがワールド空間で向く方向)。ただ、このままだとキャラクタの姿勢が確定しません(DirectX技術編その16参照)。それは、向く軸が決まっただけで、軸を中心としてくるくる回転できてしまうからです。キャラクタの姿勢までもビシッと定めるには、「もう1本の軸」がどうしても必要になります。それが「空方向」です。

 通常、空方向はワールド空間座標のY軸(0,1,0)です。ちょっと紛らわしいので空方向ベクトルをUとしておきます。空方向Uがわかると何がわかるのか?「そりゃ空の方向でしょう」、と思われるかもしれませんが、本質はちょっと違います。空方向がわかると、実は「水平」がわかるんです。イメージし難い人は、空方向ベクトルUが「=平面の法線」だとすれば、納得するのではないでしょうか。平面の傾きは法線ただ1つで決まります。

 空方向Uに垂直なベクトルは、上の考えから水平平面と平行、もっと分りやすく言えば地面と平行になることがわかりますね。そりゃ当然です。では、空方向Uとキャラクタが向くZ軸。この両方に垂直なベクトルも、地面と平行になるのがわかりますでしょうか?それは少なくともUと垂直だからです。そして、そのベクトルは向きであるZ軸とも垂直です。Z軸に垂直なベクトルはX軸かY軸しかありません!水平線に平行な軸は、通常X軸です。つまり、ここが大切ですよ、「空方向Uとキャラクタが向きたい任意のZ軸に垂直なのはX軸だ!」となるわけです。

 Z軸もX軸も定まりました。となると後は簡単で、両方の軸に垂直なのがY軸となります。これで、ローカル座標でZ軸を向いているキャラクタを、ワールド空間の任意の方向に向かせる準備が整いました。「各軸の向きはわかったけど、それをどうワールド変換行列にするの?」と思われた方は、是非DirectX技術編その39「知っていると便利?ワールド変換行列から情報を抜き出そう」をご覧下さい。3軸の向きがわかると、回転行列が作れてしまう事が記載されています。


B 回転行列を作ろう

 Aの理屈を元に、回転行列Rを作ってみましょう。

 まず入力値は、

・ キャラクタのいるワールド位置 P(Px,Py,Pz)
・ キャラクタが向きたい注視点 A(Ax,Ay,Az)
・ 空の方向 U(Ux,Uy,Uz)

です。最初に決まるのはZ軸の方向ですね。これは、(注視点-自分の位置)ですから、

です。次に決まるのは、空方向とZ軸方向の両方に垂直であるX軸です。2つのベクトルに垂直なベクトルは外積で一発ですね:

外積の向きは左手系なのでU(親指)からZw(人差し指)です。2軸が定まったので、Y軸は、

と算出されます。あ、忘れていましたが、上のXw、Yx、Zwは計算後に必ず標準化してください。そうしないとうまくいきません。回転行列だけならばこれで十分です。ついでに位置もという人はPを使って行列を作ってみてください。位置も考えたワールド変換行列は、結局次のようになります:

わかってしまうと、理屈は簡単ですね。上の行列だとスケールを入れるのが面倒にはなりますので、回転行列とオフセットは分けた方が良いかもしれません。



C キャラクタは常に水平にありたい時

 Bで導いた行列を使うと、キャラクタはその向きに素直に向こうとします。しかし、地面に立つべきキャラクタが坂道でも登っているかのような姿勢になるのは当然不自然です。下に重力があるのなら、キャラクタは基本的に水平を保っておきたいわけです。つまりY軸回転のみでありたいというわけです。

 ポイントは「キャラクタの上方向と空方向が一致する」という事です。つまり、上の縛りを入れている時は、すでにY軸が決定しているわけです。これは振り向き方を束縛します。ある位置にいるキャラクタがある点を注視しようとするのですが、Y軸回転縛りがあるので、その1軸回転でしか振り向けません。この動作は、何だか先程出てきた「Z軸が決まっている時に空方向Uで姿勢がきまる」という流れとそっくりだと思いませんか?その通りでして、向きたいベクトルDとY軸との外積を取ると、それはX軸となります!そこから本当に向く事が可能なZ軸も一つ決まってしまうわけです。この行列の方が使い勝手が良いかもしれません。



D 公開、キャラクタ姿勢行列算出関数

 ここまでの考え方に基づいたキャラクタをカメラのように向かせるワールド変換行列を算出する関数を公開します。ここでは縛り無しで向くCalcLookAtMatrix関数と、CalcLookAtMatrixAxisFix関数を公開します。これらの行列は回転行列として働きますので、位置は皆さんがさらに付記(行列掛け算)して下さい:

// キャラクタ姿勢行列算出関数
D3DXMATRIX* CalcLookAtMatrix(
   D3DXMATRIX* pout,
   D3DXVECTOR3* pPos,
   D3DXVECTOR3* pLook,
   D3DXVECTOR3* pUp
) {
   D3DXVECTOR3 X, Y, Z;
   Z = *pLook - *pPos;
   D3DXVec3Normalize( &Z, &Z );
   D3DXVec3Cross( &X, D3DXVec3Normalize(&Y, pUp), &Z );
   D3DXVec3Normalize( &X, &X );
   D3DXVec3Normalize( &Y, D3DXVec3Cross( &Y, &Z, &X ));


   pout->_11 = X.x; pout->_12 = X.y; pout->_13 = X.z; pout->_14 = 0;
   pout->_21 = Y.x; pout->_22 = Y.y; pout->_23 = Y.z; pout->_24 = 0;
   pout->_31 = Z.x; pout->_32 = Z.y; pout->_33 = Z.z; pout->_34 = 0;
   pout->_41 = 0.0f; pout->_42 = 0.0f; pout->_43 = 0.0f; pout->_44 = 1.0f;

   return pout;
}


// キャラクタ束縛姿勢行列算出関数
D3DXMATRIX* CalcLookAtMatrixAxisFix(
   D3DXMATRIX* pout,
   D3DXVECTOR3* pPos,
   D3DXVECTOR3* pLook,
   D3DXVECTOR3* pUp
) {
   D3DXVECTOR3 X, Y, Z, D;
   D = *pLook - *pPos;
   D3DXVec3Normalize( &D, &D );
   D3DXVec3Cross( &X, D3DXVec3Normalize(&Y, pUp), &D );
   D3DXVec3Normalize( &X, &X );
   D3DXVec3Normalize( &Z, D3DXVec3Cross( &Z, &X, &Y ));

   pout->_11 = X.x; pout->_12 = X.y; pout->_13 = X.z; pout->_14 = 0;
   pout->_21 = Y.x; pout->_22 = Y.y; pout->_23 = Y.z; pout->_24 = 0;
   pout->_31 = Z.x; pout->_32 = Z.y; pout->_33 = Z.z; pout->_34 = 0;
   pout->_41 = 0.0f; pout->_42 = 0.0f; pout->_43 = 0.0f; pout->_44 = 1.0f;

   return pout;
}



E カーソルで指したくなります

 上の関数群は私がテストした範囲ではうまく動きました(法線が算出できない場合のエラー処理は抜けていますが)。確かに指定した方向にほいほい向いてくれてちょっとうれしくなります。ただ、そうなると、今度は「カーソルで注視点を指したい」と思ってしまうのが人情です。

 これについては次章で考えてみる事にしましょう。