글에 사용된 모든 그림과 내용은 직접 작성한 것입니다.

[유튜브 영상]


[깃허브 보러가기]
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의 애니메이션을 실행시키는 방법을 정리하기 위함


  • 여기선 본의 구조를 잘 살펴보아야 합니다.
  • 다음은 해당 프로젝트에서 사용하는 모델의 본 구조입니다.

  • 그리고 다음처럼 애니메이션 데이터를 봐야합니다

  • 이 글에서는 3D 모델이 완벽하게 그려졌다는 것을 가정하고 진행합니다
  • 아래의 아티클에서 모델을 그리는 방법을 배울 수 있습니다

https://velog.io/@whoamicj/DX11-Render-fbx-pmx-obj


이전 3D 모델 렌더하는 예제에서 다음의 로직을 추가합니다

로드

  1. FBX를 읽어 좌수계와 본 가중치 제한을 적용하고, 루트 역행렬을 저장한다.
  2. 씬 노드 트리를 순회해 스켈레톤과 이름-인덱스 매핑, 루트 인덱스를 만든다.
  3. 메시에 등장한 본 이름과 바인드포즈 오프셋 행렬을 모으고 해당 노드를 본으로 표시한다.
  4. 메시별 정점 시작 오프셋 테이블을 만든다.
  5. 각 본의 정점 가중치를 모아 정점당 최대 4개만 유지한다.
  6. 가중치 합을 1로 정규화하고 스키닝 사용 여부를 결정한다.
  7. 본 인덱스/가중치를 정점 데이터에 반영해 정점 버퍼를 다시 만든다.
  8. 애니메이션 클립 이름, 길이(초), 초당 틱을 저장하고 기본 클립/시간을 초기화한다.

애니메이션 재생

  1. 클립 선택, 재생 여부, 시간을 외부에서 설정한다.
  2. 매 프레임 재생 중이면 경과 시간을 더하고 클립 길이로 루프 처리한다.
  3. 현재 클립의 노드별 애니메이션 채널 매핑을 만든다.
  4. 현재 시각의 스케일/회전/이동을 보간해 로컬 변환을 만들고, 부모와 곱해 전 노드 글로벌 변환을 계산한다.
  5. 각 본에 대해 루트 역행렬·노드 글로벌·오프셋을 곱해 최종 본 행렬 팔레트를 만든다.
  6. 본 행렬을 담는 상수 버퍼가 없으면 동적으로 준비한다.
  7. 본 행렬 배열과 본 개수를 상수 버퍼에 업로드한다.
  8. 렌더링 시 상수 버퍼·정점/인덱스 버퍼·텍스처를 바인딩하고 드로우 호출한다.

Load

  • assimp로 애니메이션 데이터를 가져오기 위해 가장 중요한 부분입니다. Assimp::Importer를 사용해 파일을 읽고, 모델에 대한 정보를 가져오고, 본에 대한 GlobalInverse 매트릭스를 정보 등을 얻어낸 후 스켈레톤, 본, 가중치, 애니메이션 등의 정보를 모두 가져올 수 있습니다.
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;
}
  • 다음은 블렌더에서 확인한 본의 정보입니다

  • m_->GlobalInverse에는 다음처럼 월드에서의 인버스 행렬을 넣어줍니다

BuildSkeletonAndNodeIndex

  • 씬의 노드 트리를 DFS로 순회해서 인덱스를 넣고 Key : Value = 이름 : 인덱스인 맵을 만들어둡니다
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);
}

  • 최종적으로 루트 인덱스를 반환해줍니다

CollectBonesAndOffsets

  • 메시에 있는 본 이름을 수집하고, aiBone::mOffsetMatrix를 XMLOAT4X4에 담아 저장합니다. directX에서 쓰기 편하기 위함입니다.
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;
                }
            }
        }
    }
}

  • 각 정보가 들어가는 것을 확인할 수 있습니다

  • 다음처럼 블렌더에서 실제 본 이름을 찾아볼 수 있습니다

  • 각 본의 오프셋도 확인 가능합니다

