Computer Graphics/OpenGL

OpenGL 정리 - 12. 카메라, View Transform

surkim 2024. 9. 19. 12:52

이번에는 OpenGL에서 카메라를 다루는 방법에 대해 정리하겠다.

카메라는 3D 그래픽에서 화면을 어떤 시점에서, 어떤 방향으로 바라볼지를 결정하며, 이를 통해 사용자는 3D 공간을 자유롭게 탐색할 수 있다. 특히 카메라 조작과 관련된 수식과 이를 코드로 구현하는 방법을 설명하겠다.


1. 카메라의 기본 개념

3D 그래픽에서 카메라는 가상의 위치에 있으며, 우리가 보는 시야를 결정한다. 이를 위해 카메라 좌표계(Camera Space)에서 View Transform을 유도하는 여러 파라미터들이 필요하다.

카메라 파라미터

  • Camera Position: 카메라의 위치.
  • Camera Target: 카메라가 바라보는 중심점.
  • Camera Up Vector: 카메라 화면의 세로축을 결정하는 벡터. (일반적으로 Y축)

위의 파라미터들을 통해 카메라가 어디를 보고 있는지, 어떻게 배치되어 있는지를 정의하고, 이를 바탕으로 View Matrix를 생성한다. 이 View Matrix는 월드 좌표계를 카메라 좌표계로 변환해준다.

2. 카메라 수식과 구현

2.1. 카메라 좌표계의 축 계산

카메라의 위치, 목표점, 그리고 위쪽 방향을 이용해 카메라 좌표계의 축을 계산할 수 있다. OpenGL에서는 이를 직접 계산하거나, glm::lookAt 함수를 사용하여 간편하게 계산할 수 있다.

카메라 좌표계의 축을 계산하는 방식은 다음과 같다:

auto cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);  // 카메라 위치
auto cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);  // 카메라가 바라보는 점
auto cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);  // 카메라의 위쪽 방향

auto cameraZ = glm::normalize(cameraPos - cameraTarget);  // 카메라의 Z축 (전방 벡터의 반대 방향)
auto cameraX = glm::normalize(glm::cross(cameraUp, cameraZ));  // 카메라의 X축 (카메라의 오른쪽)
auto cameraY = glm::cross(cameraZ, cameraX);  // 카메라의 Y축 (카메라의 위쪽)

위 수식을 통해 카메라 좌표계에서 X, Y, Z 축을 계산할 수 있으며, 이 축들을 조합해 View Matrix를 만든다. 이 계산을 glm::lookAt 함수로 간편하게 처리할 수 있다.

2.2. glm::lookAt 함수

glm::lookAt 함수는 카메라의 위치, 목표점, 위쪽 방향을 받아 View Matrix를 생성해준다. 이를 통해 카메라의 변환을 손쉽게 구현할 수 있다.

auto view = glm::lookAt(cameraPos, cameraTarget, cameraUp);

이렇게 생성된 View Matrix는 물체를 카메라 시점에서 바라본 형태로 변환해준다.


3. 카메라 조작

카메라를 움직이기 위해서는 카메라의 위치와 방향을 조작해야 한다. 이를 위해 키보드 입력마우스 움직임을 통해 카메라를 이동하거나 회전시킬 수 있다.

3.1. 카메라의 위치 이동

W/A/S/D/Q/E 키를 사용하여 카메라를 전후좌우, 상하로 움직일 수 있다. 이를 구현하는 방식은 간단히 카메라의 위치를 이동하는 수식을 사용한다.

void Context::ProcessInput(GLFWwindow* window) {
    if (!m_cameraControl)
        return;
    const float cameraSpeed = 0.05f;
    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
        m_cameraPos += cameraSpeed * m_cameraFront;
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
        m_cameraPos -= cameraSpeed * m_cameraFront;

    auto cameraRight = glm::normalize(glm::cross(m_cameraUp, -m_cameraFront));
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
        m_cameraPos += cameraSpeed * cameraRight;
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
        m_cameraPos -= cameraSpeed * cameraRight;    

    auto cameraUp = glm::normalize(glm::cross(-m_cameraFront, cameraRight));
    if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS)
        m_cameraPos += cameraSpeed * cameraUp;
    if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS)
        m_cameraPos -= cameraSpeed * cameraUp;
}
3.2. 카메라의 회전 (Yaw와 Pitch)

Yaw(좌우 회전)Pitch(상하 회전)는 마우스 움직임을 이용해 구현한다. 마우스의 움직임을 기반으로 카메라의 각도를 변경하고, 이를 통해 카메라의 방향을 계산한다.

void Context::MouseMove(double x, double y) {
    if (!m_cameraControl)
        return;
    auto pos = glm::vec2((float)x, (float)y);
    auto deltaPos = pos - m_prevMousePos;

    const float cameraRotSpeed = 0.8f;
    m_cameraYaw -= deltaPos.x * cameraRotSpeed;
    m_cameraPitch -= deltaPos.y * cameraRotSpeed;

    if (m_cameraYaw < 0.0f)   m_cameraYaw += 360.0f;
    if (m_cameraYaw > 360.0f) m_cameraYaw -= 360.0f;

    if (m_cameraPitch > 89.0f)  m_cameraPitch = 89.0f;
    if (m_cameraPitch < -89.0f) m_cameraPitch = -89.0f;

    m_prevMousePos = pos;
}

위 코드는 마우스 움직임을 기반으로 YawPitch 각도를 업데이트한다. Pitch는 상하 회전이므로 89도 이상의 각도를 제한하여 카메라가 완전히 뒤집히는 현상을 방지한다.

3.3. 카메라의 새로운 방향 벡터 계산

회전 각도에 따라 카메라의 새로운 방향 벡터를 계산하여 Camera Front 벡터를 업데이트한다.

m_cameraFront =
glm::rotate(glm::mat4(1.0f), glm::radians(m_cameraYaw), glm::vec3(0.0f, 1.0f, 0.0f)) *
glm::rotate(glm::mat4(1.0f), glm::radians(m_cameraPitch), glm::vec3(1.0f, 0.0f, 0.0f)) *
glm::vec4(0.0f, 0.0f, -1.0f, 0.0f);

위 수식을 통해 YawPitch 각도에 따른 새로운 Camera Front 방향을 계산할 수 있다.


4. 카메라의 실제 적용

최종적으로 카메라 조작을 반영한 View Matrix는 렌더링할 때 적용된다. View MatrixProjection Matrix를 곱하여 물체가 화면에 올바르게 표시되도록 한다.

    m_cameraFront =
        glm::rotate(glm::mat4(1.0f), glm::radians(m_cameraYaw), glm::vec3(0.0f, 1.0f, 0.0f)) *
        glm::rotate(glm::mat4(1.0f), glm::radians(m_cameraPitch), glm::vec3(1.0f, 0.0f, 0.0f)) *
        glm::vec4(0.0f, 0.0f, -1.0f, 0.0f);

    auto projection = glm::perspective(glm::radians(45.0f),
        (float)m_width / (float)m_height, 0.01f, 30.0f);
    auto view = glm::lookAt(
        m_cameraPos,
        m_cameraPos + m_cameraFront,
        m_cameraUp);
  
    // cube positions을 도는 for문 안에서
    auto& pos = cubePositions[i];
    auto model = glm::translate(glm::mat4(1.0f), pos);
    model = glm::rotate(model,
            glm::radians((float)glfwGetTime() * 120.0f + 20.0f * (float)i),
            glm::vec3(1.0f, 0.5f, 0.0f));
    auto transform = projection * view * model;
    m_program->SetUniform("transform", transform);
    glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);
    //