DirectX

53. 모델_Material 로딩

devRiripong 2024. 2. 21.
반응형

Assimp 라이브러리를 본격적으로 사용해 볼 것이다.

 

 

대략적으로 이런 구조로 로드를 한다.

오른쪽 Mesh와 Material은 Engine단에서 작업하던 것과 비슷하다.

왼쪽의 계층 구조는 Node 기반으로 표현이 된다.

노드가 무조건 뼈는 아니지만 기본적으로 뼈와 연관성이 있는 애들을 넣어준다고 보면 된다.

 

파싱 하는 전략을 세워야 한다.

Scene에서 Material은 독립적이니까 긁어서 하나씩 들고 있으면 될 것이고,

Mesh도 따로따로 불러 가지고 들고 있어도 되고, 나중에 FBX를 보면 대부분은 노드를 타고 타고 가면 마지막에 어떤 Mesh와 연관성이 있는 건지 가리키고 있기 때문에 파싱 하는 과정에서 만나는 메시를 로드해도 되고 여러 전략이 있다.

 

Material, Mesh가 있고 계층구조는 Node로 나타낸다가 끝이다.

이걸 이용해서 파싱을 해보는 전략을 세울 것이다.

 

테스트를 하기 위해 오브젝트가 있어야 한다.

오늘은 애니메이션이 없는 오브 젝트를 살펴 본다.

free3d.com에 가서 fbx를 살펴보면 많다.

모든 에셋이 다 되는 건 아니다. fbx는 복잡하기에 모든 것들을 맞춰서 하는 건 힘들다.

일부 가장 이상적인 에셋은

이렇게 다운로드할 때 여러 가지 같이 있는 게 이상적이다.

 

1. 초기 세팅

1) 리소스 파일 받기 

수업 자료의  완성 프로젝트를 보면 Resources\Asset에 Dragon, House, Tank, Tower를 받아 주었다.

이걸 우리의 프로젝트에 복붙 한다.

 

어떤 모습인지 보고 싶으면 fbx파일을 vs에 드래그 앤 드롭을 하면 3d 모델링을 볼 수 있다.

 

2) Converter 클래스에 aiScene* 변수 선언하기

Converter 클래스로 와서 시작한다.

 

scene에 이런저런 정보가 있다고 했었다.

const aiScene* _scene;

ai가 붙으면 기본적으로 assimp에서 제공하는 struct 형태라고 보면 된다.

ai 뭐 하면 우리가 만든 버전이 아니라 Assimp로 import 한 구조체의 형식이 이렇게 돼있다고 보면 된다.

struct aiScene을 보면 위에서 본 지도대로 되어 있다.

 

2. Converter에 ReadAssetFile 함수 구현하기 

public:
	void ReadAssetFile(wstring file);

을 선언하는데

경로는 절대경로, 상대경로로 할지 선택하는데 오늘은 상대경로로 해준다.

private: 
	wstring _assetPath = L"../Resources/Assets/";
	wstring _modelPath = L"../Resources/Models/";
	wstring _texturePath = L"../Resources/Textures/";

이렇게 삼총사로 나눠서 상대 위치로 만들어 준다.

 

Convert.cpp에 

 #include <filesystem>

을 추가한다. 기본 라이브러리로 포함시킨 게 17 버전부터이니 AssimpTool프로젝트의 Properties에 들어가서 C/C++의 Language에서 C++ Language Standard를 C++20으로 해준다.

 

 

ReadAssetFile에서 Asset을 읽어 올 것이다.

#include "pch.h"
#include "Converter.h"
#include <filesystem>
#include "Utils.h"
#include "tinyxml2.h"

Converter::Converter()
{
	_importer = make_shared<Assimp::Importer>(); 
}

Converter::~Converter()
{
}

void Converter::ReadAssetFile(wstring file)
{
	wstring fileStr = _assetPath + file; // Asset을 기반으로 했을 때 폴더랑 파일 이름을 적어 준다. 

// filesystem 같은 부분은 예전에도 해본 적 있긴 하다. 
// 이렇게 툴 같은 거 다룰 때 사용할 일이 많기 때문에 연습해보면 된다. 
// filesystem::path라는 클래스로 관리하게 될 것이다.
	auto p = std::filesystem::path(fileStr);
	assert(std::filesystem::exists(p)); // 파일이 있는지 

	_scene = _importer->ReadFile(
		Utils::ToString(fileStr),         // 파일 이름
		aiProcess_ConvertToLeftHanded |   // fbx 포멧을 어떻게 읽어들일지 플래그
		aiProcess_Triangulate |   // 삼각형 단위로
		aiProcess_GenUVCoords |   // uv 좌표도 생성 해줘라
		aiProcess_GenNormals |    // 노멀 매핑하기 위해 normal과 관련된 정보들도 있었으면 좋겠으니 정점에 연산해 달라 
		aiProcess_CalcTangentSpace
	); // 많은 정보들을 넣어 줄 수 있는데 매번 하기엔 속도가 느리다. 그래서 파일로 갖고 있는 거다.

	assert(_scene != nullptr); // 파일이 있는지 검색해서 없으면 에러를 낸다. 
}

파일을 로드해서 _scene에 온갖 잡동사니들이 들어간다고 보면 된다.

정보가 들어갔는지 assert에 breakpoint를 잡아서 살펴볼 수 있다. 

 

#pragma once
class Converter
{
public: 
	Converter(); 
	~Converter(); 

public:
	void ReadAssetFile(wstring file); 

private: 
	wstring _assetPath = L"../Resources/Assets/";
	wstring _modelPath = L"../Resources/Models/";
	wstring _texturePath = L"../Resources/Textures/";

private: 
	shared_ptr<Assimp::Importer> _importer; 
	const aiScene* _scene; 
};
#include "pch.h"
#include "Converter.h"
#include <filesystem>
#include "Utils.h"

Converter::Converter()
{
	_importer = make_shared<Assimp::Importer>(); 
}

Converter::~Converter()
{
}

