DirectX11을 포함한 모든 렌더링 파이프라인에서 "드로우콜"은 굉장히 중요한 개념입니다. 쉽게 말해, 드로우콜은 GPU에게 "이거 그려!"라고 명령을 보내는 호출입니다.
예를 들어, 큐브 하나를 그리고 싶다면:
DrawIndexed와 같은 함수로 GPU에 렌더링을 요청합니다.그런데 게임 화면에 큐브가 100개 있으면 어떻게 될까요?
🤯 정답은? DrawIndexed를 100번 호출해야 합니다.
이게 바로 드로우콜이 많아지는 현상이고, 실제로 유니티 엔진에서는 이 드로우콜 횟수를 Batches 수치로 표시합니다. Stats 창을 열어보면 Batches: 2 같은 표시가 그것입니다.
✅ 드로우콜이 많아지면 CPU-GPU 간의 통신 오버헤드가 커지고, 게임 성능이 급격히 하락합니다.
그래서 등장한 것이 바로…
인스턴싱은 이런 문제를 해결하기 위한 렌더링 최적화 기법입니다.
동일한 메쉬와 머테리얼을 사용하는 객체들이 다수 있을 때, 한 번의 드로우콜로 모두를 그려버리는 기술입니다.
예시로 10,000개의 같은 큐브가 게임 맵 위에 있다면?
DrawIndexed를 10,000번 호출DrawIndexedInstanced 한 번 호출 + 위치 정보만 따로 넘김즉, GPU 파이프라인을 세팅하는 비용은 단 한 번, 그리고 인스턴스마다 위치, 회전 등의 정보만 넘겨주는 것이 핵심입니다.
DirectX에서는 DrawIndexedInstanced라는 함수가 이를 지원합니다:
void Shader::DrawIndexedInstanced(
UINT technique,
UINT pass,
UINT indexCountPerInstance,
UINT instanceCount,
UINT startIndexLocation,
INT baseVertexLocation,
UINT startInstanceLocation
)
여기서 핵심은 instanceCount – 몇 개의 인스턴스를 동시에 그릴 것인가를 명시하는 부분입니다.
즉, DrawIndexedInstanced(...) 를 통해 GPU에 이만큼의 객체들을 한 번에 그려줘! 라고 말하는 거죠.
우리가 인스턴싱을 사용하기 위해서는 물체마다 변하는 정보(위치, 회전, 스케일 등)를 GPU에 전달할 수 있어야 합니다.
그래서 다음과 같은 요소들을 도입하게 됩니다:
vector<Matrix>를 만들어, 각 오브젝트의 World Matrix를 저장합니다.
vector<Matrix> _worlds;
이 데이터를 담는 VertexBuffer를 따로 만들고, 슬롯 1번에 바인딩해 사용합니다.
_instanceBuffer = make_shared<VertexBuffer>();
_instanceBuffer->Create(_worlds, 1);
인스턴싱을 지원하려면 VertexBuffer도 확장해야 합니다. 다음과 같은 정보를 추가로 관리해야 합니다:
_slot : 슬롯 번호 (0: 기본 정점 버퍼, 1: 인스턴스 버퍼)_cpuWrite, _gpuWrite: 버퍼 접근 권한 (동적 갱신 가능 여부)if (!cpuWrite && !gpuWrite)
desc.Usage = D3D11_USAGE_IMMUTABLE;
else if (cpuWrite && !gpuWrite)
desc.Usage = D3D11_USAGE_DYNAMIC;
else if (!cpuWrite && gpuWrite)
desc.Usage = D3D11_USAGE_DEFAULT;
else
desc.Usage = D3D11_USAGE_STAGING;
쉐이더 입력 구조를 다음처럼 구성합니다:
struct VS_IN
{
float4 position : POSITION;
float2 uv : TEXCOORD;
float3 normal : NORMAL;
float3 tangent : TANGENT;
matrix world : INST; // 인스턴스별 월드 행렬
};
그리고 Shader::CreateInputLayout() 함수에서는 "INST"로 시작하는 이름을 가진 요소들을 인스턴스 데이터로 자동 인식하도록 구현합니다:
if (Utils::StartsWith(name, "INST"))
{
elementDesc.InputSlot = 1;
elementDesc.InputSlotClass = D3D11_INPUT_PER_INSTANCE_DATA;
elementDesc.InstanceDataStepRate = 1;
}
VS_OUT VS(VS_IN input)
{
VS_OUT output;
output.position = mul(input.position, input.world); // 각 인스턴스의 world 행렬 사용
output.worldPosition = output.position;
output.position = mul(output.position, VP);
output.uv = input.uv;
output.normal = input.normal;
return output;
}
input.world를 곱해줌으로써, 각 오브젝트의 변환 좌표를 반영한 렌더링이 가능합니다.
FPS 측정 예시:
void Game::ShowFps()
{
uint32 fps = GET_SINGLE(TimeManager)->GetFps();
WCHAR text[100] = L"";
wsprintf(text, L"FPS : %d", fps);
SetWindowText(_desc.hWnd, text);
}
공장 비유로 이해하면 쉽습니다.
- 공장을 한 번 세우고(렌더링 파이프라인)
- 다른 제품들(인스턴스)만 위치만 바꿔 찍어냅니다.
- 공장을 1만 번 세우는 것보다 훨씬 효율적이죠.
드로우콜 최적화의 핵심
- DrawIndexed() → 호출 수 만큼 CPU-GPU 통신 발생
- DrawIndexedInstanced() → 단 1회 호출로 다수의 오브젝트 처리
// 인스턴스 버퍼 생성
_instanceBuffer->Create(_worlds, 1);
// 렌더링
_mesh->GetVertexBuffer()->PushData();
_instanceBuffer->PushData();
_mesh->GetIndexBuffer()->PushData();
_shader->DrawIndexedInstanced(0, 0, _mesh->GetIndexBuffer()->GetCount(), _objs.size());