AssimpTool로 가서 툴부터 건드려 보도록 한다.
1. Animation 파일과 Mesh파일 가져오기
본격적으로 뼈대가 있는 휴먼 모양의 오브젝트를 파싱 한다.
수업 자료의 Resources\Assets\Kachujin을 복붙 해준다.

애니메이션이 3개, 메쉬가 1개 들어가 있다.
Mesh를 visual studio에 드래그 앤 드롭해서 살펴볼 수 있다.


이 이미지들이 자동적으로 추출이 된다는 것을 알 수 있다.
설령 추출이 안된다고 해도, 코드에 fbx파일 내에 이미지 파일이 있으면 추출하는 것을 넣어 놨었다.
Converter::WriteTexture를 보면
const aiTexture* srcTexture = _scene->GetEmbeddedTexture(file.c_str());
if (srcTexture) // fbx에 텍스쳐가 들어가 있는 경우
{
string pathStr = saveFolder + fileName;
이 if문 안에서 처리해 주게 될 것이다.
2. Converter::ReadSkinData로 스킨데이터를 추출하기
Converter에서 내용을 하나씩 넣어 볼 것이다.
스킨과 관련된 정보를 추출하는 것부터 시작을 해 볼 것이다.
1) ReadSkinData함수를 선언하고, ExportModelData에서 ReadModelData 다음에 호출하기
Converter.h에
void ReadSkinData();
를 선언한다.
이 함수를 만들면서 스킨 데이터를 추출하는 동시에 어떤 역할을 하는지도 겸사겸사 알 수가 있을 것이다.
ReadSkinData는 ModelData와 관련이 있기 때문에 ExportModelData를 할 때 ReadModelData를 한 다음에 ReadSkinData를 호출해서 실제로 정보를 여기서 꺼내서 작업하는 식으로 할 것이다.
void Converter::ExportModelData(wstring savePath)
{
wstring finalPath = _modelPath + savePath + L".mesh"; // .data, .model 로 해도 상관 없다. 하나 골라서 사용하면 된다.
ReadModelData(_scene->mRootNode, -1, -1);
ReadSkinData();
WriteModelFile(finalPath);
}
2) blendIndices와 blendWeights가 들어간 struct VertexTextureNormalTangentBlendData 확인하기
Skin이란 정보는 정점마다 어떤 뼈에 영향을 받아가지고 움직일 것이냐를 나타낸다고 볼 수 있는 건데
사실은 예전에 오늘을 위해 VertexData.h에
struct VertexTextureNormalTangentBlendData
{
Vec3 position = { 0, 0, 0 };
Vec2 uv = { 0, 0 };
Vec3 normal = { 0, 0, 0 };
Vec3 tangent = { 0, 0, 0 };
Vec4 blendIndices = { 0, 0, 0, 0 };
Vec4 blendWeights = { 0, 0, 0, 0 };
};
blendIndices와 blendWeights를 넣어줬었다.
blendIndices는 뼈대의 번호,
blendWeights는 그 뼈대에 몇 %를 적용받을 것이냐 비율을 얘기하는 것이다.
스키닝을 할 때는 정점에 연관성이 있는 뼈대의 정보를 기입을 해준다.
이 VertexTextureNormalTangentBlendData는 VS의 input 인자로 들어가기 때문에
받아준 부분에서 어떤 뼈대 영향을 받는지를 알고 있기 때문에 그 연산을 우리가 해줄 준비를 미리 해줄 수 있다는 얘기다.
결국 Vertex정보에 나랑 관련 있는 뼈대의 정보를 넣어준다가 핵심이다.
그걸 하기 위해 fbx파일에서 찾아줘야 한다.
bone을 하나씩 순회해가지고, 연관된 정점과 가중치 정보를 구할 것이다.
assimp library의 경우 그 정보가 뼈에 들어가 있다. vertex가 아닌 뼈에 들어가 있으니까 그 뼈에 있는 정보를 추출해서 정점에 파싱을 해 가지고 붙여 놓는 작업을 일단 해줘야 한다.
3) Converter::GetBoneIndex 헬퍼 함수 만들기
GetBoneIndex헬퍼 함수도 만들어 줘야 한다.
private:
uint32 GetBoneIndex(const string& name);
uint32 Converter::GetBoneIndex(const string& name)
{
for (shared_ptr<asBone>& bone : _bones)
{
if (bone->name == name)
return bone->index;
}
assert(false);
return 0;
}
4) AsTypes.h에 VertexId와 Weight를 받아 관리하기 위한 자료구조 asBlendWeight와 asBoneWeights 만들기
VertexId와 Weight를 받아줄 관리하기 위한 자료구조를 만들어 놓지 않았다.
AsTypes.h에 Animation과 관련된 애들을 추가해 주도록 한다.
// Animation
// 정점마다 -> (관절번호, 가중치)
struct asBlendWeight
{
void Set(uint32 index, uint32 boneIndex, float weight)
{
float i = (float)boneIndex;
float w = weight;
switch (index)
{
case 0: indices.x = i; weights.x = w; break;
case 1: indices.y = i; weights.y = w; break;
case 2: indices.z = i; weights.z = w; break;
case 3: indices.w = i; weights.w = w; break;
}
}
Vec4 indices = Vec4(0, 0, 0, 0); // 쉐이더에 넘겨줄 때도 Vec4로 받아줄 것이기 때문
Vec4 weights = Vec4(0, 0, 0, 0);
};
struct asBoneWeights
{
void AddWeights(uint32 boneIndex, float weight)
{
if (weight <= 0.0f)
return;
auto findIt = std::find_if(boneWeights.begin(), boneWeights.end(),
[weight](const Pair& p) { return weight > p.second; }); // 가장 처음으로 weight이 다른 애 보단 큰 경우 return
// (1, 0.4) (2, 0, 0.2) 이렇게 있을 때 (5, 0.5)를 넣는다 하면
// (5, 0.5) (1, 0.4) (2, 0, 0.2) 이렇게 앞에 들어가게 된다. 가중치가 높은 애들을 앞에 배치하기 위해서 이렇게 해줬다.
boneWeights.insert(findIt, Pair(boneIndex, weight));
}
// `asBoneWeights` 구조체는 뼈 인덱스와 가중치 정보를 pair<int32, float>의 형태로 관리합니다.
// 이 정보는 `asBlendWeight` 구조체를 통해 쉐이더에 호환되는 Vec4 형식으로 변환되어 전달될 예정입니다.
// 이 변환 과정은 쉐이더에서 데이터를 효율적으로 사용하기 위해 필요합니다.
asBlendWeight GetBlendWeights()
{
asBlendWeight blendWeights;
for (uint32 i = 0; i < boneWeights.size(); i++)
{
if (i >= 4)
break;
blendWeights.Set(i, boneWeights[i].first, boneWeights[i].second);
}
return blendWeights;
}
// 만약에 모든 정점들이 뼈대 4개의 영향을 받으면 좋겠지만, 어떤 애는 하나에만 영향을 받고, 어떤 애는 2개에만 영향을 받게 될 것이다.
// 결국 최종적인 모든 애들의 합을 1로 맞춰주기 위해 연산을 해준다.
// (1, 0.3) (2, 0.2) 이 경우
// (1, 0.6) (2, 0.4) 로 해서 최종적인 합을 1로 구해주겠다가 핵심이다. 퍼센티지라고 보면 된다.
void Normalize()
{
if (boneWeights.size() >= 4)
boneWeights.resize(4);
float totalWeight = 0.f;
for (const auto& item : boneWeights)
totalWeight += item.second;
float scale = 1.f / totalWeight;
for (auto& item : boneWeights)
item.second *= scale;
}
using Pair = pair<int32, float>; // boneIndex, weight
vector<Pair> boneWeights; // 나중엔 4개 이상이면 정렬해서 가중치 높은 순서로 커트할 것이다.
};
AddWeights를 해서 가중치를 넣어준 다음에
Normalize 해줘 가지고 정리를 해주고
GetBlendWeight로 치환한 다음에
이 아이를 이용해서 최종적으로 indices랑 weights를 갖고 있으면 된다가 결론이다.
5) Converter::ReadSkinData() 구현하기
이게 스키닝과 관련된 부분에서 핵심이 되는 부분이었고,
이걸 이용해서 Converter::ReadSkinData를 구현해보면
// TODO에 있는 부분이 결국에는 가중치와 관련된 부분들을 파싱 해서 들고 있어야 하는 것이기 때문에 vector<asBoneWeights>를 만들어 들고 있도록 할 것이다.
그리고 vector<shared_ptr<asMesh>> _meshes에 넣어 줄 것이다.
void Converter::ReadSkinData()
{
for (uint32 i = 0; i < _scene->mNumMeshes; i++)
{
aiMesh* srcMesh = _scene->mMeshes[i];
if (srcMesh->HasBones() == false)
continue;
shared_ptr<asMesh> mesh = _meshes[i]; // aiMesh와 매칭을 해줘서 정보를 긁어 놓는 목표로 한다.
vector<asBoneWeights> tempVertexBoneWeights; // vertex마다 갖고 있는 boneWeight을 갖고 있는 벡터다.
tempVertexBoneWeights.resize(mesh->vertices.size()); // Mesh의 vertex갯수 만큼 맞춰놓는다.
// Bone을 순회하면서 연관된 VertexId, Weight를 구해서 기록한다.
for (uint32 b = 0; b < srcMesh->mNumBones; b++)
{
aiBone* srcMeshBone = srcMesh->mBones[b];
uint32 boneIndex = GetBoneIndex(srcMeshBone->mName.C_Str());
for (uint32 w = 0; w < srcMeshBone->mNumWeights; w++)
{
// assimp에 따라 들어가 있던 정보
uint32 index = srcMeshBone->mWeights[w].mVertexId;
float weight = srcMeshBone->mWeights[w].mWeight;
// todo
tempVertexBoneWeights[index].AddWeights(boneIndex, weight);
}
}
// 최종 결과 계산
// 정점에 있는 뼈의 번호를 정리
for (uint32 v = 0; v < tempVertexBoneWeights.size(); v++)
{
tempVertexBoneWeights[v].Normalize();
asBlendWeight blendWeight = tempVertexBoneWeights[v].GetBlendWeights();
mesh->vertices[v].blendIndices = blendWeight.indices;
mesh->vertices[v].blendWeights = blendWeight.weights;
}
}
}
ReadSkinData에서 하는 기능은 어려운 부분은 없고 단순한데 mesh의 모든 bone들을 다 순회하면서
bone의 이름을 이용해 boneIndex를 구하고,
그 bone에게 영향을 받은 vertex를 순회하면서 그 vertexId와 weight를 추출해
vector<asBoneWeights> tempVertexBoneWeights에 채워주었고,
모든 정점을 순회하며 거꾸로 그것을 mesh에 아직 채워지지 않았던
struct VertexTextureNormalTangentBlendData
{
Vec3 position = { 0, 0, 0 };
Vec2 uv = { 0, 0 };
Vec3 normal = { 0, 0, 0 };
Vec3 tangent = { 0, 0, 0 };
Vec4 blendIndices = { 0, 0, 0, 0 };
Vec4 blendWeights = { 0, 0, 0, 0 };
};
blendIndices와 blendWeights를 채워주고 있다.
코드로만 보면 이해가 어려우니 중단점을 찍어가며 디버깅을 해보는 게 도움이 많이 된다.
6) Main에서 desc.app을 AssimpTool로 세팅하기
AssimpTool\Main\Main.cpp에서
desc.app = make_shared<ImGuiDemo>();
이건 이제 필요 없으니 삭제하고
desc.app = make_shared<AssimpTool>();
이걸로 바꾼다.
7) AssimpTool::Init에서 인간형 AssetFile로 ReadAssetFile, ExportMaterialData, ExportModelData 해주기
AssimpTool::Init에서 Tank와 Tower를 삭제한다.
void AssimpTool::Init()
{
{
shared_ptr<Converter> converter = make_shared<Converter>();
converter->ReadAssetFile(L"Kachujin/Mesh.fbx");
converter->ExportMaterialData(L"Kachujin/Kachujin");
converter->ExportModelData(L"Kachujin/Kachujin");
}
}
이렇게 해주면 지난번에 했던 Tank, Tower때와 거의 똑같이 작동을 하게 되는데
이번엔 ExportModelData를 하는 순간
void Converter::ExportModelData(wstring savePath)
{
wstring finalPath = _modelPath + savePath + L".mesh"; // .data, .model 로 해도 상관 없다. 하나 골라서 사용하면 된다.
ReadModelData(_scene->mRootNode, -1, -1);
ReadSkinData();
WriteModelFile(finalPath);
}
이렇게 ReadSkinData가 들어가게 될 것이니까
ReadSkinData에서 어떤 식으로 동작하게 될지 볼 수 있다.
ReadSkinData 첫 줄에 breakPoint를 잡고 실행을 해본다.
mesh가 하나밖에 없고, T포즈로 되어 있는 인간형 메쉬다.
Bone이 있으니까 하나씩 순회를 할 것이고,
각 vertex와 연관성 있는 정점(Bone)번호, 가중치가 구해질 것이고, 넣어지고,
최종적으로 연산이 끝나면

