Assimp를 통해 FBX 파일의 루트 노드부터 재귀적으로 트리를 순회하며 Bone 정보를 가져옵니다. 현재는 스켈레톤보다 "계층 노드 = Bone" 이라고 가정하고 구조를 구성합니다.
📌 FBX의 Transform 정보는 Relative(부모 기준) 좌표계입니다. 따라서 부모의 Transform과 곱해 최종 월드 기준 좌표계로 전환해야 합니다.
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 → Transpose(전치) → Local Transform 계산
Matrix transform(node->mTransformation[0]);
bone->transform = transform.Transpose();
Matrix matParent = Matrix::Identity;
if (parent >= 0)
matParent = _bones[parent]->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);
}
각 노드는 하나 이상의 Mesh를 가질 수 있으며, 각 Mesh는 여러 개의 서브메쉬를 포함할 수 있습니다. 이들을 통합해 관리하며, 정점 오프셋을 고려한 인덱스 관리를 수행합니다.
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];
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++)
{
VertexType vertex;
::memcpy(&vertex.position, &srcMesh->mVertices[v], sizeof(Vec3));
if (srcMesh->HasTextureCoords(0))
::memcpy(&vertex.uv, &srcMesh->mTextureCoords[0][v], sizeof(Vec2));
if (srcMesh->HasNormals())
::memcpy(&vertex.normal, &srcMesh->mNormals[v], sizeof(Vec3));
mesh->vertices.push_back(vertex);
}
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);
}
모델의 구조가 방대하므로 XML이 아닌 바이너리 형태로 저장해 효율적인 로딩을 지원합니다. 이 과정은 FileUtils를 통해 추상화되어 있습니다.
void Converter::WriteModelFile(wstring finalPath)
{
shared_ptr<FileUtils> file = make_shared<FileUtils>();
file->Open(finalPath, FileMode::Write);
file->Write<uint32>(_bones.size());
for (auto& bone : _bones)
{
file->Write<int32>(bone->index);
file->Write<string>(bone->name);
file->Write<int32>(bone->parent);
file->Write<Matrix>(bone->transform);
}
file->Write<uint32>(_meshes.size());
for (auto& mesh : _meshes)
{
file->Write<string>(mesh->name);
file->Write<int32>(mesh->boneIndex);
file->Write<string>(mesh->materialName);
file->Write<uint32>(mesh->vertices.size());
file->Write(&mesh->vertices[0], sizeof(VertexType) * mesh->vertices.size());
file->Write<uint32>(mesh->indices.size());
file->Write(&mesh->indices[0], sizeof(uint32) * mesh->indices.size());
}
}
이제 저장된 .mesh 파일을 메모리로 불러오고 렌더링을 위한 컴포넌트 기반 시스템에 연결합니다.
Model 클래스에서 Load & Cache 수행ModelRenderer를 통해 Mesh + Material + Transform을 GPU에 전송🎮 실제 렌더링을 위한 주요 로직:
void ModelRenderer::Update()
{
if (_model == nullptr) return;
RENDER->PushTransformData(TransformDesc{ GetTransform()->GetWorldMatrix() });
for (auto& mesh : _model->GetMeshes())
{
if (mesh->material)
mesh->material->Update();
DC->IASetVertexBuffers(0, 1, mesh->vertexBuffer->GetComPtr().GetAddressOf(), &stride, &offset);
DC->IASetIndexBuffer(mesh->indexBuffer->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0);
_shader->DrawIndexed(0, _pass, mesh->indexBuffer->GetCount(), 0, 0);
}
}
FBX에 포함된 또는 외부 텍스처를 자동 복사하여 최종 텍스처 폴더로 이동시켜 줍니다. 텍스처가 내장되어 있는 경우 처리도 가능하지만, 일반적으로 외부 텍스처를 사용하는 것이 관리가 편리합니다.
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 == nullptr)
{
string originStr = (filesystem::path(_assetPath) / folderName / file).string();
string pathStr = (filesystem::path(saveFolder) / fileName).string();
Utils::Replace(originStr, "\\", "/");
Utils::Replace(pathStr, "\\", "/");
::CopyFileA(originStr.c_str(), pathStr.c_str(), false);
}
return fileName;
}