프로그래밍 공부
작성일
2023. 11. 16. 16:50
작성자
WDmil
728x90

애니메이션 리깅을 위해서는 FBX데이터로 애니메이션 데이터를 뽑아와야 한다.

 

https://www.mixamo.com/#/

 

Mixamo

 

www.mixamo.com

에서 에니메이션 데이터를 가져와보자.

우리는 모델 데이터가 존재하기 때문에 fbx로 뽑아온 데이터의 Skin데이터가 필요하지 않다.

 

그럼으로, 데이터에서 Skin데이터를 뺀 에니메이션 데이터만 가져온다.


 

리딩데이터를 불러오기위해 사전 설정을 기입한다.

 

모델의 움직임 데이터는 Texture2D형태로 DeviceContext에 전달되는데,

각 데이터는 다음과 같다.

한 칸 내부에 Float4, Float3형태로, 컬러값형태로 모션이 저장되는데.

3픽셀당 한개의 뼈대가 움직이는 데이터를 저장하게 된다.

 

즉, 세로길이는 프레임, 가로길이는 뼈대정보

로 해서 2D형태의 이미지로 DeviceContext에 전달되는것 이다.

 

이러한 데이터를 넘기기위해 FBX에서 데이터를 끌고와야한다.


FBX에서 데이터 가져오기.

 

우선, 원래 모델을 끌고오는 코드를 살펴보자.

 

모델의 각 본 데이터를 가져오기 위해 처리해야할 항목은 다음과 같다.

 

// NodeData 구조체는 모델의 노드(뼈대) 데이터를 저장하는 구조체입니다.
struct NodeData
{
	int index;         // 노드(뼈대)의 인덱스
	string name;       // 노드(뼈대)의 이름
	int parent;        // 부모 노드(뼈대)의 인덱스
	Matrix transform;  // 노드(뼈대)의 변환 행렬
};

// BoneData 구조체는 모델의 뼈대(Bone) 데이터를 저장하는 구조체입니다.
struct BoneData
{
	int index;         // 뼈대의 인덱스
	string name;       // 뼈대의 이름
	Matrix offset;     // 뼈대의 로컬 변환 행렬
};

// vertice의 보간값을 채우기 위한 데이터정보.
struct VertexWeights
{
	UINT indices[4] = {};
	float weights[4] = {}; // 가중치

	void Add(const UINT& index, const float& weight)
	{
		FOR(4)
		{
			if (weights[i] == 0.0f)
			{
				indices[i] = index;
				weights[i] = weight;
				return;
			}
		}
	}

	void Normalize()
	{
		float sum = 0.0f;

		FOR(4)
			sum += weights[i];

		// 표준편차로 1로 정규화한다.
		FOR(4)
			weights[i] /= sum;
	}
};

여기에서 보간값이란, 어떠한 뼈대와 다른 뼈대가 움직일 때, 모델링 특성상 vertice의 사이사이가 떨어지는 현상이 발생하는데,

 

그 떨어진 부분을 어떤 비율로 채워줄 것 인가를 의미한다.

 

위 구조체들을 사용해서 데이터를 뽑아보자.

    // 애니메이션(Animation) 관련 함수들

    // ReadClip: 애니메이션 클립을 읽어옵니다.
    Clip* ReadClip(aiAnimation* animation);

    // ReadKeyFrame: 클립의 키프레임을 읽어옵니다.
    void ReadKeyFrame(Clip* clip, aiNode* node, vector<ClipNode>& clopNodes);

    // WriteClip: 클립을 저장합니다.
    void WriteClip(Clip* clip, string clipName, UINT index);

private:
    // 애니메이션 관련 보조 함수들

    // SetClipNode: 키프레임 데이터와 프레임 수를 이용하여 ClipNode를 설정합니다.
    void SetClipNode(const KeyData& keyData, const UINT& frameCount, ClipNode& clipNode);

    // CalcInterpolationVector: 벡터 키프레임 데이터의 보간값을 계산합니다.
    Float3 CalcInterpolationVector(const vector<KeyVector>& keyData, UINT& count, int curFrame);

    // CalcInterpolationQuat: 쿼터니언 키프레임 데이터의 보간값을 계산합니다.
    Float4 CalcInterpolationQuat(const vector<KeyQuat>& keyData, UINT& count, int curFrame);

private:
    Assimp::Importer* importer;
    const aiScene* scene;
    // aiScene은 반드시 상수로 사용되어야 합니다.

    string name;

    vector<Material*> materials;
    vector<MeshData*> meshes;
    vector<NodeData*> nodes;
    vector<BoneData*> bones;

    map<string, UINT> boneMap;
    UINT boneCount = 0;
};

모델을 가져오는 코드뭉치에 위와같은 항목들을 추가하여 연산한다.

 

각 클립데이터와 Bone, Node데이터를 가져와서 데이터를 저장하고 사용해야한다.

 

클립은 Bone과 Node를 참고하여 모델을 움직이게 할 것 이다.

 

BoneMap은 Node와 bone에서 가져온 데이터를 사용해서 모델의 번호를 뽑아올 것 이다.

 

이제 mesh를 뽑아올 때, Bone정보도 같이 가져와보자.

