Back to top

LearnOpenGL(07) - 좌표계

작성날짜 2022/06/30

이전글:

다음글: LearnOpenGL(08) - 카메라


개요


지난 챕터에서 버텍스들을 변환 시킬 때 행렬을 이용하는 것의 장점을 살펴봤습니다. OpenGL은 버텍스 셰이더들이 실행된 후 우리가 보여주길 원하는 버텍스들이 정규화된 좌표계(NDC - Normalized Device Coordinate)에 있을 것으로 예상합니다. 즉, 각 버텍스들의 x, y, z 좌표는 -1.0에서 1.0 사이여야 합니다. 이 범위 밖의 좌표는 표시되지 않습니다. 이 때 주로 우리가 해주는 일은 좌표의 범위(또는 공간)을 정해주고 버텍스 셰이더에서 좌표를 NDC로 변환해주는 것입니다. 그런 다음 이 NDC는 레스터라이저에게 전달되어 2D 좌표/픽셀로 변환됩니다.

 

좌표를 NDC로 변환하는 것은 일반적으로 객체의 버텍스를 최종적으로 NDC로 변환하기 전에 여러 좌표계로 변환하는 단계별 방식으로 수행됩니다. 버텍스들을 여러 중간 좌표계로 변환하는 이유는 각 좌표계에서 특정 연산/계산이 더 쉽기 때문입니다. 중요하게 알아둘 5개의 좌표계가 있습니다.

  • 로컬 공간
  • 월드 공간
  • 뷰 공간
  • 클립 공간
  • 스크린 공간

위 리스트는 최종 결과가 출력되기 전에 버텍스들이 변환되는 상태의 일부분들입니다.

 

아마 여러분들은 저 공간이나 좌표계가 무엇인지 혼란스러울 텐데 각 단계들을 대략적으로 설명해 드리겠습니다.


The Global Picture


한 좌표계에서 다른 좌표계로 변환할때 가장 중요한 점은 모델(Model), 뷰(View), 투영(Projection) 행렬 같은 여러 행렬을 사용한다는 점입니다. 버텍스 좌표는 우선 로컬 공간에서 로컬 좌표를 가지며 몇가지 과정을 거쳐, 월드 좌표, 뷰 좌표, 클립 좌표를 지나 결국 스크린 좌표를 가지게 됩니다. 다음 이미지는 각 과정과 변환이 무얼을 하는지 보여줍니다.

article_img_0_rte_image_58.png

  1. 로컬 좌표는 로컬 원점을 기준으로 한 객체의 좌표입니다. 객체가 시작되는 좌표입니다.
  2. 다음 단계는 표컬 좌표를 더 큰 공간인 월드-공간 좌표로 변환하는 것입니다. 이 좌표는 다른 객체들과 함께 월드의 원점에 대해 상대적으로 배치됩니다.
  3. 다음으론 월드 좌표를 뷰-공간 좌표로 변환합니다. 각 좌표를 카메라의 시점처럼 보이도록 변환합니다.
  4. 뷰 공간으로 변환되면 좌표들을 클립 좌표로 변환시켜야 합니다. 클립 좌표는 -1.0부터 1.0까지의 범위이며 화면에 보일 버텍스들을 결정합니다. 이 때 원근투영을 사용하고 싶다면 원근을 추가할 수 있습니다.
  5. 마지막으로 -1.0과 1.0의 좌표를 glViewport에 의해 정의된 좌표 범위로 변환하는 뷰포트 변환이라고 하는 프로세스에서 클립 좌표를 화면 좌표로 변환합니다. 그런 다음 결과 좌표를 레스터라이저로 보내 프레그먼트로 바꿉니다.


Going 3D


이제 3D좌표를 2D로 변환하는 법을 알았으니 2D 평면이 아닌, 기다리고 기다리던 진짜 3D 오브젝트를 렌더링 해봅시다.

 

3D를 그리기 위해선 우선 모델 행렬을 만들어야 합니다. 모델 행렬은 모든 객체의 버텍스를 전역 월드 공간으로 변환하기 위해 적용하려는 이동, 크기 조정 및 회전으로 구성됩니다. 평면을 x축에서 회전시켜 바닥에 누워 있는 것처럼 보이도록 하겠습니다. 모델 행렬은 다음과 같습니다.

glm::mat4 model = glm::mat4(1.0f);
model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));

버텍스 좌표를 이 모델 행렬과 곱하면 버텍스 좌표가 월드 좌표로 변환됩니다. 이로써 바닥에 살짝 떠있는 우리의 평면은 클로벌 월드의 평면이 됩니다.

 

다음으로 뷰 행렬을 만들어야 합니다. 오브젝트를 볼 수 있도록(우리가 월드 공간에서 (0, 0, 0)에 위치한다고 했을 때) 씬의 살짝 뒤로 옮기겠습니다. 씬에서 이동하려는 경우, 다음 사항을 생각해봐야 합니다.

  • 카메라를 뒤로 옮기는 것은 씬 전체를 앞으로 옮기는 것과 같습니다.

이 것이 바로 뷰 행렬이 하는 일입니다. 뷰 행렬은 우리가 카메라를 움직이기 원하는 방향과 반대로 씬 전체를 움직입니다. 지금 우리는 뒤로 이동하길 원하고 OpenGL은 오른손 좌표계이므로 양의 z축 방향으로 이동해야 합니다. 이는 음의 z축으로 씬을 이동시키는 방식으로 수행되고 우리가 뒤로 움직이고 있다는 인상을 줍니다.


More 3D


