DirectX 2D 개발에서 가장 기반이 되는 작업은 랜더타겟을 세팅하고 이를 바탕으로 필요한 그리기 도구를 생성하는 일이다. 이 DirectX 2D 랜더 타겟에 대해 알아보자.
DirectX 2D 랜더 타겟이란?
DirectX 2D의 기본 팩토리 객체이다. 다시 말하지만 DirectX는 Windows의 COM 인터페이스에 기반한다. (지난 포스팅 참조)
DirectX 2D환경에서는 ID3D1Factory 인터페이스가 그리기 함수들을 가지고 있는 다양한 COM Interface를 불러 오게 해주는 기반 객체(ID3D1Render Target)인 것.
참고로 DirectX 3D에서도 랜더 타겟이라는 용어가 쓰이는데 이는 DirectX 2D와 별개이다.
DirectX 3D | DirectX 2D | |
팩토리 객체 | Device 인터페이스 (DeviceContext 인터페이스 생성) | ID3D1Factory 인터페이스 (ID3D1RenderTarget 인터페이스 생성) |
주로 쓰는 그리기 인터페이스(함수 묶음) | DeviceContext 인터페이스 | ID3D1RenderTarget 인터페이스 |
ID3D1Factory 인터페이스는 GPU독립적인 일들 (대표적으로 기본 도형인 사각형등을 이용해 게임의 충돌체크 처리를 해주는 작업, 그래픽 처리와 상관없지만 게임의 핵심 파트이기도 하다)과 GPU를 활용하기위한 기본 세팅작업(랜더 타겟 생성)들의 일을한다.
이를 바탕으로 DLL 같은 동적 라이브러리와 비슷한 역할을 하는 랜더타겟의 COM은 GPU의존적인 작업을 하며 해당 PC안에 하나만 존재한다.
COM은 이외에도 다양하게 사용된다.
예를들어 그림(비트맵 BMP, JPG, PNG파일)을 로딩하는 기능이라던지, 그래픽 카드(GPU)를 이용해 우리가 만들고 있는 게임이 요구하는데로 그래픽 출력 작업을 해주는 기능이라던지(다이렉트X의 주기능), Windows Office의 액셀파일이나 워드 파일을 로드하는 기능이라던지(뷰어) 등등의 각종 기능들을 코딩없이 우리 개발물에 넣어줄 수 있는것.(개발물이 C이건 C++이건 Java이건 상관이 없다)
이런 Windows의 COM 인터페이스는 서비스 제공자(COM 객체, 많은 경우 Windows, 또는 개발자가 직접 제작, 또는 원격 서버의 무언가)와 사용자(우리 프로세스)를 COM 서버, COM 클라이언트라는 호칭으로 구분한다.
DirectX 2D 랜더타겟과 GDI의 DC
이런 COM기반의 DirectX 말고 윈도우가 그냥 기본적으로 지원하는 그래픽 환경이 있다. 이 환경을 통해 윈도우 바탕화면과, 윈도우의 각 창들의 프레임들, 또는 엑셀, 워드등의 버튼들이 그려진다. 이를 GDI 환경이라고 하며, 윈도우에서 C++을 프로그래밍한다면 당연히 사용할 수 있다.
이 GDI는 DirectX의 랜더 타겟과 비슷한 기능을 한는 객체가 있다 DC이다.

