2024년 6월 28일 금요일

DirectX Tutorial - Assimp

 


이전에는 FbxSDK를 사용해서 3D 모델 파일을 렌더링 하는

프로그램을 만들었는데

이번엔 Assimp 라이브러리를 사용해볼것임



FBX SDK랑 사용법이 비슷해서 만약 처음 공부하는 사람이면

FBX SDK로 먼저 해본 다음 써보는게 편할것 같긴 하다



설치는 The Asset-Importer Library Home (assimp.org)
링크타고 다운받아도 되고 터미널로 다운하는법도 있음



먼저 결과 사진

똑같이 Fbx 파일을 로드 가능하고 이외에 blend, 3ds, obj 등 

여러 확장자도 가능한걸로 앎




단, FBX SDK 와 달리 파일 안에 내장되어있던 텍스처를 따로 빼서

png로 만들어주는 기능이나, 폴리곤 모양을 세모로 변환해주는 기능은

지원하지 않으므로 조금 불편할 수 있음




그래서 3DS max로 파일을 미리 폴리곤을 삼각형으로 만들어준 파일들만 사용가능

그리고 파일을 여는 순간, 텍스처 빼서 저장해주니 이는 따로 작업 필요없음


변환한 모델파일과 텍스처는 같은 위치에만 있으면 됨




 참고로 Assimp 헤더를 선언할때는 다른 헤더들 보다 먼저 선언 해야함

이유는 모름 자꾸 구문 오류 떠서 맨위로 보냄





Assimp 설치 위치로 가면 include랑 lib 폴더가 있는데

프로젝트 속성에 디렉터리 추가하면 됨



그리고 assimp-vc143-mt.dll 이 없다고 실행이 안될경우

bin폴더에 있는걸 프로젝트 폴더에 복붙하면 해결됨




전체 소스코드

h117562/Tutorial_Assimp: Tutorial Assimp (github.com)

2024년 6월 23일 일요일

DirectX11 Tutorial - FbxSDK

 

이전에는 간단한 삼각형만 렌더링 했었는데

이번엔 외부 라이브러리로 파일을 로드시켜서 화면에 띄워볼 것임



모델 파일을 불러오기 위해 일단 확장자가 다양하게 있음

obj, fbx, blend 등이 대표적으로 많이 사용됨


이런 파일들을 읽어오는 라이브러리가 몇가지가 있는데

Assimp 라는 라이브러리는 위에 적어놓은 확장자를 포함하여

다양한 확장자들을 읽을 수 있다.

The Asset-Importer Library Home (assimp.org)


하지만 사용법이 안보이고 샘플 프로그램을 

직접 분석해야 하기 때문에 공부하기 까다롭다



다른 방법으로 FbxSDK를 사용하면 Fbx 파일만 읽을 수는 있지만

이 또한 사용법은 제대로 안알려주니 똑같이 샘플 코드보고 따라하던가 해야함



Blender Sdk 는 안써봐서 잘모르겠음



아무튼 근성으로 FbxSDK 이것저것 뜯어보고 실험 해본 결과

대충 쓸 수는 있게 됐으므로 정리해보기로 하였음


결과


이전에 빨간 삼각형 출력하는 프로그램에 추가로 작업


일단 정점 쉐이더 

cbuffer ConstantBuffer : register(b0)
{
	matrix World;
	matrix View;
	matrix Projection;
}

struct Vin {
	float4 pos : POSITION;
	float2 texcoord : TEXCOORD;
};

struct VOut {
	float4 pos : SV_POSITION;
	float2 texcoord : TEXCOORD;
};

VOut main(Vin input)
{
	VOut output;

	output.pos = mul(input.pos, World);
	output.pos = mul(output.pos, View);
	output.pos = mul(output.pos, Projection);
	output.texcoord = input.texcoord;

	return output;
}

Vin은 쉐이더로 들어오는 데이터이고 VOut은 나가는 데이터

texcoord 텍스처 좌표는 들어오자마자 건들지말고 고대로 픽셀 쉐이더로 넘겨버리고

3 매트릭스는 정점 위치랑 순서대로 곱하여 최종 위치를 계산하여 픽셀로 넘겨준다


픽셀 쉐이더

Texture2D diffuseTexture;
SamplerState SampleType;

struct Pin
{
	float4 position : SV_POSITION;
	float2 tex : TEXCOORD;
};

float4 main(Pin input) : SV_TARGET
{
    float4 textureColor = diffuseTexture.Sample(SampleType, input.tex);

	return textureColor;
}

정점 쉐이더로 부터 전달받은 데이터로 Texture2D랑 샘플링 하여

