ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • OpenGL 4.5 강좌 - (4) Framebuffer, 더블 버퍼링, V-Sync
    OpenGL 2021. 1. 31. 15:53

    잠깐 다시 디스플레이 얘기로 돌아갑시다. 디스플레이는 움직이는 영상을 어떻게 표현할까요?

    결론부터 말하자면 고정된 이미지 (프레임, Frame) 를 재빠르게 바꿔치기하면 잔상 효과에 의해 우리 뇌가 움직이는 영상이라고 인식하게 되는 것입니다. 얼마나 빠르게 바꿔치기해야 할까요? 보통 1초에 적어도 30번 이상은 해야 움직인다고 인식한다고 합니다. 제 경험으로는 '부드럽게 움직인다' 정도의 인식이 되려면 적어도 1초에 60번은 프레임이 바뀌어야 하는 것 같습니다.

    따라서 우리가 일반적으로 쓰는 디스플레이는 보통 1초에 60번 정도, 비싼 게이밍 모니터는 1초에 240번까지도 프레임이 바뀝니다. 우리는 이렇게 화면이 1초에 몇 번 바뀌는지의 수를 주사율(Refresh Rate) 이라고 부르고, 단위는 Hz (헤르츠) 를 씁니다. 그래픽 카드에서는 디스플레이의 주사율이 얼마인지를 알아내 60Hz이면 1초에 60번, 240Hz이면 1초에 240번씩 디스플레이에 프레임을 보내줍니다.

    1강에서 말했듯이 그래픽카드는 프레임버퍼라는 메모리 영역을 읽어 화면을 띄웁니다. 그런데 이 프레임버퍼는 하나가 아니라 여러 개를 사용합니다. 왜 그럴까요? 만약 프레임버퍼가 하나라면, 프레임버퍼에 이미지를 채우는 도중에 모니터에 전송이 될 수 있겠죠? 그럼 모니터에는 그리다 만 이미지가 표시될 것입니다. 보통 프레임을 새로 그릴 때, 프레임버퍼의 내용을 모두 검은색으로 채우고 다시 그리기 때문에, 이런 현상이 자주 반복되면 화면은 빠르게 깜빡깜빡거리며, 우리 눈이 아플 것입니다. 따라서 GLFW에서는 이런 현상을 방지하기 위해 더블 버퍼링 (Double Buffering) 이라는 기법을 사용합니다.

    더블 버퍼링은 지금 화면에 표시되어야 하는, 혹은 표시되고 있는 프런트 버퍼(Front Buffer)와 현재 그리고 있는 미완성된 백 버퍼(Back Buffer) 두 개의 프레임버퍼를 사용하는 방식입니다. Back Buffer에 프레임을 다 그리면, Front Buffer와 빠르게 Swap(바꿔치기)해 버리는 것입니다. 이렇게 하면 적어도 밝은 프레임이 표시되다가 그리다 만 어두운 프레임으로 바뀌는 일은 없겠죠?

    이 Swap하는 작업은 glfwSwapBuffers() 함수를 통해 구현되어 있습니다. 이 함수는 프레임을 모두 그린 후 호출하면 됩니다.

    while (!glfwWindowShouldClose(window)) {
        // TODO: 화면에 뭔가 그리기
    
        // Front Buffer와 Back Buffer Swap하기
        glfwSwapBuffers(window);
    
        glfwPollEvents();
    }

    그런데 한가지 문제점이 더 있습니다. 만약 이 Front Buffer와 Back Buffer를 스왑하는 도중에 디스플레이에 전송된다면 어떻게 될까요? 디스플레이는 위에서 아래로 스캔하듯이 업데이트되기 때문에 현재까지 스왑된 선을 기준으로 윗부분은 n번째 프레임, 아랫부분은 n+1번째 프레임이 표시될 것입니다. 만약 화면전환이 빠르다면 이 현상이 눈에 띌 정도로 거슬리는데요, 마치 화면이 위아래로 찢어진 것 같다 해서 스크린 테어링 (Screen Tearing) 현상이라고 합니다.

    Screen Tearing 예시

    이 현상의 해결방법은 디스플레이가 업데이트 된 직후에 다음 업데이트가 이루어지기 전에 재빠르게 프레임버퍼를 스왑하는 것인데요, 이것은 렌더 루프가 시작하기 전에 glfwSwapInterval(int interval)함수를 호출해서 설정할 수 있습니다. 함수 인자 intervalglfwSwapBuffers()가 호출이 되었을 때 디스플레이 업데이트 (화면에 프레임 전송)을 interval번 기다렸다가 스왑하라는 의미입니다. 보통 1보다 큰 값은 사용하지 않는다고 합니다.

    glfwSwapInterval(1);

    이 기능은 다른 말로 V-Sync (Vertical Sync, 수직 동기화)라고도 합니다. V-Sync를 사용하게 되면 FPS (Frames Per Second, 초당 프레임수)가 디스플레이의 주사율로 고정되게 됩니다.

    그럼 이제 화면을 노란색으로 채워 볼까요? OpenGL 4.5에서 프레임버퍼를 전부 채우는 방법은 void glClearNamedFramebufferfv(GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLfloat *value) 함수를 사용하는 것입니다. 이름부터가 굉장이 복잡해 보이는데, 차근차근 뜯어 보면 어렵지 않습니다.

    이름
    void리턴 타입 없음
    gl모든 OpenGL 함수 앞에 붙는 접두사
    Clear지우다, 치우다 등의 의미
    Named"이름 붙은"이라는 뜻으로, 프레임버퍼의 번호(Name)를 인자로 받는다는 의미
    Framebuffer프레임버퍼
    fvGLfloat 타입 (f: float) 들의 배열 (v: vector) 인자로 받는다는 의미
    (함수 인자 리스트 시작
    GLuint framebuffer,GLuint (OpenGL에서 사용하는 32비트 unsigned int 자료형) 타입의 framebuffer 번호.
    0이면 화면으로 보낼 Back Buffer를 의미한다.
    GLenum buffer,열거형 GLenum 타입으로, 버퍼의 종류를 의미.
    GL_COLOR, GL_DEPTH, GL_STENCIL등의 값이 올 수 있다.
    GLint drawbuffer,GLint (32비트 signed int 자료형) 타입으로, drawbuffer의 인덱스. Drawbuffer 여러개를 사용하는 것은 고급 기술이므로 여기서는 설명하지 않겠다. 0으로 두면 된다.
    const GLfloat *value프레임버퍼를 채울 색상 값들을 가리키는 포인터. 0.0 ~ 1.0 사이의 값을 가질 수 있다.
    )함수 인자 리스트 끝

    우리는 0번 프레임버퍼 (Back buffer)의 색상 (GL_COLOR)을 노란색 (1.0, 1.0, 0.0)으로 만들고 싶으므로, 아래와 같이 호출하면 됩니다. 이 코드를 렌더 루프 안에 넣으면 매 프레임마다 프레임 전체를 노란색으로 채우는 작업을 하겠죠?

    // 화면 노란색으로 채우기
    GLfloat color[4] = {1.0, 1.0, 0.0, 1.0};
    glClearNamedFramebufferfv(0, GL_COLOR, 0, &color[0]);

    잠깐, 근데 색상은 R, G, B 세 개의 값으로 이루어진다고 했는데, 왜 4개를 넣는 걸까요? 마지막 4번째 값은 알파(Alpha) 값으로, 일반적으로 투명도를 나타내는 값입니다. 알파값에 대해서는 나중에 블렌딩(Blending)을 배울 때 더 자세히 알아보도록 합시다.

    이렇게까지 하면 아래와 같이 예쁜 노란색 창이 뜰 것입니다.

    여기까지 전체 코드 (main.cpp):

    #include <iostream>
    
    #define GLFW_INCLUDE_NONE
    #include <GLFW/glfw3.h>
    #include <glad/glad.h>
    
    int main() {
        if (!glfwInit()) {
            std::cerr << "GLFW Initialization Failed!\n";
            return -1;
        }
    
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5);
    
        GLFWwindow* window = glfwCreateWindow(640, 480, "My Title", NULL, NULL);
        if (!window) {
            std::cerr << "Create Window Failed!\n";
            glfwTerminate();
            return -1;
        }
    
        glfwMakeContextCurrent(window);
        gladLoadGL();
        glfwSwapInterval(1);
    
        while (!glfwWindowShouldClose(window)) {
            // 화면 노란색으로 채우기
            GLfloat color[4] = {1.0, 1.0, 0.0, 1.0};
            glClearNamedFramebufferfv(0, GL_COLOR, 0, &color[0]);
    
            // Front Buffer와 Back Buffer Swap하기
            glfwSwapBuffers(window);
    
            glfwPollEvents();
        }
    
        glfwDestroyWindow(window);
    
        glfwTerminate();
        return 0;
    }
    

    댓글

Designed by Tistory.