입력 조립기(IA, Input Assembler) .단계는 메모리에서 기하자료(정점들과 색인들) 을 읽어서 기하학적 기본도형 을 조립한다. (Pirimitive; 삼각형, 선분, 점 등.. 더 복잡한 형태를 만드는 데 사용할 수 있는 도형)
점 목록 | D3D_PRIMITIVE_TOPOLOGY_POINTLIST |
선 띠 | D3D_PRIMITIVE_TOPOLOGY_LINESTRIP |
선 목록 | D3D_PRIMITIVE_TOPOLOGY_LINELIST |
삼각형 띠 | D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP |
삼각형 목록 | D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST |
인접성 정보를 가진 기본 도형 | D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST_ADJ |
제어점 패치 목록 | D3D_PRIMITIVE_TOPOLOGY_N_CONTROL_POINT_PATCHLIST |
수학에서 삼각형의 정점은 두 변이 만나는 점 선분의 정점은 양 끝점 점의 정점은 그 점 자체 이다.
정점은 그냥 기하도형을 특징짓는 고유한 점이라는 인상을 받을 수도 있다. 그러나 Direct3D 의 정점은 그보다 더 훨씬 일반적이다. 본질적으로 Direct3D 의 정점은 공간적위치 이외의 정보도 담을 수 있다. 이 덕분에 좀 더 복잡한 렌더링 효과의 구현이 가능해진다. 예를들어 조명을 구현하기 위해 정점에 법선벡터를 추가하거나 텍스처 적용을 위해 정점에 텍스처 좌표를 추가한다. Direct3D 는 응용프로그램이 자신만의 정점 형식을 정의할 수 있는 유연성을 제공한다. (즉, 정점의 성분들을 직접 정의할 수 있는) 렌더링 효과에 맞게 다양한 정점 형식들을 정의한다.
정점들은 정점버퍼(vertex buffer) 라고 하는 특별한 Direct3D 자료구조 안에 담겨서 렌더링 파이프라인에 묶인다. 정점 버퍼는 그냥 일단의 정점들을 연속적인 메모리에 저장하는 자료구조이다. 정점 버퍼 자체에는 기본도형을 형성하기 위해 정점들을 조합하는 방법에 관한 정보가 들어있지 않다. 예를들어 한 정점 버퍼 안에 담긴 정점들을 두 개씩 사용해서 선분을 만들어야 하는지, 아니면 세 개씩 사용해서 삼각형을 형성해야 하는지엥 대한 정보는 없다.
자료를 이용해서 기하학적 기본도형을 형성하는 방법을 Direct3D 에 알려주려면 기본도형 위상구조(Primitive Topology) 라는 것을 설정해야 한다. 그리고 이후의 모든 그리기 호출은 현재 설정된 기하구조 위상구조를 사용한다. CommandList 를 통해서 다른 위상구조가 설정되기 전까지는.
/* 위상구조 설정 메서드 */
void ID3D12GraphicsCommandList::IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY Topology);
/* 위상구조 열거형 */
typedef enum D3D_PRIMITIVE_TOPOLOIGY
{
D3D_PRIMITIVE_TOPOLOGY_UNDEFINED = 0,
D3D_PRIMITIVE_TOPOLOGY_POINTLIST = 1,
D3D_PRIMITIVE_TOPOLOGY_LINELIST = 2,
D3D_PRIMITIVE_TOPOLOGY_LINESTRIP = 3,
D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST = 4,
D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP = 5,
D3D_PRIMITIVE_TOPOLOGY_LINELIST_ADJ = 10,
D3D_PRIMITIVE_TOPOLOGY_LINESTRIP_ADJ = 11,
D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST_ADJ = 12,
D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP_ADJ = 13,
D3D_PRIMITIVE_TOPOLOGY_1_CONTROL_POINT_PATCHLIST = 33,
D3D_PRIMITIVE_TOPOLOGY_2_CONTROL_POINT_PATCHLIST = 34,
...
D3D_PRIMITIVE_TOPOLOGY_32_CONTROL_POINT_PATCHLIST = 64,
}
// 예시
// 선 목록을 적용해서 물체들을 그린다.
mCommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_LINELIST);
// 삼각형 목록을 적용해서 물체들을 그린다. 주로 많이 사용
mCommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
// 삼각형 띠를 적용해서 물체들을 그린다.
mCommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
D3D_PRIMITIVE_TOPOLOGY_POINTLIST 를 지정해서 IASetPrimitiveTopology 를 호출하면 점 목록(point list) 이 현재 위상 구조로 설정된다. 점 목록이 설정된 상태에서 그리기 호출의 모든 정점은 개별적인 점으로 그려진다.
D3D_PRIMITIVE_TOPOLOGY_LINESTRIP 을 지정하면 선 띠 (line strip ; 또는 선분 띠) 또는 선분 띠 가 설정된다. 선 띠가 설정된 상태애서 그리기 호출의 정점들은 차례로 연결된 선분들을 형성한다. 아래 그림을 보면 n+1 개의 정점으로 n 개의 선분이 만들어진다.
D3D_PRIMITIVE_TOPOLOGY_LINELIST 를 지정하면 선 목록 (line list; 또는 선분 목록)이 설정된다. 선 목록이 설정된 상태에서 그리기 호출의 매 정점 두개가 하나의 선분을 형성한다.
따라서 정점 2n 개로 선분 n 개가 만들어진다. 선분들이 자동으로 연결되는 선 띠와는 달리, 선 목록에서는 손 목록으로는 분리된 선분들을 형성할 수 있다. 선 띠에서는 선분들이 모두 연결되어 있다는 가정하에서 두 선분이 정점을 공유하므로, 정점 개수가 더 적다.
D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP 을 지정하면 삼각형 띠(tiangle strip) 가 설정된다. 삼각형 띠가 설정된 상태에서 그리기 호출의 정점들은 아래 방식대로 보든 정점들이 연결되어 삼각형들을 형성한다.
그림에서 보듯이, 삼각형 띠 위상구조에서는 삼각형들이 연결되어 있다는 가정하에서 인접한 두 삼각형이 정점들을 공유하며, 결과적으로 n개의 정점으로 n-2 개의 삼각형이 만들어진다. 삼각형 띠에서 짝수 번째 삼각형과 홀수 번째 삼각형의 정점들이 감기는 순서(winding order) 가 다르다는 점을 주목하기 바란다. 이 때문에 후면 선별(Backface Culling) 시 문제가 발생한다. 이를 바로잡기위해, GPU 는 내부적으로 짝수 번째 삼각형의 처음 두 정점의 순서를 맞바꾸어서 홀수 번째 삼각형과 같은 순서가 되게 만든다.
D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST 를 지정하면 삼각형 목록(Triangle list) 이 설정된다. 삼각형 목록이 설정된 상태에서 그리기 호출의 매 정점 세 개가 하나의 삼각형을 형성한다. 따라서 3n 개의 정점으로 n 개의 삼각형이 만들어진다. 삼각형들이 모두 연결되는 삼각형 띠와는 달리, 삼각형 목록으로는 따로 떨어진 삼각형들을 형성할 수 있다.
삼각형 목록을 만들때, 각 삼각형에 그에 접한 이웃 삼각형 세 개에 관한 정보를 포함할 수 있다. 그렇게 만든 삼각형 목록을 "인접성(Adjacency) 정보를 가진 삼각형 목록" 이라고 부르고, 주어진 삼각형에 접한 삼각형들을 인접 삼각형(adjacent triangle) 이라고 부른다. 아래 그림에는 인접 삼각형들을 지정하는 방식이 나와 있다.
이러한 삼각형 목록은 인접 삼각형들에 접근해야 하는 특정한 기하 셰이딩 알고리즘을 기하 셰이더 (Geometry Shader) 에서 구현할 때 쓰인다. 기하 셰이더가 그런 인접 삼각형들에 접근하려면 삼각형 자체와 함께 인접 삼각형들의 정보도 정점 버퍼와 색인 버퍼에 담아서 파이프라인에 제출해야 한다. 또한, 반드시 위상구조를 D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST_ADJ 로 지정해야 한다. 그래야 파이프라인이 정점 버퍼로부터 삼각형과 그 인접 삼각형들으 구축할 수 있다. 인접 기본도형의 정점들은 오직 기하 셰이더 (Geometry Shader) 의 입력으로만 쓰일뿐, 실제로 그려지는 것은 아님을 주의하기 바란다. 기하 셰이더가 아예 없는 경우에도 인접 기본도형들은 그려지지 않는다. 인접성 정보를 가진 삼각형 목록 외에 인접성 정보를 가진 선 목록이나 인접성 정보를 가진 선 띠 인접성 정보를 가진 삼각형 띠도 가능하다. 자세한 사항은 SDK 문서화를 보기 바란다.
D3D_PRIMITIVE_TOPOLOGY_N_CONTROL_POINT_PATCHLIST 위상 구조는 정점 자료를 N 개의 제어점(control point) 들로 이루어진 패치목록으로 해석해야 함을 뜻한다. 이 제어점 패치는 렌더링 파이프라인의 테셀레이션 단계들(생략가능함) 에 쓰인다.
앞에서 이야기 했듯이, 고형 3차원 물체의 기본 구축 요소는 삼각형이다. 예를 들어, 사각형 하나와 팔각형 하나를 삼각형 목록을 이용해서 (즉, 정점 세개당 삼각형 하나를 형성해서) 구축한다고 하자. 다음은 이에 필요한 정점 배열들이다.
Vertex quad[6] = {
v0, v1, v2, // 삼각형 0
v0, v2, v3 // 삼각형 1
}
Vertex octagon[24] = {
v0, v1, v2, // 삼각형 1
v0, v2, v3, // 삼각형 2
v0, v3, v4, // 삼각형 3
v0, v4, v5, // 삼각형 4
v0, v5, v6, // 삼각형 5
v0, v6, v7, // 삼각형 6
v0, v7, v8, // 삼각형 7
v0, v8, v1 // 삼각형 8
}
삼각형의 정점들을 지정하는 순서가 중요한데, 이를 감기 순서 (winding order; 또는 감는 순서) 라고 불느다. 그림에서 보듯이, 하나의 3차원 물체를 형성하는 삼각형들은 여러 개의 정점 을 공유 한다. 좀더 구체적으로, 그림의 사각형을 구성하는 두 삼각형은 정점 v0 와, v2 를 공유한다. 배열에는 이 정점들이 두 번 들어 있음을 주목하기 바란다. 정점 두 개가 중복되는 것은 그리 큰 문제가 아니지만. 그림의 발각형의 경우에는 문제가 좀 더 심각해진다. 이 팔각형의 모든 삼각형이 중심 정점 v0 를 공유하며, 팔각형 테두리의 각 정점을 인접한 두 삼각형이 공유한다. 일반적으로, 모형이 세밀하고 복잡할수록 중복되는 정점들도 많다. 정점들의 중복이 바람직하지 않은 이유는 크게 두가지이다. 1. 메모리 요구량이 증가한다. (같은 정점 자료를 여러번 저장할 필요가 있겠는가?) 2. 그래픽 하드웨어의 처리량이 증가한다. (같은 정점 자료를 여러 번 처리할 필요가 있겠는가?) 삼각형 띠를 이용하면 중복 정점 문제가 완화된다. 단, 모형의 기하구조를 삼각형 띠 형태로 구성할 수 있어야 하는데, 항상 가능하지는 않다. 삼각형 띠보다는 삼각형 목록이 더 유연하므로 (삼각형들이 연결될 필요가 없다는 점에서) 삼각형 목록에서 중복 정점들을 제거하는 방법을 고안하는 것은 가치가 있는 일이다. 해결책은 색인(index) 을 사용하는 것이다. 색인을 이용하는 정점 목록과 함께 색인 목록을 하나 만든다. 정점 목록은 모든 고유한 정점들로 이루어지고, 색인 목록은 어떤 정점들을 어떤 순서로 사용해서 삼각형을 형성해야 하는지를 나타내는 색인들로 이루어진다. 위 그림의 예로 돌아가서, 색인을 사용한다면 사각형을 위한 정점 목록은 정점 4개로 충분하다.
Vertex v[4] = {v0, v1, v2, v3};
다음으로, 정점 목록의 정점들로 두 개의 삼각형을 형성하는 방법을 결정하는 색인 목록을 만들어야 한다.
UINT indexList[6] = {
0, 1, 2, // 삼각형 0
0, 2, 3 // 삼각형 1
}
삼각형 목록을 위한 색인 목록은 매 원소 (색인) 세 개가 하나의 삼각형을 정의한다. 따라서 이 색인 목록은 "정점v[0], v[1], v[2]" 로 삼각형 0를 만 만들고" "정점v[0], v[2], v[3]" 로 삼각형 1을 만들어라" 라는 뜻이다. 마찬가지로 팔각형을 위한 정점 목록과 색인 목록은
vertex v[9] = {v0, v1, v2, v3, v4, v5, v6, v7, v8};
UINT indexList[24] = {
0, 1, 2, // 삼각형 1
0, 2, 3, // 삼각형 2
0, 3, 4, // 삼각형 3
0, 4, 5, // 삼각형 4
0, 5, 6, // 삼각형 5
0, 6, 7, // 삼각형 6
0, 7, 8, // 삼각형 7
0, 8, 1 // 삼각형 8
};
로 두면 된다. 그래픽 카드는 정점 목록의 고유한 정점들을 처리한 후, 색인 목록을 이용해서 정점들을 조합해 삼각형을 형성한다. 정점 목록의 '중복' 이 색인 목록으로 옮겨간 셈인데, 별로 문제가 되지 않는다.
1. 색인은 그냥 정수이므로 완전한 정점 구조체보다 적은 양의 메모리를 차지한다. (정점에 새로운 성분을 더 추가하면 정점 구조체가 더욱 커진다) 2. 정점들이 적절한 순서로 캐시에 저장된다면, 그래픽 하드웨어는 중복된 정점들을 (너무 자주)처리할 필요가 없다.
Direct3D 정점에 공간적 위치 이외의 추가적인 자료를 부여할 수 있다. 원하는 자료('특성' 들)를 가진 커스텀 정점 형식(vertex format) 을 만들려면 우선 그러한 자료를 담을 구조체를 정의해야 한다. 다음은 서로 다른 두가지 정점 형식의 예이다. 하나는 위치와 색상으로 구성되고 또하나는 위치와 법선, 그리고 두개의 2차원 텍스처 좌표로 구성된다.
struct Vertex1{
XMFLOAT3 Pos;
XMFLOAT4 Color;
};
struct Vertex2{
XMFLOAT3 Pos;
XMFLOAT3 Nomral;
XMFLOAT2 Tex0;
XMFLOAT2 Tex1;
}
정점 구조체를 정의한 다음에는 정점 구조체의 각 필드, 즉 정점의 각 성분으로 무엇을 해야 하는지를 Direct3D 에게 알려주어야 한다. 그러한 정보를 Direct3D 에 알려주는 수단으로 쓰이는 것이 입력 배치 서술 (input layout description) 이다. 이 서술은 다음과 같은 D3D12_INPUT_LAYOUT_DESC 라는 구조체로 대표된다.
/** 정점형식 1 */
struct Vertex1
{
XMFLOAT3 pos; // 위치
XMFLOAT4 Color; // 색상
};
/** 정점형식 2 */
struct Vertex2
{
XMFLOAT3 Pos; // 위치
XMFLOAT3 Normal; // 법선
XMFLOAT2 Tex0; // 텍스처 좌표
XMFLOAT2 Tex1; // 텍스처 좌표
};
/**
* 입력 배치 서술 (input layout description)
* Direct3D 에게 정점의 각 성분으로 무엇을 해야 하는지
* 알려주기 위한 수단
*/
typedef struct D3D12_INPUT_LAYOUT_DESC{
const D3D12_INPUT_ELEMENT_DESC* pInputElementDescs; // 각 원소가 정점 구조체의 성분을 서술한다.
UINT NumElements;
} D3D12_INPUT_LAYOUT_DESC;
/**
@param SemanticName 성분에 부여된 문자열 이름. 이것은 정점 셰이더에서 의미소 (semantic) 이름으로 쓰이므로,
반드시 유효한 변수 이름이어야 한다.
의미소는 정점 구조체의 성분을
정점 셰이더 입력 서명과 대응시키는 역할을 한다.
@param SemanticIndex 의미소에 부여된 색인. 이런 색인이 필요한 이유가 그림에 나와있다.
하나의 정점 구조체에 텍스처 좌표가 여러개 있을 수 있는데,
각 텍스처 좌표에 개별적인 의미소 이름을 부여하는 대신
그냥 색인을 통해서 구별해도 된다.
셰이더 코드에서 색인이 지정되지 않은 의미소는
색인이 0 인 의미소로 간주된다.
예를 들어 그림에서 POSITION 은 POSITION0 에 해당한다.
@param Format DXGI_FORMAT 열거형의 한 멤버로, 이 정점 성분의 자료 형식을
Direct3D 에게 알려주는 역할을 한다.
@param InputSlot 이 성분의 자료를 가져올 정점 버퍼 슬롯의 색인이다.
Direct3D 에서는 총 16개의 정점 버퍼 슬롯(색인은 0에서 15까지)을 통해서
정점 자료를 공급할 수 있다.
@param AlignedByteOffset 지정된 입력 슬롯에서, c++ 정정 구조체의 시작 위치와
이 정점 성분의 시작 위치 사이의 거리를 나타내는 오프셋(바이트 단위) 이다.
예시:
struct Vertex2{
XMFLoat3 Pos; // offset : 0 byte
XMFLoat3 Normal; // offset : 12 byte
XMFLoat2 Tex0; // offset : 24 byte
XMFLoat2 Tex1; // offset : 32 byte
}
@param InputSlotClass 보통 D3D12_INPUT_PER_VERTEX_DATA
@param InstanceDataStepRate 고급 기법인 인스턴싱에 사용되는 값. 기본값 0
*/
typedef struct D3D12_INPUT_ELEMENT_DESC{
LPCSTR SemanticName;
UINT SemanticIndex;
DXGI_FORMAT Format;
UINT InputSlot;
UINT AlignedByteOffset;
D3D12_INPUT_CLASSIFICATION InputSlotClass;
UINT InstanceDataStepRate;
}D3D12_INPUT_ELEMENT_DESC;
D3D12_INPUT_ELEMENT_DESC VertexDesc1[] = {
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_PER_VERTEX_DATA, 0},
{"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_PER_VERTEX_DEATA, 0}
};
D3D12_INPUT_ELEMENT_DESC VertexDesc2[] = {
{"POSITION", 0 , DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_PER_VERTEX_DATA, 0},
{"NORMAL", 0 , DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D12_INPUT_PER_VERTEX_DATA, 0 },
{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D12_INPUT_PER_VERTEX_DATA, 0},
{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 32, D3D12_INPUT_PER_VERTEX_DATA, 0}
};
GPU 가 정점들의 배열에 접근하려면, 그 정점들을 버퍼 라고 부르는 GPU 자원(ID3D12Resource) 에 넣어두어야 한다. 정점들을 저장하는 버퍼를 정점 버퍼(VertexBuffer) 라고 부른다. 버퍼는 텍스처보다 단순한 자원이다. 버퍼는 다차원이 아니며, 밉맵이나 필터, 다중표본화 기능이 없다. 응용프로그램에서 정점 자료 원소들의 배열을 GPU 에 제공해야 할때에는 항상 버퍼를 사용한다.
정점 버퍼를 생성하려면, 버퍼 자원을 서술하는 D3D12_RESOURCE_DESC 를 채우고 ID3D12_Device::CreateCommittedResource 메서드를 호출해서 ID3D12Resource 객체를 생성한다. D3D12_RESOURCE_DESC -- CreateCommittedResource -- > ID3D12Resource
Direct3D 12 는 D3d12_RESOURCE_DESC 를 상속해서 편의용 생성자들과 메서드들을 추가한 c++ 래퍼 클래스 CD3DX12_RESOURCE_DESC 를 제공한다. 특히 이 클래스의 다음 메서드를 이용하면, 버퍼를 서술하는 D3D12_RESOURCE_DESC 구조체 인스턴스를 간단히 생성할 수 있다. CommittedResource 를 생성하는 방법 ( CD3DX12_RESOURCE_DESC )
정적 기하구조(즉, 프레임마다 변하지 않는 기하구조) 를 그릴 때에는 최적의 성능을 위해 정점 버퍼들은 기본 힙(D3D12_HEAP_TYPE_DEFAULT) 에 넣는다. 일반적으로 게임의 기하구조들은 대부분 정적이다. (이를테면 나무, 건물, 지형, 무기 등) 정적 기하구조의 경우, 정점 버퍼를 초기화 한 후에는 GPU 만 버퍼의 정점을 읽으므로(기하구조를 그리기 위해) 기본 힙에 넣는 것이 합당하다. CPU 는 기본 힙에 있는 정점 버퍼를 수정하지 못한다. 그렇다면 , 애초에 응용프로그램이 어떻게 정점 버퍼를 초기화 하는 것일까? 실제 정점 버퍼 자원을 생성하는 것과 더불어, 응용 프로그램은 D3D12_HEAP_TYPE_UPLOAD 형식의 힙에 임시 업로드용 버퍼 자원을 생성해야 한다. CPU 메모리에서 GPU 메모리로 자료를 복사하려면 업로드 힙에 자원을 맡겨야 한다. 업로드 버퍼를 생성한 다음에는 시스템 메모리에 있는 정점 버퍼로 복사한다. 기본버퍼(D3D12_HEAP_TYPE_DEFAULT 형식의 힙에 있는 버퍼) 의 자료를 사용하려면 항상 임시 업로드 버퍼가 필요하므로 아래와 같은 함수를 정의할 수도 있다.
ComPtr<ID3D12Resource> CreateDefaultBuffer(
ID3D12Device* device,
ID3D12GraphicsCommandList* cmdList,
const void* initData,
UINT64 byteSize,
ComPtr<ID3D12Resource>& uploadBuffer
)
{
ComPtr<ID3D12Resource> defaultBuffer;
ThrowIfFailed(
device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(byteSize),
D3D12_RESOURCE_STATE_COMMON,
nullptr,
IID_PPV_ARGS(defaultBuffer.GetAddressOf())
)
);
// CPU 메모리의 자료를 기본 버퍼에 복사하려면 임시 업로드 힙을 만들어야 한다.
ThrowIfFailed(
device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3DX12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(byteSize),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(uploadBuffer.GetAddressOf())
)
);
// 기본 버퍼에 복사할 자료를 서술한다.
D3D12_SUBRESOURCE_DATA subResourceData = {};
subResourceData.pData = initData;
subResourceData.RowPitch = byteSize;
subResourceData.SlicePitch = subResourceData.RowPitch;
/*
기본 버퍼 자원으로의 자료 복사를 요청한다.
개략적으로 말하면,
보조함수 UpdateSubresources 라는 CPU 메모리를
임시 업로드 힙에 복사하고,
ID3D12CommandList::CopySubresourceRegion 을 이용해서
임시 업로드 힙의 자료를 mBuffer 에 복사한다.
*/
cmdList->ResourceBarrier(
1,
&CD3DX12_RESOURCE_BARRIER::Transition(
defaultBuffer.Get(),
D3D12_RESOURCE_STATE_COMMON,
D3D12_RESOURCE_STATE_COPY_DEST
)
);
UpdateSubresource<1>(
cmdList,
defaultBuffer.Get(),
uploadBuffer.Get(),
0, 0, 1,
&subResourceData
);
cmdList->ResourceBarrier(
1,
&CD3DX12_RESOURCE_BARRIER::Transition(
defaultBuffer.Get(),
D3D12_RESOURCE_STATE_COPY_DEST,
D3D12_RESOURCE_STATE_GENERIC_READ
)
);
/*
주의 : 위의 함수 호출 이후에도 uploadBuffer 를 계속 유지해야 한다.
실제로 복사를 수행하는 명령 목록이
아직 실행되지 않았기 때문이다.
복사가 완료되었음을 확실해진 후의 호출자가
uploadBuffer 를 해제하면 된다.
*/
return defaultBuffer;
}
다음 코드는 입방체의 정점 여덟 개( 각자 다른 색상이 부여되었다) 를 저장하는 기본버퍼를 이 클래스를 이용해서 생성하는 방법은 아래와 같다.
struct Vertex
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
}
Vertex vertices[] = {
{XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::White) },
{XMFLOAT3(-1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Black) },
{XMFLOAT3(+1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Red) },
{XMFLOAT3(+1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::Green) },
{XMFLOAT3(-1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Blue) },
{XMFLOAT3(-1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Yellow) },
{XMFLOAT3(+1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Cyan) },
{XMFLOAT3(+1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Magenta) }
};
const UINT64 vbByteSize = 8 * sizeof(Vertex);
ComPtr<ID3D12Resource> VertexBufferGPU = nullptr; // Default Resource
ComPtr<ID3D12Resource> VertexBufferUploader = nullptr; // Upload Resource
VertexBufferGPU = CreateDefaultBuffer(md3dDevice.Get(), mCommandList.Get(), vertices, vbByteSize, VertexBufferUploader);
정점 버퍼를 파이프라인에 묶으려면 정점 버퍼 자원을 서술하는 정점 버퍼 뷰를 만들어야 한다. PTV(렌더 대상 뷰) 와는 달리, 정점 버퍼 뷰에는 서술자 힙이 필요하지 않다. 정점 버퍼 뷰를 대표하는 형식은 D3D12_VERTEX_BUFFER_VIEW 구조체이다.
/**
@param BufferLocation 생성할 뷰의 대상이 되는 정점 버퍼 자원의 가상주소
이 주소는 ID3D12Resource::GetGPUVirtualAddress 메서드로 얻을 수 있다.
@param SizeInBytes BufferLocation 에서 시작하는 정점 버퍼의 크기 (바이트 개수)
@param StrideInBytes 버퍼에 담긴 한 정점 원소의 크기 (바이트 개수)
*/
typedef struct D3D12_VERTEX_BUFFER_VIEW
{
D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;
UINT SizeInBytes;
UINT StrideInByte;
}D3D12_VERTEX_BUFFER_VIEW;
정점 버퍼를 생성하고 그에 대한 뷰 까지 생성했다면, 이제 정점 버퍼를 파이프라인의 한 입력 슬롯에 묶을 수 있다. 그러면 정점 파이프라인의 입력 조립기 단계 (IA; InputAssembler)로 공급된다. 다음은 정점 버퍼를 파이프 라인에 묶는 메서드이다.
/**
@param StartSlot : 시작 슬롯, 즉 첫째 정점 버퍼를 묶을 입력슬롯의 색인.
입력슬롯은 총 16개이다. (슬롯 색인은 0 에서 15까지)
@param NumBuffers : 입력 슬롯들에 묶을 정점 버퍼 개수.
시작 슬롯의 색인이 k 이고, 묶을 버퍼가 n 개이면
버퍼들은 입력슬롯 Ik, Ik+1, ... , Ik+n-1 에 묶이게 된다.
@param pViews : 정점 버퍼 뷰 배열의 첫번째 원소를 가리키는 포인터
*/
void ID3D12GraphicsCommandList::IASetVertexBuffer(
UINT StartSlot,
UINT NumBuffers,
const D3D12_VERTEX_BUFFER_VIEW* pViews
);
다음은 이 메서드의 호출 예이다.
D3D12_VERTEX_BUFFER_VIEW vbv;
vbv.BufferLocation = VertexBufferGPU->GetGPUVirtualAddress();
vbv.StrideInBytes = sizeof(Vertex);
vbv.SizeInBytes = 8 * sizeof(Vertex);
D3D12_VERTEX_BUFFER_VIEW vertexBuffers[1] = {vbv};
mCommandList->IASetVertexbuffers(0, 1, vertexBuffers);
일단 입력 슬롯에 묶은 정점 버퍼는 다시 변경하지 않는 한 계속 그 입력 슬롯에 묶여 있다. 따라서, 정점버퍼를 여러 개 사용하는 경우 코드의 전반적인 구조를 다음과 같이 짜면 될것이다.
ID3D12Resourec* mVB1; // Vertex1 형식의 정점들을 담는 정점 버퍼
ID3D12Resource* mVB2; // Vertex2 형식의 정점들을 담는 정점 버퍼
D3D12_VERTEX_BUFFER_VIEW mVBView1; // mVB1 에 대한 뷰
D3D12_VERTEX_BUFFER_VIEW mVBView2; // mVB2 에 대한 뷰
/* ...정점 버퍼들과 뷰들을 생성한다. */
mCommandList->IASetVertexBuffers(0, 1, &mVBView1);
/* ...정점 버퍼 1 을 이용해서 물체들을 그린다... */
mCommandList->IASetVertexBuffers(0, 1, &mVBView2);
/* ...정점 버퍼 2 을 이용해서 물체들을 그린다... */
정점 버퍼를 입력 슬롯에 설정한다고 해서 버퍼의 정점들이 그려지는 것은 아니다. 단지 그 정점들을 파이프라인데 공급할 준비가 된 것일 뿐이다. 정점들을 실제로 그리려면 ID3D12GraphicsCommandList::DrawInstanced 메서드를 호출해야 한다.
/*
@param VertexCountPerInstance : 그릴 정점들의 개수(인스턴스당)
@param InstanceCount : 그릴 인스턴스 개수. 인스턴싱이라고 부르는 고급 기법에서는 여러개의 인스턴스를 그리지만
보통의 경우에는 인스턴스를 하나만 그리므로 1로 설정한다.
@param StartVertexLocation : 정점 버퍼에서 이 그리기 호출로 그릴 일련의 정점들 중 첫 정점의 색인(0 기반)
@param StartInstanceLocation : 고급 기법인 인스턴싱에 쓰이며, 지금은 그냥 0 으로 설정한다.
*/
void ID3D12CommandList::DrawInstanced(
UINT VertexCountPerInstance,
UINT InstanceCount,
UINT StartVertexLocation,
UINT StartInstanceLocation
);
두 매개변수 VertexCountPerInstance 와 StartVertexLocation 에 의해 정점 버퍼의 정점들중 이 그리기 호출에 쓰이는 일련의 정점들이 결정된다.
그런데 DrawInstanced 메서드를 보면 주어진 정점들로 그릴 기본도형이 어떤 종류인지에 관한 매개변수는 없다. 지정된 정점들을 Direct3D 가 점들로 취급할지 아니면 선 목록이나 삼각형 목록 등으로 취급할지는 ID3D12GraphicsCommandList::IASetPrimitiveTopology 메서드로 설정하는 기본도형 위상구조 상태가 결정한다. 다음은 이 메서드의 호출 예이다.
cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
정점들과 마찬가지로, GPU가 색인드르이 배열에 접근할 수 있으려면 색인들을 버퍼 GPU 자원 (ID3D12Resource) 에 넣어 두어야 한다. 색인(index)들을 받는 버퍼를 색인 버퍼 라고 부른다. 앞서 정점 버퍼부분에서 따로 정의한 함수 CreateDefaultBuffer 함수는 void* 를 통해서 일반적 자료를 처리하므로, 색인 버퍼(그리고 그 외의 모든 기본 버퍼) 도 이 함수로 생성할 수 있다.
ComPtr<ID3D12Resource> CreateDefaultBuffer(
ID3D12Device* device,
ID3D12GraphicsCommandList* cmdList,
const void* initData,
UINT64 byteSize,
ComPtr<ID3D12Resource>& uploadBuffer
)
{
ComPtr<ID3D12Resource> defaultBuffer;
ThrowIfFailed(
device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(byteSize),
D3D12_RESOURCE_STATE_COMMON,
nullptr,
IID_PPV_ARGS(defaultBuffer.GetAddressOf())
)
);
// CPU 메모리의 자료를 기본 버퍼에 복사하려면 임시 업로드 힙을 만들어야 한다.
ThrowIfFailed(
device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3DX12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(byteSize),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(uploadBuffer.GetAddressOf())
)
);
// 기본 버퍼에 복사할 자료를 서술한다.
D3D12_SUBRESOURCE_DATA subResourceData = {};
subResourceData.pData = initData;
subResourceData.RowPitch = byteSize;
subResourceData.SlicePitch = subResourceData.RowPitch;
/*
기본 버퍼 자원으로의 자료 복사를 요청한다.
개략적으로 말하면,
보조함수 UpdateSubresources 라는 CPU 메모리를
임시 업로드 힙에 복사하고,
ID3D12CommandList::CopySubresourceRegion 을 이용해서
임시 업로드 힙의 자료를 mBuffer 에 복사한다.
*/
cmdList->ResourceBarrier(
1,
&CD3DX12_RESOURCE_BARRIER::Transition(
defaultBuffer.Get(),
D3D12_RESOURCE_STATE_COMMON,
D3D12_RESOURCE_STATE_COPY_DEST
)
);
UpdateSubresource<1>(
cmdList,
defaultBuffer.Get(),
uploadBuffer.Get(),
0, 0, 1,
&subResourceData
);
cmdList->ResourceBarrier(
1,
&CD3DX12_RESOURCE_BARRIER::Transition(
defaultBuffer.Get(),
D3D12_RESOURCE_STATE_COPY_DEST,
D3D12_RESOURCE_STATE_GENERIC_READ
)
);
/*
주의 : 위의 함수 호출 이후에도 uploadBuffer 를 계속 유지해야 한다.
실제로 복사를 수행하는 명령 목록이
아직 실행되지 않았기 때문이다.
복사가 완료되었음을 확실해진 후의 호출자가
uploadBuffer 를 해제하면 된다.
*/
return defaultBuffer;
}
색인 버퍼를 파이프라인에 묶으려면 색인 버퍼 자원을 서술하는 색인 버퍼 뷰를 만들어야 한다. 정점 버퍼 뷰처럼 색인 버퍼 뷰에도 서술자 힙이 필요하지 않다. 색인 버퍼 뷰를 대표하는 형식은 D3D12_INDEX_BUFFER_VIEW 이다.
/*
@param BufferLocation : 생성할 뷰의 대상이 되는 색인 버퍼 자원의 가상주소.
이 주소는 ID3D12Resource::GetGPUVirtualAddress 메서드로 얻을 수 있다.
@param SizeInBytes : BufferLocation 에서 시작하는 색인 버퍼의 크기(바이트 개수)
@param Format : 색인의 형식. 16bit 색인이면 DXGI_FORMAT_R16_UINT 를.
36bit 색인이면 DXGI_FORMAT_R32_UINT 를 지정해야 한다.
메모리와 대역폭을 절약하려면 16bit 색인을 사용해야 하며,
32bit 색인 값들에 추가적인 32bit 범위가 필요한 경우에만 사용해야 한다.
*/
typedef struct D3D12_INDEX_BUFFER_VIEW
{
D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;
UINT SizeInBytes;
DXGI_FORMAT Format;
}D3D12_INDEX_BUFFER_VIEW;
정점 버퍼와 마찬가지로 (그리고 사실 다른 모든 Direct3D 자원과 마찬가지로) 색인 버퍼를 사용하려면 파이프라인에 묶어야 한다. 색인 버퍼는 ID3D12CommandList::IASetIndexBuffer 메서드를 통해서 입력 조립기 단계에 묶는다. 다음 코드는 한 입방체의 삼각형들을 정의하는 색인 버퍼를 사나 생성하고, 그에 대한 뷰를 생성하고, 그것을 파이프라인에 묶는 방법을 보여준다.
std::uint16_t indices[] = {
// 앞면
0, 1, 2,
0, 2, 3,
// 뒷면
4, 6, 5,
4, 7, 6,
// 왼쪽 면
4, 5, 1,
4, 1, 0,
// 오른쪽 면
3, 2, 6,
3, 6, 7,
// 윗면
1, 5, 6,
1, 6, 2,
// 아랫면
4, 0, 3,
4, 3, 7
};
const UINT ibByteSize = 36*sizeof(std::uint16_t);
ComPtr<ID3D12Resource> IndexBufferGPU = nullptr;
ComPtr<ID3D12Resource> IndexBufferUploader = nullptr;
IndexBufferGPU = CreateDefaultBuffer(
md3dDevice.Get(), mCommandList.Get(), indices, ibByteSize, IndexBufferUploader
)
D3D12_INDEX_BUFFER_VIEW ibv;
ibv.BufferLocation = IndexBufferGPU->GetGPUVirtualAddress();
ibv.Format = DXGI_FORMAT_R16_UINT;
ibv.SizeInBytes = ibByteSize;
mCommandList->IASetIndexBuffer(&ibv);
마지막으로, 색인들을 이용해서 기본더형을 그리려면 DrawInstanced 메서드가 아니라 ID3D12GraphicsCommandList::DrawIndexedInstanced 메서드를 사용해야 한다.
/*
@param IndexCountPerInstance : 그리기에 사용할 색인들의 개수 (인스턴스당)
@param InstanceCount : 그릴 인스턴스 개수. 인스턴싱이라고 부르는 고급기법에는 여러 개의 인스턴스를 그릴 수도 있다.
@param StatIndexLocation : 색인버퍼의 색인들 중 이 그리기 호출에서 사용할 첫 색인의 색인 (0기반)
@param BaseVertexLocation : 이 그리기 호출에 쓰이는 색인들에 더할 정수 값. 더한 결과를 최종 색인으로 사용해서 정점 버퍼에서 정점을 가져온다.
@param StartInstanceLocation : 그급기법인 인스턴싱에 쓰인다. 기본 0
*/
void ID3D12GraphicsCommandList::DrawIndexedInstanced(
UINT IndexCountPerInstance,
UINT InstanceCount,
UINT StartIndexLocation,
INT BaseVertexLocation,
UINT StartInstanceLocation
);
Vertex vertex, D3D12_VERTEX_BUFFER_VIEW, uint16_t Index, D3D12_INDEX_BUFFER_VIEW D3D12GraphicsCommandList::IASetPrimitiveTopology D3D12GraphicsCommandList::IASetVertexBuffers; D3D12GraphicsCommandList::IASetIndexBuffer; D3D12GraphicsCommandList::DrawInstanced; D3D12GraphicsCommandList::DrawIndexedInstanced;
이해를 돕기 위해 다음과 같은 상황을 예로 들겠다. 구, 상자(직육면체) , 원기둥 으로 이루어진 장면을 그린다고 하자. 처음에는 세 물체에 각자 개별적인 정점 버퍼와 색인 버퍼가 있다. 각 지역 색인 버퍼의 색인들은 그에 해당하는 지역 정점 버퍼를 기준으로 한다. 그러나 실제로 장면을 그릴 때에는 구, 상자, 원기둥의 정점들과 색인들을 아래 그림과 같은 하나의 전역 정점 버퍼와 하나의 색인 버퍼로 합친다고 하자. (정점 버퍼들과 색인 버퍼들을 합치는 한가지 이유는 정점 버퍼나 색인 버퍼의 전환에 따른 API 추가 비용을 피하고자 하는 것이다. 사실 버퍼 전환이 병목이 될 가능성이 별로 없지만, 그래도 작은 정점 버퍼들과 색인 버퍼들이 많이 있다면, 버퍼들을 합치는 것이 성능에 도움이 될 수 있다.)
버퍼들을 합치고 나면 색인들이 잘못된 정점을 가리키게 된다. 개별 색인 버펑의 색인들은 자신만의 개별 정점 버퍼를 기준으로 한것이지 전체 정점 버퍼를 기준으로 한 것이 아니기 때문이다. 따라서 전역 정점 버퍼에 맞게 색인들을 다시 계산할 필요가 있다. 예를 들어 원래의 상자 색인들은 상자 정점들의 색인이 다음과 같다는 가정하에 계산된 것이다.
0, 1, ..., numBoxVertices-1
그러나 버퍼들을 그림과 같이 병합하고 나면 상자 정점들의 실제 색인은 다음이 된다.
firstBoxVertexPos, firstBoxVertexPos+1, ... firstBoxVertexPos+numBoxVertices-1
따라서 색인 버퍼의 색인들을 갱신하려면 모든 상자 색인에 firstBoxVertexPos (정점 버퍼에서 첫번째 상자 정점의 위치) 를 더해야 한다. 마찬가지로 모든 원기둥 색인에는 firstCylVertexPos 를 더해야 한다. 구의 색인들은 변경할 필요가 없음을 주목하기 바란다. (첫 구의 정점이 위치 0에 있으므로). 전역 정점 버퍼에서 한 물체의 첫번째 정점 위치를 그 물체의 기준 정점 위치 (base vertex location) 라고 부르기로 하자. 일반적으로, 한 물체의 새 색인들은 각 색인에 해당 기준 정점 위치를 더한 것이다. 그런데 이러한 색인 갱신 작업을 GPU 에서 직접 수행할 필요가 없다. DrawIndexedInstanced 의 넷째 매개변수로 기준 정점 위치를 지정하면 Direct3D 가 처리해준다. 이제 구와 상자, 원기둥을 다음과 같은 세번의 그리기 호출로 그릴 수 있다.
mCmdList->DrawIndexedInstanced(numSphereIndices, 1, 0, 0, 0 );
mCmdList->DrawIndexedInstanced(numBoxIndices, 1, firstBoxIndex, firstBoxVertexPos, 0);
mCmdList->DrawIndexedInstanced(numCylIndices, 1, firstCylIndex, firstCylVertexPos, 0);
다음은 간단한 정점 셰이더의 구현이다.
cbuffer cbPerObject : register(b0)
{
float4x4 gWorldViewProj;
};
void VS(
float3 iPosL : POSITION,
float4 iColor : COLOR,
out float4 oPosH : SV_POSITION,
out float4 oColor : COLOR
){
// 동차 절단 공간으로 변환한다.
oPosH = mul(float4(iPosL, 1.0f), gWorldViewProj);
// 정점 색상을 그대로 픽셀 셰이더에 전달한다.
oColor = iColor;
}
셰이더는 HLSL (high level shading language; 고수준 셰이딩 언어) 이라고 하는 언어로 생성한다. 이 언어는 문법이 c++ 과 비슷하기 때문에 c++ 프로그래머라면 쉽게 배울 수 있다. c++ 소스 코드오아 마찬가지로, 일반적으로 셰이더 소스 코드는 보통의 텍스트 파일로 작성한다 (확장자는 .hlsl). 본질적으로 정점 셰이더는 하나의 함수이다. 지금 예에서는 VS 라는 이름을 사용했지만, 유효한 함수 이름이면 어떤 것이든 정점 셰이더의 이름으로 사용할 수 있다. 지금 예의 정점 셰이더는 매개변수가 네개 인데, 처음 둘은 입력 매개 변수이고 나머지 둘은 출력 매개변수(out 키워드가 붙어있으므로) 이다. HLSL 에는 참조나 포인터가 없으므로, 함수가 여러 개의 값을 돌려주려면 구조체를 사용하거나 out 이 지정된 매개변수를 사용해야 한다. HLSL 에서 함수는 항상 인라인화 된다.
처음 두 입력 매개변수는 정점 셰이더의 입력 서명 (Input Signature) 을 형성한다. 이들은 형재의 그리기 작업에 쓰이는 정점 구조체의 멤버들에 대응된다. 매개변수 의미소 ":POSITION" 과 ":COLOR"는 정점 구조체의 멤버들을 정점 셰이더 입력 매개변수들에 대응시키는 역할을 한다. 출력 매개변수에도 의미소가 부여되어 있다 (":SV_POSITION" 과 ":COLOR"). 이들은 정점 셰이더의 출력을 파이프라인의 다음 단계(기하셰이더 또는 픽셀셰이더) 의 해당 입력에 대응 시키는 역할을 한다. SV_POSITION 의미소가 특별한 의미소임을 주목하기 바란다. SV는 이 것이 system value(시스템 값) 의미소임을 뜻한다. 이 의미소는 해당 정점 셰이더 출력 성분이 정점의 위치(동차 절단 공간에서의)를 담고 있음을 나타낸다. GPU 는 절단, 깊이 판정, 래스터화 등등 다른 특성들에는 적용하지 않는 특별한 연산들을 위치에 적용하므로, 이처럼 SV_POSITION 의미소를 지정해서 GPU 에게 이것이 위치를 담은 출력 성분임을 알려주어야 한다. 반면 "COLOR"는 그냥 응용프로그램이 D3D12_INPUT_ELEMENT_DESC 배열을 통해 지정한 이름이다. 시스템 값 의미소가 아닌 출력 매개변수 의미소에는 HLSL 의 유효한 식별자이기만 하면 그 어떤 이름도 사용할 수 있다.
정점 셰이더의 함수 본문의 첫줄은 gWorldViewProj 를 곱해서 정점을 LocalSpace(지역공간, 국소공간) 에서 동차 절단 공간으로 변환한다.
// 동차공간으로 변환한다.
oPosH = mul(float4(iPosL, 1.0f), gWorldViewProj);
생성자 구문 float4(iPosL, 1.0f) 는 하나의 4차원 벡터를 생성한다. 이는 float4(iPosL.x, iPosL.y, iPosL.z, 1.0f) 와 같다. 정점의 위치는 벡터가 아니라 점이므로 넷째 성분을 1로 두었다. (w = 1) float2 형식과 float3 형식은 각각 2차원 벡터, 3차원 벡터 를 나타낸다. 행렬은 변수 gWorldViewProj 는 상수버퍼라고 부르는 버퍼에 들어 있는 것인데, 상수 버퍼에 관해서는 다음절에 논의한다. mul 은 HLSL 의 내장함수로, 벡터 대 행렬 곱셈을 수행한다. 덧붙이자면, mul 함수는 여러 크기의 행렬 곱셈들에 대해 중복 적재되어 있다. (function overload) 예를들어 이 함수를 이용해서 두개의 4x4 행렬이나 두개의 3x3 행렬을 곱팔 수 있으며, 1x3 행렬(행벡터) 와 3x3 행렬을 곱할 수도 있다.
셰이더 본문의 마지막 줄은 입력 색상을 그대로 출력 매개변수에 복사한다. 이에 의해 색상이 파이프라인의 다음단계에 공급된다.
oColor = iColor
이 정점 셰이더를 다음과 같이 구현할 수도 있다. 하는 일은 동일하지만, 반환형식과 입력 서명에 구조체를 사용한다는 점이 다른다. 덕분에 매개변수 목록이 짧아졌다.
cbuffer cbPerObject : register(b0)
{
float4x4 gWorldViewProj;
};
struct VertexIn
{
float3 PosL : POSITION;
float4 Color : COLOR;
}
struct VertexOut
{
float4 PosH : SV_POSITION;
float4 Color : COLOR
}
VertxOut VS(VertexIn vin)
{
VertexOut vOut;
// 동차 절단 공간으로 변환한다.
vout.PosH = mul(float4(vin.PosL,1.0f), gWorldViewProj);
// 정점 색상을 그대로 픽셀 셰이더에 전달한다.
vout.Color = vin.Color;
return vout;
}
참고로 기하 셰이더를 사용하지 않는다면, 정점 셰이더의 출력은 반드시 의미소가 SV_POSITION 인, 동차 절단 공간에서의 정점 위치이어야 한다. 기하 셰이더가 없을 때 하드웨어는 정점 셰이더를 떠난 정점들이 동차 절단 공간에 있다고 가정하기 때문이다. 기하 셰이더를 사용하는 경우에는 동차 절단 공간 위치의 출력을 기하 셰이더에 미룰 수 있다. 정점 셰이더(또는 기하 셰이더)가 원근 나누기 까지는 수행하지는 말아야 한다. 투영행렬을 곱하는 부분만 책임지면 된다. 원근 나누기는 나중에 하드웨어가 수행한다.
파이프라인에 공급되는 정점들의 특성들과 정점 셰이더의 매개변수들 사이에는 연관관계가 존재한다. 그러한 관계 정의하는 것은 입력 배치 서술이다. 파이프라인에 공급된 정점들이 정점 셰이더가 기대하는 모든 입력을 제공하지 못하면 오류가 발생한다.
예를들어 다음의 정점 셰이더 입력 서명과 정점 자료는 호환되지 않는다.
// C++ 응용프로그램 코드
struct Vertex
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
}
D3D12_INPUT_ELEMENT_DESC[] = {
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_PER_VERTEX, 0},
{"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_PER_VERTEX, 0}
};
// 정점 셰이더
struct VertexIn
{
float3 PosL : POSITION;
float4 Color : COLOR;
float3 Normal : NORMAL;
}
sturct VertexOut
{
float4 PosH : SV_POSITION;
flaot4 Color : COLOR;
}
VertexOut VS(VertexIn vin){
...
}
후에 ID3D12PipelineState 객체를 생성할 때 반드시 입력 배치 서술과 정점 셰이더를 함께 지정해야 한다. 그러면 Direct3D 는 주어진 입력 배치 서술과 정점 셰이더가 호환되는지 점검한다. 정점 자료와 입력 서명이 정확하게 일치할 필요는 없다. 중요한 것은 정점 셰이더가 기대하는 모든 정점의 정보를 정점 자료가 제공하느냐 이다. 따라서, 정점 셰이더가 사용하지 않는 추가적인 정보를 정점 자료가 제공하는 것은 오류가 아니다.
예를 들어 다음의 정점 자료와 정점 셰이더는 호환된다.
// C++ 응용프로그램 코드
struct Vertex{
XMFLOAT3 Pos;
XMFLOAT4 Color;
XMFLOAT3 Normal;
}
D3D12_INPUT_ELEMENT_DESC desc[] = {
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_PER_VERTEX_DATA, 0},
{"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_PER_VERTEX_DATA, 0},
{"NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 28, D3D12_INPUT_PER_VERTEX_DASTA, 0}
};
// 정점 셰이더 코드
struct VertexIn
{
float3 PosL : POSITION;
float4 Color : COLOR;
}
struct VertexOut
{
float4 PosH : SV_POSITION;
float4 Color : CORLOR;
}
VertexOut VS(VertexIn vin)
{
...
}
다음으로, 정점 구조체와 입력 서명의 정점 특성들이 부합하긴 하지만 구체적인 형식이 다른 경우를 살펴보자. 다음 예에서 정점의 색상 특성 (Color 성분) 의 자료 형식을 주목하기 바란다.
// c++ 응용 프로그램
struct Vertex
{
XMFLOAT3 Position;
XMFLOAT4 Color;
}
D3D12_INPUT_ELEMENT_DESC desc[] = {
{"POSITION", 0 , D3D12_FORMAT_R32B32G32_FLOAT, 0, 0, D3D12_INPUT_PER_VERTEX_DATA, 0},
{"COLOR", 0, D3D12_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_PER_VERTEX_DATA, 0}
};
// 정점 셰이더
struct VertexIn
{
float3 PosL : POSITION;
int4 Color : COLOR; // float 4 가 아니라 int 인점!!
}
struct VertexOut
{
float4 PosH : SV_POSITION;
float4 Color : Color;
}
VertexOut VS(VertexIn vin)
{
...
}
이는 사실 위법이 아니다. Direct3D 는 입력 레지스터 비트들의 재해석 (reinterpret) 을 허용하기 때문이다. 그러나 VC++ 의 디버그 출력 창은 다음과 같은 경고 메시지를 낸다.
간단히 요약하자면, 셰이더 레지스터에 담긴 비티들을 어떻게 해석해서 사용하는지는 응용 프로그램과 셰이더의 몫이므로 입력 자료 형식의 불일치가 오류는 아니지만 혹시 셰이더 작성자가 의도적으로 그런 것이 아니라 실수일 수도 있으므로 경고 메시지를 출력했다는 뜻이다.
InputLayout 정의 |
정점 데이터를 GPU 에 어떻게 전달할지 정의
셰이더와 정점 데이터를 연결
|
D3D12_INPUT_LAYOUT_DESC
D3D12_INPUT_ELEMENT_DESC
D3D12_GRAPHICS_PIPELINE_STATE_DESC
CreateGraphicsPipelineState
|
Primitive Topology 설정 |
어떤 도형(삼각형, 선, 점) 으로 그릴 것인지 설정
|
D3D12_PRIMITIVE_TOPOLOGY
IASetPrimitiveTopology
|
VertexBuffer 바인딩 |
GPU 에 정점 데이터 넘겨주기
|
D3DD12_VERTEX_BUFFER_VIEW
IASetVertexBuffers
|
IndexBuffer 바인딩 |
정점 버퍼를 사용할 때 함께 사용할 인덱스 데이터 설정
|
D3D12_INDEX_BUFFER_VIEW
IASetIndexBuffer
|
드로우콜 호출 |
GPU 가 정점 버퍼와 인덱스 버퍼를 사용하여 렌더링 실행
|
DrawIndexedInstanced
|