PathManager를 만든이유는 셰이더같은 리소스들을 편하게 관리하기 위해서이다.
보통 리소스들은 리소스용 폴더에 모아놓고 사용을 하기때문에 PathManager로 해당 path를 쉽게 가져올 수 있도록 제작해두면 매우 편하다.
또한 파일 경로에서 상대경로만 알고 싶을때도 있고, 경로에 대한 여러가지 작업을 해야할 수도 있기때문에 이러한 매니저 클래스를 만든다.
//header
class PathManager :
public Singleton<PathManager>
{
SINGLE(PathManager);
public:
void Init();
const wstring& GetContentPath() { return content_path_; }
wstring GetRelativePath(const wstring& _filepath);
private:
wstring content_path_;
};
//cpp
#include "pch.h"
#include "PathManager.h"
#include <boost/filesystem.hpp>
using boost::filesystem::current_path;
using boost::filesystem::path;
PathManager::PathManager()
{
}
PathManager::~PathManager()
{
}
void PathManager::Init()
{
path fullPath = current_path();
fullPath += L"\\content\\";
content_path_ = fullPath.wstring();
}
wstring PathManager::GetRelativePath(const wstring& _filepath)
{
wstring relativePath = _filepath.substr(content_path_.size(), _filepath.size() - content_path_.size());
return relativePath;
}
Boost에 있는 FileSystem library를 이용해 경로를 가져왔다. 그리고 리소스용 폴더를 추가적으로 지정해서 셰이더를 쉽게 가져올수있는 content path를 만들었다.
참고로 PathManager는 초기화 순서가 거의 1순위라고 생각해야한다.(그래야 리소스들과 각종 셰이더를 불러올수있다.)
본격적으로 렌더링을 하기전에 먼저 셰이더 코드를 간단히 작성했다.
//std_vertex_shader.hlsl
#ifndef _STD_SHADER
#define _STD_SHADER
struct VertexInput
{
float3 position : POSITION;
float4 color : COLOR;
};
struct VertexOutput
{
float4 position : SV_POSITION;
float4 color : COLOR;
};
VertexOutput vs_main(VertexInput input)
{
VertexOutput output = (VertexOutput) 0;
output.position = float4(input.position, 1.f);
output.color = input.color;
return output;
}
#endif
아직 카메라가 없기 때문에 정점 정보를 그대로 출력을 해주도록 작성했다.
나중에 변환을 적용해야한다.
//std_pixel_shader.hlsl
struct VertexOutput
{
float4 position : SV_POSITION;
float4 color : COLOR;
};
float4 ps_main(VertexOutput input) : SV_TARGET
{
float4 outputColor = (float4) 0;
outputColor = float4(1.f, 0.f, 0.f, 1.f);
return outputColor;
}
그래픽파이프라인을 거쳐서 나온 영역에 붉은 색을 칠하도록 코드를 작성했다.
셰이더를 작성하고 그래프 파이프라인을 설명하는 이유는 개인적으로 공부를 할때 쭉 설명을 듣는것보단 연결지어 생각하는게 머리에 더 잘 남기 때문이다.
위 그림들의 파이프라인 스테이지를 정리한 내용이다. 위 이미지에서 사각형들은 고정 스테이지로 프로그래밍이 불가능한 부분을 말하고 타원형 부분들은 프로그래밍이 가능하다.
우리는 프로그래밍 가능한 부분중 VertexShader와 PixelShader에 대해서만 작성을 했다.
기본적인 삼각형을 그리는데는 이 두가지정도면 충분하기 때문이다.
Input Assembly를 제외한 고정스테이지의 경우 기본설정값이 있기 때문에 별도의 설정을 하지 않아도 렌더링을 진행하는데 큰 문제는 없다.
그렇기 때문에 우리는 그릴 삼각형좌표의 위치, 색상에 대한 정보만 입력을 하도록 코드를 만들어 주면 된다.
이제 삼각형을 그리기 위한 준비가 끝났다. 기본적으로 렌더링 진행과정은 다음과 같다
정점버퍼 만들기, 인덱스(색인)버퍼만들기 -> 셰이더 컴파일 -> 레이아웃지정 -> InputAssambly에 정점버퍼와 인덱스버퍼, 레이아웃 세팅-> 셰이더 세팅-> 드로우 콜
이러한 과정을 거치게 된다. 다음 코드들은 위의 과정을 하나씩 진행한 코드들이다.
// 삼각형 하나 만들기
array<Vertex, 4> arrVTX = {};
// 투영 좌표계 기준
arrVTX[0].position = Vec3(-0.5f, 0.5f, 0.5f);
arrVTX[0].color = Vec4(1.f, 1.f, 1.f, 1.f);
arrVTX[1].position = Vec3(0.5f, 0.5f, 0.5f);
arrVTX[1].color = Vec4(1.f, 1.f, 1.f, 1.f);
arrVTX[2].position = Vec3(0.5f, -0.5f, 0.5f);
arrVTX[2].color = Vec4(1.f, 1.f, 1.f, 1.f);
arrVTX[3].position = Vec3(-0.5f, -0.5f, 0.5f);
arrVTX[3].color = Vec4(1.f, 1.f, 1.f, 1.f);
//버퍼 정보 작성
D3D11_BUFFER_DESC desc = {};
desc.ByteWidth = sizeof(Vertex) * (UINT)arrVTX.size();
desc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.CPUAccessFlags = 0;
D3D11_SUBRESOURCE_DATA sub = {};
sub.pSysMem = arrVTX.data();
if (FAILED(DEVICE->CreateBuffer(&desc, &sub, g_vertex_buffer_.GetAddressOf())))
{
MessageBox(nullptr, L"Vertex Buffer 생성실패", L"Engine 초기화 실패", MB_OK);
return E_FAIL;
}
//index buffer
array<UINT, 6> indexArray = { 0,1,2,2,3,0 };
desc = {};
desc.ByteWidth = sizeof(UINT) * (UINT)indexArray.size();
desc.BindFlags = D3D11_BIND_INDEX_BUFFER;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.CPUAccessFlags = 0;
sub = {};
sub.pSysMem = indexArray.data();
if (FAILED(DEVICE->CreateBuffer(&desc, &sub, g_index_buffer_.GetAddressOf())))
{
MessageBox(nullptr, L"Index Buffer 생성실패", L"Engine 초기화 실패", MB_OK);
return E_FAIL;
}
삼각형이라고 해놓고 왜 점을 4개나 저장을 했는지 의문이 들수도 있다. 일단 삼각형을 만들고 바로 사각형도 테스트 해보기 위해 4개를 저장했다.
인덱스버퍼를 사용하는 이유는 효율성때문인데. 만약 인덱스 버퍼없이 사각형을 그리게 된다면 6개의 점을 버퍼에 저장해야한다. 지금 Vertex구조체의 크기는 12+16+8= 36 바이트이다. 6개를 저장하면 총 216바이트가 소모가된다.
반면 인덱스버퍼는 int를 저장하기때문에 4바이트만 소모를한다. 점4개에 인덱스버퍼 6개(168바이트)를 지정하는것이 훨씬 메모리적인 측면에서 효율적이라는것을 알 수 있다. 그렇기때문에 인덱스버퍼를 이용해 드로우를 한다.
//shader
wstring path = PathManager::Get().GetContentPath();
path += L"shader\\std_vertex_shader.hlsl";
if (FAILED(D3DCompileFromFile(path.c_str()
, nullptr
, D3D_COMPILE_STANDARD_FILE_INCLUDE
, "vs_main", "vs_5_0"
, D3DCOMPILE_DEBUG, 0
, g_vs_blob_.GetAddressOf()
, g_error_blob_.GetAddressOf())))
{
if(nullptr != g_error_blob_)
MessageBox(nullptr, (wchar_t*)g_error_blob_->GetBufferPointer(), L"Shader 컴파일 실패", MB_OK);
}
path = PathManager::Get().GetContentPath();
path += L"shader\\std_pixel_shader.hlsl";
if (FAILED(D3DCompileFromFile(path.c_str()
, nullptr
, D3D_COMPILE_STANDARD_FILE_INCLUDE
, "ps_main", "ps_5_0"
, D3DCOMPILE_DEBUG, 0
, g_ps_blob_.GetAddressOf()
, g_error_blob_.GetAddressOf())))
{
if (nullptr != g_error_blob_)
MessageBox(nullptr, (wchar_t*)g_error_blob_->GetBufferPointer(), L"Shader 컴파일 실패", MB_OK);
}
DEVICE->CreateVertexShader(g_vs_blob_->GetBufferPointer(), g_vs_blob_->GetBufferSize(), nullptr, g_vs_.GetAddressOf());
DEVICE->CreatePixelShader(g_ps_blob_->GetBufferPointer(), g_ps_blob_->GetBufferSize(), nullptr, g_ps_.GetAddressOf());
셰이더를 바로 생성하는게 아닌 파일에서 셰이더를 컴파일해서 그 정보를 Blob이라고하는 포인터 덩어리에 저장을한다.
그다음 셰이더를 생성하는 과정을 진행한다. 여기서 중요한 것은 진입함수이름을 꼭 셰이더에 작성한 함수이름으로 맞추어 줘야한다는점이다.
D3D11_INPUT_ELEMENT_DESC g_layout[3] =
{
D3D11_INPUT_ELEMENT_DESC {"POSITION",0,DXGI_FORMAT_R32G32B32_FLOAT,0,0,D3D11_INPUT_PER_VERTEX_DATA, 0},
D3D11_INPUT_ELEMENT_DESC {"COLOR",0,DXGI_FORMAT_R32G32B32A32_FLOAT,0,12,D3D11_INPUT_PER_VERTEX_DATA, 0},
D3D11_INPUT_ELEMENT_DESC {"TEXCOORD",0,DXGI_FORMAT_R32G32_FLOAT,0,28,D3D11_INPUT_PER_VERTEX_DATA, 0},
};
if (FAILED(DEVICE->CreateInputLayout(g_layout, 3, g_vs_blob_->GetBufferPointer(), g_vs_blob_->GetBufferSize(), g_input_layout_.GetAddressOf())))
{
MessageBox(nullptr, L"layout 생성실패", L"Engine 초기화 실패", MB_OK);
return E_FAIL;
}
레이아웃은 셰이더에 있는 Sementic의 정보를 셰이더에 알려주는 역할을 한다. 이것이 작성되지 않으면 셰이더에서 정확한 정보를 읽어들일 수 없기때문에 자신의 셰이더에 맞게 꼭 작성해주어야한다.
DirectDevice::Get().ClearTarget();
//Render Code
UINT stride = sizeof(Vertex);
UINT offset = 0;
CONTEXT->IASetVertexBuffers(0, 1, g_vertex_buffer_.GetAddressOf(), &stride, &offset);
CONTEXT->IASetIndexBuffer(g_index_buffer_.Get(), DXGI_FORMAT_R32_UINT, 0);
CONTEXT->IASetInputLayout(g_input_layout_.Get());
//점들을 어떻게 연결해서 그릴지
CONTEXT->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
CONTEXT->VSSetShader(g_vs_.Get(), nullptr, 0);
CONTEXT->PSSetShader(g_ps_.Get(), nullptr, 0);
CONTEXT->DrawIndexed(6, 0, 0);
DirectDevice::Get().Present();
여기서 설명을 하지않은건 Topology(위상구조)에 대해서인데 간단히 말하면 들어온 점들을 어떻게 연결해 그릴지에 정하는것이다. 크게 점, 삼각형, 라인이 있으며 그려야하는 모양에 맞게 지정하면된다.
그다음에는 DrawIndexed를보면 이전에 우리가 6개의 인덱스를 지정했기 때문에 크기를 6으로 잡아두었는데 만약 삼각형을 그리고 싶다면 3으로 바꿔주면된다.
아마 위 3가지에 대해 작업을 할 예정이다.