Modern OpenGL로 그래픽스 프로그래밍 환경 구현하기 - 0. 들어가며

* 이 글은 제가 연구 목적으로 그래픽스 프로그래밍을 하면서 얻은 관련 지식을 정리하고 나누기 위해 작성하였습니다. 저는 3차원 기하 모델링(Geometric modeling)을 전공하고 있기 때문에 렌더링이나 애니메이션 등 그래픽스 내 타 분야에 대한 서술에서 잘못된 내용이 있을 수 있습니다. 혹시 그러한 점을 발견하신다면 지적해주시면 감사하겠습니다.

1. 들어가며


안녕하세요, 본 글에서는 Modern OpenGL을 이용하여 다양한 목적의 그래픽스 프로그래밍을 하기 위한 환경을 만드는 과정을 소개하고자 합니다. 이 과정에서 다룰 내용들은 다음과 같습니다.

- 기본적인 Modern OpenGL 사용법
- 좌표계와 카메라
- 셰이더(GLSL)
- UI(ImGui 라이브러리 https://github.com/ocornut/imgui)
- 씬 그래프(Scene graph)

다음 글에서 본격적으로 프로그래밍을 시작하기 전에, OpenGL이 무엇인지 간단하게 소개하도록 하겠습니다.

2. OpenGL


OpenGL이란 Open Graphics Library의 줄임말로, 2차원 혹은 3차원 그래픽스를 컴퓨터 상에서 구현하기 위한 라이브러리입니다. 우리가 컴퓨터 게임이나 영화에서 보는 사실적인 컴퓨터 그래픽 화면들은 모두 OpenGL을 통해서 구현할 수 있습니다. 우리는 앞으로 간단한 삼각형부터 시작하여 점점 사실적인 렌더링을 하는 법을 공부할 것입니다.

간단한 삼각형

Utah Teapot을 Phong shading으로 렌더링한 결과

OpenGL은 왜 Open- 이라는 접두어를 가지게 되었을까요? 그 이유는 바로 OpenGL2차원 혹은 3차원 그래픽스 표준 API로서 서로 다른 그래픽스 하드웨어에서 공통적으로 사용할 수 있는 API를 제공하기 때문입니다. 이렇게 설명하면 피부에 잘 와닿지 않죠? 따라서 간단한 예시를 들어 설명하도록 하겠습니다.

A, B라는 두 그래픽 카드 제조사가 있다고 가정하겠습니다. 두 제조사는 모두 각자의 그래픽 카드를 설계하면서 자신의 그래픽 카드에 어떤 명령을 내리면 어떤 결과가 나오는지 명세를 작성합니다. 그런데 문제는, 이 명세가 제조사별로 모두 다르다는 데에 있습니다. 예를 들어, (0, 0, 0)의 위치에 점을 그리고 싶다고 가정하겠습니다. 이 때, 두 제조사는 각자 다음과 같은 명령을 내려야 합니다.

A : drawPointA(0, 0, 0)
B : drawPointB(0, 0, 0)

이렇게 서로 다른 명령을 내려야만 점을 그릴 수 있다면, 프로그래머들은 코드를 작성할 때 굉장히 고생할 것입니다. 사용자가 어떤 그래픽 카드를 사용하는지에 따라 명령을 모두 다르게 내려줘야 하니까요. 따라서 사람들은 이렇게 다른 그래픽스 하드웨어에서도 동일한 명령을 내려주면 동일한 결과가 나왔으면 좋겠다고 생각하게 되었습니다. 이러한 역사적 배경에서 탄생한 것이 OpenGL입니다.

OpenGL : glDrawPoint(0, 0, 0)
⇨ A : drawPointA(0, 0, 0)
⇨ B : drawPointB(0, 0, 0)
(* glDrawPoint는 실제 존재하는 명령이 아닙니다! )

위와 같이, OpenGL이 제공하는 명령(glDrawPoint)을 사용하면 그래픽 카드 별로 자동으로 자신의 명세에서 해당 명령에 대응하는 명령을 찾아서 호출합니다. 이와 같은 대응 관계는 제조사에서 드라이버를 통해 모두 제공하므로 우리가 걱정할 필요는 없습니다. 우리는 그저 OpenGL에서 제공하는 명령들을 사용하기만 하면 되는 것입니다.

이렇게 OpenGL은 모든 제조사에서 준수해야 할 규칙같은 것이므로, 어떤 특정 회사에서 독점적으로 관리하지는 않습니다. 현재는 Khronos Group(https://kr.khronos.org/)이라는 비영리 기관에서 OpenGL을 관리하고 있으며, 제조사들이 그 규칙 제정에 관여하고 있습니다. 이는 마이크로소프트가 독점적으로 관리하는 Direct3D와 정확히 반대라고 할 수 있겠습니다.

3. GLSL과 Modern OpenGL


OpenGL은 최초 출시 이후 시간이 지나면서 점점 개선되었습니다. OpenGL의 최초 버전은 1992년에 출시되었고, 최신인 4.6버전이 2017년에 출시되었습니다. 그런데 그 과정에서 특히 주목해야 할 버전들이 있습니다. 바로 OpenGL 2.0과 3.0이 그 것입니다.


3-1. 셰이더와 GLSL


해당 버전들이 왜 중요한지 알기 위해서는 먼저 GLSL이 무엇인지 살펴보아야 합니다. GLSL이란 GL Shader Language의 약자로써, OpenGL과 호환되는 하이레벨(High-level) 셰이더 언어입니다. 셰이더컴퓨터 화면에 그려지는 특정 픽셀을 어떤 색으로 칠할 것인지를 결정하는 프로그램이라고 할 수 있습니다.

우리가 위에서 보았던 삼각형 화면을 다시 생각해볼까요? 해당 삼각형 그림을 그릴 때, 우리가 해야 할 일은 별로 많지 않습니다. 그저 삼각형의 세 점의 위치와, 색깔 등의 추가 정보를 제공해주는 것 뿐이죠. 하지만, 실제 컴퓨터가 해야 할 일은 훨씬 많습니다. 우리가 정의한 삼각형의 세 점이 컴퓨터 화면 상에서 어떤 위치의 픽셀에 대응되는지, 그리고 점들 사이를 잇는 선분이 화면의 어떤 픽셀을 지나가는지 등을 모두 결정해야 하죠. 그리고 이 결정은 모두 특정 픽셀의 '색' 이라는 결과로 나타나게 됩니다. 셰이더가 바로 이러한 일을 담당하여 처리합니다. 제 나름대로 표현하자면, 셰이더는 우리의 머리 속에 존재하는 완벽하고 추상적인 장면을 실제 사각형 픽셀들로 구성된 컴퓨터 화면 상으로 옮기는 일을 한다고 할 수 있겠습니다.

3-2. Modern OpenGL


지금까지 셰이더가 어떤 역할을 하는지 간단하게 알아보았습니다. 셰이더는 하드웨어와 긴밀하게 연관되어 있습니다. 때문에, 개발자들은 OpenGL과 마찬가지로 동일한 셰이더 언어로 코드를 작성하여 모든 하드웨어에서 사용하길 바랐습니다. 그 결과 GLSL이 출시되었고, GLSL은 출시 이후 OpenGL과 함께 지속적으로 개선되어 왔습니다.

다시 위로 돌아가 봅시다. 위의 버전들이 중요한 이유는 GLSL과 맺는 다음과 같은 관계 때문입니다.

A. OpenGL 2.0 : 이 버전부터 GLSL이 사용되기 시작하였습니다. OpenGL은 초창기부터 셰이더 언어를 염두에 두고 개발되었지만, GLSL이 정식으로 출시되어 사용되기 시작한 것은 이 때부터 입니다. 따라서 OpenGL 2.0에서는 기존의 방법을 사용하여 GLSL 없이 화면을 그릴 수도 있지만, GLSL을 사용하여 화면을 그릴 수도 있습니다.

B. OpenGL 3.0 : 이 버전부터 GLSL의 사용이 강제되었습니다. 따라서 이 버전의 OpenGL부터는 GLSL을 사용하지 않으면 화면에 아무것도 그릴 수 없습니다. 즉 이전의 방법으로는 더 이상 화면에 그림을 그릴 수 없는, 굉장히 중대한 변화가 이루어졌습니다.

왜 이런 변화가 이루어졌을까요? 그 이유를 알기 위해서는, GLSL이 등장하기 이전에 OpenGL이 사용했던 방법에 대해 알아볼 필요가 있습니다.

GLSL이 등장하기 이전에, 프로그래머는 화면 상에 어떤 도형을 그릴 때마다 프로그램에
1) 어떤 종류의 도형을 그릴 것인지
2) 해당 도형의 위치 정보는 어떤지
3) 해당 도형의 색깔 정보는 어떤지
....
등의 정보를 모두 전송해줬어야 했습니다. 간단하게, 예전의 방법으로 크기가 10인 빨간색 점을 (0, 0, 0)에 그리는 코드를 살펴보겠습니다.
1
2
3
4
5
glPointSize(10);        // Point 크기 전달
glColor3d(100);        // Point 색깔 전달
glBegin(GL_POINTS);        // Point 그리기 시작
glVertex3d(000);    // Point 위치 전달
glEnd();                // Point 그리기 종료
cs