// 뼈대에 대한 보간정보 뽑기.
void ModelExporter::ReadBone(aiMesh* mesh, vector<VertexWeights>& vertexWeights)
{
	FOR(mesh->mNumBones)
	{
		UINT boneIndex = 0;
		string name = mesh->mBones[i]->mName.C_Str();

		// 뼈대가 이미 있는지 확인
		if (boneMap.count(name) == 0)
		{
			boneIndex = boneCount++;
			boneMap[name] = boneIndex;

			BoneData* boneData = new BoneData();
			boneData->name = name;
			boneData->index = boneIndex;

			Matrix matrix(mesh->mBones[i]->mOffsetMatrix[0]);// 배열로 제공하기는 하지만, 통상적으로 맨 앞의 값만 쓰게 됨.
			boneData->offset = XMMatrixTranspose(matrix);

			bones.push_back(boneData);
		}
		else
		{
			boneIndex = boneMap[name];
		}

		for (UINT j = 0; j < mesh->mBones[i]->mNumWeights; j++)
		{
			UINT index = mesh->mBones[i]->mWeights[j].mVertexId;
			vertexWeights[index].Add(boneIndex,
				mesh->mBones[i]->mWeights[j].mWeight);
		}
	}
}

void ModelExporter::ReadNode(aiNode* node, int index, int parent)
{
	NodeData* nodeData = new NodeData();
	nodeData->index = index;
	nodeData->parent = parent;
	nodeData->name = node->mName.C_Str();

	Matrix matrix(node->mTransformation[0]);
	nodeData->transform = XMMatrixTranspose(matrix); // 메트릭스 변환해야함.

	nodes.push_back(nodeData);

	FOR(node->mNumChildren)
		ReadNode(node->mChildren[i], nodes.size(), index);
}

우선 각 뼈대와 Node값을 읽어오는 함수를 제작한다.

 

받아온 aiNode와 aiBone값을 사용해서 해당 데이터의 정보와, 상속관계를 정리한다.

 

모델은 scene에 Assimp를 사용해서 데이터가 저장됨으로, scene에서 데이터를 뽑아온다.

void ModelExporter::ExportMesh()
{
	ReadNode(scene->mRootNode, -1, -1);
	// rootnode가-1임을 기억하자.
	ReadMesh(scene->mRootNode);
	WriteMesh();
}

ExportMesh가 호출되면, node값을 가져올 수 있도록 한다.

 

-1일경우, 데이터가 가장 윗단계에 있다는 의미이다.

		// Mesh의 정점(Vertex) 데이터를 복사
		mesh->vertices.resize(srcMesh->mNumVertices);

		for(UINT v = 0; v < srcMesh->mNumVertices; v++)
		{
			ModelVertex vertex;

			// 정점 위치 정보를 복사
			memcpy(&vertex.pos, &srcMesh->mVertices[v], sizeof(Float3));

			// 텍스처 좌표 정보가 있는 경우, 복사
			if (srcMesh->HasTextureCoords(0))
				memcpy(&vertex.uv, &srcMesh->mTextureCoords[0][v], sizeof(Float2));

			// 법선 벡터 정보가 있는 경우, 복사
			if (srcMesh->HasNormals())
				memcpy(&vertex.normal, &srcMesh->mNormals[v], sizeof(Float3));

			// 접선 벡터 및 이접선 벡터 정보가 있는 경우, 복사
			if (srcMesh->HasTangentsAndBitangents())
				memcpy(&vertex.tangent, &srcMesh->mTangents[v], sizeof(Float3));

			if (!vertexWeights.empty())
			{
				vertexWeights[v].Normalize();
				
				vertex.indices.x = (float)vertexWeights[v].indices[0];
				vertex.indices.y = (float)vertexWeights[v].indices[1];
				vertex.indices.z = (float)vertexWeights[v].indices[2];
				vertex.indices.w = (float)vertexWeights[v].indices[3];

				vertex.weights.x = vertexWeights[v].weights[0];
				vertex.weights.y = vertexWeights[v].weights[1];
				vertex.weights.z = vertexWeights[v].weights[2];
				vertex.weights.w = vertexWeights[v].weights[3];
			}
			// MeshData의 정점 목록에 현재 정점 정보를 추가
			mesh->vertices[v] = vertex;
		}

		// Mesh의 인덱스 데이터를 복사 복사하기위해 배열을 resize로 늘려줌.
		mesh->indices.resize(srcMesh->mNumFaces * 3);
		for (UINT f = 0; f < srcMesh->mNumFaces; f++)
		{
			// face = 그리기순서 얼굴, 면 이라고 생각하면됨.
			aiFace& face = srcMesh->mFaces[f];

			// 각 면(Face)의 인덱스를 MeshData 객체의 인덱스 목록에 추가
			for (UINT j = 0; j < face.mNumIndices; j++)
			{
				mesh->indices[f * 3 + j] = face.mIndices[j];
			}
		}

		// MeshData 객체를 meshes 벡터에 추가
		meshes.push_back(mesh);
	}

	FOR(node->mNumChildren)
	{
		// 현재 노드의 자식 노드에 대해 재귀적으로 Mesh를 읽고 처리 메쉬는 트리형태로 되어있기에 재귀로 읽어야함.
		ReadMesh(node->mChildren[i]);
	}

 

mesh의 각 뼈대 데이터를 벡터값에 추가하면서, 상속관계에 제귀적으로 계속 호출하며 저장하게 된다.

void ModelExporter::ReadNode(aiNode* node, int index, int parent)
{
	NodeData* nodeData = new NodeData();
	nodeData->index = index;
	nodeData->parent = parent;
	nodeData->name = node->mName.C_Str();

	Matrix matrix(node->mTransformation[0]);
	nodeData->transform = XMMatrixTranspose(matrix); // 메트릭스 변환해야함.

	nodes.push_back(nodeData);

	FOR(node->mNumChildren)
		ReadNode(node->mChildren[i], nodes.size(), index);
}

Node데이터를 각 정점정보와 함께 상속관계로 저장하여, 데이터를 나열한다.

 

matrix는, 해당 node데이터가 가지고있는 Scail, Position, Rotation정보이다.

 

728x90