void Converter::ReadAssetFile(wstring file)
{
	wstring fileStr = _assetPath + file;

	auto p = std::filesystem::path(fileStr);
	assert(std::filesystem::exists(p));

	_scene = _importer->ReadFile(
		Utils::ToString(fileStr),
		aiProcess_ConvertToLeftHanded |
		aiProcess_Triangulate |
		aiProcess_GenUVCoords |
		aiProcess_GenNormals |
		aiProcess_CalcTangentSpace
	); // 이걸 매번 하기엔 속도가 느리다. 그래서 파일로 갖고 있는 거다.

	assert(_scene != nullptr); 
}

 

 

3. Converter를 테스트하는 AssimpTool 클래스 구현하기 

Converter를 테스트하는 Demo 같은 애들을 만들어 볼 것이다.

 

1) 기본 세팅

AssimpTool/Game 필터에 AssimpTool 클래스를 추가한다.

#pragma once
#include "IExecute.h"

class AssimpTool : public IExecute
{
public:
	void Init() override; 
	void Update() override; 
	void Render() override; 
};
#include "pch.h"
#include "AssimpTool.h"
#include "Converter.h"

void AssimpTool::Init()
{

    {
        shared_ptr<Converter> converter = make_shared<Converter>(); 

        // FBX -> Memory
        converter->ReadAssetFile(L"House/House.fbx");

        // Memory -> CustomData 
        // 1차 목표

        // CustomData -> Memory

    }
}

void AssimpTool::Update()
{
}

void AssimpTool::Render()
{
}

이런 여러 가지를 만들어 줘야 한다.

 

Main.cpp에 가서

#include "AssimpTool.h"

헤더를 추가하고

desc.app = make_shared<AssimpTool>(); // 실행 단위

이렇게 해준다.

 

Converter::ReadAssetFile(wstring file)의

assert(_scene == nullptr);

에 break point를 걸어 놓고

Assimp 프로젝트를 실행을 해보면

문제가 일어난다.

Binaries 폴더에

이 두 개의 파일을 복사해 넣는다.

Assimp 라이브러리를 빌드했을 때 나오는 일반 라이브러리뿐만 아니라 dll도 복붙 해야 한다.

 

dll 은 일반 라이브러리와 달리 실행되는 순간에 붙어 가지고 들어가는 그런 포맷이다.

 

2) AssimpTool::Init에서 ReadAssetFile을 호출해 fbx를 메모리에 올리는 것에 성공했는지 테스트하기

실행을 해보면

구조가 정상적으로 나와 있다.

 

이 정보를 곧이곧대로 사용할 수 없으니 분류를 해서 사용할 준비를 해야 한다.

카메라나 라이트 정보는 별로 중요하지 않다. 새로 만들 것이기 때문에.

잡동사니를 제거해서 깔끔하게 만들어야 한다.

작업할 때 유니티에 넣어서 살펴보는 것도 도움이 많이 된다.

유니티는 최적화를 많이 해 놓은 상태라 완벽하게 동일하게 되지는 않을 수 있긴 하지만 그럼에도 불구하고 그 전체적인 구조 자체는 똑같이 로딩이 되어야 한다.

 

Resources/Assets/House에

House.fbx라는 게 있는데

이거를 유니티에서 살펴본다. Camera, Light, Material, Mesh 정보가 들어가 있다. Camera, Light는 삭제하고 House에 텍스쳐를 입혀서 살펴보면 된다.

이러한 집 모양이고 이러한 정점들을 들고 있을 것이다가 핵심이라고 보면 된다.

 

1차적으로 fbx에서 메모리에 올리는 거 까지 성공을 한 것이다.

 

혼자 작업했다면 엄청 품이 많이 드는 작업이었을 것이다. fbx 포맷을 분석해서 하나씩 로드하고 이런 부분이 들어가는데 좀 힘들긴 했지만 라이브러리를 통해 쉽게 해 준 것이다.

// FBX -> Memory
converter->ReadAssetFile(L"House/House.fbx");

 

 

4. converter의 ReadAssetFile로 로드한 에셋 데이터를 우리만의 구조로 만들기 위한 AsTypes 클래스를 구현해 메모리에 들어간 Asset의 정보를 분석할 준비하기

로드한 에셋 파일을 메모리에 들고 있긴 하지만

메모리에 들고 있는 게 Assimp 라이브러리에서 제공한 형태의 구조로 들고 있기 때문에

그거를 다시 우리가 우리만의 구조로 끌고 오기 위해서

이것저것 정보를 추출해서 우리만의 방법으로 저장을 하는 걸로 시작을 하도록 할 것이다.

 

이 단계는 꼭 필요하지는 않지만 나중에라도 관리 측면에서 이거를 우리만의 포맷으로 할 때는 그렇게 들고 있는 것이 편하기 때문에 실질적으로 타입을 하나 추가하는 것이다.

 

Main필터에 AsTypes라는 클래스를 만든다. Assimp와 관련된 것을 우리만의 방식으로 분석하겠다는 의미의 이름이다.

 

1) VertexData.h에서 struct VertexTextureNormalTangentBlendData를 정의하고 AsTypes의 VertexType으로 사용하기

AsTypes.h 여기서

#pragma once

using VertexType = VertexTextureNormalTangentBlendData;

VertexType을 넣어주는데

VertexData.h에 여러 가지 VertexData를 엔진 쪽에다가 파주고 있다.

VertexData.h에 다음 주에 필요한 걸 예상해서

struct VertexTextureNormalTangentBlendData
{
	Vec3 position = { 0, 0, 0 };
	Vec2 uv = { 0, 0 };
	Vec3 normal = { 0, 0, 0 };
	Vec3 tangent = { 0, 0, 0 };
	Vec4 blendIndices = { 0, 0, 0, 0 }; 
	Vec4 blendWeights = { 0, 0, 0, 0 }; 
};

blending 정보가 추가된 이 걸 이용한 버전으로 작업을 해줄 것이다.

나중에 애니메이션을 섞을 때 스키닝과 관련된 정보라고 보면 된다.

 

2) AsTypes.h에서 3가지 정보인 struct asBone, asMesh, asMaterial 정의하기

다시 AsTypes.h로 가서 3가지 정보를 들고 있을 준비를 한다.

#pragma once

using VertexType = VertexTextureNormalTangentBlendData;
// 새로 정의해준 타입으로 한다. 만약 생각이 바뀌면 얘만 교체해주면 된다. 

// 핵심이 되는 정보 3가지 : Bone, Mesh, Material