위 코드를 화면을 그릴 때마다 실행하게 되면, 60프레임을 가정했을 때 1초에 60번 위에서 명시해놓은 정보들을 CPU에서 GPU로 전송해줘야 합니다. 특히 위의 정보 중에서 가장 큰 비중을 차지하는 것은 단연 위치(와 그 부수적인) 정보입니다. 예를 들어, 우리가 하나의 점이 아닌 100만 개의 삼각형을 화면에 그린다고 가정합시다. 그 경우, 우리는 총 300만 개의 3차원 좌표 정보를 매 프레임마다 CPU에서 GPU로 전송해야 합니다. 안그래도 CPU에서 GPU로 정보를 전송하는 속도는 각자의 장치 내부에서 정보를 전송하는 속도에 비해 굉장히 느린데, 그 정보의 양까지 증가한다면 그 비용은 굉장히 커지게 됩니다.

이러한 비용을 줄이기 위해, OpenGL은 새로운 방법을 모색하기 시작하였습니다. 그 결과, 처음부터 GPU에 대부분의 정보를 저장해두고 CPU에서는 그저 해당 정보를 사용하여 화면을 그리라는 명령만 전달하는 방식을 고안하였습니다. 실제 이러한 방법은 기존의 방법에 비해 훨씬 적은 비용으로 복잡한 화면을 그리는 것을 가능하게 하였습니다.