결국 ReadSkinData를 통해서 어떻게 생겼는지, 뼈대가 어떻게 붙어있는지를 대략적으로 알 수 있다.
8) Converter::ExportModelData에서 ReadSkinData의 결과를 엑셀 파일로 출력하기
경우에 따라 이걸 Text나 XML 형태로 만들어 주는 경우가 많은데
편하게 보게 해주는 방법 중 하나가 csv로 만들어 주는 건데 그게 엑셀 파일의 포맷이다.
모든 정점들의 블렌딩 비율을 추출해서 디버깅을 통해 빨리 판단할 수 있다.
나중에 문제가 일어나서 원인을 찾을 때는 자료가 많을수록 좋기 때문에 그럴 때를 대비해 넣어주는 것을 고려할 수 있다.
void Converter::ExportModelData(wstring savePath)
{
wstring finalPath = _modelPath + savePath + L".mesh"; // .data, .model 로 해도 상관 없다. 하나 골라서 사용하면 된다.
ReadModelData(_scene->mRootNode, -1, -1);
ReadSkinData();
//Write CSV File
{
FILE* file;
::fopen_s(&file, "../Vertices.csv", "w");
for (shared_ptr<asBone>& bone : _bones)
{
string name = bone->name;
::fprintf(file, "%d,%s\\n", bone->index, bone->name.c_str());
}
::fprintf(file, "\\n");
for (shared_ptr<asMesh>& mesh : _meshes)
{
string name = mesh->name;
::printf("%s\\n", name.c_str());
for (UINT i = 0; i < mesh->vertices.size(); i++)
{
Vec3 p = mesh->vertices[i].position;
Vec4 indices = mesh->vertices[i].blendIndices;
Vec4 weights = mesh->vertices[i].blendWeights;
::fprintf(file, "%f,%f,%f,", p.x, p.y, p.z);
::fprintf(file, "%f,%f,%f,%f,", indices.x, indices.y, indices.z, indices.w);
::fprintf(file, "%f,%f,%f,%f\\n", weights.x, weights.y, weights.z, weights.w);
}
}
::fclose(file);
}
WriteModelFile(finalPath);
}
원리는 단순하다. Vertices.csv파일을 넣어주고,
고전적인 방식으로 fopen을 하고, fprintf를 해서 원하는 것을 하나씩 넣어주고 있는 것이다.
넣어준 순서를 보면 모든 뼈의 목록을 다 넣어준다.
정점들에 대한 정보를 싹 넣어준다.
x, y, z 좌표랑
bone의 index번호
weight번호
순서로 넣어줬다.
실행을 해서 문제없이 끝나면
솔루션파일이 있는 위치에 Vertices.csv파일이 있는 것을 볼 수 있다.
3. 맺음말
핵심적인 부분을 이해하면 된다.
결국 모든 정점이라는 거 자체가 그냥 덩그러니 있는 게 아니라 어떤 관절에 붙어있다는 게 스키니 기법이었고,
실제로 애니메이션을 틀 때는 관절만 움직이게 될 것이지만 그럼에도 그 관절을 따라서 정점들이 움직이는 것이기 때문에 살을 뼈대에 붙여주는 것이 스키닝이고 그걸 하기 위해서 우리가 정해주는 게 아니라 애당초 vertex와 관련된 정보는 정점들이 어떠한 뼈에 붙어 있어야 하는데 그 관절을 비율로 섞어 줘서 엮어 줄 수가 있는데 그 부분이 파일에 들어가 있다.
파싱 해가지고 Converter::ReadSkinData에서
mesh->vertices[v].blendIndices = blendWeight.indices;
mesh->vertices[v].blendWeights = blendWeight.weights;
이렇게 넣어줬다는 걸 알 수 있다.
사소한 부분은 assimp와 관련된 부분이니 복붙을 해도 상관이 없고,
for (uint32 v = 0; v < tempVertexBoneWeights.size(); v++)
{
tempVertexBoneWeights[v].Normalize();
asBlendWeight blendWeight = tempVertexBoneWeights[v].GetBlendWeights();
mesh->vertices[v].blendIndices = blendWeight.indices;
mesh->vertices[v].blendWeights = blendWeight.weights;
}
Converter::ReadSkinData 이 부분만 이해하면 된다.
이 다음 순서는 애니메이션과 관련된 정보를 추출할 것이다.
애니메이션 정보는 정점과는 상관이 없고 관절이랑 상관이 있다고 했다. 관절이 움직이는 무엇인가를 해주는 그 부분을 처리 한 다음에 blendIndices, blendWeights랑 엮어서 관절의 위치에 따라 정점에 따라 갖고 있는 BoneIndex와 Weight를 이용해 적절히 섞어 주는 것을 나중에 쉐이더에 만들게 된다고 보면 된다.
다음 시간은 애니메이션 파싱으로 넘어간다.
'DirectX' 카테고리의 다른 글
| 61. 애니메이션_애니메이션#1_ReadAnimation, ModelAnimator (0) | 2024.03.05 |
|---|---|
| 60. 애니메이션_애니메이션 데이터 (0) | 2024.03.04 |
| 58. 애니메이션_애니메이션 이론 (0) | 2024.03.02 |
| 57. 모델_ImGUI (0) | 2024.03.01 |
| 56. 모델_계층 구조 (0) | 2024.02.29 |
댓글