인스턴싱과 드로우콜

Jaemyeong Lee·2025년 4월 2일

게임 서버1

목록 보기
199/220

🧠 드로우콜(Draw Call) 완벽 이해

DirectX11을 포함한 모든 렌더링 파이프라인에서 "드로우콜"은 굉장히 중요한 개념입니다. 쉽게 말해, 드로우콜은 GPU에게 "이거 그려!"라고 명령을 보내는 호출입니다.

예를 들어, 큐브 하나를 그리고 싶다면:

  • 메쉬(Mesh) 정보
  • 머테리얼(Material)
  • 쉐이더(Shader)
    이 세 가지를 준비한 다음, DrawIndexed와 같은 함수로 GPU에 렌더링을 요청합니다.

그런데 게임 화면에 큐브가 100개 있으면 어떻게 될까요?

🤯 정답은? DrawIndexed를 100번 호출해야 합니다.

이게 바로 드로우콜이 많아지는 현상이고, 실제로 유니티 엔진에서는 이 드로우콜 횟수를 Batches 수치로 표시합니다. Stats 창을 열어보면 Batches: 2 같은 표시가 그것입니다.

✅ 드로우콜이 많아지면 CPU-GPU 간의 통신 오버헤드가 커지고, 게임 성능이 급격히 하락합니다.

그래서 등장한 것이 바로…


🚀 인스턴싱(Instancing)의 등장

인스턴싱은 이런 문제를 해결하기 위한 렌더링 최적화 기법입니다.

동일한 메쉬와 머테리얼을 사용하는 객체들이 다수 있을 때, 한 번의 드로우콜로 모두를 그려버리는 기술입니다.

예시로 10,000개의 같은 큐브가 게임 맵 위에 있다면?

  • 기존 방식: DrawIndexed를 10,000번 호출
  • 인스턴싱 방식: DrawIndexedInstanced 한 번 호출 + 위치 정보만 따로 넘김

즉, GPU 파이프라인을 세팅하는 비용은 단 한 번, 그리고 인스턴스마다 위치, 회전 등의 정보만 넘겨주는 것이 핵심입니다.


🛠️ DrawIndexedInstanced 함수 구조

DirectX에서는 DrawIndexedInstanced라는 함수가 이를 지원합니다:

void Shader::DrawIndexedInstanced(
	UINT technique,
	UINT pass,
	UINT indexCountPerInstance,
	UINT instanceCount,
	UINT startIndexLocation,
	INT baseVertexLocation,
	UINT startInstanceLocation
)

여기서 핵심은 instanceCount몇 개의 인스턴스를 동시에 그릴 것인가를 명시하는 부분입니다.

즉, DrawIndexedInstanced(...) 를 통해 GPU에 이만큼의 객체들을 한 번에 그려줘! 라고 말하는 거죠.


🎨 인스턴싱을 위한 구조 설계

우리가 인스턴싱을 사용하기 위해서는 물체마다 변하는 정보(위치, 회전, 스케일 등)를 GPU에 전달할 수 있어야 합니다.

그래서 다음과 같은 요소들을 도입하게 됩니다:

✅ 인스턴스 버퍼 (InstanceBuffer)

vector<Matrix>를 만들어, 각 오브젝트의 World Matrix를 저장합니다.

vector<Matrix> _worlds;

이 데이터를 담는 VertexBuffer를 따로 만들고, 슬롯 1번에 바인딩해 사용합니다.

_instanceBuffer = make_shared<VertexBuffer>();
_instanceBuffer->Create(_worlds, 1);

📦 VertexBuffer 커스터마이징

인스턴싱을 지원하려면 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;

🎯 InputLayout에서 인스턴스 데이터 인식

쉐이더 입력 구조를 다음처럼 구성합니다:

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)에서 인스턴스 적용

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를 곱해줌으로써, 각 오브젝트의 변환 좌표를 반영한 렌더링이 가능합니다.


🧪 성능 테스트: 10,000개 오브젝트 렌더링

  • 인스턴싱 없이: 드로우콜 10,000회 → 프레임 급감
  • 인스턴싱 적용: 드로우콜 단 1회 → 프레임 안정 유지

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());

profile
李家네_공부방

0개의 댓글