struct asBone // as가 붙은 건 우리만의 버전이다. 
{
	string name; 
	int32 index = -1;	// 몇 번째 뼈대인지 
	int32 parent = -1;	// 부모님은 몇번인지 파싱을 하는 도중에 번호를 지어서 관리 
	Matrix transform;	// 유니티에서 봤던 것처럼 계층구조 안에서 SRT 정보가 만들어진다. 그걸 여기에 들고 있는거 
};

struct asMesh
{
	string name; 
	aiMesh* mesh; 
	vector<VertexType> vertices; 
	vector<uint32> indices; 
// 여기까지 default
	 
	int32 boneIndex; // 매핑할 때 계층구조에서 누구랑 연관 있는지, 어떤 뼈대에 붙어 있는지 추적하기 위한 용도
	string materialName; 
};

struct asMaterial
{
	string name; 
	Color ambient; 
	Color diffuse; 
	Color specular; 
	Color emissive; 
	string diffuseFile; // 이미지 파일 경로
	string specularFile; 
	string normalFile; 
};

이렇게 3가지 정보를 들고 있을 준비를 끝냈다.

 

assimp 라이브러리를 이용할 때는 오리지널을 이용하기보다는 구글에서 샘플을 몇 개 찾아보는 것을 권장한다. 그거를 지키는 게 좋다. 문제가 일어났을 때 원인 찾기가 어렵기 때문이다.

완벽하게 이해하고 쓰는 게 아니기 때문에 인터넷에 있는 것을 잘 분석해서 코드에 이식하는 것을 권장한다. 지금 그렇게 작업하고 있는 것이다.

 

이렇게 ReadAssetFile 후 메모리에 들어가 있는 것을 분석할 준비를 시작을 한다 볼 수 있다.

 

5. Converter에서 Asset을 읽어 메모리에 들어 있는 것을 read 하고 다시 write 할 함수 정의하기

Converter.h에 돌아가서 메모리에 들어간 것을 하나씩 read 할 함수를 만들어 줄 것이다.

private: 
	void ReadModelData(aiNode* node, int32 index, int32 parent); // 뼈대 구조와 Material이 아닌 나머지 부분들을 불러오는 부분

private: 
	void ReadMaterialData(); // Material과 관련된 정보를 로드하는 부분

이 Read 함수들을 어느 시점에 호출할 것이냐

 

처음에 ReadAssetFile을 해서 AssetFile을 메모리에 긁어 온 다음에

void Converter::ReadAssetFile(wstring file)
{
	wstring fileStr = _assetPath + file; // Asset을 기반으로 했을 때 폴더랑 파일 이름을 적어 준다. 

// 파일시스템 같은 부분은 예전에도 해본 적 있긴 하다. 
//연습 해보 면서 이렇게 툴 같은 거 다룰 때 사용할 일이 많기 때문에 연습해보면 된다. 
//filesystem::path라는 클래스로 관리하게 될 것이다.
	auto p = std::filesystem::path(fileStr);
	assert(std::filesystem::exists(p)); // 파일이 있는지 

	_scene = _importer->ReadFile(
		Utils::ToString(fileStr),         // 파일 이름
		aiProcess_ConvertToLeftHanded |   // fbx 포멧을 어떻게 읽어들일지 플래그
		aiProcess_Triangulate |   // 삼각형 단위로
		aiProcess_GenUVCoords |   // uv 좌표도 생성 해줘라
		aiProcess_GenNormals |    // 노멀 매핑하기 위해 normal과 관련된 정보들도 있었으면 좋겠으니 정점에 연산해 달라 
		aiProcess_CalcTangentSpace
	); // 많은 정보들을 넣어 줄 수 있는데 매번 하기엔 속도가 느리다. 그래서 파일로 갖고 있는 거다.

	assert(_scene != nullptr); // 파일이 있는지 검색해서 없으면 에러를 낸다. 
}

이 중에서 골라서 어떠한 정보를 파일로 빼주고 싶은 지를 따로 함수로 호출해서 관리를 해줄 것인데 그 함수에다가 실질적으로 이 부분을 넣어준다고 보면 된다.

 

예를 들어 ReadMaterialData를 호출하게 되면

ReadMaterialData을 함과 동시에 file로 만들어서 저장을 해라 하는 세트가 된다.

그렇게 하는 이유는 파일에 많은 정보가 있을 텐데 모든 정보를 추출하고 싶을지는 그때 그때마다 다르기 때문에 두 단계로 나눠서 한다.

결국에는 세트가 되어야 하는데

 

나중에 가면

void WriteMaterialData(wstring finalPath);

에서 우리가 원하는 최종 경로에다가 추출해 달라는 하나의 함수를 더 만들어 준 다음에

 

ExportMaterialData에서

ReadMaterialData();로 이 정보를 먼저 읽어가지고 이미 메모리에 들어가 있는 거를 우리만의 버전으로 바꿔치기 한 다음에

그거를 다시 WriteMaterialData로 실질적으로 파일에다가 적용하는 형태로 저장할 것이다.

 

void ExportModelData(wstring savePath); 
void ExportMaterialData(wstring savePath);

이 Export함수를 2개 더 파줄 건데, 파일 정보를 추출하는 함수를 두 개 파주도록 할 거다.

 

1) Converter.h에 필요한 함수들 선언하기 

한 번에 선언하고, 정의해 주도록 하자.

#pragma once
class Converter
{
public: 
	Converter(); 
	~Converter(); 

public:
	void ReadAssetFile(wstring file); 
	void ExportModelData(wstring savePath); 
	void ExportMaterialData(wstring savePath); 

private: 
	void ReadModelData(aiNode* node, int32 index, int32 parent); // 뼈대 구조와 Material이 아닌 나머지 부분들
	void ReadMeshData(aiNode* node, int32 bone); 
	void WriteModelFile(wstring finalPath); 

private: 
	void ReadMaterialData(); 
	void WriteMaterialData(wstring finalPath); 
	string WriteTexture(string saveFolder, string file); 

private: 
	wstring _assetPath = L"../Resources/Assets/";
	wstring _modelPath = L"../Resources/Models/";
	wstring _texturePath = L"../Resources/Textures/";

private: 
	shared_ptr<Assimp::Importer> _importer; 
	const aiScene* _scene; 
};

