GameDev #9 カメラ(FPS・追従・軌道・スプライン) [Devlog #025]
+ + + + +Table of Contents
+ +ゲーム開発者の教科書:Game Programming in C++ を読んで理解したことについてを要約します(内容の転載を避け、詳しく説明しすぎないように配慮します)
+ゲームプログラミング in C++
++
カメラ
+ここでは、4種類のカメラの実装を行う(FPSカメラ、追従カメラ、軌道カメラ、スプラインカメラ)
+FPSカメラ
+FPSカメラ(first-person camera)は、キャラクターの視点から映すようなカメラである
+PCの一人称シューターでは、キーボードとマウスを使う操作が一般的であり、WとSキーで前進と後退、AキーとDキーで左右移動(ストレイフ)、マウスの動きに合わせてビューがピッチングする
+基本的な一人称の動き
+キャラクターを動かす
+Actor.h
+左右移動のため、右方ベクトルを取得するGetRight()
を追加する
Vector3 GetRight() const { return Vector3::Transform(Vector3::UnitY, mRotation); }
+
MoveComponent.h
+左右移動の速さを表すメンバ変数mStrafeSpeed
を追加する
ゲッターとセッターを追加する
+float GetAngularSpeed() const { return mAngularSpeed; }
+float GetForwardSpeed() const { return mForwardSpeed; }
+float GetStrafeSpeed() const { return mStrafeSpeed; } //※
+void SetAngularSpeed(float speed) { mAngularSpeed = speed; }
+void SetForwardSpeed(float speed) { mForwardSpeed = speed; }
+void SetStrafeSpeed(float speed) { mStrafeSpeed = speed; } //※
+
MoveComponent.cpp
+MoveComponent::Update()
+mStrafeSpeed
をもとに右方ベクトルで位置を調整する
if (!Math::NearZero(mForwardSpeed) || !Math::NearZero(mStrafeSpeed)) {
+ Vector3 pos = mOwner->GetPosition();
+ pos += mOwner->GetForward() * mForwardSpeed * deltaTime;
+ pos += mOwner->GetRight() * mStrafeSpeed * deltaTime;
+ mOwner->SetPosition(pos);
+ }
+
FPSActor.cpp
+FPSActor::ActorInput()
+AキーとDキーを監視して左右移動の速度を調整する
+float forwardSpeed = 0.0f;
+float strafeSpeed = 0.0f;
+// wasd movement
+if (keys[SDL_SCANCODE_W]) {
+ forwardSpeed += 400.0f;
+}
+if (keys[SDL_SCANCODE_S]) {
+ forwardSpeed -= 400.0f;
+}
+if (keys[SDL_SCANCODE_A]) {
+ strafeSpeed -= 400.0f;
+}
+if (keys[SDL_SCANCODE_D]) {
+ strafeSpeed += 400.0f;
+}
+
+mMoveComp->SetForwardSpeed(forwardSpeed);
+mMoveComp->SetStrafeSpeed(strafeSpeed);
+
Game.cpp
+Game::LoadData()
+マウスの相対運動モードを有効にする
+// Enable relative mouse mode for camera look
+SDL_SetRelativeMouseMode(SDL_TRUE);
+
FPSActor.cpp
+FPSActor::ActorInput()
+SDL_GetRelativeMouseState()
で移動量$(x,y)$を取得する
1フレームでの最大移動量maxMouseSpeed
を設定する(ユーザー定義でもよい)
最大移動量での角速度maxAngularSpeed
を設定し、角速度angularSpeed
をMoveComponent
に送る
// Mouse movement
+// Get relative movement from SDL
+int x, y;
+SDL_GetRelativeMouseState(&x, &y);
+// Assume mouse movement is usually between -500 and +500
+const int maxMouseSpeed = 500;
+// Rotation/sec at maximum speed
+const float maxAngularSpeed = Math::Pi * 8;
+float angularSpeed = 0.0f;
+if (x != 0) {
+ // Convert to ~[-1.0, 1.0]
+ angularSpeed = static_cast<float>(x) / maxMouseSpeed;
+ // Multiply by rotation/sec
+ angularSpeed *= maxAngularSpeed;
+}
+mMoveComp->SetAngularSpeed(angularSpeed);
+
カメラ(ピッチなし)
+Component
派生クラスCameraComponent
を作る
4種類のカメラはこのCameraComponent
を派生する
CameraComponent.h
+ビュー行列をレンダラとオーディオシステムに送るprotected関数SetViewMatrix()
を追加する
CameraComponent.cpp
+CameraComponent::SetViewMatrix()
+// Pass view matrix to renderer and audio system
+Game* game = mOwner->GetGame();
+game->GetRenderer()->SetViewMatrix(view);
+game->GetAudioSystem()->SetListener(view);
+
FPSCamera.cpp
+CameraComponent
の派生クラス
FPSCamera::Update()
+Update()
をオーバーライドする
カメラの位置は、所有者であるアクターの位置
+
ターゲットポイントは、所有アクターの前方に位置する点
+
上方ベクトルは$z$軸とする
Matrix4::CreateLookAt()
でビュー行列を作る
ピッチ(横向きの軸での回転)を加える
+FPSCamera.h
+新しいメンバ変数を追加する
+// Rotation/sec speed of pitch
+float mPitchSpeed;
+// Maximum pitch deviation from forward
+float mMaxPitch;
+// Current pitch
+float mPitch;
+
上下にピッチできる量に制限mMaxPitch
を加えることで、仰向けになったときの制御の不具合を防ぐ
FPSCamera.cpp
+FPSCamera::Update()
+現在のピッチの値を、ピッチのスピードとデルタタイムに基づいて更新する
+ピッチの量が最大ピッチ(+/-)を超えないようにクランプする
+ピッチを表現するクォータニオンを作る(所有アクターの右向きの軸を中心とする(ピッチの軸が所有者のヨーに依存している))
+所有者の前方ベクトルをピッチのクォータニオンで変換して前方への視線viewForward
を作る
注視行列view
を作成し、ビューに設定する
FPSActor.cpp
+FFPSActor::ActorInput()
+ピッチのスピードをFPSCamera
(CameraComponent派生クラス)に送る
// Compute pitch
+const float maxPitchSpeed = Math::Pi * 8;
+float pitchSpeed = 0.0f;
+if (y != 0) {
+ // Convert to ~[-1.0, 1.0]
+ pitchSpeed = static_cast<float>(y) / maxMouseSpeed;
+ pitchSpeed *= maxPitchSpeed;
+}
+mCameraComp->SetPitchSpeed(pitchSpeed);
+
一人称モデル
+FPSActor.cpp
+FPSActor::UpdateActor()
+モデルがアニメーションするパーツ(腕や脚や武器)を持つことを考慮し、フレームごとに位置と回転を更新する
+一人称モデルの位置は、FPSActor
の位置にオフセットを加えたもの
回転は、FPSActor
の回転にビューのピッチの回転を追加する
// Update position of FPS model relative to actor position
+const Vector3 modelOffset(Vector3(10.0f, 10.0f, -10.0f));
+Vector3 modelPos = GetPosition();
+modelPos += GetForward() * modelOffset.x;
+modelPos += GetRight() * modelOffset.y;
+modelPos.z += modelOffset.z;
+mFPSModel->SetPosition(modelPos);
+// Initialize rotation to actor rotation
+Quaternion q = GetRotation();
+// Rotate by pitch from camera
+q = Quaternion::Concatenate(q, Quaternion(GetRight(), mCameraComp->GetPitch()));
+mFPSModel->SetRotation(q);
+
追従カメラ
+追従カメラ(follow camera)は、ターゲットオブジェクトを後方から追いかけるカメラである
+基本的な追従カメラ
+基本的な追従カメラは、所有アクターを上後方から常に決まった距離で追いかける
+車を追いかける追従カメラを例とすると、カメラは車の後方に水平距離で$HDist$、上方に垂直距離で$VDist$の位置に固定する
+カメラの注視点は、車より$TargetDist$先の点とする
+カメラの位置: +$$ +CameraPos = OwnerPos - OwnerForward\cdot HDist + OwnerUp\cdot VDist +$$ +注視点: +$$ +TargetPos = OwnerPos + OwnerForward\cdot TargetDist +$$
+FollowCamera.h
+CameraComponent
の派生クラス
水平距離CameraComponent
、垂直距離mVertDist
、ターゲット距離mTargetDist
のメンバ変数を追加する
FollowCamera::ComputeCameraPos()
+カメラの位置を計算する
+FollowCamera::Update()
+カメラ位置と注視点を使ったビュー行列を作る
+ばねを追加する
+カメラの位置を"理想"のポジションと"実際"のポジションに分け、ばねで連結する
+FollowCamera.h
+メンバ変数mSpringConstant
は、ばねの硬さを表現する
カメラの実際の位置mActualPos
と速度mVelocity
を追加する
FollowCamera.cpp
+FollowCamera::Update()
+ばね定数mSpringConstant
から、ばねの減衰dampening
を計算する
“実際の位置と理想の位置との差"と"前フレームの速度"から、カメラの加速度を計算する
+注視点の計算は元のままで、CreateLookAt()
では実際のカメラポジションmActualPos
を使う
CameraComponent::Update(deltaTime);
+// Compute dampening from spring constant
+float dampening = 2.0f * Math::Sqrt(mSpringConstant);
+// Compute ideal position
+Vector3 idealPos = ComputeCameraPos();
+// Compute difference between actual and ideal
+Vector3 diff = mActualPos - idealPos;
+// Compute acceleration of spring
+Vector3 acel = -mSpringConstant * diff - dampening * mVelocity;
+// Update velocity
+mVelocity += acel * deltaTime;
+// Update actual camera position
+mActualPos += mVelocity * deltaTime;
+// Target is target dist in front of owning actor
+Vector3 target = mOwner->GetPosition() + mOwner->GetForward() * mTargetDist;
+// Use actual position here, not ideal
+Matrix4 view = Matrix4::CreateLookAt(mActualPos, target, Vector3::UnitZ);
+SetViewMatrix(view);
+
FollowCamera::SnapToIdeal()
+カメラがゲームの開始時点で正しく動くように、SnapToIdeal()
をFollowActor()
の初期化時に呼び出す
軌道カメラ
+軌道カメラ(orbit camera)は、ターゲットを中心として、その周りを回るカメラである
+基本的な軌道カメラ
+軌道を回るヨーとピッチの両方をマウスで操作する
+カメラは、右マウスボタンを押している時だけ回転する
+OrbitCamera.h
+ターゲットからのオフセットoffset
、カメラの上方ベクトルmUp
、ピッチの角速度mPitchSpeed
、ヨーの角速度mYawSpeed
のメンバ変数を追加する
マウスによる回転操作に応じて、角速度mPitchSpeed
とmYawSpeed
を更新する
カメラの回転に応じて、上方ベクトルmUp
を更新する必要がある(常に$(0,0,1)$ではない)
OrbitCamera.cpp
+コンストラクタで、mPitchSpeed
とmYawSpeed
を0で初期化し、mOffset
の初期値は任意で後方400単位、mUp
はワールド空間の上方$(0,0,1)$で初期化する
OrbitCamera::Update()
+ヨーの回転(ワールドの上方を軸とする)クォータニオンyaw
を作る
yaw
でカメラのオフセットmOffset
と上方ベクトルmUp
を変換する
カメラの前方ベクトルforward
を新しいオフセットから求める
カメラの右方ベクトルright
を、カメラの上方と前方のクロス積で求める
ピッチのクォータニオンpitch
を作成し、再度オフセットmOffset
と上方ベクトルmUp
を変換する
注視行列においては、カメラの注視点は所有アクターの位置、カメラポジションは注視点にオフセットを足したもの、上方はカメラの上方とする
+スプラインカメラ
+スプラインカメラ(spline camera)は、曲線上の点列で指定される
+プレイヤーがゲームワールドを進む時、その道筋をカメラが追従するようにする用途に使われる
+基本的なスプラインカメラ
+Catmull-Romスプラインは、比較的計算が単純なスプライン曲線で、最小で4つの制御点が必要がある
+4つの制御点がある時、$P_1$から$P_2$までの位置は、以下のパラメトリック方程式(parametric equation)で表現できる
+$$ +p(t) = 0.5\cdot (2P_1 + (-P_0 + P_2)t + (2P_0 - 5P_1 + 4P_2 - P_3)t^2 + (-P_0 + 3P_1 - 3P_2 + P_3)t^3) +$$
+$n$個の点を通るカーブを表現するのに、$n+2$個の制御点が必要になる
+SprineCamera.h
+スプラインを定義するSpline
構造体は、制御点の配列mControlPoints
をメンバデータとする
Spline::Compute()
+スプライン方程式の関数
+引数のstartIdx
は$P_1$に対応し、引数のt
は$[0.0, 1.0]$の範囲とする
SplineCamera::SplineCamera()
+スプラインmPath
、$P_1$に対応する現在のインデックスmIndex
、現在の$t$の値mT
、スピードmSpeed
、カメラを経路に沿って動かすかどうかmPaused
のメンバを管理する
SplineCamera::Update()
+$t$の値を、スピードとデルタタイムの積だけ増やす
+$t$の値が1.0以上であれば、$P_1$は経路の次の点に進む(進む際は$t$の値から1.0を引く)
+もし十分な数の点がなければ、スプラインカメラは停止する
+逆射影
+スクリーン空間の座標からワールド空間の座標に変換する計算を逆射影(unprojection)と呼ぶ
+FPSシューターの例では、スクリーン上の標準レティクルに沿って弾丸を発射する場合、狙いどおりに反射するにはスクリーン空間の座標ではなく、ワールド空間の座標が必要となる
+逆射影の基本
+スクリーン空間の座標の$x$と$y$の両方の成分をデバイス座標系(NDC)($[-1,1]$の範囲に正規化)に変換する +$$ +ndcX = screenX/512 \quad \quad ndcY = screenY/384 +$$
+$[0,1]$の範囲に存在する任意の$z$座標を考慮し、デバイス座標を同次座標で表すと、 +$$ +ndc = (ndcX, ndcY, z, 1) +$$
+逆射影行列は、ビュー射影行列の逆行列となる +$$ +Unprojection = ((View)(Procection))^{-1} +$$
+NDCの座標に逆射影行列を掛けるとw成分が変化するが、各成分を$w$で割ることで再び正規化できる +$$ +temp = (ndc)(Unprojection) \quad \quad worldPos = \frac{temp}{temp_w} +$$
+Renderer.cpp
+Renderer::Unproject()
+ビュー行列と射影行列の両方にアクセスできる唯一のクラスであるRenderer
クラスに追加する
TransformWithPerspDiv()
はw成分を正規化する
// Convert screenPoint to device coordinates (between -1 and +1)
+Vector3 deviceCoord = screenPoint;
+deviceCoord.x /= (mScreenWidth) * 0.5f;
+deviceCoord.y /= (mScreenHeight) * 0.5f;
+
+// Transform vector by unprojection matrix
+Matrix4 unprojection = mView * mProjection;
+unprojection.Invert();
+return Vector3::TransformWithPerspDiv(deviceCoord, unprojection);
+
Renderer::GetScreenDirection()
+3D空間のオブジェクトをクリックで選択するピッキング(picking)の操作の実装では、スクリーン空間の"ある点"に向かうベクトルを得ることで計算できる
+法句おベクトルを得るため、Unproject()
で始点と終点を変換する
ベクトルの引き算をした後、正規化する
+// Get start point (in center of screen on near plane)
+Vector3 screenPoint(0.0f, 0.0f, 0.0f);
+outStart = Unproject(screenPoint);
+// Get end point (in center of screen, between near and far)
+screenPoint.z = 0.9f;
+Vector3 end = Unproject(screenPoint);
+// Get direction vector
+outDir = end - outStart;
+outDir.Normalize();
+
ゲームプロジェクト
+確認
+ + + +まとめ
+参考文献
+ゲームのカメラに関するテクニックを紹介する書籍(筆者は"メトロイドプライム"用カメラシステムの主任プログラマー):
Real Time Cameras: A Guide for Game Designers and Developers