들어가며

 

Fbx파일은 Autodesk에서 개발한 파일 형식으로 3D모델, 애니메이션, Material, 카메라, 조명등 다양한 정보를 저장할 수 있는 파일입니다.

 

Autodesk에서는 Fbx파일을 다루는 Fbx sdk라는 c++ 라이브러리를 제공합니다.

Fbx sdk를 사용하면 Fbx파일을 읽고, 쓰고, 수정할 수 있습니다.

 

최근 튜토리얼에서 텍스트 파일로 이루어진 정점 데이터를 가져오는 기능을 만든 김에, 자주쓰는 fbx파일에서 정점 데이터를 추출해 볼 생각입니다.

 

이번 포스팅에서는 정점의 위치만 가져와, 단색으로 렌더링 하겠습니다.

(라이브러리 적용은 건너뜁니다.)


FbxTool.h

 

이 FbxTool의 흐름은 간단합니다.

FindMesh로 Mesh데이터를 찾아 SaveVertexData로 Mesh의 데이터를 분석하여 저장합니다.

#include <fbxsdk.h>

class FbxTool
{
public:

	FbxTool();
	~FbxTool() = default;

	bool Initialize();
	// 파일을 불러옵니다.
	bool Load(const char* fileName);
	
	// VertexBuffer를 만들때 호출 됩니다.
	XMFLOAT3* GetVertexPos() { return m_pos; };
	unsigned int* GetVertexIdx() { return m_idx; };

private:
	
	// 재귀형식으로 트리형 노드를 탐색해 Mesh데이터를 찾아 저장
	bool FindMesh(FbxNode* node);
	// 찾은 Mesh로부터 정점 데이터 저장
	bool SaveVertexData();
	void Shotdown();

private:

	FbxManager* m_manager;
	FbxImporter* m_importer;
	FbxScene* m_scene;
	FbxMesh* m_mesh;
	
	// 가져온 Fbx자료형을 DX자료형으로 변환하여 저장해둘 변수
	XMFLOAT3* m_pos;
	unsigned int* m_idx;
};

 

FbxTool.h

 

1. Intialize

Fbx sdk는 싱글톤패턴으로 개체가 관리되기에, 프로그램당 한개의 개체만 이용됩니다.

bool FbxTool::Initialize()
{
	// 관리자 객체 생성
	m_manager = FbxManager::Create();

Fbx 파일의 내용을 가져오려면 FbxIoSettings와 FbxScene, FbxImporter개체를 생성해야 합니다.

 

앞서 말했듯이 Fbx에는 여러 데이터를 가지고 있습니다.(3D모델, Material, 카메라, ...) 그래서 가져오거나, 내보내고 싶은 데이터를 설정할 수 있는데 이 설정을 FbxIoSettings으로 지정할 수 있습니다.

이번 시간은 설정필요가 없어 기본 값을 사용하겠습니다.

	// 가져올 씬의 요소(camera, light, mesh, texture, ...) 세팅
	FbxIOSettings* ios = FbxIOSettings::Create(m_manager, IOSROOT);
	m_manager->SetIOSettings(ios);

FbxScene은 Fbx파일의 전체 3D 장면을 나타내며 이 장면 속에 속한 카메라, 메시, 텍스쳐 모든 데이터를 포함합니다.

Fbx파일의 본체 격인것 같습니다.

	m_scene = FbxScene::Create(m_manager, "My Scene");

다음으로 FbxImporter의 생성 차례인데,  FbxImporter는 Fbx의 Scene데이터를 FbxScene으로 가져와주는 개체입니다.

일단 생성만 해둡니다 

	m_importer = FbxImporter::Create(m_manager, "");

	return true;
}

 

2. Load

Load함수의 목적은 Fbx파일명을 받아와 해당 파일에서 정점 데이터를 찾아 저장합니다.

파일을 찾아 해당 파일에서 씬 데이터를 m_scene에 저장합니다.

 

Fbx파일마다 기본 폴리곤 형태가 다릅니다.(삼각형 or 사각형) 제가 사용하는 DX에서는 삼각형을 통해 렌더링이 이루어지기 때문에 삼각형으로 만들어줍니다.

 

그리고 메시 데이터를 저장한는데 매개변수에 GetRootNode라는 매개변수가 있습니다.

Scene은 Node라는 트리 구조로 구성되어 있습니다. 즉, 이 Node 하나하나가 Scene의 각 개별 데이터를 의미합니다.(메쉬 노드, 조명 노드, 카메라 노드)

우린는 최 상단 노드인 Root Node로부터 메시 데이터를 찾아 저장할 것 입니다.

bool FbxTool::Load(const char* fileName)
{
	bool ret;

	// 1. 임포터 초기화
	ret = m_importer->Initialize(fileName, -1, m_manager->GetIOSettings());

	m_importer->Import(m_scene);
	// 임포트 후 임포터는 해제하여 메모리 사용량을 줄입니다.
	m_importer->Destroy();

	// 2. 삼각형화할 수 있는 노드를 삼각형화 시키기
	FbxGeometryConverter converter(m_manager);
	converter.Triangulate(m_scene, true);

	// 3. 메시 데이터 저장
	ret = FindMesh(m_scene->GetRootNode());
	if (ret == false) return false;

	// 4. 메시의 정점 데이터(위치, 인덱스) 저장
	SaveVertexData();

	return true;
}

 

3. FindMesh

트리 구조를 메시 노드가 나올때까지 재귀형으로 탐색합니다. 노드가 가진 데이터는 FbxNodeAttribute로 알 수 있습니다.

발견했다면 m_mesh에 저장합니다 .

bool FbxTool::FindMesh(FbxNode* node)
{
	// 찾은 노드가 메시타입이면 메시에 저장 후 반환합니다.
	FbxNodeAttribute* attribute = node->GetNodeAttribute();
	if (node->GetNodeAttribute() != nullptr)
	{
		if (node->GetNodeAttribute()->GetAttributeType() == FbxNodeAttribute::eMesh)
		{
			m_mesh = node->GetMesh();
			return true;
		}
	}
	
	// 노드의 자식의 수만큼 반복
	int childCnt = node->GetChildCount();
	for (int i = 0; i < childCnt; ++i)
	{
		if (FindMesh(node->GetChild(i)) == true)
		{
			return true;
		}
	}
	
	// 다 순회해도 메시 데이터가 없으면 false반환합니다.
	return false;
}

 

4. SaveVertexData

찾은 메시 노드로 정점 데이터중 위치 데이터를 추출하여 저장합니다.

특별히 어려운 부분은 없으나 중간에 위치 데이터를 0, 1, 2순이 아니라 0, 2, 1순으로 된걸 볼 수 있는데, 그 이유는 backface의 방향 때문입니다.

 

Dx11같은 경우 정점이 시계 방향으로 그려진 면이 front, 반대 방향으로 그려진 면이 back 입니다 

반면에, Fbx sdk가 사용하는 면은 그 반대입니다.

 

그렇기 때문에 Fbx sdk에서 Dx11으로 정점 데이터를 가져오는 순은 0, 2, 1순으로 가져와야 합니다.

이렇게 두 번째, 세 번째 만 바꿔주어도 그리는 방향을 반대로 만들수 있습니다.

