[DX] ExportMesh, BinaryFile/BinaryWriter (WriteMeshData), ReadBoneData, ReadMeshData
3D 모델로 제작된 탱크를 fbx 파일로 정점들을 읽은 후, .mesh 형식의 파일로 저장해보았다.
🚩 ExportFile
이전 게시글에 Assimp를 이용한 ReadFile 함수를 작성하였다.
2023.03.13 - [DirectX] - [DX] 3D 모델, Assimp
작성된 ReadFile 함수를 이용하여 aiScene 객체에 읽어들인 파일 정보를 저장해두었고, Export Mesh 함수를 작성하여 .mesh 형식으로 파일을 저장하는 기능을 구현해보았다.
3D 파일을 Export 하기위해선 모델의 Bone Data를 모두 읽어들인 후, 읽은 Data를 토대로 Mesh파일을 저장해야 한다.
📃 Converter.cpp - ReadBoneData
void Converter::ReadBoneData(aiNode* node, int index, int parent)
{
// TODO: 본 정보 읽기
asBone* bone = new asBone();
bone->Index = index;
bone->Parent = parent;
bone->Name = node->mName.C_Str();
Matrix transform(node->mTransformation[0]); // 시작 주소를 전달 == Trasform 복사
D3DXMatrixTranspose(&bone->Transform, &transform); // 하나씩 초기화한 Transform을 전체 Transform으로 확정
Matrix matParent;
if (parent < 0) // parent값이 음수라면 해당 node가 root인 것 == 부모가 없는 것
D3DXMatrixIdentity(&matParent);
else
matParent = bones[parent]->Transform;
bone->Transform = bone->Transform * matParent;
bones.push_back(bone);
// TODO: 메시 정보 읽기
ReadMeshData(node, index); // 각 노드가 가지고있는 Mesh 정보 불러오기
for (UINT i = 0; i < node->mNumChildren; i++)
ReadBoneData(node->mChildren[i], bones.size(), index);
}
따라서 ReadBoneData 함수를 먼저 작성하였다.
읽어들일 Bone Data를 추출하기 위해 Bone 정보를 담을 asBone 구조체 객체를 선언해주었고, RootNode부터 Bone 정보를 읽고, 해당 node의 Mesh 정보를 불러 읽었다.
이후에는 자식으로 내려가며 똑같이 Bone 데이터를 읽고, bones 배열에 추가해주었다.
📃 Converter.cpp - ReadMeshData
void Converter::ReadMeshData(aiNode* node, int bone)
{
if (node->mNumChildren < 1) return;
asMesh* mesh = new asMesh();
mesh->Name = node->mName.C_Str();
mesh->BoneIndex = bone;
for(UINT i = 0; i < node->mNumMeshes; i++)
{
UINT index = node->mMeshes[i];
aiMesh* srcMesh = scene->mMeshes[index];
aiMaterial* material = scene->mMaterials[srcMesh->mMaterialIndex];
mesh->MaterialName = material->GetName().C_Str();
UINT startVertex = mesh->Vertices.size(); // 각 Mesh마다 정점이 각각 존재
for(UINT v = 0; v < srcMesh->mNumVertices; v++)
{
Model::ModelVertex vertex;
memcpy(&vertex.Position, &srcMesh->mVertices[v], sizeof(Vector3));
if (srcMesh->HasTextureCoords(0))
memcpy(&vertex.Uv, &srcMesh->mTextureCoords[0][v], sizeof(Vector2));
if(srcMesh->HasNormals())
memcpy(&vertex.Normal, &srcMesh->mNormals[v], sizeof(Vector3));
mesh->Vertices.push_back(vertex);
}
for(UINT f = 0; f < srcMesh->mNumFaces; f++)
{
aiFace& face = srcMesh->mFaces[f]; // Mesh의 면을 Face라고 함
for (UINT k = 0; k < face.mNumIndices; k++)
mesh->Indices.push_back(face.mIndices[k] + startVertex);
}
meshes.push_back(mesh);
}
}
Mesh 데이터는 asMesh 구조체에 Name과 BoneIndex를 저장하고, 해당 Node에 존재하는 모든 Mesh들을 for문을 돌며 읽어들인다.
Mesh에 존재하는 Material 값도 함께 읽어들이며 각 Mesh별로 존재하는 정점들에 대한 for문을 돌리며 asMesh의 Vertices 정보에 추가하였다.
마지막으로 Mesh의 Face라고 불리는 각 면들에 대한 Index 정보를 읽어, asMesh의 Indices 정보에 추가하였다.
📃 Converter.cpp - WriteMeshData
void Converter::WriteMeshData(wstring savePath)
{
// Path::CreateFolder() : 해당 지정된 경로만 만들어준다.
// Path::CreateFolders() : 해당 경로까지 가는데 상위폴더가 없다면 상위폴더까지 만들어준다.
Path::CreateFolders(Path::GetDirectoryName(savePath));
BinaryWriter* w = new BinaryWriter();
w->Open(savePath);
w->UInt(bones.size());
for(asBone* bone : bones)
{
w->Int(bone->Index);
w->String(bone->Name);
w->Int(bone->Parent);
w->Matrix(bone->Transform);
SafeDelete(bone);
}
w->UInt(meshes.size());
for (asMesh* meshData : meshes)
{
w->String(meshData->Name);
w->Int(meshData->BoneIndex);
w->String(meshData->MaterialName);
w->UInt(meshData->Vertices.size());
w->Byte(&meshData->Vertices[0], sizeof(Model::ModelVertex) * meshData->Vertices.size());
w->UInt(meshData->Indices.size());
w->Byte(&meshData->Indices[0], sizeof(UINT) * meshData->Indices.size());
SafeDelete(meshData);
}
w->Close();
SafeDelete(w);
}
Bone 정보와 Mesh 정보까지 모두 읽은 후 마침내 WriteMeshData 함수를 작성하였다.
Path 클래스의 CreateForders 함수를 이용하여 매개변수로 받아온 path를 만들어주거나, 읽어들인다.
Write의 방법으로 BinaryWriter를 사용하였다.
💡 BinaryWriter
BinaryWriter는 .NET 에서 제공해주는 기본 이진 형식을 스트림에 쓰고 특정 인코딩으로 된 문자열 쓰기를 지원해주는 클래스이다. BinaryWrite 클래스는 스트림에 이진 데이터를 기록하고, BinaryReader 클래스는 스트림으로부터 이진 데이터를 읽는다.
즉, BinaryWriter, BinaryReader를 사용하면 알아서 데이터를 바이트 단위로 저장하고 읽는다.
문자열을 저장할 때는 문자열의 길이를 먼저 바이트에 저장해두고, 그 뒤부터 문자열 데이터를 저장한다.
예를 들어 bool 값을 BinaryWriter로 저장하면다면, 스트림에 1바이트 값으로 쓸 수 있다.
WriteMeshData 함수에서는 이러한 BinaryWriter 클래스를 이용하여 Bone의 Index, Name, Parent, Transform 정보를 각각 저장하고, Mesh의 Name, BoneIndex, MaterialName 등등 까지 저장하였다.
위 함수에서 사용된 BinaryWriter는 파일입출력을 편리하도록 만든 클래스이다.
📃 BinaryFile.cpp
#include "Framework.h"
#include "BinaryFile.h"
//////////////////////////////////////////////////////////////////////////
BinaryWriter::BinaryWriter()
: fileHandle(NULL), size(0)
{
}
BinaryWriter::~BinaryWriter()
{
}
void BinaryWriter::Open(wstring filePath, UINT openOption)
{
assert(filePath.length() > 0);
fileHandle = CreateFile
(
filePath.c_str()
, GENERIC_WRITE
, 0
, NULL
, openOption
, FILE_ATTRIBUTE_NORMAL
, NULL
);
bool isChecked = fileHandle != INVALID_HANDLE_VALUE;
assert(isChecked);
}
void BinaryWriter::Close()
{
if (fileHandle != NULL)
{
CloseHandle(fileHandle);
fileHandle = NULL;
}
}
void BinaryWriter::Bool(bool data)
{
WriteFile(fileHandle, &data, sizeof(bool), &size, NULL);
}
void BinaryWriter::Word(WORD data)
{
WriteFile(fileHandle, &data, sizeof(WORD), &size, NULL);
}
void BinaryWriter::Int(int data)
{
WriteFile(fileHandle, &data, sizeof(int), &size, NULL);
}
void BinaryWriter::UInt(UINT data)
{
WriteFile(fileHandle, &data, sizeof(UINT), &size, NULL);
}
void BinaryWriter::Float(float data)
{
WriteFile(fileHandle, &data, sizeof(float), &size, NULL);
}
void BinaryWriter::Double(double data)
{
WriteFile(fileHandle, &data, sizeof(double), &size, NULL);
}
void BinaryWriter::Vector2(const D3DXVECTOR2& data)
{
WriteFile(fileHandle, &data, sizeof(D3DXVECTOR2), &size, NULL);
}
void BinaryWriter::Vector3(const D3DXVECTOR3& data)
{
WriteFile(fileHandle, &data, sizeof(D3DXVECTOR3), &size, NULL);
}
void BinaryWriter::Vector4(const D3DXVECTOR4& data)
{
WriteFile(fileHandle, &data, sizeof(D3DXVECTOR4), &size, NULL);
}
void BinaryWriter::Color3f(const D3DXCOLOR& data)
{
WriteFile(fileHandle, &data, sizeof(D3DXCOLOR) - 4, &size, NULL);
}
void BinaryWriter::Color4f(const D3DXCOLOR& data)
{
WriteFile(fileHandle, &data, sizeof(D3DXCOLOR), &size, NULL);
}
void BinaryWriter::Matrix(const D3DXMATRIX& data)
{
WriteFile(fileHandle, &data, sizeof(D3DXMATRIX), &size, NULL);
}
void BinaryWriter::String(const string & data)
{
UInt(data.size());
const char* str = data.c_str();
WriteFile(fileHandle, str, data.size(), &size, NULL);
}
void BinaryWriter::Byte(void * data, UINT dataSize)
{
WriteFile(fileHandle, data, dataSize, &size, NULL);
}
//////////////////////////////////////////////////////////////////////////
BinaryReader::BinaryReader()
: fileHandle(NULL), size(0)
{
}
BinaryReader::~BinaryReader()
{
}
void BinaryReader::Open(wstring filePath)
{
assert(filePath.length() > 0);
fileHandle = CreateFile
(
filePath.c_str()
, GENERIC_READ
, FILE_SHARE_READ
, NULL
, OPEN_EXISTING
, FILE_ATTRIBUTE_NORMAL
, NULL
);
bool isChecked = fileHandle != INVALID_HANDLE_VALUE;
assert(isChecked);
}
void BinaryReader::Close()
{
if (fileHandle != NULL)
{
CloseHandle(fileHandle);
fileHandle = NULL;
}
}
bool BinaryReader::Bool()
{
bool temp = false;
ReadFile(fileHandle, &temp, sizeof(bool), &size, NULL);
return temp;
}
WORD BinaryReader::Word()
{
WORD temp = 0;
ReadFile(fileHandle, &temp, sizeof(WORD), &size, NULL);
return temp;
}
int BinaryReader::Int()
{
int temp = 0;
ReadFile(fileHandle, &temp, sizeof(int), &size, NULL);
return temp;
}
UINT BinaryReader::UInt()
{
UINT temp = 0;
ReadFile(fileHandle, &temp, sizeof(UINT), &size, NULL);
return temp;
}
float BinaryReader::Float()
{
float temp = 0.0f;
ReadFile(fileHandle, &temp, sizeof(float), &size, NULL);
return temp;
}
double BinaryReader::Double()
{
double temp = 0.0f;
ReadFile(fileHandle, &temp, sizeof(double), &size, NULL);
return temp;
}
D3DXVECTOR2 BinaryReader::Vector2()
{
float x = Float();
float y = Float();
return D3DXVECTOR2(x, y);
}
D3DXVECTOR3 BinaryReader::Vector3()
{
float x = Float();
float y = Float();
float z = Float();
return D3DXVECTOR3(x, y, z);
}
D3DXVECTOR4 BinaryReader::Vector4()
{
float x = Float();
float y = Float();
float z = Float();
float w = Float();
return D3DXVECTOR4(x, y, z, w);
}
D3DXCOLOR BinaryReader::Color3f()
{
float r = Float();
float g = Float();
float b = Float();
return D3DXCOLOR(r, g, b, 1.0f);
}
D3DXCOLOR BinaryReader::Color4f()
{
float r = Float();
float g = Float();
float b = Float();
float a = Float();
return D3DXCOLOR(r, g, b, a);
}
D3DXMATRIX BinaryReader::Matrix()
{
D3DXMATRIX matrix;
matrix._11 = Float(); matrix._12 = Float(); matrix._13 = Float(); matrix._14 = Float();
matrix._21 = Float(); matrix._22 = Float(); matrix._23 = Float(); matrix._24 = Float();
matrix._31 = Float(); matrix._32 = Float(); matrix._33 = Float(); matrix._34 = Float();
matrix._41 = Float(); matrix._42 = Float(); matrix._43 = Float(); matrix._44 = Float();
return matrix;
}
string BinaryReader::String()
{
UINT size = Int();
char* temp = new char[size + 1];
ReadFile(fileHandle, temp, sizeof(char) * size, &this->size, NULL); //데이터 읽기
temp[size] = '\0';
return temp;
}
void BinaryReader::Byte(void ** data, UINT dataSize)
{
ReadFile(fileHandle, *data, dataSize, &size, NULL);
}
🚩 Tank Mesh 저장하기
위와 같은 tank.fbx 파일을 읽은 후 .mesh 파일로 저장해보았다.
프로그램 실행 결과, 지정해두었던 path인 "_Models/Tank"에 Tank.mesh 파일이 생긴것을 확인할 수 있었다.
참조
https://learn.microsoft.com/ko-kr/dotnet/api/system.io.binarywriter?view=net-7.0
BinaryWriter 클래스 (System.IO)
기본 이진 형식을 스트림에 쓰고 특정 인코딩으로 된 문자열 쓰기를 지원합니다.
learn.microsoft.com