본문 바로가기
게임 공부/게임 개발 일지

[DX] 카메라 클래스 설계 및 구현

by woohyeon 2021. 5. 4.
반응형

카메라 클래스?

여기서 카메라는 3D 공간에서 우리의 눈이 되어줄 가상의 오브젝트를 말한다.

월드 공간에 카메라를 임의의 위치에 놓으면 그 카메라 위치에서 보이는 물체들을 화면에 적절하게 나타낼 수 있어야 한다. 예를 들어 물체는 그대로지만 우리가 보는 시점에 따라 그 물체는 다르게 보인다. 카메라가 움직이면 우리가 보는 화면도 자연스럽게 변해야 한다. 이러한 기능을 담당하는 카메라 클래스가 필요하다. 

출처: real time rendering

 

렌더링 순서

월드 공간에 물체들이 있다. 월드 공간의 물체들은 View Transform(뷰 변환)과 Perspective Projection Transform(원근 투영 변환)을 통해 뷰 공간와 투영 공간으로 이동되어야 한다. 뷰 공간은 카메라 공간이라고도 한다. 위에서 보았듯이 카메라가 중심인 공간이다. 이러한 뷰 공간은 카메라의 위치(Eye), 어디를 응시하고 있는지(LookAt), 카메라 기준 Up 방향(Up) 3가지 데이터로 정의된다. 즉 카메라의 위치가 변하거나 focusing하는 점이 달라질 때마다 뷰 행렬도 변경되어야 한다. 

카메라 공간으로 이동되었다고 끝이 아니다. 카메라가 볼 수 있는 시야를 제한해야 한다. 카메라의 시야를 의미하는 View frustum을 구성하기 위해 4가지 데이터가 필요하다. 이 4가지 데이터가 있다면 원근 투영 행렬을 만들 수 있다. 원근 투영 행렬은 카메라의 이동과 회전에 따라 변하지 않으므로, 시야 범위를 변경하지 않는 이상 매번 업데이트할 필요는 없다. 

https://stackoverflow.com/questions/28286057/trying-to-understand-the-math-behind-the-perspective-matrix-in-webgl

카메라가 움직일 때마다 이러한 행렬들을 알아서 계산해준다면, 우리는 다른 것 신경쓰지 않고 그리기 전에 정점 셰이더에 카메라가 가진 두 행렬을 전달해주기만 하면 된다.
 

카메라 기능

  1. 이동 및 회전
    - 카메라는 우리가 보는 시야를 의미한다. 따라서 상하좌우로 이동할 수 있어야 한다. 회전도 마찬가지다. 회전은 우리가 고개를 돌리는 것에 해당한다.
  2. View Matrix 및 Perspective Projection Matrix 생성
    - 카메라가 이동하거나 회전할 때마다 View Matrix를 알아서 갱신해주어야 한다.

간단한 구현 예

// 이동, 회전, 뷰 행렬 업데이트

// 이동
void Camera::Move(float x, float y, float z)
{
    // 현재 카메라의 위치로부터 (x,y,z)만큼 카메라를 이동
    m_F3Pos.x += x;
    m_F3Pos.y += y;
    m_F3Pos.z += z;
    
    // 위 점의 벡터 버전도 따로 저장
    m_PosVector = XMLoadFloat3(&m_F3Pos);
    
    // 카메라가 이동 및 회전할 때마다 뷰 매트릭스 업데이트
    UpdateMatrix(); 
}

// 회전
void Camera::Rotate(float x, float y, float z)
{
    m_F3Rotation.x += x;
    m_F3Rotation.y += y;
    m_F3Rotation.z += z;
    
    m_RotationVector = XMLoadFloat3(&m_F3Rotation);
    
    UpdateMatrix();
}

// 행렬 업데이트
void UpdateMatrix()
{
    // 변경된 회전 각을 기준으로 새로운 회전 행렬을 생성
    XMMATRIX matCamRotation = XMMatrixRotationRollPitchYawFromVector(m_RotationVector);
    
    // 뷰 행렬을 생성하기 위해선 Eye, At, Up 3가지 데이터가 필요
    // Eye는 카메라의 위치이므로 Move()에서 업데이트되는 데이터를 그대로 사용하면된다.  
    XMVECTOR At = XMVector3Transform(UnitVector::AT, matCamRotation);
    XMVECTOR Up = XMVector3Transform(UnitVector::Up, matCamRotation);
    
    // m_PosVector 위치에서 At 방향을 바라보고, 카메라의 Up벡터가 Up인 
    // 왼손 좌표계 공간으로 이동시키는 행렬을 생성
    m_ViewMatrix = XMMatrixLookToLH(m_PosVector, At, Up);
    
    // 카메라가 회전할 경우 Front, Back, Left, Right 방향도 업데이트 필요
    if(카메라가 회전했다면)
    {
        XMMATRIX matRotation = XMMatrixRotationRollPitchYaw(m_F3Rotation.x, m_F3Rotation.y, 0.f);
        m_ForwardVector  = XMVector3Transform(UnitVector::FORWARD, matRotation);
        m_BackwardVector = XMVector3Transform(UnitVector::BACKWARD, matRotation);
        m_LeftVector     = XMVector3Transform(UnitVector::LEFT, matRotation);
        m_RightVector    = XMVector3Transform(UnitVector::RIGHT, matRotation);
    }
}

