그러데이션과 팔레트

Win32의 GDI API로 그러데이션을 어떻게 그리면 될까요? 특히 1990년대 중반의 윈도우 3.x 시절에는 설치 프로그램이 파랑-검정의 그러데이션 화면으로 나타나는 게 유행을 타던 시절도 있었죠.

그런데 이것이 기본 API 함수만으로는 쉽지 않습니다. 원래 윈도우 GDI는 매우 정적이고 추상적으로 설계되었습니다. 선 하나, 색깔 하나도 PEN, BRUSH 같은 계층을 거쳐서 표현되죠. 덕분에 GDI는 윈도우 초창기 시절부터 지금까지 거의 변한 게 없는 일관된 API를 제공함과 동시에 철저한 장치 독립성을 실현했지만, 그 대신 비디오 화면이라는 매체에 특화되어 있는 에니메이션이라든가 색깔이 현란하게 바뀌는 그러데이션을 표현하는 데에는 상대적으로 한계를 드러냅니다.

그러데이션의 경우, 중간색에 대해서 일일이 GDI 오브젝트를 만들었다가 소멸을 해야 하기 때문에 비효율적입니다. 디더링이 필요한 비디오 카드나 프린터 등에서는 이런 추상화 계층이 쓸모가 있지만 요즘처럼 고성능 그래픽 환경에서는 이것이 거추장스럽게만 하겠죠. 또한 요즘 굳이 게임이 아니더라도 거의 필수라 할 수 있는 비트맵을 다루는 기본 API 자체도 32비트 윈도우로 와서야 운영체제 API의 일부로 정식 편입된 것입니다.

이 프로그램에서는 무려 다섯 가지 방법으로 그러데이션을 그려 보았습니다. 물론 이런 용도로는 GDI+라는 새로운 API가 도입된 것도 있지만, 이 프로그램에서는 일단 재래식 GDI만 생각하기로 했습니다.

  1. 매 줄마다 중간색에 해당하는 PEN을 새로 생성해서 MoveToEx()와 LineTo()로 색깔을 바꿔 가며 선을 그렸습니다. 선이기 때문에 임의의 방향의 직선이나 곡선으로도 그러데이션을 표현할 수 있죠.
  2. ExtTextOut()를 이용하여 텍스트 배경색으로 채워진 사각형을 그리는 기법을 썼습니다. 일종의 편법입니다. MFC의 CDC::FillSolidRect()가 이런 방식으로 구현돼 있지요. GDI 오브젝트의 생성과 소멸이 없기 때문에 속도가 빠릅니다. 하지만 수평/수직 그러데이션만 그릴 수 있다는 한계가 있습니다.
  3. 매 줄마다 중간색에 해당하는 PEN 대신 BRUSH를 생성해서 FillRect()로 선을 그렸습니다. PEN과는 달리 SelectObject()의 호출이 필요 없다는 점은 그나마 다행입니다. 이것이 가장 정석적인 방법입니다.
  4. 3과 비슷한 방식이지만 윈도우 2000에서 새로 추가된 DC BRUSH를 이용했습니다. 운영체제가 제공하는 stock brush를 한 번 지정만 한 뒤, 이 BRUSH의 색깔은 SetDCBrushColor()로 지정만 하면 GDI 오브젝트 자체를 건드릴 필요 없이 즉시 바뀌게 됩니다. 색을 빈번하게 바꿔 가며 그래픽을 출력할 때 매우 편리합니다. BRUSH뿐만 아니라 PEN에 대해서도 같은 기능이 추가됐지요.
  5. 앞의 네 가지와는 성격이 사뭇 다른 API를 이용했습니다. 윈도우 98부터는 프로그램의 제목 표시줄에 파랑-청록 그러데이션이 추가됨과 동시에, 앞서 말씀드린 대로 기존 GDI API만으로는 그리기가 곤란한 그러데이션을 한번에 그려 주는 GradientFill()이 추가되었는데, 이를 이용한 것입니다. 이 함수는 gdi32.dll에 있지 않고 msimg32.dll에 있습니다.

컴퓨터가 하이컬러 이상을 표현할 수 있는 환경이라면 위의 다섯 방식 중 어느 것을 사용하더라도 아래 그림에서 A와 동일한 결과가 나옵니다. 하지만 256색 이하의 환경에서는 1과 2번 방식은 C처럼 흉하게 출력되고, 3~5번 방식은 그나마 B처럼 디더링이라도 되어서 나옵니다. 오로지 solid color(순색)만 지원하는 펜보다는, 브러시가 그래도 색깔 처리 면에서 더 완성도가 높은 방식임을 알 수 있습니다.

