글에 사용된 모든 그림과 내용은 직접 작성한 것입니다.
[유튜브 영상]
[깃허브 보러가기]
https://github.com/Chang-Jin-Lee/D3D11-AliceTutorial/tree/main/18_fbx_Animation
[풀리퀘 보러가기]
https://github.com/Chang-Jin-Lee/D3D11-AliceTutorial/pull/32
D3D11에서 assimp로 로드한 FBX의 애니메이션을 실행시키는 방법을 정리하기 위함



https://velog.io/@whoamicj/DX11-Render-fbx-pmx-obj
이전 3D 모델 렌더하는 예제에서 다음의 로직을 추가합니다
bool FbxManager::Load(ID3D11Device* device, const std::wstring& pathW)
{
Release();
m_->Importer = new Impl::AssimpImporterHolder();
// Importer properties for speed/robustness
m_->Importer->importer.SetPropertyInteger(AI_CONFIG_PP_LBW_MAX_WEIGHTS, 4);
std::string pathA(pathW.begin(), pathW.end());
const aiScene* scene = m_->Importer->importer.ReadFile(pathA,
aiProcess_Triangulate | aiProcess_JoinIdenticalVertices | aiProcess_ImproveCacheLocality |
aiProcess_GenSmoothNormals | aiProcess_CalcTangentSpace | aiProcess_ConvertToLeftHanded |
aiProcess_OptimizeMeshes | aiProcess_OptimizeGraph | aiProcess_LimitBoneWeights);
if (!scene || !scene->HasMeshes()) return false;
m_->SceneMutable = const_cast<aiScene*>(scene);
// Save global inverse of root (row-vector mapping)
{
aiMatrix4x4 I = scene->mRootNode->mTransformation;
I.Inverse();
DirectX::XMFLOAT4X4 gi;
gi._11 = (float)I.a1; gi._12 = (float)I.a2; gi._13 = (float)I.a3; gi._14 = (float)I.a4;
gi._21 = (float)I.b1; gi._22 = (float)I.b2; gi._23 = (float)I.b3; gi._24 = (float)I.b4;
gi._31 = (float)I.c1; gi._32 = (float)I.c2; gi._33 = (float)I.c3; gi._34 = (float)I.c4;
gi._41 = (float)I.d1; gi._42 = (float)I.d2; gi._43 = (float)I.d3; gi._44 = (float)I.d4;
m_->GlobalInverse = gi;
}
std::wstring baseDir = pathW;
size_t slash = baseDir.find_last_of(L"/\\");
baseDir = (slash == std::wstring::npos) ? L"" : baseDir.substr(0, slash + 1);
if (!LoadMaterials(device, scene, baseDir)) return false;
if (!BuildMeshBuffers(device, scene)) return false;
// 스켈레톤/본/가중치/애니메이션 메타 분리 호출
BuildSkeletonAndNodeIndex(scene);
CollectBonesAndOffsets(scene);
std::vector<size_t> baseVertex; BuildBaseVertexTable(scene, baseVertex);
AccumulateVertexWeights(scene, baseVertex);
NormalizeInfluencesAndFlag();
if (m_->pVB) { ApplyInfluencesToVB(device); }
InitAnimationMetadata(scene);
return true;
}


void FbxManager::BuildSkeletonAndNodeIndex(const aiScene* scene)
{
m_->Skeleton.clear();
m_->NodeIndexOfName.clear();
std::function<int(const aiNode*, int)> build = [&](const aiNode* node, int parent){
int idx = (int)m_->Skeleton.size();
// UTF-8 -> UTF-16 변환(Helper 사용)으로 디버거에서 깨지지 않게 표시
std::string nmUtf8 = node->mName.C_Str();
std::wstring nmW = WStringFromUtf8(nmUtf8);
FbxManager::SkeletonNode sn{}; sn.name = nmUtf8; sn.nameW = nmW; sn.parent = parent; sn.isBone = false;
m_->Skeleton.push_back(std::move(sn));
m_->NodeIndexOfName[m_->Skeleton.back().name] = idx;
for (unsigned ci = 0; ci < node->mNumChildren; ++ci)
{
int ch = build(node->mChildren[ci], idx);
m_->Skeleton[idx].children.push_back(ch);
}
return idx;
};
m_->RootIndex = build(scene->mRootNode, -1);
}


