그래픽스(DirectX)

[DirectX11] Direct3D에서 그리기

gamzachips 2023. 5. 19. 19:25

정점과 input layout

 

정점은 공간적인 위치 뿐만 아니라 추가적인 데이터를 가지고 있을 수 있다고 했다. 

우리가 정의한 정점 형식을 만들기 위해서 먼저 이를 구조체로 만들어야 한다. 

 

struct Vertex
{
    XMFLOAT3 Pos;
    XMFLOAT4 Color;
};

 

 

그리고 D3D에게 각 구성요소를 가지고 무엇을 할지를 알려주기 위해서 정점 구조체에 대한 input layout desciption (D3D11_INPUT_ELEMENT_DESC)을 input layout (ID3D11InputLayout)에 제시해야 한다.

 

Input layout은 각 구조체에 대한 D3D11_INPUT_ELEMENT_DESC의 배열로 이루어져 있다. 

 

ComPtr<ID3D11InputLayout> _inputLayout;

 

D3D11_INPUT_ELEMENT_DESC vertexDesc[] =
{
    {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
    {"COLOR",    0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0}
};

 

D3D11_INPUT_ELEMENT_DESC의 구성은 다음과 같다. 

{
    "POSITION", //SemanticName : vertex shader input signiture 요소들에 매핑되는데 쓰인다
    0, //semanticIndex : 여러 set에 대해 이름을 동일하게 하고 인덱스로 구분할 때 사용
    DXGI_FORMAT_R32G32B32_FLOAT, //Format : 자료형. 3D 32bit float vector
    0, //InputSlot : 이 요소가 공급될 input 인덱스 슬롯. 16개 지원됨
    0, //AlignedByteOffset : 정점 구조체의 시작 위치와 정점 구성요소의 시작 위치 사이의 offset
    D3D11_INPUT_PER_VERTEX_DATA, //InputSlotClass : 다른 값들은 인스턴싱에 쓰인다
    0  //InstanceDataStepRate : 다른 값들은 인스턴싱에 쓰인다
}

 

 

이제 ID3D11Device::CreateInputLayout 메서드를 호출해 input layout을 생성한다. 

 

D3DX11_PASS_DESC passDesc;
_tech->GetPassByIndex(0)->GetDesc(&passDesc);

HR(_device->CreateInputLayout(vertexDesc, 2, passDesc.pIAInputSignature, passDesc.IAInputSignatureSize, _inputLayout.GetAddressOf()));

 

마지막으로, 생성한 input layout을 바운딩한다.

 

_deviceContext->IASetInputLayout(_inputLayout.Get());

 

 

 

 

Vertex Buffers

 

GPU가 정점의 배열에 접근할 수 있게 하기 위해서 buffer라고 부르는 특별한 리소스 구조체(ID3D11Buffer )를 배치해야 한다. 

D3D 버퍼는 데이터만 저장하는 것이 아니라 어떻게 정점이 접근되고 렌더링 파이프라인의 어디에 바운딩 되어야 하는지도 설명한다.

 

정점을 저장하는  vertex buffer를 만들기 위해서 먼저 버퍼를 설명하는 구조체인 D3D11_BUFFER_DESC 을 채우고,  D3D11_SUBRESOURCE_DATA를 채워야 한다. 

 

Vertex vertices[] =
{
    { XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4((const float*)&Colors::White)   },
    { XMFLOAT3(-1.0f, +1.0f, -1.0f), XMFLOAT4((const float*)&Colors::Black)   },
    { XMFLOAT3(+1.0f, +1.0f, -1.0f), XMFLOAT4((const float*)&Colors::Red)     },
    { XMFLOAT3(+1.0f, -1.0f, -1.0f), XMFLOAT4((const float*)&Colors::Green)   },
    { XMFLOAT3(-1.0f, -1.0f, +1.0f), XMFLOAT4((const float*)&Colors::Blue)    },
    { XMFLOAT3(-1.0f, +1.0f, +1.0f), XMFLOAT4((const float*)&Colors::Yellow)  },
    { XMFLOAT3(+1.0f, +1.0f, +1.0f), XMFLOAT4((const float*)&Colors::Cyan)    },
    { XMFLOAT3(+1.0f, -1.0f, +1.0f), XMFLOAT4((const float*)&Colors::Magenta) }
};

D3D11_BUFFER_DESC vbd;
vbd.Usage = D3D11_USAGE_IMMUTABLE;
vbd.ByteWidth = sizeof(Vertex) * 8;
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER; //vertex buffer
vbd.CPUAccessFlags = 0;
vbd.MiscFlags = 0;
vbd.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA vinitData;
vinitData.pSysMem = vertices;
HR(_device->CreateBuffer(&vbd, &vinitData, _vertexBuffer.GetAddressOf()));

 

 

vertex buffer가 만들어지면, 정점을 파이프라인에 input으로 주기 위해서 device의 input slot에 바운딩해야한다. 

 

uint32 stride = sizeof(Vertex);
uint32 offset = 0;
_deviceContext->IASetVertexBuffers(0, 1, _vertexBuffer.GetAddressOf(), &stride, &offset);

 

 

인덱스와 Index Buffers

마찬가지로 GPU가 인덱스에 접근할 수 있어야 하므로,  index buffer를 두어야 한다. 

이는 vertex buffer만드는 것과 비슷하다. 

uint32 indices[] = {
    // front face
    0, 1, 2,
    0, 2, 3,

    // back face
    4, 6, 5,
    4, 7, 6,

    // left face
    4, 5, 1,
    4, 1, 0,

    // right face
    3, 2, 6,
    3, 6, 7,

    // top face
    1, 5, 6,
    1, 6, 2,

    // bottom face
    4, 0, 3,
    4, 3, 7
};

D3D11_BUFFER_DESC ibd; 
ibd.Usage = D3D11_USAGE_IMMUTABLE;
ibd.ByteWidth = sizeof(uint32) * 36;
ibd.BindFlags = D3D11_BIND_INDEX_BUFFER; //index buffer
ibd.CPUAccessFlags = 0;
ibd.MiscFlags = 0;
ibd.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA iinitData;
iinitData.pSysMem = indices;
HR(_device->CreateBuffer(&ibd, &iinitData, _indexBuffer.GetAddressOf()));

 

 

마찬가지로 index buffer를 사용하려면 파이프라인에 바운딩해야 한다. 

 

_deviceContext->IASetIndexBuffer(_indexBuffer.Get(), DXGI_FORMAT_R32_UINT, 0);

 

Vertex Shader

Shader는 HLSL(hight level shading language) 언어로 작성한다. 

(현재 effect는 공식적으로 지원되지 않는 라이브러리이지만 교재에서는 effect file(fx)로 작성한다. )

 

cbuffer cbPerObject
{
	float4x4 gWorldViewProj;
};

struct VertexIn
{
	float3 PosL  : POSITION;
	float4 Color : COLOR;
};

struct VertexOut
{
	float4 PosH  : SV_POSITION;
	float4 Color : COLOR;
};

VertexOut VS(VertexIn vin)
{
	VertexOut vout;
	
	// Transform to homogeneous clip space.
	vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj);
	
	// Just pass vertex color into the pixel shader.
	vout.Color = vin.Color;
    
	return vout;
}

