애니메이션 리깅을 위해서는 FBX데이터로 애니메이션 데이터를 뽑아와야 한다.
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정보이다.
'서울게임아카데미 교육과정 6개월 국비과정' 카테고리의 다른 글
20231121 34일차 모델의 애니메이션 선형보간 (0) | 2023.11.21 |
---|---|
20231117 33일차 모델의 애니메이션 리깅출력하기 (0) | 2023.11.17 |
20231115 31일차 모델의 본위치 받아와서 사용하기 (0) | 2023.11.15 |
20231114 30일차 함수포인터를 활용한 인벤토리 구축2 (0) | 2023.11.14 |
20231113 29일차 함수포인터를 활용한 인벤토리 구축. (0) | 2023.11.13 |