void FbxManager::CollectBonesAndOffsets(const aiScene* scene)
{
m_->HasSkinning = false;
m_->BoneNames.clear();
m_->BoneOffset.clear();
m_->BoneIndexOfName.clear();
for (unsigned mi = 0; mi < scene->mNumMeshes; ++mi)
{
const aiMesh* mesh = scene->mMeshes[mi];
for (unsigned bi = 0; bi < mesh->mNumBones; ++bi)
{
const aiBone* b = mesh->mBones[bi];
std::string name = b->mName.C_Str();
std::wstring nameW = WStringFromUtf8(name);
if (m_->BoneIndexOfName.find(name) == m_->BoneIndexOfName.end())
{
int newIndex = (int)m_->BoneNames.size();
m_->BoneIndexOfName[name] = newIndex;
m_->BoneNames.push_back(name);
DirectX::XMFLOAT4X4 off;
off._11 = (float)b->mOffsetMatrix.a1; off._12 = (float)b->mOffsetMatrix.a2; off._13 = (float)b->mOffsetMatrix.a3; off._14 = (float)b->mOffsetMatrix.a4;
off._21 = (float)b->mOffsetMatrix.b1; off._22 = (float)b->mOffsetMatrix.b2; off._23 = (float)b->mOffsetMatrix.b3; off._24 = (float)b->mOffsetMatrix.b4;
off._31 = (float)b->mOffsetMatrix.c1; off._32 = (float)b->mOffsetMatrix.c2; off._33 = (float)b->mOffsetMatrix.c3; off._34 = (float)b->mOffsetMatrix.c4;
off._41 = (float)b->mOffsetMatrix.d1; off._42 = (float)b->mOffsetMatrix.d2; off._43 = (float)b->mOffsetMatrix.d3; off._44 = (float)b->mOffsetMatrix.d4;
m_->BoneOffset.push_back(off);
auto itNode = m_->NodeIndexOfName.find(name);
if (itNode != m_->NodeIndexOfName.end()) {
m_->Skeleton[itNode->second].isBone = true;
if (m_->Skeleton[itNode->second].nameW.empty()) m_->Skeleton[itNode->second].nameW = nameW;
}
}
}
}
}






void FbxManager::BuildBaseVertexTable(const aiScene* scene, std::vector<size_t>& baseVertex)
{
baseVertex.clear();
baseVertex.resize(scene->mNumMeshes, 0);
size_t cursor = 0;
std::function<void(const aiNode*)> fillBase = [&](const aiNode* node){
for (unsigned mi2 = 0; mi2 < node->mNumMeshes; ++mi2)
{
unsigned meshIdx = node->mMeshes[mi2];
baseVertex[meshIdx] = cursor;
cursor += scene->mMeshes[meshIdx]->mNumVertices;
}
for (unsigned ci = 0; ci < node->mNumChildren; ++ci) fillBase(node->mChildren[ci]);
};
fillBase(scene->mRootNode);
}

