Modern OpenGL로 그래픽스 프로그래밍 환경 구현하기 - 1. 개발 환경 세팅하기 & 창 띄우기 (2)
이번 글에서는 저번에 창을 띄우기 위해 작성했던 코드가 어떤 배경에서 작성된 것인지 살펴볼 것입니다. 창을 띄우기 위해 사용했던 코드는 다음과 같습니다.
전체적으로 봤을 때, 이 코드가 하는 일은 다음과 같이 요약할 수 있습니다.
1) SDL 환경 초기화
2) 창(Window) 생성
3) 렌더링 메인 루프 설정
위 순서대로 살펴보도록 하겠습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | #include <SDL.h> #undef main #include <SDL_opengl.h> // SDL 헤더를 include 합니다. #include <functional> #include <iostream> std::function<void()> mainLoop; // 프로그램이 끝날 때까지 매 프레임마다 호출하는 함수입니다. // 이 함수 안에서 이번 프레임에 그릴 화면을 결정합니다. int wndWidth = 640, wndHeight = 480; // 우리가 만들 창(Window)의 너비(wndWidth)와 높이(wndHeight) 입니다. void resizeCallback(int width, int height); // 창의 크기가 변할 때마다 호출되는 함수입니다. void windowCallback(const SDL_Event& event, bool& done); // 창에 어떤 이벤트(event)가 생길 때 호출되는 함수입니다. // @done : 이벤트에 따라 프로그램의 종료 여부를 결정하고, 종료한다면 true로 설정합니다. int main() { // SDL을 초기화합니다. 우리는 SDL을 VIDEO만을 위해서 사용하므로 SDL_INIT_VIDEO만 인수로 전달합니다. // 자세한 사항은 https://wiki.libsdl.org/SDL_Init 를 참조하세요. if (SDL_Init(SDL_INIT_VIDEO) < 0) { // 만약 SDL이 정상적으로 초기화되지 않았다면 오류 메시지를 출력합니다. std::cerr << "[ SDL_Init ] failed : %s" << SDL_GetError() << std::endl; exit(-1); } // 사용할 OpenGL 버전을 선택합니다. 우리는 OpenGL 3.3을 사용하므로 그에 맞게 지정합니다. SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); // SDL을 통해 창을 실제로 생성합니다. // 이 때, 인자로 창의 위치와 크기 등을 설정해줄 수 있습니다. // 자세한 사항은 https://wiki.libsdl.org/SDL_CreateWindow 를 참조하세요. SDL_Window* window = SDL_CreateWindow( "Viewer", // 창의 이름입니다. SDL_WINDOWPOS_CENTERED, // 창의 X 좌표입니다. 스크린의 중앙에 위치하도록 합니다. SDL_WINDOWPOS_CENTERED, // 창의 Y 좌표입니다. 스크린의 중앙에 위치하도록 합니다. wndWidth, wndHeight, // 창의 크기입니다. SDL_WINDOW_OPENGL); // 옵션들을 지정합니다. OpenGL을 사용하여 그릴 수 있도록 SDL_WINDOW_OPENGL 옵션을 줍니다. SDL_SetWindowResizable(window, SDL_TRUE); // 창이 생성된 이후에 창의 크기를 재조정할 수 있도록 합니다. // 방금 생성한 창(window)에 OpenGL을 사용하여 그리기 위해 OpenGL Context를 생성합니다. SDL_GLContext glc = SDL_GL_CreateContext(window); if (glc == nullptr) { // 만약 OpenGL Context가 생성되지 않았다면, 오류 메시지를 출력합니다. std::cerr << "[ SDL_GL_CreateContext ] failed : %s" << SDL_GetError() << std::endl; exit(-1); } // 방금 생성한 창(window)에 2D 화면을 그리기 위해 렌더러(Renderer)를 생성합니다. // 우리는 디폴트 렌더링 드라이버를 사용할 것이므로 인자로 -1과 0을 전달합니다. // 자세한 사항은 https://wiki.libsdl.org/SDL_CreateRenderer 를 참조하세요. SDL_Renderer* rdr = SDL_CreateRenderer(window, -1, 0); if (rdr == nullptr) { // 만약 렌더러가 생성되지 않았다면, 오류 메시지를 출력합니다. std::cerr << "[ SDL_CreateRenderer ] failed : %s" << SDL_GetError() << std::endl; exit(-1); } bool done = false; // 이 변수가 false인 한, 프로그램을 계속합니다. 만약 true가 된다면, 프로그램을 종료합니다. // 위에서 선언한 mainLoop 함수를 정의합니다. mainLoop = [&] { SDL_Event event; // SDL에서 관리하는 이벤트(ex. 마우스 클릭, 키보드 입력)에 관한 정보를 담고 있습니다. // SDL에서 관리하는 이벤트 중 이번 프레임에 발생한 이벤트 정보를 가져옵니다. // 각 이벤트마다 이벤트의 종류에 따라 어떻게 처리할 지 결정해줘야 합니다. while (SDL_PollEvent(&event)) { // 창과 관련된 이벤트는 [ windowCallback ] 함수에서 처리합니다. windowCallback(event, done); } // OpenGL을 이용하여 화면을 그립니다. // 지금은 아무것도 그리지 말고 화면 전체를 까맣게 색칠하도록 합니다. glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // 화면 전체를 어떤 색으로 칠할 것인지 결정합니다. glClear(GL_COLOR_BUFFER_BIT); // 화면의 색 버퍼(Color buffer)를 모두 비우고, 위에서 정한 색으로 다시 칠합니다. // OpenGL의 버퍼를 교체합니다. SDL은 디폴트로 OpenGL의 더블 버퍼링을 지원합니다. SDL_GL_SwapWindow(window); }; // [ done ] 변수가 true로 변할 때까지 mainLoop 함수를 호출합니다. while (!done) mainLoop(); // 프로그램이 끝나서 루프에서 빠져나왔으므로, OpenGL Context와 SDL 창을 제거합니다. SDL_GL_DeleteContext(glc); SDL_DestroyWindow(window); // SDL을 종료합니다. SDL_Quit(); return 0; } void resizeCallback(int width, int height) { // 새로운 창의 크기로 [ wndWidth ] 와 [ wndHeight ] 를 업데이트합니다. wndWidth = width; wndHeight = height; // OpenGL의 뷰포트를 새로운 창의 크기로 재조정합니다. glViewport(0, 0, wndWidth, wndHeight); } void windowCallback(const SDL_Event& e, bool& done) { // 이벤트가 창과 관련된 이벤트일 경우 처리합니다. if (e.type == SDL_WINDOWEVENT) { // 만약 창을 닫을 경우, [ done ] 변수를 true로 만들어줍니다. if (e.window.event == SDL_WINDOWEVENT_CLOSE) done = true; // 만약 창의 크기를 변경할 경우, [ resizeCallback ] 함수를 호출합니다. else if (e.window.event == SDL_WINDOWEVENT_RESIZED) resizeCallback(e.window.data1, e.window.data2); } } | cs |
전체적으로 봤을 때, 이 코드가 하는 일은 다음과 같이 요약할 수 있습니다.
1) SDL 환경 초기화
2) 창(Window) 생성
3) 렌더링 메인 루프 설정
위 순서대로 살펴보도록 하겠습니다.
1) SDL 환경 초기화
앞서 SDL 라이브러리는 다양한 멀티미디어를 지원하기 위한 라이브러리라고 언급한 바 있습니다. 이러한 SDL 라이브러리를 초기화하기 위해서는, 우리가 SDL 라이브러리의 어떤 기능을 이용하고 싶은지 전달해주어야 합니다. 우리는 SDL 라이브러리에서 화면에 그림을 그리는 기능, 즉 비디오 기능만 이용하고 싶으므로 다음과 같이 코드를 작성합니다.
SDL_Init(SDL_INIT_VIDEO)
다음으로, 우리는 OpenGL을 이용하여 SDL이 관리하는 창에 그림을 그릴 것이므로 SDL에 우리가 사용할 OpenGL에 관한 정보를 전달해야 합니다. 따라서 다음과 같이 우리가 사용할 OpenGL의 major version 과 minor version을 SDL에게 알려줍니다.
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3)
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3)
2) 창(Window) 생성
이제 본격적으로 창을 생성합니다. 이전 글에서 설명한 것처럼, OpenGL은 절대 창을 직접 만들지 않습니다! 대신, SDL 라이브러리를 이용해서 창을 만들도록 합니다.
SDL_CreateWindow ( ... )
위 함수를 통해 창을 만들 수 있습니다. 코드의 주석에서 밝힌 대로, 우리가 만들 창에 관한 정보들을 인자로 전달해줍니다. 코드에서 정한 옵션들은 제가 임의로 정한 것이므로, 더 자세히 알고자 하시는 분들은 주석에서 언급한 사이트를 참조해주세요.
또 우리는 창의 크기를 조절할 수 있도록 만들고 싶습니다. SDL에서는 이를 위해 다음과 같은 함수를 제공합니다.
SDL_SetWindowResizable(window, SDL_TRUE)
자, 지금까지의 작업을 통해 창을 생성했습니다! 이제, OpenGL을 통해 이 창에다가 그림을 그릴 수 있도록 설정해주어야 합니다. 이를 위해서는 다음 두 객체를 생성해야 합니다.
⇒ Context : OpenGL을 통해 이 창에 그림을 그린다는 사실을 전달합니다.
⇒ Renderer : 실제 렌더링 작업을 할 수 있도록 렌더링 드라이버를 설정하는 등의 작업을 합니다.
위 두 객체를 우리가 만든 창에 대해 생성하는 함수는 다음 두 함수입니다.
SDL_GL_CreateContext(window)
SDL_CreateRenderer(window, -1, 0)
위 함수를 보면, Context를 만드는 함수는 이름에 GL이 포함되어 있지만, Renderer를 만드는 함수는 그렇지 않은 것을 확인할 수 있습니다. 이를 통해 Context와 달리 Renderer는 OpenGL과는 독립적인 객체라는 사실을 알 수 있습니다. 즉, 우리가 OpenGL을 쓰던 Direct 3D를 쓰던 Renderer는 이에 구애받지 않습니다.
3) 렌더링 메인 루프 설정
우리가 사용하는 모니터는 주사율(Frame Rate)에 의해 정해진 시간 간격마다 화면을 새로 그립니다. 그와 마찬가지로, 우리는 OpenGL을 통해서 정해진 시간 간격마다 창의 그림을 새롭게 그려야 합니다. 우리는 그 일을 하나의 루프 함수로 만들어서 (사용자가 종료 신호를 보내지 않는 한) 무한 루프를 돌림으로서 수행할 수 있습니다.
우선, 루프 함수를 function 타입으로 선언합니다.
std::function<void()> mainLoop;
그리고 루프 함수를 다음과 같이 정의합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // 위에서 선언한 mainLoop 함수를 정의합니다. mainLoop = [&] { SDL_Event event; // SDL에서 관리하는 이벤트(ex. 마우스 클릭, 키보드 입력)에 관한 정보를 담고 있습니다. // SDL에서 관리하는 이벤트 중 이번 프레임에 발생한 이벤트 정보를 가져옵니다. // 각 이벤트마다 이벤트의 종류에 따라 어떻게 처리할 지 결정해줘야 합니다. while (SDL_PollEvent(&event)) { // 창과 관련된 이벤트는 [ windowCallback ] 함수에서 처리합니다. windowCallback(event, done); } // OpenGL을 이용하여 화면을 그립니다. // 지금은 아무것도 그리지 말고 화면 전체를 까맣게 색칠하도록 합니다. glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // 화면 전체를 어떤 색으로 칠할 것인지 결정합니다. glClear(GL_COLOR_BUFFER_BIT); // 화면의 색 버퍼(Color buffer)를 모두 비우고, 위에서 정한 색으로 다시 칠합니다. // OpenGL의 버퍼를 교체합니다. SDL은 디폴트로 OpenGL의 더블 버퍼링을 지원합니다. SDL_GL_SwapWindow(window); }; | cs |
이 함수는 다음 두 가지 일을 합니다.
1. 이벤트 처리(Event handling)
SDL은 창을 생성할 뿐만 아니라, 창에 관련된 이벤트 역시 처리합니다. 창에 관련된 이벤트는 여러 가지가 있을 수 있습니다. 대표적인 것이 마우스 입력과 키보드 입력일 것입니다. 만약 현재 SDL이 생성한 창을 선택한 상황에서 특정한 입력에 대해 특정한 작업을 수행하도록 만들고 싶다면, SDL에게 그러한 사항들을 미리 알려주어야 합니다.
먼저, 루프 함수 내부에 SDL의 이벤트 객체를 하나 만들도록 합니다.
SDL_Event event;
그리고 이 이벤트 객체에 SDL이 지금까지 받은 이벤트 정보를 불러오도록 합니다. 현재 프레임에서 입력받은 이벤트가 하나 이상이 될 수 있으므로, while 문을 사용하여 이벤트 정보를 모두 불러오도록 합니다.
while(SDL_PollEvent(&event))
그리고 이렇게 불러온 이벤트 정보를 이용하여, 우리가 미리 정해놓은 함수를 실행하도록 합니다. 위의 코드에서는 windowCallback만을 정의하여 사용하였습니다. 이 함수는 창과 관련된 이벤트를 처리하는 함수입니다. 이 함수는 다음과 같이 정의되었습니다.
1 2 3 4 5 6 7 8 9 10 11 12 | void windowCallback(const SDL_Event& e, bool& done) { // 이벤트가 창과 관련된 이벤트일 경우 처리합니다. if (e.type == SDL_WINDOWEVENT) { // 만약 창을 닫을 경우, [ done ] 변수를 true로 만들어줍니다. if (e.window.event == SDL_WINDOWEVENT_CLOSE) done = true; // 만약 창의 크기를 변경할 경우, [ resizeCallback ] 함수를 호출합니다. else if (e.window.event == SDL_WINDOWEVENT_RESIZED) resizeCallback(e.window.data1, e.window.data2); } } | cs |
코드를 보면 쉽게 이 함수가 어떤 일을 하는지 알 수 있습니다. 인자로 받은 이벤트가 어떤 이벤트인지 검사하여, 해당 이벤트에 맞는 작업을 해주기만 하면 됩니다. 여기서는 창을 닫을 때에는 done 변수를 true로 바꿔서 루프 함수를 끝내도록 하였고, 창의 크기를 바꿀 때에는 resizeCallback이라는 다른 콜백 함수를 호출하도록 했습니다. (해당 함수에서 사용하는 glViewport 함수는 다음 글에서 설명하도록 하겠습니다.) windowCallback뿐만 아니라 다른 모든 종류의 이벤트에 대해서도 이와 같은 함수를 정의하고 사용하면 됩니다.
2. OpenGL로 화면 그리기
자, 이제 정말로 OpenGL을 이용하여 창에 그림을 그리도록 합니다. 일단 우리는 아무것도 없는, 검은색 화면을 창에 띄우고자 합니다. 그렇게 하기 위해 다음 두 함수를 호출합니다.
glClear : 우리가 인자로 전달하는 버퍼를 비우고 default 값으로 채웁니다.
glClearColor : glClear를 호출할 때 색 버퍼를 비우고 나서 채울 default 값을 설정합니다. OpenGL에서는 0.0부터 1.0까지의 값을 RGBA 채널에 설정하여 색을 정의할 수 있습니다.
위에서 강조한 버퍼(Buffer)란, 화면 상의 각 픽셀이 현재 화면을 그리기 위해 담고 있는 정보를 의미합니다. 즉 glClear 함수는 현재 화면에 그려진 내용과 관련된 정보를 모두 지우는 역할을 합니다. 그런데 그 정보는 색깔로만 구성되어 있지 않습니다. glClear를 통해 비울 수 있는 버퍼는 세 가지입니다.
⇒ Color Buffer : 각 픽셀의 색깔 값입니다.
⇒ Depth Buffer : 각 픽셀의 깊이 값입니다.
⇒ Stencil Buffer : 각 픽셀의 스텐실 값입니다.
여기서는 일단 Color buffer만 지우도록 하겠습니다. 나머지 두 버퍼에 관한 내용은 차후에 관련 내용이 나오면 살펴보도록 하겠습니다.
종합해보자면, 이 두 줄의 코드는 단지 현재 창의 내용을 모두 지우고 우리가 정의한 default 값으로 색을 칠하는 일을 합니다. 그리고 우리가 default 값으로 (0, 0, 0), 즉 검은색을 전달하였으므로 창은 검은색으로 칠해질 것입니다.
이렇게 OpenGL로 화면을 그리고 난 후, 화면을 스왑(Swap) 해주도록 합니다. 여기서 화면을 스왑하는 작업은 우리가 더블 버퍼링(Double buffering)을 사용하고 있기 때문에 필요합니다. 더블 버퍼링이란 말 그대로 스크린 버퍼를 두 개 사용한다는 뜻입니다. 스크린 버퍼를 두 개 사용하면 하나를 사용할 때보다 부드러운 화면 전환이 가능해집니다. 스크린 버퍼가 두 개 있다면 하나의 버퍼를 사용자에게 보여주는 동안 다른 버퍼에 그림을 그리고 스왑함으로써 빠른 화면 전환이 가능하기 때문입니다. SDL은 기본적으로 더블 버퍼링을 지원하며, 다음 함수로 버퍼를 스왑할 수 있습니다.
SDL_GL_SwapWindow(window)
========================================================
이렇게 메인 루프 함수를 모두 작성했습니다.
이제 나머지 코드를 살펴보면, 위에서 말한대로 done 변수가 false인 한 mainLoop가 무한 호출되는 것을 확인할 수 있습니다. 만약 창을 닫아서(done 변수가 true가 되어) 해당 루프에서 빠져나오게 되면, 우리가 위에서 만든 OpenGL context와 window를 모두 해제해줘야 합니다. 이는 다음 함수들로 수행할 수 있습니다.
SDL_GL_DeleteContext()
SDL_DestroyWindow()
마지막으로, SDL 환경을 종료하고 프로그램을 끝마칩니다.
SDL_Quit()
지금까지 창을 띄우는 코드들에 대해서 자세하게 살펴보았습니다. 앞으로는 이와 같이 부차적인 내용들은 간략히 언급하거나 건너뛸 것입니다. 대신, OpenGL과 그래픽스 프로그래밍에만 초점을 맞춰서 글을 진행할 것입니다. 다음 글에서는 Modern OpenGL이 그림을 그리는 원리와 렌더링 파이프라인, 그리고 셰이더에 대해서 설명하고, 이를 통해 화면에 삼각형을 그려보도록 하겠습니다.


댓글
댓글 쓰기