67일차 DirectX 그래픽 파이프라인 정리
TextureRect::TextureRect(Vector3 position, Vector3 size, float rotation)
: position(position), size(size), rotation(rotation)
{
// vertices
{
vertices.assign(4, VertexTexture());
vertices[0].position = verticesLocalPosition[0] = Vector3(-0.5f, -0.5f, 0.0f);
vertices[1].position = verticesLocalPosition[1] = Vector3(0.5f, 0.5f, 0.0f);
vertices[2].position = verticesLocalPosition[2] = Vector3(0.5f, -0.5f, 0.0f);
vertices[3].position = verticesLocalPosition[3] = Vector3(-0.5f, 0.5f, 0.0f);
vertices[0].uv = Vector2(0, 1);
vertices[1].uv = Vector2(1, 0);
vertices[2].uv = Vector2(1, 1);
vertices[3].uv = Vector2(0, 0);
}
// Vertex Buffer
{
vb = new VertexBuffer();
vb->Create(vertices, D3D11_USAGE_DYNAMIC);
}
// Index Buffer
{
indices = { 0,1,2,0,3,1 };
ib = new IndexBuffer();
ib->Create(indices, D3D11_USAGE_IMMUTABLE);
}
// Vertex Shader
{
vs = new VertexShader();
vs->Create(ShaderPath + L"VertexTexture.hlsl", "VS");
}
// Pixel Shader
{
ps = new PixelShader();
ps->Create(ShaderPath + L"VertexTexture.hlsl", "PS");
}
// InputLayout
{
il = new InputLayout();
il->Create(VertexTexture::descs, VertexTexture::count, vs->GetBlob());
}
// World Buffer
{
wb = new WorldBuffer();
}
}
Vertex Buffer
각 버텍스의 정점을 Buffer로 뭉쳐놓아서 그래픽카드로 전달하기 위한 데이터 뭉치
Index Buffer
각 버텍스의 정점간 이어지는 선을 Buffer로 뭉쳐놓아서 그래픽카드로 전달하기 위한 데이터 뭉치
Vertex Shader
각 버텍스의 정점을 카메라에 이어주는 쉐이더. 카메라의 위치 기준값의 앞에다가 가져다 놓는 역할을 한다.
Pixel Shader
각 버텍스의 픽셀 Color값을 설정해주는 쉐이더. Blob에 각 버텍스의 색정보를 대입하여 설정할 수 있다.
Input Layout
버텍스 쉐이더에서 생성한 Blob의 위치좌표를 가져다가 월드버퍼에 대입시켜주는 레이아웃.
World Buffer
각 원점정보를 SRT를 사용하여 원하는 위치값에다가 대입시켜주는 버퍼
SRV
AddSRV를 사용하여 wstring값의 path를 가져와. 설정하려는 이미지 픽셀이 위치한 png파일을 가지게 되는 역할
SETIA
생성된 버퍼를 레지스터에 옮겨주는 역할
Vertex Buffer
class VertexBuffer
{
public:
~VertexBuffer();
// 정점 자료형으로 어떤게 들어올지 모르기 때문에 템플릿 사용
template<typename T>
void Create(const vector<T>& vertices, const D3D11_USAGE& usage = D3D11_USAGE_DEFAULT);
ID3D11Buffer* GetResource() { return buffer; }
uint GetStride() { return stride; }
uint GetOffset() { return offset; }
uint GetCount() { return count; }
void SetIA();
private:
ID3D11Buffer* buffer = nullptr;
uint stride = 0; // 정점 버퍼에서 한 정점의 크기를 나타내는 값
uint offset = 0; // 버퍼에서 읽기 시작할 위치
uint count = 0; // 정점 버퍼에서 읽을 정점 개수
};
template<typename T>
inline void VertexBuffer::Create(const vector<T>& vertices, const D3D11_USAGE& usage)
{
stride = sizeof(T); // 정점 크기 저장
count = vertices.size(); // 정점 개수 저장
D3D11_BUFFER_DESC desc; // 버퍼 생성을 위한 구조체 선언
ZeroMemory(&desc, sizeof(D3D11_BUFFER_DESC)); // 구조체 초기화
// desc 설정
{
desc.Usage = usage; // 버퍼의 사용 용도 설정
desc.BindFlags = D3D11_BIND_VERTEX_BUFFER; // 정점 버퍼로 사용함을 정의
desc.ByteWidth = stride * count; // 버퍼 크기
// CPU 접근 설정
switch (usage)
{
case D3D11_USAGE_DEFAULT:
case D3D11_USAGE_IMMUTABLE:
break;
case D3D11_USAGE_DYNAMIC:
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
break;
// CPU에서 접근 가능, GPU에서 사용할 수 있는 형태로 변환 가능한 버퍼
case D3D11_USAGE_STAGING:
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE | D3D11_CPU_ACCESS_READ;
break;
}
}
// Buffer 생성
{
D3D11_SUBRESOURCE_DATA subData; // 정점 데이터를 담을 구조체
ZeroMemory(&subData, sizeof(D3D11_SUBRESOURCE_DATA)); // 구조체 초기화
subData.pSysMem = vertices.data(); // 정점 데이터를 할당한 메모리에 복사
HRESULT hr = DEVICE->CreateBuffer(&desc, &subData, &buffer); // 정점 버퍼 생성
CHECK(hr); // 생성 실패 시 펑
}
}
버텍스의 위치좌표와 정점 사이즈를 계산하여 정점 크기를 stride에 저장해놓고,
count 에 정점 개수를 저장한다.
그후, 데이터를 삽입할 구조체를 선언한 뒤, 구조체를 zeroMemory로 초기화한다.
desc 에 대한 설정값은, 데이터 버퍼의 사이즈, 사용가능 여부, 이게 어떤 버퍼인지 의 값을 대입하게 된다.
switch를 사용하여, CPU가 접근이 가능한지, GPU가 접근이 가능한지에 대해 설정해준다.
그후, 정점 데이터를 담을 D3D11_SUBRESOURCE_DATA 구조체를 생성해준다음, 초기화 해준다.
정점 데이터를 할당된 메모리에 복사한 다음,
DEVICE->CreateBuffer(...)를 통하여, 기입된 데이터의 메모리를 전부 Buffer에 담아 hlsl에 보내준다.
즉, 위에 생성한 desc는 버퍼 자체의 헤더에 해당하고, 밑에서 생성한 subData는 실질적인 데이터의 모음이라고 생각하면 된다.
IndexBuffer
// 정점의 인덱스를 저장하는 버퍼
#pragma once
class IndexBuffer
{
public:
~IndexBuffer();
void Create(const vector<uint>& indices, const D3D11_USAGE& usage = D3D11_USAGE_DEFAULT);
ID3D11Buffer* Getresource() { return buffer; }
uint GetStride() { return stride; }
uint GetOffset() { return offset; }
uint GetCount() { return count; }
void SetIA();
private:
ID3D11Buffer* buffer = nullptr;
uint stride = 0;
uint offset = 0;
uint count = 0;
};
void IndexBuffer::Create(const vector<uint>& indices, const D3D11_USAGE& usage)
{
stride = sizeof(uint);
count = indices.size();
D3D11_BUFFER_DESC desc;
ZeroMemory(&desc, sizeof(D3D11_BUFFER_DESC));
// desc
{
desc.Usage = usage;
desc.BindFlags = D3D11_BIND_INDEX_BUFFER;
desc.ByteWidth = stride * count;
// CPU 접근 설정
switch (usage)
{
case D3D11_USAGE_DEFAULT:
case D3D11_USAGE_IMMUTABLE:
break;
case D3D11_USAGE_DYNAMIC:
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
break;
case D3D11_USAGE_STAGING:
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE | D3D11_CPU_ACCESS_READ;
break;
}
}
// Buffer 생성
{
D3D11_SUBRESOURCE_DATA subData;
ZeroMemory(&subData, sizeof(D3D11_SUBRESOURCE_DATA));
subData.pSysMem = indices.data();
HRESULT hr = DEVICE->CreateBuffer(&desc, &subData, &buffer);
CHECK(hr); // 생성 실패 시 펑
}
}
각 버텍스가 연결되는 선을 만들어주는 역할을 한다.
위의 버텍스 버퍼와 큰 차이점이 없이 Buffer를 생성하는 모습을 확인할 수 있다.
Buffer에 들어가는 데이터 포멧들은 전부 uint형임을 확인할 수 있는데,
이는, 각 배열의 숫자가 버텍스의 연결되는 숫자이기 때문이다.
어차피 버텍스 정보는 다른 레지스터에 존재하기 때문에 IndexBuffer에는 해당되는 버텍스가 어떤식으로 연결되는지만 나타내주면 되기 때문,
Vertex Shader
// 정점들의 위치, 색상, uv좌표 등의 속성을 계산
#pragma once
#include "IShader.h"
class VertexShader : public IShader
{
public:
~VertexShader();
virtual void Create(const wstring path, const string entryName) override;
virtual void Clear() override;
virtual void SetShader() override;
ID3DBlob* GetBlob() { return blob; }
ID3D11VertexShader* GetResource() { return shader; }
private:
ID3D11VertexShader* shader = nullptr;
ID3DBlob* blob = nullptr;
};
void VertexShader::Create(const wstring path, const string entryName)
{
// 쉐이더 경로와 엔트리 이름을 저장
this->path = path;
this->entryName = entryName;
// 쉐이더 컴파일
CompileShader(path, entryName, "vs_5_0", &blob);
// 버텍스 쉐이더 생성
HRESULT hr = DEVICE->CreateVertexShader
(
blob->GetBufferPointer(),
blob->GetBufferSize(),
nullptr,
&shader
);
CHECK(hr);
}
쉐이더는 버퍼와 다르게 설정된 hlsl로 이동하여 픽셀데이터를 수정하게 되는데,
그래서 생성자 역할을 하는 Create에서는 해당되는 경로와 해당 hlsl에서 사용하게되는 펑션의 이름을 작성하게 된다.
쉐이더 컴파일 시. 경로, 함수이름, 그리고 어떤 버전의 vs로 컴파일하게 되는지. 또한 컴파일된 결과물을 어디에 저장하게 되는지를 작성하게 된다.
컴파일된 쉐이더는 shader에 들어가게 되는데,
DEVICE->CreateVertexShader에 인수값으로 삽입되어, blob에 들어가게된 위치값과 사이즈를 담게된다.
nullptr에 들어가는것은 ID3D11ClassLinkage 인데, 현재 쉐이더에 쉐이더를 그리기 전에 hlsl의 코드함수 정의부를 자세하게 나누는 알고리즘을 사용할 것인지 여부이다. 만약 해당되는 알고리즘이 존재하는 포인터 객체가 존재한다면 기입하면 된다.
PixelShader
// 렌더 타겟에 그려진 픽셀들의 최종 색상을 계산하는 역할
#pragma once
#include "IShader.h"
class PixelShader : public IShader
{
public:
~PixelShader();
virtual void Create(const wstring path, const string entryName) override;
virtual void Clear() override;
virtual void SetShader() override;
ID3DBlob* GetBlob() { return blob; }
ID3D11PixelShader* GetResource() { return shader; }
private:
ID3D11PixelShader* shader = nullptr;
ID3DBlob* blob = nullptr;
};
void PixelShader::Create(const wstring path, const string entryName)
{
// 쉐이더 경로와 엔트리 이름을 저장
this->path = path;
this->entryName = entryName;
// 쉐이더 컴파일
CompileShader(path, entryName, "ps_5_0", &blob);
// 버텍스 쉐이더 생성
HRESULT hr = DEVICE->CreatePixelShader
(
blob->GetBufferPointer(),
blob->GetBufferSize(),
nullptr,
&shader
);
CHECK(hr);
}
위의 버텍스 쉐이더와 비슷한 코드구조를 가지는데,
버텍스 쉐이더 생성시, ps_5.0 을 사용하여 컴파일하게 되며, 해당되는 쉐이더의 함수부를 지정하고, hlsl의 경로를 지정하는것 까지 전부 동일하게 진행한다.
어차피 정점데이터는 이미 레지스터에 존재함으로 정점데이터는 따로 수집하지 않는다.
그리고 SetSHader()함수를 사용하여 레지스터에 값을 넘겨주게 된다.
InputLayout
// 정점 데이터의 레이아웃을 정의하고 설정하는 역할
#pragma once
class InputLayout
{
public:
~InputLayout();
void Create(D3D11_INPUT_ELEMENT_DESC* descs, uint count, ID3DBlob* blob);
void SetIA();
private:
ID3D11InputLayout* inputLayout = nullptr;
};
/*
InputLayout
- 정점 버퍼의 데이터 구조를 정의
- 정점 데이터의 각 요소(위치, 색상, 텍스처 좌표 등)의 형식, 크기, 순서 등을 지정
- IA 단계에서 정점 셰이더로 데이터를 전달하기 전에 정점 데이터를 올바르게 해석하는데 사용
*/
void InputLayout::Create(D3D11_INPUT_ELEMENT_DESC* descs, uint count, ID3DBlob* blob)
{
// 하나라도 없으면 프로그램 종료
if (!descs || !count || !blob)
CHECK(false);
HRESULT hr = DEVICE->CreateInputLayout
(
descs, // InputLayout을 구성하는 각 선언들의 배열
count, // InputLayout을 구성하는 선언의 수
blob->GetBufferPointer(), // 셰이더 코드를 포함하는 블롭에 대한 포인터
blob->GetBufferSize(), // 셰이더 코드 블롭의 크기
&inputLayout // 리턴받을 변수
);
CHECK(hr);
// blob = 메모리 블록
}
생성된 정점의 위치좌표를 카메라에 대입하여 카메라에 보이는대로의 위치좌표로 수정하는 역할을 한다.
2D객체 생성의 경우, Z값을 사용하지 않음으로 크게 의미가 없을 수 있다.
입력받는 각 항목에 대해, VectrxTexture에 해당하는 descs와 count를 가져와 대입하게 된다.
카메라의 position은 이미 레지스터에 기입이 되었기 때문에, 여기서는 다루지 않고 hlsl에서 연산을 진행하게 된다.
WorldBuffer
class WorldBuffer : public ShaderBuffer
{
public:
WorldBuffer() : ShaderBuffer(&data, sizeof(Data))
{
D3DXMatrixIdentity(&data.world);
}
void SetWorld(Matrix world)
{
D3DXMatrixTranspose(&data.world, &world);
}
struct Data
{
Matrix world;
};
private:
Data data;
};
.WorldBuffer는 아주 간단하게 이루어지는데, 이미지 좌표연산이 float4*4의 행렬연산으로 이루어지기 때문에,
Matrix형태의 world를 한개 생성해주고, 해당되는 Data를 사용하여 비어있는 world를 한개 생성해주는게 다이다.
SRV
SRV::Get()->AddSRV(String::ToString(path), srv);
#pragma once
class SRV : public SingletonBase<SRV>
{
public:
friend SingletonBase<SRV>;
private:
SRV();
~SRV();
public:
void AddSRV(string path, ID3D11ShaderResourceView* srv);
ID3D11ShaderResourceView* GetSRV(string path);
private:
map<string, ID3D11ShaderResourceView*> srvs;
};
#include "Framework.h"
#include "SRV.h"
SRV::SRV()
{
}
SRV::~SRV()
{
for (auto it = srvs.begin(); it != srvs.end(); ++it)
SAFE_RELEASE(it->second);
srvs.clear();
cout << "SRV Manager : srvs clear complete" << endl;
}
void SRV::AddSRV(string path, ID3D11ShaderResourceView* srv)
{
if (srv == nullptr || path == "")
{
cout << "SRV Manager : Add Error" << endl;
return;
}
srvs.insert(make_pair(path, srv));
}
ID3D11ShaderResourceView* SRV::GetSRV(string path)
{
if (srvs.find(path) != srvs.end())
return srvs.find(path)->second;
else
{
cout << "SRV Manager : Can't find path" << endl;
return nullptr;
}
}
SRV는 넣으려는 png파일의 경로를 받아서 srv에 대입하여 생성해주는 역할이다.
픽셀데이터를 가져와서 png파일을 float4 형태의 픽셀배열 형태로 저장하게 된다.
SetIA, SetShader
void TextureRect::Render()
{
vb->SetIA();
ib->SetIA();
il->SetIA();
DC->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
vs->SetShader();
ps->SetShader();
wb->SetVSBuffer(0);
DC->PSSetShaderResources(0, 1, &srv);
DC->DrawIndexed(ib->GetCount(), 0, 0);
}
void VertexBuffer::SetIA()
{
// 입력 어셈블러에 정점 버퍼를 입력하는 함수
DC->IASetVertexBuffers(0, 1, &buffer, &stride, &offset);
}
void VertexShader::SetShader()
{
DC->VSSetShader(shader, nullptr, 0);
}
void SetVSBuffer(uint slot)
{
MapData();
DC->VSSetConstantBuffers(slot, 1, &buffer);
}
SetIA를 통해 해당되는 레지스터로 레지스트 하게 된다. vb와 ib, il값이 전부 레지스터로 이동되어 데이터 처리과정을 준비한다.
hlsl에서 받은 데이터 버퍼를 분리하여 데이터를 가공하게 된다.
SetShader또한 마찬가지로 해당되는 레지에 값을 이동시키고, 이동된 정점데이터, 픽셀 데이터들을 전부 가공처리 하게된다.
위 항목들에서 들어가는 숫자0은 데이터가 들어가게될 레지스터 번호를 의미한다.
// cbuffer : 상수 버퍼 레지스터
// 상수 버퍼 레지스터 b0에 할당된 월드 행렬을 저장하는 상수 버퍼
cbuffer WorldBuffer : register(b0) // 0 ~ 127
{
matrix _world;
}
위와같이 hlsl에 등록되어 있을 경우, WorldBuffer 생성되면, 버퍼레지스터 b0에 matrix _world가 저장된다는 의미이다.