본문 바로가기

Game/Graphics

Learn OpenGL - Getting started : Camera

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


Camera


 앞의 튜토리얼에서 우리는 뷰 매트릭스와 뷰 매트릭스를 사용해 장면 주위를 이동하는 방법에 대해


논의했다. OpenGL 자체는 카메라 개념에 익숙하지 않지만 장면의 모든 객체를 반대 방향으로 이동해


움직이는 듯한 착각을 불러 일으켜 시뮬레이션 할 수 있다.



 이 튜토리얼에서는 OpenGL에서 카메라를 설정하는 방법에 대해 설명한다. 3D 장면에서 자유롭게


움직일 수 있는 FPS  스타일의 카메라에 대해 설명한다. 이 튜토리얼에서는 키보드 및 마우스 입력에 대해


설명하고 사용자 정의 카메라 클래스로 마무리한다.






Camera/View space


 카메라/뷰 공간에서 우리는 카메라의 관점에서 볼 때 모든 정점 좌표를 장면의 원점이라고 말한다.


뷰 행렬은 모든 월드 좌표를 카메라 위치와 관련된 뷰 좌표로 변환한다. 카메라를 정의하기 위해


우리는 세계 공간에서의 위치, 바라보는 방향, 오른쪽을 가리키는 벡터 및 카메라에서 위쪽을 가리키는


벡터가 필요하다.




1. Camera position


 카메라 위치를 얻는 것은 쉽다. 카메라 위치는 기본적으로 카메라의 위치를 가리키는 월드 공간의 벡터이다.


이전 튜토리얼에서 카메라를 설정한 위치와 동일한 위치에 카메라를 설정했다.

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

양의 z축이 사용자쪽으로 화면을 통과한다는 것을 잊지 말아라. 카메라를 뒤로 이동하려면 z축의 양의 방향으로 이동해라.


2. Camera direction


 다음으로 요구되는 벡터는 카메라의 방향이다. 그것이 가리키는 방향으로 우리는 카메라가 장면의 원점을 


가리키도록 할 것이다: (0,0,0). 우리가 두 벡터를 빼면 이 두 벡터의 차이인 벡터를 얻을 수 있다는 것을


기억해라. 따라서 장면의 원점 벡터에서 카메라 위치 벡터를 빼면 방향 벡터가 된다.


카메라가 음의 z방향을 가리킨다는 것을 알기 때문에 방향 벡터가 카메라의 양의 z축을 향하게 한다.


우리가 빼기 순서를 바꾼다면 이제는 카메라의 양의 z축을 가리키는 벡터를 얻을 수 있다.

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

name direction 벡터는 실제로 그것이 복표로 하는 것과 반대 방향을 가리키고 있기 때문에 가장 잘 선택된 이름은 아니다.


3. Right axis


 우리가 필요로 하는 다음 벡터는 카메라 공간의 양의 x축을 나타내는 오른쪽 벡터이다. 올바른 벡터를 얻으려면


먼저 (월드 공간에서) 위로 향하는 위쪽 벡터를 지정해 약간의 트릭을 사용한다. 그런 다음 2단계 위쪽 벡터와


방향 벡터에 교차 곱을 수행한다. 교차 곱의 결과는 두 벡터에 수직인 벡터이므로 양의 x축 방향을


가리키는 벡터를 가져온다. (예: 벡터를 바꾸려면 음의 x축을 가리키는 벡터를 얻습니다)

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


4. Up axis


 이제 우리는 x축 벡터와 z축 벡터를 모두 가지므로 카메라의 양수 y축을 가리키는 벡터를 검색하는 것은 쉽다.


오른쪽 벡터와 방향 벡터의 외적을 취한다:

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

  교차 제품과 몇 가지 트릭을 사용해 뷰/카메라 공간을 구성하는 모든 벡터를 만들 수 있었다.


이 카메라 벡터를 사용해 카메라를 만드는 데 매우 유용한 것으로 입증된 LookAt 행렬을 만들 수 있다.






Look At


 행렬에 대한 좋은 점은 3개의 직각 (또는 비선형) 축을 사용해 좌표 공간을 정의하면 해당 3축과


변환 벡터가 있는 행렬을 생성할 수 있다. (벡터에 변환 공간을 곱하는 것으로)


이것은 LookAt 행렬이 하는 것과 정확히 같다. 이제 우리는 3개의 직사각형 축과 위치 벡터를 가지고