보통 하나의 DC객체는 위의 그림처럼 한 윈도우의 클라이언트 화면(또는 전체 화면)을 자신의 영역으로 삼으며 개발자가 펜과 페인트, 또는 그림 파일 등을 사용할 수 있게 해주는 ‘관련 영역 그리기(render) 객체’ 정도로 해석하면 된다. 해당 영역에 대한 모든 그래픽 처리 기능을 담당하고 있는 것.
DirectX 2D의 랜더 타겟 객체는 사실 우리 코드 내에서는 비어있는 함수 포인터 목록만 보인다.(C++인 경우, C#이나 자바에서는 인터페이스(함수 목록만 가지고 있는 객체 껍데기 형식)
수차례 말했듯 DirectX 객체는 COM 인터페이스 너머 Windows 어딘가에 있으며 그것이 우리의 컴퓨터의 그래픽 디바이스(GPU)와 직접 소통하며 윈도우에 출력 작업을 관장한다.
그래서 컴퓨터의 그래픽 카드(GPU)에 따라서 이 랜더타겟의 세팅도 달라질 수 있다. 대부분 자동으로 세팅되며 경우에 따라서 일부를 우리가 바꿔준다.
DirectX라는 COM 객체가 하드웨어 의존적인 작업을 어느정도 처리해야 하기 때문
우리 코드상에서의 랜더 타겟

라인 44 랜더 타겟 객체가 있는 것이 보인다. 자료형을 타고 들어가 보면 (F12) 자료형 선언이 아래와 같이 되어있다.
interface DX_DECLARE_INTERFACE(“2cdxxxxx-xxxxx-xxxx-xxxxx-xxxxxxx”) ID2D1HwndRenderTarget : public ID2D1RenderTarget
interface라는 선언을 한 번 더 들어가면 이것이 구조체임을 알 수 있다(C++의 경우) 뒤에 보이는 2cdxxxxx-xxxxx-xxxx-xxxxx-xxxxxxx는 uuid라고 해서 Windows상에 이 DirectX의 COM객체가 가지고 있는 고유 번호(ID)이다. (다른 DirectX의 이 COM객체 역시 호환성을 위해 같은 uuid 값을 가진다)
이 번호를 통해 COM객체를 식별하고 실시간 소통을 하는 것.
참고로 우리 ID2D1HwndRenderTarget의 부모객체가 보이는데 ID2D1RenderTarget이다. 이것이 기본 랜더 타겟기능을 가지고있는 객체이다. 이를 기반으로 삼는 아래와 같은 것들이 있다.
- ID2D1HwndRenderTarget 우리가 쓰는 랜더 타겟 차일드 보통 하나의 윈도우를 클라이언트를 랜더 하는데 쓴다
- ID2D1DCRenderTarget 앞서 GDI 프로그래밍에서는 DC를 사용한다고 했다 이때 이 DC와 연계하기 위한 랜더타겟을 만든다
- 앞서서 우린 DirectX가 두 개의 계층을 가지고 있으며 하나는 기계와 맞닿아있기 때문에 잘 변하지 않는 DXGI층이 있고 소프트웨어적으로 업데이트가 활발한 DirectX3D 계층이 있다고 했다. (DirectX2D는 그 윗 계층이다. 4번 포스팅 참조) 이때 DXGI가 받아들이는 입력 표면에 직접 그림을 그리는 DXGIRenderTarget를 만들 수 있다. CreateDxgiSurfaceRenderTarget() 함수를 이용해서 랜더 타겟(ID2D1RenderTarget)을 만들면 된다.
랜더 타겟 초기화
앞선 포스팅에서 우린 우리가 원하는 COM 객체를 사용하기 위해 우선 모든 COM 객체가 가지고 있는 IUnknown이라는 인터페이스를 불러온다고 했다. 이 IUnknown 인터페이스의 기능은 하나다.

“네가 실제로 가지고 있는 인터페이스 정보를 내놓아라.”이다. 왜냐하면 처음엔 이 COM객체가 어떤 서비스를 어떤 함수들을 통해 제공해주는지 모르기 때문에 처음에 이 COM객체를 파악할 수 있는 가이드가 필요하기 때문. 심지어 알고 있던 COM객체도 업데이트하고 나면 뭐가 바뀔 수 있으니 이 과정은 필수이다.
이런 IUnknown을 가져오는 함수들이 보통 팩토리 함수로 불린다. 여기서도 처음 프로그램 초기화 과정에서 윈도우 창을 만들어주면서 (API과정 CreateWindow() 하면서 버튼 모양 창 크기, 메뉴 등 옵션등 기본 세팅 과정) 동시에 이 DirectX 용 팩토리 함수를 사용한다.

76라인 CreateDeviceIndependentResources()는 그래픽 카드 하드웨어와 밀접한 DXGI 레이어가 아닌 소프트웨어 부분을 랜더타겟이 관장하기위해서 그렇게 붙여진 함수명으로 보인다.

그리고 여기 보이는 D2D1CreateFactory() 함수가 m_pDirect2dFactory에 IUnknown 인터페이스 정보를 담아주고 있다. 이 변수(m_pDirect2dFactory)의 타입을 확인해보면 IUnknown인터페이스임이 보인다. (포스팅 상단 코드의 43라인 참고)
interface DX_DECLARE_INTERFACE(“0xxxxxxxxx-xxxxx-xxxxx-xxxxxxxx”) ID2D1Factory : public IUnknown
그리고 우린 매 그리기 순간마다 ( 대충 1초에서 300번 정도? ) 아래 함수를 불러주고 있는데 이때 이 IUnknown 인터페이스를 활용하고 있다.

CreateDeviceResources() 함수는 아래와 같다.

이 함수에서는 우리가 원하는 랜더 타겟을 만드는 CreateHwndRenderTarget 함수가 보인다. 하지만 1초에 300번씩 이것을 만드는 것은 아니다. 89라인에 보면 이 것이 없을 때만(NULL일 때만) 만든다. 이런 루틴은 랜더타겟 없이 랜더 작업을 하는 것을 미연에 방지하기 위한 안정장치인 셈.
그리고 아래쪽에서는 이렇게 랜더 타겟이 성공적으로 만들어졌을 때 이 기반 객체를 이용해서 m_pLightSlateGrayBrush와 m_pCornflowerBlueBrush라는 그리기 연필 같은 객체를 만들어준다. (이걸로 색칠도 된다 GDI의 DC는 팬과 브러쉬가 다른 그리기 도구지만 DirectX에서는 하나이이며 그저 선을 그릴 땐 stroke함수, 색을 채울 땐 fill 함수를 쓰면 된다)
우리 코드의 실제 그리기 과정

DirectX의 랜더 타겟은 기본적으로 더블 버퍼링 기능을 가지고 있다. (삼중 버퍼링도 되며 이는 만들 때 인자들로 설정해주면 된다).
더블 버퍼링이란 DirectX같은 그래픽 툴은, 모니터 화면에 그릴, 그림 정보를 버퍼로 저장해두었다 화면에 쏘는데 이때 이 버퍼에 다음에 그릴 그림 정보를 또 그리면 다시 그리는 시간동안 화면 표시가 이상할 수 있다. 이를 막기위해 두개의 버퍼를 번갈아가며 쓰는 기법.
이 그리는 작업은 m_pRenderTarget->BeginDraw();와 m_pRenderTarget->EndDraw(); 사이에 있는걸 한 번에 해버린다.
참고로 m_pRenderTarget는 위에서 열심히 설명한 우리의 랜더 타겟이다. 그리고 그려진 순간 장면 교체하도록 설정되어있으면 (방금 말한 섬세한 설정, 기본 설정이 이럴 것이다) 우리 화면에 그림이 바로 나타난다.
앞으로 볼 필요 없지만 지금은 잘 봐 둬야 하는 게 141라인에 SetTransform()이다 인자로 들어가는 데이터인 D2D1::Matrix3x2F::Identity() 는 3행 2열의 항등 행렬을 나타낸다. 수학적으로는 이렇게 생긴 애다

이는 2×2 행렬의 밑에 0 0 행을 하나 더 넣은 모습이다. 나중에 3D 프로세싱을 하게 되면 그려진 사물을 돌리거나 이동시킬 때 (또 원근감을 표현할 때) 이런 것들을 이용해서(곱해서) 어떤 수학적 표현을 해준다. 지금은 2D이므로 화면이나 게임 오브젝트들을(캐릭터, 아이템, UI 등)을 삼차원적으로 움직일 필요가 없다. 그래서 “3차원적으로는 뭔가를 하지 말고 그냥 둬라..”라는 설정을 할 필요가 있다. 그게 이 행렬을 넣어주는 행동이다.
일단 저 3×2 항등 행렬이 그런 의미라고 알고 있기만 하면 된다. 앞으로의 2D프로젝트에서 이는 더 이상 안 나온다. 그냥 써넣으면 된다. 이런 일을 해야 하는 이유는 위에 레이어 그림에서도 알 수 있듯 DirectX2D는 DirectX3D를 기반으로 하기 때문.
라인 142 m_pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White));
일단 그림을 그리기 전에 새롭게 하얗게 칠해주는 코드이다. 캔버스 정리(이걸 안 하면 잔상이 남을 것이다)
라인 144
D2D1_SIZE_F rtSize = m_pRenderTarget->GetSize();
int width = static_cast<int>(rtSize.width);
int height = static_cast<int>(rtSize.height);
현재 클라이언트 영역에 크기를 가져와서 저장해준다. static_cast는 int로 충분히 변환할수 있는 것들(unsigned int나 long같은)을 안전하게 형변환해주는 캐스팅일뿐 큰 의미는 없다.

