서의 공간

[T16.1] Draw Triangle 본문

Graphics API/DirectX 11 - Chili

[T16.1] Draw Triangle

홍서의 2020. 11. 28. 11:47

[그림 16.1] 프레임워크 구조

Exception 부분은 넘어간다. 또한 디버깅을 위해 여러 유틸 클래스가 추가되었지만 생략한다. 여기서 핵심은 Graphics.h의 DrawTestTriangle()함수이다. 삼각형이 어떻게 그려지는지 .cpp 코드 내에서 설명한다.

 

핵심 개념

1. Microsoft::WRL::ComPtr: 템플릿 매개 변수로 지정된 인터페이스를 나타내는 스마트 포인터 형식을 만든다. ComPtr은 기본 인터페이스 포인터에 대한 참조 횟수를 자동으로 유지하고 참조 횟수가 0이 되면 인터페이스를 해제한다. 즉 자동으로 해제해주기 때문에 인터페이스의 Release() 함수를 사용할 필요가 없다.

2. ID3DBlob: ID3DBlob 인터페이스는 D3DCommon.h 헤더 파일에 ID3D10Blob으로 정의된 인터페이스로서 정의된다. ID3DBlob은 버전 중립적이어서 모든 Direct3D 버전의 코드에서 사용 가능하다. Blob은 데이터 버퍼로 사용되어 메시 최적화 및 로드 작업 중에 정점, 인접성 및 머티리얼 정보를 저장할 수 있다. 또한 이 객체는 정점, 지오메트리 및 픽셀 셰이더를 컴파일하는 API에서 객체 코드 및 오류 메시지를 반환하는데도 사용한다. 여기서의 주 사용처가 바로 셰이더를 컴파일 한 코드 및 오류 메시지를 반환하는 데에 있다.

3. D3DReadFileToBlob: D3DCompiler.lib가 필요하다. <d3dcompiler.h> 헤더 파일에 정의된 함수이다. 이 헤더 파일은 HLSL에서 사용된다. 

4. Shader(셰이더): 셰이더는 소프트웨어의 명령 집합이다. 그러니까 이해한대로 설명하자면 어떤 하나의 특정 도형을 그릴 때 GPU에 입력할 정점과 정점 정보들을 정의하고, 필요한 셰이더를 만든다. 그리고 Input layout에서 셰이더의 bytecode를 저장한 pBlob과 정점들의 정보들을 결정한다. 그렇다는 의미는 아마도 우리가 원하는 도형을 그리기 위해서는 그러한 정점 정보와 이 정점들을 이렇게 이렇게 계산해달라(shader)라고 설명서를 GPU에게 건넨다라고 보는게 맞을 듯하다. 그럼 GPU는 입력된 정점에 대해 병렬적으로 설명서(shader)대로 처리한다.


Graphics 클래스

더보기

Graphics.h

#pragma once
#include "ChiliWin.h"
#include "ChiliException.h"
#include <d3d11.h>
#include <wrl.h>
#include <vector>
#include "DxgiInfoManager.h"

class Graphics
{
public:
	class Exception : public ChiliException
	{
		using ChiliException::ChiliException;
	};
	class HrException : public Exception
	{
	public:
		HrException( int line,const char* file,HRESULT hr,std::vector<std::string> infoMsgs = {} ) noexcept;
		const char* what() const noexcept override;
		const char* GetType() const noexcept override;
		HRESULT GetErrorCode() const noexcept;
		std::string GetErrorString() const noexcept;
		std::string GetErrorDescription() const noexcept;
		std::string GetErrorInfo() const noexcept;
	private:
		HRESULT hr;
		std::string info;
	};
	class InfoException : public Exception
	{
	public:
		InfoException( int line,const char* file,std::vector<std::string> infoMsgs ) noexcept;
		const char* what() const noexcept override;
		const char* GetType() const noexcept override;
		std::string GetErrorInfo() const noexcept;
	private:
		std::string info;
	};
	class DeviceRemovedException : public HrException
	{
		using HrException::HrException;
	public:
		const char* GetType() const noexcept override;
	private:
		std::string reason;
	};
public:
	Graphics( HWND hWnd );
	Graphics( const Graphics& ) = delete;
	Graphics& operator=( const Graphics& ) = delete;
	~Graphics() = default;
	void EndFrame();
	void ClearBuffer( float red,float green,float blue ) noexcept;
	void DrawTestTriangle();
private:
#ifndef NDEBUG
	DxgiInfoManager infoManager;
#endif
    // 모든 인터페이스 포인터 타입을 스마트 포인터 타입으로 바꾼다.
	Microsoft::WRL::ComPtr<ID3D11Device> pDevice;
	Microsoft::WRL::ComPtr<IDXGISwapChain> pSwap;
	Microsoft::WRL::ComPtr<ID3D11DeviceContext> pContext;
	Microsoft::WRL::ComPtr<ID3D11RenderTargetView> pTarget;
};

