.깜박임 문제

윈도우에 무엇인가를 반복적으로 출력하려면 깜박임(Flickering)이 발생한다. 특히 반복적으로 그림이 교체되는 애니메이션이나 게임같은 경우 깜박임이 무척 심한데 이런 깜박임은 눈을 피로하게 만들기 때문에 프로그램의 질을 현격하게 떨어뜨린다. 이런 프로그램을 오래 쓰다 보면 심지어 머리까지 아파올 지경이다. 그렇다면 왜 깜박임이 발생하는지 원인을 분석해 보고 그 해결책을 모색해 보도록 하자.

다음 Bounce 예제는 배경 화면에 바둑판 모양의 무늬를 그려 두고 이 무늬 위에서 공을 이동시킨다. 공은 윈도우의 벽에 부딪치면 입사각과 같은 각도로 반사되어 사각의 폐쇄된 공간에서 반복적인 반사 운동을 한다. 게임이든 애니메이션이든 화면에서 무엇인가 움직이는 프로그램이라고 가정하도록 하자. 문제를 정형화하기 위해 메인 윈도우의 크기는 640*350의 고정 크기를 갖도록 했는데 가변 크기라도 해결 방법은 비슷하게 적용할 수 있다.

 

#define R 20

int x,y;

int xi,yi;

void OnTimer()

{

RECT crt;

 

GetClientRect(hWndMain,&crt);

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

    xi*=-1;

}

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

    yi*=-1;

}

x+=xi;

y+=yi;

 

InvalidateRect(hWndMain,NULL,TRUE);

}

 

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

{

HDC hdc;

PAINTSTRUCT ps;

HPEN hPen,OldPen;

HBRUSH hBrush,OldBrush;

RECT crt;

int i;

 

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);

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

       MoveToEx(hdc,i,0,NULL);

       LineTo(hdc,i,crt.bottom);

    }

 

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

       MoveToEx(hdc,0,i,NULL);

       LineTo(hdc,crt.right,i);

    }

 

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

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

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

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

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

    DeleteObject(SelectObject(hdc,OldPen));

    DeleteObject(SelectObject(hdc,OldBrush));

    EndPaint(hWnd, &ps);

    return 0;

case WM_DESTROY:

    PostQuitMessage(0);

    KillTimer(hWnd,1);

    return 0;

}

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

}

 

코드 자체는 무척 간단하다. 네 개의 전역 변수와 한 개의 매크로 상수가 정의되어 있는데 x,y는 공의 현재 좌표이며 R은 공의 반지름, xi, yi는 각각 공의 수평, 수직 이동 증분이다.  공의 반지름은 20으로 정의되어 있으며 최초 공은 (50,50)에 위치하여 매 0.025초마다 x쪽으로 4픽셀씩 움직이며 y쪽으로 5픽셀씩 움직인다.

WM_CREATE에서 타이머를 설치하고 OnTimer에서 x,y좌표를 xi, yi만큼 증가시키되 벽에 부딪치면 xi, yi의 부호를 바꾸어준다. 즉 아래로 내려가다가 바닥에 닿으면 다시 위로 올라가며 오른쪽 벽에 부딪히면 왼쪽으로 이동한다. OnPaint에서는 바둑판의 격자 무늬를 출력하고 x,y좌표에 공을 출력하였다. 실행 모습은 다음과 같다.

실제로 실행해 보면 공이 움직이기는 하지만 화면 깜박임이 아주 심하게 느껴질 것이며 컴퓨터가 느리면 느릴수록 깜박임이 더욱 심해진다. 컴퓨터가 아주 빠르다면 깜박임의 정도가 덜하겠지만 그렇다고 해서 깜박임이 없어지는 것은 아니며 다만 깜박임의 주파수만 높아질 뿐이다. 그렇다면 왜 이런 깜박임이 생기는 것일까? 그 이유는 InvalidateRect 함수의 마지막 인수에 있는데 이 값이 TRUE이면 화면 전체를 다시 지운 후 그리기 때문이다.

화면을 다시 지우면 WM_ERASEBKGND 메시지가 발생하며 이 메시지에서 윈도우 클래스에 등록된 배경 브러시로 작업영역 전체를 완전히 지운다. 그리고 WM_PAINT에서 깨끗하게 지워진 작업영역에 격자와 공을 다시 그리기 때문에 완전히 지워진 상황과 그림이 그려진 상황이 반복적으로 눈에 보이게 되므로 깜박임이 느껴지는 것이다.

