Search

[StartD2D-6] 이동/회전 변환 이해하기

DirectX 11 2011. 9. 21. 08:00 Posted by 알 수 없는 사용자


 

행렬의 등장

기존의 윈도우 애플리케이션 개발자가 Direct2D를 접할 때,
가장 생소한 개념은 행렬( Matrix ) 일 것입니다.

즉, 아래의 API 이겠죠?

<코드>

::g_ipRT->SetTransform( ::D2D1::Matrix3x2F::Identity() );

</코드>

우리는 항상 렌더링 작업을 하기 전에, 이 작업을 해주었습니다.
이것은 과연 무슨 의미를 가지고 있는 것일까요?  

불행하게도(?) Direct2D에서 각종 변환 작업을 위해서는 행렬( Matrix )을 사용합니다.
저는 고등학교 2학년 학기 초에 배웠던 기억이 있습니다.
그 때는 주로 연립 방정식의 해를 구하기 위해서 풀이 방법을 익혔습니다.
아마 대부분 저와 비슷하실 것이라 생각이 듭니다.

왜 뜬금없이 행렬이 등장했는지를 언급하고자 하면, 상당히 고통스럽습니다.^^
저는 이 행렬에 대한 깊은 이해를 원하지 않습니다.
제가 원하는 것은 이 행렬 관련 API를 이용해서 화면 상에 나오는 오브젝트들의
이동이나 회전 등과 같은 변환을 표현 할 수 있다는 것만 아셨으면 좋겠습니다.^^


Direct2D 에서는 3ⅹ2 행렬을 사용합니다.
행렬의 각 성분들은 아래와 같습니다.  

M11

Default : 1.0

M12

Default : 0.0

M21

Default : 0.0

M22

Default : 1.0

M31

OffsetX : 0.0

M32

OffsetY : 0.0

 

이 행렬은 D2D1_MATRIX_3X2 라는 구조체로 표현됩니다.
Direct2D에서는 이 행렬을 손쉽게 다루기 위해서 Matrix3x2F 라는 유틸리티 기능을 지원합니다.
우리는 주로 이 Matrix3x2F 의 기능을 이용해서 변환 정보를 설정할 것입니다.
행렬의 M11, M12, M21, M22 성분은 회전과 확대/축소와 관련된 수치가 입력됩니다.
M31과 M32에는 이동과 관련된 수치가 입력됩니다.
행렬의 수치를 통해서 우리가 원하는 변형을 하고자 할 때,
이들 수치를 직접 입력해서 행렬을 제어하기는 무척 어렵습니다.

그래서 Direct2D 에서 제공하는 개발자 편의를 위한 여러 클래스들을 사용해서 이들 수치를 제어합니다.

 

스크린 좌표계  

변환의 이해를 위해서는 좌표계에 대한 이해가 필수적입니다.
우리가 가장 많이 사용하는 좌표계는 당연히 2차원 좌표계입니다.
바로 모니터가 2차원이기 때문입니다.
하지만 이전에도 언급드린 적이 있지만,
컴퓨터 모니터의 좌표계는 우리가 일반적으로 알고 있는
2차원 좌표계가 아닙니다.

 

좌측이 우리가 흔히 학창시절에 배우는 2차원 데카르트 좌표계( 2D Cartesian coordinate system ) 입니다.
우측이
컴퓨터 모니터가 사용하는 스크린 좌표계입니다.
이점 숙지 하시고, 다음 내용을 보시기 바랍니다.^^

 

이번에 제공되는 샘플의 결과는 다음과 같습니다.

 

이동( Translate )

먼저 살펴 볼 것은 이동( Translate ) 입니다.
가장 위에 있는 이미지가 바로 이동만 적용된 형태입니다.
샘플에서는 단순히 ( X,Y ) 축으로 +20씩만 움직였습니다

<코드>
D2D1::Matrix3x2F matTranslation; 
matTranslation = ::D2D1::Matrix3x2F::Translation( 20.0f, 20.0f );
::g_ipRT->SetTransform( matTranslation );                    
::g_ipRT->DrawBitmap( ::g_ipD2DBitmap, dxArea );    
</코드 

