[DirectX 11로 Voxel Engine 만들기] 2일차

MS Choi·2022년 1월 7일
1

VoxelEngine

목록 보기
3/4

진행사항

PathManager 제작

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;
}

그래픽파이프라인을 거쳐서 나온 영역에 붉은 색을 칠하도록 코드를 작성했다.

그래픽 파이프 라인

셰이더를 작성하고 그래프 파이프라인을 설명하는 이유는 개인적으로 공부를 할때 쭉 설명을 듣는것보단 연결지어 생각하는게 머리에 더 잘 남기 때문이다.
directX Graphics pipeline

위 그림들의 파이프라인 스테이지를 정리한 내용이다. 위 이미지에서 사각형들은 고정 스테이지로 프로그래밍이 불가능한 부분을 말하고 타원형 부분들은 프로그래밍이 가능하다.

  • Input Assembler
    • 입력에 대한 준비를 하는 단계로, 정점 버퍼, 픽셀버퍼등의 메모리를 준비하고 연결하는 단계이다. 그리고 렌더링에 필요한 데이터를 전달한다.
  • Vertex Shader Stage
    • 어셈블러에서 정점을 처리 하 고 변환, 스키닝, 모핑 및 정점 별 조명 등의 정점 작업을 수행 한다.
    • 정점 셰이더는 항상 단일 입력 정점에 대해 작업을 수행 하고 단일 출력 정점을 생성 한다.
  • Hull Shader Stage
    • 각 입력 패치 (쿼드, 삼각형 또는 선)에 해당 하는 기 하 도형 패치 (및 패치 상수)를 생성 하는 프로그래밍 가능한 셰이더 단계
  • Tesellation Stage
    • 기하 도형 패치를 나타내는 도메인의 샘플링 패턴을 만들고 이러한 예제를 연결 하는 작은 개체 (삼각형, 요소 또는 선) 집합을 생성 하는 고정 함수 파이프라인 단계
  • Domain Shader
    • 각 도메인 샘플에 해당 하는 꼭 짓 점 위치를 계산 하는 프로그래밍 가능한 셰이더 단계입니다.
  • Geometry Shader
    • 꼭짓점을 입력으로 사용하여 애플리케이션 지정 셰이더 코드를 실행하고 출력 시 꼭짓점을 생성하는 기능을 실행
    • 주로 파티클 기능을 만드는데 사용
  • Rasterizer
    • 기본 형식은 픽셀로 변환 되 고 각 기본 형식에는 정점 별 값을 보간 합니다.
    • 래스터화에는 뷰에 클리핑 꼭지점이 포함 됩니다. 즉, 원근감을 제공 하기 위해 z로 나누기를 수행 하고, 기본 형식을 2D 뷰포트에 매핑하고, 픽셀 셰이더를 호출 하는 방법을 결정 합니다
  • Pixel shader
    • 화면의 색상을 지정하는 셰이더 선택된 타겟의 해당하는 픽셀의 색상을 지정한다.
    • 여기서 지정된 색상을 통해 BlendState에서 보간을 진행하고 최종색상이 지정된다.
  • Output Merge State Stage
    • Depth Stencil State
      • 깊이 텍스쳐에 따라 물체의 깊이를 평가하는 단계
      • Direct에서는 z축의 값에 따라 깊이를 평가해 깊이가 얕은부분을 그리고 깊은 부분은 그리지 않는다.
      • 그렇기 때문에 물체가 곂쳐지게 되면 깊이가 앝은 물체가 보이게 된다.
      • 이러한 깊이버퍼덕분에 렌더링 순서에 영향을 받지 않는다.
    • Blend State
      • 색상을 조합하는 단계 보간을 통해 메쉬의 색상을 결정한다.

우리는 프로그래밍 가능한 부분중 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으로 바꿔주면된다.

오늘의 작업결과

  • 삼각형
  • 사각형

추후 진행사항

  • 상수버퍼
  • TimeManager
  • KeyMananger

아마 위 3가지에 대해 작업을 할 예정이다.

profile
다양한 경험을 하는걸 좋아하는 개발자입니다.

0개의 댓글