(2022) 공부 (Study)/게임 개발 (Development)

[Win32API] Rendering과 Double Buffering

수낭 2022. 4. 13. 18:54

화면을 흰 색으로 clear 시키려면, 멤버 변수로 갖고 있던 m_ptResolution.x / m_ptResolution.y 값을 쓰면 되지만,
Rectangle() 함수의 경우, 테두리 1px을 검은색으로 그리게 되므로, 이 1px을 무시하고 그리도록 해야 한다.

 

// 화면 Clear
Rectangle(m_hDC, -1, -1, m_ptResolution.x + 1, m_ptResolution.y + 1);

Rectangle과 같은 도형 그리기 함수들은, 도형이 전체적으로 다 그려진 채로 렌더링 되지만,
여러 개의 도형들을 그리는 경우, 따로 따로 그려지게 되고, 이 과정에서 깜빡임 현상이 심해지게 된다.

이에 Double Buffering 기법을 이용하여 해결해보도록 한다.

 

메인 HDC인 m_hDC에 이어서 또 하나의 HDC인 m_memDC를 만들도록 한다.
그리고 m_hDC가 화면에 보여지는 동안에, m_memDC로 뒤에서 미리 그린 뒤에,
다음 프레임이 되면 m_memDC의 내용을 m_hDC에 그대로 옮겨서 모든 내용을 한번에 rendering 하는 것이다.
(모든 오브젝트가 다 그려진 뒤에, 통째로 HDC 객체가 옮겨지므로, 과정을 보이지 않으므로 깜빡임 현상이 사라진다.)

이제 구현해보도록 하자

 

// Double Buffering
HBITMAP		m_hBit;
HDC 		m_memDC;

CCore 객체에서, 위 두 개의 객체가 추가로 필요하다.

 

int CCore::init(HWND _hWnd, POINT _ptResolution)
{
    m_ptResolution = _ptResolution;

    RECT rt = { 0, 0, _ptResolution.x, _ptResolution.y };
    AdjustWindowRect(&rt, WS_OVERLAPPEDWINDOW, true);
    SetWindowPos(_hWnd, nullptr, 100, 100, rt.right - rt.left, rt.bottom - rt.top, 0);
    ...
}

우리가 1280*768로 해상도를 정해서 AdjustWindowRect()를 하고, SetWindowPos()도 해줬었다.
이 때, 총 Pixel의 수는 1280 * 768 = 983,040개가 되고, 이러한 Pixel 데이터들을 다 묶어서 Bitmap 이라고 한다.

 

m_hDC = GetDC(m_hWnd);

 

모든 Window는 Bitmap을 가지고 있는 것이고, 우리는 결과적으로 이 Bitmap이라는 메모리에 그림을 그리는 것이다.
위 코드는, m_hWnd 핸들이 가리키는 Window가 가진 Bitmap을 그리기를 할 목적지로 삼는다는 내용인 것이다.

 

Rectangle(m_hDC, -1, -1, m_ptResolution.x + 1, m_ptResolution.y + 1);

그래서 위와 같이, m_hDC로 DC를 지정해서 그림을 그리게 되면, 해당 Window가 보유하는 Bitmap에
그림이 그려졌던 것이다. (이후 출력 장치인 모니터로 볼 수 있었던 것이다.)

하지만 우리는 Window 창을 하나 더 띄우려던 것이 아니고, 그림을 그릴 스케치북이 한 장 더 필요했던 것 뿐이므로,
Bitmap 객체만 하나 더 만들면 되는 것이다.

 

// Double Buffering에 사용될 객체 생성
m_hBit = CreateCompatibleBitmap(m_hDC, m_ptResolution.x, m_ptResolution.y);

추후 또 다른 DC(스케치북)에 그려진 내용들을 통채로 m_hDC에 옮길 것이므로, 똑같은 해상도를 지정해주고,
또한 CreateCompatibleBitmap()이라는 함수명 그대로 m_hDC와 사본인 m_memDC에 호환되는 Bitmap을 만든다.
이렇게 만들어진 Bitmap도 handle(ID) 값을 받아서 관리를 한다.

 

m_memDC = CreateCompatibleDC(m_hDC);

뒤에서 그려질 목적지가 될 또 하나의 DC(스케치북)인 m_memDC도 CreateCompatibleDC() 함수를 통해,
m_hDC와 똑같은 성질의 DC를 만들어서 넣어준다.

 

HBITMAP hOldBit = (HBITMAP)SelectObject(m_memDC, m_hBit);
DeleteObject(hOldBit);

만들어준 m_memDC를 통해 그림을 그려질 목적지(스케치북)를 만들어준 m_hBit로 교체해준다.
(HDC는, 그림이 그려질 스케치북(Bitmap), 그림 도구들(Pen, Brush, ...), 등의 뭉치라고 보면 된다.)
(단순히 HDC 객체에 draw를 하면, 생각한대로 모두 됐었던 이유이다.)