Graphics.cpp

#include "Graphics.h"
#include "dxerr.h"
#include <sstream>
#include <d3dcompiler.h>

namespace wrl = Microsoft::WRL;

#pragma comment(lib,"d3d11.lib")
#pragma comment(lib,"D3DCompiler.lib")

// graphics exception checking/throwing macros (some with dxgi infos)
#define GFX_EXCEPT_NOINFO(hr) Graphics::HrException( __LINE__,__FILE__,(hr) )
#define GFX_THROW_NOINFO(hrcall) if( FAILED( hr = (hrcall) ) ) throw Graphics::HrException( __LINE__,__FILE__,hr )

#ifndef NDEBUG
#define GFX_EXCEPT(hr) Graphics::HrException( __LINE__,__FILE__,(hr),infoManager.GetMessages() )
#define GFX_THROW_INFO(hrcall) infoManager.Set(); if( FAILED( hr = (hrcall) ) ) throw GFX_EXCEPT(hr)
#define GFX_DEVICE_REMOVED_EXCEPT(hr) Graphics::DeviceRemovedException( __LINE__,__FILE__,(hr),infoManager.GetMessages() )
#define GFX_THROW_INFO_ONLY(call) infoManager.Set(); (call); {auto v = infoManager.GetMessages(); if(!v.empty()) {throw Graphics::InfoException( __LINE__,__FILE__,v);}}
#else
#define GFX_EXCEPT(hr) Graphics::HrException( __LINE__,__FILE__,(hr) )
#define GFX_THROW_INFO(hrcall) GFX_THROW_NOINFO(hrcall)
#define GFX_DEVICE_REMOVED_EXCEPT(hr) Graphics::DeviceRemovedException( __LINE__,__FILE__,(hr) )
#define GFX_THROW_INFO_ONLY(call) (call)
#endif


Graphics::Graphics( HWND hWnd )
{
	/* 
    	DXGI_SWAP_CHAIN_DESC sd = {};
        생성 부분 생략
    */

	UINT swapCreateFlags = 0u;
#ifndef NDEBUG
	swapCreateFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif

	// for checking results of d3d functions
	HRESULT hr;

	// create device and front/back buffers, and swap chain and rendering context
    // 디바이스와 프론트/백 버퍼를 만든다.
	GFX_THROW_INFO( D3D11CreateDeviceAndSwapChain(
		nullptr,
		D3D_DRIVER_TYPE_HARDWARE,
		nullptr,
		swapCreateFlags,
		nullptr,
		0,
		D3D11_SDK_VERSION,
		&sd,
		&pSwap,
		&pDevice,
		nullptr,
		&pContext
	) );
    
    /* 
		SwapChain의 백버퍼를 얻어와서 백버퍼에 대한 뷰를 생성한다. 
		일종의 자원인 백버퍼를 파이프라인에 직접 바인딩하지 않는다.
		대신 자원에 대한 또 하나의 뷰를 만들고, 그 뷰를 파이프라인에 바인딩한다.
		뷰를 자원을 바라보는 또 하나의 시야라고 생각할 수 있다.
		또는 그냥 자원에 대한 참조형 변수를 만든다라고 생각해버리자.
        pBackBuffer는 자동으로 해제하기 때문에 따로 Release할 필요가 없다.
	*/
	// gain access to texture subresource in swap chain (back buffer)
	wrl::ComPtr<ID3D11Resource> pBackBuffer;
	GFX_THROW_INFO( pSwap->GetBuffer( 0,__uuidof(ID3D11Resource),&pBackBuffer ) );
	GFX_THROW_INFO( pDevice->CreateRenderTargetView( pBackBuffer.Get(),nullptr,&pTarget ) );
}