float4의 픽셀 컬러로 반환



Mesh 클래스

#include <vector>
#include <string>
#include <fbxsdk.h>
#include <d3dx11.h>
#include <DirectXMath.h>

struct Vertex
{
	DirectX::XMFLOAT3 position;
	DirectX::XMFLOAT2 textureCoord;
};

class Mesh
{
public:
	Mesh();

	bool InitializeBuffer(ID3D11Device* pDevice);
	void Render(ID3D11DeviceContext* pDeviceContext);
	void Shutdown();

	void SetResource(ID3D11Device* pDevice, std::string filePath);
public:
	std::vector<Vertex> vertices;
	std::vector<UINT> indices;
	ID3D11ShaderResourceView* diffuseTexture;

private:
	ID3D11Buffer* m_vertexBuffer;
	ID3D11Buffer* m_indexBuffer;

	UINT stride = sizeof(Vertex);
	UINT offset = 0;
};

메쉬 하나마다 정점의 위치와 정점을 그리는 순서(Index), 텍스처를 저장해두고

렌더링할 때 마다 Render함수를 호출하면 미리 초기화 둔 정점, 인덱스 버퍼를 통해 

정점 쉐이더로 전달하는 클래스임



FbxLoader 클래스

#ifndef _FBXLOADER_H_
#define _FBXLOADER_H_

#pragma comment(lib, "libfbxsdk-mt.lib")
#include "Mesh.h"

class FbxLoader
{
public:
	FbxLoader();

	bool LoadFile(ID3D11Device*, HWND, std::string FileDirectoryPath);
	void Shutdown();
	void Render(ID3D11DeviceContext* pDeviceContext);
private:
	void processNode(FbxNode*, ID3D11Device* pDevice);
	void processMesh(FbxNode*, ID3D11Device* pDevice);
	bool GetTextureFromMaterial(FbxSurfaceMaterial*, ID3D11Device* pDevice, Mesh& mesh);

private:
	std::vector<Mesh> m_meshes;
};

#endif

Fbx 파일로부터 메쉬 데이터를 추출하는 클래스

FbxSDK 2018 버전을 설치하고 폴더 위치를 포함 디렉터리에 추가하면됨


다중 스레드 디버그를 선택하고 링커 추가 종속성에 libfbxsdk-mt.lib 를 추가하면 됨



#include "FbxLoader.h"

FbxLoader::FbxLoader()
{

}

bool FbxLoader::LoadFile(ID3D11Device* pDevice, HWND hwnd, std::string fileDirectory)
{
	bool result;

	FbxIOSettings* pIos;
	FbxImporter* pImporter;
	FbxScene* pScene;

	//FBX 매니저 생성
	FbxManager* pManager = FbxManager::Create();

	//파일 IO 입출력 설정(IOSROOT 기본 설정)
	pIos = FbxIOSettings::Create(pManager, IOSROOT);

	//설정 적용
	pManager->SetIOSettings(pIos);

	//FileName을 두번째 매개변수에 넣어 씬 이름을 저장하는데 딱히 쓸데는 없다
	//Scene 생성
	pScene = FbxScene::Create(pManager, "");
	
	//불러온 데이터에 접근 할 수 있는 인터페이스를 제공
	pImporter = FbxImporter::Create(pManager, "");

	//파일이 제대로 로드가 됐는지 확인
	result = pImporter->Initialize(fileDirectory.c_str(), -1, pManager->GetIOSettings());
	if (!result)
	{
		MessageBox(hwnd, L"FbxImport Error", L"Error", MB_OK);
		std::string str = pImporter->GetStatus().GetErrorString();
		std::wstring w_trans = std::wstring(str.begin(), str.end());
		MessageBox(hwnd, w_trans.c_str(), L"Error", MB_OK);

		return false;
	}

	//로드된 파일의 데이터를 Scene에 담고 Importer 자원 반환
	pImporter->Import(pScene);
	pImporter->Destroy();
	pImporter = nullptr;

	//보통 폴리곤 형태가 사각형으로 되어 있기도 하여 일단 삼각형 폴리곤으로 변환함
	FbxGeometryConverter converter(pManager);
	converter.Triangulate(pScene, true);//true시 원본 데이터 유지 

	//모델 파일의 노드 탐색
	processNode(pScene->GetRootNode(), pDevice);

	//메쉬마다 정점과 인덱스 버퍼를 생성
	for (Mesh& m : m_meshes)
	{
		result = m.InitializeBuffer(pDevice);
		if (!result)
		{
			MessageBox(hwnd, L"Could not initialize Vertex Buffer", L"Error", MB_OK);
			return false;
		}
	}

	return true;
}

