1. Vertex Buffer(정점 버퍼)

  • 컴퓨터 그래픽스에서 3D 객체를 구성하는 정점(Vertex) 데이터를 저장하는 메모리 공간

    • 쉽게 말해, 3D 객체(정확히는 Primitive)의 뼈대를 정의하는 점들의 집합
  • 정점 데이터를 GPU에 효율적으로 전달하기 위해 사용하는 메모리 공간으로, 프레임 버퍼가 GPU가 화면에 전달하기 위한 공간이였다면 정점 버퍼는 CPU가 GPU에 전달하기 위한 공간

Vertex

  • 3D 그래픽스에서 점(Point)를 의미

  • 3D 객체는 삼각형 Polygon의 집합으로 표현되며, 이 삼각형의 꼭짓점이 바로 Vertex

  • 다음과 같은 정보들을 포함함

    • 위치(Position): 점의 3D 좌표(x, y, z)

    • 법선(Normal): 표면의 방향(조명 계산 등에 사용)

      • Polygon이 아니라 Vertex의 Normal
    • 텍스처 좌표(Texture Coordinates): 텍스처 매핑을 위한 좌표(u, v)

    • 색상(Color): 정점의 색상 정보(RGB + A)

Vertex Buffer의 동작 과정

  1. 정점 버퍼 생성: CPU에서 삼각형의 세 꼭짓점 좌표 등을 준비하여 정점 버퍼에 저장

    • 복잡한 형태의 모델은 모든 정점을 하드코딩하기 어려우니, 실제로는 Blender / Maya 등으로 3D 모델을 생성 후 export한 결과 파일을 정점 버퍼로 로드함

    • ID3D11Device::CreateBuffer()등으로 버퍼 생성

  2. 셰이더와 연결: 정점 버퍼를 GPU에서 돌아가는 프로그램인 셰이더와 연결함

    • 물론, VS로 들어가기 전 정점 버퍼의 내용을 셰이더가 이해할 수 있도록 IA 스테이지에서 Input Layout등을 설정함
  3. GPU에서 렌더링

코드 예제

struct Vertex {
    float x, y, z;
    float r, g, b;
};

Vertex vertices[] = {
    { 0.0f,  0.5f, 0.0f, 1.0f, 0.0f, 0.0f }, // Red
    { 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f }, // Green
    {-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f } // Blue
};

// 정점 버퍼 생성
D3D11_BUFFER_DESC bufferDesc = {};
bufferDesc.Usage = D3D11_USAGE_DEFAULT; // GPU 읽기 / 쓰기 허용
bufferDesc.ByteWidth = sizeof(vertices);
bufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER; // 이 버퍼는 정점 버퍼임을 바인딩

// 실제 데이터는 subresource를 통해 넘겨주기
D3D11_SUBRESOURCE_DATA initData = {};
initData.pSysMem = vertices; // CPU 메모리의 vertices가 가리키는 데이터를 그대로 복사하여 초기화

ID3D11Buffer* vertexBuffer = nullptr;
device->CreateBuffer(&bufferDesc, &initData, &vertexBuffer); // GPU 접근 가능 메모리로 정점 데이터 복사(subresource가 안주어지면 접근 가능 메모리만 생성)

2. Index Buffer(인덱스 버퍼)

  • 정점을 효율적으로 재사용하기 위해 사용하는, 정점의 인덱스를 통해 삼각형을 구성하는 데이터 구조

  • 대부분의 3D 객체는 여러 삼각형이 동일한 정점을 공유함

    • 예를 들어, 정육면체를 표현하기 위해서는 12개의 삼각형을 위해 36개의 정점이 필요하나, 실제 고유한 정점은 단 8개 뿐임

    • 인덱스 버퍼를 사용하면 8개의 고유한 정점들의 인덱스만 반복 사용하여 정점 데이터 크기를 줄일 수 있음

장점

  • 정점 재사용: 동일한 정점을 여러 삼각형에 반복 사용 가능

  • 메모리 절약: 동일한 정점을 중복 저장하지 않아도 되어 GPU 메모리 절약 가능

  • 렌더링 최적화: GPU가 삼각형을 효율적으로 그릴 수 있도록 도움

    • VS 실행 횟수 감소, 메모리 접근 감소(캐시 친화), 대형 mesh의 효율적 처리 등
  • 유지보수 용이: 3D 모델의 정점 데이터와 삼각형 구성 정보를 분리하여 관리하기 쉬워짐

