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

その56 カメラの視線で移動・回転してみよう


 ゲームの世界を切り取る「カメラ」。3Dのキャラクタが動き回る世界では、カメラは固定されているか、キャラクタの後ろもしくは側面を追従するのが通例です。固定カメラと側面追従カメラは大抵Y-UP(キャラクタの空方向と一致)です。後方カメラもY-UPでしょう。たまに真上からの映像になる事がありますが、この時はキャラクタの進行方向であるZ軸を上とするZ-UPカメラに変わったりもします。

 一方で、そういう感覚とはまた違うカメラもあります。代表例は戦闘機のコックピットから眺めたカメラです。これはもうどちらが空かもわからないような状態で空間中を縦横無尽に動き回ります。別の例で、モデルビューワで使われるカメラなどはある注視点を中心として自分が回転するような挙動になります。

 Upベクトルが固定しているカメラも、そうでないカメラもどちらも使い勝手がありますが、後者側のカメラについては前者側ほど情報が無いように思います。そこでこの章では、Upベクトルがめまぐるしく変わる自由度の高い「カメラの視線での移動・回転」について試行錯誤してみます。



@ カメラの見た目からさらに移動・回転

 主人公視点のFPSなどでは、カメラの見た目が中心です。例えば目の前に弾が飛んできた時、それをかわすためにコントローラの右をいれますね。ゲームをしていて極々自然な反応です。この時カメラはワールドでどの方向に移動するか?というと、これはXY平面に平行などの方向でも成り立ってしまいます。だって、動く方向は、カメラの見た目の角度で決まるわけですから:

 上の2台のカメラにとって、右方向とは赤い矢印(カメラのX軸)です。

 カメラの見た目で右に行きたい場合、この赤い軸に沿って動いてD3DXMatrixookAtLH関数を使って・・・とやりたくなりますが、ではカメラのZ軸でごろんと回転するというような動きが入るとか、首をうんうんと振るとか、複雑な動きが加わるとしたらどうでしょう。ワールド空間にあるZ軸は「任意軸」です。その回転となると・・・頭が痛くなってしまいます。

 カメラを通してみている世界では、世界の中心は自分自身、つまりカメラ自身であり、方向はカメラの軸が基本(基底)となります。この世界では、右に行くのは簡単です。世界を自分の左方向に動かせばいいんですから。Z軸でゴロンも簡単。Z軸回りの回転行列を作るだけです(回転方向は逆にします):


上図の左下カメラからの目線。カメラの世界では自分が右に行くことは
世界を「この世界で」左に動かす事と同じです。

 ワールドをカメラの世界に持っていくのは「ビュー行列」です。つまり、ビュー行列を通した後のモデルに対して、さらに左方向移動やZ軸回転を掛けると、それはカメラを通して見た世界の移動となります:

 こうして出来た「新しいビュー行列」をデバイスにセットすれば、カメラ自身の世界を中心として動くカメラの出来上がりです。この考え方はゲームを作る上でとっても大切です。



A 注視点を中心に自分を回転させてみる(球面束縛カメラ)

 モデリングツールなどでは、対象となるモデル(注視点)に対してカメラが人工衛星のようにぐるぐると回ります。この時、空方向、いわゆるUpベクトルが(0, 1, 0)で固定ならば、ID3DXMatrixLookAtLH関数を使えば望むビュー行列を得られます。しかし、UpベクトルがY-UPで固定されると、例えばモデルを真下や真上から見る事ができません。視線とUpベクトルが重なるために姿勢が定まらないためです。

 そこで、ここでは注視点を中心とする球面上を移動しながらその姿勢も刻々と変化させるカメラを考えてみます。


 ワールド空間にある半径rの球の中心に注視点があるとします。カメラはその球面上に位置するとします。考えやすくするために「注視点空間」というのを設けます。これは注視点が原点にある空間です。この空間で考えると、カメラのZ軸は自分の位置から常に中心の注視点に向くベクトルになります。
 注視点空間での位置座標とZ軸が定まると、カメラはある位置から注視点を見ながらZ軸回転ができるようになります。ただ、この段階ではカメラのX軸(右方向)及びY軸を定められません。そこで初期姿勢をプログラマが与えます。条件はZ軸に対して垂直である事だけです。
 カメラの初期位置と3軸の初期姿勢が決まると、そのカメラを中心とした緯度経度線を描く事ができるようになります。イメージするなら、カメラの今の位置を赤道上の経度0度の位置だと思って下さい。カメラのX軸は赤道に平行で、Y軸は経度線(子午線)に平行です。


 この初期状態をカメラ姿勢行列としてまとめます。3軸の姿勢(ワールド空間軸)を1〜3行目に打ち込みます。4行目は使いません:

D3DXMATRIX CamMat;
D3DXMatrixIdentity( &CamMat );
memcpy( CamMat.m[0], &D3DXVECTOR3( 1, 0, 0 ), sizeof( D3DXVECTOR3 ) );
memcpy( CamMat.m[1], &D3DXVECTOR3( 0, 1, 0 ), sizeof( D3DXVECTOR3 ) );
memcpy( CamMat.m[2], &D3DXVECTOR3( 0, 0, 1 ), sizeof( D3DXVECTOR3 ) );

また、球面上の位置ベクトルも保持しておきます:

D3DXVECTOR3 CamPos( 0, 0, -100.0f );  // 適当に

 この初期状態を格納したカメラ行列を1フレーム分更新してみます。
 最初にカメラのZ軸回転を考えます。回転変化は実はZ軸回転しかありません。XY軸の回転をするとカメラの視線が注視点をそれてしまうためです。Z軸で任意の角度回転した時に、カメラのXY軸も一緒に回転してあげます。これにより、カメラの新しい姿勢が確定するわけです。今カメラのZ軸は任意軸になっていますので、クォータニオンを使って回転行列を作ってあげましょう:

D3DXQUATERNION ZAxisQ;
D3DXQuaternionRotationAxis( &ZAxisQ, &ZAxis, angle );
D3DXMATRIX ZAxisRotMat;
D3DXMatrixRotationQuaternion( &ZAxisRotMat, &ZAxisQ );

CamMat *= ZAxisRotMat;

 クォータニオンを使うのが嫌な場合は、「Z軸回転行列×カメラ姿勢行列」で求めるというのもあります。どういう事かというと、カメラ空間でZ軸回転させてワールドに戻してあげているわけです(カメラの姿勢行列はビュー行列の逆行列です):

D3DXMATRIX ZRotMatV;
D3DXMatrixRotationZ( &ZRotMatV, angle );

CamMat = ZRotMatV * CamMat;

 いずれの方法でも、カメラのZ軸回転で姿勢が更新されます。

 続いて、新しい姿勢を基準とした緯度経度上(球面上)の差分移動を行います。移動はカメラの軸を基底としたベクトルDで方向を指定します。例えばD( Dx, Dy, 0 )だとしましょう。このベクトルを注視点の空間に戻します。これはベクトルDにカメラの姿勢を掛けるだけです。

D3DXVECTOR3 D( Dx, Dy, 0 );  // Dx,Dyは適当に
D3DXVECTOR3 DL;
D3DXVec3TransformCoord( &DL, &D, &CamMat );

これで何がわかるのか?それは「回転面」です。カメラは常に注視点を向いています。よって、回転面にはかならずカメラのZ軸が含まれます。面を定めるにはZ軸と重ならないもう1つの方向が必要で、先のベクトルDL(注視点空間の方向ベクトル)を定めた事で、回転すべき面が一意になったわけです。

 回転面がわかれば回転軸を求められます:

D3DXVECTOR3 RotAxis;
D3DXVECTOR3* CamZAxis = (D3DXVECTOR3*)CamMat.m[2];
D3DXVec3Cross( &RotAxis, &DL, CamZAxis );

 この回転軸と適当な回転角度を用いて緯度経度移動用の回転クォータニオンを作り、回転行列を求めます。それを現在のカメラの位置に適用します:

D3DXQUATERNION TransQ;
D3DXQuaternionRotationAxis( &TransQ, &RotAxis, angle );
D3DXMATRIX TransRotMat;
D3DXMatrixRotationQuaternion( &TransRotMat, &TransQ );

D3DXVec3TransformCoord( &CamPos, &CamPos, &TransRotMat );


 さて、こうしてカメラの位置が移動(実際は平行移動)してしまうと、先ほどの姿勢のままではカメラが注視点を向かなくなります。これを補正します。カメラの新しいZ軸は今の位置から原点を向くベクトルです。X軸とY軸はまた不定になってしまいました。そこで前のY軸の方向を基準にして、新しいX軸の方向を外積で求める事にします。Y軸はZX軸の外積で求めます:

D3DXVECTOR3 X, Y, Z;

Z = -CamPos;
D3DXVec3Normalize( &Z, &Z );

memcpy( &Y, CamMat.m[1], sizeof( D3DXVECTOR3 ) );
D3DXVec3Cross( &X, &Y, &Z );
D3DXVec3Normalize( &X, &X );

D3DXVec3Cross( &Y, &Z, &X );
D3DXVec3Normalize( &Y, &Y );

D3DXMatrixIdentity( CamMat );
memcpy( CamMat.m[0], &X, sizeof( D3DXVECTOR3 ) );
memcpy( CamMat.m[1], &Y, sizeof( D3DXVECTOR3 ) );
memcpy( CamMat.m[2], &Z, sizeof( D3DXVECTOR3 ) );

 これで、カメラの新しい姿勢と位置が定まりました。しかし、カメラの奥行き方向も考慮したいはずです。これは簡単で、新しい位置を原点方向にオフセットすれば良いんです。原点方向はカメラのZ軸ですから、

D3DXVECTOR3* CamZAxis = (D3DXVECTOR3*)CamMat.m[2];
CamPos += Offset_Z * (*CamZAxis);

で奥行き方向に点が移動します。

 ここからビュー行列を求めるには、カメラ姿勢行列に位置を加えたカメラ行列を作り、その逆行列を作成するだけです。

 プロセスをまとめますと、

・ 現在のZ軸を中心として回転 → 姿勢更新
・ カメラを中心とした移動方向決定 → 回転面確定
・ カメラ位置更新
・ 姿勢更新
・ カメラ行列を作り、その逆行列でビュー行列を算出

とすることで、注視点を見つめながらぐるぐる回転するカメラができます。



B サンプルプログラム

 本章で紹介したカメラの動きをするサンプルプログラムをこちらで公開しております。ViewerCameraという最低限の機能を実装したカメラクラスがありますので、ご自由にお使い下さい。