첨부하는 프로그램을 실행해서 A~E 키를 누르면 그러데이션 그리는 방식을 바꿔 가며 확인할 수 있습니다. 또한 숫자 1~3을 누르면 색깔을 바꿀 수 있습니다.

소스 코드, 실행 파일 받기 (src19_color.zip, 23K)


팔레트

16컬러에서야 색깔 수가 워낙 부족하니 위의 그림에서 B 아니면 C밖에 더 답이 없지만, 256컬러 환경에서는 팔레트를 바꿔서 그러데이션이 아쉬운 대로 A처럼 나오게 할 수 있습니다.

팔레트는 오로지 256컬러라는 과도기 비디오 환경에서만 존재한 매우 유별나고 특이한 개념입니다. 시스템 전체가 공유하는 팔레트라는 자원에서 응용 프로그램별로 마음대로 사용 가능한 색깔을 제어하는 메커니즘인 셈이죠. 팔레트를 사용하기 위해서는 다음을 염두에 두면 됩니다.

  1. 내가 쓰고 싶은 색상을 모아서 팔레트 오브젝트를 WM_CREATE 같은 곳에서 미리 만들어 둡니다. (CreatePalette()) 이 오브젝트는 물론 우리가 알아서 소멸도 해 주어야 합니다. (WM_DESTROY)
  2. WM_PAINT에서는 그래픽을 출력하기 전에 SelectPalette와 RealizePalette를 호출합니다.
  3. 팔레트가 적용된 색상을 실제로 사용하려면 COLORREF를 인자로 받는 각종 GDI 함수에, RGB 매크로 대신 PALETTERGB 매크로를 사용하도록 합니다. RGB 매크로로 생성된 24비트 정수에다가 0x02000000라는 마스크가 OR된 결과입니다.
  4. WM_QUERYNEWPALETTE와 WM_PALETTECHANGED 메시지를 처리합니다. 전자는 내 프로그램이 활성화되어 팔레트를 1순위로 사용할 수 있게 되었을 때 오는 메시지이고 후자는 자신이나 다른 프로그램에 의해 팔레트가 변경되었을 때 오는 메시지입니다.

프로그램에서 사이띄개를 누르면 팔레트의 사용 여부를 바꿀 수 있습니다. 팔레트를 사용하게 되면, GradientFill() 방식을 제외한 다른 모든 방식에서 그러데이션이  "A. 원색"으로 예쁘게 출력됩니다. 이 프로그램을 여러 개 실행해서 제각기 다른 색상으로 팔레트를 사용하게 해 보면 재미있는 광경이 펼쳐질 것입니다. 내가 활성화해 있을 때는 A 모드이지만, 팔레트를 나만큼 사용하는 다른 인스턴스로 포커스를 빼앗기고 나면 그러데이션의 색깔이 도로 C 모드로 돌아가게 됩니다. 차라리 팔레트를 사용하지 않고 애초에 시스템 표준 컬러만으로 디더링을 하는 B 모드만이 아무 영향도 받지 않습니다.

아래 그림에서 포커스를 파란색 인스턴스로 옮기고 나면, 빨간색이 지금의 파란색처럼 울퉁불퉁하게 바뀌겠죠. 초록색은 포커스를 얻든 잃든 달라지는 게 없습니다. 우리가 지정해 준 팔레트 컬러만 이용해서 똑똑하게 A와 B 사이만을 오고가게 디더링을 시키는 기능은 없습니다. 내 프로그램이 활성화일 때와 비활성일 때의 화면 출력 로직을 완전히 따로 분리하지 않는 이상, A와 C 사이, 아니면 B 이렇게 딱 갈라지는 구도인 것 같습니다.

GradientFill()은 팔레트의 영향을 받지 않는다는 것도 흥미로운 점입니다. 운영체제와 함께 제공하는 API이기는 하지만 GDI 자체와는 출신이 다르기 때문에 그런 것이겠죠?

요즘은 최후의 보루라 여겨지던 게임조차도 256컬러를 사용하지 않고, 디스플레이 설정에도 하이컬러 이하는 뜨지도 않는 세상이 됐습니다. 지금은 그러데이션 정도는 일도 아닌 간단한 과정이 됐지만, 이를 윈도우 운영체제에서 막상 구현하다 보면, 어느 정도까지 호환성이 보장되는 API를 써서 어느 정도의 완성도(256색 환경에서 디더링이나 팔레트까지 다 고려할지 같은)로 만들지가 고민거리로 남는다는 것을 알 수 있습니다.

운영체제도, 비디오 카드도 하루아침에 지금과 같은 눈부신 성능으로 발전한 것이 아니지요. 다 과도기를 거쳤고 과거 없이는 현재가 있을 수 없기에 팔레트 같은 ‘과거 유물’도 남아 있는 것입니다.