본문 바로가기

컴퓨터그래픽스

[OpenGL 공부] Camera (3)

학습을 위해 참고한 사이트

https://learnopengl.com/Getting-started/Camera

 

LearnOpenGL - Camera

Camera Getting-started/Camera In the previous chapter we discussed the view matrix and how we can use the view matrix to move around the scene (we moved backwards a little). OpenGL by itself is not familiar with the concept of a camera, but we can try to s

learnopengl.com

 

목표

  • View 행렬 이해하기
  • 오일러 각도에 기반한 Look around 카메라 구현

 

카메라를 정의하기 위해 필요한 요소

 

물체를 모델 행렬을 통해 월드 공간에 나타냈다면, 원하는 시점에서 바라보기 위해 뷰 공간으로 변환해야 한다.

이때 시점을 카메라라고 할 때, 시점을 변경하기 위해 카메라에서 정의되는 좌표계가 필요하게 되고 물체를 바라보는 방향, 카메라의 위 방향, 앞서 두 방향에 대해 수직인 오른쪽을 가리키는 벡터가 필요하게 된다.

 

따라서 바라보는 지점과 카메라를 이루는 벡터를 Direction, 카메라의 오른쪽 벡터를 Right, 두 벡터에 대해 수직인 벡터를 Up이라고 정의하고, 이를 normalize하여 카메라의 축으로 사용한다.

 

Direction이 카메라 → 관측 지점 방향이 아닌 반대 방향으로 정의되는 이유에 대해서는 다음과 같이 설명하고 있다.

뷰 행렬의 좌표계의 경우 z축이 양수이기를 원하고 관례에 따라(OpenGL에서) 카메라가 음수 z축을 가리키기 때문에 방향 벡터를 부정하려고 합니다. 빼기 순서를 바꾸면 이제 카메라의 양수 z축을 가리키는 벡터를 얻습니다.

 

카메라의 위치와 각 축을 계산하는 방법은 다음과 같다.

 

1. 카메라 위치

glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);

OpenGL에서는 화면 안쪽이 음의 z축이므로, 카메라를 뒤로 이동시키기 위해선 z값을 양수로 설정해야 한다.

 

2. 카메라 방향 (Direction)

카메라의 위치가 정의되었으므로, 두 점의 차를 이용해 벡터를 계산할 수 있다.

glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);

카메라의 위치에서 바라보는 지점의 위치 값을 빼야 제대로 된 Direction을 구할 수 있다.

 

3. 오른쪽 축 (Right)

카메라의 축이 아직 하나밖에 설정되지 않았기 때문에, 카메라의 요소만으로는 또 다른 축을 계산할 수 없다. 따라서 월드 좌표계에서 y축을 가리키는 노멀벡터를 설정하여 외적을 수행하는 cross 함수를 통해 Right 축을 계산한다.

glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); 
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));

 

4. 위쪽 축 (Up)

이제 Right와 Direction의 외적으로 Up 벡터를 계산할 수 있다.

glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);

 

View 행렬

카메라의 3개의 수직인 축을 구성하였으므로, 이를 월드 좌표계에 적용하여 뷰 공간으로 변환할 수 있다.

이때 월드 좌표계에 실제로 카메라 객체가 이동, 회전하여 시점을 재구성하는 것이 아니라 월드 좌표계를 이동, 회전하여 카메라를 구성하기 때문에 반대 방향으로 변환 행렬이 구성되어야 한다. 따라서 이동 변환(translate)의 경우 부호가 반대되고, 회전 변환(rotate)인 경우 전치되어야 한다. 이를 반영한 view 행렬은 아래와 같다.

R : Right, U : Up, D : Direction, P : position

 

위의 모든 과정을 glm 라이브러리에선 glm::lookAt 함수를 사용하여 카메라의 위치, 카메라의 방향, 카메라의 위쪽 방향만 지정하면 view 행렬을 계산하여 준다.

glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
        
glm::mat4 view;
view = glm::lookAt(cameraPos, cameraFront, cameraUp);

 

 

오일러 각도(Euler angles)

하늘을 날고 있는 종이비행기를 상상해 보자. 비행기는 바람에 따라서 더 높게 올라가거나, 오른쪽으로 방향을 틀거나, 혹은 회전할 수도 있을 것이다.

이처럼 종이비행기가 자유롭게 비행을 하면서 보이는 움직임에 대해 세 가지의 회전축을 정의할 수 있게 되는데, 아래의 그림처럼 Pitch, Yaw, Roll을 정의할 수 있다.

 

 

각 축에 따른 회전반경을 정의

Pitch는 얼마나 위, 아래를 바라보는지 나타내는 축이며, x축의 회전반경으로 정의된다.

Yaw는 좌, 우로 바라보는 각도로 y축의 회전반경으로 정의된다.

Roll은 물체가 향하는 방향에 축이 설정되어(z축), 물체 자체가 회전하는 반경을 나타낸다.

 

Pitch, Yaw, Roll 이해하기

삼각법을 통해 인접한 변의 길이를 구할 수 있다.
y축에서 아래로 바라본 경우

 