ReadAssetFile만 구현이 된 상태이다.

 

2) Converte::ExportModelData에서 1단계: 최종 경로 설정, 2단계: ReadModelData를 호출해 _scene에 있는 정보 중 ModelData를 추출, 3단계: WriteModelFile을 호출해 우리만의 파일을 만들게 하기

1단계: 파일을 만들 최종 경로 설정

Convert::ExportModelData를 하면 ReadAssetFile에서 채워준 _scene 정보에 있는 ModelData를 추출해서 별도의 우리만의 파일로 만들어 줘라가 되는 것이니까 여기서는 최종적인 경로를 골라줘야 한다.

void Converter::ExportModelData(wstring savePath)
{
wstring finalPath = _modelPath + savePath + L".mesh"; 
// _modelPath에 savePath에 추가로 구분할 수 있는 뭔가를 만들어 준다.
// .data, .model 로 해도 상관 없다. 하나 골라서 사용하면 된다.  
ReadModelData(_scene->mRootNode, -1, -1);
// 여기서 모델 정보를 읽을 것이다. 유니티에서 봤을 때 계층구조로 있떤 거를 로드해야 한다.
// 여기서 로드할 때 중요한 정보들은 Transform의 SRT 정보를 포함해서 로드할 것이다. 
// 지나가는 길에 다른 다른 정보가 있으면 포함해서 로드를 할건데 
// 중요한 건 루트 노드부터 하나씩 타고 내려가서 트리 노드를 파싱할 때 과연 어떻게 했는지를 
// 생각해 보면 된다. 제귀함수가 가장 핵심이 된다고 했다.

그래서 ReadModelData에서

void Converter::ReadModelData(aiNode* node, int32 index, int32 parent)

이렇게 매개 변수로 node, index, parent가 붙은 이유는 ReadModelData를 재귀적으로 호출할 예정이기 때문에 그렇게 만드는 것이다.

 

2단계: ReadModelData를 호출

ReadModelData(_scene->mRootNode, -1, -1);

이렇게 mRootNode와 -1, -1로 시작을 한다라고 건네주면 최상위 루트는 -1, -1이라고 해서 번호를 지정해 줄 것이고, 타고 가서 다른 자식을 만나면 다른 자식들은 0번 1번 2번 3번 올라가면서 계층적인 구조에 넘버링을 해가지고 실행을 해준다고 보면 된다. 우리가 정해준 번호인거지, fbx 포맷 자체에 들어가 있는 공식 넘버링은 아닌 거다.

 

3단계: WriteModelFile을 호출

이게 끝나면 최종적인 결과물을 WriteModelFile을 통해서 실제로 메모리에 들고 있던 거를 다시 최종 파일 형태로 만드는 단계이다.

void Converter::ExportModelData(wstring savePath)
{
	wstring finalPath = _modelPath + savePath + L".mesh"; // .data, .model 로 해도 상관 없다. 하나 골라서 사용하면 된다. 
	ReadModelData(_scene->mRootNode, -1, -1); 
	WriteModelFile(finalPath); 
}

이렇게 세 단계를 거치게 될 것이다.

 

3) Material도 마찬가지로 Convert::ExportMaterialData에서 최종경로 설정, Read, Write 하기 해서 우리만의 파일 만들게 하기

Material도 마찬가지이다.

void Converter::ExportMaterialData(wstring savePath)
{
// 최종적인 material을 저장할 경로
	wstring finalPath = _texturePath + savePath + L".xml"; // mh도 되지만 xml을 만들어서 바로 키면 문서로 살펴 볼 수 있으니 이렇게 만들어 본다. 
	ReadMaterialData();
	WriteMaterialData(finalPath); // 읽은 것을 토대로 Write, 원하는 경로에 파일로 저장하는게 목표
}

 

4) ReadMaterailData의 결과물 House.xml 미리 보기

완성된 결과물이 어떻게 뜨냐면 AssimpTool 프로젝트를 실행해서 살펴본다고 하면,

Resources/Assets에 있는 .fbx를 긁어서 완성된 결과물을 보면

Resources/Models에는 관련된 .mesh 파일이 하나가 생성이 된다. 말 그대로 mesh 정보 즉 뼈대랑 그리고 그와 관련된 vertex, index 정보가 다 들어간 잡동사니라고 보면 된다. binary 형태이기 때문에 열어서 읽어 봐도 수치가 들어가 있는 게 아니라서 읽을 수가 없다.

.xml로 만든 material 파일은 xml로 만드는 이유가 xml 같은 경우는 메모장으로 열어보면 편리하게 읽을 수 있다.

완성된 프로젝트의 Resources\Textures\House의 House.xml을 열어보면

<?xml version="1.0" encoding="UTF-8"?>
<Materials>
    <Material>
        <Name>cottage_texture</Name>
        <DiffuseFile></DiffuseFile>
        <SpecularFile></SpecularFile>
        <NormalFile></NormalFile>
        <Ambient R="0.2" G="0.2" B="0.2" A="1"/>
        <Diffuse R="0.80000001" G="0.80000001" B="0.80000001" A="1"/>
        <Specular R="0.2" G="0.2" B="0.2" A="20"/>
        <Emissive R="0" G="0" B="0" A="1"/>
    </Material>
</Materials>

편리하게 읽을 수 있게 보인다.

나중에 수정을 할 수 있는데 fbx파일 자체에 추출할 때 애당초 정보가 기입이 되어있지 않아서 관련된 이미지 파일이 들어가지 않으면 수동으로 채워서 조작할 수 있다. 여기 있는 숫자 정보들은 일부러 기입한 게 아니라 원래 fbx에 들어가 있던 내용들이다.

내용들을 추출해서 xml 파일로 만드는 게 목표다. 물론 xml이 아니라 json으로 해도 된다. 우리가 정한 우리만의 포맷으로 하건 자유다. xml의 장점이 많기 때문에 여기선 xml로 진행을 한다.

 

5) AssimpTool::Init에서 ExportMaterialData와 ExportModelData를 호출하기

사용하는 쪽에서는