D2D1::Matrix3x2F::Translation() 를 통해서 우리가 원하는 이동을 수행하고 있습니다.
쉽죠? 이런 식으로 하면 행렬에 대한 깊은 이해도 필요하지 않을 것이라 생각합니다.^^  


회전( Rotate ) 

두 번째는 회전입니다.
샘플에서는 두 번째 그림과 세 번째 그림이 바로 회전 변환에 대한 결과물입니다.
사실 회전은 행렬의 성분들을 굉장히 복잡한 수치로 변경시킵니다.
왜냐하면 이들 회전 수치들은 삼각함수 값들이기 때문입니다.

하지만, 두려워하지 마시기 바랍니다.
Direct2D 에서는 이를 편리하게 수행하기 위해
::D2D1::Matrix3x2F::Rotation() 를 제공하고 있습니다.
회전 시킬 때, API 에 입력되는 값이 회전량과 회전 중점입니다.
회전량의 경우에는 라디안 값이 아닌, 각도 값이 입력됨을 주의하시기 바랍니다. 
즉, Degree 값입니다.
그리고 이 회전량의 경우 음의 값과 양의 값을 모두 입력이 가능합니다.
음의 값이 입력이 되면 시계 반대 방향으로 회전을 수행하게 됩니다.
( 양수면 시계방향으로 회전 합니다. )

 

또한 회전 중점의 경우도 유의해야 하는데, 아래의 그림을 보시기 바랍니다.

 

좌측 그림의 경우에는 물체의 중점 위치를 기준으로 회전을 수행한 것입니다.
반면에, 우측 그림의 경우에는 물체의 좌측 상단을 기준으로 회전을 수행한 것입니다.
결과물이 다르죠?
이처럼 회전 변환을 할 때는 이 두 가지를 잘 고려해서 수행해야 합니다.  

<코드>
matRot = ::D2D1::Matrix3x2F::Rotation( -30.0f,
                                       D2D1::Point2F( 120.0f, 320.0f ) );
 

::g_ipRT->SetTransform( matRot );                           
::g_ipRT->DrawBitmap( ::g_ipD2DBitmap, dxArea );    
</코드>


이동과 회전의 결합 

지금까지 이동과 회전을 각각 수행해 보았습니다.
만약에 이동 작업과 회전 작업이 모두 한 오브젝트에 필요하다면 어떻게 해야 할까요?
그런 경우가 필요하다면 바로 행렬의 곱셈 작업을 적용합니다.
그러면 이 두 가지 작업을 모두 표현 할 수 있습니다.
그런데 이 때 주의 할 부분이 있습니다.
행렬은 교환 법칙이 성립하지 않습니다.
즉 행렬 A, B 가 있을 때, A * B != B * A 라는 것입니다.

<코드>
matTranslation = ::D2D1::Matrix3x2F::Translation( 20.0f, 220.0f );         matRot = ::D2D1::Matrix3x2F::Rotation( -30.0f,
                                       D2D1::Point2F( 120.0f, 320.0f ) );
matTM = matTranslation * matRot;            
::g_ipRT->SetTransform( matTM );
                          
::g_ipRT->DrawBitmap( ::g_ipD2DBitmap, dxArea );     

 
matTranslation = ::D2D1::Matrix3x2F::Translation( 300.0f, 220.0f );         matRot = ::D2D1::Matrix3x2F::Rotation( -30.0f, 
                                       D2D1::Point2F( 400.0f, 320.0f ) );

matTM = matRot * matTranslation;                        
::g_ipRT->SetTransform( matTM );
::g_ipRT->DrawBitmap( ::g_ipD2DBitmap, dxArea ); 
</코드>

 

이 코드가 우리의 샘플에서 두 번째와 세 번째 그림의 차이를 보여주는 것입니다.
두 번째 그림은 이동을 한 후에 회전 작업을 수행한 것이고,
세 번째 그림은 회전 작업을 수행한 후에 이동 작업을 수행한 것입니다.  