void FbxLoader::processNode(FbxNode* pNode, ID3D11Device* pDevice)
{
	//노드의 속성을 가져옴
	FbxNodeAttribute* nodeAttribute = pNode->GetNodeAttribute();

	if (nodeAttribute != nullptr)
	{
		//노드 타입이 메쉬나 스켈레톤일 경우에만 작업
		switch (nodeAttribute->GetAttributeType())
		{
		case FbxNodeAttribute::eMesh:
			processMesh(pNode, pDevice);
			break;
		case FbxNodeAttribute::eSkeleton:
			break;
		}
	}

	//자식 노드가 있는지 확인하며 탐색
	if (pNode->GetChildCount() > 0)
	{
		for (int i = 0; i < pNode->GetChildCount(); i++)
		{
			processNode(pNode->GetChild(i), pDevice);
		}
	}
	
	return;
}

void FbxLoader::processMesh(FbxNode* pNode, ID3D11Device* pDevice)
{
	FbxMesh* pMesh = pNode->GetMesh();
	Mesh lMesh;

	if (pMesh->IsTriangleMesh())
	{
		//메쉬내 모든 정점 정보를 받아옴 
		FbxVector4* positions = pMesh->GetControlPoints();

		//UVSet 이름들을 불러옴
		FbxStringList uvNames;
		pMesh->GetUVSetNames(uvNames);
		
		//모든 텍스처 좌표를 배열에 저장
		int polygonCount = pMesh->GetPolygonCount();
		for (int j = 0; j < polygonCount; j++)
		{
			for (int i = 0; i < 3; i++)//삼각형 폴리곤 기준 정점 그리는 순서에 따라 데이터를 배열에 밀어넣는다.
			{
				bool unmappedUV;//매핑 여부 (주의*) false를 리턴받게 되면 매핑이 되어있음을 의미
				Vertex vt;

				//정점 그리는 순서를 받아옴
				UINT index = pMesh->GetPolygonVertex(j, i);

				//정점 좌표를 저장
				vt.position = DirectX::XMFLOAT3(
					static_cast<float>(positions[index].mData[0]),
					static_cast<float>(positions[index].mData[1]),
					static_cast<float>(positions[index].mData[2]));
				
				//텍스처 좌표를 저장
				FbxVector2 fv2;
				pMesh->GetPolygonVertexUV(j, i, uvNames[0], fv2, unmappedUV);
				vt.textureCoord = DirectX::XMFLOAT2(
					static_cast<float>(fv2.mData[0]),
					1.0f - static_cast<float>(fv2.mData[1])
				);
				
				//배열에 추가
				lMesh.vertices.emplace_back(vt);
			}
		}

		//인덱스 순서대로 나열
		int vertexCount = pMesh->GetPolygonVertexCount();
		for (int i = 0; i < vertexCount; i++)
		{
			lMesh.indices.emplace_back(i);
		}

		//텍스처가 있는 경우  저장
		int materialCount = pNode->GetMaterialCount();
		if (materialCount > 0)
		{
			FbxSurfaceMaterial* pMaterial = pNode->GetMaterial(0);
			if (!pMaterial)
			{
				return;
			}
		
			GetTextureFromMaterial(pMaterial, pDevice, lMesh);
		}

		m_meshes.emplace_back(lMesh);
		pMesh->Destroy();
	}

	return;
}

//Material을 통해 텍스처 파일 경로를 알아내고 해당 경로를 통해 텍스처를 로드하는 함수
bool FbxLoader::GetTextureFromMaterial(FbxSurfaceMaterial* pMaterial, ID3D11Device* pDevice, Mesh& mesh)
{
	HRESULT result;

	//Diffuse 속성을 가져옴 
	FbxProperty prop = pMaterial->FindProperty(FbxSurfaceMaterial::sDiffuse);
	if (!prop.IsValid()) 
	{
		return false;
	}

	//텍스처 보유 확인
	int textureCount = prop.GetSrcObjectCount<FbxTexture>();
	if (!textureCount) 
	{
		return false;
	}

	//텍스처 파일 정보를 가져옴
	FbxString tempstr;
	FbxFileTexture* pTexture = prop.GetSrcObject<FbxFileTexture>();
	if (!pTexture) {

		return false;
	}
	
	//파일 경로를 저장한다.
	//저장한 FbxString을 string으로 변환
	tempstr = pTexture->GetFileName();
	mesh.SetResource(pDevice, tempstr.Buffer());

	pTexture->Destroy();

	return true;
}

