9장 Camera - ② 다양한 카메라 구현(궤도, 스플라인)
카메라는 3D 게임 세계에서 플레이어의 시점을 결정한다. 카메라를 단순히 보기용으로 생각할 수 있지만, 카메라는 플레이어 캐릭터가 게임 세계를 어떻게 움직여야 할지를 플레이어에게 알려준다. 즉, 카메라와 이동 시스템 구현은 서로 의존적인 것이다. 여기서는 다양한 카메라 구현과 각 카메라에 대한 이동 코드 갱신을 다룬다. 또한 카메라를 응용하여 2D를 3D에 언프로젝션 하는 방법을 다룬다.
💡 9장의 목차
- 다양한 카메라 구현
- 1인칭 카메라
- 팔로우 카메라
- 궤도 카메라
- 스플라인 카메라
- 언프로젝션
궤도 카메라
궤도 카메라는 대상 물체에 초점을 맞추고 그 물체 주위를 공전한다. 이런 유형의 카메라는 유저가 물체를 다각도로 관찰하는 경험을 줄 수 있어서 플래닛 코스터 같은 빌더 게임에서 많이 사용한다. 궤도 카메라의 기본적인 원리는 대상과 오프셋으로서 카메라 위치를 저장하는 것이다. 이렇게 해서 카메라의 회전은 항상 원점을 중심으로 회전한다는 이점을 취할 수 있다.
궤도 카메라의 구현
일반적으로 궤도 카메라를 제공해주는 게임에서는 플레이어가 오른쪽 마우스 버튼을 누를 때만 카메라가 회전하도록 한다. 따라서 플레이어로부터 입력을 받을 때 마우스 오른쪽 버튼이 눌러졌는지 확인하는 조건을 추가해서 마우스 오른쪽 버튼이 눌러졌을 때만 마우스 커서의 이동을 Yaw, Pitch에 반영하자.
void OrbitActor::ActorInput(const uint8_t* keys)
{
// 마우스 회전
// SDL로부터 마우스의 상대좌표를 받음
int x, y;
Uint32 buttons = SDL_GetRelativeMouseState(&x, &y);
// Only apply rotation if right-click is held
float yawSpeed = 0.0f;
float pitchSpeed = 0.0f;
if (buttons & SDL_BUTTON(SDL_BUTTON_RIGHT))
{
// 마우스의 최대 이동속도를 500으로 추측
const int maxMouseSpeed = 500;
// 초당 최대 회전 속도
const float maxOrbitSpeed = Math::Pi * 8;
// yaw 계산
if (x != 0)
{
// [-1.0, 1.0] 범위로 변환
yawSpeed = static_cast<float>(x) / maxMouseSpeed;
// 초당 최대 회전 속도를 곱해서 초당 yaw회전 속도 계산
yawSpeed *= maxOrbitSpeed;
}
// pitch 계산
if (y != 0)
{
// [-1.0, 1.0] 범위로 변환
pitchSpeed = static_cast<float>(y) / maxMouseSpeed;
pitchSpeed *= maxOrbitSpeed;
}
}
mCameraComp->SetYawSpeed(yawSpeed);
mCameraComp->SetPitchSpeed(pitchSpeed);
}
유저로부터 Yaw와 Pitch 각각의 속도를 입력받았다. 이제 이를 활용해서 회전을 하는 코드를 살펴보자. Yaw와 Pitch를 계산 후 mOffset과 mUp에 회전을 적용하여 회전을 만들어낸다. 이후 갱신된 mOffset과 mUp로 View Matrix를 다시 갱신하는 과정을 살펴볼 수 있다.
void OrbitCamera::Update(float deltaTime)
{
CameraComponent::Update(deltaTime);
// 게임 세계의 상향 벡터와 요를 사용한 쿼터니언 생성
Quaternion yaw(Vector3::UnitZ, mYawSpeed * deltaTime);
// 오프셋과 상향 벡터를 요 쿼터니언을 사용해서 변환
mOffset = Vector3::Transform(mOffset, yaw);
mUp = Vector3::Transform(mUp, yaw);
// 위의 벡터로부터 카메라의 전방/오른 축 벡터를 계산
Vector3 forward = -1.0f * mOffset;
forward.Normalize();
Vector3 right = Vector3::Cross(mUp, forward);
right.Normalize();
// 카메라 오른 축 벡터를 기준으로 회전하는 피치 쿼터니언 생성
Quaternion pitch(right, mPitchSpeed * deltaTime);
// 카메라 오프셋과 상향 벡터(카메라의 상향 벡터)를 피치 쿼터니언으로 회전시킴
mOffset = Vector3::Transform(mOffset, pitch);
mUp = Vector3::Transform(mUp, pitch);
// 변환 행렬을 계산
Vector3 target = mOwner->GetPosition();
Vector3 cameraPos = target + mOffset;
Matrix4 view = Matrix4::CreateLookAt(cameraPos, target, mUp);
SetViewMatrix(view);
}
스플라인 카메라
스플라인은 곡선상의 일련의 점들로 구성된 곡선을 수학적으로 표현한 것이다. 카메라가 미리 정의된 곡선 경로를 따라가는 컷신 카메라에서 스플라인은 유용하게 사용될 수 있다. 스플라인 중 캣멀롬스플라인은 상대적으로 계산하기 간단한 스플라인 타입이라서 게임과 컴퓨터 그래픽스에서 자주 사용된다. 캣멀롬 스플라인에서는 \(P_0\)에서 \(P_3\)까지 4개의 제어점을 요구한다. 실제 곡선은 \(P_1\)에서 \(P_2\)까지이며, \(P_0\)는 곡선이 시작되기 전의 제어점에 해당하며, \(P_3\)는 곡선이 끝난 후의 제어점에 해당한다.
곡선의 방정식 \(P(t)\)는 다음과 같이 정의할 수 있다.
\[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개의 점만 있으면 된다. 이러한 스플라인은 다음과 같이 정의할 수 있다.
struct Spline
{
// 스플라인에 대한 제어점
// 세그먼트상에 n개의 점이 있다면 총 n+2개의 점이 필요하다
std::vector<Vector3> mControlPoints;
// 계산을 시작할 정점의 인덱스와
// [0.0, 1.0]범위에 있는 t 값이 주어졌을 때
// 곡선위의 좌표를 계산하여 반환
Vector3 Compute(size_t startIdx, float t) const;
size_t GetNumPoints() const { return mControlPoints.size(); }
};
Spline의 Compute함수를 활용하면 아래와 같이 SplineCamera는 미리 입력된 곡선을 따라서 이동할 수 있다.
void SplineCamera::Update(float deltaTime)
{
CameraComponent::Update(deltaTime);
// t값 갱신
if (!mPaused)
{
mT += mSpeed * deltaTime;
// T값이 1.0보다 크다면 다음 제어점으로 이동
// 카메라 이동 속도는 한 프레임에 복수개의 제어점을 건너뛸 만큼 빠르지 않다고 가정
if (mT >= 1.0f)
{
// 경로를 진행하는 데 있어 충분한 접을 가져야 한다
if (mIndex < mPath.GetNumPoints() - 3)
{
mIndex++;
mT = mT - 1.0f;
}
else
{
// 경로 진행을 완료했으므로 카메라 이동을 중지시킨다.
mPaused = true;
}
}
}
// 카메라 위치는 현재 t/인덱스값에 해당하는 스플라인에 있다.
Vector3 cameraPos = mPath.Compute(mIndex, mT);
// 목표 지점은 t값에 작은 델타값을 더해서 얻은 위치다.
Vector3 target = mPath.Compute(mIndex, mT + 0.01f);
// 스플라인 카메라는 거꾸로 뒤집어지지 않는다고 가정
const Vector3 up = Vector3::UnitZ;
Matrix4 view = Matrix4::CreateLookAt(cameraPos, target, up);
SetViewMatrix(view);
}
전체 구현 코드는 아래를 참고하자.
- 카메라 구현 전체 소스코드 보기
출처:
에이콘 출판 <Game Programming in C++>