티스토리 뷰
개요
이번 시간에는 Instanced Drawing이라는 개념에 대해서 알아보겠습니다.
Instanced Drawing이란 같은 Geometry 여러 개를 하나의 DrawCall로 그리는 것을 뜻합니다.
여기서 말하는 Instance란 한 번에 그려지는 개체들을 뜻합니다.
DirectX12에는 DrawIndexedInstanced라는 함수가 있는데 여기 InstanceNum 인자로 5를 보내게 되면 5개의 Instance가 한 번의 DrawCall로 그려지게 됩니다.
각 Instance 별로 별도의 Scale, Rotation, Translation, Color 정보를 주는 것도 가능합니다.
물론 Instanced Drawing 대신 5번의 일반적인 Draw를 통해서도 같은 효과를 낼 수 있습니다.
다만, 이렇게 할 경우 DrawCall을 보내는 CPU의 부하가 생기고, GPU의 병렬처리 이점을 살릴 수 없게 됩니다.
따라서 최적화 관점에서 되도록이면 Instanced Drawing을 사용하는 것이 좋습니다.

구현 방법
구현 방법을 간략하게 설명하면 아래와 같습니다.
1) Instance 별로 가지게 될 정보를 정의하고 해당 데이터를 GPU 버퍼로 올림
2) 버퍼를 어떻게 해석할 것인지에 대한 정보를 GPU에 알려주고 DrawCall을 함
3) Vertex Shader가 입력받은 데이터를 알맞게 해석하도록 수정
보시는 것처럼 과정이 많거나 복잡하지 않습니다.
각 과정을 어떻게 코드로 구현하였는지 살펴보도록 하겠습니다.
1) Instance 별로 가지게 될 정보를 정의하고 해당 데이터를 GPU 버퍼로 올림
먼저 VertexData 외에 InstanceData를 추가해야 합니다.
Instance 별로 다르게 하고 싶은 데이터를 정의합니다.
이번 예제에서는 World Matrix와 Color를 정의했습니다.
struct InstanceData
{
XMFLOAT4X4 WorldMatrix;
XMFLOAT4 Color;
};
그 다음으로는 데이터를 선언하고 값을 채워 넣어야 합니다.
InstanceData* instanceData = new InstanceData[instanceNum];
for (int i = 0; i < instanceNum; i++)
{
float scale = halfToOne(gen) * 0.15f;
instanceData[i].Color = XMFLOAT4(zeroToOne(gen), zeroToOne(gen), zeroToOne(gen), 1.0f);
instanceData[i].WorldMatrix = XMFLOAT4X4(
scale, 0.0f, 0.0f, 0.0f,
0.0f, scale, 0.0f, 0.0f,
0.0f, 0.0f, scale, 0.0f,
minusOneToOne(gen) * drawInstancedMaxDist,
minusOneToOne(gen) * drawInstancedMaxDist,
minusOneToOne(gen) * drawInstancedMaxDist,
1.0f);
}
예제에서는 색깔, Scale과 Translation 값을 랜덤으로 설정했습니다.
다음으로는 Instance Buffer를 생성해서 아까 만든 데이터를 넘기고, Instance Buffer View를 Instance Buffer와 연결해주어야 합니다.
ComPtr<ID3D12Resource> m_InstanceBuffer;
D3D12_VERTEX_BUFFER_VIEW m_InstanceBufferView;
ComPtr<ID3D12Resource> intermediateInstanceBuffer;
size_t bufferSize = instanceNum * sizeof(InstanceData);
device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(bufferSize, D3D12_RESOURCE_FLAG_NONE),
D3D12_RESOURCE_STATE_COMMON,
nullptr,
IID_PPV_ARGS(&m_InstanceBuffer));
device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(bufferSize),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&intermediateInstanceBuffer));
D3D12_SUBRESOURCE_DATA subresourceData = {};
subresourceData.pData = instanceData;
subresourceData.RowPitch = bufferSize;
subresourceData.SlicePitch = subresourceData.RowPitch;
UpdateSubresources(commandList.Get(),
m_InstanceBuffer.Get(), intermediateInstanceBuffer.Get(),
0, 0, 1, &subresourceData);
m_InstanceBufferView.BufferLocation = m_InstanceBuffer->GetGPUVirtualAddress();
m_InstanceBufferView.SizeInBytes = instanceNum * sizeof(InstanceData);
m_InstanceBufferView.StrideInBytes = sizeof(InstanceData);
먼저 CreateCommittedResource 함수를 호출해서 Instance Buffer를 만들어 m_InstanceBuffer로 참조하도록 합니다.
데이터 복사 효율을 위해 중간 버퍼인 intermediateInstnaceBuffer를 만듭니다.
D3D12_SUBRESOURCE_DATA의 pData로 앞서 만든 instanceData를 참조하도록 하고 UpdateSubresources 함수 호출을 통해 데이터를 업로드합니다.
마지막으로는 미리 정의한 m_InstanceBufferView.BufferLocation이 m_InstanceBuffer의 주소를 가리키도록 합니다.
이 Instance Buffer View는 나중에 Rendering Pipeline에 연결해줄 예정입니다.
마지막으로는 아까 만들어둔 Instance Buffer View를 이용해서 Rendering Pipeline에 연결해줍니다.
commandList->IASetVertexBuffers(1, 1, &m_InstanceBufferView);
IASetVertexBuffers 함수를 호출해서 Instance Buffer View와 연결합니다.
2) 버퍼를 어떻게 해석할 것인지에 대한 정보를 GPU에 알려주고 DrawCall을 함
업로드된 버퍼를 해석할 방식을 알려주려면 Input Layout을 수정해주어야 합니다.
Pipeline State Object를 만들 때 사용하는 Input Layout을 정의하는 코드를 아래와 같이 수정해줍니다.
D3D12_INPUT_ELEMENT_DESC inputLayout[] = {
// 기존에 존재하던 부분
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "UV", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
// Instanced Drawing을 위해 추가된 부분
{ "WORLD", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 0, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1},
{ "WORLD", 1, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 16, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1},
{ "WORLD", 2, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 32, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1},
{ "WORLD", 3, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 48, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1},
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 64, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 },
};
D3D12_INPUT_ELEMENT_DESC 구조체의 변수들을 순서대로 살펴보면 아래와 같습니다.
SemanticName: Shader에서 사용할 이름입니다. Shader에 같은 이름으로 변수를 정의해두어야 합니다.
SemanticIndex: 동일한 Sematic을 이용할 경우 구분하기 위한 용도입니다. WORLD는 World Matrix로 총 16개의 float을 사용해야 하는데 그런 포맷은 없기에 4개의 float 포맷 타입을 4번 정의하고 Semantic Index를 다르게 주어 사용했습니다.
Format: 데이터 포맷입니다. 4개의 32bit float 값을 사용할 것이므로 R32G32B32A32_FLOAT 타입으로 지정했습니다.
InputSlot: 여러 개의 Vertex Buffer를 사용할 때 설정하는 번호입니다. 기존에 정점별로 사용하던 Vertex Buffer는 0으로 설정했고 이번에 추가한 Instance 별로 사용하는 Instance Buffer는 1로 설정했습니다.
AlignedByteOffset: 이 요소가 Buffer의 어느 위치에 있는지에 대한 정보입니다. 4개 float이니 4개의 4byte라서 16씩 증가시켜서 설정했습니다.
InputSlotClass: 정점별 데이터인지 인스턴스별 데이터인지에 대한 정보입니다.
InstanceDataStepRate: 몇 개의 인스턴스 마다 새로운 정보를 사용할지에 대한 정보입니다. 각각의 Instance가 새로운 데이터를 사용할 것이므로 1로 설정했습니다.
이제 마지막으로 DrawIndexedInstanced 함수를 호출할 때 InstanceNum을 인자로 주어 호출합니다.
commandList->DrawIndexedInstanced(m_SphereModel->IndicesCount(), instanceNum, 0, 0, 0);
3) Vertex Shader가 입력받은 데이터를 알맞게 해석하도록 수정
struct VS_INPUT
{
// 기존에 존재하던 부분
float3 Position : POSITION;
float3 Normal : NORMAL;
float2 UV : UV;
// Instanced Drawing을 위해 추가된 부분
float4x4 World : WORLD; // 4행 float4로 선언된 인스턴스 행렬
float4 Color : COLOR;
};
Vertex Shader에서 입력으로 받는 VS_INPUT 구조체에 float4x4 World 변수와 float4 Color 변수를 추가했습니다.
주의하셔야 할 점은 Vertex Shader 입장에서 별도로 Vertex Data와 Instance Data를 구분하지 않는다는 것입니다.
Instance Data도 같은 Instance 내에서 같은 값을 가질 뿐이지 결국은 각 Vertex별로 필요한 데이터이기 때문에 그런게 아닐까 싶습니다.
이렇게 입력받은 데이터는 용도에 알맞게 사용하기만 하면 됩니다.
예제에서 World 변수는 객체를 World Transform을 하는 Matrix로 사용했고, Color 변수는 객체별 Color로 사용했습니다.
지금까지 Instanced Drawing에 대해서 알아봤습니다.
Instanced Drawing을 사용하지 않더라도 원하는 씬을 그릴 수는 있으나 같은 Geometry를 조금만 다르게 해서 여러 번 그리는 경우에는 DrawCall 낭비입니다.
코드도 많이 복잡하지 않기 때문에 되도록이면 Instanced Drawing을 사용하면 좋을 것 같습니다.
'프로그래밍 > DirectX12' 카테고리의 다른 글
| DirectX12를 활용한 Deferred Shading (0) | 2025.07.22 |
|---|---|
| DirectX12를 활용한 Bloom 효과 (0) | 2025.03.30 |
| DirectX12에서의 CPU & GPU 동기화 (1) | 2025.01.26 |
| Microsoft PIX를 활용한 DirectX12 프로그램 디버깅 (0) | 2025.01.13 |
| DirectX12를 활용한 PostProcessing (0) | 2024.12.28 |
- Total
- Today
- Yesterday
- perspective projection
- 경우의 수
- normalized device coordinate
- Unreal
- MeshProcessing
- Mesh Processing
- DirectX12
- 중복 순열
- 순열
- 조합
- CollisionDetection
- Mesh
- 값 형식
- VTK
- opengl
- C#
- value type
- 최적화
- 중복 조합
- 통계학
- collision detection
- 유니티
- RL
- 수학
- RubiksCube
- 루빅스큐브
- 참조 형식
- Unity
- AABB
- Scriptable Render Pipeline
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |