ConstantBuffer

HLSL이나 GLSL, Vulkan 같은 그래픽 API에서 셰이더에 전달되는 데이터를 담는 버퍼이다.

ConstantBuffer의 역할

중복 데이터 관리

  • 그래픽 렌더링에서 여러 Mesh나 Material, FX를 그릴 때 계속 사용되는 공통 데이터(조명 정보, 카메라 변환 행렬, 전역 변수 등)를 한 곳에 모아두고 셰이더에 전달한다.
  • 뷰 행렬(View Matrix), 투영 행렬(Projection Matrix), 조명 관련 매개변수, 프레임 단위로만 달라지는 전역 변수 등이 이에 해당한다.

데이터 동기화/성능 최적화

  • CPU에서 셰이더로 전달해야 하는 작은 데이터들을 개별적으로 전달하면 오버헤드가 발생한다.
  • Constant Buffer를 사용해 한꺼번에 묶어서 전송하면, API 호출 횟수메모리 접근 비용을 줄일 수 있다.
  • 프레임마다 혹은 이벤트가 발생할 때마다 변동 사항을 빠르고 효율적으로 반영할 수 있다.

셰이더 접근 방식 표준화

  • HLSL(DirectX)이나 GLSL(OpenGL) 등에서 상수 버퍼는 표준 인터페이스(Uniform Block, cbuffer 등)로 정의 가능.
  • 셰이더 내부에서는 “Uniform/Constant”으로 인식되어, Vertex Shader, Pixel Shader 등 여러 파이프라인 스테이지에서 동일하게 참조할 수 있다.

ConstantBuffer의 필요성

공통 데이터 전달

  • 3D(또는 2D) 렌더링에서는 여러 정점에 대해 반복적으로 사용되는 정보가 많다.
    • 예: 월드 변환 행렬(World Matrix), 뷰/투영 행렬(View/Projection), 조명 정보, 화면 해상도, 시간 값 등.
  • 이런 전역 데이터를 각 정점마다 별도로 전달하거나 텍스처 리소스로 사용하는 것은 비효율적이다.
  • 전용 버퍼(상수 버퍼)에 담아 한 번에 넘길 필요가 있다.

빈번한 업데이트 & 효율

  • 게임과 그래픽 애플리케이션에서는 매 프레임 물체 위치, 애니메이션 시간, 조명 세기 등이 바뀔 수 있다.
  • 이런 자주 변하는(하지만 정점 수만큼 방대하지는 않은) 데이터를 CPU→GPU로 일괄 전달해야 하는데, 개별 변수를 일일이 전송하면 오버헤드가 커진다.
  • 상수 버퍼는 이러한 자잘한 데이터를 한 덩어리로 묶어서 GPU에 전송하는 구조로 만들어져 있어, API 호출 횟수와 메모리 접근 비용을 낮출 수 있습니다.

파이프라인 표준화

  • DirectX(특히 HLSL)는 상수 버퍼를 b0, b1, b2 같은 슬롯으로 관리하여, Vertex Shader, Pixel Shader 등 여러 셰이더 스테이지에서 공통 데이터에 쉽게 접근 가능하다.
  • 이를 통해 여러 스테이지 간에 공유되는 전역 변수를 구조적으로 관리할 수 있다.

HLSL에서의 사용법

cbuffer

cbuffer FrameData : register(b0)
{
    float4x4 gWorldViewProj;   // 행렬
    float4   gAmbientColor;    // 앰비언트 조명 색
    float3   gLightDir;        // 광원 방향
    float    gTime;            // 현재 시간 (예: 애니메이션 등)
};
  • cbuffer 키워드 뒤에 이름(FrameData)을 지정

  • : register(b0)는 이 상수 버퍼가 GPU 바인딩 슬롯의 b0 레지스터에 할당됨을 의미
    • DirectX에서는 Vertex/Pixel 등 여러 셰이더 스테이지에서 각기 최대 14~15개(DX11 기준)의 cbuffer 슬롯(b0, b1, b2…)을 사용할 수 있음
  • 내부에는 HLSL 타입(float4x4, float3, float4 등)들을 선언

정렬(Alignment) 규칙

  • HLSL의 cbuffer는 내부적으로 16바이트(128비트) 경계 기준으로 정렬된다. (패킹 규칙)
    • ex) float4 하나로 16바이트를 차지
    • float3는 12바이트지만, 실제 cbuffer 내에서는 16바이트 공간을 차지하고, 다음 멤버는 새 16바이트 슬롯에서 시작
  • 일반적으로 구조체로 묶을 때 16바이트를 기준으로 생각해야 한다.

C++에서의 사용법

ConstantBuffer생성

D3D11_BUFFER_DESC ConstantBufferDesc = {};
ConstantBufferDesc.Usage = D3D11_USAGE_DEFAULT;         // 또는 D3D11_USAGE_DYNAMIC
ConstantBufferDesc.ByteWidth = sizeof(Transform); // 16바이트 배수 권장
ConstantBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; // 만들어질때 용도(Constant) 지정
ConstantBufferDesc.CPUAccessFlags = 0;         // (Dynamic일 경우 D3D11_CPU_ACCESS_WRITE)

if (FAILED(_DEVICE->CreateBuffer(&ConstantBufferDesc, nullptr, ConstantBuffer.GetAddressOf())))
{
    return E_FAIL;
}

용도 지정

  • BindFlags = D3D11_BIND_CONSTANT_BUFFER: 이 버퍼가 상수 버퍼로 사용됨을 Direct3D에 선언한다.

크기(ByteWidth) 설정

  • sizeof(Transform) : 버퍼에 넣을 구조체(Transform)의 크기
  • 주의: 일반적으로 16바이트(16-byte) 배수로 맞추는 것이 좋다. (HLSL cbuffer 정렬 이슈를 피하기 위해)

CPUAccessFlags & Usage

  • D3D11_USAGE_DYNAMIC + D3D11_CPU_ACCESS_WRITE 로 지정
  • 이렇게 하면 매 프레임마다(또는 필요할 때마다) CPU가 Map/Unmap을 통해 버퍼 내용을 쉽게 업데이트할 수 있다.

구조체(Transform) 예시

struct Transform
{
    Vector3 Position; // (x, y, z)
    float   pad;      // 패딩:16바이트 정렬 맞추기용(필요시)
    // 회전, 스케일 등이 있다면 추가
};

HLSL 상수 버퍼(cbuffer)는 16바이트 단위로 패킹(packing)되므로, float3 뒤에는 패딩(float) 등을 넣어 16바이트 경계를 맞춰주는 것이 좋다.

태그: ,

카테고리:

업데이트:

댓글남기기