라인 149 – 164
우리가 이미 본 결과 화면에서 배경 격자를 그려주는 부분이다. 처음에 세로줄을 그려주는데 각 선의 높이는 클라이언트 사이즈의 높이만큼이고 x좌표는 10칸씩 떨어져서 반복적으로 그려주고 있다. 가로줄도 비슷하게 처리되고 있는 게 보인다.

라인 167 – 176
우리가 원하는 사각형을 그리기 위해 두 사각형에 관한 구조체를 세팅해주고 있다. 첫 번째 사각형은 화면 높이와 너비의 중간 위치에서 가로세로 50만큼 떨어진 크기의 사각형을 정의해주고 있고 두 번째는 100만큼 떨어진 사각형을 정의해주고 있다.
이 사각형들을 177라인과 178라인에서 그려주고 있는데 하나는 속이 꽉 찬 사각형이고 다른 하나는 비어있는 사각형임을 알 수 있다.
라인 180
그리기 과정 루틴을 마무리되었으니 실제로 표현하라는 EndDraw() 함수가 보인다. 백버퍼에 그린걸 전면으로 돌리라는 뜻.(더블 버퍼링)
라인 183 – 187
예외처리 부분이다. 실제 이런 애러가 생기는 경우는 많지 않으며 보통 하드웨어적으로 스크린 해상도가 변경되었다거나 갑자기 그래픽 카드 기능이 중지되었을 때 생길 수 있다. 이 경우 우리 랜더타겟을 코드가 지워버리면서(DiscardDeviceResources()) 위에서 설명한 루틴에 의해 다시 랜더타겟을 만들려고 할 것이다. 안 만들어지면 그리기 수행을 건너 뛸테고 말이다.