-
[Direct2D] Device context 사용하기 (ID2D1DeviceContext)C++/미분류 2024. 3. 29. 17:40
이전 글 까지 Direct2D의 렌더링에 Render target을 사용했습니다.
[Direct2D] 비트맵 이미지 그리기
정확히는 ID2D1HwndRenderTarget객체를 사용했습니다.
이 객체는 ID2D1RenderTarget의 하위 객체입니다.
Microsoft는 Windows8발매 이후, 새 렌더링 API를 소개했습니다.
How to render by using a Direct2D device context - Win32 apps | Microsoft Learn
제목에도 있는 Device Context가 그것으로, 이 객체도 ID2D1RenderTarget의 하위 객체입니다.
Micorosoft는 Device context를 사용하면 얻을 수 있는 이점을 다음과 같이 소개하고 있습니다.
- Windows store앱에 렌더링 할 수 있습니다.
- Render target을 렌더링 전, 중, 후 언제든지 변경할 수 있습니다.
- Swap chain을 사용하여 여러 유형의 Window를 이용할 수 있습니다.
- Direct2D Effect객체를 생성하여 이미지에 이펙트를 적용할 수 있습니다.
- 다수의 Device context를 이용함으로써, 멀티스레드 환경에서의 최적화가 용이해집니다.
- Direct3D에 더 가까워서 관련 기능을 이용하기 용이합니다.
이번 글에서는 기존 글에서 사용된 Render target을 Device context로 변경해보도록 하겠습니다.
이번 글은 상기 링크한 지난 글에서 코드가 이어집니다.
1. 살펴보기
더보기추가 / 변경되는 헤더는 다음과 같습니다.
// Before #include <d2d1.h> // After #include <d2d1_1.h> #include <d3d11.h>
Device context의 생성에는 Direct3D의 API가 필요합니다.
이번 글에서는 Direct3D 11버전을 사용하기 때문에, 위와 같이 d3d11을 추가했습니다.
라이브러리 참조 또한 수정이 필요합니다.
Property Pages -> Linker -> Input항목의 Additional Dependencies에 다음과 같이 d3d11라이브러리를 추가합니다.
추가되는 함수는 다음과 같습니다.
class MyApp { private: HRESULT CreateDeviceContext(); D2D1_SIZE_U CalculateD2DWindowSize(); ... };
CreateDeviceContext함수는 Device context를 초기화하는 함수입니다.
CalculateD2DWindowSize함수는 Window의 크기를 반환하는 함수입니다.
초기화 및 관련 기능에 기존과는 다른 방식을 사용하기 때문에 추가했습니다.
변경되는 함수는 다음과 같습니다.
class MyClass { private: HRESULT CreateDeviceResources(); HRESULT LoadBitmapFromFile( PCWSTR uri, ID2D1Bitmap** ppBitmap); HRESULT LoadBitmapFromFile2( PCWSTR uri, MyBitmap* myBitmap); HRESULT OnRender(); void OnResize(UINT width, UINT height); ... };
Render target을 이용하던 대부분의 함수에 수정이 발생합니다.
추가 / 변경되는 변수는 다음과 같습니다.
// Before class MyApp { private: ComPtr<ID2D1Factory> myDirect2dFactory; ComPtr<ID2D1HwndRenderTarget> myRenderTarget; ComPtr<IWICImagingFactory> myWICFactory; ... }; // After class MyApp { private: ComPtr<ID2D1Factory1> myDirect2dFactory; ComPtr<IWICImagingFactory2> myWICFactory; ComPtr<ID2D1Device> myDirect2dDevice; ComPtr<ID2D1DeviceContext> myDirect2dContext; ComPtr<IDXGISwapChain> mySwapChain; ... };
Factory는 각각 관련 기능들을 사용하기 위해 이후 버전으로 교체합니다.
HwndRenderTarget이 수행하던 작업은 Device context가 수행하게 됩니다.
그 외에 추가되는 Device와 Swap chain은 초기화 및 렌더링에 사용되는 객체입니다.
다음 문단부터, 수정이 발생하는 함수들을 자세히 살펴보도록 하겠습니다.
2. 추가 : CalculateD2DWindowSize
더보기CalculateD2DWindowSize함수는 현재 Window의 크기를 반환하는 함수입니다.
구현은 다음과 같이 되어있습니다.
D2D1_SIZE_U MyApp::CalculateD2DWindowSize() { RECT rc; GetClientRect(myHwnd, &rc); D2D1_SIZE_U d2dWindowSize = { 0 }; d2dWindowSize.width = rc.right; d2dWindowSize.height = rc.bottom; return d2dWindowSize; }
Window크기가 필요한 함수에서 호출되어 사용될 수 있습니다.
3. 추가 : CreateDeviceContext
더보기CreateDeviceContext함수는 Device context및 관련 객체를 초기화 하는 함수입니다.
관련 객체의 초기화에는 장치 독립적 자원인 Factory객체가 필요하므로, CreateDeviceIndependentResources함수의 호출이 선행되어야 합니다.
또한, 해당 객체들은 장치 의존적이므로, CreateDeviceResources함수에 의해 호출되어 초기화됩니다.
CreateDeviceContext는 이하의 로직을 수행합니다.
- Swap chain과 D3D device를 생성합니다.
- D3D device를 통해 DXGI device를 생성합니다.
- D2D factory와 DXGI device를 통해 D2D device를 생성합니다.
- D2D device를 통해 D2D device context를 생성합니다.
위 과정을 코드로 살펴보도록 하겠습니다.
Swap chain과 D3D device를 생성하는 것은 D3D11CreateDeviceAndSwapChain함수로 동시에 이뤄집니다.
D2D1_SIZE_U size = CalculateD2DWindowSize(); UINT createDeviceFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; D3D_DRIVER_TYPE driverTypes[] = { D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP, }; UINT countOfDriverTypes = ARRAYSIZE(driverTypes); DXGI_SWAP_CHAIN_DESC swapDescription; ZeroMemory(&swapDescription, sizeof(swapDescription)); swapDescription.BufferDesc.Width = size.width; swapDescription.BufferDesc.Height = size.height; swapDescription.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; swapDescription.BufferDesc.RefreshRate.Numerator = 60; swapDescription.BufferDesc.RefreshRate.Denominator = 1; swapDescription.SampleDesc.Count = 1; swapDescription.SampleDesc.Quality = 0; swapDescription.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; swapDescription.BufferCount = 1; swapDescription.OutputWindow = myHwnd; swapDescription.Windowed = TRUE; ComPtr<ID3D11Device> d3dDevice; for (UINT driverTypeIndex = 0; driverTypeIndex < countOfDriverTypes; driverTypeIndex++) { hr = D3D11CreateDeviceAndSwapChain( nullptr, driverTypes[driverTypeIndex], nullptr, createDeviceFlags, nullptr, 0, D3D11_SDK_VERSION, &swapDescription, &mySwapChain, &d3dDevice, nullptr, nullptr ); if (SUCCEEDED(hr)) { break; } }
코드는 크게 D3D11CreateDeviceAndSwapChain함수의 호출에 필요한 값들을 초기화 하는 부분과
D3D11CreateDeviceAndSwapChain함수를 호출하는 부분으로 나누어집니다.
특이 사항으로, driverTypes배열의 값 만큼 반복문으로 함수를 호출하는 것을 볼 수 있습니다.
해당 배열에 들어가는 D3D_DRIVER_TYPE값은 아래 MSDN문서에서 확인할 수 있습니다.
D3D_DRIVER_TYPE(d3dcommon.h) - Win32 apps | Microsoft Learn
다음은 DXGI device를 초기화합니다.
ComPtr<IDXGIDevice> dxgiDevice; if (SUCCEEDED(hr)) { hr = d3dDevice->QueryInterface(IID_PPV_ARGS(&dxgiDevice)); }
상기 초기화한 D3D device를 통해 DXGI device를 초기화합니다.
이후 D2D device와 D2D device context를 초기화합니다.
if (SUCCEEDED(hr)) { hr = myDirect2dFactory->CreateDevice( dxgiDevice.Get(), &myDirect2dDevice ); } if (SUCCEEDED(hr)) { hr = myDirect2dDevice->CreateDeviceContext( D2D1_DEVICE_CONTEXT_OPTIONS_NONE, &myDirect2dContext ); }
D3D device와 DXGI device는 D2D device와 device context를 초기화하기 위해 생성한 객체이므로, 지역변수로 선언했습니다.
스마트포인터를 사용했으므로, 메모리를 초기화 하는 과정은 필요하지 않습니다.
문단 3에서 작성한 CreateDeviceContext함수의 전체 코드는 다음과 같습니다.
더보기HRESULT MyApp::CreateDeviceContext() { HRESULT hr = S_OK; D2D1_SIZE_U size = CalculateD2DWindowSize(); UINT createDeviceFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; D3D_DRIVER_TYPE driverTypes[] = { D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP, }; UINT countOfDriverTypes = ARRAYSIZE(driverTypes); DXGI_SWAP_CHAIN_DESC swapDescription; ZeroMemory(&swapDescription, sizeof(swapDescription)); swapDescription.BufferDesc.Width = size.width; swapDescription.BufferDesc.Height = size.height; swapDescription.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; swapDescription.BufferDesc.RefreshRate.Numerator = 60; swapDescription.BufferDesc.RefreshRate.Denominator = 1; swapDescription.SampleDesc.Count = 1; swapDescription.SampleDesc.Quality = 0; swapDescription.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; swapDescription.BufferCount = 1; swapDescription.OutputWindow = myHwnd; swapDescription.Windowed = TRUE; ComPtr<ID3D11Device> d3dDevice; for (UINT driverTypeIndex = 0; driverTypeIndex < countOfDriverTypes; driverTypeIndex++) { hr = D3D11CreateDeviceAndSwapChain( nullptr, driverTypes[driverTypeIndex], nullptr, createDeviceFlags, nullptr, 0, D3D11_SDK_VERSION, &swapDescription, &mySwapChain, &d3dDevice, nullptr, nullptr ); if (SUCCEEDED(hr)) { break; } } ComPtr<IDXGIDevice> dxgiDevice; if (SUCCEEDED(hr)) { hr = d3dDevice->QueryInterface(IID_PPV_ARGS(&dxgiDevice)); } if (SUCCEEDED(hr)) { hr = myDirect2dFactory->CreateDevice( dxgiDevice.Get(), &myDirect2dDevice ); } if (SUCCEEDED(hr)) { hr = myDirect2dDevice->CreateDeviceContext( D2D1_DEVICE_CONTEXT_OPTIONS_NONE, &myDirect2dContext ); } return hr; }
4. 수정 : CreateDeviceResources
더보기CreateDeviceResources함수는 장치 의존적 자원인 Render target과 브러시 객체를 초기화하는 역할을 수행합니다.
이번 글에서 Render target은 Device context로 바뀌었고, 초기화는 새로 만드는 함수인 CreateDeviceContext함수에서 수행합니다.
기존 함수에서는 초기화된 Device context의 렌더링 타겟을 지정하고, 브러시 객체를 생성하는 역할을 수행합니다.
Device context를 초기화하고, 렌더링 타겟을 지정하는 코드는 다음과 같습니다.
hr = CreateDeviceContext(); ComPtr<IDXGISurface> surface = nullptr; if (SUCCEEDED(hr)) { hr = mySwapChain->GetBuffer( 0, IID_PPV_ARGS(&surface) ); }
Device context를 초기화하고, Swap chain의 백 버퍼를 가져옵니다.
ComPtr<ID2D1Bitmap1> bitmap = nullptr; if (SUCCEEDED(hr)) { FLOAT dpiX, dpiY; dpiX = (FLOAT)GetDpiForWindow(GetDesktopWindow()); dpiY = dpiX; D2D1_BITMAP_PROPERTIES1 properties = D2D1::BitmapProperties1( D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW, D2D1::PixelFormat( DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_IGNORE ), dpiX, dpiY ); hr = myDirect2dContext->CreateBitmapFromDxgiSurface( surface.Get(), &properties, &bitmap ); } if (SUCCEEDED(hr)) { myDirect2dContext->SetTarget(bitmap.Get()); }
다음 코드는 가져온 백 버퍼로부터 Bitmap을 생성하고, Device context의 타겟으로 지정하는 코드입니다.
여기서 비트맵이란 비트맵 이미지가 아니라, 화면 크기와 동일한 출력 비트맵입니다.
브러시를 생성하는 코드는 기존의 코드와 동일합니다.
다만 호출 대상을 Render target이 아닌 Device context로 변경해야 합니다.
if (SUCCEEDED(hr)) { hr = myDirect2dContext->CreateSolidColorBrush( D2D1::ColorF(D2D1::ColorF::LightSlateGray), &myLightSlateGrayBrush ); } if (SUCCEEDED(hr)) { hr = myDirect2dContext->CreateSolidColorBrush( D2D1::ColorF(D2D1::ColorF::CornflowerBlue), &myCornflowerBlueBrush ); }
5. 수정 : LoadBitmapFromFile
더보기LoadBitmapFromFile, LoadBitmapFromFile2함수는 URI로부터 이미지를 불러오는 함수입니다.
해당 함수들의 기능적 변경은 없지만, Render target의 함수들을 Device context로 변경하는 과정에서 일부 코드의 수정이 발생합니다.
함수 실행 전, Redner target의 유효성을 검사하는 코드는 Device context의 유효성을 검사하는 코드로 변경됩니다.
// Before if (!myRenderTarget) CreateDeviceResources(); // After if (!myDirect2dContext) CreateDeviceContext();
WIC비트맵에서 D2D비트맵으로 변환하는 함수를 Render target이 아닌 Device context에서 호출합니다.
// Before if (SUCCEEDED(hr)) { hr = myRenderTarget->CreateBitmapFromWicBitmap( pConverter.Get(), NULL, ppBitmap ); } // After if (SUCCEEDED(hr)) { hr = myDirect2dContext->CreateBitmapFromWicBitmap( pConverter.Get(), NULL, ppBitmap ); }
해당 함수는 상위 클래스인 ID2DRenderTarget의 함수이므로, 파라미터의 변경은 없습니다.
6. 수정 : OnRender
더보기OnRender함수는 호출 시간 단위 Tick을 계산하고, 해당 Tick에 대해 이미지의 위치와 프레임을 수정 및 그리는 함수입니다.
해당 함수 내부의 그리기 연산은 대부분 Render target에서 이루어졌기 때문에 Device context로의 수정이 필요합니다.
또한, Device context는 Swap chain을 이용하므로 관련 기능의 호출이 필요합니다.
Render target에 의해 호출된 함수들은 Device context에 의해 호출되도록 수정됩니다.
예시는 다음과 같습니다.
// Before myRenderTarget->BeginDraw(); ... hr = myRenderTarget->EndDraw(); // After myDirect2dContext->BeginDraw(); ... hr = myDirect2dContext->EndDraw();
BeginDraw, EndDraw뿐만 아니라 Render target에 의해 호출되는 모든 함수를 Device context로 변경합니다.'
또한, Swap chain으로 버퍼를 교환하는 과정이 EndDraw이후에 이루어져야 합니다.
if (SUCCEEDED(hr)) { hr = mySwapChain->Present(0, 0); }
EndDraw가 정상적으로 처리되었을 경우, Swap chain을 통해 버퍼를 교환하여 다음 프레임을 출력합니다.
7. 수정 : OnResize
더보기OnResize함수는 Window의 크기가 변경되었을 때 호출되어, Render target의 크기를 조정하는 함수를 호출합니다.
기존 코드는 다음과 같습니다.
void MyApp::OnResize(UINT width, UINT height) { if (myRenderTarget) { myRenderTarget->Resize(D2D1::SizeU(width, height)); } }
수정되는 OnResize함수는 CreateDeviceResources에서 수행한, 비트맵을 생성하여 타겟을 지정하는 로직을 그대로 수행합니다.
코드를 나누어서 살펴보도록 하겠습니다.
myDirect2dContext->SetTarget(nullptr); if (SUCCEEDED(hr)) { hr = mySwapChain->ResizeBuffers( 0, width, height, DXGI_FORMAT_B8G8R8A8_UNORM, 0 ); }
우선, 기존 Device context의 타겟을 제거합니다.
이후 Swap chain의 버퍼 사이즈를 새 Window 사이즈로 변경합니다.
이후 코드는 CreateDeviceResources함수의 로직과 동일합니다.
ComPtr<ID2D1Bitmap1> bitmap = nullptr; if (SUCCEEDED(hr)) { FLOAT dpiX, dpiY; dpiX = (FLOAT)GetDpiForWindow(GetDesktopWindow()); dpiY = dpiX; D2D1_BITMAP_PROPERTIES1 properties = D2D1::BitmapProperties1( D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW, D2D1::PixelFormat( DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_IGNORE ), dpiX, dpiY ); hr = myDirect2dContext->CreateBitmapFromDxgiSurface( surface.Get(), &properties, &bitmap ); } if (SUCCEEDED(hr)) { myDirect2dContext->SetTarget(bitmap.Get()); }
크기가 미리 변경된 Swap chain의 백 버퍼를 통해, 해당 크기와 동일한 비트맵을 생성합니다.
위 과정이 성공적으로 마무리되었을 경우 Device context의 타겟을 생성한 비트맵으로 지정합니다.
이번 글의 실행 결과는 이전 글과 동일합니다.
이번 글은 Render target을 Device context로 변경함으로써, 이후 글에서 다루어질 여러 기능들을 사용할 수 있게 됨에 의의가 있습니다.
이번 글의 전체 코드는 다음 Github에서 확인하실 수 있습니다.
GitHub - ruru14/WinapiStudy: WinAPI및 DirectX를 공부하며, 간단한 운동 법칙을 적용해봅니다.
WinAPI및 DirectX를 공부하며, 간단한 운동 법칙을 적용해봅니다. Contribute to ruru14/WinapiStudy development by creating an account on GitHub.
github.com
이번 글이 도움이 되셨기를 바랍니다.
감사합니다.
'C++ > 미분류' 카테고리의 다른 글
[Direct2D] ID2D1Effect : Affine transformation (0) 2024.04.09 [Direct2D] ID2D1Effect 사용하기 (0) 2024.04.05 [DirectX] 스마트 포인터 ComPtr (0) 2024.03.21 [Direct2D] 비트맵 이미지 움직이기 (2) (0) 2024.03.13 [Direct2D] 비트맵 이미지 움직이기 (1) (0) 2024.03.10