Deeper Learning

[WinAPI] 카메라 시점 기반 렌더링 구현 본문

Game Development/WinAPI

[WinAPI] 카메라 시점 기반 렌더링 구현

Dlaiml 2024. 6. 6. 12:13

언리얼 엔진을 학습하면서 작은 게임을 만들 때에도 로드할 데이터가 매우 많은데 오픈월드 게임처럼 방대한 게임은 어떻게 이를 처리하는지 궁금하여 찾아본 적이 있다.

 

http://imgur.com/a/kQkOr

 

언리얼 엔진에서는 이를 레벨을 Chunk로 나누고 플레이어의 시야에 따라 이를 로드하는 레벨 스트리밍(Level Streaming) 기술을 활용하여 로딩 없는 심리스 월드를 구현할 수 있도록 하였다.

 

 

현재 WinAPI를 활용한 코드에서도 레벨(=씬)에 배치된 오브젝트가 1만개를 넘어가는데 (대부분 맵을 구성하는 타일 오브젝트) 매 Tick 마다 이를 전부 렌더링 하고 있어 프레임 드랍이 심한 상황이다. 

 

적 오브젝트의 경우 카메라 밖에서 생성되거나,  플레이어가 멀어져 카메라 밖으로 벗어나도 정해진 State에 따라 행동을 반복해야 하기 때문에 시점을 기반으로 렌더링 하기에는 무리가 있다. 시점 밖에서 피격, 공격 액션이 불가능하다고 가정하면 위치와 오브젝트 종류 데이터를 벡터 내에 쌓아두어 관리할 수는 있겠지만 현재 레벨에 배치된 오브젝트의 99% 이상이 바닥을 구성하는 Tile 클래스의 오브젝트다. 

 

타일은 이동을 하지 않으며 카메라 밖에서 업데이트가 이루어질 필요가 없기 때문에 타일 클래스를 플레이어 카메라 시점주변에서만 렌더링 하도록 코드를 수정해 보자.

 

void CScene::Render(HDC _hdc)
{
	for (UINT i = 0; i < (UINT)GROUP_TYPE::END; ++i)
	{
		vector<CObject*>::iterator iter = m_arrObj[i].begin();

		for (;iter != m_arrObj[i].end();)
		{
			if (!(*iter)->IsDead())
			{
				(*iter)->Render(_hdc);
				++iter;
			}
			else
			{
				iter = m_arrObj[i].erase(iter);
			}
		}
	}
}

 

현재 코드는 오브젝트 종류와 상관없이 렌더링 시점에 현재 레벨에 배치된 모든 오브젝트를 렌더링 하고 있다.

 

void CScene::Render(HDC _hdc)
{
	for (UINT i = 0; i < (UINT)GROUP_TYPE::END; ++i)
	{
    		if ((UINT)GROUP_TYPE::TILE == i)
		{
			RenderTile(_hdc);
			continue;
		}
		vector<CObject*>::iterator iter = m_arrObj[i].begin();

		for (;iter != m_arrObj[i].end();)
		{
			if (!(*iter)->IsDead())
			{
				(*iter)->Render(_hdc);
				++iter;
			}
			else
			{
				iter = m_arrObj[i].erase(iter);
			}
		}
	}
}

 

오브젝트를 순회하는 반복문 위쪽에 타일 오브젝트를 따로 처리할 수 있도록 위처럼 코드를 삽입하였다.

 

타일을 렌더링 하는 함수인 RenderTile 내부 동작은 아래 순서로 진행된다 

1. 현재 카메라가 보고 있는 좌표 파악

2. 카메라에서 보이는 타일의 index 파악

3. 해당 타일들만 렌더링

 

 

Vec2 vRes = CCore::GetInstance()->GetResolution();
Vec2 vCamPos = CCamera::GetInstance()->GetCurLookPos();

Vec2 vLeftTop = vCamPos - vRes / 2.f;

 

매니저 클래스인 Core, Camera에서 각각 해상도와 카메라 시야의 중앙 좌표를 가져온다.

카메라 시야 중앙점에서 해상도를 2로 나눈 값을 빼서 좌상단의 좌표를 vLeftTop 변수에 할당한다.

 

int iLeftTopTileCol = (int)vLeftTop.x / (int)TILE_SIZE;
int iLeftTopTileRow = (int)vLeftTop.y / (int)TILE_SIZE;

 

타일은 정사각형으로 설정해 두었기 때문에 좌상단 좌표에 해당하는 타일의 Row, Column 인덱스를 구할 수 있다.

 

 

// Create Tile 
// Scene.cpp

for (UINT i = 0 ; i <_iYCount; ++i)
{
    for (UINT j = 0; j < _iXCount; ++j)
    {
        CTile* pTile = new CTile;
        pTile->SetPos(Vec2((float)(j * TILE_SIZE), (float)(i * TILE_SIZE)));
        pTile->SetTexture(pTileTex);
        AddObject(pTile, GROUP_TYPE::TILE);
    }
}

 

위 코드를 보면 Scene에 타일을 배치할 때 X축으로 우선 배치하고 Y축으로 하나씩 내려가며 순서대로 Object를 보관하는 벡터에 push_back 하였기 때문에 행렬의 인덱스를 알면 해당 타일을 특정할 수 있다. 이를 활용해서 구한 행렬 인덱스를 가지고 알맞은 타일을 불러와 렌더링 할 수 있다.

 

int iNumTilesInViewX = ((int)vRes.x / (int)TILE_SIZE)+1;
int iNumTilesInViewY = ((int)vRes.y / (int)TILE_SIZE)+1;

for (int iCurRow = iLeftTopTileRow; iCurRow < (iLeftTopTileRow + iNumTilesInViewY); ++iCurRow)
{
    for (int iCurCol = iLeftTopTileCol; iCurCol < (iLeftTopTileCol + iNumTilesInViewX); ++iCurCol)
    {
        if (iCurCol < 0 || m_iXTileCount <= iCurCol || iCurRow < 0 || m_iYTileCount <= iCurRow)
        {
            continue;
        }
        int iIdx = (m_iXTileCount * iCurRow) + iCurCol;
        vecTile[iIdx]->Render(_hdc);
    }
}

 

현재 화면에 노출되어야 할 타일의 수를 현재 해상도 / 타일 사이즈로 나누어 구하고 나누느라 잘려나간 소수부를 처리하기 위해 추가로 1을 더해주었다.

 

좌상단부터 우하단까지 Column 인덱스부터 하나씩 늘려가며 씬에 배치된 타일 타입의 오브젝트가 저장되어 있는 vecTile 벡터에서 해당 타일을 찾아 렌더링 함수를 호출하여 기능 구현을 완료하였다.

 

아직 씬 크기에 따라 카메라의 이동을 제한하는 기능이 완성되지 않아 카메라가 타일 영역을 벗어날 경우 음수 인덱스가 생기는 문제가 있어 타일 벡터에서 타일을 찾기 전에 예외처리하였다.

 

후에 또 플레이하기 어려울 정도로 프레임이 저하될 경우, 반복되는 타일 패턴을 하나의 Chunk로 묶어 Bitblt 호출 횟수를 줄여 오버헤드를 줄이는 방식도 구현해 볼 예정이다.

 

 

Reference

[0] https://learn.microsoft.com/ko-kr/windows/win32/api/

[1] https://www.youtube.com/watch?v=dlFr-OnHlWU&list=PL4SIC1d_ab-ZLg4TvAO5R4nqlJTyJXsPK

Comments