.더블 버퍼링

화면 출력의 깜박임을 해결할 수 있는 근본적이고도 완전한 방법은 더블 버퍼링(Double Buffering)뿐이다. 더블 버퍼링이란 용어 그대로 버퍼를 두 개(또는 그 이상) 사용하는 방식인데 화면에 보여줄 버퍼와 내부 계산에 사용할 버퍼를 따로 유지한다. 내부 버퍼에 미리 그림을 그린 후 화면 버퍼로 고속 전송하며 그리는 중간 과정을 숨겨진 내부 버퍼에서 처리함으로써 사용자는 최종 결과만 보도록 하는 기법이다.

내부 버퍼에서 일어나는 일은 사용자에게 보이지 않기 때문에 그림이 아무리 복잡해도, 화면을 다 지운 후 다시 그리더라도 깜박임을 전혀 목격할 수가 없다. 뿐만 아니라 그리는 순서에 따라 이미지간의 수직적인 아래 위를 지정할 수 있으며 여러 개의 이미지를 동시에 움직이는 것도 아주 부드럽게 처리할 수 있다.

더블 버퍼링에 사용되는 내부 버퍼는 구체적으로 메모리 영역인데 이 메모리 영역은 외부 버퍼, 즉 화면의 포맷과 호환되어야 한다. 그래야 내부 버퍼에 그린 그림을 별도의 조작없이 외부 버퍼로 고속 전송할 수 있다. 과거 DOS 시절에는 내부 버퍼를 비디오 램의 물리적인 포맷대로 작성한 후 비디오 램으로 곧바로 전송하는 방식을 사용했었다. 또는 아예 비디오 카드가 하드웨어적으로 여러 개의 페이지를 제공하여 페이지를 교체하는 방식을 사용하기도 했다.

윈도우즈에서는 내부 버퍼를 메모리에 직접 작성할 필요가 없는데 왜냐하면 비트맵이 내부 버퍼 역할을 해 주기 때문이다. 화면 DC와 호환되는, 즉 색상 포맷이 같고 크기가 동일한 비트맵을 생성한 후 이 비트맵에 그림을 그리면 비트맵 자체가 내부 버퍼 역할을 하게 된다. 비트맵에 그려진 그림을 화면으로 전송할 때는 물론 BitBlt 함수를 사용한다. 앞에서 만든 Bounce 예제를 더블 버퍼링으로 다시 작성해 보자.

 

#define R 20

int x,y;

int xi,yi;

HBITMAP hBit;

void OnTimer()

{

RECT crt;

HDC hdc,hMemDC;

HBITMAP OldBit;

HPEN hPen,OldPen;

HBRUSH hBrush,OldBrush;

int i;

 

GetClientRect(hWndMain,&crt);

hdc=GetDC(hWndMain);

 

if (hBit==NULL) {

    hBit=CreateCompatibleBitmap(hdc,crt.right,crt.bottom);

}

hMemDC=CreateCompatibleDC(hdc);

OldBit=(HBITMAP)SelectObject(hMemDC,hBit);

 

FillRect(hMemDC,&crt,GetSysColorBrush(COLOR_WINDOW));

 

if (x <= R || x >= crt.right-R) {

    xi*=-1;

}

if (y <= R || y >= crt.bottom-R) {

    yi*=-1;

}

x+=xi;

y+=yi;

 

for (i=0;i<crt.right;i+=10) {

    MoveToEx(hMemDC,i,0,NULL);

    LineTo(hMemDC,i,crt.bottom);

}

 

for (i=0;i<crt.bottom;i+=10) {

    MoveToEx(hMemDC,0,i,NULL);

    LineTo(hMemDC,crt.right,i);

}

 

hPen=CreatePen(PS_INSIDEFRAME,5,RGB(255,0,0));

OldPen=(HPEN)SelectObject(hMemDC,hPen);

hBrush=CreateSolidBrush(RGB(0,0,255));

OldBrush=(HBRUSH)SelectObject(hMemDC,hBrush);

Ellipse(hMemDC,x-R,y-R,x+R,y+R);

DeleteObject(SelectObject(hMemDC,OldPen));

DeleteObject(SelectObject(hMemDC,OldBrush));

 

SelectObject(hMemDC,OldBit);

DeleteDC(hMemDC);

ReleaseDC(hWndMain,hdc);

InvalidateRect(hWndMain,NULL,FALSE);

}

 

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)

