한때 Windows에서 바탕 화면에 배경 그림을 표시하는 방식은 '바둑판, 화면 중앙, 화면 크기에 맞춤'이라는 딱 세 가지 중 하나를 선택할 수 있었다.
이것은 GDI에서 직사각형 영역에다 비트맵을 뿌리는 함수로 치면 각각 PatBlt, BitBlt, 그리고 StretchBlt에 대응한다. 지금은 몇 가지 방식이 더 나오는데, 그건 그림의 종횡비와 화면의 종횡비가 다를 때 확대를 어떻게 할지를 결정하는 것이므로 개념적으로 StretchBlt에 대응하는 셈이다.
GDI에서 비트맵 그래픽을 표현하는 추상적인 핸들 자료형은 잘 알다시피 HBITMAP이다. 그러나 위의 세 *Blt 함수들 중 어느 것도 HBITMAP을 인자로 받지 않는다. 이는 어찌 된 일일까?
이들은 비트맵을 자기들이 처리하기 용이한 형태로 바꾼 파생 자료형을 대신 사용한다. PatBlt를 사용하려면 뿌리려는 비트맵을 브러시로 바꿔야 하며, BitBlt와 StretchBlt는 해당 비트맵에 대한 그래픽 조작이 가능한 메모리 DC를 추가로 준비해야 한다. 그럼 그 구체적인 내역을 살펴보자.
모노크롬 아니면 16색 그래픽이 있던 시절, 도스용 그래픽 라이브러리에는 8바이트로 표현되는 8*8 단색 패턴이라는 게 있었다. 그 작은 공간으로도 벽돌, 사선 등 생각보다 기하학적으로 굉장히 기발한 무늬를 표현할 수 있었다.
Windows는 2000/ME까지만 해도 배경 그림은 오로지 BMP만 지원했으며(액티브 데스크톱을 사용하지 않는 한), 배경 그림이 차지하지 않는 나머지 영역은 그런 무늬로 도배하는 기능이 있었다. 물론 이것은 트루컬러 그래픽과는 영 어울리지 않는 낡은 기능이기에, XP부터는 깔끔하게 없어졌다.
PatBlt는 직사각형 영역을 주어진 브러시로 채우는 함수이다. 즉, 이 함수가 사용하는 원천은 함수의 별도 인자가 아니라 해당 DC에 선택되어 있는 브러시이다. 그럼 얘는 Rectangle이나 FillRect와 하는 일이 거의 차이가 없는 것 같아 보인다. 이 세 함수의 특성을 표로 일목요연하게 정리하면 다음과 같다.
PatBlt | FillRect | Rectangle | |
경계선을 current pen으로 그음 | X | X | O (유일) |
경계면을 current brush로 채움 | O | No, brush를 따로 인자로 받음 | O |
사각형 좌표 지정 방식 | x, y, 길이, 높이 | RECT 구조체 포인터 | x1, y1, x2, y2 (RECT 내용을 풀어서) |
래스터 오퍼레이션 지정 | O (유일) | X (= PATCOPY만) | X (왼쪽과 동일) |
다들 개성이 넘쳐 보이지 않는가? =_=;;
Rectangle은 선을 긋는 기능이 유일하게 존재하며, FillRect는 유일하게 사용할 브러시를 매번 인자로 지정할 수 있다. 그 반면 PatBlt가 유일하게 갖추고 있는 기능은 래스터 오퍼레이션인데, 사실 이것이 이 함수의 활용도를 크게 끌어올려 주는 기능이다. 이에 대해서도 앞으로 차차 살펴보도록 하겠다.
브러시는 '2차원 면을 바둑판 형태로 채우는 어떤 재질'을 나타내는 GDI 개체이다. 가로선· 세로선· 대각선 같은 간단한 무늬는 CreateHatchBrush로 지정 가능하지만 이건 오늘날에 와서는 영 쓸 일이 별로 없는 모노크롬 그래픽의 잔재이다.
CreateSolidBrush는 아무 무늬가 없는 순색 브러시를 표방하긴 하지만, 그래도 16/256컬러 같은 데서 임의의 RGB 값을 넘겨 주면 단순히 가장 가까운 단색이 아니라 ordered 디더링이 된 무늬가 생성된다.
그리고 다음으로 비트맵으로부터 브러시를 생성하는 함수는 바로 CreatePatternBrush이다.
여기에서 사용할 비트맵은 가장 간단하게는 CreateBitmap이라는 함수를 통해 생성할 수 있다. 이 함수가 인자로 받는 건 비트맵의 가로· 세로 크기와 픽셀 당 색상 수, 그리고 초기화할 데이터가 전부이다. 아주 간단하다.
그러나 이 비트맵은 그냥 2차원 배열 같은 픽셀 데이터 덤프 말고는 그 어떤 정보도 담겨 있지 않으며, 이걸로 만들 수 있는 건 구조가 극도로 단순해서 어느 그래픽 장비에서나 공통으로 통용되는 모노크롬 비트맵뿐이다. 즉 그 도스 시절의 8*8 패턴 같은 극도로 단순한 비트맵만 만들 수 있다. 오늘날에 와서 CreateBitmap은 모노크롬 비트맵 생성 전용이라고만 생각하면 된다.
모노크롬 비트맵을 기반으로 만들어진 DC나 브러시는 다른 solid/hatched 브러시와는 달리 자체적으로 색상 정보가 담겨 있지 않다. 그렇기 때문에 이때는 그래픽을 뿌리는 DC가 갖고 있는 텍스트의 글자색(값이 0인 곳)과 배경색이(값이 1인 곳) 양 색깔로 선택된다는 점도 참고하자. MSDN에 명시되어 있다. (0과 1 중 어느 게 글자인지 이거 은근히 헷갈린다. 빈 배경에서 뭔가 정보가 있다는 관점에서는 1이 글자 같아 보이기도 하니 말이다.)
그리고 브러시는 origin이라는 게 있어서 어떤 경우든 이를 원점으로 하여 바둑판 모양으로 뿌려진다. oxoxoxox라는 무늬가 있다면, 0,0부터 8,0까지 뿌린다면 oxoxoxox로 뿌려지지만 1,0부터 9,0까지 뿌린다면 ox가 아니라 xoxoxoxo가 된다는 뜻이다.
모노크롬이 아닌 컬러 비트맵을 저장하고 찍는 절차는 좀 복잡하다. 이미 컬러를 표현할 수 있는 DC로부터 CreateCompatibleDC와 CreateCompatibleBitmap을 거쳐서 비트맵을 생성해야 한다. 아니면 CreateDIBitmap를 써서 DIB라 불리는 '장치 독립 비트맵' 정보로부터 HBITMAP을 생성하든가.. 얘는 그냥 비트맵 데이터뿐만 아니라 팔레트 정보 같은 것도 담긴 헤더를 인자로 받는다. 출력할 그래픽 데이터와 출력 매체의 픽셀 구조가 다를 때를 대비해서 추상화 계층이 추가된 것이다.
원래 패턴 브러시는 8*8의 아주 작은 비트맵만 취급할 수 있었다. 그러나 NT 내지 95 이후의 버전부터는 그 한계가 없어지면서 브러시와 오리지널 비트맵 사이의 경계가 좀 모호해졌다. 그래도 PatBlt는 작은 비트맵 무늬 위주의 브러시를 래스터 오퍼레이션을 적용하여 그리는 용도에 원래 최적화돼 있었다는 점을 알아 두면 되겠다.
윈도우 클래스를 등록할 때 우리는 WNDCLASS의 hbrBackground 멤버를 흔히 (HBRUSH)(COLOR_WINDOW+1) 이런 식으로 때워 버리곤 하는데, 여기에다가도 저런 패턴 브러시를 지정해 줄 수 있다. 그러면 그 윈도우 배경에는 자동으로 바둑판 모양의 비트맵이 배경으로 깔리게 된다. 이런 식의 활용도 얼마든지 할 수 있다.
한편, 비트맵을 찍는 동작에는 그냥 있는 그대로 뿌리는 것뿐만이 아니라 래스터 오퍼레이션을 통해 반전을 해서 찍기(PATINVERT), 타겟 화면을 무조건 반전시키기(DSTINVERT), 타겟 화면을 무조건 검거나 희게 바꾸기 같은 세부 방식의 차이가 존재할 수 있는데, 앞서 언급한 FillRect뿐만 아니라 InvertRect나 DrawFocusRect 같은 함수도 사실은 PatBlt의 기능을 이용하여 다 구현 가능하다. cursor를 깜빡거리는 건 두 말할 나위도 없고 말이다.
임의의 색깔로 음영을 표현하는 것이라든가, 특히 이동이나 크기 조절을 나타내는 50% 반투명 검은 음영 작대기/테두리는 모두 이 함수의 xor 래스터 오퍼레이션으로 표현된다. 그걸 구현하는 데는 PatBlt 말고는 선택의 여지가 없다는 뜻. 흑백을 xor 연산 시키면 "원래 색 & 반전색"이 교대로 나타나니까 말이다.
이런 PatBlt에 반해 BitBlt는 비트맵을 SelectObject시킨 DC를 원본 데이터로 사용하기 때문에 컬러 비트맵의 출력에 더 최적화되어 있다. PatBlt처럼 비트맵을 바둑판 모양으로 반복 출력하는 기능은 없으며, 딱 원본 데이터의 크기만큼만 출력한다. PatBlt와는 달리 고정 origin이 없고 사용자가 찍으라고 한 위치가 origin이 된다. StretchBlt는 거기에다가 확대/축소 기능이 추가됐고 말이다.
이 정도면 비트맵 API에 대한 개념이 충분히 숙지될 수 있을 것이다. 각종 아이콘과 마우스 포인터들도 다 마스크 비트맵 AND와 컬러 비트맵 XOR이라는 래스터 오퍼레이션을 통해 투명 배경 내지 반전을 구현한다는 건 두 말하면 잔소리이다. 물론 오늘날은 알파 채널로 투명도를 구현하면서 래스터 오퍼레이션의 의미는 다소 퇴색했지만 말이다.
그럼 이제 비트맵 API들에 대한 개인적인 의문점과 아쉬운 점을 좀 나열하며 글을 맺겠다.
(1) GDI는 후대에 등장한 다른 그래픽 API들과는 달리, 글꼴을 제외하면 벡터와 래스터 모든 분야에서 안티앨리어싱과는 담을 싼 구닥다리 API로 전락해 있다. 그러니 비트맵을 정수 배가 아닌 확대/축소를 좀 더 부드럽게 하거나, 아예 임의의 일차변환을 한 모양으로 출력하려면 최소한 GDI+ 같은 다른 대체제를 써야 한다.
(2) 운영체제가 가로줄, 세로줄 같은 몇몇 known pattern에 대해서 CreateHatchBrush 함수를 제공하긴 하는데, 50% 음영 정도는 오늘날에도 많이 쓰이기 때문에 known 패턴이 좀 제공되어야 하지 않나 싶다. 그게 없어서 수많은 프로그램들이 내부에 0x55, 0xAA 배열을 일일이 생성해서 패턴을 만드는 것은 낭비이다.
오히려 cursor는 CreateCaret 함수에 (HBITMAP)1을 줘서 50% 음영을 만드는 기능이 있는데, 정작 그건 별로 쓸 일이 없다.
(3) 브러시 말고 펜으로 선을 그리는 걸 xor 반전 연산으로 하는 기능은 없는지 궁금하지 않으신지? 임의의 사선이나 원 테두리를 그렇게 그리는 건 그래픽 에디터를 만들 때도 반드시 필요하니 말이다.
물론 그런 기능이 없을 리가 없다. SetROP2라는 함수로 그리기 모드를 바꿔 주면 된다. 단, 여기서 입력받는 래스터 오퍼레이션 코드는 BitBlt가 사용하는 코드 체계와는 다르다. 비트맵 전송 API들은 화면의 원본 픽셀(D), 그리려는 픽셀(S)뿐만 아니라 패턴(P)이라는 변수가 또 추가되어서 원래는 3변수 코드를 사용한다. BitBlt는 PatBlt가 하는 일까지 다 할 수 있는 모양이다.
Posted by 사무엘