실제로 우리가 수행하는 모든 변환은 좌표계 변환입니다.
위에서 우리가 했던 변환은 모두 실제로 오브젝트들의 좌표계를 변환시켜서 얻은 결과물들입니다.
이 개념들은 분명히 이해하기 어려운 부분들입니다.
굳이 이렇게 어려운 부분까지 이해하실 필요는 없습니다.
샘플을 올려드리니, 수치를 바꿔보면서 여러 결과들을 눈으로 확인해 보시기 바랍니다.^^


 

 

 

[StartD2D-4] WIC 를 이용한 이미지 작업하기

DirectX 11 2011. 7. 4. 08:30 Posted by 알 수 없는 사용자

 

  

이번 시간에는 직접 이미지를 화면에 표현하는 방법에 대해서 언급합니다.

Win32 API를 이용할 때, 우리는 '비트맵(Bitmap)' 이라는

그래픽 데이터 포맷을 읽어서 화면에 그려주었습니다.

사람마다 차이는 있겠지만, 일반적으로 다음과 같은 순서를 따라서 구성했을 것입니다.

 

  1. 비트맵을 읽기 위해서 파일을 오픈한다.

  2. 파일에서 헤더를 읽어 들인다.


  3. 비트맵 헤더의 정보를 통해서 관련 메모리를 생성하고,

    파일에서 색상 데이터에 대한 정보를 읽는다.


  4. DIBSection 을 생성하고, 실제 데이터를 읽는다.
  5. 그리고 마무리 한다.

 

이 순서는 수 많은 방법 중에 하나일 뿐이지만,

기본적으로 파일을 열어서 헤더를 먼저 읽고, 관련 메모리를 생성하고,

이후에 실제 데이터를 채우게 되는 순서는 공통된 작업입니다.

Direct2D 에서도 이와 같이 작업을 해도 되지만,

이미 편의를 위해 만들어진 라이브러리를 사용해서 조금 더 확장성 있는 작업을 할 필요가 있습니다.

지금부터는 WIC를 이용한 간단한 이미지 뷰어 작업을 해보겠습니다.

  

WIC( Windows Imaging Component )

 

DirectX 가 윈도우 운영체제 전반으로 광범위하게 활용되면서,

이들과 관련한 내용들을 분리할 필요가 있었습니다.

과거까지는 DirectX 는 게임 개발자들의 전유물에 가까웠기 때문에,

다른 개발자들도 손쉬운 개념으로 접근할 수 있는 그런 분류가 필요했습니다.


결과적으로 아래와 같이 분류가 되었습니다.  

 

WIC는 모든 이미지를 쉽게 처리할 수 있도록 만들어낸 COM 기반의 프레임워크입니다.

그림에서 보듯이 WIC도 하나의 큰 영역으로서 자리 잡고 있습니다.

( 참고로 DXVA는 영상 처리를 위한 프레임워크입니다. )

 

WIC를 이용한 이미지 처리는 앞서 GDI 기반에서 작성했던 것과는 완전히 다릅니다.

WIC는 PNG, JPG, GIF 등과 같은 거의 모든 주요한 이미지 형식을 포함하고,

기본 코덱들을 지원하고 있습니다.

 

말이 참 어렵죠?

쉽게 말해서, Direct2D 기반에서 이미지 처리를 하려면 WIC를 사용하면 쉽게 할 수 있다는 것입니다.

우리는 이것을 사용하는 순서와 방법에 대해서 배우기 위해서,

윈도우 화면에 이미지를 그려주는 간단한 애플리케이션을 만들어 볼 것입니다.

 

 

기본 WIC 프로그래밍

 

애플리케이션 마법사로 새로운 프로젝트를 만들고, stdax.h 에 다음을 추가를 합니다.

 

'WindowsCodecs.lib'와 'wincodec.h' 가 바로 WIC를 사용하기 위해 추가시킨 것입니다.

눈치 빠른 분들이라면, 이름에서 약간 앞으로의 작업 방향을 예측할 수 있을 것입니다.

 

