diff --git a/index.html b/index.html index f6e8242..37647f8 100644 --- a/index.html +++ b/index.html @@ -155,6 +155,18 @@

Recent Posts

+
+

+ GameDev #9 カメラ(FPS・追従・軌道・スプライン) [Devlog #025] +

+ + + +
+ +

GameDev #8 入力システム [Devlog #024] @@ -203,18 +215,6 @@

-
-

- Unity-GameDev #1 企画・設計 [Devlog #020] -

- - - -
- -

diff --git a/index.xml b/index.xml index e1eaafc..6993e74 100644 --- a/index.xml +++ b/index.xml @@ -6,7 +6,24 @@ Recent content in Home on nishihi6 Hugo -- gohugo.io ja-jp - Wed, 24 Jan 2024 04:19:53 +0900 + Mon, 29 Jan 2024 03:15:04 +0900 + + GameDev #9 カメラ(FPS・追従・軌道・スプライン) [Devlog #025] + https://nishihi6.github.io/blog/posts/devlog_gl_09/ + Mon, 29 Jan 2024 03:15:04 +0900 + + https://nishihi6.github.io/blog/posts/devlog_gl_09/ + ゲーム開発者の教科書: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. + + GameDev #8 入力システム [Devlog #024] https://nishihi6.github.io/blog/posts/devlog_gl_08/ diff --git a/posts/devlog_gl_08/index.html b/posts/devlog_gl_08/index.html index d8ab701..0afc67a 100644 --- a/posts/devlog_gl_08/index.html +++ b/posts/devlog_gl_08/index.html @@ -296,7 +296,7 @@

Game::Initialize()

アタッチされたすべてのコンポーネントのProcessInput()を呼び出すため、オーバーライドできない

// ProcessInput function called from Game (not overridable)
 void ProcessInput(const struct InputState& state);
-

Actor自身の独自入力のためのオーバーライド可能なActorInput()の宣言を変更し、InputStateのconst参照を受け取る商にする

+

Actor自身の独自入力のためのオーバーライド可能なActorInput()の宣言を変更し、InputStateのconst参照を受け取るようにする

// Any actor-specific input code (overridable)
 virtual void ActorInput(const struct InputState& state);
 
Component.h
diff --git a/posts/devlog_gl_09/index.html b/posts/devlog_gl_09/index.html new file mode 100644 index 0000000..0ab26b3 --- /dev/null +++ b/posts/devlog_gl_09/index.html @@ -0,0 +1,621 @@ + + + + + +GameDev #9 カメラ(FPS・追従・軌道・スプライン) [Devlog #025] | nishihi6 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+

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を設定し、角速度angularSpeedMoveComponentに送る

+
// 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のメンバ変数を追加する

+

マウスによる回転操作に応じて、角速度mPitchSpeedmYawSpeedを更新する

+

カメラの回転に応じて、上方ベクトルmUpを更新する必要がある(常に$(0,0,1)$ではない)

+
OrbitCamera.cpp
+

コンストラクタで、mPitchSpeedmYawSpeedを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

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/posts/index.html b/posts/index.html index 7e513a9..af686e5 100644 --- a/posts/index.html +++ b/posts/index.html @@ -39,8 +39,8 @@ - - + + @@ -55,11 +55,11 @@ "@type": "Person", "name": "" }, - "datePublished": "2024-01-24", + "datePublished": "2024-01-29", "description": "", "wordCount": 0 , "mainEntityOfPage": "True", - "dateModified": "2024-01-24", + "dateModified": "2024-01-29", "image": { "@type": "imageObject", "url": "" @@ -192,6 +192,20 @@

2024

+
+

+ GameDev #9 カメラ(FPS・追従・軌道・スプライン) [Devlog #025] +

+ + + +
+ + + +

GameDev #8 入力システム [Devlog #024] diff --git a/posts/index.xml b/posts/index.xml index a7625a0..b2e48f0 100644 --- a/posts/index.xml +++ b/posts/index.xml @@ -6,7 +6,24 @@ Recent content in Posts on nishihi6 Hugo -- gohugo.io ja-jp - Wed, 24 Jan 2024 04:19:53 +0900 + Mon, 29 Jan 2024 03:15:04 +0900 + + GameDev #9 カメラ(FPS・追従・軌道・スプライン) [Devlog #025] + https://nishihi6.github.io/blog/posts/devlog_gl_09/ + Mon, 29 Jan 2024 03:15:04 +0900 + + https://nishihi6.github.io/blog/posts/devlog_gl_09/ + ゲーム開発者の教科書: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. + + GameDev #8 入力システム [Devlog #024] https://nishihi6.github.io/blog/posts/devlog_gl_08/ diff --git a/sitemap.xml b/sitemap.xml index 579bc6c..2dacaa8 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,13 +2,16 @@ - https://nishihi6.github.io/blog/posts/devlog_gl_08/ - 2024-01-24T04:19:53+09:00 + https://nishihi6.github.io/blog/posts/devlog_gl_09/ + 2024-01-29T03:15:04+09:00 https://nishihi6.github.io/blog/ - 2024-01-24T04:19:53+09:00 + 2024-01-29T03:15:04+09:00 https://nishihi6.github.io/blog/posts/ + 2024-01-29T03:15:04+09:00 + + https://nishihi6.github.io/blog/posts/devlog_gl_08/ 2024-01-24T04:19:53+09:00 https://nishihi6.github.io/blog/posts/devlog_gl_07/