Direct2D에 변환 적용
Direct2D를 사용하여 그리기에서 ID2D1RenderTarget::FillEllipse 메서드가 x축 및 y축에 정렬된 타원을 그리는 것을 보았습니다. 그러나 어떤 각도로 기울어진 타원을 그리려고 한다고 가정해 보겠습니다.
변환을 사용하면 다음과 같은 방법으로 셰이프를 변경할 수 있습니다.
- 점 주위를 회전합니다.
- 배율 조정
- 번역(X 또는 Y 방향의 변위).
- 기울이기(전단이라고도 함).
변환은 점 집합을 새 점 집합에 매핑하는 수학 연산입니다. 예를 들어 다음 다이어그램은 지점 P3 둘레를 회전하는 삼각형을 보여줍니다. 회전이 적용된 후 지점 P1이 P1'에 매핑되고, P2 지점이 P2'에 매핑되고, 지점 P3이 그 자신에게 매핑됩니다.
변환은 행렬을 사용하여 구현됩니다. 그러나 행렬을 사용하기 위해 행렬의 수학을 이해할 필요는 없습니다. 이 수학에 대해 자세히 알아보려면 부록: 행렬 변환을 참조하세요.
Direct2D에서 변환을 적용하려면 ID2D1RenderTarget::SetTransform 메서드를 호출합니다. 이 메서드는 변환을 정의하는 D2D1_MATRIX_3X2_F 구조체를 사용합니다. D2D1::Matrix3x2F 클래스에서 메서드를 호출하여 이 구조체를 초기화할 수 있습니다. 이 클래스에는 각 종류의 변환을 위한 행렬을 반환하는 정적 메서드가 포함되어 있습니다.
예를 들어 다음 코드는 점(100, 100)을 중심으로 20도 회전을 적용합니다.
pRenderTarget->SetTransform(
D2D1::Matrix3x2F::Rotation(20, D2D1::Point2F(100,100)));
SetTransform을 다시 호출할 때까지 이후의 모든 그리기 작업에 이 변환이 적용됩니다. 현재 변환을 제거하려면 ID 행렬을 사용하여 SetTransform을 호출합니다. ID 행렬을 만들려면 Matrix3x2F::Identity 함수를 호출합니다.
pRenderTarget->SetTransform(D2D1::Matrix3x2F::Identity());
시계 바늘 그리기
원 프로그램을 아날로그 시계로 바꾸어 변환을 사용해 보겠습니다. 시계 바늘을 위한 선을 추가하여 이 작업을 수행할 수 있습니다.
선의 좌표를 계산하는 대신 각도를 계산한 다음 회전 변환을 적용할 수 있습니다. 다음 코드는 시계 바늘 하나를 그리는 함수를 보여줍니다. fAngle 매개 변수는 바늘의 각도(도)를 제공합니다.
void Scene::DrawClockHand(float fHandLength, float fAngle, float fStrokeWidth)
{
m_pRenderTarget->SetTransform(
D2D1::Matrix3x2F::Rotation(fAngle, m_ellipse.point)
);
// endPoint defines one end of the hand.
D2D_POINT_2F endPoint = D2D1::Point2F(
m_ellipse.point.x,
m_ellipse.point.y - (m_ellipse.radiusY * fHandLength)
);
// Draw a line from the center of the ellipse to endPoint.
m_pRenderTarget->DrawLine(
m_ellipse.point, endPoint, m_pStroke, fStrokeWidth);
}
이 코드는 시계판의 중심에서 시작하여 endPoint 지점에서 끝나는 세로 선을 그립니다. 선은 회전 변환을 적용하여 타원의 중심 주변을 회전됩니다. 회전의 중심점은 시계판을 형성하는 타원의 중심입니다.
다음 코드는 전체 시계판이 그려지는 방법을 보여 줍니다.
void Scene::RenderScene()
{
m_pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::SkyBlue));
m_pRenderTarget->FillEllipse(m_ellipse, m_pFill);
m_pRenderTarget->DrawEllipse(m_ellipse, m_pStroke);
// Draw hands
SYSTEMTIME time;
GetLocalTime(&time);
// 60 minutes = 30 degrees, 1 minute = 0.5 degree
const float fHourAngle = (360.0f / 12) * (time.wHour) + (time.wMinute * 0.5f);
const float fMinuteAngle =(360.0f / 60) * (time.wMinute);
DrawClockHand(0.6f, fHourAngle, 6);
DrawClockHand(0.85f, fMinuteAngle, 4);
// Restore the identity transformation.
m_pRenderTarget->SetTransform( D2D1::Matrix3x2F::Identity() );
}
Direct2D 시계 샘플에서 전체 Visual Studio 프로젝트를 다운로드할 수 있습니다. (그냥 재미를 위해, 다운로드 버전은 시계판에 방사형 그레이디언트를 추가합니다.)
변환 결합
두 개 이상의 행렬을 곱하여 네 가지 기본 변환을 결합할 수 있습니다. 예를 들어 다음 코드는 회전을 변환과 결합합니다.
const D2D1::Matrix3x2F rot = D2D1::Matrix3x2F::Rotation(20);
const D2D1::Matrix3x2F trans = D2D1::Matrix3x2F::Translation(40, 10);
pRenderTarget->SetTransform(rot * trans);
Matrix3x2F 클래스는 행렬 곱셈을 위한 operator*()를 제공합니다. 행렬을 곱하는 순서가 중요합니다. 변환 설정(M × N)은 "먼저 M을 적용한 다음 N을 적용"을 의미합니다. 예를 들어 다음은 회전 후에 번역이 이어지는 모습을 보여줍니다.
이 변환을 위한 코드는 다음과 같습니다.
const D2D1::Matrix3x2F rot = D2D1::Matrix3x2F::Rotation(45, center);
const D2D1::Matrix3x2F trans = D2D1::Matrix3x2F::Translation(x, 0);
pRenderTarget->SetTransform(rot * trans);
이제 해당 변환을 역순인 번역 후에 회전이 이어지는 변환과 비교합니다.
회전은 원래 사각형의 중심 주변에서 수행됩니다. 이 변환을 위한 코드는 다음과 같습니다.
D2D1::Matrix3x2F rot = D2D1::Matrix3x2F::Rotation(45, center);
D2D1::Matrix3x2F trans = D2D1::Matrix3x2F::Translation(x, 0);
pRenderTarget->SetTransform(trans * rot);
보시는 것처럼 행렬은 동일하지만 작업 순서가 변경되었습니다. 이는 행렬 곱셈이 가환적이지 않기 때문에 발생합니다(M × N ≠ N × M).