Deeper Learning

[WinAPI] 방향에 따른 비트맵 회전 구현 (투사체 텍스쳐 회전) 본문

Game Development/WinAPI

[WinAPI] 방향에 따른 비트맵 회전 구현 (투사체 텍스쳐 회전)

Dlaiml 2024. 6. 14. 19:47

무료 스프라이트로 게임을 제작하다 보니까 8방향 애셋을 구하기가 쉽지 않다. 플레이어가 발사하는 투사체의 경우 만약 텍스쳐의 방향이 정해져 있다면 방향벡터에 따라 비트맵이 회전해야 자연스럽게 게임에서 표현이 가능하다.

Arrow bmp

예시로 사용할 텍스쳐는 위와 같이 방향이 정해진 화살 스프라이트이다. 플레이어는 마우스 커서 방향으로 화살을 발사하기 때문에 투사체가 향하는 방향벡터를 기준으로 텍스쳐가 회전해야 화살촉 방향으로 화살이 날아가도록 세팅할 수 있다.

 

탄환 클래스인 Bullet과 다르게 방향성을 고려한 회전 렌더링 처리가 필요하고 애니메이션 또한 추가로 설정이 필요하고 후에 하위 클래스(속성을 가진 화살, 적을 추적하는 화살)를 만들 수 있도록 추가로 Arrow 클래스를 만들었다.   

 

class CArrow :
    public CBullet
{
private:
    CTexture* m_pTex;
    virtual void Render(HDC _hdc);
 


public:
    CArrow();
    ~CArrow();
};

 

렌더링 함수를 오버라이드하여 회전 로직의 구현이 필요하기 때문에 렌더링 함수만 살펴보려한다.

 

우선 추가로 구현이 필요한 기능은 현재 Arrow 오브젝트가 향하는 방향에 따라 텍스쳐의 좌측(화살촉)이 해당 방향을 향하도록 텍스쳐를 회전시키기, 회전시키면서 바뀌는 비트맵 크기, 좌표 관리하기 두 가지로 정리할 수 있다.

 

비트맵의 회전은 wingid의 PlgBlt 함수를 사용하여 해결할 수 있다.

 

PlgBlt 함수(wingdi.h) - Win32 apps

PlgBlt 함수는 원본 디바이스 컨텍스트의 지정된 사각형에서 대상 디바이스 컨텍스트의 지정된 병렬로 색 데이터 비트를 비트 블록 전송합니다.

learn.microsoft.com

BOOL PlgBlt(
  [in] HDC         hdcDest,
  [in] const POINT *lpPoint,
  [in] HDC         hdcSrc,
  [in] int         xSrc,
  [in] int         ySrc,
  [in] int         width,
  [in] int         height,
  [in] HBITMAP     hbmMask,
  [in] int         xMask,
  [in] int         yMask
);

 

 

함수의 시그니쳐는 위와 같은데 lpPoint에는 왼쪽 위, 오른쪽 위, 왼쪽 아래 모서리의 좌표가 들어가야 한다. (나머지 매개변수는 위 MS 문서 참고)

 

우선 삼각함수를 활용하기 위한 각도를 구하기 위해 ATAN2 함수를 사용하였다.

ATAN2 함수는 지정한 좌표의 arctan 값을 반환하는 함수로 원점에서 좌표까지의 직선과 X축이 이루는 각도를 Radian으로  반환한다.

추가로 화살 텍스쳐가 좌측을 바라보고 있기 때문에 180도를 추가로 돌려야 하기 때문에 라디안에 PI를 더해준다.

double angle = atan2(GetDirection().y, GetDirection().x);
angle += PI;

 

방향벡터를 (1,1)이라고 하면 X축과의 각도는 45도로 라디안으로 변환하면 45 * pi / 280 = 0.7854.. 의 값이 angle이 된다.

여기에 180도 회전을 위해 PI를 더해주었다.

 

코사인 값과 사인값은 45도에서 1/(2^(1/2))(=0.7071...)로 동일하다.

double cosA = cos(angle);
double sinA = sin(angle);

 

이제 코사인, 사인 값을 가지고 사각형의 네 모서리를 회전시킨 좌표를 구해보자.

 

 

사각형의 무게중심이 (0,0)이 되도록 유지하면서 45도 회전시킨 그림을 그려보았다.

빨간색으로 표시된 각도는 회전시킨 각도 θ로 모두 동일한 각이다. (예시가 45도라서 반대편 각도 45도 지만 혼란을 피하기 위해 표시하지 않았다)

사각형에서 선분의 중점에서 수선의 발을 내리면 중심을 지나는 성질을 지니고 있다. 그리고 중점이 원점이 되도록 조정한 이미지로 가정하였기 때문에 위 그림에서 초록색, 보라색 선분은 각각 width, height의 절반과 같다.

 

하늘색 선분의 길이는 w/2 * Cos(θ)로 구할 수 있으며 빨간색 선분의 길이는 h/2 * Sin(θ)로 구할 수 있다. 

나머지 꼭짓점도 마찬가지로 간단한 삼각함수를 활용해서 구할 수 있으며 코드는 아래와 같다. (음수, 양수 처리 주의)

// 좌상단
vertices[0].x = (LONG)((-halfWidth * cosA + (-halfHeight) * sinA));
vertices[0].y = (LONG)((-halfWidth * sinA - (-halfHeight) * cosA));

