눈팅하는 게임개발자 블로그
OpenGL Getting Started 2-9 Camera 본문
원본 사이트
learnopengl.com/Getting-started/Camera
번역 사이트
heinleinsgame.tistory.com/12?category=757483
카메라
이전 강좌에서 scene 주위를 움직이기 위해 view 행렬을 사용하는 방법에 대해서 다루었다.
OpenGL 자체는 카메라의 개념과 친숙하지 않다.
하지만 Scene의 모든 오브젝트들을 반대 방향으로 이동시킴으로써 카메라가 움직이는 것처럼
착시효과를 일으켜 시뮬레이션 할 수 있었다.
이번 강좌에서는 OpenGL에서 카메라를 세팅하는 방법을 다룬다.
3D Scene을 자유롭게 이동할 수 있는 FPS 스타일의 카메라를 다룬다.
또한 키보드, 마우스 입력도 다루고 마지막으로 임의의 카메라 클래스를 만드는 것으로 마친다.
카메라 / View Space
Scene의 원점에 있는 카메라의 시점에서 보이는 모든 vertex 좌표들에 대해서 다뤘었는데.
view 행렬은 카메라의 위치와 방향에 따라 world 좌표를 view 좌표로 변환한다.
카메라를 정의하기 위해 world space에서 카메라의 위치, 바라보고 있는 방향, 카메라의 오른쪽을 가리키는 벡터,
카메라의 위쪽을 가리키는 벡터가 필요하다.
이로 인해 카메라의 위치를 원점으로 하고 3개의 수직인 축을 가지는 좌표계를 만들 수 있다.
카메라 위치
카메라의 위치를 얻는 것은 어렵지 않다.
기본적으로 world space의 벡터이다. 이 벡터는 카메라의 위치를 가리킨다.
이전의 강좌에서 설정했던 위치와 같은 위치로 설정한다.
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
z축의 양의 방향은 화면에서 사용자의 방향을 가리킨다는 것을 기억해야 한다.
따라서 카메라의 위치를 뒤로 옮기기 위해선 z축의 양의 방향 쪽으로 이동시켜야 한다.
카메라 방향
필요한 다음 벡터는 카메라가 가리키는 방향이다.
우선 카메라가 scene의 원점을 가리키게 한다 (원점과 카메라의 위치를 빼면 방향 벡터를 얻을 수 있다.)
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);
오른쪽 축
다음 벡터는 카메라 space에서 x축의 양의 방향을 나타내는 오른쪽 벡터이다.
오른쪽 벡터를 얻기 위해 먼저 (world space에서) 위쪽을 가리키는 위쪽 벡터를 지정하여 약간의 트릭을 사용한다.
다음 위쪽 벡터와 2번째 단계의 방향 벡터를 외적한다.
외적의 결과는 두 벡터와 수직인 벡터이므로 x축에 대해 양의 방향을 가리키는 벡터를 얻을 수 있다.
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));
위쪽 축
이제 x축 벡터와 z축 벡터를 얻었다. 카메라에 대해 y축의 양의 방향을 가리키는 벡터를 찾는 것은 쉽다.
오른쪽 벡터와 방향 벡터를 외적하면 얻을 수 있다.
glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);
이와 같이 외적과 약간의 트릭을 이용하여 view/카메라 space를 형성하는 모든 벡터들을 형성할 수 있었다.
이제 이 카메라 벡터들을 사용하여 카메라를 생성하는 데 매우 유용한 LookAt 행렬을 생성할 수 있다.
Look At
행렬에 대한 좋은 점은 3개의 직각인 축을 사용하여 좌표 space를 만들면 3개의 축과 이동 벡터와 함께
행렬을 만들 수 있고, 어떤 벡터든지 이 행렬과 곱하여 좌표 space로 변환할 수 있다는 것이다.
이것이 정확히 LookAt행렬이 수행하는 일이다.
이제 카메라 space를 정의하기 위한 3개의 직각인 축과 벡터를 가지고 있으므로 LookAt 행렬을 만들 수 있다.
R은 오른쪽 벡터, U는 위쪽 벡터, D는 방향 벡터, P는 카메라의 위치 벡터이다.
위치 벡터가 반대로 되어있음을 주의해야 한다.
결국 world를 이동할 곳의 반대로 이동시켜야 하기 때문이다.
이 LookAt 행렬을 view 행렬로서 사용하여 효과적으로 모든 world 좌표들을 방금 정의한 view space로 변환할 수 있다.
다음 LookAt 행렬은 정확히 주어진 타겟을 바라보고 있는 view 행렬을 생성한다.
GLM은 이미 이 작업들을 수행하여 lookAt함수로서 가지고 있다.
카메라의 위치와 타겟 위치, world space의 위쪽을 나타내는 벡터(오른쪽 벡터를 계산하기 위해 사용)을 지정해주기만 하면 된다.
그러면 GLM이 view 행렬로서 사용할 수 있는 LookAt 행렬을 생성해준다.
glm::mat4 view;
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f),
glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3(0.0f, 1.0f, 0.0f));
glm::LookAt 함수는 위치, 타겟, 위쪽 벡터를 각각 매개 변수로 받는다.
결과로 이 전의 강좌에서 사용했던 것과 같은 view 행렬을 생성한다.
유저의 입력을 다루기 전에 scene 주위를 카메라가 돌게 만들어보자. scene의 타겟은 (0, 0, 0)으로 유지한다.
약간의 삼각법을 사용하여 각 프레임에서 원을 따라 가리키는 x, z 좌표를 생성한다.
이 좌표를 카메라의 위치로 사용한다. x, z좌표를 재계산함으로써 원에 대한 모든 지점을 가로질러
카메라가 scene 주위를 돌게끔 만들 수 있다.
이 원을 미리 정의된 radius를 사용하여 확장시킬 수 있고 렌더링 루프가 돌 때마다 GLFW의 glfwGetTime함수를
사용하여 새로운 view 행렬을 생성할 수 있다.
float radius = 10.0f;
float camX = sin(glfwGetTime()) * radius;
float camZ = cos(glfwGetTime()) * radius;
glm::mat4 view;
view = glm::lookAt(glm::vec3(camX, 0.0, camZ), glm::vec3(0.0, 0.0, 0.0), glm::vec3(0.0, 1.0, 0.0));
결과는 다음과 같다.
돌아다니기
카메라를 직접 움직이도록 해보자.
먼저 카메라 시스템을 세팅한다.
먼저 정의해 놓은 카메라 변수들을 사용한다.
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);
이제 LookAt함수는 다음과 같다.
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
먼저 정의한 cameraPos로 카메라 위치를 설정.
방향은 현재 위치 벡터 + 방금 정의한 방향 벡터이다.
이는 카메라의 위치가 변하더라도 카메라는 타겟 방향을 바라보도록 유지한다.
키를 눌렀을 때 cameraPos가 변할 수 있도록 수정한다.
이전에 이미 GLFW의 키보드 입력을 관리하기 위해 processInput 함수를 정의하였다.
이제 새로운 키 커맨드를 추가한다.
void processInput(GLFWwindow *window)
{
...
float cameraSpeed = 0.05f; // adjust accordingly
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
cameraPos += cameraSpeed * cameraFront;
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
cameraPos -= cameraSpeed * cameraFront;
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
}
이제 WASD 키를 누를 때마다 카메라의 위치는 그에 따라 움직인다.
카메라를 움직이는 속도는 cameraSpeed 변수를 통해 조정할 수 있다.
이동 속도
지금은 움직일 때 이동속도로 상수값을 사용했다.
실제로 문제가 없어 보이지만 모든 컴퓨터는 각자 다른 프로세싱 파워를 가지고 있고 그 결과
다른 사람보다 더 많은 프레임을 그릴 수 있기도 한다.
이로 인해 같은 조건에서도 컴퓨터에 따라 이동 속도가 다르게 나타날 수 있는데.
이를 해결하기 위해 모든 종류의 하드웨어에서 이 속도가 동일하도록 해야 한다.
그래픽 응용 프로그램과 게임은 일반적으로 마지막 프레임을 렌더링하는데 걸리는 시간을 저장하는
deltaTime 변수를 기록한다.
이 deltaTime 변수를 모든 속도들에 곱한다면 속도의 균형을 유지하여 모든 유저들이 같은 경험을 하게 할 수 있다.
deltaTime의 값을 계산하기 위해 2개의 전역 변수를 선언한다.
float deltaTime = 0.0f; // 마지막 프레임과 현재 프레임 사이의 시간
float lastFrame = 0.0f; // 마지막 프레임의 시간
이제 각 프레임마다 새로운 deltaTime 값을 계산할 수 있다.
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
속도를 계산할 때 고려할 수 있는 deltaTime이 완성되었으므로 이를 cameraSpeed에 곱해준다.
void processInput(GLFWwindow *window)
{
float cameraSpeed = 2.5f * deltaTime;
...
}
이제 일관된 이동속도를 가진 카메라 시스템이 완성되었다.
둘러보기
이제 카메라를 움직이면서 마우스로 주위를 둘러볼 수 있도록 해보자.
scene의 주위를 둘러보기 위해 마우스 입력에 따라 cameraFront 벡터를 수정해야 한다.
하지만 마우스 회전에 따라 방향 벡터를 수정하는 것은 약간 복잡하며 약간의 삼각법 지식을 요구한다.
오일러 각(Euler Angles)
euler angle은 1700년대 Leonhard Euler에 의해 정의된 3D 상에서 모든 회전을 나타낼 수 있는 3개의 값이다.
아래 이미지는 3개의 euler angle을 보여준다.
pitch는 첫 번째 이미지에서 볼 수 있듯이 위나 아래를 어느정도만큼 둘러볼 것인가에 대해 묘사한다.
yaw의 값은 왼쪽이나 오른쪽을 둘러보는 정도를 나타낸다.
roll은 space-flight 카메라에서 주로 사용되는 roll을 얼마나 할 것인지에 대해 나타낸다.
각각의 euler angle은 하나의 값으로 나타나고 3개의 값을 사용하여 3D 상의 모든 회전 벡터를 계산할 수 있다.
카메라 시스템을 위해서는 yaw와 pitch만을 고려할 것이므로 여기서 roll 값은 다루지 않는다.
pitch와 yaw가 주어지면 그들을 새로운 방향 벡터를 나타내는 3D 벡터로 변환할 수 있다.
yaw와 pitch 값을 방향 벡터로 변환하는 것은 약간의 삼각법을 필요로 한다.
아래는 기본적인 예제이다.
빗변의 길이를 1로 정의했다면 삼각법에 의해 인접한 변의 길이가
cos x, 반대편 변의 길이는 sin y임을 알 수 있다.
이와 같이 각을 통해 x방향과 y방향의 길이를 구할 수 있는 일반적인 공식을 얻을 수 있다.
방향 벡터의 요소들을 계산하기 위해 이 공식을 써보자.
해당 삼각형은 이전의 삼각형과 비슷하다.
xz평면 위에서 y축을 향하여 바라보고 있다고 생각하면
첫 번째 삼각형을 기반으로 하여 y방향의 길이와 크기를 계산할 수 있다.
위 이미지로부터 y값이 주어진 pitch에 대한 sin 값과 같다는 것을 알 수 있다.
direction.y = sin(glm::radians(pitch)); // 먼저 각을 radian으로 변환해야 한다는 것을 알아두세요.
여기선 y값만 수정하게 되지만 주의 깊게 살펴보면 x, z 요소 또한 수정할 수 있다.
이 삼각형으로부터 이들의 값은 다음과 같다.
direction.x = cos(glm::radians(pitch));
direction.z = cos(glm::radians(pitch));
yaw 값을 위해 필요한 요소들을 찾을 수 있는지 살펴보자.
pitch 삼각형과 마찬가지로 x요소가 cos(yaw)값에 따라 다르다는 것을 확인할 수 있고 z값은 yaw 값의 sin 값에 따라
다르다는 것을 알 수 있다.
이전의 결과 값에서 이 값들을 추가하여 pitch와 yaw 값에 의한 최종 방향 벡터를 구할 수 있다.
direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw));
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
이는 yaw와 pitch 값을 주변을 둘러볼 수 있도록 해주는 3차원 방향벡터로 변환해주는 공식이다.
이제 yaw와 pitch 값을 어떻게 얻는지를 알아야 한다.
마우스 입력
yaw와 pitch 값은 마우스 움직임에서 얻을 수 있다.
수평에 대한 마우스 움직임은 yaw에 영향을 끼치고 수직에 대한 마우스 움직임은 pitch에 영향을 끼친다.
마지막 프레임의 마우스 위치를 저장하고 현재 프레임에서 마지막 프레임에서의 값과 현재 프레임에서의
값을 비교하여 마우스의 값이 얼마나 많이 바뀌었는지를 계산함으로써 움직임에 대한 값을 얻어낸다.
수평/수직으로 더 많이 높아졌을 수록 pitch나 yaw의 값이 더 크게 업데이트된다.
따라서 카메라는 더 많이 움직이게 된다.
먼저 GLFW에게 커서를 숨기고 capture해야 한다고 알려주어야 한다.
커서를 Capture한다는 것은 응용 프로그램이 포커스되면 마우스 커서를 윈도우 창 안에 가두어 놓는 것을 의미한다.
이를 위한 간단한 설정 명령은 다음과 같다.
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
이 명령을 호출한 후 마우스를 움직일 때마다 보이지 않고 윈도우 창을 떠날 수 없게 된다.
이는 완벽히 FPS 카메라 시스템을 위한 것이다.
pitch와 yaw 값을 계산하기 위해 GLFW에게 마우스 움직임 이벤트를 듣고 있으라고 알려주어야 한다.
다음과 같은 프로토타입의 콜백 함수를 생성함으로써 이를 수행할 수 있다.
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
여기서 xpos와 ypos는 현재 마우스의 위치를 나타낸다.
GLFW에 이 콜백 함수를 등록하기만 하면 마우스가 움직일 때마다 mouse_callback 함수가 호출된다.
glfwSetCursorPosCallback(window, mouse_callback);
FPS 스타일 카메라를 위해 마우스 입력을 관리할 때 방향 벡터를 구하기 전에 거쳐야 할 여러 단계들이 존재한다.
1. 마지막 프레임부터의 마우스 offset을 계산한다.
2. 카메라의 yaw와 pitch 값에 offset 값을 더한다.
3. pitch 값에 최댓값/최솟값을 설정한다.
4. 방향 벡터를 계산한다.
첫 번째 단계는 마지막 프레임으로부터 마우스 offset을 구하는 것이다.
먼저 응용 프로그램에서의 마지막 마우스 위치를 저장해야 한다.
마우스이 위치는 처음에 화면의 중앙으로 초기화시킨다.
float lastX = 400, lastY = 300;
다음 마우스의 콜백 함수에서 마지막 프레임과 현재 프레임 사이의 움직임 offset을 계산한다.
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // y 좌표의 범위는 밑에서부터 위로가기 때문에 반대로 바꿉니다.
lastX = xpos;
lastY = ypos;
float sensitivity = 0.05f;
xoffset *= sensitivity;
yoffset *= sensitivity;
offset 값을 sensitivity 값과 곱하는 것은 마우스의 움직임을 적절히 조절하기 위함이다.
다음 전역으로 선언한 pitch, yaw 값에 offset 값을 더한다.
yaw += xoffset;
pitch += yoffset;
세 번째 단계에서는 카메라에 제한 사항을 추가하여 사용자의 카메라 움직임을 제한한다.
pitch는 89도 이상으로 위를 볼 수 없도록 제한한다.
또한 -89도 밑으로도 허용하지 않는다.
이는 사용자가 위로 하늘을 볼 수 있고 밑으로 자신의 발을 볼 수 있도록 하고 그 이상은 보지 못하도록 한다.
이는 조건을 위반할 때마다 위반한 값을 대체하는 것으로 수행할 수 있다.
if(pitch > 89.0f)
pitch = 89.0f;
if(pitch < -89.0f)
pitch = -89.0f;
수평으로의 회전은 제한을 두지 않기 때문에 yaw 값에 대한 제한은 걸지 않는다.
마지막으로 이전 섹션에서 다루었던 yaw와 pitch 값으로부터 실제 방향 벡터를 계산하는 것이다.
glm::vec3 front;
front.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw));
front.y = sin(glm::radians(pitch));
front.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
cameraFront = glm::normalize(front);
다음 계산된 방향 벡터는 마우스 움직임으로부터 계산된 모든 회전을 포함한다.
cameraFront 벡터가 이미 glm의 lookAt 함수에 포함되어 있기 때문에 세팅할 수 있다.
지금 이대로 코드를 실행한다면 마우스 커서 포커스를 윈도우가 처음 받을 때마다
카메라가 크게 점프하는 것을 볼 수 있다.
이는 최초 커서가 윈도우 창에 들어가자마자 화면에서의 마우스 위치가
마우스 콜백 함수의 xpos, ypos 파라미터로 들어가기 때문이다.
이는 일반적으로 화면의 중앙과 멀리 떨어져 있는 위치이기 때문에 큰 offset을 생성하여 크게 점프하게 되는 것이다.
간단히 전역 bool 변수를 선언하여 이 문제를 피할 수 있다.
이 변수는 마우스 입력이 처음으로 들어온 것인지 확인하고 그렇다면 마우스 위치의 초기값을
새로운 xpos, ypos값으로 수정한다.
결과적으로 마우스 움직임의 offset을 계산하기 위해 윈도우 창에 들어온 마우스 위치 좌표를 사용할 수 있다.
if(firstMouse) // this bool variable is initially set to true
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
최종 코드는 다음과 같다.
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
if(firstMouse)
{
lastX = xpos;
lastY = ypos;
firstMouse = false;
}
float xoffset = xpos - lastX;
float yoffset = lastY - ypos;
lastX = xpos;
lastY = ypos;
float sensitivity = 0.05;
xoffset *= sensitivity;
yoffset *= sensitivity;
yaw += xoffset;
pitch += yoffset;
if(pitch > 89.0f)
pitch = 89.0f;
if(pitch < -89.0f)
pitch = -89.0f;
glm::vec3 front;
front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
front.y = sin(glm::radians(pitch));
front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
cameraFront = glm::normalize(front);
}
줌(확대 / 축소)
카메라 시스템의 확장으로서 줌 인터페이스를 구현할 수 있다.
이전의 강좌에서 Field of view 혹은 fov가 scene에서 얼만큼을 볼 수 있는지를 정의하는 것이라고 언급했었다.
field of view가 작아지면 scene projected space는 작아지기 때문에 zoom in 되는 것과 같은 착시효과를 줄 수 있다.
zoom in을 하기 위해 마우스 스크롤 휠을 사용한다.
마우스 움직임, 키 입력과 마찬가지로 마우스 스크롤에 대한 콜백 함수가 있다.
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
if(fov >= 1.0f && fov <= 45.0f)
fov -= yoffset;
if(fov <= 1.0f)
fov = 1.0f;
if(fov >= 45.0f)
fov = 45.0f;
}
스크롤 할 때 yoffset 값이 수직으로 스크롤한 정도를 나타낸다.
scroll_callback 함수가 호출될 때 전역으로 선언된 fov 변수의 내용을 수정한다.
45.0f가 기본 fov 값이기 때문에 zoom level을 1.0f와 45.0f 사이로 제한한다.
이제 루프가 돌 때마다 GPU에 perspective projection 행렬을 업로드 해야 한다.
이번에는 fov 변수를 field of view 값으로서 사용한다.
projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);
마지막으로 스크롤 콜백 함수를 등록한다.
glfwSetScrollCallback(window, scroll_callback);
이제 3D 환경에서 자유롭게 움직일 수 잇는 간단한 카메라 시스템이 구현되었다.
카메라 클래스
다가오는 강좌에서 항상 scene을 쉽게 둘러보고 결과를 모든 각도에서 볼 수 있도록 카메라를 사용한다.
하지만 카메라는 각 강좌마다 약간의 공간을 차지하기 때문에 세부사항을 약간 추상화하여
카메라 오브젝트를 만들어 대부분의 일을 약간의 추가사항과 함께 깔끔하게 수행할 수 있도록 한다.
'공부한거 > OpenGL' 카테고리의 다른 글
OpenGL Lighting 3-2 Basic Lighting (0) | 2020.10.11 |
---|---|
OpenGL Lighting 3-1 Color (0) | 2020.10.11 |
OpenGL Getting Started 2-8 Coordinate System (0) | 2020.09.29 |
OpenGL Getting Started 2-7 Translation (0) | 2020.09.27 |
OpenGL Getting Started 2-6 Texture (0) | 2020.09.24 |