ISSUE 타일 렌더링 관련 최적화
카테고리: issue
트러블 이슈 -> 타일 렌더링 최적화.
Winapi + Direct2D를 이용한 프로젝트가 거의 다 끝났다. 소소한 버그 정도만 잡으면 되는 와중에 프로젝트 내내 신경쓰이는 부분이 있었다.
메인 화면에서는 거의 2000에 육박하는 프레임이 나온다. 하지만 인게임 내부에서는 600정도라는 저조한 퍼포먼스가 나온다는 점이 굉장히 신경쓰였다. 물론 인게임에서는 내부적으로 돌아가는 오브젝트나 매니저들도 많고, 이래저래 수식이나 물리라던가 성능을 잡아먹는 부분이 많지만.
그래도 2000 -> 600까지 프레임이 떨어지는 건 뭔가 불만스럽고 이해할 수가 없었다. 분명히 어느 부분에서 성능을 잡아먹고 있을 거라고 생각했고, 늘 그 부분에 대해 어느정도 감을 잡고 있긴했다.
Brotato 모작, 인게임에서 맵은 하나의 이미지를 잘라 수많은 타일들을 만든 뒤, 타일들을 일정한 알고리즘에 의해 배치되고 있다.
void CScene::MakeTile(const wstring &tag)
{
Direct2DMgr* pD2DMgr = Direct2DMgr::GetInstance();
auto splitBitmaps = Direct2DMgr::GetInstance()->GetSplitBitmaps(tag);
int tileIdx = 0;
int rest = 0;
for (int tileY = 0; tileY < 36; tileY++)
{
for (int tileX = 0; tileX < 36; tileX++)
{
CObject* pObj = new CTile;
pObj->SetPos(Vec2((float)TILE_SIZE / 4.f + ((float)TILE_SIZE / 2.f * (float)(tileX))
, (float)TILE_SIZE / 4.f + ((float)TILE_SIZE / 2.f * (float)(tileY))));
pObj->SetScale(Vec2((float)TILE_SIZE / 2.f, (float)TILE_SIZE / 2.f));
pObj->SetName(L"TILE");
pObj->CreateImage();
if (tileY == 0 && tileX == 0) tileIdx = 0;
else if (tileY == 0 && tileX == 35) tileIdx = 7;
else if (tileY == 35 && tileX == 0) tileIdx = 56;
else if (tileY == 35 && tileX == 35) tileIdx = 63;
//맨윗줄
else if (tileY == 0 && (tileX >= 1 && tileX <= 34))
{
rest = tileX % 7;
if (rest == 0) rest++;
tileIdx = rest;
}
//맨아랫줄
else if (tileY == 35 && (tileX >= 1 && tileX <= 34))
{
rest = tileX % 7;
if (rest == 0) rest++;
tileIdx = rest + 56;
}
//맨왼쪽줄
else if (tileX == 0 && (tileY >= 1 && tileY <= 34))
{
rest = tileY % 7;
if (rest == 0) rest++;
tileIdx = rest * 8;
}
//맨 오른쪽
else if (tileX == 35 && (tileY >= 1 && tileY <= 34))
{
rest = tileY % 7;
if (rest == 0) rest++;
tileIdx = rest * 8 + 7;
}
else
{
int randX = rand() % 7;
if (randX == 0) randX = 1;
int randY = rand() % 7;
if (randY == 0) randY = 1;
tileIdx = randY * 8 + randX;
}
pObj->AddImage(splitBitmaps[tileIdx]);
AddObject(pObj, GROUP_TYPE::TILE);
}
}
}
이미 잘려진 이미지 타일 리소스를 불러와서 타일을 만들어낸다.
36 * 36 == 1296이기 때문에, 인게임 내부에서 1296개의 타일 오브젝트를 매 프레임마다 렌더링한다고 볼 수 있다. 이 부분에서 나는 개선할 수 있다고 생각했다. 그 이유로는
-
1296개의 타일을 각각 드로우 콜을 하기 때문에 거기서 생기는 오버헤드가 엄청날 것이라고 생각했기 때문이다.
-
내부적으로 오브젝트들은 vector로 관리한다. 1296개의 오브젝트를 반복문하고, 메모리 접근하는 것은 많은 성능상의 낭비라고 생각하였다.
-
캐시 효율 저하. 1296개의 오브젝트. 즉 오브젝트가 많아지면 많을수록 CPU의 캐시 히트율이 떨어진다… 라고 생각이 들었다. 이 부분은 혹시나 그렇지 않을까? 생각하는 부분이라 확실하진 않다.
그 이외에도 더 이유가 있을 수 있는데, 아직 실력이 미진한 내 입장에선 이 정도의 이유로 문제점을 파악하고 개선에 나섰다.
결국 문제는 1296개. 물론, 렌더링하는 부분에서 카메라가 보고있는 부분만 렌더링하도록 되어있긴 하지만, 맵이 카메라가 한 번에 보는 크기와 비교해 엄청나게 차이가 나진 않는다. 따라서 대다수의 타일들이 매 프레임마다 렌더링 된다고 생각을 했고, 그 오브젝트들을 최대한 줄이는 것이 이번 최적화의 목표였다.
ID2D1Bitmap* CScene::CreateCompositeMapBitmap(const wstring &tag)
{
Direct2DMgr* pD2DMgr = Direct2DMgr::GetInstance();
auto splitBitmaps = Direct2DMgr::GetInstance()->GetSplitBitmaps(tag);
const int gridCount = 36;
// 각 타일는 원래 MakeTile에서 다음과 같이 배치됨:
// 위치: (TILE_SIZE/4 + (TILE_SIZE/2 * tileX), TILE_SIZE/4 + (TILE_SIZE/2 * tileY))
// 크기: TILE_SIZE/2 × TILE_SIZE/2
// (TILE_SIZE = 64 → offset = 16, tileDrawSize = 32)
const float offset = 16.0f;
const float tileDrawSize = 32.0f;
// 전체 합성 비트맵의 크기 계산:
// 좌측 여백 offset에서 시작해 gridCount * tileDrawSize 만큼 확장.
const UINT compositeWidth = (UINT)(offset + tileDrawSize * gridCount);
const UINT compositeHeight = compositeWidth; // 정사각형
// 3. 오프스크린 렌더 타겟(비트맵 렌더 타겟) 생성
ID2D1Bitmap* pCompositeBitmap = nullptr;
ComPtr<ID2D1BitmapRenderTarget> pBitmapRT = nullptr;
HRESULT hr = Direct2DMgr::GetInstance()->GetRenderTarget()->CreateCompatibleRenderTarget(
D2D1::SizeF((FLOAT)compositeWidth, (FLOAT)compositeHeight),
&pBitmapRT
);
if (FAILED(hr))
return nullptr;
pBitmapRT->BeginDraw();
// 배경색 (예: 흰색)으로 클리어
pBitmapRT->Clear(D2D1::ColorF(D2D1::ColorF::Black));
// 4. 그리드 순회: 각 타일의 서브 비트맵 인덱스 결정 후 합성
for (int tileY = 0; tileY < gridCount; tileY++)
{
for (int tileX = 0; tileX < gridCount; tileX++)
{
int tileIdx = 0;
int rest = 0;
// 모서리 타일 처리
if (tileY == 0 && tileX == 0) tileIdx = 0;
else if (tileY == 0 && tileX == 35) tileIdx = 7;
else if (tileY == 35 && tileX == 0) tileIdx = 56;
else if (tileY == 35 && tileX == 35)tileIdx = 63;
// 상단 테두리
else if (tileY == 0 && (tileX >= 1 && tileX <= 34))
{
rest = tileX % 7;
if (rest == 0) rest++;
tileIdx = rest;
}
// 하단 테두리
else if (tileY == 35 && (tileX >= 1 && tileX <= 34))
{
rest = tileX % 7;
if (rest == 0) rest++;
tileIdx = rest + 56;
}
// 좌측 테두리
else if (tileX == 0 && (tileY >= 1 && tileY <= 34))
{
rest = tileY % 7;
if (rest == 0) rest++;
tileIdx = rest * 8;
}
// 우측 테두리
else if (tileX == 35 && (tileY >= 1 && tileY <= 34))
{
rest = tileY % 7;
if (rest == 0) rest++;
tileIdx = rest * 8 + 7;
}
// 내부 타일: 테두리 제외하고 랜덤하게 (테두리 인덱스 제외)
else
{
int randX = rand() % 7;
if (randX == 0) randX = 1;
int randY = rand() % 7;
if (randY == 0) randY = 1;
tileIdx = randY * 8 + randX;
}
// 5. 타일의 배치 위치 계산
// MakeTile에서의 위치: X = offset + tileX * tileDrawSize, Y = offset + tileY * tileDrawSize
float destLeft = offset + tileX * tileDrawSize;
float destTop = offset + tileY * tileDrawSize;
D2D1_RECT_F destRect = D2D1::RectF(destLeft, destTop, destLeft + tileDrawSize, destTop + tileDrawSize);
// 6. 서브 비트맵의 원본 영역: 여기서는 전체 서브 이미지 (크기 TILE_SIZE×TILE_SIZE)
D2D1_RECT_F srcRect = D2D1::RectF(0, 0, (FLOAT)TILE_SIZE, (FLOAT)TILE_SIZE);
// 7. 타일 텍스처에서 해당 서브 이미지를 DrawBitmap (타일 이미지가 64x64에서 32x32로 축소됨)
if (tileIdx >= 0 && tileIdx < (int)splitBitmaps.size())
{
pBitmapRT->DrawBitmap(
splitBitmaps[tileIdx], // 해당 서브 비트맵
destRect, // 합성될 위치 및 크기
1.0f, // 불투명도
D2D1_BITMAP_INTERPOLATION_MODE_LINEAR,
srcRect // 원본 영역
);
}
}
}
hr = pBitmapRT->EndDraw();
if (FAILED(hr))
return nullptr;
// 8. 오프스크린 렌더 타겟에서 최종 커다란 비트맵 추출
hr = pBitmapRT->GetBitmap(&pCompositeBitmap);
if (FAILED(hr))
return nullptr;
return pCompositeBitmap;
}
그래서 맵 이미지를 만드는 코드를 새로 작성하였다.
맵을 생성하는 알고리즘 자체는 동일하다. 하지만, 결정적으로 다른 부분은 기존에는 1296개의 오브젝트들을 각각 AddObject해주었다면, 이번에는 모든 타일들을 하나의 비트맵에 모은다.
그리고 커다란 비트맵 한 개를 만들어서, 그 비트맵의 주소값을 반환하는 방식이다. 따라서 이 비트맵으로 맵 오브젝트를 1개 생성해주었다.
내 생각대로라면 당연히 매 프레임마다 반복이 줄고, 드로우 콜도 1개로 줄어든다. 과연 성능은 어떻게 되었을까??
1200정도! 또 상황에 따라 1500정도까지 올라가기도 한다. 맵 타일을 1296개에서 1개로 줄인 것으로 프레임이 2배 ~ 2.5배 정도 상승한 것이다.
나름대로 만족스러운 프레임이 되었다. 사실 Unity 개발을 하면서 배운 부분이긴 하지만, 이렇게 WINAPI + Direct2D에서 적용되니 기분이 미묘하기도 하였다.
댓글 남기기