본문으로 바로가기

9장 Camera - ① 다양한 카메라 구현(1인칭, 팔로우)

카메라는 3D 게임 세계에서 플레이어의 시점을 결정한다. 카메라를 단순히 보기용으로 생각할 수 있지만, 카메라는 플레이어 캐릭터가 게임 세계를 어떻게 움직여야 할지를 플레이어에게 알려준다. 즉, 카메라와 이동 시스템 구현은 서로 의존적인 것이다. 여기서는 다양한 카메라 구현과 각 카메라에 대한 이동 코드 갱신을 다룬다. 또한 카메라를 응용하여 2D를 3D에 언프로젝트 하는 방법을 다룬다.

💡 9장의 목차

  • 다양한 카메라 구현
    • 1인칭 카메라
    • 팔로우 카메라
    • 궤도 카메라
    • 스플라인 카메라
  • 언프로젝션

1인칭 카메라

1인칭 카메라는 움직이는 캐릭터의 시점에서 게임 세계를 보여준다. 이런 타입의 카메라는 많은 1인칭 슈팅 게임과 일부 롤플레잉 게임에서 사용된다. 일반적으로 1인칭 슈팅 게임의 컨트롤은 키보드와 마우스를 사용한다. W/S키는 캐릭터 앞뒤로 움직이고 A/D는 캐릭터 좌우로 움직인다.

 

▲ 1인칭 슈팅게임 예시

기본 1인칭 이동

1인칭 이동은 앞서 6장에서 구현했던 MoveComponent의 앞뒤 이동과 같은 메커니즘으로 좌우 이동을 추가하면 된다. GetForward와 같은 GetRight함수를 구현하여 이동의 방향을 정하고 mStrafeSpeed라는 좌우 이동속도 변수를 통해서 이동을 구현한다.

또한 마우스 X축의 상대적 이동을 활용하면 캐릭터를 Z 축 기준으로 회전시킬 수 있어야 한다. 이를 위해서 아래와 같이 x축의 상대적 이동 값과 MoveComponent에서 기존에 구현하였던 angularSpeed를 활용한다.

void FPSActor::ActorInput(const uint8_t* keys)
{
    // w,a,s,d 이동 코드 생략

    // 마우스 이동
    // SDL로 부터 상대적인 이동을 얻는다.
    int x, y;
    SDL_GetRelativeMouseState(&x, &y);
    // 마우스 이동은 -500에서 500 사이의 값이라고 추정
    const int maxMouseSpeed = 500;
    // 초당 회전의 최대 속도
    const float maxAngularSpeed = Math::Pi * 8;
    float angularSpeed = 0.0f;
    if (x != 0)
    {
        // [-1.0, 1.0] 범위로 반환
        angularSpeed = static_cast<float>(x) / maxMouseSpeed;
        // 초당 회전을 곱한다.
        angularSpeed *= maxAngularSpeed;
    }
    mMoveComp->SetAngularSpeed(angularSpeed);
}

1인칭 카메라에서 엑터의 이동은 곧 카메라의 이동이다. 카메라가 이동했다는 것은 View Matrix와 Listener의 이동이 발생했다는 것이다. 따라서 카메라 이동이 필요한 모든 CameraComponent의 서브 클래스들은 Update문에서 아래의 함수를 호출해서 카메라와 리스너를 갱신해야 한다.

void CameraComponent::SetViewMatrix(const Matrix4& view)
{
    // 뷰 행렬을 렌더러와 오디오 시스템에 전달한다.
    Game* game = mOwner->GetGame();
    game->GetRenderer()->SetViewMatrix(view);
    game->GetAudioSystem()->SetListener(view);
}

피치 추가

앞서 구현한 카메라는 마우스 x축의 상대적 이동을 활용한 z 축 기준의 회전만 동작한다. 이제 마우스 y값을 활용한 측면 축을 중심으로 한 회전(피치)을 추가한다. 대부분의 1인칭 게임은 플레이어가 위아래로 볼 수 있는 피치 값을 제한한다. 이런 제한을 두는 이유는 플레이어가 위를 똑바로 마주 볼 시 제어가 이상해질 수 있기 때문이다. 최대 피치 값은 보통 60도를 사용한다.

피치를 구현하기 위해서는 우선 FPSActor에서 mPitchSpeed를 입력 받은 후 입력받은 pitchSpeed를 활용하여 FPSCamera::Update에 아래와 같이 피치 회전을 구현한다.

이상 자세한 코드는 여기를 참고하자.


팔로우 카메라

팔로우 카메라는 타깃 오브젝트 뒤를 따라가는 카메라다. 이런 유형의 카메라는 레이싱 게임이나 호라이즌 제로 던 같은 3인칭 액션 어드벤처 게임에서 널리 사용된다.

 

▲ 팔로우 카메라 예시

기본 팔로우 카메라

기본 팔로우 카메라는 소유자 엑터 뒤의 일정한 거리와 각도를 유지하고 따라간다. 아래의 그림처럼 기본 팔로우 카메라의 위치와 방향은 소유자 엑터의 위치와 Forward 벡터를 활용하여 구할 수 있다. 팔로우 카메라는 소유자 엑터를 바라보기보다 소유자 엑터의 일정 거리 앞에 있는 TargetPos을 바라보는 것이 더욱 자연스러울 수 있다.

 