각 본의 aiVertexWeight를 VB 인덱스로 변환해 버텍스별 영향받은 상위 4개만 저장합니다.
4개만 보관하는 이유는 Assimp에도 최대 본 가중치를 4로 제한해두었기 때문입니다.
m_->Importer->importer.SetPropertyInteger(AI_CONFIG_PP_LBW_MAX_WEIGHTS, 4);
void FbxManager::AccumulateVertexWeights(const aiScene* scene, const std::vector<size_t>& baseVertex)
{
if (!m_->Influences.empty()) m_->Influences.clear();
m_->Influences.assign(m_->BindVertices.size(), {});
for (unsigned mi2 = 0; mi2 < scene->mNumMeshes; ++mi2)
{
const aiMesh* mesh = scene->mMeshes[mi2];
size_t base = baseVertex[mi2];
for (unsigned bi = 0; bi < mesh->mNumBones; ++bi)
{
const aiBone* b = mesh->mBones[bi];
auto it = m_->BoneIndexOfName.find(b->mName.C_Str());
if (it == m_->BoneIndexOfName.end()) continue;
int boneIdx = it->second;
for (unsigned wi = 0; wi < b->mNumWeights; ++wi)
{
const aiVertexWeight& vw = b->mWeights[wi];
size_t v = base + (size_t)vw.mVertexId;
if (v >= m_->Influences.size()) continue;
int slot = 0; float minW = m_->Influences[v].w[0];
for (int s = 1; s < 4; ++s) { if (m_->Influences[v].w[s] < minW) { minW = m_->Influences[v].w[s]; slot = s; } }
m_->Influences[v].idx[slot] = (unsigned short)boneIdx;
m_->Influences[v].w[slot] = (float)vw.mWeight;
}
}
}
}





가중치의 합을 1로 정규화합니다. 비가중치 버텍스는 본0에 1 할당하고 스키닝 사용 여부 결정합니다
가중치의 합을 1로 정규화하는 이유는 합이 1이 아니면 위치가 과도하게 스케일되거나, 축소되어 포즈가 무너집니다. 애니메이션을 실행시켰을때 각 부분이 이상하게 커지고 줄어들고 하는 문제가 보통 이 정규화를 잘못했을 때 나타납니다
void FbxManager::NormalizeInfluencesAndFlag()
{
for (auto& inf : m_->Influences)
{
float s = inf.w[0] + inf.w[1] + inf.w[2] + inf.w[3];
if (s > 1e-6f)
{
float inv = 1.0f / s;
inf.w[0] *= inv; inf.w[1] *= inv; inf.w[2] *= inv; inf.w[3] *= inv;
}
else
{
inf.idx[0] = 0; inf.w[0] = 1.0f;
inf.idx[1] = 0; inf.w[1] = 0.0f;
inf.idx[2] = 0; inf.w[2] = 0.0f;
inf.idx[3] = 0; inf.w[3] = 0.0f;
}
}
m_->HasSkinning = !m_->BoneNames.empty();
}


void FbxManager::ApplyInfluencesToVB(ID3D11Device* device)
{
if (!m_->HasSkinning) return;
for (size_t i = 0; i < m_->BindVertices.size(); ++i)
{
const auto& inf = m_->Influences[i];
m_->BindVertices[i].boneIdx[0] = inf.idx[0];
m_->BindVertices[i].boneIdx[1] = inf.idx[1];
m_->BindVertices[i].boneIdx[2] = inf.idx[2];
m_->BindVertices[i].boneIdx[3] = inf.idx[3];
m_->BindVertices[i].boneWeight = { inf.w[0], inf.w[1], inf.w[2], inf.w[3] };
}
if (m_->pVB) { m_->pVB->Release(); m_->pVB = nullptr; }
D3D11_BUFFER_DESC vb{}; vb.BindFlags = D3D11_BIND_VERTEX_BUFFER; vb.Usage = D3D11_USAGE_DEFAULT;
vb.ByteWidth = (UINT)(m_->BindVertices.size() * sizeof(VertexSkinnedTBN));
D3D11_SUBRESOURCE_DATA vbd{}; vbd.pSysMem = m_->BindVertices.data();
HR_T(device->CreateBuffer(&vb, &vbd, &m_->pVB));
}

void FbxManager::InitAnimationMetadata(const aiScene* scene)
{
if (scene->mNumAnimations == 0) return;
m_->HasAnimations = true;
m_->AnimationNames.reserve(scene->mNumAnimations);
m_->ClipDurationSec.reserve(scene->mNumAnimations);
m_->ClipTicksPerSec.reserve(scene->mNumAnimations);
for (unsigned i = 0; i < scene->mNumAnimations; ++i)
{
const aiAnimation* a = scene->mAnimations[i];
std::string nm = a->mName.length > 0 ? StringFromAi(a->mName) : ("Anim" + std::to_string(i));
double tps = (a->mTicksPerSecond != 0.0) ? a->mTicksPerSecond : 25.0;
double durSec = (tps != 0.0) ? (a->mDuration / tps) : 0.0;
m_->AnimationNames.push_back(nm);
m_->ClipTicksPerSec.push_back(tps);
m_->ClipDurationSec.push_back(durSec);
}
m_->CurrentClip = 0;
m_->ClipTimeSec = 0.0;
m_->Playing = false;
}



