Assimp로 Material 로딩
유니티에서 에셋을 가져와 import 했을 때, 2D의 경우 매우 직관적이게 Texture를 틀기만 했음 됐지만 3D의 경우 수많은 파일들이 같이 들어온다. 특히 Model이 무엇인지 잘 몰라 헤맸던 경험이 있다. Material과 Shader 그리고 Animation 등등 여러 파일이 묶여 하나의 Model이 된다.
지금까지는 큐브, 구 등등 간단한 물체는 수학 식으로 생성할 수 있었지만, 그럴듯한 물체 즉 캐릭터 같은 물체들은 수많은 삼각형으로 이루어져있는데 이걸 수학 식으로 생성하는건 말이 안될 것이다.
그렇기에 3D 제작 툴을 통해서 fbx 파일을 생성하고 엔진에서는 그걸 가져와 사용하는 방식으로 이루어진다.
Assimp 라이브러리를 통해서 fbx 파일을 로드해보도록하자.
이렇게 무언가를 로드하는 툴은 Client라고 하기도 애매하고 Engine이라고도 하기 애매하니 새로운 프로젝트를 만들어서 진행할 것이다.
그리고 Assimp를 가져와 로드를 하기 위한 주요 클래스인 Converter를 만든다.
Converter::Converter()
{
_importer = make_shared<Assimp::Importer>();
}
이렇게 했을 때 아무런 문제가 없다면 라이브러리에 문제없이 사용가능하다는 것이다.
파일을 파싱하는 과정을 명확히 하고 넘어가야만한다. 어떤 구조로 파싱해야하는지
fbx 파일은 너무 방대하고 코어 정보들 뿐만 아니라 그다지 중요하지 않는 정보를 포함하고 있기 때문에 이걸 일단 Assimp를 통해 Memory에 로드를 한 후 이걸 다시 우리만에 format으로 저장할 것이다.
그 뒤로부터는 우리만의 format에서 load해서 사용하는 식으로 만들어보자.
#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"
#include "tinyxml2.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);
}
void AssimpTool::Init()
{
{
shared_ptr<Converter> converter = make_shared<Converter>();
//FBX -> Memory
converter->ReadAssetFile(L"House/House.fbx");
// Memory -> CustomData
// CustomData -> Memory
}
}
이렇게 까지 하고 브포를 잡아서 확인해보면 실제로 잘 로드되는걸 확인할 수 있다.
이제 우리가 관리할 코어 내용은 다음과 같이 저장한다.
#pragma once
using VertexType = VertexTextureNormalTangentBlendData;
struct asBone
{
string name;
int32 index = -1;
int32 parent = -1;
Matrix transform;
};
struct asMesh
{
string name;
aiMesh* mehs;
vector<VertexType> vertices;
vector<uint32> indices;
int32 boneIndex;
string materialName;
};
struct asMaterial
{
string name;
Color ambient;
Color diffuse;
Color specualr;
Color emissive;
string diffuseFile;
string specularFile;
string normalFile;
};
이걸 이제 다시 채워주기 위해 Converter를 채워준다.
#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;
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);
}
void Converter::ExportModelData(wstring savePath)
{
wstring finalPath = _modelPath + savePath + L".mesh";
ReadModelData(_scene->mRootNode, -1, -1);
WriteModelFile(finalPath);
}
void Converter::ExportMaterialData(wstring savePath)
{
wstring finalPath = _texturePath + savePath + L".xml";
ReadMaterialData();
WriteMaterialData(finalPath);
}
void Converter::ReadModelData(aiNode* node, int32 index, int32 parent)
{
}
void Converter::ReadMeshData(aiNode* node, int32 bone)
{
}
void Converter::WriteModelFile(wstring 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());
}
std::string Converter::WriteTexture(string saveFolder, string file)
{
return "";
}
이렇게 하면 실제로 xml 파일로 변경이 되었다. 즉 fbx 파일에서 쓸모없는 우리가 사용하지 않을 부분을 추출해서 Material 부분은 우리의 포멧으로 추출해서 사용하게 된것이다. 이렇게 한 이유는 나중에 모델이 수백개가 되어버리면 로딩 자체가 오래걸릴 수 있기 때문이다.
Bone, Mesh 로딩
재귀함수를 통해 계층구조로 있는 fbx 파일을 하나하나 파싱해 우리만의 포멧으로 저장할 것이다.
#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);
}
void Converter::ExportModelData(wstring savePath)
{
wstring finalPath = _modelPath + savePath + L".mesh";
ReadModelData(_scene->mRootNode, -1, -1);
WriteModelFile(finalPath);
}
void Converter::ExportMaterialData(wstring savePath)
{
wstring finalPath = _texturePath + savePath + L".xml";
ReadMaterialData();
WriteMaterialData(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();
// 좌표계를 변환하기 위해서는 SRT 변환행렬을 통해 변환해야한다.
// 가령 A가 root이고 B가 A의 자식이며, C가 B의 자식이라고 한다면
// C를 root를 기준으로 한 좌표계로 변경하기 위해선
// C transform * B transform * A transform(A의 좌표계니 얘는 원점이나 다름 없기에 없어도 됨)를 하면 된다.
// Relative Transform
// 여기서는 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
// 이미 부모의 transform은 root를 기준으로한 좌표계로 변경이 되어있을 것이다.
// 그렇기 때문에 새로운 자식 계층에 부모 transform 만 곱하면 모든 부모의 transform을 곱해서
// root를 기준으로 한 좌표계로 변환한 것이나 다름 없다.
// 이게 가능한 이유는 transform을 추출할 때 root부터 순서대로 하기 때문이다.
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());
}
}
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());
}
std::string Converter::WriteTexture(string saveFolder, string file)
{
string fileName = filesystem::path(file).filename().string();
string folderName = filesystem::path(saveFolder).filename().string();
const aiTexture* srcTexture = _scene->GetEmbeddedTexture(file.c_str());
if (srcTexture)
{
string pathStr = saveFolder + fileName;
if (srcTexture->mHeight == 0)
{
shared_ptr<FileUtils> file = make_shared<FileUtils>();
file->Open(Utils::ToWString(pathStr), FileMode::Write);
file->Write(srcTexture->pcData, srcTexture->mWidth);
}
else
{
D3D11_TEXTURE2D_DESC desc;
ZeroMemory(&desc, sizeof(D3D11_TEXTURE2D_DESC));
desc.Width = srcTexture->mWidth;
desc.Height = srcTexture->mHeight;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
desc.SampleDesc.Count = 1;
desc.SampleDesc.Quality = 0;
desc.Usage = D3D11_USAGE_IMMUTABLE;
D3D11_SUBRESOURCE_DATA subResource = { 0 };
subResource.pSysMem = srcTexture->pcData;
ComPtr<ID3D11Texture2D> texture;
HRESULT hr = DEVICE->CreateTexture2D(&desc, &subResource, texture.GetAddressOf());
CHECK(hr);
DirectX::ScratchImage img;
::CaptureTexture(DEVICE.Get(), DC.Get(), texture.Get(), img);
// Save To File
hr = DirectX::SaveToDDSFile(*img.GetImages(), DirectX::DDS_FLAGS_NONE, Utils::ToWString(fileName).c_str());
CHECK(hr);
}
}
else
{
string originStr = (filesystem::path(_assetPath) / folderName / file).string();
Utils::Replace(originStr, "\\", "/");
string pathStr = (filesystem::path(saveFolder) / fileName).string();
Utils::Replace(pathStr, "\\", "/");
::CopyFileA(originStr.c_str(), pathStr.c_str(), false);
}
return fileName;
}
'C++ > DirectX 11' 카테고리의 다른 글
[DirectX] 애니메이션 - 스키닝, 데이터 (1) | 2024.10.02 |
---|---|
[DirectX] 모델 - 모델 띄우기, 계층 구조, ImGuI (1) | 2024.09.30 |
[DirectX] Lighting & Material - Material, Normal Mapping (0) | 2024.09.25 |
[DirectX] Lighting & Material - Global Shader, Depth Stencil View, 조명 (0) | 2024.09.24 |
[DirectX] DirectX 11 3D 입문 - Height Map, Normal, Mesh (0) | 2024.09.13 |