/*
	SwapChain이 백버퍼를 화면에 보여준다.
	모든 그리기 연산을 백버퍼에 한다(Frame의 시작에서 끝으로 향하는 단계).
	그리고 모든 그리기 연산이 완료되어 화면에 출력될 준비가 된다면
	SwapChain은 백버퍼와 프론트버퍼를 스왑하여 화면에 결과를 출력한다(EndFrame).
*/
void Graphics::EndFrame()
{
	HRESULT hr;
#ifndef NDEBUG
	infoManager.Set();
#endif
	if( FAILED( hr = pSwap->Present( 1u,0u ) ) )
	{
		if( hr == DXGI_ERROR_DEVICE_REMOVED )
		{
			throw GFX_DEVICE_REMOVED_EXCEPT( pDevice->GetDeviceRemovedReason() );
		}
		else
		{
			throw GFX_EXCEPT( hr );
		}
	}
}

void Graphics::ClearBuffer( float red,float green,float blue ) noexcept
{
	const float color[] = { red,green,blue,1.0f };
	pContext->ClearRenderTargetView( pTarget.Get(),color );
}

/**** 핵심 ****/
void Graphics::DrawTestTriangle()
{
	namespace wrl = Microsoft::WRL;
	HRESULT hr;
	
    // 정점의 자료형
	struct Vertex
	{
		float x;
		float y;
	};

	// create vertex buffer (1 2d triangle at center of screen)
    // 정점들의 local space 기준 위치 설정
	const Vertex vertices[] =
	{
		{ 0.0f,0.5f },
		{ 0.5f,-0.5f },
		{ -0.5f,-0.5f },
	};
    
    /*
		GPU가 정점들의 배열에 접근하려면, 그 정점들을 버퍼라고 부르는 GPU 자원에 넣어 두어야 한다.
		정점들을 저장하는 버퍼를 정점 버퍼(Vertex buffer)라고 부른다. 응용 프로그램에서 정점 같은
		자료 원소들의 배열을 GPU에 제공해야 할 때에는 항상! 버퍼를 사용한다.
		다음은 정점 버퍼를 만들기 전에 Description을 작성한다.
	*/
	wrl::ComPtr<ID3D11Buffer> pVertexBuffer;
	D3D11_BUFFER_DESC bd = {};
	// 버퍼가 무엇으로써 어떻게 파이프라인에 바인드 될 것인지 설정
	bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
	// 자료 원소들이(정점) GPU나 CPU로부터 읽기, 쓰기 권한을 설정
	bd.Usage = D3D11_USAGE_DEFAULT;
	// 버퍼에 대해 CPU 접근 플래그
	bd.CPUAccessFlags = 0u;
	// 여러 종류의 플래그
	bd.MiscFlags = 0u;
	// 버퍼의 bytes 크기. 모든 정점들의 bytes 크기와 같다.
	bd.ByteWidth = sizeof( vertices );
    /* 정점이 배열안에서 어떻게 구분되어질지, 즉 정점배열에서 각 정점의 크기가 얼마인지.
	   GPU가 버퍼를 읽을 때 주어진 byte 크기 단위로 건너뛰면서 자료를 읽게 된다.*/
	bd.StructureByteStride = sizeof( Vertex );
    /* Subresource 데이터를 초기화하기 위한 데이터를 지정한다.
	   GPU에서 사용할 Subresource 데이터는 메모리에 올려진 데이터로 초기화해야한다.
	   즉 정점 배열은 우리가 작성한 실제 값들이 들어있으며, 
	   그 값들로 Subresource 데이터를 초기화한다.*/
	D3D11_SUBRESOURCE_DATA sd = {};
	sd.pSysMem = vertices;
    // 모든 준비가 되었다면 GPU가 읽을 수 있는(또는 쓸 수도 있는) 정점 버퍼를 만든다.
	GFX_THROW_INFO( pDevice->CreateBuffer( &bd,&sd,&pVertexBuffer ) );

	// Bind vertex buffer to pipeline
	const UINT stride = sizeof( Vertex );
	const UINT offset = 0u;
   	// Vertex buffer를 만들고 pipeline(IA stage)에 바인드 한다. 
	pContext->IASetVertexBuffers( 0u,1u,&pVertexBuffer,&stride,&offset );
	
	// create vertex shader
    // 정점 셰이더를 만든다
	wrl::ComPtr<ID3D11VertexShader> pVertexShader;
	wrl::ComPtr<ID3DBlob> pBlob;
	GFX_THROW_INFO( D3DReadFileToBlob( L"VertexShader.cso",&pBlob ) );
    // VertexShader.cso 파일을 읽어 pBlob에 저장하였고, 이 데이터(컴파일된 셰이더)를 이용하여 VertexShader 객체를 만든다.
	GFX_THROW_INFO( pDevice->CreateVertexShader( pBlob->GetBufferPointer(),pBlob->GetBufferSize(),nullptr,&pVertexShader ) );

	// bind vertex shader
	pContext->VSSetShader( pVertexShader.Get(),nullptr,0u );
	
    // 정점 그리기 호출, Draw 함수의 첫 번째 파라미터는 vertices의 size일 뿐이다.
	GFX_THROW_INFO_ONLY( pContext->Draw( (UINT)std::size( vertices ),0u ) );
}
// Graphics exception stuff
// Graphisc exception 관련 함수 구현 모두 생략
// 함수의 원형만 나열한다.
Graphics::HrException::HrException( int line,const char * file,HRESULT hr,std::vector<std::string> infoMsgs ) noexcept
const char* Graphics::HrException::what() const noexcept
const char* Graphics::HrException::GetType() const noexcept
HRESULT Graphics::HrException::GetErrorCode() const noexcept
std::string Graphics::HrException::GetErrorString() const noexcept
std::string Graphics::HrException::GetErrorDescription() const noexcept
std::string Graphics::HrException::GetErrorInfo() const noexcept
const char* Graphics::DeviceRemovedException::GetType() const noexcept
Graphics::InfoException::InfoException( int line,const char * file,std::vector<std::string> infoMsgs ) noexcept
const char* Graphics::InfoException::what() const noexcept
const char* Graphics::InfoException::GetType() const noexcept
std::string Graphics::InfoException::GetErrorInfo() const noexcept