void FbxManager::EvaluateGlobalMatrices(const aiScene* scene, const std::unordered_map<std::string, const aiNodeAnim*>& channelOf, std::vector<DirectX::XMFLOAT4X4>& outGlobal) const
{
outGlobal.resize(m_->Skeleton.size());
std::function<void(const aiNode*, int, const DirectX::XMMATRIX&)> eval = [&](const aiNode* node, int idx, const DirectX::XMMATRIX& parent){
aiVector3D S(1,1,1), T(0,0,0); aiQuaternion R;
aiMatrix4x4 mLocal = node->mTransformation;
auto itCh = channelOf.find(node->mName.C_Str());
if (itCh != channelOf.end())
{
double tTicks = m_->ClipTimeSec * ((m_->CurrentClip >= 0 && (size_t)m_->CurrentClip < m_->ClipTicksPerSec.size()) ? m_->ClipTicksPerSec[m_->CurrentClip] : 25.0);
const aiNodeAnim* ch = itCh->second;
S = (ch->mNumScalingKeys > 0) ? InterpVec(ch->mScalingKeys, ch->mNumScalingKeys, tTicks) : aiVector3D(1,1,1);
T = (ch->mNumPositionKeys > 0) ? InterpVec(ch->mPositionKeys, ch->mNumPositionKeys, tTicks) : aiVector3D(0,0,0);
R = (ch->mNumRotationKeys > 0) ? InterpQuat(ch->mRotationKeys, ch->mNumRotationKeys, tTicks) : aiQuaternion();
aiMatrix4x4 mS; mS.Scaling(S, mS); aiMatrix4x4 mR = aiMatrix4x4(R.GetMatrix()); aiMatrix4x4 mT; mT.Translation(T, mT);
aiMatrix4x4 mA = mT * mR * mS;
mLocal = mA;
}
DirectX::XMFLOAT4X4 lm;
lm._11 = (float)mLocal.a1; lm._12 = (float)mLocal.a2; lm._13 = (float)mLocal.a3; lm._14 = (float)mLocal.a4;
lm._21 = (float)mLocal.b1; lm._22 = (float)mLocal.b2; lm._23 = (float)mLocal.b3; lm._24 = (float)mLocal.b4;
lm._31 = (float)mLocal.c1; lm._32 = (float)mLocal.c2; lm._33 = (float)mLocal.c3; lm._34 = (float)mLocal.c4;
lm._41 = (float)mLocal.d1; lm._42 = (float)mLocal.d2; lm._43 = (float)mLocal.d3; lm._44 = (float)mLocal.d4;
DirectX::XMMATRIX L = DirectX::XMLoadFloat4x4(&lm);
DirectX::XMMATRIX G = DirectX::XMMatrixMultiply(parent, L);
DirectX::XMStoreFloat4x4(&outGlobal[idx], G);
for (unsigned ci = 0; ci < node->mNumChildren; ++ci)
{
auto it = m_->NodeIndexOfName.find(node->mChildren[ci]->mName.C_Str());
int childIdx = (it != m_->NodeIndexOfName.end()) ? it->second : -1;
if (childIdx >= 0) eval(node->mChildren[ci], childIdx, G);
}
};
if (m_->RootIndex >= 0) eval(scene->mRootNode, m_->RootIndex, DirectX::XMMatrixIdentity());
}
여기까지 했으면 데이터는 모두 준비가 끝났습니다. 애니메이션을 보간을 사용해 Update Tick을 계산해가며 실행하면 됩니다
void FbxManager::UpdateAnimation(ID3D11DeviceContext* ctx, double dtSec)
{
if (!m_->HasAnimations || m_->CurrentClip < 0) return;
if (m_->Playing) SetAnimationTimeSeconds(m_->ClipTimeSec + dtSec);
const aiScene* scene = reinterpret_cast<const aiScene*>(m_->SceneMutable);
if (!scene) return;
const aiAnimation* anim = (m_->HasAnimations && m_->CurrentClip >= 0) ? scene->mAnimations[m_->CurrentClip] : nullptr;
std::unordered_map<std::string, const aiNodeAnim*> channelOf;
if (anim)
{
for (unsigned i = 0; i < anim->mNumChannels; ++i)
{
const aiNodeAnim* ch = anim->mChannels[i];
channelOf[ch->mNodeName.C_Str()] = ch;
}
}
// 전 노드의 글로벌 행렬 평가(키 보간 포함)
std::vector<DirectX::XMFLOAT4X4> global;
EvaluateGlobalMatrices(scene, channelOf, global);
// 스키닝 팔레트 구성: GlobalInverse * Global(node) * Offset
std::vector<DirectX::XMMATRIX> palette;
BuildBonePalette(global, palette);
// 본 팔레트 상수 버퍼 업로드
UploadBonePalette(ctx, palette);
}