void AssimpTool::Init()
{

	{
		shared_ptr<Converter> converter = make_shared<Converter>(); 

		// FBX -> Memory
		converter->ReadAssetFile(L"House/House.fbx"); // 이렇게 메모리에 들고 있었다고 하면, 

		// Memory -> CustomData (File)
		// 1차 목표
		converter->ExportMaterialData(L"House/House"); // xml 안붙이는 이유는 함수 안에 통일이 되어 있기 때문
		// Material이 필요 없으면 이 코드는 주석 처리하면 된다. Material만 추출하고 싶으면 얘만 놔두면 된다.
		// 이렇게 읽은 정보를 이리저리 추출해서 우리만의 커스텀 데이터 파일로 만들어 주는게 목적이다.
		converter->ExportModelData(L"House/House"); // 메쉬 정보 추출

		// CustomData (File) -> Memory
	}
}

콘텐츠를 개발할 때는 ExportMaterialData, ExportModelData에서 추출한 정보를 다시 거꾸로 CustomFile을 로드해서 메모리에 들고 있게 만든 다음에 그걸 이용해서 게임을 진행시키면 된다라는 얘기가 된다.

 

6) ExportMaterialData에서 호출되는 ReadMaterialData정의하기

ReadMaterialData가 좀 더 간단하니 먼저 살펴보자.

지도에서 살펴보는 것이다. 그대로 이용하면 된다.

Scene에 Material 배열이 있고, 그것에 접근할 수 있다고 했으니까, 그걸 그대로 이용을 하면 된다.

 

Converter.h에

#include "AsTypes.h"

를 추가하고

 

private: 
	vector<shared_ptr<asBone>> _bones; 
	vector<shared_ptr<asMesh>> _meshes; 
	vector<shared_ptr<asMaterial>> _materials;

이렇게 Convert.h에 추가하고

 

struct asMaterial
{
	string name; 
	Color ambient; 
	Color diffuse; 
	Color specular; 
	Color emissive; 
	string diffuseFile; // 이미지 파일 경로
	string specularFile; 
	string normalFile; 
} 

ReadAssetFile에서 세팅한 _scene에서 AsTypes.h에 정의한 이 내용들을 꺼내주면 된다.

 

ReadMaterialData를 구현하면

void Converter::ReadMaterialData()
{
	for (uint32 i = 0; i < _scene->mNumMaterials; i++)
	{
		aiMaterial* srcMaterial = _scene->mMaterials[i]; 

		shared_ptr<asMaterial> material = make_shared<asMaterial>(); 
		material->name = srcMaterial->GetName().C_Str(); 
		
		aiColor3D color; 
		// Ambient
		srcMaterial->Get(AI_MATKEY_COLOR_AMBIENT, color);
		material->ambient = Color(color.r, color.g, color.b, 1.f); 

		// Diffuse
		srcMaterial->Get(AI_MATKEY_COLOR_DIFFUSE, color);
		material->diffuse = Color(color.r, color.g, color.b, 1.f);

		// Specular
		srcMaterial->Get(AI_MATKEY_COLOR_SPECULAR, color);
		material->specular = Color(color.r, color.g, color.b, 1.f);
		srcMaterial->Get(AI_MATKEY_SHININESS, material->specular.w); // 세기 설정

		// Emissive
		srcMaterial->Get(AI_MATKEY_COLOR_EMISSIVE, color);
		material->emissive = Color(color.r, color.g, color.b, 1.0f);

		// diffuse, specular, normal과 관련된 텍스쳐를 받아오는 부분
		aiString file;

		// Diffuse Texture
		srcMaterial->GetTexture(aiTextureType_DIFFUSE, 0, &file);
		material->diffuseFile = file.C_Str(); // 파일 이름 넣는 건 옵션이다. 
// 파일 이름을 그대로 긁어서 사용할지, 나중에 수동으로 입력해 따로 관리할지는 프로젝트마다 보고 정해야 한다. 
// 여기에 파일 경로가 있어도, 진짜로 그 파일이 없는 경우도 있고, 다운받는 순간에 뭔가 이리저리 왔다갔다 하기 때문에 

		// Specular Texture
		srcMaterial->GetTexture(aiTextureType_SPECULAR, 0, &file);
		material->specularFile = file.C_Str();

		// Normal Texture
		srcMaterial->GetTexture(aiTextureType_NORMALS, 0, &file);
		material->normalFile = file.C_Str();

		_materials.push_back(material); // 만들어준 마테리얼을 넣어준다.

	}
}

우리만의 래핑 한 버전으로 따로따로 받아서 사용할 건데

아이디어는 “파일을 로드 한 거를 ReadMaterialData를 통해서 우리만의 버전으로 변신시켜서 메모리에 들고 있겠다”가 핵심이다.

이 작업을 계속해주면 된다.

 

material과 관련된 부분들은 그나마 간단하다면 간단한데, 아직까지는 다른 부분들은 안되어 있지만 Material만 일단은 쭉 가면서 완료를 해보도록 한다.

 

7) ReadMaterialData에서 우리만의 버전으로 만들어 _materials에 넣은 내용을 WriteMaterialData에서 xml파일로 만들기

WriteMaterialData를 실행하면 원하는 게 무엇이냐

_materials에 메모리에 넣어 둔 것을 xml 파일로 만드는 것이다.

xml로 만들기 위해서는 예전에 사용했던 Engine/98. Utils에 들어 있는 tinyxml2라는 게 필요하. 얘를 이용해서 원하는 구조대로 xml파일을 만들어주도록 코드를 WriteMaterialiData에 넣어주면 된다.

 

한 번 만들 때 노가다가 많은데 신경 써서 만들어 놓으면 두고두고 편리하게 사용할 수 있지만 난이도가 있다.

7_1) WriteMaterialData에서 폴더가 없으면 만들어주게 하기

먼저 해야 하는 건

폴더가 없으면 만드는 부분부터 해준다

void AssimpTool::Init()
{

    {
        shared_ptr<Converter> converter = make_shared<Converter>(); 

        // FBX -> Memory
        converter->ReadAssetFile(L"House/House.fbx");

        // Memory -> CustomData (File)
        // 1차 목표
        converter->ExportMaterialData(L"House/House"); // xml 안붙이는 이유는 통일이 되어 있기 때문
        // Material이 필요 없으면 이 코드는 주석 처리하면 된다. Material만 하고 싶으면 얘만 놔두면 된다.
        // 읽은 데이터를 추출해서 우리만의 커스텀 데이터 파일로 만들어 주는게 목적이다.
        converter->ExportModelData(L"House/House"); 

        // CustomData (File) -> Memory
    }
}