카메라를 위에서(y축에서) 아래로 바라본 관점에서 삼각법을 대입하면, yaw 각도를 얻을 수 있다.

카메라의 위쪽 방향이 y축이라고 생각할 때, 이는 카메라의 좌우 각도를 의미한다.

따라서 x-z 평면에서 정의되는 방향벡터는 yaw에 따른 cos, sin 값을 계산하여 구할 수 있다.

 

x축에서 원점을 바라보는 경우

 

이번엔 x축에 시점을 나란히 해서 원점을 바라본다고 생각해 보자.

pitch는 단순히 y축과 바닥인 x-y평면이 이루고 있는 각도라는 점에서 yaw보다 직관적으로 받아들일 수 있다.

y 평면에서 정의되는 방향벡터 역시 pitch의 cos, sin 값으로 알 수가 있다.

 

일반적인 카메라 시스템에서 yaw와 pitch만 고려하므로, 이를 바탕으로 카메라의 방향벡터를 다음과 같이 구성할 수 있다.

direction.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
direction.y = sin(glm::radians(pitch));
direction.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));

방향벡터를 각각의 축으로 분해하여 서로 영향을 받는 요소끼리 곱하는 것으로 계산할 수 있다. 

 

 

일반적으로 OpenGL에서는 카메라의 방향벡터는 +z축의 방향과 동일하고, -z축에 바라보는 대상이 위치하기 때문에, 위의 그림처럼 +x축을 기준으로 yaw 각도가 계산되는 것이 직관적이지 않다.

따라서 -z축을 기준으로 yaw가 계산될 수 있도록 yaw를 시계방향으로 90도 회전한다.

yaw = -90.0f;

 

 


Look Around 카메라 구현

이제 오일러 각도를 카메라 시스템에 적용하여 Look around 방식의 카메라를 구현할 수 있다.

GLFW는 마우스 커서 움직임에 의한 콜백 함수를 등록할 수 있으며, 앞서 구한 카메라의 방향벡터를 마우스의 움직임과 연동하도록 하는 함수를 구현하고 등록하여 사용한다.

glfwSetCursorPosCallback(window, mouse_callback);

 

mouse_callback 함수의 동작 과정은 대략 다음과 같다.

  1. 마지막 프레임 이후 마우스의 오프셋을 계산합니다.
  2. 오프셋 값을 카메라의 yaw와 pitch 값에 추가합니다.
  3. 최소/최대 pitch 값에 몇 가지 제약 조건을 추가합니다.
  4. 방향 벡터를 계산합니다.

 

float lastX = 400, lastY = 300; 		// w=800, h=600, 마지막 커서 위치
float yaw = -90.0f;				// 카메라 좌우 각도, -z축 기준으로 yaw는 0도
float pitch = 0.0f;				// 카메라 상하 각도

void mouse_callback(GLFWwindow* window, double xpos, double ypos)

현재 마우스 커서의 위치 정보인 xpos, ypos를 받는다.

 

// offset 이동을 계산
float xoffset = xpos - lastX;
float yoffset = lastY - ypos;
lastX = xpos;
lastY = ypos;

// 마우스 민감도 조정
//const float sensivity = 0.1f;
//xoffset *= sensivity;
//yoffset *= sensivity;

1. 이전에 마지막 프레임에 위치했던 커서 위치와의 차를 계산하여 오프셋을 구하고, 변수를 업데이트한다.

 

yaw += xoffset;
pitch += yoffset;

2. 오프셋을 각각 카메라 각도에 더해준다.

 

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

3. pitch값이 너무 높아지거나 낮아지면 카메라가 뒤집히는 현상이 발생할 수 있기 때문에, 상하 90도 이상 움직이지 못하도록 제한한다.

 

glm::vec3 direction;
direction.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
direction.y = sin(glm::radians(pitch));
direction.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
cameraFront = glm::normalize(direction);

4. 카메라의 방향 벡터를 계산하고, view 행렬이 올바르게 계산되도록 정규화한다.

 

bool firstMouse = true;

void mouse_callback(GLFWwindow* window, double xpos, double ypos) {
    if (firstMouse) {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }
    
    /*...*/
}

프로그램이 처음 실행하고 마우스를 움직이면 mouse_callback 함수의 xpos, ypos값이 마지막 커서의 위치와 크게 차이나 카메라가 튀는 현상이 발생하게 된다. 따라서 첫 조작에서만 마지막 프레임의 커서 위치를 xpos, ypos로 설정하도록 하여 이러한 현상을 방지하도록 한다.

 

 

마지막으로 glm::lookAt 함수를 사용하여 view 행렬을 계산한다.

// view matrix
glm::mat4 view;
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);

 

다음은 키보드 조작(Walk around)에 이어서, 마우스 조작(Look around)으로 카메라를 구현한 결과이다.

 

'컴퓨터그래픽스' 카테고리의 다른 글

[OpenGL 공부] camera (2)  (1) 2024.02.07
[OpenGL 공부] Camera (1)  (0) 2024.02.02
[OpenGL 공부] Coordinates Systems  (2) 2024.02.01
[OpenGL 공부] Transformations  (0) 2024.01.20
[OpenGL 공부] Textures (exercises)  (0) 2024.01.16