우선 본격적으로 Assimp라이브러리를 사용하기에 앞서 Assimp라이브러리로 읽어들인 fbx파일을 어떤 구조를 가지고 있을지 살펴보자.
Assimp에서 관리하는 최상위 객체인 Scene이 하나 우선 있다.
Scene:이 안에 RootNode와 Mesh,Material 배열이 있다.
- RootNode : 메쉬의 계층구조를 위한 정보. 자신의 메쉬정보 Meshes[]와 직접 자식들의 포인터들 같은 정보가 들어있다. 만약 계층구조가 아니라면 RootNode가 아닌 Material, Mesh하나씩만 Scene에 들고 있으면 된다.
- ChildNode: RootNode의 직접자식들의 포인터가 가리키는 것 중 하나로 또 자신의 메쉬들 정보와/직접자식들의 포인터들 같은 정보가 들어있다. (*그리고 들고있는 메쉬가 꼭 메쉬가 아니라 라이팅 정보일수도 있고 여러가지 다양하다.)
- Material[]
- Material : 텍스쳐가 주요이고 외에도 라이팅 관련된(Ambient, Diffuse, Specular, Emissive)정보들이 있을 수 있다.
- GetTexture
- Material : 텍스쳐가 주요이고 외에도 라이팅 관련된(Ambient, Diffuse, Specular, Emissive)정보들이 있을 수 있다.
- Mesh[]
- Vertices[], Normals[], TextureCoords[](UV좌표),Faces[]
- MaterialIndex(메쉬에서 Scene의 mMaterials[]의 MaterialIndex번째의 정보를 가져와 그릴지)
- Face
- Indices
이런 계층구조를 오브젝트가 가지고있는것은 애니메이션 외에도 자식메쉬들이 상대적인 위치와 크기, 회전값을 가지고 재사용될수도 있기 때문이다.
위 구조를 보고 어떻게 사용할지, 파싱할지 생각해보면 우선 Scene에서 Material은 좀 자식이랑 관련이 없이 따로 독립적으로 하나씩 들고있으면 될 듯하고 메쉬들은 Material처럼 하나씩 들고있을 수도 있지만 계층구조를 fbx파일 구조에서 가져오는게 나을 듯하다. Scene노드의 mMeshes[]에 넣는게 아닌 mRootNode에 태우는 것.
무료 3D 모델을 구하기 위해서 https://free3d.com/ 요사이트를 이용해보려고 한다. EpicGames에서 제공하는 것인가 보다..? 여러 형태로 다운받을 수 있는데 fbx로 다운받으면 텍스쳐,obj다 포함되어있다. dragon, house, tower등을 다운받아서 로드해보자.
전시간에 Assimp라이브러리를 추가하면서 만들었던 새로운 프로젝트 AssimpTool의 Converter클래스를 열어서 시작한다.
Assimp에서 제공하는 Importer가 있고 위 구조에서처럼 가져 올 수 있는 Scene이 aiScene이라는 타입으로 들고있을 수 있도록 추가했다. ai로 시작하는 것은 Assimp에서 import해올 것이다(이미 정해진 타입). 반면 Assimp에서 읽어들인것을 우리만의 사용자 지정포맷으로 바꾸고자 하는데 그때 쓸 타입을 as로 시작하도록 AsTypes.h에 아래와 같이 추가하였다.
// AsTypes.h
#pragma once
using VertexType = VertexTextureNormalTangentBlendData;
struct asBone
{
string name;
int32 index = -1;
int32 parent = -1;
Matrix transform;
};
struct asMesh
{
string name;
aiMesh* mesh;
vector<VertexType> vertices;
vector<uint32> indices;
int32 boneIndex;
string materialName;
};
struct asMaterial
{
string name;
Color ambient;
Color diffuse;
Color specular;
Color emissive;
string diffuseFile;
string specularFile;
string normalFile;
};
그리고 맨 위에 VertexType을 VertexTextureNormalTangentBlendData으로 만들었는데 이전시간까지 Engine프로젝트의 VertexTextureNormalTangentData구조체를 정의해서 position, uv, normal, tangent값을 갖도록 만들어서 썼다면 이후의 애니메이션과 스키닝, 블렌드를 공부할 때 blend도 추가해서 사용할텐데 그때 번거롭게 바꾸지 않기 위해 미리 Engine\VertexData.h에 추가했다.
// 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 };
};
using ModelVertexType = VertexTextureNormalTangentBlendData;
다시 돌아와 Converter클래스를 보면 아래와 같이 생겼다.
#pragma once
#include "AsTypes.h"
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);
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;
private:
vector<shared_ptr<asBone>> _bones;
vector<shared_ptr<asMesh>> _meshes;
vector<shared_ptr<asMaterial>> _materials;
};
위 Converter클래스는 IExecute를 상속받아 만들어진 AssimpTool클래스에서 객체로 만들어져 사용될 것이다.
순서를 보면
AssimpTool프로젝트를 시작프로젝트로 실행->
Main의 WinMain에서 AssimpTool 앱(IExecute상속)실행
AssimpTool::Init에서 Converter객체를 생성해 아래와 같이 호출된다.
#include "pch.h"
#include "AssimpTool.h"
#include "Converter.h"
void AssimpTool::Init()
{
shared_ptr<Converter> converter = make_shared<Converter>();
// FBX -> Memory
converter->ReadAssetFile(L"Tower/Tower.fbx"); // 1
// Memory -> CustomData (File)
converter->ExportMaterialData(L"Tower/Tower"); // 2
converter->ExportModelData(L"Tower/Tower"); // 3
// CustomData (File) -> Memory
// 이부분은 후에 다른부분에서 진행예정.
}
// ...
AssimpTool에서 Converter툴의 객체를 호출해서 사용하는 것을 보면
1.
ReadAssetFile에서 Resources/Assets에 있는 fbx파일 경로 전달
Converter의 생성자에서 만든 Assimp::Importer객체로 메모리로 읽어드림
2.
2-1. ExportMaterialData->ReadMaterialData()
scene에서 aiMaterial로 가져온다음 AsTypes에 정의했던 asMaterial타입 정보들만 가져와 _materials에 추가한다.
(이 때 push_back으로 들어가서 자동으로 인덱스 생성된다)
2-2. ExportMaterialData->WriteMaterialData()
2-1에서 변환한 타입을 Resources/Texures에 .xml파일로 저장한다.
_materials을 순회하면서 이름, DiffuseMap이 있으면 텍스쳐로 저장하고 이름,(SpecularMap, NormalMap도 마찬가지)
머티리얼의 Ambient값, Diffuse값, Specular값, Emissive값을 xml다 저장한다.
3. 메쉬정보가 뼈대 관련된 vertex, Index정보
3-1. ExportModelData->ReadModelData(현재 노드, 현재 인덱스, 부모 인덱스)
-asBone타입 객체 동적으로 만들어서 인자로 받은 값을 넣고(+상대좌표 정보,부모좌표계로 변환행렬 함께 추가) _bones에 추가한다.
-이 계층에 메쉬 정보가 있으면(조명이나 다른것만 있으면 넘어갈수도 있다.)
이 계층에 있어 읽은 aiMesh정보를 asMesh정보로 하나씩 변환하여 _meshes에 추가한다.
(이때 VertexType, UV, Normal값을 필요에 맞게 변환한다.)
-ExportModelData에서 ReadModelData를 호출할때 최초엔 루트노드를 전달하고
그 하위 자식들을 또 ReadModelData함수 내에서 재귀함수로 호출된다. (ReadModelData는 인자로 노드와 인덱스, 부모가 있게 된다)
3-2. ExportMaterialData->WriteModelData()
3-1에서 변환한 타입을 Resources/Models에 .mesh확장자로 바이너리 파일로 저장
이렇게 진행된다.
Converter클래스에 있는 aiScene클래스는 Assimp라이브러리에 정의되어있는 타입으로 맨 위의 그림처럼
int mNumMeshes;
aiNode** mRootNode;
int mNumMaterials;
aiMaterial** mMaterials;등을 가지고있다.
얘를 이용해 경로를 입력해주면 경로에 있는 애셋파일들을 읽어올것이므로 Converter클래스에 void ReadAssetFile(wstring file);을 만들어줬다.
1. fbx 파일을 읽어들이는 ReadAssetFile함수
#include "pch.h"
#include "Converter.h"
#include <filesystem>
#include "Utils.h"
#include "tinyxml2.h"
#include "FileUtils.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);
}
// ...
C++17부터 파일 관련된 부분이 표준에 들어가 FILE*이런걸 안쓴다..!!
#include <filesystem>
ReadAssetFile에서
path클래스에 파일의 경로를 전달해 만든 객체로 관리한다.
wstring<->string을 변환하는 함수를 Utils에 넣어뒀었다.
그리고 이제 ReadFile로 fbx파일을 읽어올 때 옵션을 줄 수 있다.
aiProcess_ConvertToLeftHanded:
aiProcess_Triangulate: 삼각형 단위로 읽어온다.?
aiProcess_GenUVCoords:UV좌표도 읽어와야하고 없으면 연산으로도 얻어와야한다. GenNormals,CalcTangentSpace도 마찬가지다.
이런 옵션을 주어 연산까지 해서 정보를 추출할 수 있는것은 큰 장점이나 매번 로드할때마다 연산을 다시 하는것은 속도가 너무 느리다. 그래서 우리만의 포맷으로 저장했다가 읽어서 사용할 것이다.
우선 ReadAssetFile로 읽는 부분에 중단점을 걸고 현재 구조는 어떤지 본다음에 구조를 짜보자.
유니티에서 아무 fbx파일을 열어서 보면 (여기선 로드할건 아니지만 house.fbx파일을 열었다.)
유니티에서 같은 fbx파일을 열어서 보면 정점개수 661와 , 인덱스 갯수1433가 있다는 정보, 집의 메쉬도 있다. 즉, 메쉬가 계층구조를 이루고 있는데 모든 정점개수를 한곳에 넣은 다음에 몇번부터 몇번까지는 루트의 정보, 그 다음부터는 그 다음 뼈대, 또 그 다음부터는 그 다음 후손의 정점(인덱스)정보를 담고있는데 이를 분리하지 않고 한번에 쓰고 있는 것이다. 비슷하게 이를 활용해보자.
Assimp와 관련된 타입을 우리만의 사용자 지정 포맷으로 만들기 위해 AssimpTool프로젝트의 Utils에 AsTypes파일을 추가햇었다. 여기에 asBone, asMesh, asMaterial타입을 정의하고 Converter클래스에 vector로 그냥 들고 있게한 이유이다.
asMesh에는 이름과 Assimp에서 로드한 진짜 메쉬 aiMesh와 정점정보, 인덱스 정보를 넣어둔다.
그리고 asMesh에 연관된 머티리얼을 맵핑해서 써야하는데 이를 위해 boneIndex와 materialName을 추가했다. 여기서 boneIndex는 계층구조에서 어떤 노드와 붙어있는지를 인덱스로 나타낼 것이다.
이제 Assimp 라이브러리로 fbx파일을 읽어 메모리에 올렸으면 하나하나 분리하는 함수를 만든다.
2. MateraiData 분리하여 .xml파일로 저장하기
ReadMaterialData, ReadModelData
void Converter::ExportMaterialData(wstring savePath)
{
wstring finalPath = _texturePath + savePath + L".xml";
ReadMaterialData();
WriteMaterialData(finalPath);
}
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);
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);
}
}
void Converter::WriteMaterialData(wstring finalPath)
{
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>();
tinyxml2::XMLDeclaration* decl = document->NewDeclaration();
document->LinkEndChild(decl);
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());
}
머티리얼 정보는 쉐이더에 넘겨줄 정보로 ambient, diffuse, specular, emissive값, 디퓨즈 텍스쳐, 스페큘러 텍스쳐, 노멀텍스쳐 등이 들어있었다. 이를 갖고 있는 scene에서 각각 가져와 텍스쳐는 복사하고 조명 정보는 xml타입으로 tinyxml2를 이용해 저장한다.
3. 메쉬정보(머티리얼X) 분리하여 .mesh파일로 저장하기
// Converter.cpp
// ...
void Converter::ExportModelData(wstring savePath)
{
wstring finalPath = _modelPath + savePath + L".mesh";
ReadModelData(_scene->mRootNode, -1, -1);
WriteModelFile(finalPath);
}
void Converter::ReadModelData(aiNode* node, int32 index, int32 parent)
{
shared_ptr<asBone> bone = make_shared<asBone>();
bone->index = index;
bone->parent = parent;
bone->name = node->mName.C_Str();
// Relative Transform
Matrix transform(node->mTransformation[0]);
bone->transform = transform.Transpose();
// 2) Root (Local)
Matrix matParent = Matrix::Identity;
if (parent >= 0)
matParent = _bones[parent]->transform;
// Local (Root) Transform
bone->transform = bone->transform * matParent;
_bones.push_back(bone);
// Mesh
ReadMeshData(node, index);
// 재귀 함수
for (uint32 i = 0; i < node->mNumChildren; i++)
ReadModelData(node->mChildren[i], _bones.size(), index);
}
void Converter::ReadMeshData(aiNode* node, int32 bone)
{
if (node->mNumMeshes < 1)
return;
shared_ptr<asMesh> mesh = make_shared<asMesh>();
mesh->name = node->mName.C_Str();
mesh->boneIndex = bone;
for (uint32 i = 0; i < node->mNumMeshes; i++)
{
uint32 index = node->mMeshes[i];
const aiMesh* srcMesh = _scene->mMeshes[index];
// Material Name
const aiMaterial* material = _scene->mMaterials[srcMesh->mMaterialIndex];
mesh->materialName = material->GetName().C_Str();
const uint32 startVertex = mesh->vertices.size();
for (uint32 v = 0; v < srcMesh->mNumVertices; v++)
{
// Vertex
VertexType vertex;
::memcpy(&vertex.position, &srcMesh->mVertices[v], sizeof(Vec3));
// UV
if (srcMesh->HasTextureCoords(0))
::memcpy(&vertex.uv, &srcMesh->mTextureCoords[0][v], sizeof(Vec2));
// Normal
if (srcMesh->HasNormals())
::memcpy(&vertex.normal, &srcMesh->mNormals[v], sizeof(Vec3));
mesh->vertices.push_back(vertex);
}
// Index
for (uint32 f = 0; f < srcMesh->mNumFaces; f++)
{
aiFace& face = srcMesh->mFaces[f];
for (uint32 k = 0; k < face.mNumIndices; k++)
mesh->indices.push_back(face.mIndices[k] + startVertex);
}
}
_meshes.push_back(mesh);
}
void Converter::WriteModelFile(wstring finalPath)
{
auto path = filesystem::path(finalPath);
// 폴더가 없으면 만든다.
filesystem::create_directory(path.parent_path());
shared_ptr<FileUtils> file = make_shared<FileUtils>();
file->Open(finalPath, FileMode::Write);
// Bone Data
file->Write<uint32>(_bones.size());
for (shared_ptr<asBone>& bone : _bones)
{
file->Write<int32>(bone->index);
file->Write<string>(bone->name);
file->Write<int32>(bone->parent);
file->Write<Matrix>(bone->transform);
}
// Mesh Data
file->Write<uint32>(_meshes.size());
for (shared_ptr<asMesh>& meshData : _meshes)
{
file->Write<string>(meshData->name);
file->Write<int32>(meshData->boneIndex);
file->Write<string>(meshData->materialName);
// Vertex Data
file->Write<uint32>(meshData->vertices.size());
file->Write(&meshData->vertices[0], sizeof(VertexType) * meshData->vertices.size());
// Index Data
file->Write<uint32>(meshData->indices.size());
file->Write(&meshData->indices[0], sizeof(uint32) * meshData->indices.size());
}
}
요만 보면 ExportModelData에서 ReadModelData를 호출할때 최초엔 루트노드를 전달하고 그 하위 자식들을 또 ReadModelData함수 내에서 재귀함수로 호출된다. 그래서 ReadModelData는 인자로 노드와 인덱스, 부모가 있게 된다.
다음시간엔 AssmipTool을 사용해 만든 메쉬파일(*.mesh)과 머티리얼파일(*.xml)정보를 읽어들여서 모델(오브젝트)를 띄우는 작업을 할 예정이다.
'[DirectX11]' 카테고리의 다른 글
모델 #3 모델 띄우기 (0) | 2024.09.19 |
---|---|
모델 #1 Assimp 라이브러리 (0) | 2024.09.19 |
Normal Mapping (0) | 2024.09.18 |
Material (0) | 2024.09.10 |
Light #5 Light 통합 (0) | 2024.09.09 |