UnitVector는 다음과 같이 static 상수로 정의해 놓은 단위 벡터이다.

 

m_ForwardVector와 같은 벡터는 카메라를 기준으로 앞뒤양옆의 방향을 나타내는 벡터이다. 이 벡터들이 필요한 이유는 예를 들어 아래 첫 번째 그림처럼 카메라가 정면(front, +z방향)를 바라보고 있다고 생각해보자.

http://ogldev.atspace.co.uk/www/tutorial13/tutorial13.html

카메라가 오른쪽으로 45도 회전할 경우 카메라가 바라보는 정면은 더 이상 +z방향이 아니다. 따라서 회전할 경우 기존에 front vector를 의미하던 방향 벡터를 현재의 front에 맞게 변경해주어야 한다. 이를 해주지 않으면 카메라가 앞으로 이동할 경우 생각하던 방향이 아닌 +z방향으로 이동할 것이다.   

아래는 투영 행렬을 설정하는 코드이다. 카메라의 시야를 정의하는 4가지 데이터를 전달해주면 투영 행렬을 생성해준다.

void Camera::SetProjectionValues(float fovDegreesY, float aspectRatio, float nearZ, float farZ)
{
	// 1 radian은 부채꼴에서 
	// 반지름 r과 호의 길이 l이 같을 때의 각도를 의미
	// 이때 각도는 대략 57.2958도
	// 따라서 1도는 대략 0.01745328627.. 라디안
	// 이를 단순히 각도(degree)에 곱하여 사용하거나
	// 각도(degree)에 PI/180을 곱하여 사용 -> 삼각 함수 사용할 때를 생각 PI/3이 60도였던거..

	static const float RADIAN_PER_DEGREE = 0.01745328627f;
	float fovRadiansY = fovDegreesY * RADIAN_PER_DEGREE;
	m_ProjectionMatrix = XMMatrixPerspectiveFovLH(fovRadiansY, aspectRatio, nearZ, farZ);
}

 

이동 및 회전은 키보드 입력이나 마우스 입력을 받아서 원하는 키를 누르거나 할 때 실행되도록 하면 된다.

나의 경우 이동은 다음과 같이 W,A,S,D로 하였다. 위아래로 이동은 방향키이다. Move는 위에서 살펴본 함수인데, 좌표를 받는 것이 아닌 벡터를 받는 것만 다르다. 'W' 입력 시 앞으로 이동인데, 카메라의 현재 forward 방향 벡터를 얻어오고 그 방향으로 카메라 스피드만큼 이동하도록 했다.

void Engine::UpdateCamera(float deltaTime)
{
    Camera* pCamera = m_Graphics.GetCamera();
    float fCamSpeed = pCamera->GetSpeed() * deltaTime;
    if (m_Keyboard.IsKeyPressed('W'))
    {
        pCamera->Move(pCamera->GetForwardVector() * fCamSpeed);
    }
    if (m_Keyboard.IsKeyPressed('A'))
    {
        pCamera->Move(pCamera->GetLeftVector() * fCamSpeed);
    }
    if (m_Keyboard.IsKeyPressed('S'))
    {
        pCamera->Move(pCamera->GetBackwardVector() * fCamSpeed);
    }
    if (m_Keyboard.IsKeyPressed('D'))
    {
        pCamera->Move(pCamera->GetRightVector() * fCamSpeed);
    }
    if (m_Keyboard.IsKeyPressed(VK_UP))
    {
        pCamera->Move(0.f, fCamSpeed, 0.f);
    }
    if (m_Keyboard.IsKeyPressed(VK_DOWN))
    {
        pCamera->Move(0.f, -fCamSpeed, 0.f);
    }
}

 

아래는 카메라 회전과 관련된 코드이다. 마우스 왼쪽을 클릭한 상태에서 움직이면 회전한다. 마우스의 이전 좌표를 저장하여 마우스가 어디 방향으로 움직였는지 파악하여 좌우 회전을 구분하였다.

// 마우스를 클릭한 상태에서 움직이면 회전
if (m_Mouse.IsLeftDown())
{
  static float fMouseRotationPerFrame = 0.03f;
  MousePoint curPos = mEvent.GetPos();
  static MousePoint prevPos = {};
  float pitch = 0.f;
  float yaw   = 0.f;

  Camera* pCamera = m_Graphics.GetCamera();
  if (curPos.X < prevPos.X) // If mouse moves to left
  {
  	yaw = -fMouseRotationPerFrame;
  }
  else if (curPos.X > prevPos.X) // If mouse moves to right
  {
  	yaw = fMouseRotationPerFrame;
  }

  if (curPos.Y > prevPos.Y)  // If mouse moves to down
  {
  	pitch = fMouseRotationPerFrame;
  }
  else if (curPos.Y < prevPos.Y) // If mouse moves to up
  {
  	pitch = -fMouseRotationPerFrame;
  }

  pCamera->Rotate(pitch, yaw, 0.f);
  prevPos = curPos;
}



댓글