개요
Index Buffer(인덱스 버퍼)와 Constant Buffer(상수 버퍼)는 DirectX 11 렌더링 파이프라인에서 효율성과 유연성을 제공하는 핵심 요소입니다. 인덱스 버퍼는 정점 재사용을 통해 메모리를 절약하고, 상수 버퍼는 셰이더에 동적 데이터를 전달합니다.
Part 1: 인덱스 버퍼의 필요성
인덱싱 없이 두 개의 삼각형 그리기
다음 그림에 표시된 쿼드(사각형)를 그릴 것이라고 가정해 보겠습니다.
삼각형 목록(Triangle List) 기본 형식을 사용하여 두 삼각형을 렌더링하는 경우 각 삼각형은 3개의 개별 꼭짓점으로 저장되므로 다음 그림과 비슷한 꼭짓점 버퍼가 생성됩니다.
그리기 동작
•
꼭짓점 버퍼 내의 위치 0부터 시작하여 두 개의 삼각형을 그립니다
•
컬링을 사용하도록 설정하면 꼭짓점의 순서가 중요합니다
•
이 예제에서는 기본 시계 반대 방향 컬링 상태를 가정하므로 표시되는 삼각형을 시계 방향 순서로 그려야 합니다
•
삼각형 목록 기본 형식은 단순히 각 삼각형에 대한 버퍼에서 선형 순서로 세 개의 꼭짓점을 읽으므로 이 호출은 삼각형(0, 1, 2) 및 (3, 4, 5)를 그립니다
문제점
여기서 알 수 있듯이 꼭짓점 버퍼에는 위치 0 및 4, 2 및 5의 중복 데이터가 포함됩니다. 두 삼각형이 두 개의 공통 꼭짓점을 공유하기 때문에 의미가 있습니다.
중복의 비용
•
메모리 낭비
•
GPU로 전송하는 데이터 양 증가
•
대역폭 낭비
•
정점 셰이더 실행 횟수 증가
구체적인 예시
인덱싱을 사용하여 두 개의 삼각형 그리기
이 중복 데이터는 낭비되며 인덱스 버퍼를 사용하여 꼭짓점 버퍼를 압축할 수 있습니다.
인덱스 버퍼의 이점
메모리 절약
•
더 작은 꼭짓점 버퍼는 그래픽 어댑터로 보내야 하는 꼭짓점 데이터의 양을 줄입니다
정점 캐시 효율
•
더욱 중요한 것은 인덱스 버퍼를 사용하면 어댑터가 꼭짓점 캐시에 꼭짓점을 저장할 수 있습니다
•
그리는 기본 형식에 최근에 사용한 꼭짓점이 포함되어 있으면 꼭짓점 버퍼에서 읽는 대신 캐시에서 해당 꼭짓점을 가져올 수 있으므로 성능이 크게 향상됩니다
인덱스 버퍼 구조
각 고유 꼭짓점을 꼭짓점 버퍼에 한 번만 저장해야 하므로 인덱스 버퍼가 꼭짓점 버퍼로 '인덱스'됩니다. 다음 다이어그램은 이전 그리기 시나리오에 대한 인덱싱된 접근 방식을 보여줍니다.
용어 정리
•
VB 인덱스: 정점 버퍼 내의 특정 꼭짓점을 참조하는 값
•
IB 인덱스: 인덱스 버퍼의 인덱스
•
정점 버퍼: 꼭짓점 배열로 간주
•
인덱스 버퍼: VB 인덱스 값을 저장
주의하지 않으면 매우 빠르게 혼동될 수 있으므로 사용 중인 어휘에 대해 명확히 확인해야 합니다. 즉, VB 인덱스 값이 꼭짓점 버퍼로 인덱싱되고, IB 인덱스 값이 인덱스 버퍼에 인덱스되고, 인덱스 버퍼 자체가 VB 인덱스 값을 저장합니다.
그리기 호출
모든 인수의 의미는 다음 그리기 시나리오에 대해 길이에 따라 설명됩니다. 지금은 이 호출이 Direct3D에 인덱스 버퍼 내의 위치 0부터 시작하여 두 개의 삼각형이 포함된 삼각형 목록을 렌더링하도록 다시 지시합니다. 이 호출은 이전과 정확히 동일한 순서로 동일한 두 개의 삼각형을 그려서 적절한 시계 방향 방향을 보장합니다.
Part 2: 인덱스 버퍼 만들기
인덱스 버퍼 생성 과정
인덱스 버퍼를 초기화하려면 다음 단계를 수행합니다:
1단계: 인덱스 데이터 준비
•
인덱스 정보가 포함된 배열을 만듭니다
•
삼각형을 구성하는 정점 인덱스의 순서 결정
•
시계 방향 또는 반시계 방향 규칙 준수
2단계: 버퍼 설명 생성
•
D3D11_BUFFER_DESC 구조를 입력하여 버퍼 설명을 만듭니다
•
D3D11_BIND_INDEX_BUFFER 플래그를 BindFlags 멤버에 전달
•
버퍼 크기를 바이트 단위로 ByteWidth 멤버에 전달
3단계: 서브리소스 데이터 설명
•
D3D11_SUBRESOURCE_DATA 구조를 입력
•
pSysMem 멤버는 1단계에서 만든 인덱스 데이터를 직접 가리킴
4단계: 버퍼 생성
•
초기화할 ID3D11Buffer 인터페이스에 대한 D3D11_BUFFER_DESC 구조체, D3D11_SUBRESOURCE_DATA 구조체 및 포인터의 주소를 전달하면서 ID3D11Device::CreateBuffer를 호출
인덱스 버퍼 생성 코드
다음 코드 예제에서는 인덱스 버퍼를 만드는 방법을 보여줍니다.
ID3D11Buffer *g_pIndexBuffer = NULL;
// Create indices.
unsigned int indices[] = { 0, 1, 2 };
// Fill in a buffer description.
D3D11_BUFFER_DESC bufferDesc;
bufferDesc.Usage = D3D11_USAGE_DEFAULT;
bufferDesc.ByteWidth = sizeof( unsigned int ) * 3;
bufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER;
bufferDesc.CPUAccessFlags = 0;
bufferDesc.MiscFlags = 0;
// Define the resource data.
D3D11_SUBRESOURCE_DATA InitData;
InitData.pSysMem = indices;
InitData.SysMemPitch = 0;
InitData.SysMemSlicePitch = 0;
// Create the buffer with the device.
hr = g_pd3dDevice->CreateBuffer( &bufferDesc, &InitData, &g_pIndexBuffer );
if( FAILED( hr ) )
return hr;
// Set the buffer.
g_pd3dContext->IASetIndexBuffer( g_pIndexBuffer, DXGI_FORMAT_R32_UINT, 0 );
C++
복사
코드 분석
기본 원리
인덱스 버퍼도 버퍼 리소스의 한 종류이므로 기본적인 생성 방식은 버텍스 버퍼와 마찬가지입니다. 다른 점은 바인드 플래그에 인덱스 버퍼로써 바인드한다는 것을 알려주는 D3D11_BIND_INDEX_BUFFER를 설정해주면 됩니다.
인덱스 데이터 타입
또한 인덱스 버퍼는 정수(UINT 등)의 단순한 배열이기에 데이터 구조체를 만들 필요는 없습니다.
지원하는 인덱스 포맷
•
DXGI_FORMAT_R32_UINT: 32비트 부호 없는 정수 (0 ~ 4,294,967,295)
•
DXGI_FORMAT_R16_UINT: 16비트 부호 없는 정수 (0 ~ 65,535)
포맷 선택 가이드
IASetIndexBuffer 함수
void IASetIndexBuffer(
ID3D11Buffer *pIndexBuffer, // 인덱스 버퍼
DXGI_FORMAT Format, // 인덱스 포맷
UINT Offset // 시작 오프셋 (바이트)
);
C++
복사
매개변수 설명
•
pIndexBuffer: 바인딩할 인덱스 버퍼
•
Format: 인덱스 데이터 포맷 (R32_UINT 또는 R16_UINT)
•
Offset: 버퍼 시작부터의 오프셋 (일반적으로 0)
Part 3: 상수 버퍼의 개념
상수 버퍼란?
상수 버퍼의 역할
상수 버퍼는 그릴 때 셰이더 단계로 전송할 데이터를 나타냅니다. 일반적으로 모델 뷰 투영 행렬이나 색상, 슬라이더 등과 같은 특정 변수 데이터를 여기에 넣습니다.
상수 버퍼에 저장하는 데이터
•
Transform 정보: World, View, Projection 행렬
•
Material 속성: 색상, 광택, 투명도
•
조명 정보: 광원 위치, 색상, 강도
•
시간 값: 애니메이션, 파티클 효과
•
사용자 정의 파라미터: 게임 로직 관련 값
상수 버퍼 vs 일반 버퍼
상수 버퍼의 필요성
동적 데이터 전달
•
매 프레임 변경되는 데이터를 효율적으로 전달
•
오브젝트마다 다른 Transform 정보 제공
•
Material 속성을 런타임에 변경
셰이더 유연성
•
하나의 셰이더로 다양한 효과 구현
•
파라미터 조정으로 다양한 결과 생성
•
실시간 파라미터 튜닝 가능
성능 최적화
•
작은 데이터를 빠르게 전송
•
GPU 캐시 효율 향상
•
셰이더 재컴파일 없이 데이터 변경
Part 4: 상수 버퍼 만들기
상수 버퍼 생성 과정
상수 버퍼를 초기화하려면 다음 단계를 수행합니다:
1단계: 구조체 정의
•
꼭짓점 셰이더 상수 데이터를 설명하는 구조를 정의합니다
2단계: 메모리 할당
•
1단계에서 정의한 구조체에 대한 메모리를 할당합니다
•
꼭짓점 셰이더 상수 데이터로 이 버퍼를 채웁니다
•
malloc 또는 new를 사용하여 메모리를 할당하거나 스택에서 구조체에 대한 메모리를 할당할 수 있습니다
3단계: 버퍼 설명 생성
•
D3D11_BUFFER_DESC 구조를 입력하여 버퍼 설명을 만듭니다
•
D3D11_BIND_CONSTANT_BUFFER 플래그를 BindFlags 멤버에 전달
•
상수 버퍼 설명 구조체의 크기를 바이트 단위로 ByteWidth 멤버에 전달
중요
D3D11_BIND_CONSTANT_BUFFER 플래그는 다른 플래그와 결합할 수 없습니다.
4단계: 서브리소스 데이터 설명
•
D3D11_SUBRESOURCE_DATA 구조를 입력하여 하위 리소스 데이터 설명을 만듭니다
•
D3D11_SUBRESOURCE_DATA 구조체의 pSysMem 멤버는 2단계에서 만든 꼭짓점 셰이더 상수 데이터를 직접 가리킵니다
5단계: 버퍼 생성
•
초기화할 ID3D11Buffer 인터페이스에 대한 D3D11_BUFFER_DESC 구조체, D3D11_SUBRESOURCE_DATA 구조체 및 포인터의 주소를 전달하면서 ID3D11Device::CreateBuffer를 호출합니다
상수 버퍼 생성 코드
이러한 코드 예제에서는 상수 버퍼를 만드는 방법을 보여줍니다. 이 예제에서는 g_pd3dDevice가 유효한 ID3D11Device 개체이고 g_pd3dContext가 유효한 ID3D11DeviceContext 개체라고 가정합니다.
ID3D11Buffer* g_pConstantBuffer11 = NULL;
// Define the constant data used to communicate with shaders.
struct VS_CONSTANT_BUFFER
{
XMFLOAT4X4 mWorldViewProj;
XMFLOAT4 vSomeVectorThatMayBeNeededByASpecificShader;
float fSomeFloatThatMayBeNeededByASpecificShader;
float fTime;
float fSomeFloatThatMayBeNeededByASpecificShader2;
float fSomeFloatThatMayBeNeededByASpecificShader3;
};
// Supply the vertex shader constant data.
VS_CONSTANT_BUFFER VsConstData;
VsConstData.mWorldViewProj = {...};
VsConstData.vSomeVectorThatMayBeNeededByASpecificShader = XMFLOAT4(1,2,3,4);
VsConstData.fSomeFloatThatMayBeNeededByASpecificShader = 3.0f;
VsConstData.fTime = 1.0f;
VsConstData.fSomeFloatThatMayBeNeededByASpecificShader2 = 2.0f;
VsConstData.fSomeFloatThatMayBeNeededByASpecificShader3 = 4.0f;
// Fill in a buffer description.
D3D11_BUFFER_DESC cbDesc;
cbDesc.ByteWidth = sizeof( VS_CONSTANT_BUFFER );
cbDesc.Usage = D3D11_USAGE_DYNAMIC;
cbDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
cbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
cbDesc.MiscFlags = 0;
cbDesc.StructureByteStride = 0;
// Fill in the subresource data.
D3D11_SUBRESOURCE_DATA InitData;
InitData.pSysMem = &VsConstData;
InitData.SysMemPitch = 0;
InitData.SysMemSlicePitch = 0;
// Create the buffer.
hr = g_pd3dDevice->CreateBuffer( &cbDesc, &InitData,
&g_pConstantBuffer11 );
if( FAILED( hr ) )
return hr;
// Set the buffer.
g_pd3dContext->VSSetConstantBuffers( 0, 1, &g_pConstantBuffer11 );
C++
복사
코드 분석
Usage 플래그
cbDesc.Usage = D3D11_USAGE_DYNAMIC;
cbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
C++
복사
D3D11_USAGE_DYNAMIC
•
CPU에서 자주 업데이트하는 버퍼에 최적화
•
Map/Unmap을 통한 빠른 데이터 업데이트
•
매 프레임 변경되는 상수 버퍼에 권장
Usage 옵션 비교
VSSetConstantBuffers 함수
void VSSetConstantBuffers(
UINT StartSlot, // 시작 슬롯 번호
UINT NumBuffers, // 버퍼 개수
ID3D11Buffer *const *ppConstantBuffers // 버퍼 배열
);
C++
복사
다른 셰이더 스테이지용 함수
•
PSSetConstantBuffers(): Pixel Shader
•
GSSetConstantBuffers(): Geometry Shader
•
HSSetConstantBuffers(): Hull Shader
•
DSSetConstantBuffers(): Domain Shader
•
CSSetConstantBuffers(): Compute Shader
HLSL 상수 버퍼 정의
다음에서는 연결된 HLSL cbuffer 정의를 보여줍니다.
cbuffer 선언 규칙
레지스터 슬롯
패킹 규칙
•
각 변수는 16바이트(float4) 경계에 정렬
•
구조체는 16바이트 배수로 패딩
예시
Part 5: 상수 버퍼 업데이트
Map/Unmap을 사용한 업데이트
동적 상수 버퍼 업데이트
void UpdateConstantBuffer(ID3D11DeviceContext* context,
ID3D11Buffer* constantBuffer,
const MyConstantData& data)
{
D3D11_MAPPED_SUBRESOURCE mappedResource;
// 버퍼를 CPU 메모리에 매핑
HRESULT hr = context->Map(
constantBuffer,
0,
D3D11_MAP_WRITE_DISCARD, // 기존 데이터 폐기
0,
&mappedResource
);
if (SUCCEEDED(hr))
{
// 데이터 복사
memcpy(mappedResource.pData, &data, sizeof(MyConstantData));
// 매핑 해제
context->Unmap(constantBuffer, 0);
}
}
C++
복사
Map 플래그 설명
•
D3D11_MAP_WRITE_DISCARD: 기존 데이터 무시, 가장 빠름
•
D3D11_MAP_WRITE_NO_OVERWRITE: 기존 데이터 유지
•
D3D11_MAP_WRITE: 일반 쓰기
UpdateSubresource를 사용한 업데이트
DEFAULT 버퍼 업데이트
void UpdateConstantBufferDefault(ID3D11DeviceContext* context,
ID3D11Buffer* constantBuffer,
const MyConstantData& data)
{
// UpdateSubresource로 직접 업데이트
context->UpdateSubresource(
constantBuffer,
0,
nullptr,
&data,
0,
0
);
}
C++
복사
Map vs UpdateSubresource
Part 6: 실전 사용 예시
Transform 상수 버퍼
// 상수 버퍼 구조체 정의
struct TransformCB
{
Matrix world;
Matrix view;
Matrix projection;
};
// 생성
D3D11_BUFFER_DESC cbDesc = {};
cbDesc.ByteWidth = sizeof(TransformCB);
cbDesc.Usage = D3D11_USAGE_DYNAMIC;
cbDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
cbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
ID3D11Buffer* transformCB = nullptr;
device->CreateBuffer(&cbDesc, nullptr, &transformCB);
// 매 프레임 업데이트
void Render()
{
TransformCB cbData;
cbData.world = GetWorldMatrix();
cbData.view = GetViewMatrix();
cbData.projection = GetProjectionMatrix();
D3D11_MAPPED_SUBRESOURCE mapped;
context->Map(transformCB, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped);
memcpy(mapped.pData, &cbData, sizeof(TransformCB));
context->Unmap(transformCB, 0);
context->VSSetConstantBuffers(0, 1, &transformCB);
// Draw...
}
C++
복사
대응하는 HLSL
Material 상수 버퍼
struct MaterialCB
{
Vector4 albedoColor;
float metallic;
float roughness;
float ao;
float padding;
};
// Material 변경 시 업데이트
void SetMaterial(const Material& material)
{
MaterialCB cbData;
cbData.albedoColor = material.albedoColor;
cbData.metallic = material.metallic;
cbData.roughness = material.roughness;
cbData.ao = material.ao;
context->UpdateSubresource(materialCB, 0, nullptr, &cbData, 0, 0);
context->PSSetConstantBuffers(1, 1, &materialCB);
}
C++
복사
대응하는 HLSL
Part 7: 최적화 및 모범 사례
상수 버퍼 패킹
16바이트 정렬 규칙
// 나쁜 예: 비효율적인 패킹
struct BadCB
{
float value1; // 0-3 bytes
// 12 bytes 패딩!
float4 vector; // 16-31 bytes
float value2; // 32-35 bytes
// 12 bytes 패딩!
// 총 48 bytes
};
// 좋은 예: 효율적인 패킹
struct GoodCB
{
float4 vector; // 0-15 bytes
float value1; // 16-19 bytes
float value2; // 20-23 bytes
float padding1; // 24-27 bytes
float padding2; // 28-31 bytes
// 총 32 bytes (33% 절약)
};
C++
복사
상수 버퍼 크기 제한
하드웨어 제한
•
최대 크기: 64KB (65,536 bytes)
•
권장 크기: 256 bytes 이하
•
큰 데이터는 여러 버퍼로 분할
버퍼 분할 전략
// 업데이트 빈도에 따라 분할
cbuffer PerFrame : register(b0) // 매 프레임
{
float4x4 view;
float4x4 projection;
float time;
};
cbuffer PerObject : register(b1) // 오브젝트마다
{
float4x4 world;
};
cbuffer PerMaterial : register(b2) // Material마다
{
float4 albedoColor;
float metallic;
float roughness;
};
C++
복사
업데이트 최적화
배칭
// 나쁜 예: 매번 업데이트
for (auto& obj : objects)
{
UpdateConstantBuffer(obj.transform);
Draw(obj);
}
// 좋은 예: 같은 데이터는 한 번만
UpdateConstantBuffer(sharedData);
for (auto& obj : objects)
{
if (obj.needsUpdate)
{
UpdateConstantBuffer(obj.transform);
}
Draw(obj);
}
C++
복사
변경 감지
class ConstantBufferCache
{
std::vector<byte> cachedData;
bool NeedsUpdate(const void* newData, size_t size)
{
if (cachedData.size() != size) return true;
return memcmp(cachedData.data(), newData, size) != 0;
}
void Update(ID3D11Buffer* cb, const void* newData, size_t size)
{
if (NeedsUpdate(newData, size))
{
// 실제로 변경된 경우에만 업데이트
UpdateConstantBuffer(cb, newData, size);
cachedData.assign((byte*)newData, (byte*)newData + size);
}
}
};
C++
복사
결론
핵심 요약
Index Buffer (인덱스 버퍼)
•
정점 재사용으로 메모리 절약 (평균 60-85%)
•
정점 캐시 효율 향상으로 성능 개선
•
대역폭 절약
•
모든 복잡한 메쉬에 필수적
Constant Buffer (상수 버퍼)
•
셰이더에 동적 데이터 전달
•
Transform, Material, Lighting 등 다양한 용도
•
매 프레임 효율적인 업데이트 가능
•
16바이트 정렬 규칙 준수 필요
실전 적용 가이드
인덱스 버퍼
1.
모든 정적 메쉬에 인덱스 버퍼 사용
2.
16비트 vs 32비트 인덱스 적절히 선택
3.
정점 캐시 최적화 고려
4.
삼각형 Strip보다 List가 범용적
상수 버퍼
1.
업데이트 빈도에 따라 버퍼 분할
2.
16바이트 정렬 규칙 준수
3.
DYNAMIC Usage로 빠른 업데이트
4.
변경 감지로 불필요한 업데이트 방지
5.
64KB 크기 제한 주의
성능 최적화
1.
인덱스 버퍼로 메모리 사용량 최소화
2.
상수 버퍼 패킹으로 대역폭 절약
3.
배칭으로 상수 버퍼 업데이트 횟수 감소
4.
캐싱으로 중복 업데이트 방지
인덱스 버퍼와 상수 버퍼는 현대 3D 그래픽 프로그래밍의 필수 요소입니다. 적절한 사용과 최적화를 통해 메모리 효율성과 렌더링 성능을 크게 향상시킬 수 있습니다.





