본문으로 바로가기

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++>