우리 자신의 LookAt 행렬을 만들 수 있다.



 R은 오른쪽 벡터, U는 위쪽 벡터, D는 방향 벡터, P는 카메라의 위치 벡터이다.


우리는 결국 우리가 움직이기 원하는 방향과 반대 방향으로 세계를 translate하기를 원하는 방향과


반대 방향으로 세계를 translate하기를 원하기 때문에 위치 벡터가 뒤집혔음에 유의해라.


이 LookAt 행렬을 뷰 행렬로 사용하면 모든 월드 좌표가 방금 정의한 뷰 공간으로 효과적으로 변환된다.


LookAt 행렬은 정확히 말한대로 행한다. 주어진 대상을 보는 행렬을 만든다.



 운 좋게도 GLM은 이미 우리를 위해 이 모든 일을 한다. 카메라 위치, 목표 위치 및 월드 공간에서 위쪽 벡터를


나타내는 벡터 (오른쪽 벡터를 계산하는데 사용한 위쪽 벡터)만 지정하면 된다. 그런 다음 GLM은 뷰 매트릭스로


사용할 수 있는 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 함수는 각각 위치, 목표 및 상향 벡터가 필요하다. 이것은 튜토리얼에서 사용된 것과 같은


뷰 매트릭스를 생성한다.



 사용자 입력을 탐구하기 전에 먼저 장면을 따라 카메라를 회전시켜서 약간 펑키한 장면을 만들어 보겠다.


장면의 대상을 (0,0,0)으로 유지한다.



 원의 한 점을 나타내는 각 프레임을 x와 z좌표로 만들기 위해 약간의 삼각법을 사용하고,


이를 카메라 위치에 사용한다. x와 y좌표를 다시 계산함으로써 우리는 원 안에 있는 모든 점들을


가로 지르고 있으므로 카메라는 장면 주위를 회전한다. 이 원을 미리 정의된 반지름만큼 확대하고


GLFW의 glfwGetTime 함수를 사용해 각 반복 반복에 대한 새로운 뷰 행렬을 만든다.

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));  

 코드를 돌리면 다음과 같은 결과를 얻을 수 있다:



이 작은 코드 조각으로 카메라는 이제 시간이 지남에 따라 장면을 둘러 싼다. 반경 및 위치/방향 매개 변수를


자유롭게 실험해 이 LookAt 행렬의 작동 방식을 익혀라.








Look around


 키보드 키를 사용해 이동하는 것만으로는 흥미롭지 않다. 특히 운동을 다소 제한적으로 만들기 때문에 좋지 않다.


마우스를 사용하면 이 단점을 없앨 수 있다.



 장면을 둘러 보려면 마우스 입력을 기반으로 cameraFront 벡터를 변경해야 한다. 그러나 마우스 회전에 따라


방향 벡터를 변경하는 것은 약간 복잡하고, 몇 가지 삼각법이 필요하다. 삼각법을 이해하지 못한다면 걱정하지 말아라.


코드 섹션으로 건너뛰고 코드에 붙여넣기만 하면 된다. 더 많은 것을 알고 싶다면 언제든지 다시 올 수 있다.



Euler angles


 오일러 각은 1700년대 어딘가의 레온 하드 오일러(leonhard Euler)에 의해 정의된 3D에서의 모든 회전을 나타낼


수 있는 3가지 값이다. 3개의 오일러 각이 있다: pitch, yaw, roll. 다음 이미지는 시각적 의미를 부여한다:



 피치는 첫 번째 이미지에서 보았을 때 얼마나 위아래로 보고 있는지를 나타내는 각도이다.


두 번째 이미지는 왼쪽 또는 오른쪽을 바라보는 크기를 나타내는 yaw 값을 보여준다.


roll은 우리가 우주 비행 카메라에서 주로 사용하는 것처럼 굴리는 정도를 나타낸다.


각 오일러 angle은 단일 값으로 표현되며 3개 모두의 조합으로 3D의 모든 회전 벡터를 계산할 수 있다.



 카메라 시스템의 경우 pitch와 yaw만 신경쓴다. 여기서는 roll 값을 논의하지 않겠다.


pitch와 yaw값이 주어지면 새로운 방향 벡터를 나타내는 3D 벡터로 변환할 수 있다.