{

HDC hdc,hMemDC;

PAINTSTRUCT ps;

HBITMAP OldBit;

RECT crt;

 

switch(iMessage) {

case WM_CREATE:

    x=50;

    y=50;

    xi=4;

    yi=5;

    SetTimer(hWnd,1,25,NULL);

    return 0;

case WM_TIMER:

    OnTimer();

   return 0;

case WM_PAINT:

    hdc=BeginPaint(hWnd, &ps);

    GetClientRect(hWnd,&crt);

    hMemDC=CreateCompatibleDC(hdc);

    OldBit=(HBITMAP)SelectObject(hMemDC, hBit);

    BitBlt(hdc,0,0,crt.right,crt.bottom,hMemDC,0,0,SRCCOPY);

    SelectObject(hMemDC, OldBit);

   DeleteDC(hMemDC);

    EndPaint(hWnd, &ps);

    return 0;

case WM_DESTROY:

    if (hBit) {

       DeleteObject(hBit);

    }

    PostQuitMessage(0);

    KillTimer(hWnd,1);

    return 0;

}

return(DefWindowProc(hWnd,iMessage,wParam,lParam));

}

 

전역 비트맵 핸들 hBit가 선언되어 있으며 이 비트맵은 작업영역과 동일한 크기대로 생성된다. OnTimer에서 메모리 DC를 생성하고 이 DC에 비트맵을 선택한 후 메모리 DC에 그림을 출력하면 모든 출력이 비트맵에 작성될 것이다. Bounce 예제의 WM_PAINT에 있던 그리기 코드들이 모두 OnTimer로 이동되었다. OnTimer는 비트맵에 그림을 그린 후 InvalidateRect 함수를 호출하여 작업영역을 무효화하기만 한다. 이때 비트맵으로 화면을 완전히 덮을 수 있으므로 작업 영역은 지울 필요가 없으며 마지막 인수는 FALSE로 주어야 한다.

이 예제에서 OnTimer 함수는 내부 버퍼에 미리 그림을 그려 두는 작업을 하는데 이 함수가 더블 버퍼링의 핵심이다. OnTimer의 직접적인 결과물은 hBit에 그려진 그림뿐이며 이 비트맵에 그림을 그리는 과정은 아무래도 상관없다. 모두 지운 후 그리든, 엎어서 그리든 어차피 사용자에게는 보이지 않는다.

이 코드에서 흔히 오해하기 쉬운 것이 있는데 메모리 비트맵인 hBit와 메모리 DC인 hMemDC와의 관계이다. GDI 출력 함수들은 반드시 DC 핸들을 요구하며 비트맵에 출력하기 위해서는 이 비트맵을 선택하고 있는 메모리 DC의 핸들이 필요하다. 그래서 화면 DC와 호환되는(=비트맵과 호환되는) hMemDC를 생성하고 여기에 비트맵을 선택한 후 출력했다. 이 DC는 어디까지나 비트맵 출력을 위한 임시 DC이므로 비트맵을 다 작성하고 난 다음에는 해제되어야 한다.

더블 버퍼링에서 내부 버퍼라고 칭하는 것은 비트맵이지 메모리 DC가 아니다. 메모리 DC는 비트맵을 선택하기 위해 잠시만 사용되는 DC일 뿐인데 알다시피 비트맵을 선택할 수 있는 DC는 메모리 DC밖에 없기 때문이다. 그래서 전역으로 저장해야 할 대상은 hBit 비트맵이지 hMemDC가 아니다.

WM_PAINT에서는 OnTimer가 작성해 놓은 비트맵을 화면으로 전송하기만 한다. 즉, 이미 그려져 있는 그림(내부 버퍼)을 화면(외부 버퍼)으로 복사만 하는 것이다. 실행 결과는 다음과 같다. 지면으로 보기에는 결과가 동일하지만 실제로 실행해 보면 깜박임을 전혀 느낄 수 없을 것이다. 애니메이션이 아주 부드럽게 실행된다.

이 경우도 OnTimer에서 FillRect 함수를 호출하여 이전 그림을 지우기는 하는데 내부 버퍼에서 일어나는 일이기 때문에 화면을 지운 상태는 사용자 눈에 보이지 않으며 따라서 깜박임이 전혀 없는 것이다. 이런 방식대로라면 여러 개의 공을 한꺼번에 움직이더라도 전혀 무리가 없다. Bounce3 예제는 한꺼번에 10개의 공을 움직인다.

 

#define R 20

int x[10],y[10];

int xi[10],yi[10];

HBITMAP hBit;

void OnTimer()

