-
[Direct2D] 움직이는 비트맵 이미지 그리기C++/미분류 2024. 2. 28. 15:03
이전 글 ([Direct2D] 비트맵 이미지 그리기)에서 윈도우에 비트맵 이미지를 그리는 방법을 알아보았습니다.
이번 글에서는 지난 글의 내용을 확장하여, 움직이는 이미지를 윈도우에 그리는 방법을 살펴보도록 하겠습니다.
이번 글은 이전 글 ([Direct2D] 비트맵 이미지 그리기)의 코드가 일부 이어집니다.
0. 개요
이번 글의 움직이는 이미지는 이전 글과 동일하게 D2D의 Bitmap 객체를 이용합니다.
프레임의 경과에 따라 출력되는 Bitmap 객체가 달라지며, 이 객체들을 gif형식의 파일을 통해 초기화하기 때문에, Bitmap 객체를 배열 등의 컨테이너를 통해 취합할 필요가 있습니다.
1. 클래스 추가 (MyBitmap)
더보기개요에서 언급한 Bitmap객체의 배열과 움직이는 이미지의 정보를 담을 객체를 추가로 만들어보도록 하겠습니다.
객체의 헤더를 다음과 같이 선언하겠습니다.
#pragma once #include <vector> #include <d2d1.h> class MyBitmap { public: void Tick(FLOAT deltaTime); ID2D1Bitmap* GetBitmap(); void Initialize(UINT frameCount, std::vector<ID2D1Bitmap*> bitmapArr); public: std::vector<ID2D1Bitmap*> bitmap; UINT frameCount = -1; FLOAT elapseTime = 0.f; };
3개의 함수와 변수를 선언했습니다.
Tick은 렌더링 함수에서 계산한 시간에 대하여, 렌더링 함수에서 호출됩니다.
GetBitmap은 현재 출력해야 하는 Bitmap객체를 반환하는 함수입니다.
Initialize는 객체의 배열과 구성된 프레임의 수를 초기화 하는 함수입니다.
bitmap은 이미지를 구성하는 Bitmap객체를 모아둔 배열입니다.
frameCount는 이미지의 총 프레임 수 입니다.
elapseTime은 Tick을 통해 경과된 시간입니다.
선언한 위 함수들을 다음과 같이 구현하겠습니다.
#include "MyBitmap.h" void MyBitmap::Tick(FLOAT deltaTime) { elapseTime += deltaTime; } ID2D1Bitmap* MyBitmap::GetBitmap() { if (bitmap.size() > 0) { return bitmap[(UINT)elapseTime%bitmap.size()]; } else { return nullptr; } } void MyBitmap::Initialize(UINT frameCount, std::vector<ID2D1Bitmap*> bitmapArr) { this->frameCount = frameCount; bitmap = bitmapArr; }
Tick함수는 경과 시간을 기록합니다.
Initialize함수는 클래스의 필드를 초기화합니다.
GetBitmap함수는 현재 경과 시간에 대응하는 이미지 프레임을 반환합니다.
위 코드는 1초에 1프레임씩 진행하는 코드입니다.
참조하는 배열의 값을 적절하게 조정하는 것으로, 1초에 1프레임 이외에 다양한 연출을 구성할 수 있습니다.
2. 함수 및 변수 추가 (MyApp)
더보기기존 MyApp에도 이전 문단에서 만든 MyBitmap클래스를 포함한, 이번 글에서 사용될 변수와 함수들을 선언하도록 하겠습니다.
#pragma once ... #include "MyBitmap.h" class MyApp { private: HRESULT LoadBitmapFromFile2( PCWSTR uri, MyBitmap* myBitmap); ... private: MyBitmap* mySequenceBitmap = nullptr; ... };
추가되는 코드만 간략하게 표현했습니다.
LoadBitmapFromFile2는 지정된 URI로부터 이미지를 읽어, 인자로 전달한 myBitmap객체를 초기화합니다.
추가된 MyBitmap변수는 읽어온 이미지를 담을 변수입니다.
3. 함수 구현 (LoadBitmapFromFile2)
더보기이 함수는 기존의 LoadBitmapFromFile함수와 거의 비슷한 동작을 수행합니다.
다른 점은 이미지가 가지고 있는 모든 프레임을 저장한다는 것과, 결과를 D2D의 Bitmap객체에 담는 것이 아닌 위 문단에서 만든 MyBitmap객체에 담는 것 입니다.
우선, 디코더 및 비트맵 프레임들을 담을 배열을 선언하겠습니다.
IWICBitmapDecoder* pDecoder = NULL; std::vector<ID2D1Bitmap*> bitmapArr;
이 함수도 WIC팩토리와 Render Target을 참조합니다.
해당 포인터들의 상태를 점검하고, nullptr일 경우 초기화하겠습니다.
if (!myWICFactory) CreateDeviceIndependentResources(); if (!myRenderTarget) CreateDeviceResources();
위에 선언한 디코더를 초기화하겠습니다.
HRESULT hr = myWICFactory->CreateDecoderFromFilename( uri, NULL, GENERIC_READ, WICDecodeMetadataCacheOnLoad, &pDecoder );
이후, 디코더를 통해 불러온 이미지의 프레임 수를 확인합니다.
UINT frameCount = -1; if (SUCCEEDED(hr)) { hr = pDecoder->GetFrameCount(&frameCount); }
GetFrameCount함수가 정상적으로 실행될 경우, frameCount지역변수는 불러온 이미지의 프레임 수로 초기화됩니다.
이제 각 프레임마다 비트맵 이미지를 초기화하여 배열에 담아야 합니다.
if (SUCCEEDED(hr)) { for (int i = 0; i < frameCount; i++) { ... } }
이하의 코드는 위와 같이 프레임에 대응하는 반복문 내부에서 실행됩니다.
우선, 디코더에서 비트맵 객체로 변환할 때 필요한 객체들을 선언합니다.
IWICFormatConverter* pConverter = NULL; IWICBitmapFrameDecode* tmpSource = NULL; ID2D1Bitmap* tmpBitmap = nullptr;
각각 컨버터, 소스, 결과를 담을 비트맵 객체입니다.
컨버터를 생성합니다.
if (SUCCEEDED(hr)) { hr = myWICFactory->CreateFormatConverter(&pConverter); }
디코더에서 한 프레임을 꺼내 소스에 저장합니다.
if (SUCCEEDED(hr)) { hr = pDecoder->GetFrame(i, &tmpSource); }
소스를 통해 컨버터를 초기화합니다.
if (SUCCEEDED(hr)) { hr = pConverter->Initialize( tmpSource, GUID_WICPixelFormat32bppPBGRA, WICBitmapDitherTypeNone, NULL, 0.f, WICBitmapPaletteTypeMedianCut ); }
컨버터를 통해 비트맵 객체로 변환합니다.
if (SUCCEEDED(hr)) { hr = myRenderTarget->CreateBitmapFromWicBitmap( pConverter, NULL, &tmpBitmap ); }
결과를 비트맵 배열에 담고, 사용된 객체들의 메모리를 해제합니다.
if (SUCCEEDED(hr)) { bitmapArr.push_back(std::move(tmpBitmap)); } SAFE_RELEASE(pConverter); SAFE_RELEASE(tmpSource);
프레임 숫자와, 비트맵 배열의 변환이 완료되었습니다.
반복문을 빠져나와서 MyBitmap객체를 초기화하겠습니다.
if (myBitmap) { myBitmap->Initialize(frameCount, bitmapArr); }
다음은 위 내용을 정리한 LoadBitmapFromFile2함수의 전문입니다.
HRESULT MyApp::LoadBitmapFromFile2(PCWSTR uri, MyBitmap* myBitmap) { IWICBitmapDecoder* pDecoder = NULL; std::vector<ID2D1Bitmap*> bitmapArr; if (!myWICFactory) CreateDeviceIndependentResources(); if (!myRenderTarget) CreateDeviceResources(); HRESULT hr = myWICFactory->CreateDecoderFromFilename( uri, NULL, GENERIC_READ, WICDecodeMetadataCacheOnLoad, &pDecoder ); UINT frameCount = -1; if (SUCCEEDED(hr)) { hr = pDecoder->GetFrameCount(&frameCount); } if (SUCCEEDED(hr)) { for (int i = 0; i < frameCount; i++) { IWICFormatConverter* pConverter = nullptr; IWICBitmapFrameDecode* tmpSource = nullptr; ID2D1Bitmap* tmpBitmap = nullptr; if (SUCCEEDED(hr)) { hr = myWICFactory->CreateFormatConverter(&pConverter); } if (SUCCEEDED(hr)) { hr = pDecoder->GetFrame(i, &tmpSource); } if (SUCCEEDED(hr)) { hr = pConverter->Initialize( tmpSource, GUID_WICPixelFormat32bppPBGRA, WICBitmapDitherTypeNone, NULL, 0.f, WICBitmapPaletteTypeMedianCut ); } if (SUCCEEDED(hr)) { hr = myRenderTarget->CreateBitmapFromWicBitmap( pConverter, NULL, &tmpBitmap ); } if (SUCCEEDED(hr)) { bitmapArr.push_back(std::move(tmpBitmap)); } SAFE_RELEASE(pConverter); SAFE_RELEASE(tmpSource); } } if (myBitmap) { myBitmap->Initialize(frameCount, bitmapArr); } SAFE_RELEASE(pDecoder); return hr; }
4. MyBitmap 초기화 및 사용
더보기선언된 MyBitmap 객체를 초기화합니다.
HRESULT MyApp::CreateDeviceIndependentResources() { ... mySequenceBitmap = new MyBitmap(); return hr; }
이후 프로그램의 Initialize시, LoadBitmapFromFile함수와 비슷한 시기에 LoadBitmapFromFile2함수를 호출하여 이미지를 로드합니다.
HRESULT MyApp::initialize(HINSTANCE hInstance) { ... LoadBitmapFromFile(L"dx_logo.png", &myBitmap); LoadBitmapFromFile2(L"loading.gif", mySequenceBitmap); return hr; }
지정하는 URI는 소스 파일의 위치를 기준으로 합니다.
상위, 혹은 하위 폴더의 파일을 지정하실 때 참고하시기 바랍니다.
5. 렌더링
더보기이제 렌더링 함수에서 렌더링합니다.
우선, 움직이는 이미지의 시간 경과를 위해 Tick을 호출합니다.
Tick의 deltaTime은 이전 글에서 구한 QPC를 이용하도록 하겠습니다.
HRESULT MyApp::OnRender() { ... if (SUCCEEDED(hr)) { ... mySequenceBitmap->Tick(deltaTime); ... } ... }
이제 Render target을 이용해 비트맵을 렌더링하겠습니다.
저희가 만든 MyBitmap클래스에 대응하는 DrawBitmap오버로딩은 없으니, 이전에 선언한 GetBitmap함수를 통해 현재 프레임에 렌더링 되어야 하는 Bitmap 객체를 참조하여 그리겠습니다.
HRESULT MyApp::OnRender() { ... if (SUCCEEDED(hr)) { ... if (mySequenceBitmap) { ID2D1Bitmap* tmp = mySequenceBitmap->GetBitmap(); if (tmp) { myRenderTarget->DrawBitmap(tmp, D2D1::RectF( 0.0f, 0.0f, rtSize.width, rtSize.height )); } } ... } ... }
함수의 오류, 이미지 파일의 부재 등으로 nullptr 참조가 되어도 문제가 발생하지 않도록, 조건문을 추가했습니다.
본문의 코드가 정상적으로 작동되었을 경우, 아래와 같이 출력됩니다.
원 형태의 Progress circle이 회전하는 것을 볼 수 있습니다.
이번 글에서 사용된 이미지 및 코드는 다음 Github에서 확인하실 수 있습니다.
이번 글이 도움이 되셨기를 바랍니다.
감사합니다.
'C++ > 미분류' 카테고리의 다른 글
[Direct2D] 비트맵 이미지 움직이기 (2) (0) 2024.03.13 [Direct2D] 비트맵 이미지 움직이기 (1) (0) 2024.03.10 [Direct2D] 비트맵 이미지 그리기 (1) 2024.02.26 [WinAPI] 키보드 입력 (WM_KEYDOWN, WM_KEYUP, GetAsyncKeyState) (0) 2024.02.23 [DirectX] QPC로 FPS(프레임 레이트) 구현하기 (0) 2024.02.20