// 우상단
vertices[1].x = (LONG)((halfWidth * cosA + (-halfHeight) * sinA));
vertices[1].y = (LONG)((halfWidth * sinA - (-halfHeight) * cosA));

// 좌하단
vertices[2].x = (LONG)((-halfWidth * cosA + (halfHeight)*sinA));
vertices[2].y = (LONG)((-halfWidth * sinA - (halfHeight)*cosA));

// 우하단
vertices[3].x = (LONG)((halfWidth * cosA + (halfHeight)*sinA));
vertices[3].y = (LONG)((halfWidth * sinA - (halfHeight) * cosA));

 

이제 이를 렌더링 하기 위해 중간 Bitmap을 만들고 영역을 지정한 후 메인 Bitmap(더블 버퍼링의 백 버퍼)에 이를 옮기려 한다.

PlgBlt로 원본 텍스처 비트맵을 회전시키고 메모리 비트맵에 복사하고 TransparentBlt를 사용하여 투명화할 값을 설정하여 메인 비트맵에 렌더링 하는 코드는 아래와 같다.

 

PlgBlt(
    memDC, // 중간 저장 Device Conetext
    verticesSubset, // 좌상단, 우상단, 좌하단의 값만 매개변수로 전달
    m_pTex->GetDC(), // 원본 Device Context
    0, 0,
    bm.bmWidth, //  원본 비트맵 W
    bm.bmHeight, // 원본 비트맵 H
    NULL, 0, 0 
);

TransparentBlt(
    _hdc, // 메인 Device Conetext
    (int)(vRenderPos.x - vScale.x / 2.f), // 렌더링 위치
    (int)(vRenderPos.y - vScale.y / 2.f),
    (int)vScale.x, // 렌더링 크기
    (int)vScale.y,
    memDC,
    minX, minY, // 회전 이후 영역 좌상단
    maxX - minX, // 회전 이후 영역 W
    maxY - minY, // 회전 이후 영역 H
    RGB(255, 255, 255) // 흰색 영역 투명화
);

 

아래 코드는 아직 정리하지 않아서 조금 지저분하다. 중간에 디버깅을 위해 비트맵의 최소 X, Y 좌표를 0 이상으로 맞추는 부분이 포함되어 있는 코드이다.

아래 그림에서 회색 프레임의 좌표를 사용하여 해당 영역을 중간 비트맵에서 메인 비트맵으로 복사하는 로직으로 구현하였다.

POINT vertices[4];
double cosA = cos(angle);
double sinA = sin(angle);

int halfWidth = bm.bmWidth / 2;
int halfHeight = bm.bmHeight / 2;

// 꼭짓점 좌표 구하기
vertices[0].x = (LONG)((-halfWidth * cosA + (-halfHeight) * sinA));
vertices[0].y = (LONG)((-halfWidth * sinA - (-halfHeight) * cosA));

vertices[1].x = (LONG)((halfWidth * cosA + (-halfHeight) * sinA));
vertices[1].y = (LONG)((halfWidth * sinA - (-halfHeight) * cosA));

vertices[2].x = (LONG)((-halfWidth * cosA + (halfHeight)*sinA));
vertices[2].y = (LONG)((-halfWidth * sinA - (halfHeight)*cosA));

vertices[3].x = (LONG)((halfWidth * cosA + (halfHeight)*sinA));
vertices[3].y = (LONG)((halfWidth * sinA - (halfHeight) * cosA));


// 회색 프레임 영역 좌표 구하기 
long maxX = vertices[0].x;
long maxY = vertices[0].y;
long minX = vertices[0].x;
long minY = vertices[0].y;

for (int i = 0; i < 4 ; i++)
{
    if (vertices[i].x > maxX)
        maxX = vertices[i].x;
    if (vertices[i].y > maxY)
        maxY = vertices[i].y;
    if (vertices[i].x < minX)
        minX = vertices[i].x;
    if (vertices[i].y < minY)
        minY = vertices[i].y;
}

// 중간 DC, 비트맵 설정
HDC memDC = CreateCompatibleDC(m_pTex->GetDC());
HBITMAP hBmap = CreateCompatibleBitmap(m_pTex->GetDC(), maxX - minX, maxY - minY);
HBITMAP oldBitmap = (HBITMAP)SelectObject(memDC, hBmap);


// 흰색으로 채운 비트맵에 그리도록 하여 TransparentBlt에서 처리
RECT rect = { 0, 0, maxX-minX, maxY-minY };
HBRUSH brush = (HBRUSH)GetStockObject(WHITE_BRUSH);
FillRect(memDC, &rect, brush);

// PlgBlt에 매개변수로 넘겨줄 서브셋
POINT verticesSubset[3] = { vertices[0], vertices[1], vertices[2] };

for (int i = 0; i < 4; ++i) {
    if (minX < 0)
    {
        vertices[i].x -= minX;
        if (vertices[i].x > maxX)
        {
            maxX = vertices[i].x;
        }
    }
    if (minY < 0)
    {
        vertices[i].y -= minY;
        if (vertices[i].y > maxY)
        {
            maxY = vertices[i].y;
        }
    }


    if (i != 3)
    {
        verticesSubset[i] = vertices[i];
    }
}

if (minX < 0)
    minX = 0;
if (minY < 0)
    minY = 0;

 

방향에 따라 회전하는 텍스쳐 구현에 성공하였다.

(텍스쳐가 작아 잘 보이지 않아 앞부분을 검은색으로 임시로 칠한 테스트 파일을 만들었다)

 

Comments