• 이전 글: ❎ Direct3D 11 Graphics Pipeline - 1. DirectX

  • Input-Assembler(IA)란 D3D 11 파이프라인에서 가장 첫 단계로, 우리의 C++ 코드가 준비해 둔 버퍼(메모리 덩어리)에서 정점 데이터(position / normal / uv 등)를 읽고, 이를 점 / 선 / 삼각형같은 Primitive 형태로 묶어서 다음 단계로 넘김

  • 쉽게 말해, 그림을 그리기 위한 원재료를 꺼내서, 도형 단위로 조립 후, 셰이더가 이해하는 형태로 공급하는 단계

1. 스테이지에서 수행해 주어야 하는 핵심 작업

  1. 정점 버퍼(Vertex Buffer)에서 정점 읽기

    1. 인덱스 버퍼(Index Buffer)가 있으면 인덱스를 통해 정점 재사용
  2. 입력 레이아웃(Input-Layout)으로 메모리 해석 방법을 결정

  3. Primitive Topology로 어떻게 묶을지를 결정

  4. Draw 메서드 호출을 통해 파이프라인에 입력 밀어 넣기

2. 입력 버퍼(Input Buffers) 만들기

  • 정점 버퍼는 애플리케이션에 정의된 각 정점들을 담은 배열

  • 보통 다음과 같은 데이터가 포함됨

    • 위치(position), 법선(normal), UV(texcoord), 색(color), 탄젠트, 스키닝 가중치 등
  • 정점 버퍼 자체는 단순히 정점 관련 정보를 GPU 메모리에 복사해 둔 것으로, IA는 정점이 무엇을 의미하는지도 모르고 그냥 일정 간격(stride)으로 데이터를 읽음

  • 이를 위해 IA는 정점 버퍼의 시작 주소, stride(정점 하나의 크기), offset 정보를 함께 받음

인덱스(색인) 버퍼

  • 인덱스 버퍼는 선택 사항으로, 있으면 보통 성능과 메모리 효율이 좋아짐

  • 인덱스 버퍼는 정점 사용의 순서표같은 것으로, 인덱스 버퍼가 있으면 각 정점의 인덱스만 가지고 순서대로 정점을 꺼내도록 지시할 수 있음

  • 같은 정점을 인덱스를 통해 여러 Primitive에 재사용할 수 있어서 정점 데이터를 중복 저장하지 않아도 된다는 장점

IA 스테이지에서 버퍼 없이 진행하기?

  • 사실 D3D 11에서는 정점 버퍼나 Input-Layout 없이도 렌더링이 가능함

    • 이 경우에는 Vertex Shader에서 정점을 만들어 냄. 즉, 정점 버퍼 코드가 VS 내에 들어간 것과 유사하다고 생각할 수 있음
  • 정점 버퍼가 없어도 IA는 시스템 값(System-generated Value)은 생성함

    • 가장 대표적인 것이 SV_VertexID(지금 처리 중인 정점의 번호)
  • IA 자체는 정점 버퍼를 읽는 단계라기 보다는 정점의 입력 흐름을 정의하는 단계라고 보는 것이 타당. 말 그대로 규칙만 정해서 해당 흐름(스트림)을 넘겨주는 것

3. Input-Layout 개체 만들기

  • IA는 Input-Layout을 통해 특정 바이트를 특정 셰이더 변수로 변환하여 넘김

  • 정점 버퍼는 IA 입장에서는 그냥 바이트 덩어리이기 때문에 IA가 데이터를 셰이더 입력과 매칭하려면

    “이 바이트 구간은 POSITOIN이고, float3임”

    “다음 바이트 구간은 TEXCOORD고 float2임”

    “이 때 stride는 얼마고, offset은 얼마임”

    하는 규칙이 필요함

  • 이 규칙을 캡슐화한 것이 바로 입력 레이아웃 객체로, 런타임에 버퍼 포맷이 셰이더 입력 시그니처와 호환되는지 검사하는 데에도 사용됨

  • 중요한 것은 Input-Layout은 반드시 Vertex Shader의 입력과 호환되어야 한다는 점이며, 내용이 조금이라도 틀리면 렌더가 안되거나 이상한 값이 들어감

4. Primitive 형식 지정

  • GPU는 Primitive 단위로 데이터를 처리하기 때문에 IA에서 정점을 Primitive 단위로 묶어줄 필요가 있음

  • 같은 정점 배열이라도 어떻게 묶는지에 따라 결과가 달라짐

  • IA는 지정된 Topology 규칙에 따라 정점을 점 / 선 / 삼각형으로 조립함

  • 정점들을 점으로 쓸지, 선으로 쓸지, 삼각형으로 쓸지를 IA에게 알려줄 때 지정하는 것이 Primitive Topology(Primitive의 형태)

기본 Topology

  1. POINTLIST: 정점 1개 = 점 1개

  2. LINELIST: 정점 2개 = 선 1개(서로 독립)

  3. LINESTRIP: (v0, v1), (v1, v2), …처럼 연결된 선

  4. TRIANGLELIST: 정점 3개 = 삼각형 1개(서로 독립)

  5. TRIANGLESTRIP: 삼각형들이 변(엣지)를 공유하며 연속적으로 만들어짐

예시: TRIANGLELIST

  • 대부분의 3D 렌더링은 삼각형 기반이므로, 실무에서 가장 흔한 기본이 TRIANGLELIST
context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
  • 이렇게 하면 정점 3개는 1개의 삼각형, 인덱스 6개는 2개의 삼각형으로 인식해서 정점 스트림을 읽으면서 Primitive 스트림을 만들어 다음 단계(Vertex Shader)로 보냄