이번 프로젝트에서 사용할 전역 변수들은 아래와 같습니다.

 

 

익숙한 개념이 눈에 보이지 않으십니까?

바로 IWICImagingFactory 입니다. 네 그렇습니다~

WIC 도 바로 팩토리 형태로 생성이 됩니다.

 

추가된 변수들은 아래와 같은 절차에 의해 값이 채워집니다.

즉, 아래는 WIC의 처리 과정입니다.

 

  1. WIC 팩토리를 만든다.

  2. 파일 경로를 기반으로 해서 디코더를 만든다.

  3. 디코딩된 프레임을 가져온다.

  4. 변환기에 넣어서 Direct2D 형식으로 변환한다.

  5. Direct2D 비트맵을 생성하고, 이를 렌더링한다.

 

이제 위의 절차를 실제로 어떻게 처리하는지를 차근차근 살펴보겠습니다.

 

가장 먼저하는 초기화 작업입니다.

앞서 언급했듯이, WIC 도 팩토리 개념으로 생성됩니다.

COM 기반이기 때문에 API 인자들이 굉장히 어려워 보일 수도 있지만, 관심을 둘 부분은 아닙니다.

위와 같은 방법으로만 하면, WIC가 생성 되어진다는 개념으로만 인식하고 다음 단계로 넘어갑니다.

 

 

 

다음 단계는 디코더를 만들고, 이를 기반으로 해서 Direct2D 형식으로 데이터를 변환하는 것입니다.

이를 위해서 가장 먼저 해야 하는 일은

이미지를 읽어들이기 위한 디코더( Decoder )를 만드는 일입니다.


갑자기 등장한 생소한 용어에 조금 혼란스러울 것 같습니다.

우리가 사용하는 모든 멀티미디어 파일( 이미지, 영상, 사운드 등 )들은

굉장히 어려운 방법으로 압축이 되어있습니다.


이들에 대한 원리나 형식을 이해하는 것도 중요한 일일 수도 있지만,

이는 간단하게 본 페이지에서 설명할 수 있는 내용이 아닙니다.

물론 저도 이와 관련한 전문가는 더더욱 아닙니다.

우리는 단지 API만으로 이들에 대한 고민을 해결할 수 있습니다.

바로 그것이 WIC의 존재 이유 중 하나 일 것입니다.^^

 

즉, 우리는 이미 만들어진 API를 이용해서 손쉽게 이미지 파일을 읽어올 수 있습니다.

그런 역할을 하는 것이 바로 디코더입니다.

아래는 디코더를 가지고 실제 작업을 하는 부분입니다.

 

 

디코더는 WIC 팩토리 멤버함수로써 생성이 되어집니다.

우리가 사용했던 이 API의 원형은 다음과 같습니다.

<코드>

HRESULT CreateDecoderFromFilename(

[in] LPCWSTR wzFilename,

[in] const GUID *pguidVendor,

[in] DWORD dwDesiredAccess,

[in] WICDecodeOptions metadataOptions,

[out, retval] IWICBitmapDecoder **ppIDecoder

);

</코드>

 

이 API는 주어진 이미지 파일을 기반으로 해서 디코더를 생성해 줍니다.

첫 번째 인자로 파일명이 들어갑니다.

이 파일을 기반으로 해서 적합한 디코더를 생성해 주게 되는 것입니다.

예를 들어, PNG 파일이면 PNG에 대한 디코더가 필요하다고 인식하고,

그에 맞는 디코더를 자동적으로 생성해 주는 것입니다.

 

두 번째 인자는 선호하는 디코더 벤더(vendor)의 GUID를 입력해야 하는데, 지금은 NULL을 사용합니다.

 

세 번째 인자로는 디코더에 대한 접근 방법을 명시합니다.

읽기(read), 쓰기(write), 혹은 둘 다 가능한지를 넣어주면,

가장 최적화된 방법과 메모리 위치를 가지는 디코드를 생성해 줍니다.

위의 예제에서는 읽기용으로만 디코더를 만들었습니다.

 