여기서 ExportMaterialData를 해가지고, House 폴더 산하에 Material을 만들어 달라고 했을 때, 

Resources/Textures/에 House라는 폴더가 없으면 ExportMaterialData를

void Converter::ExportMaterialData(wstring savePath)
{
	wstring finalPath = _texturePath + savePath + L".xml"; 
	ReadMaterialData();
	WriteMaterialData(finalPath); 
}

여기서 실행하다 tinyXml에는 파일이 없으면 만드는 것까지는 들어가 있지 않기 때문에 거기서 먹통이 될 것이다.

그래서 Coverter::WriteMaterialData에서 파일이 없으면, 만드는 부분까지 해주면 되는 건데,

파일을 만들 때는 항상 filesystem을 생각하면 된다.

void Converter::WriteMaterialData(wstring finalPath)
{
	// ~~/House/House.xml
	auto path = filesystem::path(finalPath); // 최종적인 경로를 path라는 걸로 받아준 거 

	// ~~/House 폴더가 없으면 만든다.
	filesystem::create_directory(path.parent_path()); // 최종 경로의 부모 
// create_directory를 사용하는 걸 어떻게 아는 거냐면 구글링을 해보면 된다. filesystem create folder 이런식으
// 검색하면 표준의 스펙이 나오는데 그걸 따라하고 되는지 보고 된다 싶으면 그걸 외워서 다음에 잘 사용하면 된다.

	string folder = path.parent_path().string(); // 폴더의 이름 


7_2) xml 포맷을 보고 WriteMaterialData에서 그대로 만들어 주기

그다음은 xml을 사용하는 건데 xml을 사용할 때는 xml 포맷을 본 다음에 그거에 따라가지고 그대로 하나씩 해주면 된다.

완성되어야 하는 결과물 하나를 예를 들어 보면

완성된 프로 젝트의 Resources/Textures/House의 House.xml을 열어 보면

<?xml version="1.0" encoding="UTF-8"?>
<Materials>
    <Material>
        <Name>cottage_texture</Name>
        <DiffuseFile></DiffuseFile>
        <SpecularFile></SpecularFile>
        <NormalFile></NormalFile>
        <Ambient R="0.2" G="0.2" B="0.2" A="1"/>
        <Diffuse R="0.80000001" G="0.80000001" B="0.80000001" A="1"/>
        <Specular R="0.2" G="0.2" B="0.2" A="20"/>
        <Emissive R="0" G="0" B="0" A="1"/>
    </Material>
</Materials>

이런 포맷으로 완성이 되길 원하는 것이다.

여기서 Material 최상위부터 시작해서 노드 하나가 추가되고 계층구조로 파주면 된다고 했으니까 이거를 그대로 지켜서 WriteMaterialData에 만들어 주면 된다.

tinyXML2를 이용해서 만드는데

#include "tinyxml2.h"

를 추가하고

Coverter::WriteMaterialData를 이어서 작성한다.

// XML을 사용

	// document를 만든다. 이게 사실상 파일이라고 보면 된다.
	shared_ptr<tinyxml2::XMLDocument> document = make_shared<tinyxml2::XMLDocument>();

// 여기다가 뭔가를 만들어서 추가한다.

	// 이 건 사실 필요 없다. xml 포멧으로 읽을 거라는 뜻이다.
	tinyxml2::XMLDeclaration* decl = document->NewDeclaration(); 
	document->LinkEndChild(decl);

	// 여기 부터 정하는 .xml의 구조대로 넣어주면 된다. 
// 새로운 element, 새로운 노드를 루트 노드라고 부른다.
	tinyxml2::XMLElement* root = document->NewElement("Materials");
	document->LinkEndChild(root); // 문서에 <Materials> 까지 포함이 된 것이다.