HDC 객체가 생성되게 되면, 기본적으로 1px 크기의 Bitmap 객체를 그림 그릴 목적지로서 만들어서 가지고 있게 된다.
하지만, 전혀 쓸데 없으므로 되돌아 나온 기존의 1px 크기의 hOldBit는 바로 해제해주면 된다.

 

CCore::~CCore()
{
	ReleaseDC(m_hWnd, m_hDC);

	// Double Buffering에 사용된 객체 해제
	DeleteDC(m_memDC);
	DeleteObject(m_hBit);
}

이렇게 생성된 m_memDC와 m_hBit는 Core 소멸자에서 해제하도록 하자.
다만, m_memDC를 ReleaseDC()가 아닌 DeleteDC()로 지워준다는 것에 주의하자.
(CreateCompatibleDC()로 만든 HDC 객체는 항상 DeleteDC()로 지워줘야 한다.)
(Window 핸들을 얻어서 Window에 그리기 위해 만든 HDC 객체는 ReleaseDC()로 지워주면 된다.)

 

https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createcompatibledc

 

CreateCompatibleDC function (wingdi.h) - Win32 apps

The CreateCompatibleDC function creates a memory device context (DC) compatible with the specified device.

docs.microsoft.com

MSDN에서 해당 내용을 찾아볼 수 있다.

ReleaseDC()는 메모리에 할당된 화면 DC를 해제하기 위한 용도로 사용됩니다.
화면 DC 역시 메모리에 할당된 DC의 핸들을 가지고 있기 때문에 반드시 해제해야 합니다.
DeleteDC()는 메모리에 할당되고 메모리상에서 사용하는 DC를 해제하기 위한 용도로 사용됩니다.

정리해보면,
화면에 보이는 것은 ReleaseDC()로 해제를 해주어야 하고,
화면에 보이지 않고 메모리상에만 할당된DC는 DeleteDC()로 해제해야 합니다.

출처: 
https://myfreechild.tistory.com/entry/ReleaseDC와-DeleteDC의-차이점
 [★ 프로그래밍을 마냥 좋아하는 아이 - 심스의 프로그래밍 세상 ★]

또한, ReleaseDC()와 DeleteDC()의 차이점에 대해서 작성한 블로그 글을 인용해왔다. 참고하자.

 

void CCore::render()
{
	// 화면 Clear
	// m_memDC를 clear하도록 수정
	Rectangle(m_memDC, -1, -1, m_ptResolution.x + 1, m_ptResolution.y + 1);
	// 테두리 두께 1인 사각형을 그리므로, 테두리 두께를 1씩 빼줘야 한다.

	Vec2 vPos = g_obj.GetPos();
	Vec2 vScale = g_obj.GetScale();

	// m_memDC에 그리도록 수정
	Rectangle(m_memDC, (int)(vPos.x - vScale.x / 2.f)
					, (int)(vPos.y - vScale.y / 2.f)
					, (int)(vPos.x + vScale.x / 2.f)
					, (int)(vPos.y + vScale.y / 2.f));

	BitBlt(m_hDC, 0, 0, m_ptResolution.x, m_ptResolution.y
		, m_memDC, 0, 0, SRCCOPY);
}

render() 함수를 위와 같이 작성해주면 Double Buffering 구현이 끝난다.

그리는 곳을 m_hDC에서 m_memDC로 모두 바꿔주고, 
m_hDC에는 m_memDC에 완성된 그림을 통채로 불러오기만 한다.

 

BitBlt(m_hDC, 0, 0, m_ptResolution.x, m_ptResolution.y
		, m_memDC, 0, 0, SRCCOPY);

m_memDC에 이미 다 그려진 그림을 한 Pixel씩 다 긁어와서 한번에 m_hDC로 뿌려주는 작업을 하는 함수가 있는데,
이것이 BitBlt() 함수이다.

이렇게 뿌려주는 작업은 단순한 반복 노가다 작업이므로, 추후에 Direct 라이브러리를 이용하여,
GPU가 단순 반복 연산을 처리하도록 해서 FPS를 상승하도록 하는 방법이 필요하다.
(현재의 Win32API 프로젝트의 경우, rendering을 할 때 CPU를 쓰는 방법 뿐이다.)

 

지금은 CPU로 돌아가기 때문에, FPS가 600대로 확 떨어진 모습이다.
(이제부터는 이러한 큰 작업은 잘 없기에, 이 이후에는 FPS가 크게 떨어지지는 않는다.)

 

이제는 모두 그려진 그림을 m_memDC로 부터 m_hDC로 통채로 들고와서 rendering 작업을 하기 때문에,
이상적으로 (1) 깜빡임 현상도 사라졌고, (2) 이동 잔상 또한 Rectangle()로 지워주기에 사라진 모습이다.

 

다음 포스팅에 이어서 작성하겠습니다.