▲ 팔로우 카메라 계산

따라서 다음과 같이 간단한 계산 만으로도 팔로우 카메라 위치 cameraPos를 구현할 수 있다.

Vector3 FollowCamera::ComputeCameraPos() const
{
    // 소유자의 뒤쪽 및 위쪽에 카메라 위치 설정
    Vector3 cameraPos = mOwner->GetPosition();
    cameraPos -= mOwner->GetForward() * mHorzDist;
    cameraPos += Vector3::UnitZ * mVertDist;
    return cameraPos;
}

카메라의 방향은 TargetPos을 바라보는 방향이다. 따라서 OwnerForward 벡터를 활용하여 TargetPos을 구한 후 view Matrix를 구할 때 사용하자.

void FollowCamera::Update(float deltaTime)
{
    CameraComponent::Update(deltaTime);

    // .. 생략 ..

    // 타깃은 소유자 앞에 있는 대상을 의미
    Vector3 target = mOwner->GetPosition() + mOwner->GetForward() * mTargetDist;
    // 카메라가 뒤집혀지는 경우는 없기에 위쪽 방향은 항상 UnitZ이다
    Matrix4 view = Matrix4::CreateLookAt(ComputeCameraPos(), target, Vector3::UnitZ);
    SetViewMatrix(view);
}

 

스프링이 추가된 팔로우 카메라

소유자 엑터 뒤를 일정하게 따라가는 카메라는 속도감을 표현하기에 적절한 방법이 아니다. 레이싱 게임에서 카메라가 엑터의 뒤에 고정적으로 추적한다면 속도감이 전혀 표현되지 않을 뿐 아니라 엑터가 움직이는 게 아니라 배경이 움직이는 것 같은 느낌을 받을 수 있다. 조금 더 역동적인 팔로우 카메라를 구현하기 위해서 아래에 나오는 카트라이더의 예시처럼 엑터의 속도에 따라 카메라의 거리와 방향이 역동적으로 변할 수 있도록 해야 한다.

 

▲ 역동적인 팔로우 카메라 예시 (카트라이더)

 

이를 위해서 카메라에 가상의 스프링을 추가하는 방법을 사용한다. 앞서 구현한 팔로우 카메라는 엑터의 이동에 즉각적으로 이동위치가 계산되는 이상적인 위치 Ideal이 된다. 하지만 실제의 카메라는 Ideal위치에 탄성력이 있는 스프링을 달아서 움직이는 것처럼 엑터의 속도에 따라 반응하는 Actual 위치에 있다.

▲ 스프링이 추가된 팔로우 카메라

용수철을 이용한 실제 카메라의 위치를 계산하기 위해서는 오일러 적분 테크닉을 활용해야 한다. 먼저 실제 카메라가 탄성력에서 받는 가속도를 계산하고, 이로 속도와 위치까지 계산해야 한다. 우선 용수철에 작용하는 힘을 살펴보면 다음과 같다.

\[용수철에 작용하는 힘: F = -kx \]

\[기계 진동에서의 힘: F = ma + cv\]

\(c\)는 댐핑(감쇠) 인자로 여기에서는 \(c = 2\sqrt{k}\) 라는 사실을 이용한다. 용수철에서 작용하는 힘은 곧 기계 진동에서의 힘과 동일하기 때문에
\[-kx = ma + cv\]

라고 할 수 있다. 또한 카메라의 질량은 크게 고려요소가 아니기 때문에 1로 가정하더라도 무리가 없다. 따라서 카메라 가속도\(a\)를 정리하면 다음과 같다.
\[a = -kx - cv\]

void FollowCamera::Update(float deltaTime)
{
    CameraComponent::Update(deltaTime);

    // 스프링 상숫값으로부터 감쇄 인자값 계산
    float dampening = 2.0f * Math::Sqrt(mSpringConstant);

    // 이상적인 위치 계산
    Vector3 idealPos = ComputeCameraPos();
    // 이상적인 위치와 실제 위치의 차를 계산
    Vector3 diff = mActualPos - idealPos;
    // 스프링의 가속도를 계산한다.
    Vector3 acel = - mSpringConstant * diff - dampening * mVelocity;

    // 속도를 갱신
    mVelocity += acel * deltaTime;
    // 실제 카메라의 위치를 갱신
    mActualPos += mVelocity * deltaTime;

    // 타깃은 소유자 앞에 있는 대상을 의미
    Vector3 target = mOwner->GetPosition() + mOwner->GetForward() * mTargetDist;
    // 카메라가 뒤집혀지는 경우는 없기에 위쪽 방향은 항상 UnitZ이다
    Matrix4 view = Matrix4::CreateLookAt(mActualPos, target, Vector3::UnitZ);
    SetViewMatrix(view);
}

카메라 구현 전체 소스코드 보기


출처:

에이콘 출판 <Game Programming in C++>