동작 원리

  1. 정점 버퍼에는 고유한 정점 정보들만 담음

  2. 인덱스 버퍼에 각 Primitive를 구성하는 정점의 인덱스를 순서대로 기록

  3. GPU는 인덱스 버퍼를 참조하여 삼각형을 구성할 정점을 가져와 렌더링

코드 예제

struct Vertex {
    float x, y, z;
};

// 인덱스 버퍼를 사용할 때 정점 버퍼는 그냥 고유한 정점 데이터들의 집합(순서는 크게 중요하지 않음)
Vertex vertices[] = {
    {-1.0f, -1.0f, -1.0f}, {1.0f, -1.0f, -1.0f}, {1.0f, 1.0f, -1.0f},
    {-1.0f, 1.0f, -1.0f}, {-1.0f, -1.0f, 1.0f}, {1.0f, -1.0f, 1.0f},
    {1.0f, 1.0f, 1.0f}, {-1.0f, 1.0f, 1.0f}
};

// 정육면체 인덱스 데이터
UINT indices[] = {
    0, 1, 2, 0, 2, 3, // 앞면
    4, 5, 6, 4, 6, 7, // 뒷면
    0, 1, 5, 0, 5, 4, // 아래
    2, 3, 7, 2, 7, 6, // 위
    0, 3, 7, 0, 7, 4, // 왼쪽
    1, 2, 6, 1, 6, 5  // 오른쪽
};

// 정점 버퍼는 별도로 생성(여기서는 생략)

// 인덱스 버퍼 생성
D3D11_BUFFER_DESC indexBufferDesc = {};
indexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
indexBufferDesc.ByteWidth = sizeof(indices);
indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER;

D3D11_SUBRESOURCE_DATA initData = {};
initData.pSysMem = indices;

ID3D11Buffer* indexBuffer = nullptr;
device->CreateBuffer(&indexBufferDesc, &initData, &indexBuffer);

3. Constant Buffer(상수 버퍼)

  • 셰이더에 공유 데이터를 빠르고 효율적으로 전달하기 위해 사용하는 메모리 공간

  • 여러 셰이더 단계가 공통으로 사용하거나, 특정 셰이더가 일정 기간 동안 동일하게 사용하는 읽기 전용 파라미터 묶음을 GPU에 전달하기 위한 목적

  • 헷갈리면 안되는게, GPU(셰이더)에서 읽기 전용인 것이지 CPU에서 읽기 전용인 것이 아님!

역할

  • 데이터 전달: 월드 변환 행렬(MVP 행렬등)등 CPU가 계산한 데이터를 GPU로 전달함

  • 셰이더와 데이터 공유: 셰이더는 상수 버퍼를 통해 필요한 데이터를 읽어오며, 상수 버퍼 안의 데이터는 셰이더에서 읽기 전용임

  • 성능 최적화: 상수 버퍼 사용은 GPU 메모리를 효율적으로 관리하면서 데이터 전송이 더 빠르고, CPU와 GPU 간 동기화 문제를 줄일 수 있음

동작 방식

  1. 버퍼 생성: CPU에서 상수 버퍼 생성 후 데이터 초기화(MVP 행렬, 조명 색상 / 방향, 카메라 위치 등)

    • D3D11_BUFFER_DESC 생성 시 BindFlagsD3D11_BIND_CONSTANT_BUFFER로 지정
  2. 셰이더에 바인딩: 프레임 생성 시 Draw 호출 전에 생성한 상수 버퍼를 필요한 각 셰이더에 바인딩

    • ID3D11DeviceContext::VSSetConstantBuffer(), ID3D11DeviceContext::PSSetConstantBuffer() 등을 통해 상수 버퍼 바인딩
  3. 셰이더에서 접근: 셰이더 내부에서 상수 버퍼 데이터를 이용하여 계산을 수행

특징

  • 데이터 전송 속도가 빠르고, 메모리를 효과적으로 사용할 수 있음

  • 필요한 상수 데이터를 하나의 버퍼에 모아서 GPU로 전달하므로 코드가 깔끔해짐

  • CPU와 GPU가 데이터를 효율적으로 교환하여 성능 저하를 방지