위에서, VS 함수가 vertex shader 이다. (함수 이름은 아무거나 지정해도 상관없다) 

 

input, output 변수가 위치와 색상으로 각각 2개씩인데, HLSL에는 포인터나 참조가 없기 때문에 여러 개의 값을 리턴하기 위해서는 구조체를 사용하거나 out 매개변수를 사용해야 한다. 

 

VertexIn에서 변수의 오른쪽에있는 구문 " :POSITION" 과 ":COLOR"는 위에서 만들었던 vertex 구조체의 변수들을  매핑하는 데 쓰인다. 

VertexOut에서도 마찬가지로 ":SV_POSITION", ":COLOR" 구문이 있는데, 이는 vertex shader의 ouptut을 다음 단계에서 대응하는 input에 매핑하는 데 쓰인다. 

 

SV_POSITION에서 SV는 system value를 의미한다. 정점의 위치를 가지는 output 요소의 경우 SV_POSITION으로 나타내야 한다. 정점의 위치는 다른 요소들과는 다르게 clipping과 같은 연산에 관여하기 때문에 다르게 다루어지기 때문이다.

 

 

Constant Buffers

위의 코드에서 이 부분은 constant buffer를 정의한다.

cbuffer cbPerObject
{
	float4x4 gWorldViewProj;
};