{

RECT crt;

HDC hdc,hMemDC;

HBITMAP OldBit;

HPEN hPen,OldPen;

HBRUSH hBrush,OldBrush;

int i,ball;

 

GetClientRect(hWndMain,&crt);

hdc=GetDC(hWndMain);

 

if (hBit==NULL) {

    hBit=CreateCompatibleBitmap(hdc,crt.right,crt.bottom);

}

hMemDC=CreateCompatibleDC(hdc);

OldBit=(HBITMAP)SelectObject(hMemDC,hBit);

 

FillRect(hMemDC,&crt,GetSysColorBrush(COLOR_WINDOW));

for (i=0;i<crt.right;i+=10) {

    MoveToEx(hMemDC,i,0,NULL);

    LineTo(hMemDC,i,crt.bottom);

}

 

for (i=0;i<crt.bottom;i+=10) {

    MoveToEx(hMemDC,0,i,NULL);

    LineTo(hMemDC,crt.right,i);

}

 

hPen=CreatePen(PS_INSIDEFRAME,5,RGB(255,0,0));

OldPen=(HPEN)SelectObject(hMemDC,hPen);

hBrush=CreateSolidBrush(RGB(0,0,255));

OldBrush=(HBRUSH)SelectObject(hMemDC,hBrush);

 

for (ball=0;ball<10;ball++) {

    if (x[ball] <= R || x[ball] >= crt.right-R) {

       xi[ball]*=-1;

    }

    if (y[ball] <= R || y[ball] >= crt.bottom-R) {

       yi[ball]*=-1;

    }

    x[ball]+=xi[ball];

    y[ball]+=yi[ball];

 

    Ellipse(hMemDC,x[ball]-R,y[ball]-R,x[ball]+R,y[ball]+R);

}

 

 

DeleteObject(SelectObject(hMemDC,OldPen));

DeleteObject(SelectObject(hMemDC,OldBrush));

 

SelectObject(hMemDC,OldBit);

DeleteDC(hMemDC);

ReleaseDC(hWndMain,hdc);

InvalidateRect(hWndMain,NULL,FALSE);

}

 

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)

{

HDC hdc,hMemDC;

PAINTSTRUCT ps;

HBITMAP OldBit;

RECT crt;

int ball;

 

switch(iMessage) {

case WM_CREATE:

    for (ball=0;ball<10;ball++) {

       x[ball]=50;

       y[ball]=50;

    }

    xi[0]=4;yi[0]=5;

    xi[1]=5;yi[1]=4;

    xi[2]=3;yi[2]=4;

    xi[3]=8;yi[3]=3;

    xi[4]=3;yi[4]=8;

    xi[5]=2;yi[5]=1;

    xi[6]=10;yi[6]=12;

    xi[7]=12;yi[7]=16;

    xi[8]=3;yi[8]=3;

    xi[9]=6;yi[9]=7;

    SetTimer(hWnd,1,25,NULL);

    return 0;

case WM_TIMER:

    OnTimer();

    return 0;

case WM_PAINT:

    hdc=BeginPaint(hWnd, &ps);

    GetClientRect(hWnd,&crt);

    hMemDC=CreateCompatibleDC(hdc);

    OldBit=(HBITMAP)SelectObject(hMemDC, hBit);

    BitBlt(hdc,0,0,crt.right,crt.bottom,hMemDC,0,0,SRCCOPY);

    SelectObject(hMemDC, OldBit);

    DeleteDC(hMemDC);

    EndPaint(hWnd, &ps);

    return 0;

case WM_DESTROY:

    if (hBit) {

       DeleteObject(hBit);

    }

    PostQuitMessage(0);

    KillTimer(hWnd,1);

    return 0;

}

return(DefWindowProc(hWnd,iMessage,wParam,lParam));

}

 

공의 좌표를 기억하는 x,y와 좌표의 증분값인 ix,iy를 크기 10의 배열로 정의했으며 OnTimer에서 10번 루프를 돌면서 공 10개를 한꺼번에 이동시킨다. 각 공의 이동 증분을 다르게 주어 이동 속도를 다양하게 처리해 보았다. 직접 실행해 보면 알겠지만 한꺼번에 10개의 물체가 복잡하게 움직이더라도 전혀 깜박거리지 않으며 속도가 심하게 느려지는 것도 아니다.

더블 버퍼링의 단점이라면 일단 내부 버퍼에 그림을 그린 후 다시 외부 버퍼로 전송하기 때문에 전체적인 속도가 느려질 수 있다는 점이다. 이때 1초에 몇번씩 전송할 수 있는가를 프레임 레이트(Frame Rate)라고 하는데 내부 버퍼에 그림을 준비하는 과정이 복잡할수록 프레임 수가 떨어진다. 다행히 보통 사람의 눈은 1초에 16번 이상의 변화를 감지하지 못하기 때문에 16프레임 이상만 되면 부드러운 움직임을 구현할 수 있다. 물론 그보다 프레임 수가 더 높으면 애니메이션의 품질은 더욱 높아질 것이다.