네 번째 인자는 디코더의 캐시 관련 옵션입니다.

우리가 인자로 넘긴 WICDecodeMetadataCacheOnDemand는

필요한 이미지 정보만 캐시 하도록 옵션을 준 것입니다.

다음 번에 언급할 지도 모르지만, 하나의 이미지 파일에는 여러 이미지들을 포함하고 있을 수 있습니다.

예를 들면 GIF 애니메이션 이미지 같은 것들이다.

이런 경우에 유용하게 캐시하려면, 다른 옵션을 주어야 할 것입니다.

 

마지막 인자는 생성된 디코더를 저장할 디코더의 포인터입니다.

여기까지 작업하면, 우린 이제 파일을 읽은 디코더를 소유하게 되는 것입니다.

뭔가 절차 상으로 굉장히 복잡한 것처럼 느껴지죠?

 

 

다음으로 할 작업은 프레임(frame) 작업입니다.

프레임이라는 것은 실제 픽셀 데이터를 가지고 있는 비트맵입니다.

앞서 잠깐 언급했듯이, 하나의 이미지 파일은 여러 장의 이미지가 존재할 수 있습니다.

그런 경우를 대비해서 체크를 해야겠지만,

우린 여기서 단 하나의 프레임만이 존재한다고 가정할 것입니다.

디코더의 멤버 함수인 GetFrame()를 통해서 우린 가장 첫 번째 프레임을 얻을 수 있습니다.

이 프레임을 얻는다는 것은 우리가 화면에 표현할 수 있는 이미지를 얻었다는 것입니다.

 

이제 우리는 디코더를 통해서 이미지를 Direct2D에서 표현할 수 있도록

적절하게 변환을 해주어야
합니다.

CreateFormatConverter() API는 이를 위해서 컨버터를 만들어줍니다.

그리고 이 컨버터를 우리가 원하는 형태로 초기화를 시켜 줍니다.

컨버터의 멤버함수 Initialize() 는 이미지를 컨버팅 하면서

픽셀 정보를 보정해 줄 수 있는 많은 옵션을 가지고 있습니다.

이들 옵션에 대한 세부 설정을 하지 않았습니다.

그래서 위에 인자들 형태로 주면, 별다른 이미지의 수정 없이 32비트 포맷으로 남게 됩니다.

 

이제 마지막으로 실제 렌더링 가능한 형태의 메모리를 생성해야 합니다.

렌더타겟의 멤버함수인 CreateBitmapFromWicBitmap() API를 통해서 이 작업을 하게 됩니다.

여기까지 하면, 이미지를 렌더링 하기 위한 준비작업이 모두 끝난 것입니다.

 

저는 여기에 모든 옵션들을 나열하지 않습니다.

( 기본 목적인 이미지를 띄우는데 충실하고자 합니다.^^ )

 

 

생소한 API들이 눈에 많이 띄지만, 이들은 일련의 절차에 지나지 않습니다.

중요한 개념은 이미지를 읽어 들일 디코더를 만들고,

이 이미지 데이터를 Direct2D가 표현할 수 있는 픽셀 데이터로 변환하는 것입니다.

그리고 이 데이터를 렌더타겟에서 표현할 수 있는 비트맵으로 만들어서

렌더링 가능한 상태로 만듭니다.

위의 코드는 바로 이 개념들을 표현하고 있는 것입니다.

 

그러면 실제 WM_PAINT 메시지를 통해서 이들이 어떻게 화면에 그려야 하는지 살펴보겠습니다.

 

WM_PAINT 메시지에서는 렌더타겟이 존재하지 않는 경우, 렌더타겟을 생성합니다.

렌더 타겟이 존재한다면, 비트맵을 그리고 있습니다.

렌더타겟의 렌더링 작업도 BeginDraw() / EndDraw() 의 매커니즘 내부에서

특정 상태를 기반으로 작업을 수행하게 됩니다.

우리는 Clear() 라는 API를 통해서 렌더타겟의 메모리를 흰색으로 채우고 있습니다.