constant buffer는 셰이더가 접근할 수 있는 다른 변수들을 저장하는 데이터 블록이다.

위의 경우 world, view, projection 행렬이 결합된 데이터를 가지고 있다.

 

constant buffer에 있는 데이터는 정점마다 달라지는 게 아니지만, 

effect 프레임워크를 통해, C++ 코드로 런타임에 constant buffer의 내용을 업데이트할 수 있다,

 

world행렬이 물체마다 다르기 때문에, world, view, projection을 결합한 것도 물체마다 다르다.

따라서 각 물체를 그리기 전에 gWorldViewProj 변수를 업데이트해야한다. 

 

constant buffer를 업데이트하면 그 안의 모든 변수를 업데이트하므로,

효율성을 위해 업데이트하는 빈도에 따라 구분해서 constant buffer를 구성하는 것이 좋다. 

 

 

Pixel Shader

Rasterizer단계에서, vertex shader의 output으로 나온 정점 속성들은 삼각형의 픽셀들에 걸쳐 보간되어서 pixel shader에 input으로 들어간다. 

 

주어진 input에 대해 pixel shader는 각 pixel 조각들에 대해 색상 값을 계산한다. 

depth stencil 테스트에서 걸린 일부 pixel 조각은 살아남아서 후면 버퍼를 형성하지 않게 될 수 있다. 

 

float4 PS(VertexOut pin) : SV_Target
{
    return pin.Color;
}

위의 pixel shader는 4D 색상 값을 리턴하는데 단순히 보간된 색상값을 리턴하고 있다.

SV_TARGET 구문은 리턴 값의 타입이 렌더 타겟 형식에 매치되어야 한다는 것을 나타낸다. 

 

 

 

Render States

Direct3D는 우리가 변경해주기 전까지 현재의 상태에 있는 state machine이며, 

Direct3D를 구성하는 데 사용할수 있는 설정을 캡슐화하는 상태 그룹이 있다. 

ID3D11RasterizerState, ID3D11BlendState, ID3D11DepthStencilState가 있다. 

 

대표적으로 ID3D11RasterizerState를 보자. 

ComPtr<ID3D11RasterizerState> _wireframeRS;

 

D3D11_RASTERIZER_DESC wireframeDesc;
ZeroMemory(&wireframeDesc, sizeof(D3D11_RASTERIZER_DESC));
wireframeDesc.FillMode = D3D11_FILL_WIREFRAME;
wireframeDesc.CullMode = D3D11_CULL_BACK;
wireframeDesc.FrontCounterClockwise = false;
wireframeDesc.DepthClipEnable = true;

HR(_device->CreateRasterizerState(&wireframeDesc, _wireframeRS.GetAddressOf()));

 

_deviceContext->RSSetState(_wireframeRS.Get());

 

프로그램에서 여러 개의 ID3D11RasterizerState 가 필요할 수 있다. 따라서 초기화할 때 여러 개를 만들고, 업데이트하고 그릴 때 필요에 따라 교체하면 된다. 

 

 

Direct3D는 이전 세팅으로 상태를 복원하지 않으므로 물체를 그릴 때 필요한 상태를 항상 설정해주어야 한다. 

 

각 상태 블럭은 디폴트 상태를 가지고 있다. RSSetState 메서드를 null로 설정해 로 디폴트 상태로 되돌릴 수 있다.

_deviceContext->RSSetState(0);

 

 

 

Effect 사용에 대한 설명은 생략한다...