현재 시간에서 모든 노드에 있는 글로벌 변환 행렬을 계산해주고 global에 그려지도록 저장합니다.
현재 시간 tick = ClipTimeSec × TicksPerSecond로 저장합니다.
각 노드에 대해서 위치, 회전, 스케일을 보간해줍니다.
그 다음 T * R * S 로컬 행렬을 구성해주고 부모와 곱해서 글로벌 행렬을 완성해둡니다
void FbxManager::EvaluateGlobalMatrices(const aiScene* scene, const std::unordered_map<std::wstring, const aiNodeAnim*>& channelOf, std::vector<DirectX::XMFLOAT4X4>& outGlobal) const
{
outGlobal.resize(m_->Skeleton.size());
std::function<void(const aiNode*, int, const DirectX::XMMATRIX&)> eval = [&](const aiNode* node, int idx, const DirectX::XMMATRIX& parent){
aiVector3D S(1,1,1), T(0,0,0); aiQuaternion R;
aiMatrix4x4 mLocal = node->mTransformation;
auto itCh = channelOf.find(WStringFromUtf8(node->mName.C_Str()));
if (itCh != channelOf.end())
{
double tTicks = m_->ClipTimeSec * ((m_->CurrentClip >= 0 && (size_t)m_->CurrentClip < m_->ClipTicksPerSec.size()) ? m_->ClipTicksPerSec[m_->CurrentClip] : 25.0);
const aiNodeAnim* ch = itCh->second;
S = (ch->mNumScalingKeys > 0) ? InterpVec(ch->mScalingKeys, ch->mNumScalingKeys, tTicks) : aiVector3D(1,1,1);
T = (ch->mNumPositionKeys > 0) ? InterpVec(ch->mPositionKeys, ch->mNumPositionKeys, tTicks) : aiVector3D(0,0,0);
R = (ch->mNumRotationKeys > 0) ? InterpQuat(ch->mRotationKeys, ch->mNumRotationKeys, tTicks) : aiQuaternion();
aiMatrix4x4 mS; mS.Scaling(S, mS); aiMatrix4x4 mR = aiMatrix4x4(R.GetMatrix()); aiMatrix4x4 mT; mT.Translation(T, mT);
aiMatrix4x4 mA = mT * mR * mS;
mLocal = mA;
}
DirectX::XMFLOAT4X4 lm;
lm._11 = (float)mLocal.a1; lm._12 = (float)mLocal.a2; lm._13 = (float)mLocal.a3; lm._14 = (float)mLocal.a4;
lm._21 = (float)mLocal.b1; lm._22 = (float)mLocal.b2; lm._23 = (float)mLocal.b3; lm._24 = (float)mLocal.b4;
lm._31 = (float)mLocal.c1; lm._32 = (float)mLocal.c2; lm._33 = (float)mLocal.c3; lm._34 = (float)mLocal.c4;
lm._41 = (float)mLocal.d1; lm._42 = (float)mLocal.d2; lm._43 = (float)mLocal.d3; lm._44 = (float)mLocal.d4;
DirectX::XMMATRIX L = DirectX::XMLoadFloat4x4(&lm);
DirectX::XMMATRIX G = DirectX::XMMatrixMultiply(parent, L);
DirectX::XMStoreFloat4x4(&outGlobal[idx], G);
for (unsigned ci = 0; ci < node->mNumChildren; ++ci)
{
auto it = m_->NodeIndexOfName.find(node->mChildren[ci]->mName.C_Str());
int childIdx = (it != m_->NodeIndexOfName.end()) ? it->second : -1;
if (childIdx >= 0) eval(node->mChildren[ci], childIdx, G);
}
};
if (m_->RootIndex >= 0) eval(scene->mRootNode, m_->RootIndex, DirectX::XMMatrixIdentity());
}
* G * Offvoid FbxManager::BuildBonePalette(const std::vector<DirectX::XMFLOAT4X4>& global, std::vector<DirectX::XMMATRIX>& outPalette) const
{
outPalette.resize(m_->BoneNames.size(), DirectX::XMMatrixIdentity());
for (size_t bi = 0; bi < m_->BoneNames.size(); ++bi)
{
auto itN = m_->NodeIndexOfName.find(m_->BoneNames[bi]);
if (itN == m_->NodeIndexOfName.end()) continue;
int nodeIdx = itN->second;
DirectX::XMMATRIX G = DirectX::XMLoadFloat4x4(&global[nodeIdx]);
DirectX::XMMATRIX Off = DirectX::XMLoadFloat4x4(&m_->BoneOffset[bi]);
DirectX::XMMATRIX Gi = DirectX::XMLoadFloat4x4(&m_->GlobalInverse);
outPalette[bi] = DirectX::XMMatrixMultiply(DirectX::XMMatrixMultiply(Gi, G), Off);
}
}
void FbxManager::UploadBonePalette(ID3D11DeviceContext* ctx, const std::vector<DirectX::XMMATRIX>& palette)
{
if (!ctx) return;
Microsoft::WRL::ComPtr<ID3D11Device> dev;
ctx->GetDevice(dev.GetAddressOf());
EnsureBoneCB(dev.Get());
if (!m_->pBoneCB) return;
struct BoneCB { DirectX::XMFLOAT4X4 m[kMaxBones]; unsigned int boneCount; float pad[3]; };
BoneCB cb{};
size_t n = std::min(palette.size(), (size_t)kMaxBones);
DirectX::XMMATRIX I = DirectX::XMMatrixIdentity();
for (size_t i = 0; i < (size_t)kMaxBones; ++i)
{
DirectX::XMStoreFloat4x4(&cb.m[i], I);
}
for (size_t i = 0; i < n; ++i)
{
DirectX::XMMATRIX Mt = DirectX::XMMatrixTranspose(palette[i]);
DirectX::XMStoreFloat4x4(&cb.m[i], Mt);
}
cb.boneCount = (unsigned int)n;
D3D11_MAPPED_SUBRESOURCE mapped{};
if (SUCCEEDED(ctx->Map(m_->pBoneCB, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped)))
{
memcpy(mapped.pData, &cb, sizeof(BoneCB));
ctx->Unmap(m_->pBoneCB, 0);
}
}
| Animation - Phong | Animation - Blinn Phong |
|---|---|
![]() | ![]() |
| Animation - Lambert |
|---|
![]() |
| Animation - No Lighting | Animation - TextureOnly |
|---|---|
![]() | ![]() |
| Animation - Phong | Animation - Blinn Phong |
|---|---|
![]() | ![]() |
| Animation - Lambert |
|---|
![]() |
| Animation - No Lighting | Animation - TextureOnly |
|---|---|
![]() | ![]() |