Modern OpenGL로 그래픽스 프로그래밍 환경 구현하기 - 1. 개발 환경 세팅하기 & 창 띄우기

1. 개발 환경 세팅하기 & 창 띄우기


이번 글에서는 윈도우 환경에서 개발 환경을 세팅하는 방법에 대해서 알아보도록 하겠습니다. 이 글에서는 윈도우 10에서 Visual Studio 2019를 사용하여 개발할 예정입니다.

Visual Studio 2019를 설치한 후에, 새로운 프로젝트를 생성합니다. 새 프로젝트 이름은 GraphicsTutorial 이라고 하겠습니다.


빈 프로젝트 생성

GraphicsTutorial 프로젝트 생성

생성된 프로젝트를 연 후, main 함수가 들어갈 파일을 main.cpp란 이름으로 생성하겠습니다.


main.cpp 파일 생성

이제 이 파일에서 코딩을 시작하여 창을 띄워보도록 하겠습니다. 우리가 띄울 창은 OpenGL이 그림을 그릴 창으로, 일반적으로 다른 프로그램을 만들어서 띄우는 cmd 창과는 다릅니다. 그리고 그러한 창을 띄우기 위해서는 다른 라이브러리가 필요합니다.

1-1. 윈도우(window) 라이브러리

여기서 윈도우라 함은, 운영체제 윈도우(Windows)를 말하는 것이 아니라 OpenGL이 결과물을 출력할 창을 일컫습니다. 그리고 윈도우 라이브러리는 그러한 창을 생성할 뿐만 아니라 창에서 발생하는 입출력(I/O) 등을 모두 관리해주는 역할을 합니다. 예를 들어, 우리가 만든 프로그램 창의 어떤 지점을 클릭했을 때, 그 사실을 프로그램에 전달해주는 역할을 해주는 것이 이 윈도우 라이브러리입니다.

이 부분이 많이 혼동되는 부분이라, 부연설명을 조금 더 하도록 하겠습니다. OpenGL은 단지 우리가 그릴 그림을 화면에 그려주는 역할만을 합니다. 해당 그림이 그려지는 창은 우리가 다른 라이브러리를 이용하여 직접 생성해야 합니다. 즉, OpenGL이 우리가 명령한 대로 그림을 그려주는 화가라면, 그 화가가 그림을 그릴 캔버스는 우리가 따로 준비해줘야 하는 거죠. 이와 같이 기능을 분리해 놓은 것은 운영체제 별로 모두 창을 다루는 방식이 다르게 구현되어 있기 때문입니다.

이와 같은 목적으로 사용할 수 있는 윈도우 라이브러리는 여러가지가 있습니다. 여기서는 그 중 세 라이브러리를 소개하고자 합니다.

1) GLUT(OpenGL Utility Toolkit) : GLUT은 OpenGL을 위한 윈도우 라이브러리 중 아마(?) 가장 오래된 라이브러리일 것입니다. GLUT은 사용하기가 정말 간단하고 구, 육면체 등 간단한 도형을 쉽게 그릴 수 있게 도와줘서 초심자들이 사용하기에 좋은 라이브러리입니다. 단, GLUT은 개발이 중단된지 굉장히 오랜 시간이 지났으므로 관심있는 분들은  GLUT을 계승한 Freeglut (http://freeglut.sourceforge.net/)을 사용하시면 되겠습니다.

2) GLFW : GLFW는 GLUT과 마찬가지로 OpenGL(+ OpenGL ES, Vulkan)을 위한 윈도우 라이브러리지만, 지금도 계속 업데이트되고 있습니다. 이런 강력한 장점과 더불어 GLFW는 GLUT만큼 사용하기 편리하므로 좋은 선택이 될 것입니다.

3) SDL2(Simple DirectMedia Layer) : SDL은 위의 GLUT과 GLFW 보다는 규모가 큰 라이브러리입니다. 이름에서도 알 수 있듯이, SDL은 OpenGL만을 위한 라이브러리가 아니라 다양한 멀티미디어 환경을 제어하기 위해 만들어졌습니다. 따라서 SDL은 Direct3D 또한 지원하며, 화면뿐만 아니라 사운드 등의 다른 멀티미디어 기능 또한 지원합니다.

본 글에서는 SDL2를 사용할 것입니다. 단, 다른 윈도우 라이브러리를 사용하고자 하는 분들은 창을 다루는 부분의 코드만 본인의 라이브러리에 맞게 작성해주시면 됩니다. 실제 그림을 그리는 OpenGL 코드는 모두 똑같으니까요.