대신 이러한 방법을 사용하기 위해서는, 위와 같은 쉽고 간단한 코드 대신 GPU에 어떤 정보를 보낼지 설정하고, 해당 정보를 어떻게 처리하여 어떤 모습으로 화면에 그릴 것인지 등의 정보를 프로그래머가 모두 수동으로 설정해줘야 합니다. 즉, 렌더링 비용을 획기적으로 줄이고 프로그래머에게 더 큰 자유를 주는 대신에, 작성해야 할 코드의 양과 개발의 난이도는 이전에 비해 더 높아지게 된 것입니다.

우리가 흔히 얘기하는 Modern OpenGL이란 OpenGL 3.0 버전 이상을 가리키는 경우가 많습니다. 따라서 Modern OpenGL을 사용하려면 GLSL에 대한 공부 역시 병행해야 합니다. 이어지는 글들에서는 OpenGL 버전 3.3을 이용하고, GLSL 버전 110과 330을 동시에 살펴보며 코드를 공부할 예정입니다.
(OpenGL 버전과 그에 대응되는 GLSL 버전은
https://en.wikipedia.org/wiki/OpenGL_Shading_Language의 표를 참고해주세요.)

그럼, 다음 글에서는 본격적으로 OpenGL을 사용하여 코드를 작성해보도록 하겠습니다.

댓글