フィールドを歩き回るような3Dのゲームを作る際に、主観視点で周りをグルグル見渡せるカメラがあると便利です。ちょうどFPSゲームで使われるカメラです。この記事では、そういったFPSカメラの作り方を説明したいと思います。
目次
作り方概要
FPSカメラを作るには、『「3次元座標系」と「回転座標系」との相互変換』を実現します。これができればFPSカメラは簡単に作れます。
3次元座標系とは、おなじみXYZの座標系です。正確には今回はカメラの操作だけなので、視点座標と注視点座標の操作となります。
そして回転座標系とは、水平方向の回転と垂直方向の回転からなる座標系です(ただ、この用語は正確ではないかもしれません…)。
今回カメラを左右に回したり上下を見れるようにしたいので、水平・垂直方向を基準に回転ができる座標系が必要となるわけです。
ここまでをまとめると、
- 3次元座標系から回転座標系へ変換する
- プレイヤーの入力を受け取る
- 受け取った値を元に、3次元座標系へ逆変換する
この3つが実装できれば、FPSゲームのようにカメラをグルグル回すことが可能となります。
1.3次元座標系から回転座標系へ変換する
まずは、3次元座標を回転座標に変換します。正確には「カメラ視点」と「カメラ注視点」を元に、「水平方向の回転角度」と「垂直方向の回転角度」を計算します。
水平方向の回転角度を出すのは簡単です。視点から注視点への方向ベクトルを求め、Atan2を使うことで求めることができます
よくSTGを作るときに、狙い撃ちの角度を求めるために使うものですね。コードは以下のようになります。
// HAngleの取得関数
float FpsCamera::_GetHAngle(Vec3D* vEye, Vec3D* vTgt)
{
// 注視点への向きベクトルを求める
Vec3D vDir;
Vec3D_Sub( &vDir, vTgt, vEye );
// HAngle(XZ平面での角度)を求める
float deg = Atan2Deg( -vDir.z, vDir.x );
float __ADJ = 90.0f; // 調整角度(Z方向を基準に回転する)
deg += __ADJ;
// -180~180に丸める
if(deg > 180.0f) { deg -= 360.0f; }
if(deg < -180.0f) { deg += 360.0f; }
return deg;
注意点としては、90度の角度調整が入っていることです。
なぜ90度なのかというと、(x,y,z)=(0,0,1)を正面とした変換を行いたいためです。よく分からなければ、「とりあえず90度足すもの」ぐらいの理解でもいいと思います。
ちなみにこのコードの関数名として定義した「HAngle」というのは水平方向(Horizon)の角度という意味です。
続いて垂直方向の回転角度の計算方法です。ちょっと変わったことをしています。
// VAngleの取得関数
float FpsCamera::_GetVAngle(Vec3D* vEye, Vec3D* vTgt)
{
// 注視点への向きベクトルを求める
Vec3D vDir;
Vec3D_Sub( &vDir, vTgt, vEye );
float fFront;
{ // カメラの前方方向値
Vec3D _vFront;
Vec3D_Copy( &_vFront, &vDir );
_vFront.y = 0; // XZ平面での距離なのでYはいらない
fFront = Vec3D_Length( &_vFront );
}
// Y軸とXZ平面の前方方向との角度を求める
float deg = Atan2Deg( -vDir.y, fFront );
// 可動範囲は-90~90
if(deg > 90.0f) { deg = 180.0f - deg; }
if(deg < -90.0f) { deg = -180.0f - deg; }
return deg;
ポイントは2つです。
- 可動範囲を持たせる。垂直の回転は-90~90の範囲を超えないようにする
- fFrontを使うのは横軸がXZ平面における距離のため
1についてですが、上下にカメラを回す場合、-90~90を超えないようにしています。90を超えると左右が反転してしまうので、可動範囲を設定しておきます。まあ通常90度を超えて回転することはありえないで、このような制限を入れて問題ないと思います。
2についてですが、横軸に当たるのはXZ平面なので、XZ方向への距離を使用しています。XでもなくZでもなく、XZというのがポイントですね。
2.プレイヤーの入力の反映
先ほどの_GetHAngle()/_GetVAngle()で取得した値に、回転量を足しこむだけです。
// カメラの回転値を更新する
void FpsCamera::UpdateRotate()
{
// ★1.視点・注視点→回転座標への変換-----------
Vec3D vEye, vTgt;
GetEye(&vEye); // カメラ視点の取得
GetTgt(&vTgt); // カメラ注視点の取得
m_HAngle = _GetHAngle( &vEye, &vTgt ); // 水平方向の回転角度を取得
m_VAngle = _GetVAngle( &vEye, &vTgt ); // 垂直方向の回転角度を取得
// ★2.入力を元に回転する-----------------------
// ■視点移動(回転)
Vec2D stick; // ※予め移動値が入っているとします
Xへの移動(例えば左右キーの入力)を水平方向の回転、Yへの移動(例えば上下キーの入力)を垂直方向の回転として設定しています。
3.回転座標から3次元座標への変換
最後に入力を反映した値を逆変換します。以下、いままでの処理をまとめたコードとなります。
(回転のついでにカメラの平行移動もしています)
// カメラを移動・回転させる
void FpsCamera::Move()
{
// ★1.視点・注視点→回転座標への変換-----------
m_HAngle = _GetHAngle(GetEye(), GetTgt());
m_VAngle = _GetVAngle(GetEye(), GetTgt());
// ★2.入力を元に回転する-----------------------
// ■視点移動(回転)
Vec2D stick; // ※予め移動値が入っているとします
m_HAngle -= stick.x; // 水平方向への回転
m_VAngle += stick.y; // 垂直方向への回転
// ■平行移動
float mx, mz; // XZ方向 ※予め移動値が入っているとします
// ・3次元ベクトルにセット
Vec3D vTranslate;
Vec3D_Set( &vTranslate, mx, 0, mz );
// ★3.回転座標→3次元座標への変換-------------
// ■視点座標を取得する
Vec3D vEye;
GetEye( &vEye );
// ■注視点は初期化しておく(後で代入します)
Vec3D vTgt;
Vec3D_Zero( &vTgt );
// ■HAngle/VAngleを行列に変換する
Matrix34 mRot;
Matrix34_Identity(&mRot);
Matrix34_RotXYZDeg( &mRot, m_VAngle, m_HAngle, 0 ); // 回転行列生成(※1)
// ・平行移動
Vec3D_Transform( &vTranslate, &mRot, &vTranslate );
vTranslate.y = 0.0f; // XZ平面での移動なので、Y移動値は無視する
Vec3D_Add( &vEye, &vEye, &vTranslate ); // 視点を動かす
// ・回転
Vec3D vDir;
Vec3D_Set( &vDir, 0, 0, 1.0f ); // Z方向を基準に回転
Vec3D_Transform( &vDir, &mRot, &vDir ); // 方向ベクトルの回転(※2)
ポイントは※1の回転行列の生成、※2の方向ベクトルの回転です。
まず※1のところですが、水平方向の回転を「Y軸の回転」、垂直方向の回転を「X軸の回転」と見立てて、回転行列を作っています。
そして、視点から注視点への方向ベクトル(x,y,z)=(0,0,1)に対して、回転行列で回転させています。
ここで_GetHAngle()で+90度していた(Z軸を正面にしている)のが生きてくるわけです
最後に
このコードでは考え方を提示しているだけなので、このままコピーしても動きません。(そもそも環境によってベクトルや行列の関数は異なりますし…)
そこでこのコードを参考にFPSカメラを作る場合のデバッグ方法ですが、
- まずは水平方向(Hangle)が正しく動くかをデバッグする
- 続けて垂直方向(VAngle)が正しく動くかをチェックする
という手順で少しずつチェックすることをオススメします。
というのも、水平方向・垂直方向を最初から一緒に動かそうとすると、原因がよく分からずに失敗することが多いです。1つ1つを別の問題として調べていくと問題点も見つけやすいです。
(経験談)
あと回転させるのであればクォータニオンを使ったほうが早いと思ったかもしれません。ですが、クォータニオンで計算すると、どうやら真後ろを向くときに上を経由してしまうのでうまくいかないようです。