BuildBaseVertexTable

  • 노드 트리를 재귀적으로 순회하여 각 메시의 버텍스 버퍼에서 메시가 시작하는 오프셋을 저장해둡니다
  • 메시가 시작하는 오프셋이란 각 메시가 그 배열에서 시작하는 인덱스입니다. 메시의 “로컬 정점 인덱스”를 “글로벌 버텍스 버퍼 인덱스”로 바꿀 때 사용하는 값입니다
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);
}

AccumulateVertexWeights

  • 각 본의 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;
            }
        }
    }
}

  • 영향을 받은 것들의 정보를 볼 수 있습니다

  • 영향받은 것들에대해 정보를 덮어쓰기 합니다

  • 메시에서 정보들을 확인할 수 있습니다

  • 웨이트들에 정보를 업데이트합니다

  • 영향을 받은 본 인덱스 입니다

NormalizeInfluencesAndFlag

  • 가중치의 합을 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();
}

  • 영향 받은 본만 정규화합니다

ApplyInfluencesToVB

  • 정점 데이터와 컬러, UV, TBN, 본 가중치를 저장할 수 있는 BindVertices에 boneIdx와 weight를 반영하고 버텍스 버퍼를 다시 생성합니다.
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));
}

  • 영향받은 버텍스들에 정보를 업데이트합니다

InitAnimationMetadata

  • aiAnimation 배열을 for문으로 돌면서 이름, 길이(초), TicksPerSecond를 기록합니다
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;
}

  • 애니메이션 개수를 확인할 수 있습니다

  • 애니메이션의 이름을 확인할 수 있습니다

EvaluateGlobalMatrices

  • 특정 시간의 노드 로컬 변환을 채널에서 보간해 부모의 로컬 행렬을 기준으로 글로벌 행렬을 만들고, 본별 최종 스키닝 행렬을 계산합니다.
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을 계산해가며 실행하면 됩니다

UpdateAnimation

  • 재생 시간을 갱신하고 현재 클립의 키프레임을 보간해서 씬 노드의 글로벌 행렬을 계산합니다. 그 다음 본 팔레트를 생성하고 본 상수 버퍼에 업로드를 해줍니다. 그럼 애니메이션이 해당 시간에 맞게 실행됩니다
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);
}

  • 애니메이션 채널들을 확인할 수 있습니다

  • 각 본들에 해당하는 애니메이션 채널을 확인할 수 있습니다

EvaluateGlobalMatrices

  • 현재 시간에서 모든 노드에 있는 글로벌 변환 행렬을 계산해주고 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());
}

BuildBonePalette

  • 현재 포즈의 변환을 담당하는 행렬을 만들어냅니다
  • 각 본의 스키닝 행렬을 완성시키고 이를 palette에 저장합니다
  • 본들은 위에서 만든 노드의 글로벌 행렬을 사용해 다시 본 행렬을 만들어야합니다
  • 이제 aiBone의 오프셋과 루트 역행렬을 곱합니다
  • Gi * G * Off
void 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);
    }
}

UploadBonePalette

  • GPU에게 렌더를 시키기위해 palette를 버텍스 쉐이더 상수 버퍼로 전달합니다
  • 이제 만들어 놓은 본 행렬 배열과, 정점의 본 인덱스, 가중치 행렬 등의 모든 정보를 전달합니다.
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

  • 이제 다음처럼 애니메이션을 실행시킬 수 있습니다
Animation - PhongAnimation - Blinn Phong
Animation - Lambert
Animation - No LightingAnimation - TextureOnly



Animation - PhongAnimation - Blinn Phong
Animation - Lambert
Animation - No LightingAnimation - TextureOnly

profile
게임 프로그래머

0개의 댓글