1-2. SDL2.0 임포트 하기


먼저, SDL의 다운로드 페이지(https://www.libsdl.org/download-2.0.php)로 가서 SDL2.0을 다운로드 받도록 합니다.

소스 코드를 다운받아서 직접 빌드하는 것보다, 이미 빌드된 버전을 다운받도록 합니다.

이제 GraphicsTutorial 폴더 내에 Dependencies 폴더를 만듭니다. 앞으로 우리가 사용할 외부 라이브러리들을 모두 이 폴더 내에서 관리할 것입니다. 이 폴더에 방금 다운 받은 파일의 압축을 풀어서 가져다 놓습니다.


Dependencies 폴더 생성 


SDL 라이브러리를 Dependencies 폴더에 복사합니다.

이제 다시 프로젝트로 돌아가서 SDL을 프로젝트에 임포트합니다.

1-2-1. 헤더 파일

SDL 라이브러리의 헤더 파일(*.h)은 include 폴더 내에 있습니다. 따라서 다음과 같이 프로젝트 속성에서 C/C++→일반→추가 포함 디렉터리를 수정해줍니다.

SDL 라이브러리 - 헤더 추가

1-2-2. 라이브러리 파일

SDL 라이브러리의 정적 라이브러리 파일(*.lib)은 lib 폴더 내에 있습니다. 그런데 정적 라이브러리 파일은 32bit(x86)와 64bit(x64) 별로 따로 컴파일 되어 있으므로, 현재 환경에 맞도록 추가해주도록 합니다. 프로젝트 속성에서 링커→명령줄에서 다음과 같이 명령줄을 추가합니다.

SDL 라이브러리 - 정적 라이브러리 파일 추가 : 32bit(x86)

SDL 라이브러리 - 정적 라이브러리 파일 추가 : 64bit(x64)


1-2-3. DLL 파일

SDL 라이브러리의 동적 라이브러리 파일(*.dll)은 정적 라이브러리 파일과 같은 곳에 있습니다. dll 파일은 앞으로 프로젝트가 있는 폴더에 bin이라는 폴더를 만들어서 그 곳에서 관리하도록 하겠습니다. 그러기 위해서 bin 폴더와 32bit, 64bit을 위한 폴더(x86, x64)를 각각 만들도록 하겠습니다. 그리고 SDL 라이브러리의 dll 파일을 32bit, 64bit 별로 맞는 폴더에 복사합니다.

DLL 파일들을 보관할 bin 폴더 생성

bin 폴더에 DLL 파일 추가
자, 그리고 프로젝트에 동적 라이브러리 파일이 위의 폴더에 있다는 것을 알려주도록 합니다. 이 작업은 다음과 같이 프로젝트 속성의 디버깅→환경 탭에서 설정할 수 있습니다. 32bit는 x86 폴더로, 64bit는 x64 폴더로 PATH를 설정합니다.

SDL 라이브러리 - 동적 라이브러리 파일 추가 : 32bit(x86)

SDL 라이브러리 - 동적 라이브러리 파일 추가 : 64bit(x64)

1-3. OpenGL 임포트 하기

코드를 작성하기 전 마지막 관문이 남았습니다. SDL을 임포트했으니, OpenGL을 임포트하여 SDL이 만드는 창 위에 그림을 그려야 합니다. 비주얼 스튜디오를 정상적으로 설치하셨다면, OpenGL을 따로 다운로드 받을 필요는 없습니다. 대신, 이미 설치된 OpenGL 라이브러리를 링크시켜줘야 합니다. 이는 간단하게 다음과 같이 프로젝트 속성에서 링커→입력→추가 종속성에 opengl32.lib를 추가시켜주면 됩니다.

OpenGL 라이브러리 링크

1-4. 창 띄우기

자, 이제 main.cpp에 코드를 작성하여 창을 띄워봅시다. 다음과 같이 코드를 작성합니다.

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
#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)를 생성합니다.
    SDL_Renderer* rdr = SDL_CreateRenderer(window, -10);
    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(00, 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

위와 같이 코드를 작성하고 프로젝트를 빌드하여 실행하면, 다음과 같은 창이 뜨는 것을 확인할 수 있습니다.

실행 결과
위 코드에 간단하게 각 줄이 어떤 역할을 하는지 설명해두었습니다. 하지만, OpenGL이 어떤 방식으로 작동하는지에 대해 다음 글에서 더 자세하게 공부해보고 코드를 자세히 살펴보도록 하겠습니다.



댓글

가장 많이 본 글