// 메모리에 들고 있었던 모든 material들을 하나씩 순회를 하면서 
	for (shared_ptr<asMaterial> material : _materials) 
	{
// 노드를 하나 파가지지고
		tinyxml2::XMLElement* node = document->NewElement("Material");
		root->LinkEndChild(node); // <Material> 까지 붙은 것이다.
// 그 다음에 material에다가 온갖 속성들을 만들어줘야 되기 때문에 그 부분들

// 예를 들어 엘레먼트를 하나 조정한다.
		tinyxml2::XMLElement* element = nullptr;

		element = document->NewElement("Name");
		element->SetText(material->name.c_str()); // Text를 설정한건데 메모리에 들고 있는 애를 받아서 이름으로 Name 엘리먼트에 세팅한다.
		node->LinkEndChild(element); // <Material>에 Name을 붙여 주는 작업이 완료 된다.
    // 이런 식으로 작업을 계속 해주면 된다.
		element = document->NewElement("DiffuseFile");
		element->SetText(WriteTexture(folder, material->diffuseFile).c_str());
// material에 diffuseFile이라는게 있는데 fbx 파일에서 주장하는 경로가 있을 것이다. 
// 그 경로에 가서 진짜로 그 상대 경로에 파일이 있는지를 보고 없으면 비워두고 
// 있으면 그거를 어떤 식으로건 우리가 만든 경로의 텍스쳐 폴더에다가 텍스쳐를 모아두기로 했으니까
// 그 경로에다가 뭔가 모아 놓는 그런 함수를 하나 또 파준 거다.
		node->LinkEndChild(element);

		element = document->NewElement("SpecularFile");
		element->SetText(WriteTexture(folder, material->specularFile).c_str());
		node->LinkEndChild(element);

		element = document->NewElement("NormalFile");
		element->SetText(WriteTexture(folder, material->normalFile).c_str());
		node->LinkEndChild(element);

WriteTexture 부분은 채워야 될 부분이다.

    	<DiffuseFile></DiffuseFile>
        <SpecularFile></SpecularFile>
        <NormalFile></NormalFile>

<Name>cottage_texture</Name>처럼 사이에 값이 채워져야 하는데 비워져 있다.
여기 있는 텍스쳐 내용을 어떻게 긁어올 것인지가 문제다.

 

// 경로에 모아 놓는 함수 
string Converter::WriteTexture(string saveFolder, string file)
{
	return ""; 
}

이 부분을 채우긴 해야 한다. 지금은 일단은 비워 놓는다.

 

최종적으로 뱉어주는 WriteTexture의 결과물은 실제로 텍스쳐(Resources\Textures)라고 되어있는 폴더 안에서 만들어준 최종 경로를 넣어준다고 보면 된다.

 

한마디로 어떤 경로에서 받았건 상관없이

Resources\Textures\House폴더가 있었다고 하면 여기에 texure를 넣어주고, material을 넣어줄 것이기 때문에

이 material에도 만약에 texture가 있다고 하면, 여기 있는 texure 이름 두 개가 material에 박히게끔 유도를 해줄 거란 얘기가 된다.

 

 

나머지 Ambient, Diffuse, Specular, Emissive는 단순하다.

  <Ambient R="0.2" G="0.2" B="0.2" A="1"/>

 House.xml의 이 부분은

	    	element = document->NewElement("Ambient");
		element->SetAttribute("R", material->ambient.x);
		element->SetAttribute("G", material->ambient.y);
		element->SetAttribute("B", material->ambient.z);
		element->SetAttribute("A", material->ambient.w);
		node->LinkEndChild(element); // 추가가 된다.

이렇게 이 element의 attribute를 세팅하면 된다.

이 작업을 Diffuse, Specular, Emissive를 대상으로 쭉 해주면 된다.

 

나머지 코드도 다 작성하면

void Converter::WriteMaterialData(wstring finalPath)
{
	// ~~/House/House.xml
	auto path = filesystem::path(finalPath); 

	// 폴더가 없으면 만든다.
	filesystem::create_directory(path.parent_path()); 

	string folder = path.parent_path().string(); 

	shared_ptr<tinyxml2::XMLDocument> document = make_shared<tinyxml2::XMLDocument>();

	// xml 포멧으로 읽을 거라는 거
	tinyxml2::XMLDeclaration* decl = document->NewDeclaration(); 
	document->LinkEndChild(decl);

	// 정하는 .xml의 구조대로 넣어주면 된다. 
	tinyxml2::XMLElement* root = document->NewElement("Materials");
	document->LinkEndChild(root);

	for (shared_ptr<asMaterial> material : _materials)
	{
		tinyxml2::XMLElement* node = document->NewElement("Material");
		root->LinkEndChild(node);

		tinyxml2::XMLElement* element = nullptr;

		element = document->NewElement("Name");
		element->SetText(material->name.c_str());
		node->LinkEndChild(element);

		element = document->NewElement("DiffuseFile");
		element->SetText(WriteTexture(folder, material->diffuseFile).c_str());
		node->LinkEndChild(element);

		element = document->NewElement("SpecularFile");
		element->SetText(WriteTexture(folder, material->specularFile).c_str());
		node->LinkEndChild(element);

		element = document->NewElement("NormalFile");
		element->SetText(WriteTexture(folder, material->normalFile).c_str());
		node->LinkEndChild(element);

		element = document->NewElement("Ambient");
		element->SetAttribute("R", material->ambient.x);
		element->SetAttribute("G", material->ambient.y);
		element->SetAttribute("B", material->ambient.z);
		element->SetAttribute("A", material->ambient.w);
		node->LinkEndChild(element);

		element = document->NewElement("Diffuse");
		element->SetAttribute("R", material->diffuse.x);
		element->SetAttribute("G", material->diffuse.y);
		element->SetAttribute("B", material->diffuse.z);
		element->SetAttribute("A", material->diffuse.w);
		node->LinkEndChild(element);

		element = document->NewElement("Specular");
		element->SetAttribute("R", material->specular.x);
		element->SetAttribute("G", material->specular.y);
		element->SetAttribute("B", material->specular.z);
		element->SetAttribute("A", material->specular.w);
		node->LinkEndChild(element);

		element = document->NewElement("Emissive");
		element->SetAttribute("R", material->emissive.x);
		element->SetAttribute("G", material->emissive.y);
		element->SetAttribute("B", material->emissive.z);
		element->SetAttribute("A", material->emissive.w);
		node->LinkEndChild(element);
	}

	document->SaveFile(Utils::ToString(finalPath).c_str());
// 마지막으로 document의 SaveFile을 해주면 된다. 
}

// 경로에 모아 놓는 함수 
string Converter::WriteTexture(string saveFolder, string file)
{
	return ""; 
}

이렇게 하면 모든 부분이 완성이 되어

<?xml version="1.0" encoding="UTF-8"?>
<Materials>
    <Material>
        <Name>cottage_texture</Name>
        <DiffuseFile></DiffuseFile>
        <SpecularFile></SpecularFile>
        <NormalFile></NormalFile>
        <Ambient R="0.2" G="0.2" B="0.2" A="1"/>
        <Diffuse R="0.80000001" G="0.80000001" B="0.80000001" A="1"/>
        <Specular R="0.2" G="0.2" B="0.2" A="20"/>
        <Emissive R="0" G="0" B="0" A="1"/>
    </Material>
</Materials>

이 문서가 만들어질 것이다.

 

마지막 부분에 document의 SaveFile을 해줬다.

	document->SaveFile(Utils::ToString(finalPath).c_str());

SaveFile이 인자로 받아주는 게 일반적인 스트링인 const char*이다.

wstring으로 되어 있는 것을 c_str으로 변환을 해서 넣어 줬다.

 

// 경로에 모아 놓는 함수 
string Converter::WriteTexture(string saveFolder, string file)
{
	return ""; 
}

이 부분을 채우긴 해야겠지만 근본적으로 봤을 땐,

WriteMaterialData가 정상적으로 실행이 됐고, 나머지 부분도 정상적으로 만들어 놨다고 하면

Material 폴더에 원하는 xml이 만들어진다는 것을 예상할 수 있다.

 

6. .xml 파일이 만들어지는지 테스트하기

1) 실행

실제로 되는지 테스트해보자.

void AssimpTool::Init()
{

	{
		shared_ptr<Converter> converter = make_shared<Converter>(); 

		// FBX -> Memory
		converter->ReadAssetFile(L"House/House.fbx");

		// Memory -> CustomData (File)
		// 1차 목표
		converter->ExportMaterialData(L"House/House"); 		
    		converter->ExportModelData(L"House/House"); 

		// CustomData (File) -> Memory
	}
}