그리고 현재 우리가 이미지를 (0,0) 위치에 (300,300) 크기로 렌더링 합니다.

 

마법의 함수 DrawBitmap()

 

앞선 작업을 통해서 우린 Direct2D를 이용해서 이미지를 화면에 그릴 수 있었습니다.

만약 우리가 읽어 들인 이미지의 일부분만을 화면에 그리고 싶다면 어떻게 해야 할까요?

혹은 흐릿한 효과를 주고 싶다면 어떻게 해야 할까요?

굉장히 어려운 일들 같지만, 이들 기능은 DrawBitmap() 에 모두 옵션 인자로서 존재하고 있습니다.

( 무척 고마운 일이지요..^^ )

그렇기 때문에, 우리는 이 함수를 잘 사용할 수 있어야 합니다.

API의 원형은 다음과 같습니다.

 

<코드>

virtual void DrawBitmap(

[in] ID2D1Bitmap *bitmap,

[in, optional] const D2D1_RECT_F *destinationRectangle = NULL,

         FLOAT opacity = 1.0f,

D2D1_BITMAP_INTERPOLATION_MODE interpolationMode =

D2D1_BITMAP_INTERPOLATION_MODE_LINEAR

,

[in, optional] const D2D1_RECT_F *sourceRectangle = NULL

) = 0;

</코드>

 

첫 번째 인자는 우리가 렌더링 작업을 수행할 이미지입니다.

 

두 번째 인자부터는 옵션적으로 설정할 수 있다.

두 번째 인자는 렌더링 작업을 수행할 화면의 영역을 설정합니다.

NULL 로 설정한다면, 렌더타겟의 원점에 그리게 됩니다.

만약 이미지 크기보다 크게 설정된다면, 자동적으로 이미지를 확대해서 보여주게 됩니다.

 

세 번째 인자는 투명도를 설정합니다.

범위는 0.0~1.0 사이의 값으로 0.0은 투명한 상태를 나타내고 1.0은 불투명한 상태를 나타냅니다.

 

네 번째 인자는 우리가 렌더링하는 이미지가 회전을 하거나 크기가 조정되었을 때,

어떻게 부드럽게 보일 것인가에 대한 옵션을 설정하는 부분입니다.

즉, 보간( interpolation ) 옵션입니다.

 

마지막 인자는 원본 이미지에서 일정 영역을 보여주고 싶을 때 영역을 입력하는 옵션입니다.

이 때 단위는 해당 이미지 파일의 사이즈를 기준으로 영역을 설정해 주면 됩니다.

 

그러면, 간단하게 실제로 이미지의 일부 영역을 약간 투명하게 보여지는 것을 프로그램으로 구현해자면,

앞서 작성했던, 이미지 뷰어의 기능에서 DrawBitmap()만 변경해주면 됩니다.

 

<코드>

HRESULT hr = E_FAIL;

::g_ipRT->BeginDraw();

::g_ipRT->SetTransform( ::D2D1::Matrix3x2F::Identity() );

::g_ipRT->Clear( ::D2D1::ColorF( ::D2D1::ColorF::White ) );

                            

if( ::g_ipD2DBitmap != nullptr )

{

    ::D2D1_RECT_F dxArea = ::D2D1::RectF( 0.0f, 0.0f, 500.0f, 500.0f );

    ::D2D1_RECT_F dxSrc = D2D1::RectF( 0.0f, 0.0f, 250.0f, 250.0f );

    ::g_ipRT->DrawBitmap( ::g_ipD2DBitmap, dxArea, 0.3f,

D2D1_BITMAP_INTERPOLATION_MODE_LINEAR, &dxSrc );

                

}

hr = ::g_ipRT->EndDraw();                

</코드>

 

우리는 간단하게 DrawBitmap() 의 인자들만 변경해주는 것만으로 이미지의 일부 영역만을 보여주고,

투명도를 조절할 수 있음을 확인해 보았습니다.

각각의 값을 변경시키면서, 여러가지 아이디어를 구상해 보기 바랍니다. ^^


아래 소스코드를 첨부합니다..