Strip을 쓰는 이유

  • List 방식은 구성이 단순하고 직관적이지만, 연결된 면 / 선이라고 하더라도 삼각형마다 새로 정점을 지정하기 때문에 정점(인덱스)가 낭비됨

  • Strip 방식은 연속된 삼각형인 경우 정점(인덱스)가 절약될 수 있다는 점을 활용하는 것

    • 단, Mesh 전반에서 Strip을 잘 이어 붙이는 것이 번거롭고, 실무에서 인덱스 캐시 / 최적화 관점에서 꼭 이득이라고 보기만은 어렵다는 관점도 있음

권선 방향(Winding Direction)과 선행 정점(Leading Vertex)

  • 후면 제거(back-face culling)와 관련된 것으로, 삼각형을 정의하는 정점의 순서를 권선 방향이라고 함

  • Mesh 정점 순서가 뒤집히면 후면 제거 결과로 면이 사라져 보이는 현상이 발생할 수 있음

  • 특히 Triangle Strip에서는 삼각형이 연속 생성되며 선행 정점 규칙과 얽혀서, 권선 방향을 헷갈리면 결과가 쉽게 뒤틀림

Patch List

  • Tesselation(Hull / Domain Shader)을 위해 사용되는 Topology

  • Triangle List가 삼각형을 넘기는 것이라면, Patch List는 Tesselation할 제어점(control points) 묶음을 넘기는 것

    • 해당 제어점 묶음을 기반으로 Tesselation 단계에서 더 촘촘한 기하를 생성하도록 하는 것

Primitive Adjacency?

  • 예를 들어 삼각형 + 인접 Topology를 쓰면, 한 삼각형(3정점) + 그 삼각형의 각 변에 인접한 삼각형 쪽 정점들(추가 3정점)로 Primitive를 묶으며, 삼각형 1개 처리에 총 6정점을 묶는 Primitive가 됨

  • Geometry Shader를 지원하기 위해 추가된 형태인데, Geometry Shader 자체를 많이 안 쓰기도 하고, 인접 프리미티브도 흔히 다루는 개념은 아니니 ‘이런 것도 있구만~’ 정도로 생각해도 됨

Generating Multiple Strips?

  • Strip은 원래 연결된 하나의 긴 띠의 개념이지만, 실제 Mesh에서는 한 번에 하나의 Strip만으로 깔끔한 표현이 어려운 경우가 많음

  • 때문에 Strip을 끊어서 여러 개로 만드는 개념도 있는데, 고급 최적화 / 특수 기법 영역이라 지금 깊히 알 필요는 없을 듯

    • 인덱스 버퍼를 이용할 때 특정 지점에서 Strip을 끊는 동작이 가능하고, 그 결과 한 번의 드로우에서 여러 스트립의 형태를 만들 수 있는 것

5. Draw 메서드 호출

context->DrawIndexed(indexCount, 0, 0);
  1. 정점 버퍼에서 정점 데이터를 읽기

  2. (선택) 인덱스 버퍼로 정점 순서 결정

  3. Input-Layout 규칙에 따라 데이터 해석

  4. Primitive Topology 규칙으로 정점 묶기

  5. 완성된 Primitive를 Vertex Shader로 전달

6. System-generated Value(SV)

  • IA에서 버퍼와 함께 추가로 넘겨주는, IA에서 자동으로 생성하는(system-generated) 몇 가지 값

  • IA 단계는 Primitive를 조립하는 과정에서, 현재 처리 중인 정점 / Primitive / Instance의 ID들이 필요해질 수 있으니 이를 자동으로 붙여줄 수 있음

핵심 SV 3종

  • 기본적으로 모두 32비트 부호 없는 정수로, Base 0
  1. SV_VertexID

    • 각 정점을 식별

    • Primitive가 IA에서 처리될 때 각 정점에 할당하며, 정점마다 1씩 증가

    • 인덱스 버퍼를 사용하는 경우, 정점의 순번이 아니라 인덱스 버퍼의 인덱스 값을 의미하게 됨

    • 인스턴싱에서 같은 정점을 다른 색 등으로 다시 그릴 경우 어쨌든 같은 정점이기 때문에 Vertex ID도 동일하게 나오지만, 이건 같은 정점이라 그런거고 Instance마다 0으로 초기화되는 건 아님

  2. SV_PrimitiveID

    • 각 Primitive를 식별

    • 많은 SV들은 그 값을 해석할 수 있는 첫 번째 활성 셰이더만 입력으로 받고, 그 이후로는 사용자가 전달해야 하는데, SV_PrimitiveID는 예외가 있어서 Tesselation 단계에도 제공할 수 있고, 이후에는 활성화된 첫 단계에 제공될 수 있음

    • 기본적으로 Primitive마다 증가, 인스턴싱에서는 새 Instance가 시작될 때마다 0으로 다시 설정됨(즉, 인스턴싱에서는 Instance 내부에서의 Primitive 번호로 이해하면 됨)

    • Pixel Shader에서 주의할 점이 있는데, PS는 Primitive ID를 평범하게 보간하면 안되고, 상수 보간(프리미티브 전체를 동일한 값)을 사용해야 함

  3. SV_InstanceID

    • 현재 처리 중인 Instance(같은 Mesh를 여러 번 그리는 과정에서의 순번)를 식별

    • VS 입력 선언에 SV_InstanceID를 포함시키면 IA가 각 정점에 할당

    • Instance마다 가지는 고유한 ID