격자가 촘촘히 그려진 화면과 흰색으로 깨끗하게 지워진 화면이 계속 교체되므로 화면이 떨리는 것처럼 느껴진다. 이때 깜박임의 정도는 화면이 지워진 후 얼마나 빨리 다시 그리기를 하는가, 즉 빈 화면인 상태가 얼마나 오래 가는가에 따라 달라진다. 격자 무늬와 공을 최대한 빨리 그리면 깜박임의 정도가 덜해지기는 하지만 그래도 흰 화면이 아예 없어지지 않는 한 깜박임이 없어지지는 않는다.

깜박임의 원인이 화면을 지우는 것 때문이라면 화면을 지우지 않음으로써 깜박임을 없앨 수 있을 것이다. 과연 그런지 InvalidateRect 함수의 마지막 인수를 FALSE로 바꾼 후 테스트해 보자. 또는 WM_ERSEBKGND 메시지를 막아 버리든가 아니면 윈도우 클래스의 배경 브러시를 NULL로 지정해 주어도 된다. 결과는 다음과 같다.

실제로 실행해 보면 과연 화면은 전혀 깜박이지 않는다. 원래 그려져 있던 그림을 지우지 않기 때문에 빈 화면이 눈에 보이지 않으므로 깜박임이 없는 것이다. 그렇지만 지금 이 결과가 원하는 바는 아니다. 공이 움직이기는 하지만 잔상이 지워지지 않기 때문에 원래 그려져 있던 공이 그 자리에 계속 남아있으며 장면 전환이 제대로 되지 않았다.

결국 애니메이션을 제대로 처리하려면 원래 그려져 있던 그림을 지우고 새 그림을 다시 그려야 한다. 그래서 InvalidateRect 함수의 마지막 인수를 TRUE로 해 주거나 아니면 따로 잔상을 지워 주는 코드를 작성해야 한다. 애니메이션을 제대로 하면서 깜박임을 없애려면 여러 가지 방법을 동원할 수 있다.

우선 가장 쉬운 방법은 무효영역을 최소화하여 깜박임을 거의 느낄 수 없도록 만드는 것이다. 위 예제의 경우 움직이는 물체는 공 뿐이고 이 공은 최대 5픽셀 이상 움직이지 않으므로 현재 공의 위치 x,y에서 반지름 R과 최대 이동거리 5만큼의 영역만 무효화시킨다. 그러면 공 주변만 다시 그려지고 나머지는 그대로 있게 되므로 그리는 속도가 빨라지고 깜박임을 거의 느낄 수 없게 된다. 이 방법은 속도가 아주 빠른 장점이 있으며 실제로 거의 깜박임을 느낄 수 없을 정도로 효율이 좋기는 하다. 하지만 애니메이션 영역이 좁아야 한다는 제약이 있어 일반적인 해법이라 할 수 없다.

두번째 방법은 원래 그림을 지우지는 않되 새로 그려지는 그림으로 덮어 쓰는 것이다. 그러면 적어도 빈 화면이 보이지는 않기 때문에 깜박임은 눈에 보이지 않는다. 예를 들어 흰 바탕에 "ABC" 문자열이 출력되어 있는 상황에서 "de"로 교체한다고 해 보자. 이때 이전 문자를 지우지 않고 "de"만 출력하면 "deC"라고 출력될 것이다. 이때 "de" 뒤에 공백을 넣어 "de    "를 출력하면 이전 문자를 완전히 깔끔하게 덮어 버릴 수 있다. 이 방법은 배경과 그려지는 그림이 단순할 때만 적용할 수 있으며 위 예제는 격자가 있기 때문에 적용하기 어렵다.

이외에 깜박임을 최소화할 수 있는 여러 가지 방법들이 있는데 프로그램의 상황에 따라 적용할 수 있는 기법에 제약이 아주 많다. 움직이는 물체가 많거나 서로 겹치지 않도록 해야 한다면 이런 간단한 방법들을 쓰기는 어렵다. 더구나 게임같은 복잡한 프로그램은 물체가 스스로 애니메이션까지 되어야 하므로 보통의 방법으로는 깜박임을 제거하기 어렵다. 깜박임을 없애기 어려운 근본적인 이유는 원래 그림을 지워야만 새로운 그림을 출력할 수 있기 때문이다.