지금까지 우리는 3D 공간에서 2D 평면을 다루었습니다. 이번엔 한 번 2D평면을 3D큐브로 확장해봅시다. 큐브를 렌더하려면 총 36개의 버텍스들(6면 * 2개의 삼각형 * 각 3개의 버텍스)이 필요합니다. 36개의 버텍스는 적어두기엔 너무 많기에 여기에서 찾을 수 있습니다.

 

더 재밌어보이게 하기 위해 시간의 흐름에 따라 큐브를 회전 시키겠습니다.

model = glm::rotate(model, (float)glfwGetTime() * glm::radians(50.0f), glm::vec3(0.5f, 1.0f, 0.0f));

그리고나서 glDrawArrays(인덱스를 지정하지 않고)를 이용해 그려보겠습니다.

glDrawArrays(GL_TRIANGLES, 0, 36);

아마 다음과 같은 게 화면에 보일겁니다.

 

https://learnopengl.com/video/getting-started/coordinate_system_no_depth.mp4

 

큐브처럼 생겼지만 뭔가 이상합니다. 뒤에 있어 보이지 말아야될 면들이 보여지고 있습니다. 이렇게 보이는 이유는 OpenGL이 큐브를 삼각형과 프레그먼트 별로 그릴 때 먼저 그려진 것들이 나중에 그려진 것들의 픽셀을 덮어써버리기 때문입니다. OpenGL은 삼각형의 렌더링 순서를 보장하지 않기(동일한 그리기 호출 내에서) 때문에 이런 현상이 일어납니다.

 

다행히도 OpenGL은 z-buffer라고 하는 버퍼에 깊이 정보를 갖고 있기 때문에 OpenGL은 언제 위에 덮어씌우고 덮어씌우지 않을지 결정할 수 있습니다. z-buffer를 사용하여 depth-testing을 하도록 구성할 수 있습니다.


Z-buffer


OpenGL은 모든 깊이 정보를 깊이 버퍼(depth buffer)라고도 알려진 z-buffer에 저장합니다. GLFW는 자동으로 이러한 버퍼를 생성합니다(출력 이미지지에 대한 색상 정보를 색상 버퍼에 저장하듯이). 깊이는 각 fragment 내(fragment의 z값)에 저장됩니다. 그리고 fragment가 색상을 출력하려고 할 때마다  OpenGL은 깊이 값을 z-buffer와 비교합니다. 만약 현재 fragment가 다른 fragment의 뒤에 있다면 그 fragment는 폐기되고 그렇지 않다면 덮어씁니다. 이러한 과정을 depth testing이라고 하며 OpenGL이 자동으로 수행합니다.

 

하지만, 만약 OpenGL이 depth testing을 수행하고 있는지 확인하려면 depth testing을 활성화해야 합니다. glEnable로 depth testing을 활성화 할 수 있고, glEnable과 glDisable로 활성/비활성 할 수 있습니다.

glEnable(GL_DEPTH_TEST);

depth buffer를 사용하는 순간부터는 각 렌더 반복(이전 프레임의 깊이 정보가 버퍼에 남아있으므로)마다 깊이 버퍼를 정리해주어야 합니다. 색상 버퍼를 정리하듯이 glClear 함수에 DEPTH_BUFFER_BIT를 지정하여 줍니다:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

https://learnopengl.com/video/getting-started/coordinate_system_depth.mp4

 

됐습니다! 텍스쳐가 입혀진 채로 회전하는 큐브입니다. 소스 코드는 여기에서 확인할수 있습니다.


More Cubes!


더 많은 큐브를 만들어 봅시다! 큐브 자체는 똑같지만 월드내의 위치와 회전을 다르게 해보겠습니다. 그래픽적인 큐브의 형태는 정의되어 있으니 버퍼나 속성 배열을 바꿀 필요는 없습니다. 우리는 그저 각 오브젝트의 모델 행렬을 변경하여 큐브의 월드내 위치를 바꾸겠습니다.

 

먼저, 각 큐브들이 월드 공간에 위치할 이동 벡터를 정의합니다.

glm::vec3 cubePositions[] = {
    glm::vec3( 0.0f,  0.0f,  0.0f), 
    glm::vec3( 2.0f,  5.0f, -15.0f), 
    glm::vec3(-1.5f, -2.2f, -2.5f),  
    glm::vec3(-3.8f, -2.0f, -12.3f),  
    glm::vec3( 2.4f, -0.4f, -3.5f),  
    glm::vec3(-1.7f,  3.0f, -7.5f),  
    glm::vec3( 1.3f, -2.0f, -2.5f),  
    glm::vec3( 1.5f,  2.0f, -2.5f), 
    glm::vec3( 1.5f,  0.2f, -1.5f), 
    glm::vec3(-1.3f,  1.0f, -1.5f)  
};

 

이제, 렌더 루프에서 glDrwaArrays를 10번 호출합니다. 하지만 이번엔 드로우콜을 전달하기 전에 각각 다른 모델 행렬을 버텍스 셰이더에 전달하겠습니다.

glBindVertexArray(VAO);
for(unsigned int i = 0; i < 10; i++)
{
    glm::mat4 model = glm::mat4(1.0f);
    model = glm::translate(model, cubePositions[i]);
    float angle = 20.0f * i; 
    model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
    ourShader.setMat4("model", model);

    glDrawArrays(GL_TRIANGLES, 0, 36);
}

이 코드 조각은 새 큐브가 그려질 때마다 모델 행렬을 업데이트하고 이 작업을 총 10번 수행합니다.

rte_image_122.png

완벽합니다! 소스 코드는 여기에서 확인 가능합니다.


참고: https://learnopengl.com/Getting-started/Coordinate-Systems

관련글

LearnOpenGL(04) - ShadersLearnOpenGL(05) - 텍스처LearnOpenGL(08) - 카메라
An unhandled error has occurred. Reload 🗙