VertexShader

더보기
float4 main( float2 pos : Position ) : SV_Position
{
	return float4(pos.x,pos.y,0.0f,1.0f);
}

App 클래스

더보기

App.h

#pragma once
#include "Window.h"
#include "ChiliTimer.h"

class App
{
public:
	App();
	// master frame / message loop
	int Go();
private:
	void DoFrame();
private:
	Window wnd;
	ChiliTimer timer;
};

App.cpp

#include "App.h"

App::App()
	:
	wnd( 800,600,"The Donkey Fart Box" )
{}

/*
	Go() 함수는 WinMain에서 호출되어 프로그램 종료코드 메시지가 입력될 때까지
    무한 반복문을 실행한다.
*/
int App::Go()
{
	while( true )
	{
		// process all messages pending, but to not block for new messages
		if( const auto ecode = Window::ProcessMessages() )
		{
			// if return optional has value, means we're quitting so return exit code
			return *ecode;
		}
		DoFrame();
	}
}

// 매 프레임마다 실행된다.
void App::DoFrame()
{
	const float c = sin( timer.Peek() ) / 2.0f + 0.5f;
    // 매 프레임 백버퍼를 주어진 색상으로 지운다
	wnd.Gfx().ClearBuffer( c,c,1.0f );
    // 매 프레임 삼각형을 백버퍼에 그린다
	wnd.Gfx().DrawTestTriangle();
    /* 매 프레임의 끝에서 SwapChain에 의해 프론트 버퍼와 백버퍼를 스왑
       하여 백버퍼를 화면에 출력한다.(스왑과 동시에 백버퍼는 프론트버퍼가 되고
       프론트 버퍼였던 것은 백버퍼가 된다) */
	wnd.Gfx().EndFrame();
}

'Graphics API > DirectX 11 - Chili' 카테고리의 다른 글

[T17] Draw Indexed Triangle  (0) 2020.11.29
[T16.2] Draw Triangle  (0) 2020.11.28
[T16.0] DirectX 11 파이프라인  (0) 2020.11.28
[T11] Graphics class  (0) 2020.11.27
[T6] Window class  (0) 2020.11.27
Comments