-
[Direct2D] 도형 그려보기C++/미분류 2024. 2. 8. 17:53
지난 글 (Windows application 만들기)에서 WinAPI를 이용해 빈 윈도우를 만드는 방법을 알아봤습니다.
이번 글에서는 위 글의 내용에 이어, 빈 윈도우에 Direct2D(이하 D2D)로 도형을 그려보도록 하겠습니다.
이번 글은 MSDN을 참고하여 작성되었습니다.
간단한 Direct2D 애플리케이션 만들기 : Microsoft Learn
0. 라이브러리 추가
더보기Direct2D 라이브러리를 추가하는 작업이 선행되어야 합니다.
프로젝트 우클릭, Properties로 진입합니다.
Properties의 Linker->Input 항목의 Additional Dependencies에 다음과 같이 라이브러리를 추가합니다.
추가되는 항목은 다음과 같습니다.
d2d1.lib
dwrite.lib
WindowsCodecs.lib1. 헤더와 매크로 추가
더보기프로젝트에서 사용될 헤더들을 헤더파일에 추가합니다.
// Default headers #include <windows.h> #include <tchar.h> // DirectX headers #include <d2d1.h> #include <d2d1helper.h> #include <dwrite.h> #include <wincodec.h>
기본적인 헤더에, Direct2D 헤더를 추가했습니다.
이후 개발 방향에 따라 필요한 헤더를 추가해서 사용할 수 있습니다.
다음은 DirectX리소스들을 해제할 때 사용할 매크로를 정의하겠습니다.
#define SAFE_RELEASE(p) { if(p) { (p)->Release(); (p)=NULL; } }
위 매크로는 할당받은 리소스를 해제할 때 사용됩니다.
2. 클래스 함수 및 변수 선언
더보기이전 글에서 선언한 MyApp클래스에 필요한 함수, 변수를 추가하도록 하겠습니다.
우선 함수입니다.
class MyApp { public: MyApp(); ~MyApp(); HRESULT Initialize(HINSTANCE hInstance); void RunMessageLoop(); private: static LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); HRESULT CreateDeviceIndependentResources(); HRESULT CreateDeviceResources(); void DiscardDeviceResources(); HRESULT OnRender(); void OnResize(UINT width, UINT height); .... };
각각의 함수의 역할은 다음과 같습니다.
생성자 및 소멸자 리소르의 초기화(생성자), 해제(소멸자)를 수행합니다. Initialize 윈도우 클래스를 등록하고, 그리기 리소스를 할당합니다. RunMessageLoop 메시지 루프를 수행하는 함수입니다. WndProc 윈도우 프로시저 함수입니다. CreateDeviceIndependentResources 장치 독립적 자원을 생성합니다. CreateDeviceResources 장치 의존적 자원을 생성합니다. DiscardDeviceResources 장치 의존적 자원을 해제합니다. OnRender 화면에 표시할 내용을 그리는 로직을 수행합니다. OnResize 화면의 크기가 수정되었을 때 호출되는 함수입니다. 장치 독립적 자원과 의존적 자원은 하드웨어(주로 GPU등의 렌더링 장치)와의 연관성 여부입니다.
장치 독립적 자원은 CPU에 의해 관리되고, 장치 의존적 자원은 하드웨어에 의해 관리됩니다.
다음은 클래스 변수입니다.
class MyApp { ... private: HWND myHwnd = nullptr; ID2D1Factory* myDirect2dFactory = nullptr; ID2D1HwndRenderTarget* myRenderTarget = nullptr; ID2D1SolidColorBrush* myLightSlateGrayBrush = nullptr; ID2D1SolidColorBrush* myCornflowerBlueBrush = nullptr; };
각각의 변수는 다음과 같은 역할을 수행합니다.
HWND myHwnd 생성한 윈도우의 핸들입니다.
이 핸들로 윈도우를 조작합니다.ID2D1Factory* myDirect2dFactory D2D팩토리입니다.
D2D사용의 출발점이 되는 인터페이스 입니다.ID2D1HwndRenderTarget* myRenderTarget Render Target입니다.
그리기에 필요한 자원을 생성하고, 그리기 연산을 수행합니다.ID2D1SolidColorBrush* myLightSlateGrayBrush
ID2D1SolidColorBrush* myCornflowerBlueBrush브러시 객체입니다.
해당 예제에서 도형을 그릴 때 사용됩니다.상황에 따라 적절한 객체를 선언 후 사용할 수 있습니다.
3.1. 함수 구현 (자원 생성, 해제 함수)
더보기자원을 생성하고 해제하는 함수입니다.
호출되는 위치는 생성자와 소멸자, Initialize등 프로그램 로직의 초반, 후반부입니다.
예외 상황이 발생하여 자원에 대한 재생성이 필요할 경우, 프로그램 로직 중간에도 호출될 수 있습니다.
CreateDeviceIndependentResources
HRESULT MyApp::CreateDeviceIndependentResources() { HRESULT hr = S_OK; hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, &myDirect2dFactory); return hr; }
팩토리 객체의 초기화를 수행합니다.
첫 번째 인자는 자원의 동기화 지원 여부를 명시하는 것으로 D2D1_FACTORY_TYPE열거형을 인자로 받습니다.
SINGLE_THREADED와 MULTI_THREADED가 있습니다.
이번 글에서 사용될 장치 독립적 자원은 팩토리 하나이므로, 팩토리 자원에 대한 초기화만 수행합니다.
D2D1CreateFactory함수가 이 역할을 담당하고, 해당 자원의 초기화 결과를 반환합니다.
CreateDeviceResources
HRESULT MyApp::CreateDeviceResources() { HRESULT hr = S_OK; if (!myRenderTarget) { RECT rc; GetClientRect(myHwnd, &rc); D2D1_SIZE_U size = D2D1::SizeU(rc.right - rc.left, rc.bottom - rc.top); hr = myDirect2dFactory->CreateHwndRenderTarget( D2D1::RenderTargetProperties(), D2D1::HwndRenderTargetProperties(myHwnd, size), &myRenderTarget ); if (SUCCEEDED(hr)) { hr = myRenderTarget->CreateSolidColorBrush( D2D1::ColorF(D2D1::ColorF::LightSlateGray), &myLightSlateGrayBrush ); } if (SUCCEEDED(hr)) { hr = myRenderTarget->CreateSolidColorBrush( D2D1::ColorF(D2D1::ColorF::CornflowerBlue), &myCornflowerBlueBrush ); } } return hr; }
Render Target과 브러시 객체의 초기화를 수행합니다.
함수가 반복적으로 호출되어도, Render Target이 이미 유효하다면 로직이 수행되지 않도록 조건문으로 묶었습니다.
조건문 내부는 Render Target의 초기화와 초기화된 Render Target으로 브러시를 초기화 합니다.
초기화 할 때에는 Render Target의 경우 사이즈, 프로퍼티에 대한 정보, 브러시의 경우 색깔과 관련된 정보가 필요합니다.
위 코드에서는 D2D에서 제공되는 팩토리 함수와 열거형을 사용했습니다.
DiscardDeviceResources
void MyApp::DiscardDeviceResources() { SAFE_RELEASE(myRenderTarget); SAFE_RELEASE(myLightSlateGrayBrush); SAFE_RELEASE(myCornflowerBlueBrush); }
리소스 해제 함수입니다.
장치 의존적 자원들의 해제를 수행합니다.
3.2. 함수 구현 (생성자 및 소멸자)
더보기생성자 및 소멸자는 리소스의 초기화, 해제를 수행합니다.
초기화의 경우, 클래스 선언에서 nullptr로 기본값을 지정했기 때문에 생성자는 구현하지 않아도 됩니다.
생성자를 구현할 경우 다음과 같이 할 수 있습니다.
MyApp
MyApp::MyApp() : myHwnd(nullptr), myDirect2dFactory(nullptr), myRenderTarget(nullptr), myLightSlateGrayBrush(nullptr), myCornflowerBlueBrush(nullptr) { }
초기화 리스트를 통해 클래스 변수들을 초기화했습니다.
~MyApp
MyApp::~MyApp() { DiscardDeviceResources(); SAFE_RELEASE(myDirect2dFactory); }
소멸자에서는 장치 의존적 자원을 해제하는 함수와, 팩토리의 해제를 수행합니다.
3.3. 함수 구현 (Initialize)
더보기Initialize 함수는 윈도우의 생성과, 생성된 윈도우의 핸들을 받아오는 역할을 수행합니다.
이전 글에서 다루었던 Intialize함수의 구현은 다음과 같습니다.
HRESULT MyApp::Initialize(HINSTANCE hInstance) { HRESULT hr; WNDCLASSEX winCls = { sizeof(WNDCLASSEX) }; winCls.style = CS_HREDRAW | CS_VREDRAW; winCls.lpfnWndProc = MyApp::WndProc; winCls.cbClsExtra = 0; winCls.cbWndExtra = sizeof(LONG_PTR); winCls.hInstance = hInstance; winCls.hbrBackground = nullptr; winCls.lpszMenuName = nullptr; winCls.hCursor = LoadCursor(nullptr, IDI_APPLICATION); winCls.lpszClassName = L"MyApp"; RegisterClassEx(&winCls); myHwnd = CreateWindowW( L"MyApp", L"MyAppName", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 640, 480, nullptr, nullptr, hInstance, this ); hr = myHwnd ? S_OK : E_FAIL; if (SUCCEEDED(hr)) { ShowWindow(myHwnd, SW_SHOWNORMAL); UpdateWindow(myHwnd); } return hr; }
이 함수에, 위 문단에서 구현한 자원 초기화 함수를 호출하는 부분을 추가하겠습니다.
추가되는 부분은 다음과 같습니다.
HRESULT MyApp::Initialize(HINSTANCE hInstance) { HRESULT hr = CreateDeviceIndependentResources(); ... if (SUCCEEDED(hr)) { ShowWindow(myHwnd, SW_SHOWNORMAL); UpdateWindow(myHwnd); CreateDeviceResources(); } return hr; }
각각 함수의 시작부분과, 마지막 부분에 CreateDeviceIndependentResources및 CreateDeviceResources함수를 호출했습니다.
3.4. 함수 구현 (Resize)
더보기윈도우의 모서리를 잡고 끌었을 때, 윈도우의 크기를 조정할 수 있습니다.
OnResize함수는 윈도우의 크기가 변경되었을 때 호출되어, 화면의 표시 내용을 보정하는 역할을 수행합니다.
OnResize
void MyApp::OnResize(UINT width, UINT height) { if (myRenderTarget) { myRenderTarget->Resize(D2D1::SizeU(width, height)); } }
화면에 그려지는 도형들은 Render Target에 의해 그려지게 됩니다.
해당 크기는 윈도우의 크기에 의존적이기 때문에, 윈도우의 크기가 변경될 경우 그려지는 도형의 비율이 바뀔 수 있습니다.
Resize는 그런 상황에 대응하기 위해 Render Target의 크기를 변경합니다.
3.5. 함수 구현 (Rendering)
더보기렌더링 함수입니다.
화면에 그려지는 내용들, 그에 관한 로직이 해당 함수에서 수행됩니다.
함수를 부분적으로 살펴보도록 하겠습니다.
그리기 로직은 Render Target에 의해 수행됩니다.
Redner Target이 할당되어 있어야 하므로, 로직 수행 전에 Redner Target에 대한 초기화를 수행합니다.
HRESULT MyApp::OnRender() { HRESULT hr = S_OK; hr = CreateDeviceResources(); if (SUCCEEDED(hr)) { ... } }
초기화 함수인 CreateDeviceResources는 Render Target이 정상적일 경우 추가적인 로직이 수행되지 않습니다.
따라서 해당 초기화 로직을 통해 발생하는 오버헤드는 크지 않을 것으로 예상할 수 있습니다.
Render Target에 의한 그리기는 Render Target의 함수 BeginDraw, EndDraw사이에 수행되어야 합니다.
myRenderTarget->BeginDraw(); /* Draw logic */ hr = myRenderTarget->EndDraw();
그리기 연산은 일괄 처리됩니다.
EndDraw가 호출되는 시점에 일괄적으로 그려지고, 해당 결과가 EndDraw의 반환으로 전달됩니다.
따라서 그리기 연산의 결과를 EndDraw의 반환값으로 확인할 수 있습니다.
실제 그리기 로직은 위의 BeginDraw함수 호출 이후 수행합니다.
우선, Render Target의 위치를 원점으로, 배경을 하얀색으로 초기화하겠습니다.
myRenderTarget->SetTransform(D2D1::Matrix3x2F::Identity()); myRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White));
이후 화면에 격자를 그리겠습니다.
픽셀 단위로, 10픽셀의 격자를 화면에 채우는 코드입니다.
D2D1_SIZE_F rtSize = myRenderTarget->GetSize(); int width = static_cast<int>(rtSize.width); int height = static_cast<int>(rtSize.height); for (int x = 0; x < width; x += 10) { myRenderTarget->DrawLine(D2D1::Point2F(static_cast<FLOAT>(x), 0.0f), D2D1::Point2F(static_cast<FLOAT>(x), rtSize.height), myLightSlateGrayBrush, 0.5f); } for (int y = 0; y < height; y += 10) { myRenderTarget->DrawLine(D2D1::Point2F(0.0f, static_cast<FLOAT>(y)), D2D1::Point2F(rtSize.width, static_cast<FLOAT>(y)), myLightSlateGrayBrush, 0.5f); }
이후 화면 중앙에 사각형을 그리겠습니다.
그리는 사각형은 한 개는 내부가 채워져 있는 사각형으로, 한 개는 내부가 비어있는 사각형입니다.
D2D1_RECT_F rectangle1 = D2D1::RectF( rtSize.width / 2 - 50.0f, rtSize.height / 2 - 50.0f, rtSize.width / 2 + 50.0f, rtSize.height / 2 + 50.0f ); D2D1_RECT_F rectangle2 = D2D1::RectF( rtSize.width / 2 - 100.0f, rtSize.height / 2 - 100.0f, rtSize.width / 2 + 100.0f, rtSize.height / 2 + 100.0f ); myRenderTarget->FillRectangle(&rectangle1, myLightSlateGrayBrush); myRenderTarget->DrawRectangle(&rectangle2, myCornflowerBlueBrush);
그리기의 결과에 에러가 발생했을 경우, 자원의 해제를 수행하는 예외 처리가 필요합니다.
예외 처리는 다음과 같이 할 수 있습니다.
if (hr == D2DERR_RECREATE_TARGET) { DiscardDeviceResources(); }
위 코드를 묶은 OnRender 함수의 전체 코드는 다음과 같습니다.
HRESULT MyApp::OnRender() { HRESULT hr = S_OK; hr = CreateDeviceResources(); if (SUCCEEDED(hr)) { myRenderTarget->BeginDraw(); myRenderTarget->SetTransform(D2D1::Matrix3x2F::Identity()); myRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White)); D2D1_SIZE_F rtSize = myRenderTarget->GetSize(); int width = static_cast<int>(rtSize.width); int height = static_cast<int>(rtSize.height); for (int x = 0; x < width; x += 10) { myRenderTarget->DrawLine(D2D1::Point2F(static_cast<FLOAT>(x), 0.0f), D2D1::Point2F(static_cast<FLOAT>(x), rtSize.height), myLightSlateGrayBrush, 0.5f); } for (int y = 0; y < height; y += 10) { myRenderTarget->DrawLine(D2D1::Point2F(0.0f, static_cast<FLOAT>(y)), D2D1::Point2F(rtSize.width, static_cast<FLOAT>(y)), myLightSlateGrayBrush, 0.5f); } D2D1_RECT_F rectangle1 = D2D1::RectF( rtSize.width / 2 - 50.0f, rtSize.height / 2 - 50.0f, rtSize.width / 2 + 50.0f, rtSize.height / 2 + 50.0f ); D2D1_RECT_F rectangle2 = D2D1::RectF( rtSize.width / 2 - 100.0f, rtSize.height / 2 - 100.0f, rtSize.width / 2 + 100.0f, rtSize.height / 2 + 100.0f ); myRenderTarget->FillRectangle(&rectangle1, myLightSlateGrayBrush); myRenderTarget->DrawRectangle(&rectangle2, myCornflowerBlueBrush); hr = myRenderTarget->EndDraw(); } if (hr == D2DERR_RECREATE_TARGET) { hr = S_OK; DiscardDeviceResources(); } return hr; }
3.6. 함수 구현 (윈도우 프로시저)
더보기윈도우 프로시저는 윈도우 메시지가 전달되어, 메시지에 따른 로직을 수행하는 함수입니다.
이전 글에도 다루었지만, 이번 글에서는 추가된 기능이 있어 함수의 변경이 필요합니다.
우선, 이전 글의 윈도우 프로시저 함수입니다.
LRESULT MyApp::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { LRESULT result = 0; if (message == WM_CREATE) { LPCREATESTRUCT cStruct = reinterpret_cast<LPCREATESTRUCT>(lParam); MyApp* myApp = static_cast<MyApp*>(cStruct->lpCreateParams); SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(myApp)); result = 1; } else { MyApp* myApp = reinterpret_cast<MyApp*>(GetWindowLongPtrW(hWnd, GWLP_USERDATA)); bool wasHandled = false; if (myApp) { switch (message) { case WM_DESTROY: PostQuitMessage(0); result = 1; wasHandled = true; } } if (!wasHandled) { result = DefWindowProc(hWnd, message, wParam, lParam); } } return result; }
최초 실행 시 인스턴스의 포인터를 저장하고, 이후 해당 포인터에 대해 메시지에 따라 적절한 로직을 수행하는 함수입니다.
당시에는 화면을 생성하는 역할만 수행했기 때문에, 종료 메시지에 대한 구현만 추가했습니다.
추가할 기능은 message에 대한 switch 구문입니다.
switch (message) { case WM_SIZE: { UINT width = LOWORD(lParam); UINT height = HIWORD(lParam); myApp->OnResize(width, height); } result = 0; wasHandled = true; break; case WM_DISPLAYCHANGE: InvalidateRect(hWnd, NULL, FALSE); result = 0; wasHandled = true; break; case WM_PAINT: myApp->OnRender(); //ValidateRect(hWnd, NULL); result = 0; wasHandled = true; break; case WM_DESTROY: PostQuitMessage(0); result = 1; wasHandled = true; break; }
각각의 메시지는 다음과 같습니다.
WM_SIZE 크기 변경 메시지 입니다.
윈도우의 크기가 변경되었을 때 전달됩니다.WM_DISPLAYCHANGE 해상도 변경 메시지 입니다.
해상도가 변경되었을 때 전달됩니다.WM_PAINT 그리기 메시지 입니다.
화면 표시할 내용이 있을 때 전달됩니다.WM_DESTORY 종료 메시지 입니다.
윈도우가 종료될 때 전달됩니다.SIZE메시지에서는 화면의 사이즈를 계산하고, OnResize함수를 호출합니다.
DISPLAYCHANGE메시지가 전달되면 InvalidateRect함수를 호출합니다.
이 함수는 지정된 Rect영역을 갱신하는 역할로, PAINT메시지가 전송될 수 있도록 합니다.
PAINT메시지는 그리기 함수인 OnRender를 호출하여, 그리기 연산을 수행합니다.
이후 주석처리된 ValidateRect는 InvalidateRect의 반대 역할을 합니다.
이 함수는 지정 영역이 정상적으로 출력되었음을 보장하는 함수로, 이후의 PAINT메시지의 전달을 막습니다.
이번 예제에서 OnRender함수는 정적 도형을 생성하므로, 이후의 PAINT호출을 막아 불필요한 그리기 연산의 반복을 막을 수 있습니다.
3.7. 함수 구현 (WinMain, RunMessageLoop)
더보기WinMain및 RunMessageLoop는 이전 글의 구현에서 변경되는 부분이 없습니다.
이전 글에서 구현한 WinMain 함수입니다.
int WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR lpCmdLine, int nShowCmd ) { MyApp* app = new MyApp(); if (SUCCEEDED(app->initialize(hInstance))) { app->runMessageLoop(); } return 0; }
객체를 생성, 자원 초기화 후 메시지 루프를 수행합니다.
RunMessageLoop함수입니다.
void MyApp::runMessageLoop() { MSG msg; BOOL bRet; while ( (bRet = GetMessage(&msg, nullptr, 0, 0)) != 0) { if (bRet == -1) { // Error handling } else { TranslateMessage(&msg); DispatchMessage(&msg); } } }
이전 글의 RunMessageLoop함수와 동일합니다.
메시지를 해석 후 전달합니다.
4. 결과
더보기실행 시, 위와 같은 윈도우를 확인할 수 있습니다.
Direct2D를 사용해 간단한 도형을 그려봤습니다.
도형을 그리는 방법보다는, DirectX의 동작 방식에 집중해서 살펴보시는 것이 좋을 것 같습니다.
주로 사용되는 객체 (Factory, Render Target 등), 매개변수 등에 집중하시는걸 권합니다.
전체 소스 코드는 아래 Github에서 확인이 가능합니다.
이번 글이 도움이 되셨기를 바랍니다.
감사합니다.
'C++ > 미분류' 카테고리의 다른 글
[DirectX] QPC로 FPS(프레임 레이트) 구현하기 (0) 2024.02.20 [WinAPI] 로그 출력하기 (Console, TRACE) (0) 2024.02.16 [Win32] Windows application 만들기 (1) 2024.01.25 [C++] 랜덤 이벤트 (확률) 구현하기 (0) 2022.10.06 [Visual Studio] Code style 변경하기 (BSD, K&R) (0) 2022.09.14