애니메이션도 FBXLoader 클래스의 LoadFbx 함수를 통해로 로드된다.
void FBXLoader::LoadFbx(const wstring& path)
{
// 파일 데이터 로드
Import(path);
// Animation
LoadBones(_scene->GetRootNode());
LoadAnimationInfo();
// 로드된 데이터 파싱 (Mesh/Material/Skin)
ParseNode(_scene->GetRootNode());
// 우리 구조에 맞게 Texture / Material 생성
CreateTextures();
CreateMaterials();
}
LoadBones 함수
void LoadBones(FbxNode* node) { LoadBones(node, 0, -1); }
void LoadBones(FbxNode* node, int32 idx, int32 parentIdx);
- 메모리에 올라온 FBX 파일로부터 Bone 데이터를 로드하는 함수
- 노드 하나만 받는 버전과, 자신의 인덱스, 부모의 인덱스를 받는 두 가지 버전으로 오버로딩 되어 있다.
- 이전 개념 시간에 언급했듯 Bone은 재귀 구조로 만들어져 있다. (어깨 -> 팔 -> 손)
- 루트 노드를 인자로 넘겨주면 모든 Bone을 DFS로 탐색한다.
void FBXLoader::LoadBones(FbxNode* node, int32 idx, int32 parentIdx)
{
FbxNodeAttribute* attribute = node->GetNodeAttribute();
if (attribute && attribute->GetAttributeType() == FbxNodeAttribute::eSkeleton)
{
shared_ptr<FbxBoneInfo> bone = make_shared<FbxBoneInfo>();
bone->boneName = s2ws(node->GetName());
bone->parentIndex = parentIdx;
_bones.push_back(bone);
}
const int32 childCount = node->GetChildCount();
for (int32 i = 0; i < childCount; i++)
LoadBones(node->GetChild(i), static_cast<int32>(_bones.size()), idx);
}
- FbxNodeAttribute로 현재 노드의 상태를 받아온다.
- 현재 노드의 타입이 eSkeleton 즉, 관절이면 bone 구조체를 만들어서 정보를 저장하고 _bones에 푸시한다.
- 그리고 현재 노드에 자식이 있다면 자식을 인자로 다시 LoadBones를 호출한다.
- 모든 트리를 순회하여 FbxBoneInfo 구조체의 배열인 _bones를 채운 후 반환한다.
LoadAnimationInfo 함수
void LoadAnimationInfo();
- 애니메이션의 갯수와 어떤 애니메이션이 있다 정도의 정보를 파싱해서 미리 저장해두는 함수
- 실제 프레임당 변화해야 할 행렬은 나중에 LoadAnimationData 함수에서 저장된다.
void FBXLoader::LoadAnimationInfo()
{
// 어떤 애니메이션이 있다 정도만 세팅해주는 것
// 애니메이션의 이름은 뭐고, StartTime, EndTime에 대한 정보등을 먼저 세팅한다.
_scene->FillAnimStackNameArray(OUT _animNames);
const int32 animCount = _animNames.GetCount();
for (int32 i = 0; i < animCount; i++)
{
FbxAnimStack* animStack = _scene->FindMember<FbxAnimStack>(_animNames[i]->Buffer());
if (animStack == nullptr)
continue;
shared_ptr<FbxAnimClipInfo> animClip = make_shared<FbxAnimClipInfo>();
animClip->name = s2ws(animStack->GetName());
animClip->keyFrames.resize(_bones.size()); // 키프레임은 본의 개수만큼
FbxTakeInfo* takeInfo = _scene->GetTakeInfo(animStack->GetName());
animClip->startTime = takeInfo->mLocalTimeSpan.GetStart();
animClip->endTime = takeInfo->mLocalTimeSpan.GetStop();
animClip->mode = _scene->GetGlobalSettings().GetTimeMode();
_animClips.push_back(animClip);
}
}
- FillAnimStackNameArray는 애니메이션 이름의 배열을 반환한다.
- 그 아래 for문은 그 이름의 갯수만큼 루프를 돌면서, 애니메이션의 이름을 참조하여 FbxAnimStack에 접근한다.
- FbxAnimStack은 FBX SDK에서 제공하는 애니메이션을 저장하는 클래스
- 애니메이션 스택이라고 하는데, 애니메이션엔 각각 이름이 지정되어 있으며, 애니메이션 데이터와 같이 저장되어 있다.
- FbxTakeInfo는 FBX SDK에서 제공하는 애니메이션의 레이어를 저장하는 클래스
- FbxTakeInfo를 통해 이름에 해당하는 애니메이션의 정보에 접근해서 사용자정의타입 animClip에 정보를 채워넣는다
- 모든 애니메이션 초기 등록 작업을 완료하고 함수가 종료된다.
이후 ParseNode에서 정점/재질/스킨에 대한 로드 작업이 이루어질 때,
메쉬를 처리하는 부분 마지막에 LoadAnimationData 함수를 통해 애니메이션의 실제 움직임에 대한 정보를 로드한다.
LoadAnimationData 함수
void FBXLoader::LoadAnimationData(FbxMesh* mesh, FbxMeshInfo* meshInfo)
- 위에 LoadAnimationInfo 함수에서 애니메이션에 대한 대략적인 정보만을 로드했다면,
- LoadAnimationData 함수에선 본격적으로 애니메이션에 대한 정보를 로드한다.
void FBXLoader::LoadAnimationData(FbxMesh* mesh, FbxMeshInfo* meshInfo)
{
const int32 skinCount = mesh->GetDeformerCount(FbxDeformer::eSkin);
if (skinCount <= 0 || _animClips.empty())
return;
meshInfo->hasAnimation = true;
for (int32 i = 0; i < skinCount; i++)
{
FbxSkin* fbxSkin = static_cast<FbxSkin*>(mesh->GetDeformer(i, FbxDeformer::eSkin));
if (fbxSkin)
{
FbxSkin::EType type = fbxSkin->GetSkinningType();
if (FbxSkin::eRigid == type || FbxSkin::eLinear == type)
{
const int32 clusterCount = fbxSkin->GetClusterCount();
for (int32 j = 0; j < clusterCount; j++)
{
FbxCluster* cluster = fbxSkin->GetCluster(j);
if (cluster->GetLink() == nullptr)
continue;
int32 boneIdx = FindBoneIndex(cluster->GetLink()->GetName());
assert(boneIdx >= 0);
FbxAMatrix matNodeTransform = GetTransform(mesh->GetNode());
LoadBoneWeight(cluster, boneIdx, meshInfo);
LoadOffsetMatrix(cluster, matNodeTransform, boneIdx, meshInfo);
const int32 animCount = _animNames.Size();
for (int32 k = 0; k < animCount; k++)
LoadKeyframe(k, mesh->GetNode(), cluster, matNodeTransform, boneIdx, meshInfo);
}
}
}
}
FillBoneWeight(mesh, meshInfo);
}
FbxDeformer - FBX SDK에서 스킨(Skin) 정보를 다루는 클래스
- 메시, 정점들에 대한 뼈대 연결 정보와 가중치 정보를 담고 있다.
FbxSkin - FBX SDK에서 스킨 바인딩을 다루는 클래스
- 스킨 바인딩은 캐릭터의 뼈대를 기준으로 메쉬의 각 정점이 어느 뼈대에 연결되는지를 결정한다.
- FbxSkin::EType은 스킨 바인딩 타입을 나타낸다.
FbxCluster - FBX SDK에서 뼈대와 스킨을 다루는데 사용되는 클래스
- 뼈대의 영향을 받는 정점들을 추적하고, 이들에 대한 가중치 정보를 계산한다.
- 스킨의 갯수만큼 for문을 돌면서, 각 정점이 하나 이상의 뼈대에 바인딩된다면
다시 클러스터 개수(뼈대와 스킨)만큼 for문을 돌면서 LoadBoneWeight 함수를 통해 뼈대의 가중치를 로드하고,
LoadOffsetMatrix 함수를 통해 Offset Matrix를 추출한다.
* Offset Matrix는 바인딩 포지션에서 관절 각각의 포지션으로 이동하기 위한 행렬
- 그리고 LoadKeyframe 함수를 통해 각 프레임에 곱해져야 할 ToRoot 행렬을 구해준다.
- 결국 이 함수는 FBX 파일로부터 로드된 데이터를 인자로 넘어온 meshInfo 내부에 값을 채워주기 위한 함수
- 이 값들을 다 채워주면 다시 meshInfo를 인자로 아래 FillBoneWeight 함수를 호출한다.
FillBoneWeight 함수
- 로드된 값을 이용해 실제로 연결된 다른 관절들의 인덱스와 그 인덱스에 대한 가중치를 채워주는 함수
using Pair = pair<int32, double>;
vector<Pair> boneWeights;
void FBXLoader::FillBoneWeight(FbxMesh* mesh, FbxMeshInfo* meshInfo)
{
const int32 size = static_cast<int32>(meshInfo->boneWeights.size());
for (int32 v = 0; v < size; v++)
{
BoneWeight& boneWeight = meshInfo->boneWeights[v];
boneWeight.Normalize();
float animBoneIndex[4] = {};
float animBoneWeight[4] = {};
const int32 weightCount = static_cast<int32>(boneWeight.boneWeights.size());
for (int32 w = 0; w < weightCount; w++)
{
animBoneIndex[w] = static_cast<float>(boneWeight.boneWeights[w].first);
animBoneWeight[w] = static_cast<float>(boneWeight.boneWeights[w].second);
}
memcpy(&meshInfo->vertices[v].indices, animBoneIndex, sizeof(Vec4));
memcpy(&meshInfo->vertices[v].weights, animBoneWeight, sizeof(Vec4));
}
}
- for문을 돌며 meshInfo에 저장된 boneWeight의 배열에서 하나씩 꺼내서 노멀라이즈 한 후,(가중치의 합이 1이 되게 한 후)
가중치의 사이즈만큼 돌며 Index, Weight를 채워준다. (범위는 항상 0~3 중 하나)
여기까지 끝나고나면 FBXLoader의 인스턴스에 애니메이션까지 로드가 된 상태가 된다.
이런 메쉬를 가진 인스턴스의 로드는 FBX로부터 데이터를 긁어오는 과정에서 시작하기 때문에,
Resources에서 LoadFBX를 만들어서 MeshData를 만드는 것에서 시작한다.
shared_ptr<MeshData> Resources::LoadFBX(const wstring& path)
{
wstring key = path;
shared_ptr<MeshData> meshData = Get<MeshData>(key);
if (meshData)
return meshData;
meshData = MeshData::LoadFromFBX(path);
meshData->SetName(key);
Add(key, meshData);
return meshData;
}
- MeshData는 파일 주소를 키로 삼아서 리소스가 저장되고, 1차적으로 이미 만들어졌는지 확인한다.
- 파일 데이터를 MeshData의 팩토리 함수 LoadFromFBX를 통해 불러온다.
shared_ptr<MeshData> MeshData::LoadFromFBX(const wstring& path)
{
FBXLoader loader;
loader.LoadFbx(path);
shared_ptr<MeshData> meshData = make_shared<MeshData>();
for (int32 i = 0; i < loader.GetMeshCount(); i++)
{
shared_ptr<Mesh> mesh = Mesh::CreateFromFBX(&loader.GetMesh(i), loader);
GET_SINGLE(Resources)->Add<Mesh>(mesh->GetName(), mesh);
// Material 찾아서 연동
vector<shared_ptr<Material>> materials;
for (size_t j = 0; j < loader.GetMesh(i).materials.size(); j++)
{
shared_ptr<Material> material = GET_SINGLE(Resources)->Get<Material>(loader.GetMesh(i).materials[j].name);
materials.push_back(material);
}
MeshRenderInfo info = {};
info.mesh = mesh;
info.materials = materials;
meshData->_meshRenders.push_back(info);
}
return meshData;
}
- loader의 LoadFbx 함수에서 저번 시간부터 이번 시간에 언급한 모든 작업을 진행한다.
- 하나의 FBX에 여러 개의 메쉬가 있을 수 있기 때문에,
LoadFromFBX 함수에서는 메쉬데이터가 몇 개의 메쉬를 가졌는지 확인하고, 그만큼 메쉬를 생성한 후
loader에서 Material을 뽑아와서 메쉬와 함께 MeshRenderInfo 구조체에 저장한다.
struct MeshRenderInfo
{
shared_ptr<Mesh> mesh;
vector<shared_ptr<Material>> materials;
};
- MeshRenderInfo는 메쉬가 렌더링되기 위해 필요한 데이터들을 저장하는 구조체다.
- 이 정보를 토대로 메쉬데이터로부터 Instantiate 함수를 호출해서 GameObject를 생성한다.
vector<shared_ptr<GameObject>> MeshData::Instantiate()
{
vector<shared_ptr<GameObject>> v;
for (MeshRenderInfo& info : _meshRenders)
{
shared_ptr<GameObject> gameObject = make_shared<GameObject>();
gameObject->AddComponent(make_shared<Transform>());
gameObject->AddComponent(make_shared<MeshRenderer>());
gameObject->GetMeshRenderer()->SetMesh(info.mesh);
for (uint32 i = 0; i < info.materials.size(); i++)
gameObject->GetMeshRenderer()->SetMaterial(info.materials[i], i);
if (info.mesh->IsAnimMesh())
{
shared_ptr<Animator> animator = make_shared<Animator>();
gameObject->AddComponent(animator);
animator->SetBones(info.mesh->GetBones());
animator->SetAnimClip(info.mesh->GetAnimClip());
}
v.push_back(gameObject);
}
return v;
}
'DirectX > [Inflearn_rookiss] Part2: DirectX12' 카테고리의 다른 글
34. Animation - 개념 (0) | 2023.02.22 |
---|---|
33. Mesh (0) | 2023.02.21 |
32. Picking (0) | 2023.02.20 |
31. Terrain (2) | 2023.02.20 |
30. Tessellation (0) | 2023.02.19 |
댓글