yaw와 pitch값을 방향 벡터로 변환하는 과정은 약간의 삼각법을 필요로 하며 기본적인 경우부터 시작한다:



 빗변의 길이를 1로 정의하면 삼각 함수 (soh cah toa)에서 adjacant side의 길이는 cos x / h = cos x / 1 = cos x 이고,


반대편 길이는 sin y / h = sin 이라는 것을 알 수 있다. 이것은 주어진 각도에 따라 x와 y 방향 모두에서 길이를 검색하기


위한 몇 가지 일반적인 공식을 제공한다. 방향 벡터의 성분을 계산하기 위해 이것을 사용하자:



 이 삼각형은 이전 삼각형과 비슷해 보이므로 x z 평면에 앉아 y 축을 바라 보았다면 y 방향의 길이/강도를 계산할 수 있다.


첫 번째 삼각형 이미지로부터 주어진 피치에 대한 y 값이 sin θ와 같은 것을 볼 수 있다:

direction.y = sin(glm::radians(pitch)); // Note that we convert the angle to radians first 

 여기서 우리는 y 값만을 업데이트한다. 그러나 주의 깊게 살펴보면 x 및 z 구성 요소가 영향을 받을 수도 있다.


삼각형으로부터 우리는 그들의 값이 같다는 것을 알 수 있다:

direction.x = cos(glm::radians(pitch));
direction.z = cos(glm::radians(pitch));

 yaw 값에 필요한 구성 요소를 찾을 수 있나 보자:

 피치 삼각형처럼 x 성분은 cos (yaw) 값에 의존하고, z 값은 yaw 값에 의존한다는 것을 알 수 있다.


이전 값에 이 값을 더하면 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 값을 얻나?







More input


yaw와 pitch 값은 마우스 (또는 컨트롤러/조이스틱)의 움직임에서 얻는다. (yaw는 수평, pitch는 수직)


아이디어는 마지막 프레임의 마우스 위치를 저장하는 것이다. 현재 프레임에 마우스 값이 마지막 프레임의 값과


비교해 얼마나 많이 변했는지 계산한다. 수평/수직 차이가 클수록 pitch 또는 yaw 값을 더 많이 업데이트하므로


카메라를 더 많이 움직여야한다.



 먼저 GLFW에서 커서를 숨기고 캡처해야 한다고 알려줄 것이다. 커서를 캡처하면 응용 프로그램이 포커스를 잃으면


마우스 커서가 윈도우 내에 남아있게 된다. 다음과 같은 간단한 구성 호출을 통해 이를 수행할 수 있다:

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. 마지막 프레임 이후 마우스의 오프셋을 계산해라


    2. 오프셋 값을 카메라의 yaw와 pitch값에 추가해라


    3. 최대 / 최소 pitch 값에 몇가지 제약 조건을 추가해라.


    4. 방향 벡터를 계산해라


 첫 번째 단계는 마지막 프레임 이후의 마우스 오프셋을 계산하는 것이다. 우선 응용 프로그램에서 마지막 마우스 위치를


저장해야 한다. 이 위치는 화면의 중심으로 설정된다:

float lastX = 400, lastY = 300;

 그런 다음 마우스의 콜백 함수에서 마지막 프레임과 현재 프레임 간의 오프셋 이동을 계산한다:

float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // reversed since y-coordinates range from bottom to top
lastX = xpos;
lastY = ypos;

float sensitivity = 0.05f;
xoffset *= sensitivity;
yoffset *= sensitivity;

 우리는 오프셋 값에 민감도 값을 곱한다. 이 곱셈을 생략하면 마우스 움직임이 너무 강해진다. 마음에 드는 감도값을 선택해라.


 다음으로 전역적으로 선언된 pitch와 yaw 값에 오프셋 값을 추가한다:

yaw   += xoffset;
pitch += yoffset;  

 세 번째 단계에서는 카메라에 몇 가지 제약 조건을 추가해 사용자가 이상한 카메라 움직임을 만들지 못하도록하고,


pitch는 사용자가 89도와 89도 이하로 볼 수 없도록 제한된다. (보기가 90도에서 뒤집어지기 때문에 89도로 한계 고정)


이렇게하면 사용자는 하늘까지 내려다 볼 수 있지만 더 이상은 볼 수 없게된다. 제약 조건은 제약 조건을 위반할 때마다


제약 조건 값으로 결과 값을 대체하는 방식으로 작동한다:

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

 수평 회전시 사용자를 제한하고 싶지 않으므로 yaw 값에 대한 제한을 설정하지 않는다. 그러나, 만약 당신이 그것을