void FbxLoader::Render(ID3D11DeviceContext* pDeviceContext)
{
	//모델의 메쉬 하나씩 렌더링 함
	for (Mesh& m : m_meshes)
	{
		m.Render(pDeviceContext);
	}

	return;
}

void FbxLoader::Shutdown()
{
	//구조체 배열안의 포인터는 직접 해제
	for (Mesh& m : m_meshes)
	{
		m.Shutdown();
	}

	m_meshes.clear();
}


나머지 전체 코드는 깃 링크에

h117562/Tutorial_FbxSDK: Tutorial FbxSDK (github.com)


사용한 모델 파일들은 Mixamo 홈페이지에서 구할 수 있음

AppicationClass에서 모델파일의 위치를 수정하여 렌더링하면

자동으로 파일에 내장되어있는 텍스처가 fbm 폴더로 추출됨


내장되어있는 텍스처를 그대로 사용하는 방법은 알 수 없었음

2024년 6월 6일 목요일

DirectX11 Tutorial - DirectWrite

 

저번 코드는 딱 기본 삼각형 하나만 출력하는 프로그램이지만

이번엔 마크처럼 디버깅 정보를 화면에 띄우는 기능을 구현함




즉 글자를 화면에 그릴 것인데 쉬운 방법으로 두가지가 있음

Sprite Font - 폰트 이미지를 매핑하여 해당 글자에 맞는 

부분에 맞춰 텍스쳐를 입히는 방법이고





DirectWrite - Direct 2D에 있는 기능을 가져와서

3D와 함께 사용하며 글자를 그리는 방법이다.


마소 홈페이지에 잘 정리되어 있으므로 참고



첫번째 방법의 문제점은 한글은 쓰기 힘들고 폰트 이미지를

새로 그리든 어딘가에서 구해야 하는 어려움이 있다.


Dwrite는 아직까지 큰 문제점은 없었음

고로 두번째 방법으로 코드를 작성할 것





프로젝트 속성에 들어가서

링커 -> 입력 -> 추가 종속성에 위와 같이

d2d1.lib과 dwrite.lib을 추가해준다.


아니면


헤더 포함시킬때 위쪽에 #pragma로 해주어도 된다.

둘다 같은 의미



그리고 Device를 생성할때 플래그를 하나 변경해주어야 하는데


D3D11_CREATE_DEVICE_BGRA_SUPPORT

Direct3D와 2D를 둘다 쓰려면 필요하다.




TextClass.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#include "textclass.h"
 
TextClass::TextClass()
{
    m_d2dFactory = 0;
    m_dwFactory = 0;
    m_defaultFormat = 0;
    m_renderTarget = 0;
    m_defaultBrush = 0;
}
 
TextClass::~TextClass()
{
 
}
 
bool TextClass::Initialize(D3DClass* d3dClass)
{
    HRESULT result;
    IDXGISurface* backBuffer;
    D2D1_RENDER_TARGET_PROPERTIES props;
 
    //D3D의 스왑체인 버퍼를 가져옴
    result = d3dClass->GetSwapChain()->GetBuffer(0, IID_PPV_ARGS(&backBuffer));
    if (FAILED(result))
    {
        return false;
    }
 
    //변수 초기화
    ZeroMemory(&props, sizeof(D2D1_RENDER_TARGET_PROPERTIES));
 
    //Direct2D 렌더 타켓 설정
    props.type = D2D1_RENDER_TARGET_TYPE_DEFAULT;
    props.dpiX = 0.0f;//0 기본
    props.dpiY = 0.0f;//0 기본
    props.usage = D2D1_RENDER_TARGET_USAGE_NONE;
    props.minLevel = D2D1_FEATURE_LEVEL_DEFAULT;
    props.pixelFormat = D2D1::PixelFormat(DXGI_FORMAT_UNKNOWN, D2D1_ALPHA_MODE_PREMULTIPLIED);
 
    //D2D 팩토리 생성 (싱글 스레드)
    result = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_d2dFactory);
    if (FAILED(result))
    {
        return false;
    }
 
    //렌더 타켓 생성
    result = m_d2dFactory->CreateDxgiSurfaceRenderTarget(
        backBuffer,
        &props,
        &m_renderTarget
    );
 
    if (FAILED(result))
    {
        return false;
    }
 
    //백 버퍼 포인터 해제
    backBuffer->Release();
    backBuffer = 0;
 
    //Dwrite 팩토리 생성
    result = DWriteCreateFactory(
        DWRITE_FACTORY_TYPE_SHARED,    //해당 팩토리를 공유할지 격리할지 설정
        __uuidof(IDWriteFactory),    //팩토리 인터페이스를 식별하는 GUID
        reinterpret_cast<IUnknown**>(&m_dwFactory)//팩토리에 대한 포인터 주소
    );
 
    if (FAILED(result))
    {
        return false;
    }
 
    //폰트 설정
    result = m_dwFactory->CreateTextFormat(
        L"나눔스퀘어 네오 OTF",    //폰트 이름
        NULL,                    //폰트 컬렉션에 대한 포인터 주소 NULL은 시스템 폰트 컬렉션
        DWRITE_FONT_WEIGHT::DWRITE_FONT_WEIGHT_NORMAL,//폰트 두께
        DWRITE_FONT_STYLE::DWRITE_FONT_STYLE_NORMAL,//폰트 스타일
        DWRITE_FONT_STRETCH::DWRITE_FONT_STRETCH_NORMAL,//폰트 스트레치
        30.0f,                    //폰트 사이즈
        L"ko",                    //지역 이름 ex) KO, EN
        &m_defaultFormat                //텍스트 형식(IDWriteTextFormat)에 대한 포인터 주소를 반환한다
    );
 
    if (FAILED(result))
    {
        return false;
    }
 
    //기본 브러쉬 설정 (빨강)
    result = m_renderTarget->CreateSolidColorBrush(
        D2D1::ColorF(D2D1::ColorF(1.0f, 0.0f, 0.0f, 1.0f)),
        &m_defaultBrush
    );
    if (FAILED(result))
    {
        return false;
    }
 
    //기본 텍스트 정렬 (왼쪽)
    result = m_defaultFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING);
    if (FAILED(result))
    {
        return false;
    }
 
    return true;
}
 
