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

[유튜브 영상]


[깃허브 보러가기]
https://github.com/Chang-Jin-Lee/D3D11-AliceTutorial/tree/main/23_Rigid_Animation

[풀리퀘 보러가기]
https://github.com/Chang-Jin-Lee/D3D11-AliceTutorial/pull/47


글의 목적

D3D11에서 assimp로 로드한 FBX에 본이 없는 경우, 있는 경우 각각의 애니메이션을 실행하는 방법을 정리하기 위함


  • 이번 글에서는 이전에 다룬 글에서 분기처리합니다. Skinned, Rigid를 각각 예외처리를 합니다.

  • 3D 모델이 완벽하게 그려졌고, Skinned 가중치 적용 애니메이션을 구현했다는 걸 전제로 진행합니다.

  • 아래의 아티클에서 이전 내용을 복습할 수 있습니다

https://velog.io/@whoamicj/DX11-fbx-Animation


이전 Skinned 3D 모델의 애니메이션을 그리는 예제에서 로직을 수정합니다

간단하게

  • Bone이 없고 애니메이션이 존재한다면 -> Rigid 애니메이션
  • Bone이 있고 애니메이션이 존재한다면 -> Skinned 애니메이션
  • 애니메이션이 없다면 -> 그냥 메쉬

로드

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

애니메이션 재생

  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 = Utf8FromWString(pathW);
    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);

    // Rigid 애니메이션. Bone이 없음 → 가중치 누적/정규화 스킵
    if (m_->BoneNames.empty() && scene->mNumAnimations > 0)
    {
        // Rigid: 스켈레톤 노드로 가짜 본 구성 후, 각 정점에 소유 노드 1웨이트만 부여
        m_CurrentType = AnimationType::Rigid;
        BuildRigidBonesFromSkeleton();
        BuildRigidWeightsFromOwners();
        if (m_->pVB) { ApplyInfluencesToVB(device); }
        InitAnimationMetadata(scene);
        return true;
    }

    // Skinned 애니메이션. 본이 있음. → 가중치 누적/정규화 수행
    if (!m_->BoneNames.empty())
    {
        std::vector<size_t> baseVertex; BuildBaseVertexTable(scene, baseVertex);
        AccumulateVertexWeights(scene, baseVertex);
        NormalizeInfluencesAndFlag();
        if (m_->pVB) { ApplyInfluencesToVB(device); }
        InitAnimationMetadata(scene);
        m_CurrentType = AnimationType::Skinned;
        return true;
    }

    // Static Mesh 본도, 애니메이션도 없음.
    InitAnimationMetadata(scene);
    m_CurrentType = AnimationType::None;
    return true;
}
    1. Rigid 애니메이션. Bone이 없는 것을 확인할 수 있습니다 → 가중치 누적/정규화 스킵

    1. Skinned 애니메이션. 본이 있는 것을 확인할 수 있습니다 → 가중치 누적/정규화 수행

    1. Static Mesh 본도, 애니메이션도 없는 것을 확인할 수 있습니다

BuildRigidBonesFromSkeleton

  • Rigid 애니메이션 관련 helper 함수. 본이 아예 없다면 스켈레탈 노드를 통해서 가짜 본을 만들어 줍니다
void FbxManager::BuildRigidBonesFromSkeleton()
{
    if (!m_->BoneNames.empty()) return;
    m_->BoneNames.clear();
    m_->BoneOffset.clear();
    m_->BoneIndexOfName.clear();
    m_->BoneNames.reserve(m_->Skeleton.size());
    m_->BoneOffset.reserve(m_->Skeleton.size());
    for (size_t i = 0; i < m_->Skeleton.size(); ++i)
    {
        const auto& sn = m_->Skeleton[i];
        m_->BoneIndexOfName[sn.name] = (int)i;
        m_->BoneNames.push_back(sn.name);
        DirectX::XMFLOAT4X4 I{ 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 };
        m_->BoneOffset.push_back(I);
    }
}

  • 각 본을 돌면서 부모, 자식 구조로 각 본의 이름을 매핑합니다

BuildRigidWeightsFromOwners

  • 정점당 소유한 노드를 기반으로 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;
                }
            }
        }
    }
}


Animation

  • 이제 다음처럼 애니메이션을 실행시킬 수 있습니다
Animation - SkinnedAnimation - Rigid
Animation - Static Mesh

profile
게임 프로그래머

0개의 댓글