원한다면 yaw에 제약을 추가하는 것만큼 쉽다.



 네 번째이자 마지막 단계는 이전 섹션에서 설명한대로 결과로 나오는 요와 피치 값으로부터 실제 방향 벡터를 계산하는 것이다:

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 위치로


마우스 콜백 함수가 호출된다는 것이다. 이것은 대개 화면의 중심으로부터 꽤 멀리 떨어진 위치에 있기 때문에 큰 오프셋과


큰 이동 점프가 발생한다. 이 문제가 단순히 글로벌 변수를 정의해 마우스 입력을 처음 받는지를 확인하고, 그렇다면 초기 마우스


위치를 새로운 xpos / ypos 값으로 업데이트함으로써 이 문제를 피할 수 있다. 결과 마우스 이동은 입력된 마우스의 위치좌표를


사용해 오프셋을 계산한다:


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);
}  

  이제 3D 장면을 자유롭게 움직일 수 있다.







Zoom


 카메라 시스템을 조금 더 추가하기 때문에 확대/축소 인터페이스도 구현할 것이다. 앞의 튜토리얼에서 우리는 시야 (field of view)


또는 fov가 장면을 얼마나 많이 볼 수 있는지를 정의했다. 시야가 작아지면 장면의 투영된 공간이 작아져서


확대된 것처럼 보인다. 확대하려면 마우스의 스크롤 휠을 사용해보아라. 마우스 이동 및 키보드 입력과 마찬가지로


마우스 스크롤 기능을 위한 콜백 함수가 있다:

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 값이므로 줌 레벨을 1.0f~45.0f 사이로 제한하려고 한다.



 지금 우리는 반복 투영을 GPU에 perspective projection 행렬을 업로드해야 하지만 이번에는 fov 변수를 해당 뷰 필드로


업로드해야한다:

projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);  

 그리고 마지막으로 스크롤 콜백 함수를 등록하는 것을 잊지마라:

glfwSetScrollCallback(window, scroll_callback);

  우리는 3D 환경에서 자유로운 움직임을 허용하는 간단한 카메라 시스템을 구현했다:


 오일러 각을 사용하는 카메라 시스템은 여전히 완벽한 시스템이 아니다. 구속 조건과 설정에 따라 짐벌 잠금 장치를 도입 할 수 있다. 최고의 카메라 시스템은 쿼터니온을 사용해 개발 될 것이다. 그러나 우리는 그것을 나중 주제로 남겨둘 것이다.





Camera class


 다가올 튜토리얼에서는 항상 카메라를 사용해 장면을 쉽게 둘러보고 모든 각도의 결과를 본다. 그러나 카메라는


각 튜토리얼에서 많은 공간을 차지할 수 있으므로 세부 정보에서 약간을 추상화하고 대부분의 작업은


깔끔하고 작은 추가 기능으로 수행하는 자체 카메라 객체를 만든다. Shader 튜토리얼과 달리 우리는


카메라 클래스를 만드는 방법을 설명하지는 않지만 내부 동작을 알고 싶다면 주석이 달린 소스 코드를 보아라.



 Shader 객체와 마찬가지로 우리는 하나의 헤더 파일로 완전히 만든다. 여기서 카메라 객체를 찾을 수 있다.


지금까지 모든 코드를 이해할 수 있어야한다. 최소한 클래스를 한 번 확인해보고 이와 같은 카메라 객체를


만드는 방법을 확인하는 것이 좋다:

 우리가 도입한 카메라 시스템은 대부분의 목적에 부합하고 오일러 각과 잘 작동하는 FPS와 유사한 카메라이지만 비행 시뮬레이션 카메라와 같은 다른 카메라 시스템을 만들 때는 주의해야한다. 각 카메라 시스템에는 고유한 트릭과 단점이 있으므로 반드시 읽어야한다. 예를 들어, 이 FPS 카메라는 90도 이상의 피치 값을 허용하지 않으며 롤 값을 고려할 때 정적 상향 벡터 (0,1,0)가 작동하지 않는다.








** ** ** ** ** ** ** ** ** **


드디어 Getting started 파트가 끝났다. 이제부터 Lighting 파트로 들어간다.


만들고자 하는 프로젝트에서 Light 효과가 매우 중요하니 더 단단히 마음을 먹고 공부해야겠다.


퐈이팅퐈이팅