ExportModelData는 지금 기능이 없지만

ReadAssetFile로 MaterialData를 로드해가지고

로드한 거를 ExportMaterialData를 해가지고

우리만의 포맷으로 저장할 것인데

void Converter::ExportMaterialData(wstring savePath)
{
	wstring finalPath = _texturePath + savePath + L".xml"; 
	ReadMaterialData();
	WriteMaterialData(finalPath); 
}

WriteMaterialData에서 우리만의 포맷은 XML로 지정해 줬다.

 

여기까지 실행해서 정상적으로 되는지 보자.

 

실행해서 화면에 아무것도 안 뜬다는 건 실행이 완료가 된 상황이다.

 

Resources/Textures/House에 보면

House.xml이 만들어져 있다.

열어 보면 내용이 들어있다.

<?xml version="1.0" encoding="UTF-8"?>
<Materials>
    <Material>
        <Name>cottage_texture</Name>
        <DiffuseFile></DiffuseFile>
        <SpecularFile></SpecularFile>
        <NormalFile></NormalFile>
        <Ambient R="0.2" G="0.2" B="0.2" A="1"/>
        <Diffuse R="0.80000001" G="0.80000001" B="0.80000001" A="1"/>
        <Specular R="0.2" G="0.2" B="0.2" A="20"/>
        <Emissive R="0" G="0" B="0" A="1"/>
    </Material>
</Materials>

 

결국 fbx파일을 파싱 해서 그와 관련된 여러 가지의 이름들이 완성되었다. 이미지 파일이 아직 없지만 있었다고 하면 파일 이름까지 채워서 어떤 파일로 얘가 있어야 되는지를 넣어주면 된다.

        <DiffuseFile></DiffuseFile>
        <SpecularFile></SpecularFile>
        <NormalFile></NormalFile>

 

2) 파일 이름이 있는지 살펴보기

파일 이름이 있는지 궁금하니까 살펴보자면

Converter::WriteMaterialData에

    		element = document->NewElement("DiffuseFile");
		element->SetText(WriteTexture(folder, material->diffuseFile).c_str());
		node->LinkEndChild(element);

		element = document->NewElement("SpecularFile");
		element->SetText(WriteTexture(folder, material->specularFile).c_str());
		node->LinkEndChild(element);

		element = document->NewElement("NormalFile");
		element->SetText(WriteTexture(folder, material->normalFile).c_str());
		node->LinkEndChild(element);

이 부분이 파일 같은 거 설정하는 부분이었다.

		node->LinkEndChild(element);

에 break point를 찍고 diffuseFile이 material 자체에 들어가 있었는지 보고 싶다면 보면 되는데 지금은 딱히 없다.

나중에 설정하는 부분에서 갖고 오면 되는데

 

Converter::ReadMaterialData()에서

  	// diffuse, specular, normal과 관련된 텍스쳐 
		aiString file;

		// Diffuse Texture
		srcMaterial->GetTexture(aiTextureType_DIFFUSE, 0, &file);
		material->diffuseFile = file.C_Str(); // 파일 이름 넣는 건 옵션이다. 

		// Specular Texture
		srcMaterial->GetTexture(aiTextureType_SPECULAR, 0, &file);
		material->specularFile = file.C_Str();

		// Normal Texture
		srcMaterial->GetTexture(aiTextureType_NORMALS, 0, &file);
		material->normalFile = file.C_Str();

		_materials.push_back(material);

만약에 없다 싶으면 나중에 넣어주면 된다.

 

결국 mesh를 만드는 순간, fbx로 추출하는 순간에 여러 가지 그런 걸 설정할 수 있는데

그런 부분을 채워서 넣어주느냐에 따라가지고 여기에 뭔가가 들어갈 수도 있고 아닐 수도 있다.

 

그거에 따라서 이미 되어 있는 거는 자동으로 그 경로를 사용하게끔 유도를 해주고,

아니면 나중에 material에 이런저런 이미지를 실제로 수동으로 기입을 해주는 식으로 작업을 해주면 된다.

        <DiffuseFile></DiffuseFile>
        <SpecularFile></SpecularFile>
        <NormalFile></NormalFile>

여기에 이미지를 넣어주고 그것에 따라가지고

Resources/Textures/House에 실제로 이미지를 배치해 주는 식으로 하면 포폴을 만들 때 깔끔하게 만들 수가 있을 것이다.

 

7. 맺음말

 

Material은 노가다가 많지만 직관적이었다. 1차적으로 쭉 가면 되는 것이고,

물론 다음 시간에 Texure를 어떻게 할지 고민해 보고 이런 부분이 들어가야 하겠지만

그다음 나머지 부분들은 이것보다 더 까다로워진다고 보면 된다.

계층 구 조가 있고 그러다 보니까 이거에 대해 고민하는 시간을 가져볼 필요가 생기게 된다.

 

결과적으로 fbx 파일이 뜨는 거 까지 확인을 하면 포폴을 만들기 전 바로 전단계까지 오고 있다는 거다.

애니메이션, 라이팅까지 적용이 된다면 그다음부터는 스카이박스, 터레인 만들고 몇 가지 부분이 추가되면 그럴 싸한 게임을 만들 준비가 끝난다.

애니메이션까지 완료가 되면 이펙트, 라이트 고급 기법인 디퍼드 라이팅을 해보고 그림자, 환경도 넣어보고 하겠지만 그 부분까지 가면 일반적인 2D게임을 만드는 것과 큰 차이가 없다고 볼 수 있다.

여기까지 오면 유니티로 하건 DX로 하건 큰 차이가 없다.

 

Assimp를 잘 활용하는 게 중요하다.

이렇게 안 하면 로딩할 때 10분 걸리면 곤란하니 정리 작업을 하고, 그걸 다시 로드하는 작업이 필요하다.

반응형

'DirectX' 카테고리의 다른 글

55. 모델_모델 띄우기  (0) 2024.02.28
54. 모델_Bone, Mesh 로딩  (1) 2024.02.23
52. 모델_Assimp  (0) 2024.02.18
51. Light, Material_버그수정(카메라 좌표)  (0) 2024.02.17
50. Light, Material_Normal Mapping  (0) 2024.02.17

댓글