bool FbxTool::SaveVertexData()
{
	// 정점의 개수
	int vertexCnt = m_mesh->GetControlPointsCount();
	// 정점 데이터 생성
	m_pos = new XMFLOAT3[vertexCnt];
	// 정점배열의 포인터 얻기
	FbxVector4* position = m_mesh->GetControlPoints();

	// 정점 위치 구하여 저장
	for (int i = 0; i < vertexCnt; ++i)
	{
		float x = static_cast<float>(position[i][0]);
		float y = static_cast<float>(position[i][2]);
		float z = static_cast<float>(position[i][1]);
		m_pos[i] = XMFLOAT3(x, y, z);
	}

 

ModelClass.h

 

VertexBuffer를 생성하는 클래스에 FbxTool 포인터를 생성합니다.

class ModelClass
{
	// ....
private:    
    FbxTool* m_fbx;
}

 

ModelClass.cpp

 

Vertex Buffer를 만들때 이런식으로 사용합니다.

	///////////////////////////////////////////////////////////////////////////////
	// Vertex Buffer 생성
	{
		m_vertexCount = _msize(m_fbx->GetVertexPos()) / sizeof(XMFLOAT3);
		m_vertices = new VertexColor[m_vertexCount];

		// Load the vertex array and index array with data.
 		for (int i = 0; i < m_vertexCount; i++)
		{
			m_vertices[i].position = m_fbx->GetVertexPos()[i];
			m_vertices[i].color = XMFLOAT4(1, 0, 0, 1);
		}

 

Index Buffer 같은 경우는 바로 받아오기만 하면 됩니다.

	///////////////////////////////////////////////////////////////////////////////
	// Index Buffer 생성
	{
		m_indices = m_fbx->GetVertexIdx();
		m_indexCount = _msize(m_indices) / sizeof(unsigned int);

 


요약

 

Fbx sdk를 사용하여 정점의 위치 데이터를 가져와 단색으로 렌더링 했습니다.

 

다음은 다른 정점 데이터까지 불러와 렌더링 할 예정입니다.

 

 

Tutorial 7: 3D Model Rendering

Tutorial 7: 3D Model Rendering This tutorial will cover how to render 3D models in DirectX 11 using HLSL The code in this tutorial is based on the code from the diffuse lighting tutorial. We have already been rendering 3D models in the previous tutorials;

www.rastertek.com

DirectX11의 공부 목적으로 위 링크의 튜토리얼을 보고 제 생각을 포함하여 정리하였습니다.


들어가며

 

이 튜토리얼에서는 HLSL을 사용하여 DirectX 11에서 3D 모델을 렌더링하는 방법을 다룹니다.이 튜토리얼의 코드는 디퓨즈 조명 튜토리얼의 코드를 기반으로 합니다.

이전 튜토리얼에서 우리는 이미 3D 모델을 렌더링했습니다.하지만 그것들은 단일 삼각형으로 구성되어 있었고 상당히 단순했습니다.이제 기본 사항을 다루었으니, 더 복잡한 객체를 렌더링하는 방법으로 나아가겠습니다.이번에는 큐브 객체를 렌더링할 것입니다.더 복잡한 모델을 렌더링하는 방법에 대해 논의하기 전에 먼저 모델 형식에 대해 이야기하겠습니다.

사용자가 3D 모델을 만들 수 있게 해주는 많은 도구가 있습니다.Maya와 Blender가 가장 인기 있는 3D 모델링 프로그램 중 두 가지입니다.기능이 적지만 우리가 필요로 하는 기본 작업을 수행할 수 있는 다른 많은 도구들도 있습니다.

어떤 도구를 사용하든, 그들은 모두 다양한 형식으로 모델을 내보낼 것입니다. 제 제안은 여러분이 직접 모델 형식을 만들고, 내보낸 형식을 여러분의 형식으로 변환하는 파서를 작성하는 것입니다. 그 이유는 여러분이 사용하는 3D 모델링 패키지가 시간이 지남에 따라 변경될 수 있고, 그들의 모델 형식도 변경될 것이기 때문입니다.또한, 여러분은 여러 3D 모델링 패키지를 사용할 수 있어서 여러 다른 형식을 다루어야 할 수도 있습니다.그래서 여러분이 자신의 형식을 가지고, 다른 형식을 자신의 형식으로 변환한다면, 코드를 변경할 필요가 없습니다.단지 여러분의 파서 프로그램을 변경하여 그 형식을 여러분의 형식으로 변환하면 됩니다.대부분의 3D 모델링 패키지는 해당 모델링 프로그램에만 유용한 많은 불필요한 데이터를 내보내는데, 여러분의 모델 형식에는 그 모든 것이 필요하지 않습니다.

자신의 형식을 만드는 데 가장 중요한 부분은 그것이 여러분이 필요로 하는 모든 것을 포함하고, 사용하기에 간단하다는 것입니다.또한, 애니메이션 데이터가 포함된 객체와 정적인 객체 등 다양한 객체를 위해 몇 가지 다른 형식을 만드는 것도 고려할 수 있습니다.

제가 제시할 모델 형식은 매우 기본적입니다.모델 한개의 정점에 대해 한 줄이 매핑됩니다. 각 줄은 코드에서 사용되는 정점 형식에 맞게 위치 벡터(x, y, z), 텍스처 좌표(tu, tv), 그리고 법선 벡터(nx, ny, nz)를 포함할 것입니다. 형식에는 정점 수가 상단에 있어 첫 번째 줄을 읽고 데이터를 읽기 전에 필요한 메모리 구조를 빌드할 수 있습니다. 또한 형식은 세 줄마다 하나의 삼각형을 만들고, 모델 형식의 정점이 시계 방향으로 표시되어야 합니다.다음은 우리가 렌더링할 큐브의 모델 파일입니다.

 

▼Cube.txt

더보기
Vertex Count: 36

Data:

-1.0  1.0 -1.0 0.0 0.0  0.0  0.0 -1.0
 1.0  1.0 -1.0 1.0 0.0  0.0  0.0 -1.0
-1.0 -1.0 -1.0 0.0 1.0  0.0  0.0 -1.0
-1.0 -1.0 -1.0 0.0 1.0  0.0  0.0 -1.0
 1.0  1.0 -1.0 1.0 0.0  0.0  0.0 -1.0
 1.0 -1.0 -1.0 1.0 1.0  0.0  0.0 -1.0
 1.0  1.0 -1.0 0.0 0.0  1.0  0.0  0.0
 1.0  1.0  1.0 1.0 0.0  1.0  0.0  0.0
 1.0 -1.0 -1.0 0.0 1.0  1.0  0.0  0.0
 1.0 -1.0 -1.0 0.0 1.0  1.0  0.0  0.0
 1.0  1.0  1.0 1.0 0.0  1.0  0.0  0.0
 1.0 -1.0  1.0 1.0 1.0  1.0  0.0  0.0
 1.0  1.0  1.0 0.0 0.0  0.0  0.0  1.0
-1.0  1.0  1.0 1.0 0.0  0.0  0.0  1.0
 1.0 -1.0  1.0 0.0 1.0  0.0  0.0  1.0
 1.0 -1.0  1.0 0.0 1.0  0.0  0.0  1.0
-1.0  1.0  1.0 1.0 0.0  0.0  0.0  1.0
-1.0 -1.0  1.0 1.0 1.0  0.0  0.0  1.0
-1.0  1.0  1.0 0.0 0.0 -1.0  0.0  0.0
-1.0  1.0 -1.0 1.0 0.0 -1.0  0.0  0.0
-1.0 -1.0  1.0 0.0 1.0 -1.0  0.0  0.0
-1.0 -1.0  1.0 0.0 1.0 -1.0  0.0  0.0
-1.0  1.0 -1.0 1.0 0.0 -1.0  0.0  0.0
-1.0 -1.0 -1.0 1.0 1.0 -1.0  0.0  0.0
-1.0  1.0  1.0 0.0 0.0  0.0  1.0  0.0
 1.0  1.0  1.0 1.0 0.0  0.0  1.0  0.0
-1.0  1.0 -1.0 0.0 1.0  0.0  1.0  0.0
-1.0  1.0 -1.0 0.0 1.0  0.0  1.0  0.0
 1.0  1.0  1.0 1.0 0.0  0.0  1.0  0.0
 1.0  1.0 -1.0 1.0 1.0  0.0  1.0  0.0
-1.0 -1.0 -1.0 0.0 0.0  0.0 -1.0  0.0
 1.0 -1.0 -1.0 1.0 0.0  0.0 -1.0  0.0
-1.0 -1.0  1.0 0.0 1.0  0.0 -1.0  0.0
-1.0 -1.0  1.0 0.0 1.0  0.0 -1.0  0.0
 1.0 -1.0 -1.0 1.0 0.0  0.0 -1.0  0.0
 1.0 -1.0  1.0 1.0 1.0  0.0 -1.0  0.0

보시다시피 x, y, z, tu, tv, nx, ny, nz 데이터가 총 36줄 있습니다.매 세 줄이 하나의 삼각형을 구성하여 총 12개의 삼각형이 큐브를 형성하게 됩니다.이 형식은 매우 직관적이며, 정점 버퍼로 직접 읽어 들여 수정 없이 렌더링할 수 있습니다.

이제 주의할 점은 일부 3D 모델링 프로그램이 데이터를 왼손 좌표계나 오른손 좌표계와 같은 다른 순서로 내보낼 수 있다는 것입니다.기본적으로 DirectX 11은 왼손 좌표계를 사용하므로 모델 데이터도 이에 맞춰야 합니다.이러한 차이점을 유의하고, 파싱 프로그램이 데이터를 올바른 형식이나 순서로 변환할 수 있도록 해야 합니다.

 


ModelClass.h

 

파일을 읽기 위한 fstream을 사용하고, 모델 형식을 나타내는 구조체 ModelTpye을 선언, 포인터를 생성합니다.

#include <fstream>
using namespace std;

class ModelClass
{
private:
    struct ModelType
    {
        float x, y, z;
        float tu, tv;
        float nx, ny, nz;
    };
	
private:
    ModelType* m_model;

 

또한 이제 초기화에서 모델의 파일 이름을 받아 초기화 단계에서 모델 데이터를 로드할것입니다.

모델 데이터 로드, 언로드 함수를 추가합니다.

	// ....
public:
	bool Initialize(ID3D11Device* , ID3D11DeviceContext* , char*, char* modelFile);

private:
	bool LoadTexture(ID3D11Device*, ID3D11DeviceContext*, char*);
	void ReleaseTexture();
}

 

ModleClass.cpp

 

1. Initialzie

초기화 첫 부분에 모델을 로드합니다.

bool ModelClass::Initialize(ID3D11Device* device, ID3D11DeviceContext* deviceContext, char* modelFilename, char* textureFilename)
{
	// Load in the model data
	LoadModel(modelFilename);
   	
	// ....
}

 

2. InitializeBuffer.cpp

이제 더 이상 정점 및 인덱스 수를 수동으로 설정하지 않습니다.

인덱스 버퍼는 정점 버퍼에 들어간 정점의 순서와 똑같습니다.

bool ModelClass::InitializeBuffers(ID3D11Device* device)
{
	VertexType* vertices;
	unsigned long* indices;
	D3D11_BUFFER_DESC vertexBufferDesc, indexBufferDesc;
	D3D11_SUBRESOURCE_DATA vertexData, indexData;
	HRESULT result;
	
	// Create the vertex array.
	vertices = new VertexType[m_vertexCount];
    
	// Create the index array.
	indices = new unsigned long[m_indexCount];

	// Create the vertex array.
	vertices = new VertexType[m_vertexCount];

	// Load the vertex array and index array with data.
	for (int i = 0; i < m_vertexCount; i++)
	{
		vertices[i].position = XMFLOAT3(m_model[i].x, m_model[i].y, m_model[i].z);
		vertices[i].texture = XMFLOAT2(m_model[i].tu, m_model[i].tv);
		vertices[i].normal = XMFLOAT3(m_model[i].nx, m_model[i].ny, m_model[i].nz);

		indices[i] = i;
	}

 

 

3. LoadModel

간단한 파일 읽어내는 코드다.

bool ModelClass::LoadModel(char* filename)
{
	ifstream fin;
	char input;
	int i;

	// 파일 오픈.
	fin.open(filename);

	// 파일을 열수 없으면 종료.
	if (fin.fail())
	{
		return false;
	}

	// 정점 개수가 나오기 전까지 읽어냅니다.
	fin.get(input);
	while (input != ':')
	{
		fin.get(input);
	}

	// 나머지 줄을 읽어 정점 개수를 m_vertexCount에 가져옵니다.
	fin >> m_vertexCount;

	// 인덱스의 개수는 정점 개수와 같습니다.
	m_indexCount = m_vertexCount;

	// 읽어온 정점 개수를 사용하여 배열을 만들어 포인터에 넣습니다.
	m_model = new ModelType[m_vertexCount];

	// 정점 데이터가 나오기 전까지 읽어냅니다.
	fin.get(input);
	while (input != ':')
	{
		fin.get(input);
	}
	// 공백문자 읽어내기
	fin.get(input);
	fin.get(input);

	// 정점 데이터를 읽습니다 
	for (i = 0; i < m_vertexCount; i++)
	{
		fin >> m_model[i].x >> m_model[i].y >> m_model[i].z;
		fin >> m_model[i].tu >> m_model[i].tv;
		fin >> m_model[i].nx >> m_model[i].ny >> m_model[i].nz;
	}

	// 파일 닫기
	fin.close();

	return true;
}

 

ApplicationClass.cpp

 

1. Initialize

모델의 초기화에서 모델 파일의 이름을 사용하기에 모델이름과 함께 초기화 시켜줍니다.

bool ApplicationClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{
	bool result;
	char textureFileName[128];
	char modelFileName[128]; // 추가
    
	// 모델의 파일명을 설정합니다.
	strcpy_s(modelFilename, "../Engine/data/cube.txt");
    
    m_Model = new ModelClass;
    
    // 파일명과 함께 모델을 초기화합니다.
	m_Model->Initialize(m_Direct3D->GetDevice(), m_Direct3D->GetDeviceContext(), modelFilename, TextureFilename);

 


요약

 

3D모델을 로드하고 렌더링했습니다.

 

초기화

  • ModelClass
    1. 3D모델 파일의 정점 데이터를 가져옵니다.
    2. 가져온 데이터로 정점,인덱스 버퍼를 생성합니다.

 


용어 정리

 

없다!

 

'DirectX 11 > DX11 Tutorial' 카테고리의 다른 글

[06] 난반사 조명(Diffuse light)  (0) 2024.05.24
[05] 텍스쳐링  (0) 2024.05.23
[04] 버퍼, 셰이더, HLSL  (0) 2024.05.21
[03] DirectX 11 초기화 (3)  (0) 2024.05.20
[02] DirectX 11 초기화 (2)  (0) 2024.05.20

 

 

Tutorial 6: Diffuse Lighting

Tutorial 6: Diffuse Lighting In this tutorial I will cover how to light 3D objects using diffuse lighting and DirectX 11. We will start with the code from the previous tutorial and modify it. The type of diffuse lighting we will be implementing is called d

www.rastertek.com

DirectX11의 공부 목적으로 위 링크의 튜토리얼을 보고 제 생각을 포함하여 정리하였습니다.


들어가며

 

이번 튜토리얼에서는 Dx11을 사용하여 물체에 난반사 조명을 적용시키는 방법을 다룰 것입니다.

난반사 광이란 빛을 받아 여러 방향으로 고르게 반사되는 빛을 뜻합니다.

 

우리가 구현할 난반사 조명의 유형을 방향성 조명(Directional lighting)이라고 합니다.

방향성 조명은 태양이 지구를 비추는 방식과 유사합니다. 이 조명은 매우 멀리 떨어진 광원으로부터 오는 빛의 방향을 기반으로 물체에 비치는 빛의 양(밝기)을 결정할 수 있습니다. 그러나 직접 닿지 않는 표면은 조명되지 않습니다.(첫 번째로 빛이 닿는 부분만 적용되며 빛의 반사까지는 계산하지 않는다는 뜻인듯?)

 

난반사 조명의 구현은 VS와 PS에서 이루어집니다. 난반사 조명에는 방향과 우리가 조명하고자 하는 폴리곤의 법선 벡터가 필요합니다. 폴리곤을 구성하는 세 개의 정점을 이용하여 폴리곤의 법선을 계산할 수 있습니다. 이 튜토리얼에서는 조명 방적식에 난반사 조명의 색상을 구현할 것입니다.

 

프레임워크

 

(NEW) LightClass
장면에서 광원을 나타냅니다, 빛의 방향과 색상을 보유하는 것 외에는 다른 역할을 하지 않습니다.

 

(NEW) LightShaderClass

기존 TextureShaderClass를 제거하고 모델의 조명 셰이딩을 처리하는 셰이더 클래스입니다.


Light.vs

 

이전 셰이더와 특별하게 다른점은 법선 벡터가 float3형태로 정점데이터에 생기고, 월드 공간에서의  법선 벡터를구합니다.

PixelInputType LightVertexShader(VertexInputType input)
{
  	// ....
    
    // 법선벡터를 월드공간으로 공간변환 합니다.
    output.normal = mul(input.normal, (float3x3) worldMatrix);
	
    // 계산된 법선벡터를 정규화 합니다.
    output.normal = normalize(output.normal);

    return output;
}

 

Light.ps

 

정점 데이터에 법선벡터의 float3이 추가되었고 상수버퍼도 추가 되었습니다.

또한 난반사광의 양을 이곳에서 계산합니다.

Texture2D shaderTexture : register(t0);
SamplerState SampleType : register(s0);

cbuffer LightBuffer
{
    float4 diffuseColor;
    float3 lightDirection;
    float padding;
};

struct PixelInputType
{
    float4 position : SV_POSITION;
    float2 tex : TEXCOORD0;
    float3 normal : NORMAL;
};


////////////////////////////////////////////////////////////////////////////////
// Pixel Shader
float4 LightPixelShader(PixelInputType input) : SV_TARGET
{
    float4 textureColor;
    float3 lightDir;
    float lightIntensity;
    float4 color;

    textureColor = shaderTexture.Sample(SampleType, input.tex);

    // 빛의 방향을 반전시킵니다.
    lightDir = -lightDirection;

    // 이 픽셀의 빛의 양을 계산합니다.
    lightIntensity = saturate(dot(input.normal, lightDir));

    // 빛의 양과 픽셀의 최종 색상을 곱하여 최최종 색상을 만들어 냅니다.
    color = saturate(diffuseColor * lightIntensity);

    // 최최종 색상과 텍스쳐의 색상을 곱하여 텍스쳐를 적용시킵니다.
    color = color * textureColor;

    return color;
}

 

LightShaderClass.h

 

Light 셰이더를 만들고 적용하기 위한 클래스

특별하게 다른건 PS의 상수버퍼에 들어가기 위한 구조체가 추가 되었다. 이 상수버퍼 구조체는 조명 정보를 저장한다.

	struct LightBufferType
	{
	    XMFLOAT4 diffuseColor;
	    XMFLOAT3 lightDirection;
	    float padding;//구조체의 메모리 크기가 16(바이트) 배수가 되도록 쓰레기 값을 넣었습니다.
	};
    
private:
	// PS의 상수버퍼
	ID3D11Buffer* m_lightBuffer;

 

 

LightShaderClass.cpp

 

1. InitializeShader

법선 벡터의 시멘틱은 대체적으로 NORMAL 이다.

bool LightShaderClass::InitializeShader(ID3D11Device* device, HWND hwnd, WCHAR* vsFilename, WCHAR* psFilename)
{

	// Vertex input layout description 생성
	// polygonLayout[0] (POSITION) 생성 ....
	// polygonLayout[1] (TEXCOORD) 생성 ....
	
	polygonLayout[2].SemanticName = "NORMAL";
	polygonLayout[2].SemanticIndex = 0;
	polygonLayout[2].Format = DXGI_FORMAT_R32G32B32_FLOAT;
	polygonLayout[2].InputSlot = 0;
	polygonLayout[2].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT;
	polygonLayout[2].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
	polygonLayout[2].InstanceDataStepRate = 0;
    
    // ....

PS에 들어갈 상수버퍼를 생성해준다.

    // VS에 들어갈 상수버퍼 생성 ....
    
    // 픽셀 셰이더에 들어갈 상수버퍼의 Buffer description을 설정합니다.    
    lightBufferDesc.Usage = D3D11_USAGE_DYNAMIC;
    // ByteWidth가 16바이트의 배수가 아니면 실패합니다.
    lightBufferDesc.ByteWidth = sizeof(LightBufferType);
    lightBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
    lightBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
    lightBufferDesc.MiscFlags = 0;
    lightBufferDesc.StructureByteStride = 0;

    // Create the constant buffer pointer so we can access the vertex shader constant buffer from within this class.
    result = device->CreateBuffer(&lightBufferDesc, NULL, &m_lightBuffer);

    return true;
}

 

2. SetShaderParameters.cpp

PS들어갈 상수버퍼의 값을 Map/Unmap으로 수정하고 PS단계에 설정한다.

bool LightShaderClass::SetShaderParameters(ID3D11DeviceContext* deviceContext, XMMATRIX worldMatrix, XMMATRIX viewMatrix, XMMATRIX projectionMatrix,
    ID3D11ShaderResourceView* texture, XMFLOAT3 lightDirection, XMFLOAT4 diffuseColor)
{
    // VS에 들어갈 상수버퍼 업데이트 후 VS에 배치 ....
    deviceContext->PSSetShaderResources(0, 1, &texture);

    // PS에 들어갈 상수버퍼 업데이트
    result = deviceContext->Map(m_lightBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);

    // Get a pointer to the data in the constant buffer.
    dataPtr2 = (LightBufferType*)mappedResource.pData;

    // Copy the lighting variables into the constant buffer.
    dataPtr2->diffuseColor = diffuseColor;
    dataPtr2->lightDirection = lightDirection;
    dataPtr2->padding = 0.0f;

    // Unlock the constant buffer.
    deviceContext->Unmap(m_lightBuffer, 0);

    // Set the position of the light constant buffer in the pixel shader.
    bufferNumber = 0;

    // 상수버퍼 PS단계에 배치
    deviceContext->PSSetConstantBuffers(bufferNumber, 1, &m_lightBuffer);

    return true;
}

 

ModelClass.h

 

모델에서 사용하는 정점데이터를 수정합니다.

class ModelClass
{
private:
	struct VertexType
	{
		XMFLOAT3 position;
		XMFLOAT2 texture;
		XMFLOAT3 normal; //추가
	};

 

 

ModelClass.cpp

 

1. InitializeBuffers

정점 데이터에 normal의 값을 작성하여 저장합니다.

ID3D11ShaderResourceView* ModelClass::GetTexture()
{
	return m_Texture->GetTexture();
}

bool ModelClass::InitializeBuffers(ID3D11Device* device)
{
	// .... 
	
	vertices[0].position = XMFLOAT3(-1.0f, 1.0f, 0.0f);  // Top left.
	vertices[0].texture = XMFLOAT2(0.0f, 0.0f);
	vertices[0].normal = XMFLOAT3(0.0f, 0.0f,-1.0f); // 법선 벡터 추가

	vertices[1].position = XMFLOAT3(1.0f, 1.0f, 0.0f);  // Top right
	vertices[1].texture = XMFLOAT2(1.0f, 0.0f);
	vertices[1].normal = XMFLOAT3(0.0f, 0.0f, -1.0f);

	vertices[2].position = XMFLOAT3(-1.0f, -1.0f, 0.0f);  // Bottom left.
	vertices[2].texture = XMFLOAT2(0.0f, 1.0f);
	vertices[2].normal = XMFLOAT3(0.0f, 0.0f, -1.0f);


	vertices[3].position = XMFLOAT3(1.0f, -1.0f, 0.0f);  // Bottom right.
	vertices[3].texture = XMFLOAT2(1.0f, 1.0f);
	vertices[3].normal = XMFLOAT3(0.0f, 0.0f, -1.0f);

 

 

LightClass.h

 

이 클래스의 목적은 빛의 방향과 색상을 유지하는 것뿐입니다.

#pragma once
#include <directxmath.h>
using namespace DirectX;

class LightClass
{
public:
    LightClass();
    LightClass(const LightClass&) = default;
    ~LightClass() = default;

    void SetDiffuseColor(float, float, float, float);
    void SetDirection(float, float, float);

    XMFLOAT4 GetDiffuseColor();
    XMFLOAT3 GetDirection();

private:
    XMFLOAT4 m_diffuseColor;
    XMFLOAT3 m_direction;
};

 

LightClass.cpp

 

#include "LightClass.h"

void LightClass::SetDiffuseColor(float red, float green, float blue, float alpha)
{
    m_diffuseColor = XMFLOAT4(red, green, blue, alpha);
    return;
}


void LightClass::SetDirection(float x, float y, float z)
{
    m_direction = XMFLOAT3(x, y, z);
    return;
}


XMFLOAT4 LightClass::GetDiffuseColor()
{
    return m_diffuseColor;
}


XMFLOAT3 LightClass::GetDirection()
{
    return m_direction;
}

 

ApplicationClass.h

 

생성한 Light관련 클래스를 include하고 개체의 포인터를 생성합니다.

Render함수도 이제는 매개변수로 float을 받게됩니다.

#pragma once

#include <windows.h>
#include "D3DClass.h"
#include "CameraClass.h"
#include "ModelClass.h"
#include "LightClass.h"
#include "LightShaderClass.h"

const bool FULL_SCREEN = false;
const bool VSYNC_ENABLED = true;
const float SCREEN_DEPTH = 1000.0f;
const float SCREEN_NEAR = 0.3f;

class ApplicationClass
{
public:
	ApplicationClass();
	ApplicationClass(const ApplicationClass&) = default;
	~ApplicationClass() = default;

	bool Initialize(int, int, HWND);
	void Shutdown();
	bool Frame();

private:
	bool Render(float);

private:

	D3DClass* m_Direct3D;
	CameraClass* m_Camera;
	ModelClass* m_Model;
	LightShaderClass* m_LightShader;
	LightClass* m_Light;
};

 

Application.cpp

 

1. Initialize

추가된 포인터의 개체를 생성하고 초기화 합니다.

bool ApplicationClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{

	// ....
    
	// Light shader 개체를 생성하고 초기화 합니다.
	m_LightShader = new LightShaderClass;

	result = m_LightShader->Initialize(m_Direct3D->GetDevice(), hwnd);
	if (!result)
	{
		MessageBox(hwnd, L"Could not initialize the light shader object.", L"Error", MB_OK);
		return false;
	}

	// Light 개체를 생성하고 초기화 합니다.
	m_Light = new LightClass;

	m_Light->SetDiffuseColor(1.0f, 1.0f, 1.0f, 1.0f);
	m_Light->SetDirection(0.0f, 0.0f, 1.0f);

	return true;
}

 

2. Frame

각 프레임마다 계속 회전을 업데이트한 정적 변수를 추가하여 Render함수에 전달합니다.

bool ApplicationClass::Frame()
{
	static float rotation = 0.0f;
	bool result;

	// 회전 값을 프레임마다 업데이트 합니다.
	rotation -= 0.0174532925f * 0.1f;
	if (rotation < 0.0f)
	{
		rotation += 360.0f;
	}

	// 그래픽 장면을 렌더링합니다. 
	result = Render(rotation);

	return true;
}

 

3. Render

bool ApplicationClass::Render(float rotation)
{
	// ....

	// camera 와 d3d objects에서 world, view, projection 행렬을 가져옵니다.
	m_Direct3D->GetWorldMatrix(worldMatrix);
	m_Camera->GetViewMatrix(viewMatrix);
	m_Direct3D->GetProjectionMatrix(projectionMatrix);

	//(추가코드) 삼각형에 rotation값 만큼 회전을 시킵니다. 
	worldMatrix = XMMatrixRotationY(rotation);

	// 렌더링 파이프라인에 모델의 정점, 인덱스 버퍼를 배치하여 그릴 준비를 합니다.
	m_Model->Render(m_Direct3D->GetDeviceContext());

	//(수정코드) 라이트 셰이더를 사용하여 모델을 렌더링합니다.
	result = m_LightShader->Render(m_Direct3D->GetDeviceContext(), m_Model->GetIndexCount(), worldMatrix, viewMatrix, projectionMatrix, m_Model->GetTexture(),m_Light->GetDirection(), m_Light->GetDiffuseColor());

	// 렌더링된 장면을 화면에 표시한다.
	m_Direct3D->EndScene();

	return true;
}

 


요약 

 

다음은 코드에 몇 가지 변경을 가하여 기본적인 난방향 조명을 구현한 것입니다. 법선 벡터가 어떻게 작동하는지, 그리고 왜 폴리곤 면에 조명을 계산하는 데 중요한지 이해하는 것이 중요합니다. 우리의 D3DClass에서 후면 컬링(back face culling)이 활성화되어 있기 때문에 회전하는 삼각형의 뒷면은 조명이 비추지 않을 것입니다.

사각형이 뒷면이 보이면 다시 앞면이 바로 나오게 회전 값을 조정했다.

 

연습

 

1. 조명 색상을 녹색으로 변경합니다.

DiffuseColor를 녹색으로 변경해준다.

 

내 생각에 중요했던 것

 

1. 기존 물체의 공간변환시에는 float4를 사용한 공간변환이지만, 광원의 위치가 월드로 공간변환시 사용한건 float3이다.

이유는 투영 변환에서 원근감을 표현하기위해 4차원 벡터가 필요했던것.

광원은 카메라의 위치와도 무관하고 월드 공간까지만 있어도 충분하기 때문에 flaot3로 공간변환이 되었다.

 

2. 상수 버퍼 

상수 버퍼를 구성하는 구조체는 16바이트의 배수로 정의되어야 한다.

이유는 GPU에서 SIMD를 활용하기 위해서다.

 

SIMD는 "Single Instruction, Multiple Data"의 약자로 단일 명령으로 여러 데이터를 동시에 처리 한다는 뜻이다.

즉, 장치가 한 번의 명령으로 여러 데이터를 병렬로 처리할 수 있게 해주는 기술이다.

이 SIMD기술이 주로 128bit(16byte)의 단위로 데이터를 처리하기 때문이다!

 

예시를 들어보자면 숫자 배열의 덧셈이있다.

A = [1, 2, 3, 4]
B = [5, 6, 7, 8]

두 배열의 각 인덱스를 더해 새로운 배열 C를 만든다고 할때,

일반적인 방식으로는 밑의 과정을 거친다.

1. C[0] = 1 + 5 = 6

2. C[1] = 2 + 6 = 8

3. C[2] = 3 + 7 = 10

4. C[3] = 4 + 8 = 12

 

하지만 SIMD를 사용하는 경우

1. [1+5, 2+6, 3+7, 4+8] = [6, 8, 10, 12]

이렇게 한번에 C를 구할 수 있다.

 

 


용어 정리

 

Diffuse light : 난반사, 광원에 의해 여러 방향으로 고르게 반사되는 빛.

물체를 여러 방향에서 바라보아도 물체의 색이 크게 변하지 않는 이유다. (빛이 고르게 반사되는 난반사가 있기때문에)

 

Diffuse lighting : 물체에 광원에 의한 난반사를 적용시키는 것

 

법선 벡터 : 평면 벡터의 수직인 벡터

 

SIMD : 하나의 명령으로 여러 데이터 처리를 하는 기술

'DirectX 11 > DX11 Tutorial' 카테고리의 다른 글

[07] 모델 렌더링  (0) 2024.05.26
[05] 텍스쳐링  (0) 2024.05.23
[04] 버퍼, 셰이더, HLSL  (0) 2024.05.21
[03] DirectX 11 초기화 (3)  (0) 2024.05.20
[02] DirectX 11 초기화 (2)  (0) 2024.05.20
 

Tutorial 5: Texturing

Tutorial 5: Texturing This tutorial will explain how to use texturing in DirectX 11. Texturing allows us to add photorealism to our scenes by applying photographs and other images onto polygon faces. For example, in this tutorial we will take the following

www.rastertek.com

DirectX11의 공부 목적으로 위 링크의 튜토리얼을 보고 제 생각을 포함하여 정리하였습니다.


들어가며

 

이 튜토리얼에서는 DirectX11에서 텍스처링을 사용하는 방법을 설명합니다.

다음 이미지를 사용합니다.

그 후 이전 튜토리얼에 생성했던 삼각형에 적용합니다.

우리가 사용할 텍스처의 형식은 .tga파일 입니다.

 

코드에 들어가기 전에 텍스처 매핑이 어떻게 작동하는지 알아야합니다.

이미지의 픽셀을 폴리곤으로 매핑하기 위해 우리는 텍셀 좌표계를 사용합니다. 이 좌표계는 픽셀로 된 텍스쳐의 크기를 0.0f 에서 1.0f 사이의 값으로 정규화 하여 사용합니다.

예를 들어 텍스쳐 너비가 256픽셀인 경우 첫번째 픽셀은 0.0f로 매핑되고, 256번째 픽셀은 1.0f로 매핑됩니다.

128번째인 중간 픽셀은 0.5f로 매핑됩니다.

 

텍셀 좌표계에서 가로 길이는 "U" 이고 세로 길이는 "V" 입니다.

 

이번 튜토리얼의 업데이트 된 프레임 워크 입니다 

이전 튜토리얼 이후 프레임 작업에 대한 변경 사항은 ModelClass 내부에 TextrueClass와 ColorShaderClass를 대체하는 TextureShaderClass가 생겼습니다.

새로운 TextureShaderClass의 코드를 살펴보는 것으로 시작하겠습니다.


Texture.vs

 

이전시간과는 달리 정점 데이터의 유형에 색상을 사용하지 않고 텍스쳐 좌표를 사용합니다.

텍스쳐 좌표는 U와 V, 부동 소수점 두 개로 float2를 사용합니다.

텍스쳐 좌표의 의미를 가진 시멘틱은 TEXCOORD0입니다. 여러 텍스쳐 좌표가 허용되므로 0을 다른 숫자로 변경 가능합니다.

 

cbuffer MatrixBuffer
{
    matrix worldMatrix;
    matrix viewMatrix;
    matrix projectionMatrix;
};

struct VertexInputType
{
    float4 position : POSITION;
    float2 tex : TEXCOORD0;
};

struct PixelInputType
{
    float4 position : SV_POSITION;
    float2 tex : TEXCOORD0;
};


PixelInputType TextureVertexShader(VertexInputType input)
{
    PixelInputType output;

    // 행렬변환을 위해 마지막 벡터를 1.0f로 변경
    input.position.w = 1.0f;

    // 공간 변환
    output.position = mul(input.position, worldMatrix);
    output.position = mul(output.position, viewMatrix);
    output.position = mul(output.position, projectionMatrix);

    // 픽셸 세이더의 텍스쳐 좌표 저장
    output.tex = input.tex;

    return output;
}

 

Texture.ps

 

이번 픽셀 셰이더에는 두 개의 전역변수가 있습니다.

첫 번째는 텍스쳐 리소스인 Texture2D ShaderTexture입니다. 이는 모델에서 텍스쳐를 렌더링하는데 사용되는 텍스쳐 리소스가 됩니다.

두 번째는 SmaplerState SampleType입니다. SamplerState은 텍스쳐의 특정위치에서 어떻게 색을 가져오는지 방법을 정의하는 설정입니다.

예를 들어 256 * 256 픽셀의 텍스쳐를 적용시킬 폴리곤이 카메라와 멀리 떨어져 있어서 화면에서 8픽셀만 구성되는 경우, SamplerState를 사용하면 설정 값에 따라 그려질 픽셀의 색상을 결정합니다.

 

우리는 TextureShaderClass에서도 샘플러 상태를 정의한 다음, 이 픽셀 셰이더가 사용할 수 있도록 리소스 포인터에 이를 연결할 것입니다.

Texture2D shaderTexture : register(t0);
SamplerState SampleType : register(s0);

struct PixelInputType
{
    float4 position : SV_POSITION;
    float2 tex : TEXCOORD0;
};

float4 TexturePixelShader(PixelInputType input) : SV_TARGET
{
    float4 textureColor;

    // Sample the pixel color from the texture using the sampler at this texture coordinate location.
    textureColor = shaderTexture.Sample(SampleType, input.tex);

    return textureColor;
}

 

TextureShaderClass.h

 

이 클래스는 이전 튜토리얼의 ColorShaderClass의 업데이트 버전입니다.

추가된것은 private의 ID3D11SamplerState의 포인터가 추가되었습니다.

ID3D11SamplerState* m_sampleState;

 

TextureShaderClass.cpp

 

1. Initialize

새로운 셰이더 파일의 이름을 기술합니다.

bool TextureShaderClass::Initialize(ID3D11Device* device, HWND hwnd)
{
	bool result;
	wchar_t vsFilename[128];
	wchar_t psFilename[128];
	int error;

	///////////////////////////////////////////////////////////////////
	// 셰이더의 파일이름 하드코딩후 저장
	///////////////////////////////////////////////////////////////////
	error = wcscpy_s(vsFilename, 128, L"Texture_VS");
	error = wcscpy_s(psFilename, 128, L"Texture_PS");


	///////////////////////////////////////////////////////////////////
	// 셰이더 생성후 초기화
	///////////////////////////////////////////////////////////////////
	InitializeShader(device, hwnd, vsFilename, psFilename);

	return true;
}

 

2. Render

Render함수는 이제 텍스쳐 리소스에 대한 포인터인 texture라는 새 매개변수를 사용합니다. 이 포인터는 SetShaderParameters 함수로 전달되어 셰이더에 텍스쳐가 설정됩니다.

bool TextureShaderClass::Render(ID3D11DeviceContext * deviceContext, int indexCount, XMMATRIX worldMatrix, XMMATRIX viewMatrix,
	XMMATRIX projectionMatrix, ID3D11ShaderResourceView * texture)
{
	bool result;

	// Set the shader parameters that it will use for rendering.
	result = SetShaderParameters(deviceContext, worldMatrix, viewMatrix, projectionMatrix, texture);

	// Now render the prepared buffers with the shader.
	RenderShader(deviceContext, indexCount);

	return true;
}


3. InitializeShader

새로운 셰이더를 설정합니다.

	// Texture sampler에 대한 설명을 담는 새로운 변수 추가
	D3D11_SAMPLER_DESC samplerDesc;
	
	/*----------------------------------------------*/
	// 셰이더 파일 컴파일, 생성 부분
	/*----------------------------------------------*/

이제 정점 데이터에 색상대신 텍스쳐 요소가 있으므로 Input layout이 변경되었습니다. 첫 번째 위치 요소는 동일하지만 두 번째 요소의 SemanticName과 Format은 변경되었습니다.

	/*----------------------------------------------*/
	// polygonLayout[0] 생성
	/*----------------------------------------------*/
	
	polygonLayout[1].SemanticName = "TEXCOORD";
	polygonLayout[1].SemanticIndex = 0;
	polygonLayout[1].Format = DXGI_FORMAT_R32G32_FLOAT; // float2(u, v좌표)
	polygonLayout[1].InputSlot = 0;
	polygonLayout[1].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT;
	polygonLayout[1].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
	polygonLayout[1].InstanceDataStepRate = 0;
	
	// Get a count of the elements in the layout.
	numElements = sizeof(polygonLayout) / sizeof(polygonLayout[0]);

	// Create the vertex input layout.
	result = device->CreateInputLayout(polygonLayout, numElements, vertexShaderBuffer->GetBufferPointer(),
		vertexShaderBuffer->GetBufferSize(), &m_layout);
	    
	/*----------------------------------------------*/
	// 상수 버퍼 생성
	/*----------------------------------------------*/

이후 Sampler state의 설명이 설정됩니다. 이후에 픽셀 셰이더에 전달됩니다.

Sampler state에서 가장 중요한 요소는 Filter 입니다. Filter는 폴리곤에 텍스쳐의 최종 모양을 만들기 위해 어떤 픽셀을 사용하거나 결합할지의 방법을 정합니다.

 

이번 시간에서는 처리 비용이 비싸지만 최상의 시각적 결과를 제공하는 D3D11_FILTER_MIN_MAG_MIP_LINEAR을 사용해 축소, 확대 및 mip level sampling을 위해 선형 보간을 사용하도록 지시합니다.

 

AddredssU와 AddressV는 좌표가 0.0f와 1.0f 사이에 유지되도록 보장하는 Wrap으로 설정됩니다.

// Create a texture sampler state description.
	samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
	samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
	samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
	samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
	samplerDesc.MipLODBias = 0.0f;
	samplerDesc.MaxAnisotropy = 1;
	samplerDesc.ComparisonFunc = D3D11_COMPARISON_ALWAYS;
	samplerDesc.BorderColor[0] = 0;
	samplerDesc.BorderColor[1] = 0;
	samplerDesc.BorderColor[2] = 0;
	samplerDesc.BorderColor[3] = 0;
	samplerDesc.MinLOD = 0;
	samplerDesc.MaxLOD = D3D11_FLOAT32_MAX;

	// Create the texture sampler state.
	result = device->CreateSamplerState(&samplerDesc, &m_sampleState);

	return true;
}

 

4. SetShaderParameters

SetShaderParameters함수는 이제 텍스쳐 리소스에 대한 포인터를 가져온 다음 새 텍스쳐 리소스 포인터를 사용하여 셰이더에 할당합니다. 버퍼 렌더링이 발생하기 전에 텍스쳐를 설정해야 합니다.

bool TextureShaderClass::SetShaderParameters(ID3D11DeviceContext* deviceContext, XMMATRIX worldMatrix, XMMATRIX viewMatrix,
	XMMATRIX projectionMatrix, ID3D11ShaderResourceView* texture)
{
	/*----------------------------------------------*/
	// 상수 버퍼 Map, Unmap, VSSetConstantBuffers 과정
	/*----------------------------------------------*/
	
	// pixel shader에 shader texture resource를 배치합니다.
	deviceContext->PSSetShaderResources(0, 1, &texture);
	
	return true;
}

 

5. RenderShader

RenderShader함수는 렌더링 전에 Smaple state 설정을 픽셀 셰이더에 배치합니다.

void TextureShaderClass::RenderShader(ID3D11DeviceContext* deviceContext, int indexCount)
{
	// Set the vertex input layout.
	deviceContext->IASetInputLayout(m_layout);

	// Set the vertex and pixel shaders that will be used to render this triangle.
	deviceContext->VSSetShader(m_vertexShader, NULL, 0);
	deviceContext->PSSetShader(m_pixelShader, NULL, 0);

	// 픽셀 셰이더에 Texture sampler state를 배치합니다.
	deviceContext->PSSetSamplers(0, 1, &m_sampleState);

	// Render the triangle.
	deviceContext->DrawIndexed(indexCount, 0, 0);

	return;
}

 

TextureClass.h

 

TextureClass는 단일 텍스처 리소스의 로드, 언로드 및 액세스를 캡슐화합니다. 각 텍스쳐는 인스턴스화 된 이 개체가 필요합니다.

데이터를 더 쉽게 읽을 수 있도록 TargaHeader 구조체를 정의했습니다.

#pragma once
#include <d3d11.h>
#include <stdio.h>

class TextureClass
{
private:

    struct TargaHeader
    {
        unsigned char data1[12];
        unsigned short width;
        unsigned short height;
        unsigned char bpp;
        unsigned char data2;
    };

public:
    TextureClass();
    TextureClass(const TextureClass&) = default;
    ~TextureClass() = default;

    bool Initialize(ID3D11Device*, ID3D11DeviceContext*, char*);
    void Shutdown();

    ID3D11ShaderResourceView* GetTexture();

    int GetWidth();
    int GetHeight();

private:

    // Targa 읽기 기능
    bool LoadTarga32Bit(char*);

private:

    // 파일에서 직접 읽은 원시 Targa데이터
    unsigned char* m_targaData;
    // DirectX가 렌더링에 사용할 구조화된 텍스쳐 데이터
    ID3D11Texture2D* m_texture;
    // 셰이더가 그림을 그릴 때 텍스쳐 데이터에 액세스하는 리소스 장착 칸
    ID3D11ShaderResourceView* m_textureView;
    // 텍스쳐의 크기
    int m_width, m_height;
};

 

TextureClass.cpp

 

1. Initialize

이 함수는 먼저 Targa 데이터를 배열에 로드합니다. 그런 다음 텍스쳐를 생성하고 Targa 데이터를 올바른 형식으로 로드합니다. 즉, Targa 이미지 파일을 Dx 텍스쳐로 변환하는 기능을 합니다.

▼올바른 형식?

더보기

Targa 이미지 데이터는 파일의 시작 부분에 이미지의 맨 아래 행부터 저장됩니다.즉, 첫 번째 데이터는 이미지의 맨 아래쪽 픽셀에 해당하고, 마지막 데이터는 이미지의 맨 위쪽 픽셀에 해당합니다.

 

하지만 Dx11은 이미지 데이터를 위쪽에서 아래쪽으로 순서대로 읽기를 기대합니다

그래서 Targa 데이터를 뒤집어야 한다고 하는 것 입니다.

그런 다음 텍스쳐가 저장되면 Shader resource view를 생성합니다.

bool TextureClass::Initialize(ID3D11Device * device, ID3D11DeviceContext * deviceContext, char* filename)
{
	bool result;
	int height, width;
	D3D11_TEXTURE2D_DESC textureDesc;
	HRESULT hResult;
	unsigned int rowPitch;
	D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;

	// targa image data를 메모리에 저장합니다.
	result = LoadTarga32Bit(filename);

다음으로 Targa 데이터를 로드할 DirectX 텍스처에 대한 설명을 설정합니다. 설명이 완료되면 CreateTexture2D를 호출하여 빈 텍스처를 만듭니다. 다음 단계는 Targa 데이터를 빈 텍스처에 복사하는 것입니다.

	// Targa 이미지 데이터의 높이와 너비를 사용
	textureDesc.Height = m_height;
	textureDesc.Width = m_width;
	textureDesc.MipLevels = 0;
	textureDesc.ArraySize = 1;
	// 픽셀 형식을 32비트 RGBA 텍스처로 설정
	textureDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
	// SampleDesc를 기본값으로 설정합니다
	textureDesc.SampleDesc.Count = 1;
	textureDesc.SampleDesc.Quality = 0;
	textureDesc.Usage = D3D11_USAGE_DEFAULT;
	textureDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET;
	textureDesc.CPUAccessFlags = 0;
	textureDesc.MiscFlags = D3D11_RESOURCE_MISC_GENERATE_MIPS;

	// Create the empty texture.
	hResult = device->CreateTexture2D(&textureDesc, NULL, &m_texture);

여기서 우리는 Targa 데이터 배열을 DirectX 텍스처로 복사하기 위해 UpdateSubresource를 사용합니다.
이전 튜토리얼에서 기억하실지 모르겠지만, 우리는 ModelClass의 행렬을 행렬 상수 버퍼로 복사하기 위해 Map과 Unmap을 사용했습니다.

사실, 여기서도 우리의 텍스처 데이터를 Map과 Unmap으로 복사할 수 있었습니다.
일반적으로 Map과 Unmap을 사용하는 것이 UpdateSubresource를 사용하는 것보다 훨씬 빠르지만, 
두 가지 로딩 방법 모두 특정한 목적이 있으며 성능상의 이유로 올바르게 선택해야 합니다.

추천 사항은 매 프레임마다 또는 매우 자주 다시 로드될 데이터에는 Map과 Unmap을 사용하라는 것 입니다.
반면, 한 번만 로드되거나 로딩 시퀀스 동안 드물게 로드되는 데이터에는 UpdateSubresource를 사용해야 합니다. 
그 이유는 UpdateSubresource가 데이터를 고속 메모리에 넣어 캐시 유지 우선순위를 얻기 때문입니다. 
이는 데이터가 곧 제거되거나 다시 로드되지 않을 것임을 알고 있기 때문입니다. 

반면, Map과 Unmap은 DirectX가 곧 덮어쓸 것이라고 예상하기 때문에 메모리 위치를 캐시하지 않는 곳에 데이터를 넣습니다. 

 

그래서 D3D11_USAGE_DEFAULT를 사용하면 DirectX에 UpdateSubresource를 사용하니 데이터가 자주 변경되지 않음을 알려주고, D3D11_USAGE_DYNAMIC을 사용하면 이 유형의 데이터가 일시적인 것임을 DirectX에 알립니다.

	// targa 이미지 데이터를 텍스처에 복사합니다.
	deviceContext->UpdateSubresource(m_texture, 0, NULL, m_targaData, rowPitch, 0);

텍스처가 로드된 후 셰이더에서 텍스처를 설정하기 위한 포인터를 가질 수 있는 셰이더 리소스 뷰를 생성합니다.설명에서 우리는 어떤 거리에서도 고품질 텍스처 렌더링을 위한 전체 Mipmap 수준 범위를 제공하는 두 가지 중요한 Mipmap 변수를 설정했습니다.

 

셰이더 리소스 뷰가 생성되면 GenerateMips를 호출하고 Mipmap이 생성됩니다. 그러나 원하는 경우 더 나은 품질을 찾고 있다면 자신의 Mipmap 레벨을 수동으로 로드할 수 있습니다.

	// shader resource view description를 설정합니다.
	srvDesc.Format = textureDesc.Format;
	srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
	srvDesc.Texture2D.MostDetailedMip = 0;
	srvDesc.Texture2D.MipLevels = -1;

	// 텍스쳐를 위한 shader resource view를 생성합니다.
	hResult = device->CreateShaderResourceView(m_texture, &srvDesc, &m_textureView);
	if (FAILED(hResult))
	{
		return false;
	}

	// 이 텍스쳐의 mipmaps을 만듭니다.
	deviceContext->GenerateMips(m_textureView);

	// Release the targa image data now that the image data has been loaded into the texture.
	delete[] m_targaData;
	m_targaData = 0;

	return true;
}

 

2. GetTexture

어느 셰이더나 렌더링하는데 필요로하는 Textrue view를 제공해주는 헬퍼 함수입니다. 

ID3D11ShaderResourceView* TextureClass::GetTexture()
{
	return m_textureView;
}

 

3. LoadTarga32Bit

Targa 이미지 로딩 기능입니다. Targa 이미지는 거꾸로 저장되므로 사용하기 전에 뒤집어야 한다는 점을 다시 한 번 참고하세요. 따라서 여기서는 파일을 열고 배열로 읽은 다음 해당 배열 데이터를 가져와 올바른 순서로 m_targaData 배열에 로드하겠습니다.

우리는 의도적으로 알파 채널이 있는 32비트 Targa 파일만 다루고 있으며, 이 기능은 24비트로 저장된 Targa 파일을 거부합니다.

bool TextureClass::LoadTarga32Bit(char* filename)
{
	int error, bpp, imageSize, index, i, j, k;
	FILE* filePtr;
	unsigned int count;
	TargaHeader targaFileHeader;
	unsigned char* targaImage;


	// 바이너리 모드로 targa 파일을 엽니다.
	error = fopen_s(&filePtr, filename, "rb");
	if (error != 0)
	{
		return false;
	}

	// targaFileHeader 형식의 내용을 저장합니다.
	count = (unsigned int)fread(&targaFileHeader, sizeof(TargaHeader), 1, filePtr);
	if (count != 1)
	{
		return false;
	}

	// 헤더에서 중요한 정보를 가져옵니다.
	m_height = (int)targaFileHeader.height;
	m_width = (int)targaFileHeader.width;
	bpp = (int)targaFileHeader.bpp;

	// 32bit Targa 이미지인지 확인합니다.
	if (bpp != 32)
	{
		return false;
	}

	// 32bit 이미지의 데이터 크기를 계산합니다.
	imageSize = m_width * m_height * 4; // 각 픽셀이 4바이트이기 때문에 4를 곱해줍니다.

	// Targa 이미지 데이터를 저장할 메모리를 할당합니다.
	targaImage = new unsigned char[imageSize];

	// Targa 이미지 데이터를 읽어옵니다.
	count = (unsigned int)fread(targaImage, 1, imageSize, filePtr);
	if (count != imageSize)
	{
		return false;
	}

	// 파일을 닫습니다.
	error = fclose(filePtr);
	if (error != 0)
	{
		return false;
	}

	// targa destination data를 위한 메모리 할당을 합니다.
	m_targaData = new unsigned char[imageSize];

	// 우리의 빈 텍스쳐가 채워지기 시작할 인덱스 입니다.
	index = 0;

	// Targa 텍스쳐를 탐색하기 시작할 인덱스 입니다.
	k = (m_width * m_height * 4) - (m_width * 4);

	// targa 이미지 데이터를 올바른 순서로 targa 대상 배열에 복사합니다.
	for (j = 0; j < m_height; j++)
	{
		for (i = 0; i < m_width; i++)
		{
			m_targaData[index + 0] = targaImage[k + 2];  // Red.
			m_targaData[index + 1] = targaImage[k + 1];  // Green.
			m_targaData[index + 2] = targaImage[k + 0];  // Blue
			m_targaData[index + 3] = targaImage[k + 3];  // Alpha

			// targa 데이터에 대한 인덱스를 증가시킵니다.
			k += 4;
			index += 4;
		}

		// targa 이미지 데이터 인덱스를 거꾸로 읽어서 열 시작 부분의 이전 행으로 다시 설정합니다.
		k -= (m_width * 8);
	}

	// 대상 배열에 복사되었으므로 이제 targa 이미지 데이터를 해제합니다.
	delete[] targaImage;
	targaImage = 0;

	return true;
}

 

ModleClass.h

 

이제 TextureClass가 ModelClass에 포함됩니다.

	// 1. 구조 변경
	struct VertexType
	{
		XMFLOAT3 position;
		XMFLOAT2 texture;
	};
	
	// 2. 함수 추가
	ID3D11ShaderResourceView* GetTexture();
	// 텍스쳐 불러오기
	bool LoadTexture(ID3D11Device*, ID3D11DeviceContext*, char*);
	// 텍스쳐 해제
	void ReleaseTexture();
	
	// 3. 변수 추가
	//m_Texture는 이 모델의 텍스처 리소스를 로드, 해제 및 액세스하는 데 사용됩니다.
	TextureClass* m_Texture;

 

ModelClass.cpp

 

1. Initialize

매개변수가 추가 됐습니다.

// DeviceContext*와 char* 매개변수가 추가 됐습니다.
bool ModelClass::Initialize(ID3D11Device* device, ID3D11DeviceContext* deviceContext, char* textureFilename)
{
	bool result;

	result = InitializeBuffers(device);

	// 이 모델의 텍스쳐를 불러옵니다.
	result = LoadTexture(device, deviceContext, textureFilename);

	return true;
}

 

2. InitializeBuffer

정점 데이터를 color에서 texture로 수정합니다.

bool ModelClass::InitializeBuffers(ID3D11Device* device)
{
	
	///////////////////////////////////////////////////////////////////////////////
	// Vertex Buffer 생성
	///////////////////////////////////////////////////////////////////////////////
	{
		m_vertexCount = 4;
		// Create the vertex array.
		vertices = new VertexType[m_vertexCount];

		// Load the vertex array with data. (x, y, z)
		vertices[0].position = XMFLOAT3(-1.0f, 1.0f, 0.0f);  // Top left.
		vertices[0].texture = XMFLOAT2(0.0f, 0.0f);

		vertices[1].position = XMFLOAT3(1.0f, 1.0f, 0.0f);  // Top right
		vertices[1].texture = XMFLOAT2(1.0f, 0.0f);

		vertices[2].position = XMFLOAT3(-1.0f, -1.0f, 0.0f);  // Bottom left.
		vertices[2].texture = XMFLOAT2(0.0f, 1.0f);

		vertices[3].position = XMFLOAT3(1.0f, -1.0f, 0.0f);  // Bottom right.
		vertices[3].texture = XMFLOAT2(1.0f, 1.0f);
     
		/*----------------------------------------------*/
		// 생략
		/*----------------------------------------------*/
}

 

3. LoadTexture

이 함수는 텍스쳐 개체를 생성한 후 제공된 파일 이름으로 초기화하는 새로운 함수입니다. 이 함수는 초기화 중 호출됩니다.

bool ModelClass::LoadTexture(ID3D11Device* device, ID3D11DeviceContext* deviceContext, char* filename)
{
	bool result;

	// TextureClass개체를 생성하고 초기화 합니다.
	m_Texture = new TextureClass;

	result = m_Texture->Initialize(device, deviceContext, filename);

	return true;
}

 

4. ReleasTextrue

LoadTexture함수로 생성된 TextureClass개체를 해제 합니다.

void ModelClass::ReleaseTexture()
{
	// Release the texture object.
	if (m_Texture)
	{
		m_Texture->Shutdown();
		delete m_Texture;
		m_Texture = nullptr;
	}

	return;
}

 

ApplicationClass.h

 

// 삭제 됩니다.
//#include "ColorShaderClass.h"
// 추가 됩니다.
#include "TextureShaderClass.h"


class ApplicationClass
{
private:
	// private 멤버 변수가 삭제됩니다.
	//ColorShaderClass* m_ColorShader;

	// private 멤버 변수가 추가됩니다.
	TextureShaderClass* m_TextureShader;
}

 

ApplicaitionClass.cpp

 

1. Initialize

bool ApplicationClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{
	// 텍스쳐 파일의 이름을 저장할 버퍼
	char textureFileName[128];
    
    // ....
    
    // 우리가 불러올 텍스쳐 파일의 이름을 세팅합니다.
	strcpy_s(textureFileName, "stone01.tga");
	result = m_Model->Initialize(m_Direct3D->GetDevice(), m_Direct3D->GetDeviceContext(), textureFileName);
    
    // TextureShaderClass의 개체를 생성, 초기화 합니다.
	m_TextureShader = new TextureShaderClass;
	result = m_TextureShader->Initialize(m_Direct3D->GetDevice(), hwnd);
    
    // ....
}

 

2. Render

bool ApplicationClass::Render()
{
	// ....
	
	// TextureShaderClass를 사용하여 모델을 렌더링 합니다.
	result = m_TextureShader->Render(m_Direct3D->GetDeviceContext(), m_Model->GetIndexCount(), worldMatrix, viewMatrix, projectionMatrix, m_Model->GetTexture());
	if (!result)
	{
		return false;
	}

	// 렌더링된 장면을 화면에 표시한다.
	m_Direct3D->EndScene();

	return true;
}

 


요약

 

텍스쳐 불러오기, 이를 폴리곤에 매핑한 다음 셰이더를 사용하여 렌더링하는 기본을 배웠습니다.

 

초기화

  • TextureShaderClass
    1. 셰이더 컴파일 및 개체 GPU에 생성
    2. 색상 대신 uv좌표값으로 Input layout 초기화
    3. 상수 버퍼 생성
    4. Sampler state 생성

  • TextureClass
    1. char* 형으로 Tga이미지 뒤집어 가져오기
    2. Texture2D형 GPU에 생성하여 UpdateSubresource으로 GPU에 복사하여 넣어주기
    3. Shader resoruce view 생성하면서 만든 Texture2D를 장착시켜준다.

업데이트(Render)

  • TextureShaderClass
    1. Set ShaderParameters
    • 상수 버퍼 Map/Unmap으로 업데이트 후 VS에 배치,
    • Shader resoruce view를 TextureClass에서 받아와 PS단계에 배치
    2. RenderShader
    • InputLayout IA단계 배치, 셰이더 각 단계에 배치, 
    • ID3D11SamplerState PS에 배치
  • TextureClass
    1. TextureShaderClass에서 ShaderResourceView를 가져갑니다.

 

내 생각에 중요했던 것

 

1. CreateTexture2D

CreateTexture2D로 텍스쳐를 생성하면 GPU에 생성이 되고 그 포인터를 받아오는 것.

 

근데 이전에 있던 튜토리얼에서도 무언가 GPU에 생성이 되고 그 포인터를 받아오는 게 꽤 많았는데

사실은 GPU의 쌩 주소를 받아오는게 아니라 GPU의 특정 데이터 영역과 매핑된 RAM의 주소를 받아오는 것이라고 한다!

왜냐면 GPU가 사용하는 메모리 주소는 CPU가 직접 접근할 수 없기 때문!

 

그래서 MAP을 호출하게 되면 GPU의 메모리를 RAM에 매핑하여서 그 RAM의 주소를 반환하는 것이였다!

 

2. Map / Unmap & UpdateSubresource

특징 Map/Unmap UpdateSubresource
주요 사용 상황 매 프레임마다 자주 업데이트되는 데이터 초기화 또는 자주 변경되지 않는 데이터
접근 방식 CPU가 직접 GPU 메모리에 접근 CPU 메모리에서 GPU 메모리로 복사
성능 빠르지만 신중하게 사용해야 함 최적화된 방식으로 빠르게 복사
예시 동적 버퍼, 임시 데이터 텍스처 초기화, 정적 버퍼

 

이게 특징은 뭐 그런가보다.. 하고 알겠는데 왜 그런지는 잘 이해가 가지 않는다.

그냥 Map/Unmap이 빠르니까 다 이거 쓰면 되는거 아님? 라는 생각도 있고 한데

UpdateSubresource는 좀 느리지만 무언가 GPU와 가장 가까운 메모리에 최적화로 쑤셔 넣기때문에 쓴다는 느낌..?

나중에 좀 더 알아봐야겠다.


용어 정리

 

텍셀 좌표계 : 픽셀로 된 텍스쳐의 크기를 0.0f 에서 1.0f 사이의 값으로 정규화 하여 사용하는 좌표계

 

텍스처 : 2D 이미지 데이터

 

샘플링 : 특정 위치에서 데이터를 추출하는 과정

 

Sampler State : 텍스처 샘플링(특정 위치에서 색상을 추출) 방법을 정의하는 설정

 

mip level sampling : 텍스쳐의 다양한 해상도들을 미리 계산하여 저장하고 렌더링시 적절한 해상도의 텍스쳐를 선택해 사용하는 방법

 

ShaderResourceView : 셰이더가 접근할 수 있는 리소스 장착 칸.
셰이더가 텍스쳐와 같은 리소스를 읽어올 수 있도록 연결해주는 인터페이스

 

Pitch : 한 줄의 픽셀 데이터가 차지하는 메모리의 바이트 수.

예시로 가로 3, 세로 12 인 8비트 이미지가 있으면, 가로 하나의 픽셀 수는 3이다.

그럼 8비트 이미지니까 1픽셀당 8비트가 있으므로 가로 하나의 메모리는 3 * 8 = 24비트 = 3바이트라 할 수 있다.

 

bpp : "Bits Per Pixel"의 약어로, 한 화소(픽셀)를 표현하는 데 필요한 비트 수를 나타냄

'DirectX 11 > DX11 Tutorial' 카테고리의 다른 글

[07] 모델 렌더링  (0) 2024.05.26
[06] 난반사 조명(Diffuse light)  (0) 2024.05.24
[04] 버퍼, 셰이더, HLSL  (0) 2024.05.21
[03] DirectX 11 초기화 (3)  (0) 2024.05.20
[02] DirectX 11 초기화 (2)  (0) 2024.05.20
 

Tutorial 4: Buffers, Shaders, and HLSL

Tutorial 4: Buffers, Shaders, and HLSL This tutorial will be the introduction to writing vertex and pixel shaders in DirectX 11. It will also be the introduction to using vertex and index buffers in DirectX 11. These are the most fundamental concepts that

www.rastertek.com

DirectX11의 공부 목적으로 위 링크의 튜토리얼을 보고 제 생각을 포함하여 정리하였습니다.


들어가며

 

이 튜토리얼에서는 DirectX 11에서 정점 및 픽셀 셰이더를 작성하는 방법을 소개합니다. 또한 DirectX 11에서 정점 및 인덱스 버퍼를 사용하는 방법을 소개합니다.

이는 3D 그래픽을 렌더링하기 위해 이해하고 활용해야 하는 가장 기본적인 개념입니다.

 

1. 정점 버퍼(Vertex buffer)

구의 3D모델을 예로 들어보겠습니다.

이러한 거의 모든 모델들은 실제로 수백 개의 삼각형으로 구성됩니다.

삼각형(폴리곤)에는 세 개의 점이 있으며 각 점을 정점이라고 부릅니다. 따라서 위 모델을 렌더링하려면 구를 형성하는 모든 정점을 정점 버퍼라고 하는 배열에 넣어야 합니다. 정점 데이터를 가진 이 정점 버퍼를 GPU로 전송하면 GPU에서 모델을 렌더링할 수 있습니다.

 

2. 인덱스 버퍼 (Index buffer)

인덱스 버퍼는 정점 버퍼와 관련이 있습니다. 그 목적은 정점 버퍼에 있는 각 정점이 그려질 순서를 기록하는 것입니다. 그러면 GPU는 인덱스 버퍼를 사용하여 정점 버퍼에서 특정 정점을 빠르게 찾아 그려냅니다.

인덱스 버퍼의 개념은 책에서 인덱스를 사용하는 개념과 유사하며, 원하는 정점을 훨씬 빠른 속도로 찾는데 도움이 됩니다.

DirectX 문서에 따르면 인덱스 버퍼를 사용하면 VRAM의 더 빠른 위치에 정점 데이터를 캐싱할 가능성이 높아질 수 있습니다. 따라서 성능상의 이유로 이러한 기능을 사용해야합니다.

 

3. 셰이더 (Shaders)

셰이더는 GPU에 존재하는, 화면에 출력할 픽셀의 위치와 색상을 계산하는 함수입니다.

 

4. 정점 셰이더 (Vertex shaders)

정점 셰이더란 정점 버퍼에 들어있는 정점을 3D공간으로 변환하기 위해 작성된 작은 함수입니다. 각 정점에 대한 법선 계산과 같이 수행할 수 있는 다른 계산도 있습니다. 정점 셰이더 프로그램은 처리해야 하는 각 정점에 대해 GPU가 호출합니다. 그러므로 정점 셰이더의 호출횟수는 처리해야하는 정점의 개수와 같다. 예시로 정점이 5000개인 모델에 정점 셰이더는 5000번 실행됩니다.

 

5. 레스터라이저 (Rasterizer)

정점 셰이더로 정점을 3D공간으로 불러 3개씩 모으면 삼각형이 만들어집니다. 그럼 이 삼각형을 픽셀화 해주는게 래스터라이저가 하는 일입니다.(삼각형에 픽셀이 몇개 들어가고 어디에 위치하는지 계산합니다.)

 

6. 픽셀 셰이더 (Pixel shaders)

픽셀 셰이더란 래스터라이저로 구해진 픽셀이 화면에 출력할 최종색상을 계산하는 작은 함수입니다. 이 또한 정점 셰이더와 같이 GPU가 호출합니다. 모델에 사진을 입힌다던지, 조명 및 기타 대부분의 효과는 픽셀 셰이더에서 처리됩니다.

 

7. HLSL (High level shader language)

HLSL은 DirectX11에서 정점, 픽셀 셰이더를 코딩하는데 사용하는 언어입니다. 

 

이번 시간에 업데이트 할 프레임워크 구성

ApplicationClass 하위에 Modle, Camera, ColorShader 클래스가 추가했습니다.

Camera클래스는 이전에 이야기한 뷰 행렬을 처리합니다. 이는 게임월드에서의 카메라의 위치를 제어하고 셰이더들이 장면을 그릴 때 어디를 보고 있는지를 계산해야 할 때 카메라의 위치를 셰이더들에게 전달합니다.

Model클래스는 3D모델의 형태와 구조를 제어합니다. 이번 시간에서는 단순성을 위해 3D모델을 삼각형 평면으로 만듭니다.

ColorShader클래스는 HLSL 셰이더를 호출하여 모델을 화면에 렌더링하는 역할을 담당합니다.

 

먼저 셰이더 함수를 살펴보는 것으로 코드를 시작하겠습니다.


Color.vs

 

정점 셰이더이며 아직은 단순한 텍스트 파일입니다. 그렇기에 빌드에 참여하지 않습니다. 이번시간에 만드는 셰이더의 목적은 가능한 한 단순하게 유지하면서 색상이 있는 삼각형을 그리는 것입니다.

 

정점 셰이더는 전역 변수의 정의로 시작합니다. 이 전역 변수는  int, float과 같은 다양한 타입을 사용할 수 있고, 셰이더가 그 전역변수를 이용하여 작업을 하며 이 값들은 C++ 코드에서 접근해 수정할 수 있습니다. 

 

일반적으로 전역 변수가 한 개, 그 이상이더라도 모든 전역 변수는 "cbuffer"라는 버퍼 개체 유형에 넣습니다. 이러한 버퍼를 논리적으로 구성하는 것은 셰이더의 효율적인 실행뿐만 아니라 그래픽 카드가 버퍼를 저장하는 방식에도 영향을 미치기에  중요합니다. 이 예제에서는 3개의 행렬을 각 프레임을 동시에 업데이트할 것이므로 동일한 버퍼에  넣었습니다.

cbuffer MatrixBuffer
{
    matrix worldMatrix;
    matrix viewMatrix;
    matrix projectionMatrix;
};

이제 이 정점 셰이더 함수의 Input, Output 데이터의 구조를 정의합니다.

셰이더도 C++언어와 마찬가지로 구조체 같은 사용자 정의 타입을 만들 수 있습니다. 밑의 코드에서는 두 개의 float4를 사용하여 위치(x, y, z, w)와, 색상(r, g, b, a)으로 정점 데이터 구조를 정의합니다.

 

POSITION, COLOR, SV_POSITION 같은코드는 시멘틱(Semantics)이라 불립니다.

시멘틱은 셰이더 함수에 들어오는 데이터가 어떤 데이터와 매핑해야 하는지에 대한 힌트를 주는 동시에 용도를 직관적으로 알수 있게 해줍니다.

 

정점 셰이더와 픽셀 셰이더의 의미가 다르기 때문에 POSITION은 버텍스 셰이더에 작동하고, SV_POSITION은 픽셀 셰이더에 작동하기 때문에 VertexInputType과 PixelInputType의 데이터 구조가 동일하여도 따로 정의하였습니다.

COLOR는 두 가지 모두에 작동합니다. 동일한 유형을 두 개 이상 원하는 경우 끝에 COLOR0, COLOR1 등과 같은 숫자를 추가해야 합니다.

struct VertexInputType
{
    float4 position : POSITION;
    float4 color : COLOR;
};

struct PixelInputType
{
    float4 position : SV_POSITION;
    float4 color : COLOR;
};

정점 셰이더는 정점 버퍼가 GPU로 전송되어 GPU가 정점 데이터를 처리할 때 GPU에서 호출합니다. 즉, 정점 버퍼에 들어 있는 정점만큼 호출한다는 뜻입니다.

 

정점 셰이더의 수행 로직은 ColorVertexShaader입니다. 정점 셰이더에 들어오는 input 데이터는(매개변수)는 당연히 VertexInputType의 데이터 구조와 같아야 합니다.

들어온 데이터(VertexInputType input)에서 world, view, projection행렬을 곱하여 공간 변환을 실행합니다. 그럼 3D게임 월드에서 우리의 관점(카메라)으로, 우리의 관점이 2D 화면에 정점이 배치됩니다. 들어온 데이터의 w 값을 1.0f로 변경한 이유는 1.0f로 변경하지 않으면 위치에 대한 XYZ 벡터만 읽기 때문입니다.(일반적으로 위치는 3차원좌표지만 공간 변환시에는 4차원 벡터를 사용하기 때문에 1로 설정하는 것, 나중에 더 자세히 기술)

 

그리고 들어온 데이터의 색상 정보를 그대로 아까 계산한 position과 함께 픽셀 셰이더로 결과를 전송합니다.

PixelInputType ColorVertexShader(VertexInputType input)
{
    PixelInputType output;
    

    // Change the position vector to be 4 units for proper matrix calculations.
    input.position.w = 1.0f;

    // Calculate the position of the vertex against the world, view, and projection matrices.
    output.position = mul(input.position, worldMatrix);
    output.position = mul(output.position, viewMatrix);
    output.position = mul(output.position, projectionMatrix);
    
    // Store the input color for the pixel shader to use.
    output.color = input.color;
    
    return output;
}

 

Color.ps

 

픽셀 셰이더는 화면에 렌더링될 폴리곤의 각 픽셀의 최종 색상을 계산합니다. 픽셀 셰이더는 정점 셰이더의 출력에서 데이터가 들어옵니다.

이번 시간의 픽셀 셰이더는 들어온 데이터의 색을 바로 반환할것이기 때문에 매우 간단합니다.

struct PixelInputType
{
    float4 position : SV_POSITION;
    float4 color : COLOR;
};


float4 ColorPixelShader(PixelInputType input) : SV_TARGET
{
    return input.color;
}

 

ColorShaderClass.h

 

이 클래스는 GPU에 있는 3D모델(정점)을 그리기 위해 HLSL 셰이더를 호출하는 데 사용됩니다.

class ColorShaderClass
{
private:

	// 이 구조체가 바로 정점 셰이더의 cbuffer 값을 수정할 데이터 유형
	// 당연히 cbuffer 형식과 정확히 동일해야함
	struct MatrixBufferType
	{
		XMMATRIX world;
		XMMATRIX view;
		XMMATRIX projection;
	};

public:
	ColorShaderClass();
	ColorShaderClass(const ColorShaderClass&);
	~ColorShaderClass();

	// 셰이더의 초기화 및 종료를 처리합니다.
	bool Initialize(ID3D11Device*, HWND);
	void Shutdown();
	// 셰이더가 사용하는 매개변수를 설정하고 셰이더가 사용할 준비된 모델의 정점을 그립니다.
	bool Render(ID3D11DeviceContext*, int, XMMATRIX, XMMATRIX, XMMATRIX);

private:
	bool InitializeShader(ID3D11Device*, HWND, WCHAR*, WCHAR*);
	void ShutdownShader();
	void OutputShaderErrorMessage(ID3D10Blob*, HWND, WCHAR*);

	bool SetShaderParameters(ID3D11DeviceContext*, XMMATRIX, XMMATRIX, XMMATRIX);
	void RenderShader(ID3D11DeviceContext*, int);

private:
	ID3D11VertexShader* m_vertexShader;
	ID3D11PixelShader* m_pixelShader;
	ID3D11InputLayout* m_layout;
	ID3D11Buffer* m_matrixBuffer;
};

 

ColorShaderClass.cpp

 

1. 생성자

ColorShaderClass::ColorShaderClass()
{
	m_vertexShader = nullptr;
	m_pixelShader = nullptr;
	m_layout = nullptr;
	m_matrixBuffer = nullptr;
}

 

2. Initialize

정점, 픽셀 셰이더의 파일 이름을 가져와 InitializeShader에 전달하면서 호출한다.

bool ColorShaderClass::Initialize(ID3D11Device* device, HWND hwnd)
{
	bool result;
	wchar_t vsFilename[128];
	wchar_t psFilename[128];
	int error;

	// Set the filename of the vertex shader.
	error = wcscpy_s(vsFilename, 128, L"../Engine/color.vs");

	// Set the filename of the pixel shader.
	error = wcscpy_s(psFilename, 128, L"../Engine/color.ps");

	// Initialize the vertex and pixel shaders.
	result = InitializeShader(device, hwnd, vsFilename, psFilename);

	return true;
}

3. Shoutdown

ShoutdownShader함수를 호출합니다.

void ColorShaderClass::Shutdown()
{
	// vertex, pixel shaders와 관련된 개체들을 종료합니다.
	ShutdownShader();

	return;
}

 

4. Render

SetShaderParameters로 셰이더의 cbuffer 값을 수정하고, RenderShader를 통해 셰이더를 통해 준비된 버퍼를 렌더링 합니다.

bool ColorShaderClass::Render(ID3D11DeviceContext* deviceContext, int indexCount, XMMATRIX worldMatrix, XMMATRIX viewMatrix,
	XMMATRIX projectionMatrix)
{
	bool result;

	// Set the shader parameters that it will use for rendering.
	result = SetShaderParameters(deviceContext, worldMatrix, viewMatrix, projectionMatrix);

	// Now render the prepared buffers with the shader.
	RenderShader(deviceContext, indexCount);

	return true;
}

 

5. InitializeShader

이번 튜토리얼에서 가장 중요한 기능 중 하나인 함수입니다. 

이 함수는 셰이더 파일을 불러오고 DirectX와 GPU에서 사용할 수 있도록 만드는 기능입니다. 또한 레이아웃 설정과 정점 버퍼 데이터가 GPU의 그래픽 파이프라인에서 어떻게 보이는지 확인할 수 있습니다.

bool ColorShaderClass::InitializeShader(ID3D11Device* device, HWND hwnd, WCHAR* vsFilename, WCHAR* psFilename)
{
	HRESULT result;
	ID3D10Blob* errorMessage = nullptr;
	ID3D10Blob* vertexShaderBuffer = nullptr;
	ID3D10Blob* pixelShaderBuffer = nullptr;
	D3D11_INPUT_ELEMENT_DESC polygonLayout[2];
	unsigned int numElements;
	D3D11_BUFFER_DESC matrixBufferDesc;

셰이더 파일을 컴파일한 코드를 버퍼(ID3D10Blob)에 넣어줍니다. 컴파일이 실패하면 셰이더 파일을 찾을 수 없다는 의미로 팝업창으로 표시 됩니다.

	// 정점 셰이더 코드를 컴파일 합니다.
	result = D3DCompileFromFile(vsFilename, NULL, NULL, "ColorVertexShader", "vs_5_0",D3D10_SHADER_ENABLE_STRICTNESS, 0,
		&vertexShaderBuffer, &errorMessage);
	
	/* -- 오류 메세지 코드 생략 -- */
	
	// 픽셀 셰이더 코드를 컴파일 합니다.
	result = D3DCompileFromFile(psFilename, NULL, NULL, "ColorPixelShader", "ps_5_0", D3D10_SHADER_ENABLE_STRICTNESS, 0,
		&pixelShaderBuffer, &errorMessage);
	
	/* -- 오류 메세지 코드 생략 -- */

성공적으로 코드가 버퍼에 들어가면 각 버퍼를 이용해 각 셰이더의 개체를 생성하여 포인터로 저장합니다.

이 포인터를 정점, 픽셀 셰이더의 인터페이스로 사용합니다.

	// 버퍼를 이용해 정점 버퍼 개체를 생성합니다.
	result = device->CreateVertexShader(vertexShaderBuffer->GetBufferPointer(), vertexShaderBuffer->GetBufferSize(), NULL, &m_vertexShader);

	// 버퍼를 이용해 픽셀 버퍼 개체를 생성합니다.
	result = device->CreatePixelShader(pixelShaderBuffer->GetBufferPointer(), pixelShaderBuffer->GetBufferSize(), NULL, &m_pixelShader);

 

다음 단계는 Input layout을 생성하는데 목적이 있습니다.

Input layout은 우리가 정점 데이터를 만들어서 GPU(셰이더의 Input data)에 전달하면, 전달 된 정점 데이터의 형식과 구성을 알려주는 구조체 입니다.

즉, 정점 데이터가 어떻게 구성되어 어떻게 사용할건지 설명하는 역할입니다.

 

▼의문점. 어차피 정점 데이터가 정점 셰이더에서 이용이 되는데 왜 GPU에 전달하는지?

더보기

아직 구조를 잘 몰라서 생겼던 의문, 셰이더 함수며 셰이더의 데이터며 모두 GPU 메모리에 위치해 있다.

그래서 GPU에게 전달하는게 당연한 것!

 

다시 글로 돌아가서 먼저 셰이더에서 처리할 정점 데이터의 레이아웃(형식과 구성) 설정을 다룹니다.

우리가 만든 셰이더에서 사용하는 정점 데이터는 위치 벡터(float4)와 색상 벡터(float4) 두 가지를 사용하므로 두 가지의 정점 데이터 레이아웃 설정 생성해야 합니다.

 

가장 먼저 작성해야하는 중요한 SemanticName은 정점 셰이더에 들어갈 정점 데이터가 정점 셰이더의 입력 구조체의 멤버에 매핑 될 수 있게 하는 힌트가 됩니다.

 

아까 말했듯이 우리는 두 개의 서로 다른 정점 데이터를 사용하므로 첫 번째 레이아웃에는 POSITION을 사용하고 두 번째 레이아웃에는 COLOR를 사용합니다.

 

다음 중요한 부분은 Format(형식)입니다. 위치 벡터에는 DXGI_FORMAT_R32G32B32_FLOAT를 사용하고 색상에는 DXGI_FORMAT_R32G32B32A32_FLOAT를 사용합니다.

마지막으로 주의해야 할 사항은 버퍼에서 데이터의 간격을 나타내는 AlignedByteOffset입니다.

이 레이아웃의 경우 처음 12바이트는 위치이고 다음 16바이트는 색상이며 AlignedByteOffset은 각 요소가 시작되는 위치를 보여줍니다.AlignedByteOffset에 직접 값을 넣는 대신 D3D11_APPEND_ALIGNED_ELEMENT를 사용하면 간격이 자동으로 파악됩니다.다른 설정은 이 튜토리얼에서는 필요하지 않으므로 지금은 기본값으로 설정했습니다.

	// 셰이더에서 사용하는 데이터의 레이아웃 설명 생성
	polygonLayout[0].SemanticName = "POSITION";
	polygonLayout[0].SemanticIndex = 0;
	polygonLayout[0].Format = DXGI_FORMAT_R32G32B32_FLOAT;
	polygonLayout[0].InputSlot = 0;
	polygonLayout[0].AlignedByteOffset = 0;
	polygonLayout[0].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
	polygonLayout[0].InstanceDataStepRate = 0;

	polygonLayout[1].SemanticName = "COLOR";
	polygonLayout[1].SemanticIndex = 0;
	polygonLayout[1].Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
	polygonLayout[1].InputSlot = 0;
	polygonLayout[1].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT;
	polygonLayout[1].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
	polygonLayout[1].InstanceDataStepRate = 0;

그 후 레이아웃이 몇 개인지 구하여 정점 셰이더의 Input layout을 생성합니다. 또한 Input Layout이 생성되면 더 이상 필요없는 Vertex,Pixel shader buffer를 해제 합니다.

	// Get a count of the elements in the layout.
	numElements = sizeof(polygonLayout) / sizeof(polygonLayout[0]);

	// Create the vertex input layout.
	result = device->CreateInputLayout(polygonLayout, numElements, vertexShaderBuffer->GetBufferPointer(),
		vertexShaderBuffer->GetBufferSize(), &m_layout);
        
	// Release the vertex shader buffer and pixel shader buffer since they are no longer needed.
	vertexShaderBuffer->Release();
	vertexShaderBuffer = 0;

	pixelShaderBuffer->Release();
	pixelShaderBuffer = 0;

 

셰이더를 활용하기 위해 설정해야 하는 마지막은 상수 버퍼(Constant Buffer)를 생성하는 일입니다.

정점 셰이더에서 우리가 전역변수라며 cbuffer의 구조를 정의만 했는데 바로 이 cbuffer가 상수 버퍼의 데이터 유형입니다.

다음 코드에서 CreateBuffer을 이용해 실제로 GPU 메모리에 상수 버퍼를 생성하여 포인터로 가지고 있을 것입니다.

 

상수 버퍼는 셰이더에서 여러 모델이 공통적으로 사용되는 변수를 런타임에서 동적으로 업데이트하기 위해 사용되며, 상수버퍼의 포인터와 SetShaderParmeters를 사용하여 셰이더의 상수 버퍼 값에 접근할 수 있습니다.

	// 정점 셰이더에 있는 상수 버퍼(cbuffer)의 설명을 설정합니다.
	// 상수 버퍼의 사용 방식을 지정합니다. 여기서는 매 프레임마다 업데이트되므로 D3D11_USAGE_DYNAMIC을 사용합니다.
	matrixBufferDesc.Usage = D3D11_USAGE_DYNAMIC; 
	// ByteWidth는 상수 버퍼의 크기를 바이트 단위로 지정합니다. 이 경우에는 MatrixBufferType 구조체의 크기를 사용합니다.
	matrixBufferDesc.ByteWidth = sizeof(MatrixBufferType); 
	// BindFlags는 버퍼가 어떤 유형의 버퍼가 될지 지정합니다. 여기서는 상수 버퍼로 사용하므로 D3D11_BIND_CONSTANT_BUFFER를 사용합니다.
	matrixBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
	// CPUAccessFlags는 Usage와 맟춰 사용해야하기에 여기서 D3D11_CPU_ACCESS_WRITE를 사용합니다.
	matrixBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
	matrixBufferDesc.MiscFlags = 0;
	matrixBufferDesc.StructureByteStride = 0;

	// Create the constant buffer pointer so we can access the vertex shader constant buffer from within this class.
	result = device->CreateBuffer(&matrixBufferDesc, NULL, &m_matrixBuffer);
    
	return true;
}

 

6. ShutdownShader

InitializeShader함수에서 설정된 4개의 인터페이스를 해제 합니다.

void ColorShaderClass::ShutdownShader()
{
	// Release the matrix constant buffer.
	if (m_matrixBuffer)
	{
		m_matrixBuffer->Release();
		m_matrixBuffer = nullptr;
	}

	// Release the layout.
	if (m_layout)
	{
		m_layout->Release();
		m_layout = nullptr;
	}

	// Release the pixel shader.
	if (m_pixelShader)
	{
		m_pixelShader->Release();
		m_pixelShader = nullptr;
	}

	// Release the vertex shader.
	if (m_vertexShader)
	{
		m_vertexShader->Release();
		m_vertexShader = nullptr;
	}

	return;
}

 

7. OuputShaderErrorMessage

셰이더관련 에러인지 알려주는 창을 띄워줍니다.

void ColorShaderClass::OutputShaderErrorMessage(ID3D10Blob* errorMessage, HWND hwnd, WCHAR* shaderFilename)
{
	char* compileErrors;
	unsigned long long bufferSize, i;
	ofstream fout;


	// error message text buffer의 포인터를 가져옵니다.
	compileErrors = (char*)(errorMessage->GetBufferPointer());

	// 메세지의 길이 구하기
	bufferSize = errorMessage->GetBufferSize();

	// 에러 메세지를 저장할 텍스트 파일 열기 (없으면 생성)
	fout.open("shader-error.txt");

	// 파일에 에러 메세지 작성
	for (i = 0; i < bufferSize; i++)
	{
		fout << compileErrors[i];
	}

	// 텍스트 파일 닫기
	fout.close();

	// 에러메세지 메모리 해제
	errorMessage->Release();
	errorMessage = nullptr;

	// compile errors를 확인하라는 메세지를 팝업창으로 띄웁니다.
	MessageBox(hwnd, L"Error compiling shader.  Check shader-error.txt for message.", shaderFilename, MB_OK);

	return;
}

 

8. SetShaderParameters

이 함수는 셰이더의 상수 버퍼를 쉽게 설정할 수 있도록 하기 위해 존재합니다.

이 함수에 사용된 행렬은 ApplicationClass 내부에서 생성된 후 Render함수 호출 중에 정점 셰이더로 보내기 위해 이 함수가 호출 됩니다.

bool ColorShaderClass::SetShaderParameters(ID3D11DeviceContext* deviceContext, XMMATRIX worldMatrix, XMMATRIX viewMatrix,
	XMMATRIX projectionMatrix)
{
	HRESULT result;
	D3D11_MAPPED_SUBRESOURCE mappedResource;
	MatrixBufferType* dataPtr;
	unsigned int bufferNumber;

행렬을 셰이더로 보내기 전에 DirectX11의 요구사항으로 행렬을 전치 해야합니다.

	// 셰이더에 사용하기 위해 행렬을 전치합니다. (대각선 \ 을 기준으로 뒤집음)
	worldMatrix = XMMatrixTranspose(worldMatrix);
	viewMatrix = XMMatrixTranspose(viewMatrix);
	projectionMatrix = XMMatrixTranspose(projectionMatrix);

그리고 상수 버퍼의 값을 수정해야 하는데 잠금(Map), 잠금 해제(UnMap)라는 개념이 나옵니다. 지금 우리가 수정하려는 값을 가지고 있는건 CPU쪽(+ RAM)이고, 상수 버퍼는 GPU(VRAM)에 존재합니다. 그래서 값을 수정하면서 데이터의 동기화, 일관성을 가지게 해야하기에 나온 개념이 잠금, 잠금 해제 입니다.

 

잠금은 CPU가 상수버퍼의 메모리 영역에 접근할 수 있게 하며 GPU가 상수 버퍼 메모리 영역에 접근하지 못하게 하며,

잠금해제는 그 반대가 됩니다.

bool ColorShaderClass::SetShaderParameters(ID3D11DeviceContext* deviceContext, XMMATRIX worldMatrix, XMMATRIX viewMatrix,
	XMMATRIX projectionMatrix)
{
	HRESULT result;
	D3D11_MAPPED_SUBRESOURCE mappedResource;
	MatrixBufferType* dataPtr;
	unsigned int bufferNumber;

	// 셰이더에 사용하기 위해 행렬을 전치합니다. (대각선 \ 을 기준으로 뒤집음)
	worldMatrix = XMMatrixTranspose(worldMatrix);
	viewMatrix = XMMatrixTranspose(viewMatrix);
	projectionMatrix = XMMatrixTranspose(projectionMatrix);

	// 상수 버퍼 포인터를 이용해 상수버퍼를 잠급니다.
	result = deviceContext->Map(m_matrixBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);
	if (FAILED(result))
	{
		return false;
	}

	// 상수 버퍼의 데이터에 대한 포인터를 가져옵니다.
	dataPtr = (MatrixBufferType*)mappedResource.pData;

▼의문점. 마지막에 상수 버퍼의 데이터에 대한 포인터를 가져오는데 이미 상수 버퍼 포인터가 있지만 굳이 또 가져오는 이유가 뭘까?

더보기

이 답이 확실할지는 모르겠지만 내가 찾아 결론 내린건,

 

일반적으로 개체 포인터만 있어도 개체의 멤버에 접근할 수 있지만 cpu와 gpu의 메모리 관리 방식이 다르기에 gpu에 있는 개체 포인터로는 그 데이터에 직접 접근할 수 있는 방법을 제공하지 않기 때문이다.

 

그래서 Map을 이용하여 상수 버퍼를 잠구고 데이터를 변환할거니까 그 데이터를 수정할 수 있는 포인터를 넘겨주는 것이라 생각한다.

Map으로 잠그고 난 후 상수 버퍼의 데이터 값을 수정하고 잠금 해제 후 VS단계에 등록합니다.

	// constant buffer의 데이터에 값을 수정한다.
	dataPtr->world = worldMatrix;
	dataPtr->view = viewMatrix;
	dataPtr->projection = projectionMatrix;

	// constant buffer의 잠금 해제
	deviceContext->Unmap(m_matrixBuffer, 0);

	// Set the position of the constant buffer in the vertex shader.
	bufferNumber = 0;

	// 셰이더의 업데이트된 상수버퍼를 0번 슬롯에 1개 VS단계에 등록한다.
	deviceContext->VSSetConstantBuffers(bufferNumber, 1, &m_matrixBuffer);

	return true;
}

 

9. RenderShader

이 함수는 Render 함수에서 호출되는 두 번째 함수 입니다. 세 가지 기능을 합니다.

 

첫 번째 기능은 IA단계에서 Input layout을 등록하는 것 입니다. 이를 통해 GPU는 정점 버퍼의 데이터 형식을 알 수 있습니다.

두 번째 기능은 정점 버퍼를 렌더링하는 데 사용할 정점 셰이더와 픽셀 셰이더를 렌더링 파이프 라인에 등록하는 것입니다.

세 번째 기능은 D3D device context를 이용하여 DX11함수인 DrawIndexed를 호출해 폴리곤을 렌더링 합니다.

 

우리의 목표는 녹색 삼각형을 렌더링하는 것입니다.

void ColorShaderClass::RenderShader(ID3D11DeviceContext* deviceContext, int indexCount)
{
	// Set the vertex input layout.
	deviceContext->IASetInputLayout(m_layout);

	// Set the vertex and pixel shaders that will be used to render this triangle.
	deviceContext->VSSetShader(m_vertexShader, NULL, 0);
	deviceContext->PSSetShader(m_pixelShader, NULL, 0);

	// Render the triangle.
	deviceContext->DrawIndexed(indexCount, 0, 0);

	return;
}

 

ModelClass.h

 

이 클래스는 3D모델의 기하 구조를 캡슐화하는 역할을 합니다. 이 튜토리얼에서는 단순한 녹색 삼각형에 대한 데이터를 수동으로 설정합니다. 또한 삼각형이 렌더링 될 수 있도록 정점,인덱스 버퍼를 생성합니다.

 

구조체 VertexType은 정점 데이터의 유형입니다. 이 형식의 정의는 아까 생성했던 Input layout과 같아야 합니다.

#pragma once
#include <d3d11.h>
#include <directxmath.h>
using namespace DirectX;

class ModelClass
{
private:
	struct VertexType
	{
		XMFLOAT3 position;
		XMFLOAT4 color;
	};

Render 함수는 모델의 기하 구조를 그래픽 카드에 배치하여 color shader로 그릴 수 있도록 준비합니다.

public:
	ModelClass();
	ModelClass(const ModelClass&);
	~ModelClass();

	bool Initialize(ID3D11Device*);
	void Shutdown();
	void Render(ID3D11DeviceContext*);

	int GetIndexCount();

private:
	bool InitializeBuffers(ID3D11Device*);
	void ShutdownBuffers();
	void RenderBuffers(ID3D11DeviceContext*);

ModelClass의 private 변수는 정점 버퍼와 인덱스 버퍼, 그리고 각 버퍼의 크기를 저장하는 두 개의 정수 입니다.

Dx11의 모든 변수는 일반적으로 ID3D11Buffer유형을 사용합니다.

private:
	ID3D11Buffer* m_vertexBuffer;
	ID3D11Buffer * m_indexBuffer;
	int m_vertexCount;
	int m_indexCount;
};

 

ModelClass.cpp

 

1. 생성자

생성자는 버퍼를 null로 초기화 합니다.

ModelClass::ModelClass()
{
	m_vertexBuffer = nullptr;
	m_indexBuffer = nullptr;
}

 

2. Initialize

초기화 함수는 정점, 인덱스 버퍼에 대한 초기화 함수를 호출합니다.

bool ModelClass::Initialize(ID3D11Device* device)
{
	bool result;

	// Initialize the vertex and index buffers.
	result = InitializeBuffers(device);

	return true;
}

 

3. Shoutdown

Shutdown 함수는 정점 및 인덱스 버퍼에 대한 종료 함수를 호출합니다.

void ModelClass::Shutdown()
{
	// Shutdown the vertex and index buffers.
	ShutdownBuffers();

	return;
}

 

4. Render

이 함수는 ApplicationClass::Render 함수에서 호출됩니다. 이 함수는 RenderBuffers를 호출하여 정점, 인덱스 버퍼를 렌더링 파이프라인에 배치하므로 color shader가 이를 렌더링할 수 있습니다.

void ModelClass::Render(ID3D11DeviceContext* deviceContext)
{
	// vertex and index buffers를 렌더링 파이프 라인에 배치합니다.
	RenderBuffers(deviceContext);

	return;
}

 

5. GetIndexCount

GetIndexCount는 모델의 인덱스 수를 반환합니다. color shader는 이 모델을 그리려면 이 정보가 필요합니다.

int ModelClass::GetIndexCount()
{
	return m_indexCount;
}

 

6. InitialzieBuffers

이 함수는 정점,인덱스 버퍼 생성을 처리하는 곳입니다. 일반적으로 모델을 읽고 해당 데이터 파일에서 버퍼를 생성합니다.

이 튜토리얼에서는 모델이 아닌 단순 삼각형이기에 수동으로 정점을 설정합니다.

bool ModelClass::InitializeBuffers(ID3D11Device* device)
{
	VertexType* vertices;
	unsigned long* indices;
	D3D11_BUFFER_DESC vertexBufferDesc, indexBufferDesc;
	D3D11_SUBRESOURCE_DATA vertexData, indexData;
	HRESULT result;

정점, 인덱스 버퍼의 배열을 사용할큼의 크기로 생성합니다.

	// Set the number of vertices in the vertex array.
	m_vertexCount = 3;

	// Set the number of indices in the index array.
	m_indexCount = 3;

	// Create the vertex array.
	vertices = new VertexType[m_vertexCount];

	// Create the index array.
	indices = new unsigned long[m_indexCount];

이제 정점과 인덱스 배열을 세 점과 각 점의 인덱스로 채웁니다. 시계방향으로 점을 그린다는 점에 유의하시길 바랍니다.

이 시계방향으로 그려진 면은 앞면이고 반대방향으로 그려지면 뒷면으로 인식됩니다. 뒷면일 경우 back face culling으로 인해 그려지지 않습니다. 색상은 녹색으로 설정합니다.

	// Load the vertex array with data.
	vertices[0].position = XMFLOAT3(-1.0f, -1.0f, 0.0f);  // Bottom left.
	vertices[0].color = XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f);

	vertices[1].position = XMFLOAT3(0.0f, 1.0f, 0.0f);  // Top middle.
	vertices[1].color = XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f);

	vertices[2].position = XMFLOAT3(1.0f, -1.0f, 0.0f);  // Bottom right.
	vertices[2].color = XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f);

	// Load the index array with data.
	indices[0] = 0;  // Bottom left.
	indices[1] = 1;  // Top middle.
	indices[2] = 2;  // Bottom right.

정점, 인덱스 배열이 채워지면 이제 이를 사용하여 정점 버퍼와 인덱스 버퍼를 만들 수 있습니다. 두 버퍼의 생성은 동익한 방식으로 수행됩니다.

먼저 버퍼에 대한 설명과 SubResource 포인터를 가지고 CreateBuffer을 호출하여 버퍼를 생성할 수 있습니다 

	// Set up the description of the static vertex buffer.
	vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
	vertexBufferDesc.ByteWidth = sizeof(VertexType) * m_vertexCount;
	vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
	vertexBufferDesc.CPUAccessFlags = 0;
	vertexBufferDesc.MiscFlags = 0;
	vertexBufferDesc.StructureByteStride = 0;

	// Give the subresource structure a pointer to the vertex data.
	vertexData.pSysMem = vertices;
	vertexData.SysMemPitch = 0;
	vertexData.SysMemSlicePitch = 0;

	// Now create the vertex buffer.
	result = device->CreateBuffer(&vertexBufferDesc, &vertexData, &m_vertexBuffer);

	// Set up the description of the static index buffer.
	indexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
	indexBufferDesc.ByteWidth = sizeof(unsigned long) * m_indexCount;
	indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER;
	indexBufferDesc.CPUAccessFlags = 0;
	indexBufferDesc.MiscFlags = 0;
	indexBufferDesc.StructureByteStride = 0;

	// Give the subresource structure a pointer to the index data.
	indexData.pSysMem = indices;
	indexData.SysMemPitch = 0;
	indexData.SysMemSlicePitch = 0;

	// Create the index buffer.
	result = device->CreateBuffer(&indexBufferDesc, &indexData, &m_indexBuffer);

정점, 인덱스 버퍼가 생성된 후에는 데이터가 버퍼에 복사되었기 때문에 필요하지 않은 정점, 인덱스 배열을 삭제할 수 있습니다.

// Release the arrays now that the vertex and index buffers have been created and loaded.
	delete[] vertices;
	vertices = 0;

	delete[] indices;
	indices = 0;

	return true;
}

 

7. ShoutdownBuffers

InitializeBuffers함수에서 생성된 정점, 인덱스 버퍼를 메모리 해제 합니다.

void ModelClass::ShutdownBuffers()
{
	// Release the index buffer.
	if (m_indexBuffer)
	{
		m_indexBuffer->Release();
		m_indexBuffer = nullptr;
	}

	// Release the vertex buffer.
	if (m_vertexBuffer)
	{
		m_vertexBuffer->Release();
		m_vertexBuffer = nullptr;
	}

	return;
}

 

8. RenderBuffers

이 함수는 Render함수에서 호출됩니다. 이 함수의 목적은 정점, 인덱스 버퍼를 렌더링 파이프 라인의 IA단계에 배치하는 것입니다. GPU에 배치된 정점 버퍼가 있으면 셰이더를 사용하여 해당 버퍼로 정점을 렌더링할 수 있습니다.

또한 이 함수는 삼각형뿐만 아니라 점, 선, 등 같이 그리는 방법을 정의 합니다. 이 튜토리얼에서는 정점 버퍼와 인덱스 버퍼를 IA단계에 배피하고 IASetPrimitiveTopology함수를 이용하여 버퍼를 삼각형으로 그려야 함을 GPU에 알립니다. 

void ModelClass::RenderBuffers(ID3D11DeviceContext* deviceContext)
{
	unsigned int stride;
	unsigned int offset;


	// Set vertex buffer stride and offset.
	stride = sizeof(VertexType);
	offset = 0;

	// Set the vertex buffer to active in the input assembler so it can be rendered.
	deviceContext->IASetVertexBuffers(0, 1, &m_vertexBuffer, &stride, &offset);

	// Set the index buffer to active in the input assembler so it can be rendered.
	deviceContext->IASetIndexBuffer(m_indexBuffer, DXGI_FORMAT_R32_UINT, 0);

	// Set the type of primitive that should be rendered from this vertex buffer, in this case triangles.
	deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

	return;
}

 

CameraClass.h

 

우리는 HLSL셰이더를 코딩하는 법, ColorShaderClass에서 셰이더를 호출하여 각 버퍼를 렌더링하는 법을 살펴보았습니다. 그러나 우리가 놓치고 있는 한 가지는 그것들을 그리는 관점입니다. 이를 위해 Dx11이 장면을 어디서 어떻게 보고 있는지 알려주는 카메라 클래스가 필요합니다. 카메라 클래스는 카메라의 위치와 현재 회전을 추적합니다. 위치, 회전 정보를 사용하여 렌더링을 위해 셰이더에 전달될 뷰 행렬을 생성합니다.

 

CameraClass의 헤더는 4개의 함수만 사용하여 매우 간단합니다.

SetPosition, SetRotation 함수는 카메라 개체의 위치와 회전을 설정하는 데 사용됩니다.

Render는 카메라의 위치와 회전을 기반으로 뷰 행렬을 생성하는데 사용됩니다.

GetViewMatrix는 셰이더가 렌더링에 사용할 수 있도록 카메라 개체에서 뷰 행렬을 검색하는데 사용됩니다.

#pragma once
#include <directxmath.h>
using namespace DirectX;

class CameraClass
{
public:
	CameraClass();
	CameraClass(const CameraClass&);
	~CameraClass();

	void SetPosition(float, float, float);
	void SetRotation(float, float, float);

	XMFLOAT3 GetPosition();
	XMFLOAT3 GetRotation();

	void Render();
	void GetViewMatrix(XMMATRIX&);

private:
	float m_positionX, m_positionY, m_positionZ;
	float m_rotationX, m_rotationY, m_rotationZ;
	XMMATRIX m_viewMatrix;
};

 

CameraClass.cpp

 

1. 생성자

카메라의 위치와 회전을 원점으로 초기화 합니다.

CameraClass::CameraClass()
{
	m_positionX = 0.0f;
	m_positionY = 0.0f;
	m_positionZ = 0.0f;

	m_rotationX = 0.0f;
	m_rotationY = 0.0f;
	m_rotationZ = 0.0f;
}

 

2. SetPosition & SetRotation

카메라의 위치, 회전을 설정합니다.

void CameraClass::SetPosition(float x, float y, float z)
{
	m_positionX = x;
	m_positionY = y;
	m_positionZ = z;
	return;
}


void CameraClass::SetRotation(float x, float y, float z)
{
	m_rotationX = x;
	m_rotationY = y;
	m_rotationZ = z;
	return;
}

 

3. GetPosition & GetRotation

카메라의 위치, 회전을 반환합니다.

XMFLOAT3 CameraClass::GetPosition()
{
	return XMFLOAT3(m_positionX, m_positionY, m_positionZ);
}


XMFLOAT3 CameraClass::GetRotation()
{
	return XMFLOAT3(m_rotationX, m_rotationY, m_rotationZ);
}

 

4. Render

이 함수는 카메라의 위치, 회전을 토대로 뷰 행렬을 생성하고 업데이트 합니다.

먼저 카메라의 윗방향을 의미하는 벡터, 위치, 회전에 대한 변수를 설정합니다.

그 후 원점에서 먼저 카메라의 x, y, z 회전을 기준으로 카메라를 회전합니다.

적절히 회전되면 카메라를 3D공간의 위치로 변환합니다.

올바른 윗방향, 위치, 회전을 이용하여 XMMatrixLookAtLH 함수로 현재 카메라 회전 및 이동을 나타내는 뷰 행렬을 생성할 수 있습니다.

void CameraClass::Render()
{
	XMFLOAT3 up, position, lookAt;
	XMVECTOR upVector, positionVector, lookAtVector;
	float yaw, pitch, roll;
	XMMATRIX rotationMatrix;


	// Setup the vector that points upwards.
	up.x = 0.0f;
	up.y = 1.0f;
	up.z = 0.0f;

	// Load it into a XMVECTOR structure.
	upVector = XMLoadFloat3(&up);

	// Setup the position of the camera in the world.
	position.x = m_positionX;
	position.y = m_positionY;
	position.z = m_positionZ;

	// Load it into a XMVECTOR structure.
	positionVector = XMLoadFloat3(&position);

	// Setup where the camera is looking by default.
	lookAt.x = 0.0f;
	lookAt.y = 0.0f;
	lookAt.z = 1.0f;

	// Load it into a XMVECTOR structure.
	lookAtVector = XMLoadFloat3(&lookAt);

	// Set the yaw (Y axis), pitch (X axis), and roll (Z axis) rotations in radians.
	pitch = m_rotationX * 0.0174532925f;
	yaw = m_rotationY * 0.0174532925f;
	roll = m_rotationZ * 0.0174532925f;

	// Create the rotation matrix from the yaw, pitch, and roll values.
	rotationMatrix = XMMatrixRotationRollPitchYaw(pitch, yaw, roll);

	// Transform the lookAt and up vector by the rotation matrix so the view is correctly rotated at the origin.
	lookAtVector = XMVector3TransformCoord(lookAtVector, rotationMatrix);
	upVector = XMVector3TransformCoord(upVector, rotationMatrix);

	// Translate the rotated camera position to the location of the viewer.
	lookAtVector = XMVectorAdd(positionVector, lookAtVector);

	// Finally create the view matrix from the three updated vectors.
	m_viewMatrix = XMMatrixLookAtLH(positionVector, lookAtVector, upVector);

	return;
}

 

5. GetViewMatrix

Render함수로 뷰 행렬을 생성,업데이트 한 후, 이 GetViewMatrix를 사용하여 업데이트 된 뷰 행렬을 제공할 수 있습니다.

뷰 행렬은 셰이더에 사용되는 주 행렬중 하나입니다.

void CameraClass::GetViewMatrix(XMMATRIX& viewMatrix)
{
	viewMatrix = m_viewMatrix;
	return;
}

 

ApplicationClass.h

 

ApplicaitionClass에 이번 시간에 만든 세 개의 새로운 클래스를 include하고 멤버 포인터를 추가합니다.

ApplicaitionClass는 프로젝트에 필요한 모든 클래스 개체를 호출하여 장면을 렌더링하는 데 사용되는 기본 클래스라는 점을 기억하세요.

#include "CameraClass.h"
#include "ModelClass.h"
#include "ColorShaderClass.h"
private:

    CameraClass* m_Camera;
	ModelClass* m_Model;
	ColorShaderClass* m_ColorShader;

 

ApplicationClass.cpp

 

1. 생성자

첫 번째 변경 사항은 클래스 생성자에서 추가한 포인터를 null로 초기화 하는 것입니다.

ApplicationClass::ApplicationClass()
{
	m_Direct3D = nullptr;
	//////////////////////////////////////////////////////////////////////
	// 추가 부분
	m_Camera = nullptr;
	m_Model = nullptr;
	m_ColorShader = nullptr;
	//////////////////////////////////////////////////////////////////////
}

 

2. Initialize

세 개의 새로운 개체를 생성하고 초기화하는 코드를 추가합니다.

bool ApplicationClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{
	bool result;

	m_Direct3D = new D3DClass;

	result = m_Direct3D->Initialize( screenWidth, screenHeight, VSYNC_ENABLED, hwnd, FULL_SCREEN, SCREEN_DEPTH, SCREEN_NEAR);
	
	//////////////////////////////////////////////////////////////////////
	// 추가 부분
	// Create the camera object.
	m_Camera = new CameraClass;

	// Set the initial position of the camera.
	m_Camera->SetPosition(0.0f, 0.0f, -5.0f);

	// Create and initialize the model object.
	m_Model = new ModelClass;

	result = m_Model->Initialize(m_Direct3D->GetDevice());

	// Create and initialize the color shader object.
	m_ColorShader = new ColorShaderClass;

	result = m_ColorShader->Initialize(m_Direct3D->GetDevice(), hwnd);
	//////////////////////////////////////////////////////////////////////

	return true;
}

 

3. Shutdown

세 개의 개체를 추가로 메모리 해제 합니다.

void ApplicationClass::Shutdown()
{
	//////////////////////////////////////////////////////////////////////
	// 추가 부분
	// Release the color shader object.
	if (m_ColorShader)
	{
		m_ColorShader->Shutdown();
		delete m_ColorShader;
		m_ColorShader = nullptr;
	}

	// Release the model object.
	if (m_Model)
	{
		m_Model->Shutdown();
		delete m_Model;
		m_Model = nullptr;
	}

	// Release the camera object.
	if (m_Camera)
	{
		delete m_Camera;
		m_Camera = nullptr;
	}
	//////////////////////////////////////////////////////////////////////


	// Direct3D 개체를 해제합니다. 
	if (m_Direct3D)
	{
		m_Direct3D->Shutdown();
		delete m_Direct3D;
		m_Direct3D = nullptr;
	}

	return;
}

 

4. Render

여전히 장면을 초기화 하는 것으로 시작 됩니다. 그런 다음 CameraClass 개체를 이용해 뷰 행렬을 생성하여 얻어내고

D3DClass개체에서 월드, 투영 행렬을 얻습니다.

그런 다음 ModelClass::Render함수를 이용해 녹색 삼각형 모델 형상을 렌더링 파이프 라인에 배치합니다.

이제 정점이 준비되었으므로 ColorShader를 이용해 정점을 그립니다. 그럼 녹색 삼각형이 백 버퍼에 그려집니다.

이것으로 한 장면이 완성되고 EndScene을 호출하여 백 버퍼를 화면에 렌더링합니다.

bool ApplicationClass::Render()
{
	//////////////////////////////////////////////////////////////////////
	//추가 부분
	XMMATRIX worldMatrix, viewMatrix, projectionMatrix;
	bool result;

	// 화면을 검은색으로 초기화
	m_Direct3D->BeginScene(0.0f, 0.0f, 0.0f, 1.0f);

	// 카메라 위치를 기반으로 뷰 행렬 생성
	m_Camera->Render();

	// camera 와 d3d objects에서 world, view, projection 행렬을 가져옵니다.
	m_Direct3D->GetWorldMatrix(worldMatrix);
	m_Camera->GetViewMatrix(viewMatrix);
	m_Direct3D->GetProjectionMatrix(projectionMatrix);

	// 렌더링 파이프라인에 모델의 정점, 인덱스 버퍼를 배치하여 그릴 준비를 합니다.
	m_Model->Render(m_Direct3D->GetDeviceContext());

	// ColorShaderClass를 사용하여 모델을 렌더링 합니다.
	result = m_ColorShader->Render(m_Direct3D->GetDeviceContext(), m_Model->GetIndexCount(), worldMatrix, viewMatrix, projectionMatrix);
	if (!result)
	{
		return false;
	}
	//////////////////////////////////////////////////////////////////////

	// 렌더링된 장면을 화면에 표시한다.
	m_Direct3D->EndScene();

	return true;
}

 


행렬에 대한 첨언

 

행렬이 이 튜토리얼에서 어떻게 작동하는 설명합니다.

정점 셰이더를 살펴보면 해당 셰이더에 들어오는 정점을 월드, 뷰, 투영 행렬로 곱하는 것을 볼 수 있습니다.

정점에 월드 행렬을 곱하면 해당 정점을 3D 세계에 배치하게 됩니다. 월드 행렬에는 이동, 회전, 크기 등과 같은 항목이 적용될 수 있습니다. 따라서 이를 통해 3D공간에서 해당 정점을 올바르게 배치할 수 있습니다.

다음으로 3D세계에 배치된 정점에 뷰 행렬을 곱하면 정점의 위치를 카메라가 보고 있는 위치를 기준으로 다시 업데이트 합니다.

마지막으로 카메라 위치를 기준으로 업데이트된 정점을 투영행렬에 곱하면 2D화면을 기준으로 위치가 업데이트 됩니다.

 

요약

 

이제 정점 버퍼와 인덱스 버퍼가 어떻게 작동하는지 기본적인 내용을 배웠을 것입니다. 또한 HLSL을 사용하여 정점 셰이더와 픽셀 셰이더를 작성하는 기본적인 방법을 배웠을 것입니다. 마지막으로, 이러한 새로운 개념을 우리의 프레임워크에 통합하여 화면에 초록색 삼각형을 렌더링하는 방법을 이해했을 것입니다.

그리고 저는 단일 삼각형을 그리기 위해 코드가 꽤 길다는 것을 인지하고 있습니다. 이 모든 것을 하나의 main() 함수 안에 넣을 수도 있었을 것입니다. 그러나 앞으로의 튜토리얼에서는 코드의 변경을 최소화하면서 더 복잡한 그래픽을 처리할 수 있도록 하기 위해 이렇게 올바른 프레임워크를 사용하여 작성했습니다.


초기화

  • Camera : 카메라 초기 위치 설정
  • Model : 정점, 인덱스 버퍼 생성 후, 각 버퍼에 데이터 하드코딩
  • ColorShader : GPU에 셰이더 생성,  셰이더가 사용할 (shader) Input Layout 생성, 상수 버퍼 생성

업데이트(Render)

  • Camera : 뷰 행렬 생성
  • Model : 정점, 인덱스 버퍼를 IA단계에 배치
  • ColorShader : 상수 버퍼 값(3개의 행렬) 업데이트 후 VS단계에 배치, 셰이더 각 단계에 배치, DrawIndexed 호출(그리기 명령)

 

연습

 

1. 삼각형의 색상을 빨간색으로 변경합니다.

정점 데이터의 색상코드를 변경

 

2. 삼각형을 정사각형으로 바꿔보세요.

정점, 인덱스 버퍼에 들어가는 데이터 수정

 

3. 카메라를 10만큼 뒤로 이동해보세요.

카메라 초기 위치설정의 깊이(z) 값 조정

 

4. 색상의 밝기를 절반으로 줄이도록 픽셀 셰이더를 변경해보세요.

픽셀셰이더의 최종 값에 0.5를 곱한다.(나는 더 명확한 차이를 위해 0.2를 곱함)

 


용어 정리

 

시멘틱 : 셰이더 함수에 들어온 Input data 구조의 멤버 데이터와 매핑해주는 일종의 태그


셰이더 : 화면에 출력할 픽셀의 위치와 색상을 계산하는 함수

Input layout : 셰이더 함수의 Input data(매개변수)에 들어가는 구조이다.
내 생각으로는 (Shader) Input layout이라고도 생각한다.

상수 버퍼 : 셰이더함수에서 여러 모델이 공통적으로 사용하는 데이터


+ CreateBuffer로 버퍼를 생성하면 GPU에 생성되고 그 인터페이스를 CPU쪽에서 포인터로 관리한다고 하는데 나중에 확인 해 볼 것.

+ 그래서 IASet(Vertex,Index)Buffer는 GPU에 생성된 버퍼를 렌더링 파이프에 배치하는것이라고 한다는데 확인 해 볼 것.

'DirectX 11 > DX11 Tutorial' 카테고리의 다른 글

[06] 난반사 조명(Diffuse light)  (0) 2024.05.24
[05] 텍스쳐링  (0) 2024.05.23
[03] DirectX 11 초기화 (3)  (0) 2024.05.20
[02] DirectX 11 초기화 (2)  (0) 2024.05.20
[01] DirectX 11 초기화 (1)  (0) 2024.05.19

 

 

 

Tutorial 3: Initializing DirectX 11

Tutorial 3: Initializing DirectX 11 This tutorial will be the first introduction to working with DirectX 11. We will address how to initialize and shut down Direct3D as well as how to render to a window. Updated Framework We are going to add another class

www.rastertek.com

DirectX11의 공부 목적으로 위 링크의 튜토리얼을 보고 제 생각을 포함하여 정리하였습니다.


들어가며

 

이번엔 정말 초기화 끝내야지


D3DClass.cpp

 

2. Initalize

Rasterizer state라는 단어가 보이는데 Rasterizer는 렌더링 파이프 라인의 단계중 하나인데, 하는 역할은 3D장면을 2D 픽셀 장면으로 변환하는 단계다.

이전에 생성한 Depth, Stencil state가 Depth, Stencil Buffer의 동작 유형을 정의 했으므로, Rasterizer state는 Rasterizer의 동작 유형을 정의한다.

// 폴리곤이 어떻게 그려질지의 속성을 Desc에 채워넣기
rasterDesc.AntialiasedLineEnable = false;
rasterDesc.CullMode = D3D11_CULL_BACK;
rasterDesc.DepthBias = 0;
rasterDesc.DepthBiasClamp = 0.0f;
rasterDesc.DepthClipEnable = true;
rasterDesc.FillMode = D3D11_FILL_SOLID;
rasterDesc.FrontCounterClockwise = false;
rasterDesc.MultisampleEnable = false;
rasterDesc.ScissorEnable = false;
rasterDesc.SlopeScaledDepthBias = 0.0f;

// Desc를 이용하여 rasterizer state를 생성
m_device->CreateRasterizerState(&rasterDesc, &m_rasterState);

// 생성한 rasterizer state 파이프 라인에 지정
m_deviceContext->RSSetState(m_rasterState);

이쯤 되면 이런 그래픽 리소스(Rasterizer state, Depth buffer, Swap chain, ...)등을 만들 때 대부분 Desc 구조체에 속성값을 채워 넣고 이를 이용해 생성하는 것을 알 수 있다.

 

그 후 Viewport가 작성된다.

Viewport 의 역할을 설명하면 Render target view에 그려지는 그림이 모니터에 다 출력되는 건 아니다, 그래서 출력되는 영역을 이 Viewport가 알려준다.

이는 렌더링을 좀 더 효율적으로 만들어 주는데 그 예시로 만약 사진처럼 프로그램의 반이 화면에서 벗어나면 벗어난 화면에 대해서는 굳이 렌더링하며 시간과 노력을 할 필요 없다.

또한 뷰포트를 여러개 배치할 수도 있는데 유명한 "It takes two"처럼 왼쪽, 오른쪽 뷰포트를 설정해 같은 공간에서 다른 시점으로 제공할 수도 있다.

https://www.ea.com/ko-kr/games/it-takes-two

// rasterizer state 파이프 라인 RS단계에 지정
m_deviceContext->RSSetState(m_rasterState);

// Viewport는 다른 그래픽 리소스와 달리 Desc없이 직접 설정한다.
// 뷰포트 속성 설정
m_viewport.Width = (float)screenWidth;
m_viewport.Height = (float)screenHeight;
m_viewport.MinDepth = 0.0f;
m_viewport.MaxDepth = 1.0f;
m_viewport.TopLeftX = 0.0f;
m_viewport.TopLeftY = 0.0f;

// viewport를 생성하고 파이프 라인 RS단계에 지정.
m_deviceContext->RSSetViewports(1, &m_viewport);

 

다음은 3D 공간변환을 위한 행렬 설정이 작성된다.  사용 되는 공간은 네 가지로 볼 수 있다.

1. 물체 공간

2. 월드 공간

3. 뷰 공간

4. 투영 공간

 

3D게임이라고 해도 결국 결국 최종으로 보는 것은 2D Textrue다. 한 프레임 한 프레임마다 그냥 2D Texture가 바뀌는 것 뿐이다.

우리가 만드는 3D공간을 2D Texture위로 투영 시키게 되면 그 Texture가 가진 공간이 투영공간이다.

투영 : 도형이나 입체를 다른 평면에 옮기는 일.

 

우리가 여태 설정한 코드에 따르면 결국 뷰포트 영역만이 렌더링 되어 모니터로 나타난다.

즉, 게임 월드에서 원하는 시점, 원하는 각도로 렌더링할 공간을 일단 잘라내서 우리의 모니터(투영공간)으로 그리는데, 이 일단 잘라낸 공간을 뷰 공간이라 한다. 카메라와 같다고 생각하면 된다.

뷰 공간 이전은 "뷰포트의 영역 + 뷰포트에 포함되지 않은 영역" 인데 이 공간을 월드 공간이라 한다.

위의 사진처럼 게임 월드에 있는 모두 그려지는것들은 월드 공간을 사용한다.

 

갑자기 Player4에게 검이 주어져 검을 손에 장착 시키고 싶어졌다.

우리가 월드 공간만 조작할 수 있다고 하면 계속 월드 공간을 기준으로 힘들게 계산을 할 수 밖에 없다.

근데 Player4가 기준이 되는 공간이 있다면 Player4가 어느곳을 이동하더라도 Player4의 손 좌표는 같기 때문에 캐릭터 고유 공간을 기준으로 계산하면 쉽게 검을 쥐어줄 수 있다.

이런 Player4처럼 게임 월드 속에서 각각 그려지는 것 마다의 고유 공간을 로컬 공간이라 한다.

이렇게 각 공간이 있는데 최종적으로 우리가 보기까지

"로컬 공간 → 월드 공간 → 뷰 공간 → 투영 공간" 으로 공간 변환이 이루어 진다. (계속 좌표의 기준이 바뀜)

각 공간마다 행렬을 만들어 행렬을 곱해주면 공간 변환이 이루어진다.

// 월드 행렬을 단위 행렬로 초기화 합니다. (월드 변환시 사용)
m_worldMatrix = XMMatrixIdentity();

// 2D 렌더링을 위해 직교 투영 행렬을 생성한다.
m_orthoMatrix = XMMatrixOrthographicLH((float)screenWidth, (float)screenHeight, screenNear, screenDepth);

// 투영 행렬 설정
fieldOfView = 3.141592654f / 4.0f;
screenAspect = (float)screenWidth / (float)screenHeight;

// 3D렌더링을 위한 투영 행령 생성 (투영 변환시 사용)
m_projectionMatrix = XMMatrixPerspectiveFovLH(fieldOfView, screenAspect, screenNear, screenDepth);

튜토리얼에서는 뷰 변환을 위한 행렬 생성이 빠졌는데 나중에 카메라 클래스를 만들 때 생성한다.

직교 투영은 우리가 눈으로 보는 것처럼 깊이가 있지 않고 깊이가 없다고 생각하면 된다.

3. Shutdown

모든 포인터 함수의 초기화를 시도 한다 (코드 생략)

4. BeginScene & EndScene

D3DClass에는 몇 가지 도움 함수가 있다. 처음 설명할 건 BeginScene과 EndScene이다.

BeginScene은 버퍼를 초기화하여 그릴 준비가 되게끔 하고, EndScene은 모든 그림이 그려지면 스왑체인에 버퍼를 표시하도록 한다.

void D3DClass::BeginScene(float red, float green, float blue, float alpha)
{
	float color[4];

	// 매개변수로 들어온 색상으로 버퍼를 초기화 한다.
	color[0] = red;
	color[1] = green;
	color[2] = blue;
	color[3] = alpha;

	// Clear the back buffer.
	m_deviceContext->ClearRenderTargetView(m_renderTargetView, color);

	// Clear the depth buffer.
	m_deviceContext->ClearDepthStencilView(m_depthStencilView, D3D11_CLEAR_DEPTH, 1.0f, 0);

	return;
}


void D3DClass::EndScene()
{
	// Present the back buffer to the screen since rendering is complete.
	if (m_vsync_enabled)
	{
		// Lock to screen refresh rate.
		m_swapChain->Present(1, 0);
	}
	else
	{
		// Present as fast as possible.
		m_swapChain->Present(0, 0);
	}

	return;
}

5. GetDevice & GetDeviceContext

단순히 Direct device와 Direct device context에 대한 포인터를 가져오는 함수다. (코드 생략)

 

6. GetProjectionMatrix & GetWorldMatrix & GetOrthoMatrix

Get함수지만 매개변수가 들어간 레퍼런스로 각 공간의 행렬의 복사본을 제공해준다.

나중에 셰이더란걸 작업할때 이 행렬들이 필요하므로 미리 작성해준다. (코드 생략)

 

7. GetVideoCardInfo

그래픽 카드 이름과 VRAM양을 반환한다, 나중에 디버깅시 도움이 될 수도 있다. (코드 생략)

 

8. SetBackBufferRenderTarget & ResetViewport

우리가 했던 설정들을 함수화 했는데 나중의 튜토리얼에 다시 사용하기에 미리 함수화 했다.

void D3DClass::SetBackBufferRenderTarget()
{
	// Bind the render target view and depth stencil buffer to the output render pipeline.
	m_deviceContext->OMSetRenderTargets(1, &m_renderTargetView, m_depthStencilView);

	return;
}


void D3DClass::ResetViewport()
{
	// Set the viewport.
	m_deviceContext->RSSetViewports(1, &m_viewport);

	return;
}

요약

 

마침내 DirectX를 화면의 색을 지정하여 초기화하여 생성하고 종료할 수 있게 되었다.


연습

 

1. 전체화면 만들어보기

ApplicationClass.h의 전역 변수인 FULL_SCREEN을 true로 바꾸어준다.

2. 노란 화면으로 창 초기화하기

 

3. 그래픽 카드의 이름을 텍스트 파일에 저장

나는 Application.cpp의 Initilalize함수에서 D3DClass가 초기화 된 후에 저장했다.

// 1. 쓰기 전용으로 파일 열기 (파일이 없으면 생성해 연다.)
std::ofstream myTXTfile;
myTXTfile.open("Graphic_card_name.txt");

// 파일이 정상적으로 열렸는지 확인
if (myTXTfile.is_open() == true)
{
	// 2. 그래픽 카드 정보 가져와 파일에 쓰기
	char GCname[128];
	int VRAMsize = 0;
	m_Direct3D->GetVideoCardInfo(GCname, VRAMsize);
	myTXTfile << GCname;

	// 3. ofstream이 자동으로 소멸돼 알아서 닫아주지만 굳이 파일 닫기를 해줬다.
	myTXTfile.close();
}

 

저장된 파일의 위치는 해당 작업 폴더에 저장된다.


개념 정리

 

렌더 타겟 뷰 : DX가 그림을 그리는 버퍼 장착 칸, 보통 스왑체인의 백 버퍼 포인터와 연결된다.

뷰포트 : 렌더 타겟 뷰에서 그려질 영역을 정의하는 구조체

투영 공간 : 모니터 공간

뷰 공간 : 카메라 공간

월드 공간 : 게임속 세상 공간

로컬 공간 : 월드의 그려지는 각 오브젝트들의 고유 공간

'DirectX 11 > DX11 Tutorial' 카테고리의 다른 글

[05] 텍스쳐링  (0) 2024.05.23
[04] 버퍼, 셰이더, HLSL  (0) 2024.05.21
[02] DirectX 11 초기화 (2)  (0) 2024.05.20
[01] DirectX 11 초기화 (1)  (0) 2024.05.19
[00] 프레임 워크와 창 생성  (0) 2024.05.17

 

 

Tutorial 3: Initializing DirectX 11

Tutorial 3: Initializing DirectX 11 This tutorial will be the first introduction to working with DirectX 11. We will address how to initialize and shut down Direct3D as well as how to render to a window. Updated Framework We are going to add another class

www.rastertek.com

DirectX11의 공부 목적으로 위 링크의 튜토리얼을 보고 제 생각을 포함하여 정리하였습니다.


들어가며

 

이전 포스팅에 이어 D3DClass의 Initialize의 중간인 Swap chain부터 시작한다.

 

 


D3DClass.cpp - Initialzie( )

 

스왑체인부터 시작하는데 이 영상을 보고오면 더 이해하기 쉽다.

https://www.youtube.com/watch?v=7J0tA3Bg6vc

// 스왑체인의 세팅값(DXGI_SWAP_CHAIN_DESC)을 설정하기전 0으로 초기화 한다.
ZeroMemory(&swapChainDesc, sizeof(swapChainDesc));

// 버퍼를 보통 2개, 혹은 3개로 설정하지만 일단 튜토리얼에서는 버퍼 개수를 1개, 싱글 버퍼링으로 동작시킨다.
swapChainDesc.BufferCount = 1;

// 버퍼(도화지)의 크기 설정.
swapChainDesc.BufferDesc.Width = screenWidth;
swapChainDesc.BufferDesc.Height = screenHeight;

// 버퍼(도화지)의 픽셀 형식을 설정.
swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;

// 버퍼의 주사율을 설정, 수직 동기화 여부에 따라 분기가 갈린다.
if (m_vsync_enabled)
{
	swapChainDesc.BufferDesc.RefreshRate.Numerator = numerator;
	swapChainDesc.BufferDesc.RefreshRate.Denominator = denominator;
}
// 수직 동기화가 아니라면 주사율의 제한이 없어진다.
else
{
	swapChainDesc.BufferDesc.RefreshRate.Numerator = 0;
	swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;
}

// 버퍼의 용도를 설정하는건데 일단 튜토리얼에서는 렌더링할 목적으로 설정했다.
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;

// 버퍼의 핸들을 설정.
swapChainDesc.OutputWindow = hwnd;

// 멀티 샘플링(안티 에일리어싱) 을 끈다.
swapChainDesc.SampleDesc.Count = 1;
swapChainDesc.SampleDesc.Quality = 0;

// 전체 화면의 설정 분기.
if (fullscreen)
{
	swapChainDesc.Windowed = false;
}
else
{
	swapChainDesc.Windowed = true;
}

// 버퍼를 위에서부터 차례대로 그릴건지, 짝수 줄부터 그릴건지, ..등에 대한 세팅, 튜토리얼에서는 기본값 사용.
swapChainDesc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
// 스케일링에 대한 설명, 튜토리얼에서는 기본값 사용.
swapChainDesc.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;

// 버퍼의 교체 방식의 세팅 값인데 지금은 단일 버퍼라 의미 없음.
swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;

// 플래그를 설정하지 않는다 = 일단 중요하지 않다.
swapChainDesc.Flags = 0;

// 사용할 DirectX의 버전을 정의
featureLevel = D3D_FEATURE_LEVEL_11_0;

// 여태 세팅한 DXGI_SWAP_CHAIN_DESC를 이용해 스왑체인, Direct3D device, Direct3D device context를 생성한다.
result = D3D11CreateDeviceAndSwapChain(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, 0, &featureLevel, 1,
		D3D11_SDK_VERSION, &swapChainDesc, &m_swapChain, &m_device, NULL, &m_deviceContext);

여기까지 스왑체인 생성과 동시에 버퍼가 생성되었다. (버퍼는 따로 DX를 통해 운영체제단까지 가서 생성되어 포인터로 연결되는듯 하다.)

 

DX가 그림을 그리는 버퍼 칸이 있는데 이 칸을 Render target view 라고 한다. 우리가 만든 버퍼를 이 칸에 장착시키자.

Render target이라하여 자칫 모니터와 바로 연결된다 착각할 수 도 있지만(바로 나..) 정말 단지 그려지는 타겟(백 버퍼)일 뿐이고 모니터에 뿌려지는 그림은 프론트 버퍼가 담당한다.

이 Render target view를 생성하고 스왑체인의 버퍼와 연결한다.

// 스왑체인의 버퍼 포인터를 가져온다.
result = m_swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&backBufferPtr);

// 렌더 타겟 뷰(실제 버퍼)를 버퍼를 연결하면서 생성한다.
result = m_device->CreateRenderTargetView(backBufferPtr, NULL, &m_renderTargetView);

// 이제 이용가치가 사라진 버퍼의 포인터를 메모리 해제한다.
backBufferPtr->Release();
backBufferPtr = nullptr;

 

다음은 DepthBuffer(깊이 버퍼), StencilBuffer(스텐실 버퍼) 가 작성된다.

DepthBuffer 는 3d 공간에서 각 픽셀의 깊이 값을 저장하는 버퍼이다.

게임에서 여러 건물이 겹쳐면 화면에서는 가려진 건물은 보이지 않고 가장 앞으로 나와있는 건물만 픽셀로 표현되는데, 이렇게 어떤게 가려지고 어떤걸 렌더링 해야 하는지의 방법에 주로 사용 된다.

https://learn.microsoft.com/en-us/windows/win32/direct3d9/depth-buffers

StencilBuffer 는 특정 조건을 충족하는 픽셀만 렌더링되도록 할때 사용되는 버퍼다.

같은 스텐실... 혹시 불어불어 불어펜을 아시나요..?

그럼 Depth, StencilBuffer(이하 D,S buffer) 를 만들고 DX에 등록해보자.

 

1. 버퍼는 결국 2D Texture이기에 사용할 2D Texture 생성.

// Initialize the description of the depth buffer.
ZeroMemory(&depthBufferDesc, sizeof(depthBufferDesc));

// 버퍼는 결국 2D 텍스쳐일 뿐이기에 버퍼로 사용할 2D 텍스쳐부터 만들것이다.
// Set up the description of the depth buffer.
depthBufferDesc.Width = screenWidth;
depthBufferDesc.Height = screenHeight;
depthBufferDesc.MipLevels = 1;
depthBufferDesc.ArraySize = 1;
// 32bit중 24bit는 Depth buffer로, 8bit는 Stencil buffer로 사용.
depthBufferDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
depthBufferDesc.SampleDesc.Count = 1;
depthBufferDesc.SampleDesc.Quality = 0;
depthBufferDesc.Usage = D3D11_USAGE_DEFAULT;
depthBufferDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
depthBufferDesc.CPUAccessFlags = 0;
depthBufferDesc.MiscFlags = 0;

// 하나의 2D Texture를 깊이, 스텐실 버퍼로 사용한다.
m_device->CreateTexture2D(&depthBufferDesc, NULL, &m_depthStencilBuffer);

위 코드에서 마지막 부분에 // 하나의 2D Texture를 D,S buffer로 사용한다 고 하는데,

중간에 depthBufferDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT의 뜻이 32bit중 24bit는 Depth buffer로, 8bit는 Stencil buffer로 사용한다는 뜻이기에 하나의 Texture에 두 개의 버퍼를 사용가능하게 해준다.

 

2. D,S buffer 의 동작 방법을 정의하는 D3D11_DEPTH_STENCIL_DESC 생성 후 등록

// Initialize the description of the stencil state.
ZeroMemory(&depthStencilDesc, sizeof(depthStencilDesc));

// Set up the description of the stencil state.
depthStencilDesc.DepthEnable = true;
depthStencilDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
depthStencilDesc.DepthFunc = D3D11_COMPARISON_LESS;

depthStencilDesc.StencilEnable = true;
depthStencilDesc.StencilReadMask = 0xFF;
depthStencilDesc.StencilWriteMask = 0xFF;

// 픽셀이 전면을 향하고 있는 경우 스텐실 작업. 
depthStencilDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
depthStencilDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_INCR;
depthStencilDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
depthStencilDesc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;

// 픽셀이 후면을 향하고 있는 경우 스텐실 작업. 
depthStencilDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
depthStencilDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_DECR;
depthStencilDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
depthStencilDesc.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS;

// depth stencil state 생성
m_device->CreateDepthStencilState(&depthStencilDesc, &m_depthStencilState);

// depth stencil state 등록
m_deviceContext->OMSetDepthStencilState(m_depthStencilState, 1);

 

3. 런타임중 D,S buffer 에 접근할 수 있는 ID3D11DepthStencilView 생성 후 등록

// depth stencil view 초기화
ZeroMemory(&depthStencilViewDesc, sizeof(depthStencilViewDesc));

// depth stencil view Desc 설정
depthStencilViewDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
depthStencilViewDesc.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D;
depthStencilViewDesc.Texture2D.MipSlice = 0;

// depth stencil view 생성
result = m_device->CreateDepthStencilView(m_depthStencilBuffer, &depthStencilViewDesc, &m_depthStencilView);

// 렌더 타겟 뷰와 깊이 스텐실 버퍼를 출력 렌더 파이프라인에 바인딩합니다. 
m_deviceContext->OMSetRenderTargets(1, &m_renderTargetView, m_depthStencilView);

위 코드의 마지막 OMSetRenderTargets은 렌더 파이프 라인의 OM단계에 배치한다는 말인데 나중에 더 자세히 알게 된다.

여기까지 Render target view와 D, S buffer를 생성 후 렌더링 파이프 라인에 배치 하였다.

 


중간 마감

 

Initialize 내용이 너무너무너무너무너무너무 길어서;;; 진짜 다음 포스팅에서 마무리한다;;;;;;;

 


개념 정리

 

스왑체인 : 두 개 이상의 버퍼를 체인처럼 연결해 프레임마다 버퍼를 교체하며 사용하는 기술.

 

Direct3D device(ID3D11Device) : 그래픽 리소스의 생성, 관리를 맡는 DXGI의 주 인터페이스.

특히 생성했다 하면 대부분 이 인터페이스를 사용한다.

 

Direct3D device context(ID3D11DeviceContext) : 그래픽 리소스를 등록하고 렌더링 명령을 하는 DXGI의 주 인터페이스.

위의 D3Ddevice로 만든 리소스들을 파이프 라인에 등록한다거나 셰이더를 등록한다거나 이런 등록이 대부분 이 인터페이스를 사용한다.

 

Render target view : 프레임마다 렌더링 결과물이 그려질 버퍼를 가리키는 포인터를 가진 인터페이스

 

Depth buffer : 각 픽셀의 깊이 값을 저장하는 버퍼

 

Stencil buffer : 각 픽셀의 스텐실 값을 저장하는 버퍼

'DirectX 11 > DX11 Tutorial' 카테고리의 다른 글

[05] 텍스쳐링  (0) 2024.05.23
[04] 버퍼, 셰이더, HLSL  (0) 2024.05.21
[03] DirectX 11 초기화 (3)  (0) 2024.05.20
[01] DirectX 11 초기화 (1)  (0) 2024.05.19
[00] 프레임 워크와 창 생성  (0) 2024.05.17

 

 

Tutorial 3: Initializing DirectX 11

Tutorial 3: Initializing DirectX 11 This tutorial will be the first introduction to working with DirectX 11. We will address how to initialize and shut down Direct3D as well as how to render to a window. Updated Framework We are going to add another class

www.rastertek.com

DirectX11의 공부 목적으로 위 링크의 튜토리얼을 보고 제 생각을 포함하여 정리하였습니다.


들어가며

 

이전에 만들었던 그래픽을 처리하는 클래스인 ApplicationClass에 또 다른 클래스 'D3DClass'를 추가한다.

 


ApplicationClass.h

 

업데이트 된건 windows.h가 아닌 새로운 클래스인 D3DClass를 include하고, D3DClass의 포인터 추가다.

private:

	//  D3DClass의 포인터 추가, 이 튜토리얼에서 모든 멤버 변수는 m_접두사를 사용.
	D3DClass* m_Direct3D;

 

ApplicationClass.cpp

 

1. 생성자

안전상의 이유로 방금 추가한 멤버 변수를 초기화 한다.

ApplicationClass::ApplicationClass()
{
	m_Direct3D = nullptr;
}

 

2. Initialize

bool ApplicationClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{
	bool result;


	// Direct3D 개체를 생성 및 초기화 합니다.
	m_Direct3D = new D3DClass;

	// 이 클래스의 전역 변수 4개와 창의 가로 세로 길이를 매개변수로 D3DClass의 초기화 함수를 호출한다.
	// 그러면 D3DClass는 이 매개변수를 이용해 Direct3D 시스템을 설정한다.
	result = m_Direct3D->Initialize( screenWidth, screenHeight, VSYNC_ENABLED, hwnd, FULL_SCREEN, SCREEN_DEPTH, SCREEN_NEAR);
	
	if (!result)
	{
		MessageBox(hwnd, L"Could not initialize Direct3D", L"Error", MB_OK);
		return false;
	}

	return true;
}

 

3. Shutdown

이전에 언급했듯이 특정 Windows 함수는 클래스 소멸자를 호출하지 않아 메모리 누수가 발생할 수 있기 때문에 소멸시 해야하는 작업은 이 Shutdown함수에서 담당한다.

void ApplicationClass::Shutdown()
{
	// Direct3D 개체가 초기화 되었는지 확인 후 해제합니다. 
	if (m_Direct3D != nullptr)
	{
		m_Direct3D->Shutdown();
		delete m_Direct3D;
		m_Direct3D = 0;
	}

	return;
}

모든 그래픽 개체의 메모리 해제, 소멸이 여기서 발생해 마찬가지로 D3DClass 개체도 여기에서 소멸된다.

 

4. Frame

이제 Frame함수는 매 프레임마다 Render함수를 호출한다.

bool ApplicationClass::Frame()
{
	bool result;


	// 그래픽 Scene을 렌더링합니다. 
	result = Render();

	if (result == false)
	{
		return false;
	}

	return true;
}

 

5. Render

화면에 그림을 그리는 함수. 아직은 별 게 없다.

bool ApplicationClass::Render()
{
	// Scene을 회색으로 초기화 한다.
	m_Direct3D->BeginScene(0.5f, 0.5f, 0.5f, 1.0f);

	// 렌더링된 장면을 화면에 표시한다.
	m_Direct3D->EndScene();

	return true;
}

 

D3DClass.h

 

이 헤더파일에서는 닉값처럼 DirectX의 기능을 사용하기 위한 코드가 포함된다. 첫 부분은 라이브러리를 연결한다.

// DirectX의 라이브러리를 사용한다.
// #pragma comment : 해당 lib파일을 링크하라는 명령
#pragma comment(lib, "d3d11.lib") // 3D그래픽을 그리기 위한 모든 기능 포함
#pragma comment(lib, "dxgi.lib") // 하드웨어에 대한 정보를 얻는 기능 포함
#pragma comment(lib, "d3dcompiler.lib") // 셰이더 컴파일 기능 포함

 

그리고 가져온 라이브러리에서 사용하는 헤더파일을 include한다.

#include <d3d11.h> 
#include <directxmath.h>
using namespace DirectX;

 

클래스는 지금 시간엔 단순하게 유지한다. 중요한 건 '초기화 및 종료' 이다. 이 튜토리얼의 중점이다.

class D3DClass
{
public:
    D3DClass();
    D3DClass(const D3DClass&) = default;
    ~D3DClass() = default;

    bool Initialize(int, int, bool, HWND, bool, float, float);
    void Shutdown();

    void BeginScene(float, float, float, float);
    void EndScene();

    ID3D11Device* GetDevice();
    ID3D11DeviceContext* GetDeviceContext();

    void GetProjectionMatrix(XMMATRIX&);
    void GetWorldMatrix(XMMATRIX&);
    void GetOrthoMatrix(XMMATRIX&);

    void GetVideoCardInfo(char*, int&);

    void SetBackBufferRenderTarget();
    void ResetViewport();

private:
    bool m_vsync_enabled;
    int m_videoCardMemory;
    char m_videoCardDescription[128];
    IDXGISwapChain* m_swapChain;
    ID3D11Device* m_device;
    ID3D11DeviceContext* m_deviceContext;
    ID3D11RenderTargetView* m_renderTargetView;
    ID3D11Texture2D* m_depthStencilBuffer;
    ID3D11DepthStencilState* m_depthStencilState;
    ID3D11DepthStencilView* m_depthStencilView;
    ID3D11RasterizerState* m_rasterState;
    XMMATRIX m_projectionMatrix;
    XMMATRIX m_worldMatrix;
    XMMATRIX m_orthoMatrix;
    D3D11_VIEWPORT m_viewport;
};

 

D3DClass.cpp

 

1. 생성자

여태 그래왔던 것 처럼 모든 멤버 포인터를 초기화한다.

D3DClass::D3DClass()
{
	m_swapChain = nullptr;
	m_device = nullptr;
	m_deviceContext = nullptr;
	m_renderTargetView = nullptr;
	m_depthStencilBuffer = nullptr;
	m_depthStencilState = nullptr;
	m_depthStencilView = nullptr;
	m_rasterState = nullptr;
}

 

2. Intialize

이 초기화 함수는 Direct3D의 전체 설정을 수행하는 기능이다. 너무 길어 중요하다 싶은 것만 짚고 넘어간다. 먼저 매개변수를 확인한다.

bool D3DClass::Initialize(
	int screenWidth, // 창의 가로 길이
    int screenHeight, // 창의 세로 길이
    bool vsync, // 수직 동기화 여부
    HWND hwnd, // 프로그램 창의 핸들
    bool fullscreen, // 전체화면의 여부
    float screenDepth, // 렌더링 될 3D 환경에 대한 깊이 설정 1
    float screenNear // 렌더링 될 3D 환경에 대한 깊이 설정 2
)

 

첫번째로 하는 것은 그래픽카드, 모니터의 정보를 통해 주사율 값을 가져와 주사율을 설정해야 한다.

그렇지 않으면 사용하는 장치보다 높은 주사율으로 설정될 수 있는데 이는 성능 저하, 디버그 출력에 오류를 발생시킨다.

 

※ 원문에서 나오는 디스플레이 모드란 나는 처음에 해상도만을 뜻하는 줄 알았지만 사실 해상도 뿐만아니라 주사율, 픽셀 형식 등 정보가 포함된 구조체였다. 먼저 보고가면 이해하기 수월하다.

// 디스플레이 모드
typedef struct DXGI_MODE_DESC {
    UINT Width;                 // 화면의 가로 해상도
    UINT Height;                // 화면의 세로 해상도
    DXGI_RATIONAL RefreshRate;  // 주사율 (초당 화면 재생 빈도)
    DXGI_FORMAT Format;         // 픽셀 형식
    DXGI_MODE_SCANLINE_ORDER ScanlineOrdering; // 프레임을 그리는 방법
    DXGI_MODE_SCALING Scaling;  // 스케일링 방식
}

 

주사율을 가져오는 방법은 꼬리를 물듯이 이어진다. 먼저 IDXGIFactory 개체를 생성한다.

// DirectX graphics Infrastructure(DXGI) 개체를 생성하고 관리하는 기능을 한다.
// DXGI는 일단 프로그램과 그래픽 기능 장치(모니터, 그래픽카드 ..) 사이에서 일해주는 기능이라고 생각하자.
IDXGIFactory* factory;

// IDXGIFactory를 생성	
result = CreateDXGIFactory(__uuidof(IDXGIFactory), (void**)&factory);

 

그리고 만든  IDXGIFactory 개체를 이용해 그래픽카드의 정보를 가져올 수 있는 인터페이스인 IDXGIAdapter을 생성한다.

// IDXGIAdapter : 그래픽 카드의 특정 기능과 정보를 관리하는 인터페이스
// 그래픽 카드의 정보가 나열 되고 해당 그래픽 카드와 관련된 작업을 수행 가능
IDXGIAdapter* adapter;

// 방금 만든 DXGI factory를 사용해 0번째 사용하는 그래픽카드의 IDXGIAdapter를 생성한다.
result = factory->EnumAdapters(0, &adapter);

 

그리고 만든 IDXGIAdapter를 이용해 그래픽 카드와 연결 된 모니터의 정보를 가져올 수 있는 IDXGIOuput을 생성한다.

// IDXGIOutput : 그래픽 카드의 출력장치(모니터, ...)의 정보를 관리하는 인터페이스
IDXGIOutput* adapterOutput;

// IDXGIAdapter를 이용해 그래픽 카드의 사용 가능한 0번째 모니터의 정보를 저장한다.
result = adapter->EnumOutputs(0, &adapterOutput);

 

그리고 만든 IDXGIOuput을 이용해  Display mode 종류 개수를 저장한다.

여기서 중요한건 이 함수는 마지막에 NULL이냐 아니냐에 따라 동작이 달라지는데, NULL인 경우 지원 하는 Display mode의 총 개수를 3번째 매개변수에 저장한다.

// 해상도의 개수를 저장할 uint
unsigned int numModes

// 픽셀 형식이 DXGI_FORMAT_R8G8B8A8_UNORM인 해상도의 종류 개수를 numModes에 저장한다.
// DXGI_FORMAT_R8G8B8A8_UNORM는 32비트 색상 형식으로 각 채널(R,G,B,A)가 8비트로 표현 된다는 뜻.
result = adapterOutput->GetDisplayModeList(DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_ENUM_MODES_INTERLACED, &numModes, NULL);

 

아까 저장한 Display mode 개수만큼 동적 배열을 생성한다.

// 위에서 말했던 구조체!
DXGI_MODE_DESC* displayModeList;

// 방금 저장한 해상도의 종류 개수 만큼 배열을 생성한다.
displayModeList = new DXGI_MODE_DESC[numModes];

 

다시 한 번 GetDisplayModeList를 사용하는데 이번에 마지막에 NULL이 아닌 값을 넣는다. 이렇게 하면 마지막 매개변수 배열에  세번째 매개변수 만큼 Display mode의 정보를 채워 넣는다.

※ GetDisplayModeList는 원래 두 번 사용하게끔 설계되었다

// numModes의 수 만큼 디스플레이 모드 정보를 displayModeList으로 가져온다.
result = adapterOutput->GetDisplayModeList(DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_ENUM_MODES_INTERLACED, &numModes, displayModeList);

 

이제 모든 Display mode를 보면서 모니터의 크기와 맞는 모드를 찾는다. 일치하는 모드면 그 모드에 주사율 분자 분모를 저장한다.

// 디스플레이 모드 목록에서 모니터의 크기에 맞는 디스플레이 모드를 찾는다.
// 일치하는 디스플레이 모드를 찾으면 주사율의 분자 분모를 저장한다.
for (i = 0; i < numModes; i++)
{
	if (displayModeList[i].Width == (unsigned int)screenWidth)
	{
		if (displayModeList[i].Height == (unsigned int)screenHeight)
		{
			numerator = displayModeList[i].RefreshRate.Numerator;
			denominator = displayModeList[i].RefreshRate.Denominator;
		}
	}
}

 

이렇게 주사율 설정 값을 알아보았고 다음으로 할 것은 그래픽 카드의 이름과 vram 용량을 알아낸다.

// 그래픽 카드의 정보 구조체
DXGI_ADAPTER_DESC adapterDesc;

// DXGI_ADAPTER_DESC를 IDXGIAdapter를 통해 가져온다.
result = adapter->GetDesc(&adapterDesc);

// dedicated video card memory(VRAM)를 메가바이트 단위로 저장합니다.
m_videoCardMemory = (int)(adapterDesc.DedicatedVideoMemory / 1024 / 1024);

// 그래픽 카드의 이름을 Char형 배열로 변환하여 저장합니다.
wcstombs_s(&stringLength, m_videoCardDescription, 128, adapterDesc.Description, 128);

 

원하는 정보였던 주사율에 대한 분자 분모, 그래픽 카드의 정보를 저장했으니 사용했던 인터페이스들을 해제한다.

// Release the display mode list.
delete[] displayModeList;
displayModeList = 0;

// Release the adapter output.
adapterOutput->Release();
adapterOutput = 0;

// Release the adapter.
adapter->Release();
adapter = 0;

// Release the factory.
factory->Release();
factory = 0;

 


중간 마감

 

Initialize 내용이 너무 길어 다음 포스팅에서 마무리하고, 이번에 중점이였던 DXGI를 잠깐 짚고 간다.

 

DXGI(DirectX graphics intrastructure)

DXGI : 애플리케이션과 그래픽 하드웨어 사이에서 그래픽 자원을 관리하고 제어하는 프레임워크

(유저모드단에서 커널 모드 드라이버 및 시스템 하드웨어와 통신하는 것)

https://learn.microsoft.com/ko-kr/windows/win32/direct3ddxgi/d3d10-graphics-programming-guide-dxgi

 

우리가 아까 값을 가져왔던 주사율, 그래픽카드의 이름, VRAM의 크기 등... 모두 이 DXGI를 이용한것이다.

DXGI와 IDXGIFactory,, IDXGIAdapter 같은 IDXGI와의 관계라면, 이 IDXGI가 붙은건 모두 DXGI가 제공하는 많고 많은 인터페이스중 하나라고 할 수 있다.

 

DXGI 인터페이스 - Win32 apps

이 섹션에는 DXGI에서 제공하는 인터페이스에 대한 정보가 포함되어 있습니다.

learn.microsoft.com

 


개념 정리

 

모듈 : 특정 기능을 하는 단위, 그 크기는 함수부터 개체 등 기능에 따라 다양할 수있다.

 

pragma : 뒤에 오는 내용을 컴파일에게 수행하라는 전처리 명령.

 

디스플레이 모드 : DXGI_MODE_DESC 구조체를 뜻함

 

DXGI : 애플리케이션과 그래픽 하드웨어 사이에서 그래픽 자원을 관리하고 제어하는 프레임워크

 

픽셀 형식 : 픽셀의 색이 어떻게 표현되는지, 각 색상 채널이 몇 비트를 사용하는지, 알파 채널(투명)이 포함되는지 등을 지정하는 형식


IDXGIAdapter : 그래픽 카드와 관련된 정보 제공, 관련 작업을 수행하는 인터페이스


IDXGIOutput : 출력장치(대표적으로 모니터)에 대한 정보를 제공하고 제어하는 인터페이스

'DirectX 11 > DX11 Tutorial' 카테고리의 다른 글

[05] 텍스쳐링  (0) 2024.05.23
[04] 버퍼, 셰이더, HLSL  (0) 2024.05.21
[03] DirectX 11 초기화 (3)  (0) 2024.05.20
[02] DirectX 11 초기화 (2)  (0) 2024.05.20
[00] 프레임 워크와 창 생성  (0) 2024.05.17

+ Recent posts