수업


주제

Constant Buffer (상수 버퍼)의 역할과 GPU 활용 흐름 완전 정복
– Vertex Shader 단계에서 CPU → GPU로 데이터를 전달해 객체의 위치를 효율적으로 조정하는 상수 버퍼의 구조, 사용 목적, 구현 방식 전부 설명


개념

Constant Buffer란?

Constant Buffer(상수 버퍼)는 GPU의 쉐이더, 특히 Vertex Shader에서 일정 시간 동안 유지되는 데이터를 전달하기 위해 사용하는 읽기 전용 버퍼다.
주로 위치, 회전, 스케일 같은 객체의 변환 정보, 조명, 카메라 등 자주 바뀌지만 소량인 데이터를 GPU에 전달하는 데 사용된다.

왜 필요한가?

  • VertexBuffer는 한 번 GPU에 올리면 읽기 전용(Immutable)으로 처리된다. 정점을 매번 수정하려면 매 프레임마다 거대한 데이터를 GPU에 다시 복사해야 하므로 매우 비효율적이다.
  • 하지만 캐릭터의 위치처럼 자주 바뀌는 정보는 매 프레임 GPU에 전달해야 한다.
  • 이때 필요한 것이 상수 버퍼(Constant Buffer). 정점 자체는 그대로 두고, 쉐이더에서 offset 값을 더하는 방식으로 도형을 이동시킬 수 있다.
  • 이 구조는 정점 데이터를 공용으로 사용하면서도, 개별 오브젝트의 위치만 다르게 렌더링할 수 있게 해준다.
    → MMO 게임처럼 수많은 객체가 존재할 때도 효율적이다.

용어 정리

  • cbuffer: HLSL에서 Constant Buffer를 선언할 때 사용하는 키워드.
  • register(b0): 상수 버퍼를 쉐이더 레지스터 b0 슬롯에 바인딩.
  • D3D11_USAGE_DYNAMIC: CPU가 자주 쓰고 GPU가 읽는 방식으로 버퍼 사용을 정의.
  • D3D11_CPU_ACCESS_WRITE: CPU가 해당 버퍼에 접근해 데이터를 쓸 수 있도록 허용.
  • Map/Unmap: GPU 버퍼를 CPU가 접근 가능한 메모리로 매핑(Map)하고, 작업 후 해제(Unmap).
  • memcpy: CPU 메모리 데이터를 GPU 버퍼에 복사할 때 사용되는 함수.
  • Vec3: x, y, z로 구성된 3차원 벡터.
  • 16바이트 정렬: GPU 버퍼는 16바이트 단위로 정렬되어야 하기 때문에 padding(float dummy)을 추가해 크기를 맞춘다.

코드 분석

1. 상수 버퍼 선언 (HLSL)

cbuffer TransformData : register(b0)
{
    float4 offset;
}
  • cbuffer TransformData: 상수 버퍼 이름.
  • register(b0): 이 버퍼는 b0 슬롯에 바인딩됨.
  • offset: 외부에서 전달된 이동 벡터. float4 타입으로 쉐이더에서 position과 연산 가능.

2. Vertex Shader에서 상수 버퍼 사용

VS_OUTPUT VS(VS_INPUT input)
{
    VS_OUTPUT output;
    output.position = input.position + offset;
    output.uv = input.uv;
    return output;
}
  • input.position: 정점 버퍼에서 전달된 원래 위치.
  • offset: 외부에서 전달된 이동값을 더해 위치 변경.
  • output.uv: 텍스처 좌표 그대로 전달.
  • 이 방식은 도형의 모양은 고정한 채, 위치만 변화시킨다.

3. CPU 측 상수 버퍼 구조체

struct TransformData
{
    Vec3 offset;
    float dummy; // 16바이트 정렬 맞춤
};
  • offset: 위치를 나타내는 벡터.
  • dummy: 3D 벡터(Vec3)가 12바이트이기 때문에 GPU 요구사항(16바이트)에 맞춰 정렬.

4. Constant Buffer 생성 함수

void Game::CreateConstantBuffer()
{
    D3D11_BUFFER_DESC desc;
    ZeroMemory(&desc, sizeof(desc));
    desc.Usage = D3D11_USAGE_DYNAMIC; // CPU Write + GPU Read
    desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
    desc.ByteWidth = sizeof(TransformData);
    desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;

    HRESULT hr = _device->CreateBuffer(&desc, nullptr, _constantBuffer.GetAddressOf());
    CHECK(hr);
}
  • D3D11_USAGE_DYNAMIC: GPU에서 읽고 CPU에서 자주 쓰는 용도.
  • D3D11_CPU_ACCESS_WRITE: CPU가 데이터를 쓸 수 있도록 설정.
  • ByteWidth: 전달할 구조체 크기와 일치.
  • CreateBuffer: GPU에 버퍼 생성.

5. 클래스 멤버 변수

private:
    TransformData _transformData;
    ComPtr<ID3D11Buffer> _constantBuffer;
  • _transformData: 매 프레임 CPU에서 수정할 위치 데이터.
  • _constantBuffer: GPU에 전달할 상수 버퍼 객체.

6. CPU → GPU 데이터 전달 (Update 함수)

void Game::Update()
{
    _transformData.offset.x += 0.003f;
    _transformData.offset.y += 0.003f;

    D3D11_MAPPED_SUBRESOURCE subResource;
    ZeroMemory(&subResource, sizeof(subResource));

    _deviceContext->Map(_constantBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &subResource);
    ::memcpy(subResource.pData, &_transformData, sizeof(_transformData));
    _deviceContext->Unmap(_constantBuffer.Get(), 0);
}
  • Map: GPU 메모리를 CPU가 접근할 수 있도록 연다.
  • memcpy: CPU 메모리 데이터를 GPU 버퍼에 복사.
  • Unmap: 다시 GPU가 해당 버퍼를 사용할 수 있도록 닫는다.
  • 매 프레임 반복 호출되며 실시간으로 위치 이동 가능.

7. 상수 버퍼 바인딩 (렌더링 시점)

_deviceContext->VSSetShader(_vertexShader.Get(), nullptr, 0);
_deviceContext->VSSetConstantBuffers(0, 1, _constantBuffer.GetAddressOf());
  • 정점 셰이더 설정 후, b0 슬롯에 상수 버퍼를 바인딩.
  • 이후 셰이더 내부에서 offset 변수 사용 가능.

핵심

  • 정점 버퍼는 도형의 기본 형태를 유지하며, 위치 등의 변화는 상수 버퍼로 따로 전달한다.
  • 상수 버퍼를 사용하면 매 프레임마다 변화하는 데이터를 CPU에서 GPU로 효율적으로 전달할 수 있다.
  • Map/Unmap을 통해 GPU 버퍼를 안전하게 업데이트하며, 반드시 짝을 맞춰야 한다.
  • TransformData16바이트 정렬 필수이고, 셰이더의 float4와 정확히 대응되도록 설계되어야 한다.
  • 객체 1000개가 있어도 하나의 VertexBuffer를 공유하고, 각 객체의 위치는 상수 버퍼에서 따로 전달하면 된다.
  • 쉐이더는 마치 함수처럼 상수 버퍼 값을 받아서 계산만 하도록 만든다.

profile
李家네_공부방

0개의 댓글