포스트

DirecX12 | Lecture 7. Constant Buffer (인프런)

DirectX12 강의 7. Constant Buffer를 정리한 글입니다.

DirecX12 | Lecture 7. 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 파라미터
    1. 레지스터 번호
    2. 레지스터 공간 -> 동일한 register 번호를 다른 리소스 그룹에 재사용 가능하게 한다.
    3. 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 활용 방식 정리

  1. 하나의 ConstantBuffer(ID3D12Resource)를 여러 인덱스로 쪼개서 관리한다.
    • 실제로 GPU에 올라가는 리소스는 하나의 CBV용 리소스지만,
    • 내부를 256-byte aligned 단위로 잘라서 마치 슬롯처럼 구분하여 사용함.
    • 만약 이 인덱스 개념 없이 매번 같은 위치에 데이터를 쓰면, 여러 번의 Render 호출 시 최종 값만 적용되어, 모든 오브젝트가 동일한 Transform으로 그려지는 문제가 발생할 가능성이 생긴다.
  2. 각 인덱스에는 Transform t 구조체의 데이터를 복사해 넣는다.
  3. 첫 번째 Render() 시:
    • PushData(0, &t)CBV 슬롯 0 → 루트 파라미터 b0에 연결
    • PushData(1, &t)CBV 슬롯 1 → 루트 파라미터 b1에 연결
  4. 이후 Transform t 값을 수정한다.
    • 만약 인덱스 없이 같은 슬롯(CBV 0 같은 곳)에 계속 쓰면, 덮어쓰기가 발생함.
    • 이 경우 GPU는 항상 가장 마지막 값만 보게 되어, 서로 다른 Transform이 적용되지 않음.
    • 참고: 아래 코드처럼 단순 2슬롯을 번갈아 쓰는 경우도 프레임 내에서 여러 오브젝트를 그리면 같은 문제 발생 가능:

      1
      2
      3
      
        mCurrentIndex++;
        if (mCurrentIndex > 1)
            mCurrentIndex = 0;
      

      → 두 개의 슬롯만으로는 동일 프레임 내 여러 오브젝트를 그릴 수 없음.

  5. 두 번째 Render() 시:
    • PushData(0, &t)CBV 슬롯 2 → 루트 파라미터 b0에 연결
    • PushData(1, &t)CBV 슬롯 3 → 루트 파라미터 b1에 연결
  6. 결과적으로:
    • 첫 번째 Render()t 수정 전의 오브젝트
    • 두 번째 Render()t 수정 후의 오브젝트
    • → 각기 다른 위치에 그려지는 두 개의 삼각형이 렌더링됨 ✅

참고

Device 함수들 → 당장 뭔가가 일어난다.

CommandQueue → 나중에 몰아서 일어난다. ( 실행 시점 )

모델 계층 구조에서 CBV가 자식마다 필요한 이유

  1. Transform은 계층적으로 누적된다

    • 자식 노드는 자신의 로컬 변환(Local Transform)을 가지고 있다.
    • 렌더링 시에는 부모의 월드 변환 × 자신의 로컬 변환을 계산해서 월드 행렬(World Transform) 생성한다.
    1
    
    XMMATRIX world = XMMatrixMultiply(child->local, parent->world);
    
  2. 자식마다 월드 행렬이 다르다

    • 부모의 위치가 (0, 1, 0)일 때:
    자식로컬 위치최종 위치 (월드)
    자식 1(1, 0, 0)(1, 1, 0)
    자식 2(0, 0, 1)(0, 1, 1)

    → 결과적으로 각 자식의 월드 행렬이 다르며,

    셰이더에서 참조할 값이 각각 달라진다.

  3. 그래서 각 자식은 서로 다른 CBV 슬롯을 사용해야 한다

    • CBV는 GPU에 전달할 Transform 데이터를 담는 버퍼
    • 만약 하나의 CBV만 사용하면, 여러 자식이 같은 버퍼에 덮어쓰며 모두 같은 위치에 렌더링되는 문제 발생한다.
    • 따라서 PushData()로 각 자식의 Transform을 독립된 슬롯에 복사해야 한다.
    1
    2
    
    // 각 자식 노드마다
    constantBuffer->PushData(0, &worldMatrix, sizeof(Transform)); // 슬롯 인덱스 증가
    
  4. CBV는 Transform 데이터 전송용이며, 위치가 다르면 반드시 나눠야 한다

    • TransformMeshMaterial
    • 동일한 메시(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 슬롯을 사용해야 한다.


프로젝트 상태 💾

04Result.png

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.