DirecX12 | Lecture 7. Constant Buffer (인프런)
DirectX12 강의 7. Constant Buffer를 정리한 글입니다.
강의 정보 🎓
- 강의명: C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈 - Part2: 게임 수학과 DirectX12
- 플랫폼: 인프런
- 현재 섹션: Lecture 7 – Constant Buffer
- 주요 내용: DirectX 12 Constant Buffer를 활용하여 삼각형 위치, 색상을 수정하여 그렸다.
1. Default.hlsli에 Test_B0, Test_B1 constant buffer 작성한다.
2. RootSignature init에 추가
1
2
3
4
5
CD3DX12_ROOT_PARAMETER rootParam[2];
rootParam[0].InitAsConstantBufferView(0); // 0번 -> b0 -> CBV
rootParam[1].InitAsConstantBufferView(1); // 1번 -> b1 -> CBV
D3D12_ROOT_SIGNATURE_DESC rootSignatureDesc = CD3DX12_ROOT_SIGNATURE_DESC((2, rootParam); // Default 에서 param을 넘기도록 수정
InitAsConstantBufferView
파라미터- 레지스터 번호
- 레지스터 공간 -> 동일한 register 번호를 다른 리소스 그룹에 재사용 가능하게 한다.
- SHADER_VISIBILITY → 볼 수 있는 셰이더 단계를 설정한다.
3. CommandQueueLayer → RenderBegin에 추가
1
2
ComPtr<ID3D12RootSignature> rootSignature = Engine::GetInstance().GetRootSignatureLayer()->GetRootSignature();
mCommandList->SetGraphicsRootSignature(rootSignature.Get());
4. Mesh → Render에서 다음을 작성하여도 작동하지 않을 것이다
commandList->SetComputeRootConstantBufferView(0, …)
1) Buffer에다가 데이터 세팅 (리소스 생성)→ 즉시 일어난다. (Device)
2) Buffer의 주소를 register에다가 전송 → 나중에 일어난다. (CommandList)
실행 시점이 다르기 때문에 CommandList의 명령이 실행되는 시점에는 Buffer의 데이터가 의도한 값이 맞을지 생각을 하고 구현해야 됨!
5. ConstantBuffer 클래스 작성
- ConstantBuffer를 관리 → 하나의 CBV
- (size + 255) & ~255; → 하위 8비트를 0으로 만들어서 256 배수로 만든다.
- 일단 Engine에서 관리 및 초기화 호출
mConstantBuffer->Init(sizeof(Transform), 256);
- CBV는 64kb = 4096개의 백터 까지 보관 가능, 256개 슬롯을 사용하면 Transform 은 최대 64바이트까지 허용된다.
- CommandQueue → RenderBegin에서 ConstantBuffer의 Clear 호출 ( 인덱스를 0으로 초기화한다. )
- mCurrentIndex를 통해서 버퍼가 덮여 쓰이는 것을 방지 → Ring-buffer 방식
- CBV는 256개의 인덱스를 만들었는데 4개만 쓰고 0, 2는 b0을 1, 3은 b1을 사용한다.
6. Render에서 PushData 호출
1
2
Engine::GetInstance().GetConstantBuffer()->PushData(0, &mTransform, sizeof(Transform)); // b0에 mTransform 정보를 넣음
Engine::GetInstance().GetConstantBuffer()->PushData(1, &mTransform, sizeof(Transform)); // b1에 mTransform 정보를 넣음
7. Client 프로젝트 Game::Update 수정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
Transform t;
t.offset = Vec4(0.75f, 0.0f, 0.0f, 0.0f);
mesh->SetTransform(t);
mesh->Render();
}
{
Transform t;
t.offset = Vec4(0.0f, 0.75f, 0.0f, 0.0f);
mesh->SetTransform(t);
mesh->Render();
}
ConstantBuffer와 CBV 활용 방식 정리
- 하나의 ConstantBuffer(ID3D12Resource)를 여러 인덱스로 쪼개서 관리한다.
- 실제로 GPU에 올라가는 리소스는 하나의 CBV용 리소스지만,
- 내부를
256-byte aligned
단위로 잘라서 마치 슬롯처럼 구분하여 사용함. - 만약 이 인덱스 개념 없이 매번 같은 위치에 데이터를 쓰면, 여러 번의 Render 호출 시 최종 값만 적용되어, 모든 오브젝트가 동일한 Transform으로 그려지는 문제가 발생할 가능성이 생긴다.
- 각 인덱스에는
Transform t
구조체의 데이터를 복사해 넣는다. - 첫 번째
Render()
시:PushData(0, &t)
→ CBV 슬롯 0 → 루트 파라미터 b0에 연결PushData(1, &t)
→ CBV 슬롯 1 → 루트 파라미터 b1에 연결
- 이후
Transform t
값을 수정한다.- 만약 인덱스 없이 같은 슬롯(CBV 0 같은 곳)에 계속 쓰면, 덮어쓰기가 발생함.
- 이 경우 GPU는 항상 가장 마지막 값만 보게 되어, 서로 다른 Transform이 적용되지 않음.
참고: 아래 코드처럼 단순 2슬롯을 번갈아 쓰는 경우도 프레임 내에서 여러 오브젝트를 그리면 같은 문제 발생 가능:
1 2 3
mCurrentIndex++; if (mCurrentIndex > 1) mCurrentIndex = 0;
→ 두 개의 슬롯만으로는 동일 프레임 내 여러 오브젝트를 그릴 수 없음.
- 두 번째
Render()
시:PushData(0, &t)
→ CBV 슬롯 2 → 루트 파라미터 b0에 연결PushData(1, &t)
→ CBV 슬롯 3 → 루트 파라미터 b1에 연결
- 결과적으로:
- 첫 번째
Render()
는t
수정 전의 오브젝트 - 두 번째
Render()
는t
수정 후의 오브젝트 - → 각기 다른 위치에 그려지는 두 개의 삼각형이 렌더링됨 ✅
- 첫 번째
참고
Device 함수들 → 당장 뭔가가 일어난다.
CommandQueue → 나중에 몰아서 일어난다. ( 실행 시점 )
모델 계층 구조에서 CBV가 자식마다 필요한 이유
Transform은 계층적으로 누적된다
- 자식 노드는 자신의 로컬 변환(Local Transform)을 가지고 있다.
- 렌더링 시에는
부모의 월드 변환 × 자신의 로컬 변환
을 계산해서 월드 행렬(World Transform) 생성한다.
1
XMMATRIX world = XMMatrixMultiply(child->local, parent->world);
자식마다 월드 행렬이 다르다
- 부모의 위치가 (0, 1, 0)일 때:
자식 로컬 위치 최종 위치 (월드) 자식 1 (1, 0, 0) (1, 1, 0) 자식 2 (0, 0, 1) (0, 1, 1) → 결과적으로 각 자식의 월드 행렬이 다르며,
→ 셰이더에서 참조할 값이 각각 달라진다.
그래서 각 자식은 서로 다른 CBV 슬롯을 사용해야 한다
- CBV는 GPU에 전달할
Transform
데이터를 담는 버퍼 - 만약 하나의 CBV만 사용하면, 여러 자식이 같은 버퍼에 덮어쓰며 모두 같은 위치에 렌더링되는 문제 발생한다.
- 따라서
PushData()
로 각 자식의 Transform을 독립된 슬롯에 복사해야 한다.
1 2
// 각 자식 노드마다 constantBuffer->PushData(0, &worldMatrix, sizeof(Transform)); // 슬롯 인덱스 증가
- CBV는 GPU에 전달할
CBV는 Transform 데이터 전송용이며, 위치가 다르면 반드시 나눠야 한다
Transform
≠Mesh
≠Material
- 동일한 메시(geometry)를 공유하더라도, 위치가 다르면
CBV
는 분리해야 함
Descriptor Table 예시
1
2
3
4
[ Descriptor Heap ]
| CBV 0 | CBV 1 | CBV 2 | CBV 3 | CBV 4 | ...
↑ ↑ ↑ ↑ ↑
본체 팔 다리 머리 꼬리
Root Descriptor(Ring-Buffer) 예시
1
2
3
4
ConstantBuffer 리소스 (하나)
[ 슬롯 0 ][ 슬롯 1 ][ 슬롯 2 ][ 슬롯 3 ][ 슬롯 4 ] ...
↑ ↑ ↑ ↑ ↑
본체 팔 다리 머리 꼬리
CBV 5개 각각에 로컬 행렬을 가짐 vs 하나의 CBV에 5개의 슬롯으로 표현됨
결론
모델에서 부모-자식 관계가 있을 때, 자식마다 CBV가 필요한 이유는 각각의 최종 월드 변환 행렬이 다르기 때문이며, 이를 GPU에 전달하기 위해 각 자식은 독립된 CBV 슬롯을 사용해야 한다.