본문 바로가기
DirectX/[Inflearn_rookiss] Part2: DirectX12

35. Animation - 실습

by 헛둘이 2023. 2. 23.

애니메이션도 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

댓글