//텍스트 랜더링 기본 폰트, 컬러
void TextClass::RenderText(CONST WCHAR* ptext, FLOAT x, FLOAT y, FLOAT width, FLOAT height)
{
    m_renderTarget->DrawTextW(
        ptext,
        wcslen(ptext),
        m_defaultFormat,
        D2D1::RectF(x, y, width, height),
        m_defaultBrush,
        D2D1_DRAW_TEXT_OPTIONS_NONE,
        DWRITE_MEASURING_MODE_NATURAL
    );
}
 
//텍스트 랜더링 지정 폰트, 컬러
void TextClass::RenderText(CONST WCHAR* ptext, FLOAT x, FLOAT y, FLOAT width, FLOAT height, IDWriteTextFormat* pformat, ID2D1SolidColorBrush* pbrush)
{
    m_renderTarget->DrawTextW(
        ptext,
        wcslen(ptext),
        pformat,
        D2D1::RectF(x, y, width, height),
        pbrush,
        D2D1_DRAW_TEXT_OPTIONS_NONE,
        DWRITE_MEASURING_MODE_NATURAL
    );
}
 
//텍스트 랜더링 시작 함수
void TextClass::BeginDraw()
{
    m_renderTarget->BeginDraw();
}
 
//텍스트 랜더링 종료 함수
void TextClass::EndDraw()
{
    m_renderTarget->EndDraw();
}
 
void TextClass::Shutdown()
{
    if (m_defaultBrush)
    {
        m_defaultBrush->Release();
        m_defaultBrush = 0;
    }
 
    if (m_defaultFormat)
    {
        m_defaultFormat->Release();
        m_defaultFormat = 0;
    }
 
    if (m_renderTarget)
    {
        m_renderTarget->Release();
        m_renderTarget = 0;
    }
 
    if (m_dwFactory)
    {
        m_dwFactory->Release();
        m_dwFactory = 0;
    }
 
    if (m_d2dFactory)
    {
        m_d2dFactory->Release();
        m_d2dFactory = 0;
    }
 
}
cs


결과



+저번 코드에서 이어서 만들다가

삼각형이랑 글자가 픽셀비율이 이상한 오류가 있었는데 

D3DClass.cpp에서 윈도우 크기를 잘못 전달해서 생긴 문제였음



hwnd에서 바로 창크기를 구해서 구현함


원인이 무엇인지 콘솔로 확인해봤는데

아마 ㅡ ㅁ X 있는 부분인 테두리 때문에 

비율이 안맞아 픽셀이 늘어나는 현상인것으로 보임


SystemClass에서 윈도우 생성할 때 테두리 포함한 크기를

전달 해야 해결이 될듯함

h117562/Tutorial_DirectWrite (github.com)



c++ thread.h

 c++에서 쓰레드 돌릴려면 thread.h 헤더를 쓰면 되는데 이 친구는 쓰레드가 아직 실행 중인지, 아니면 강제 종료하거나 하는 함수가 없어서 조금 아쉬운 애다. std::thread 는 로컬 변수로 선언하든 new 동적 할당을 하든 start 함...