1. 3ds Max Exporter Plugin
게임에서는 FBX, OBJ 같은 확장자를 가진 파일을 직접 탑재해서 구동하지 않는다.
FBX, OBJ, ASE 같은 확장자의 모델링 된 파일을 가지고 해석하는 과정, 컨버팅 하는
작업은 미리 끝난, 해석된 정보를 갖고 있는 파일을 탑재한다.
그런 파일들을 편하게 만들기 위해서는 MAX SDK를 사용해서
자체적으로 맥스 익스포터 (3ds Max Exporter Plugin) 제작을 한다.
대용량 데이터 저장하는 xml이나 json, 또는 본인만의 방식으로 저장한다.
이렇게 제작하는 하는 사람, 제작하는 회사마다 저장하는 방식이 다르다면
저장된 방식을 알아야 읽어드릴 수 있다.
2. Assimp
가장 많이 쓰이는 Importing 라이브러리이다. Open Asset Import Library라는 의미로,
거의 모든 모델의 데이터를 Assimp가 생성한 데이터 구조로 불러오고 보낼 수가 있다.
https://github.com/begla/Intrinsic/issues/14
Assimp을 사용함으로써 이점은 무료 라이선스와 다양한 모델 형식을 지원한다는 점,
또한 오픈 소스여서 지원하는 문서도 다양하다.
반면에, 이번에 사용할 FBX SDK는 라이선스도 제한적이며, 공식문서도 불친절하다고 한다.
그럼에도 불구하고 내가 FBX SDK를 사용하려는 이유는
게임은 대부분 FBX 파일 형식을 사용하는데, Assimp는 FBX 파일 형식에서
기능이 완전하지 않고 FBX SDK가 좀더 FBX에 전문적이기 때문이다.
예를 들어, FBX 애니메이션에서 정점별 애니메이션을 지원하지 않는다고 한다.
3. FBX SDK
https://dlemrcnd.tistory.com/76
이전 글에서는 Obj 확장자 파일을 파일 입출력으로 읽어와 렌더 했다.
FBX SDK 사용하여 직접 파일 입출력을 하지 않아도, 메모리에 다 올려준다.
Direct Toolkit의 텍스쳐 로더 (WICTextureLoader)처럼 PNG, JPG 포맷에 규칙을 몰라도
라이브러리에서 다 해결해주는 것처럼 말이다.
FBX SDK 환경구성을 위한 링크이다. 2020.2.1 버전으로 사용한다.
설치
https://www.autodesk.com/developer-network/platform-technologies/fbx-sdk-2020-0
공식문서
https://help.autodesk.com/view/FBX/2020/ENU/
FBX 파일을 FBX Importer가 FBX Scene에 불러와서 트리를 구성한다.
FBX Scene은 N개의 트리로 이루워져 있다. (트리의 자식의 개수가 N개수)
씬에는 트리 구조에는 모델의 정보를 가지고 있고
전 순회해서 원하는 정보를 가져오면 된다. 그 정보를 가지고
본인 엔진에 맞는 렌더링 데이터로 바꾸면 된다.
대략적인 순서는 다음과 같다.
1. FBX Manager, FBX Scene, FBX Importer 생성
2. FBX Importer에게 불러올 파일명을 전달 (초기화)
3. FBX Importer에게 생성한 FBX Scene을 전달, (씬에다가 모델을 풀어놓음)
4. Rootnode부터 순회하면서 원하는 정보를 얻는다. (Vertex, Normal, Tangent..)
환경설정 : <fbxsdk.h>, libfbxsdk-md.lib;, libxml2-md.lib;, zlib-md.lib;
KFbxLoader.h
#include <fbxsdk.h>//헤더 추가
class KFbxLoader
{
public:
FbxManager* m_pFbxManager;
FbxImporter* m_pFbxImporter;
FbxScene* m_pFbxScene;
.
.
fbxsdk.h 를 추가하고 FBX Manager, FBX Scene, FBX Importer 포인터 변수 선언
KFbxLoader.cpp
bool KFbxLoader::Init()
{
m_pFbxManager = FbxManager::Create();
m_pFbxImporter = FbxImporter::Create(m_pFbxManager, "");
m_pFbxScene = FbxScene::Create(m_pFbxManager, "");
return true;
}
..
bool KFbxLoader::Release()
{
m_pFbxScene->Destroy();
m_pFbxImporter->Destroy();
m_pFbxManager->Destroy();
return true;
}
1번 순서처럼, Manager를 생성하고 매니저로 임포터와 씬을 생성한다.
해제는 그 반대의 순서로 해제해준다.
bool KFbxLoader::Load(std::wstring filename)
{
//파일 - > 임포터 -> 씬 (트리) 해석 -> 매쉬 -> 정보 가져오기 pnct -> 출력
std::string temp = to_wm(filename);
bool bRet = m_pFbxImporter->Initialize(temp.c_str()); //파일명 넘김
bRet = m_pFbxImporter->Import(m_pFbxScene);
FbxAxisSystem::MayaZUp.ConvertScene(m_pFbxScene); //마야 Z축 버젼 사용
if (bRet)
{
//fbx는 트리 구조로 이어져있음
//재귀호출로 전 순회 가능, N 트리여서 자식 수를 알아야함
//N트리 : 자식 개수가 N개임
m_pRootNode = m_pFbxScene->GetRootNode();
//전 순회해서 씬에 저장되어 있는 트리구조에서 오브젝트를 찾아옴
NodeProcess(nullptr, m_pRootNode);
//N트리에서 찾아낸 오브젝트를 해석함
for (int iobj = 0; iobj < m_ObjectList.size(); iobj++)
{
ParseMesh(m_ObjectList[iobj]);
}
return true;
}
return false;
}
Fbx Importer에게 파일명을 넘긴다. 그리고 그걸 FbxScene에서 넘겨주는 모습이다.
결과가 bRet에 저장해서 잘 불러왔으면 씬에 있는 모델의 정보를 가져온다.
트리 구조이기 때문에, 루트 노드부터 재귀 함수를 통해 오브젝트 개수와 종류를 알아온다.
NodeProcess가 그 함수이다.
void KFbxLoader::NodeProcess(FbxNode* pParentNode, FbxNode* pNode)
{
// 카메라나 라이트 등 매쉬가 아니라면 리턴
/*if (pNode->GetCamera() || pNode->GetLight())
{
return;
}*/
//매쉬타입 이니까 매쉬를 얻어옴
FbxMesh* pMesh = pNode->GetMesh();
if (pMesh)
{
KFBXObj* fbx = new KFBXObj;
fbx->m_pFbx_ParentNode = pParentNode;
fbx->m_pFbx_ThisNode = pNode;
m_ObjectList.push_back(fbx);
}
int iNumChild = pNode->GetChildCount();
for (int iNode = 0; iNode < iNumChild; iNode++)
{
FbxNode* child = pNode->GetChild(iNode);
NodeProcess(pNode, child);
}
}
카메라, 라이트 들도 예외처리를 해야 하지만,
우선 카메라와 라이트가 없는 파일이라는 전제로 진행한다.
매쉬 타입이라면, 오브젝트를 생성하고 오브젝트 리스트에 저장한다.
N트리라고 했듯이, 자식의 N개이기 때문에, GetChildCount() 함수를 통해
노드의 자식수를 알 수 있다. 재귀 함수로 모든 노드를 탐색한다.
//N트리에서 찾아낸 오브젝트를 해석함
for (int iobj = 0; iobj < m_ObjectList.size(); iobj++)
{
ParseMesh(m_ObjectList[iobj]);
}
return true;
사실 FBX 파일의 PNCT(position, normal, color, texture) 이 외에도 애니메이션 등등..
너무 정보가 방대하고, 각각 해줘야 할 처리가 다르기 때문에
우선 Vertex 위치만 하고 나중에 천천히 정리해보겠다.
위의 함수 NodeProcess로 모든 노드를 탐색해서 얻은 오브젝트를
해석하는 함수 PraseMesh로 이제 본인이 원하는 정보를 얻어오면 된다.
//현재 노드의 매쉬를 만듬, 버텍스 PNCT를 채워줘야함
FbxMesh* pFbxMesh = pObject->m_pFbx_ThisNode->GetMesh();
아까 NodeProcess에서 저장한 매쉬를 가져온다.
FBXMesh는 Vertex, Edge, Polygon 등 다양한 정보를 담고 있다.
// 폴리곤, 면 개수 만큼 돌면서 위치를 저장
// 삼각형, 사각형
int iCurpolyIndex = 0; // 증가되는 폴리곤 인덱스
int iNumPolyCount = pFbxMesh->GetPolygonCount(); //폴리곤 수
FbxVector4* pVertexPositions = pFbxMesh->GetControlPoints(); //정점 위치
int iNumFace = 0;
for (int iPoly = 0; iPoly < iNumPolyCount; iPoly++)
{
int iPolySize = pFbxMesh->GetPolygonSize(iPoly); //4또는 3 삼각형이나 사각형이냐
iNumFace = iPolySize - 2; // 한면
for (int iFace = 0; iFace < iNumFace; iFace++)
{
int VertexIndex[3] = { 0, iFace + 2, iFace + 1 };
int CornerIndex[3];
CornerIndex[0] = pFbxMesh->GetPolygonVertex(iPoly, VertexIndex[0]);
CornerIndex[1] = pFbxMesh->GetPolygonVertex(iPoly, VertexIndex[1]);
CornerIndex[2] = pFbxMesh->GetPolygonVertex(iPoly, VertexIndex[2]);
for (int iIndex = 0; iIndex < 3; iIndex++)
{
PNCT_VERTEX Vertex;
// Max(x,z,y) ->(dx)x,y,z
FbxVector4 v = pVertexPositions[CornerIndex[iIndex]];
v = mat_Geo.MultT(v); // 로컬 좌표로 행렬 곱
Vertex.pos.x = v.mData[0];
Vertex.pos.y = v.mData[2];
Vertex.pos.z = v.mData[1];
FbxMesh 함수, GetPolygonCount(), GetControlPoints(), GetPolygonSize, GetPolygonVertex()등
여러 함수로 원하는 정보를 파싱 할 수 있다. 폴리곤은 한 벽을 의미한다. 트라이 앵글로 이루어진 면은
폴리곤 하나당 페이스가 두 개 있다고 할 수 있다.
iNumFace = iPolySize - 2;으로 만약에 폴리 사이즈가 3이면 삼각형으로 면이 하나 있다는 것이다.
폴리 사이즈가 4개라면 면이 2개라는 뜻, 우리의 Primitive는 삼각형이기 때문이다.
맥스는 오른손 좌표계, DirectX는 왼손 좌표계여서,
두 번째 하고 세 번째 인덱스가 바뀌어 있다. 또한 맥스는 X, Z, Y로 저장되어 있다.
그리고 mData가 0번이 x축 1번 z 축 2번 y 축이여서 순서를 바꿔서 대입한다.
이거는 텍스쳐 축, UV 축도 마찬가지다. U는 같지만 V는 다르다.
왼쪽 하단이 원점이다.
이런 식으로 Mesh의 정보를 꺼내 주는 내장 함수를 통해
원하는 UV값, 노말, 탄젠트, 바이 노말, 애니메이션 등
렌더링 할 수 있는 데이터로 변환해야 한다.
결과
아래는 FBX SDK를 이용한 탄젠트 공간 생성 포스팅이다.