LoginSignup
0
0

More than 1 year has passed since last update.

Assimpを使ってUnityChanを描画する

Last updated at Posted at 2022-12-21

フリーで使用できるFBXモデルで代表的なものに、UnityChanがあります。これをC++で読み込んで、OpenGLで描画するプログラムを作成しました。

UnityChanをAssimpで読み込む

Assimpで読み込めない問題

UnityChanをそのままAssimpのImporterで読み込ませると、下記のようなエラーが発生し、読み込むことができません。
Error parsing './unitychan.fbx': 'FBX-Parser (TOK_DATA, offset 0x192452) failed to parse ID, unexpected data type, expected L(ong)
(binary)'

これの対策として、いくつか方法が考えられます。
例えば、Blenerにインポートして、エクスポートすれば、Assimpで読み込むことが可能です。しかし、このサイトにあるように、そのままUnityChanを読み込むと、Boneの配置が崩れたり、顔の位置がずれたりなど、Blender内での調整が必要となります。

ここでは、FBX SDKのConverterを活用しました。

UnityChanをAssimpで読めるようにする

FBX SDKをインストールすると、Sampleプログラムの中に、Sceneを変更して再出力するものがあります。
http://docs.autodesk.com/FBX/2014/ENU/FBX-SDK-Documentation/index.html?url=cpp_ref/_convert_scene_2main_8cxx-example.html,topicNumber=cpp_ref__convert_scene_2main_8cxx_example_htmlbd068b85-cbd6-4fa5-b5b7-274ab3b3ff24
このプログラムにunitychan.fbxを入力し、コンバートします。Assimpでは、「_fbx7binary.fbx」がファイルの末尾についたメッシュデータを読み込むことができます。

UnityChanを描画する

顔にBoneを割り当てる

メッシュデータは、OGL DEVさんのソースコードで描画できます。しかし、顔にBoneが割り当てられていないため、Bone情報を反映させると、顔が描画されません(恐怖)。
そのため、新たにBoneを追加します。私の実装では、名前とBoneを関連付ける配列mBoneNameToIndexMapにmeshIdx + という名前で、新規BoneIndexを追加しました。

Skeleton.cpp
if (mesh->mNumBones == 0) {  // meshはconst aiMesh*型
        // Boneが割り当てられていないので、新たに作成
        int BoneIndex = (int)mBoneNameToIndexMap.size();
        std::string newBoneName = "meshIdx" + std::to_string(meshIdx);
        mBoneNameToIndexMap[newBoneName] = BoneIndex;

        for (int vertIdx = 0; vertIdx < mesh->mNumVertices; vertIdx++) {
            // 追加したBoneに関連づいた頂点Index
            unsigned int GlobalVertexID = baseVertex + vertIdx;
            mBones[GlobalVertexID].AddBoneData(BoneIndex, 1.f); // weight = 1.0
        }

        if (BoneIndex == mOffsetMatrices.size()) {
            mOffsetMatrices.push_back(glm::mat4(1.f));
        }

        printf("warn: this mesh does not assigned bone: %s, meshIdx: %d\n", mesh->mName.C_Str(), meshIdx);

        return true;
    }

この処理は元からすべてのMeshのBoneが割り当てられていたら発生しない処理です。なので、Blenderで正しく読み込めたなら、すべてのポリゴンにBoneを割り当てれば実装はもっと楽になります。

顔の位置も考慮したSkeletal Animationの再生

Assimpを用いたSkeletal Animationの再生では、Nodeを再帰的に走ることで読み出します。UnityChanをダウンロードすると、付属で走るアニメーションなどが付属されています。これは、Boenが割り当てられていない顔のMeshだけ、一緒に入っているので、Nodeを読んだときにMeshがあるNodeが、新規に追加したBoneだとわかります。

Animation.cpp
    std::string NodeName(pNode->mName.data);
    for (int i = 0; i < pNode->mNumMeshes; i++) { // meshがある = 顔
        unsigned int meshIdx = pNode->mMeshes[i];
        std::string meshName = m_pScene->mMeshes[meshIdx]->mName.C_Str();
        std::string boneName = "meshIdx" + std::to_string(meshIdx);
        NodeName = boneName;
    }

BoneのTransformは基本的にNodeのTransformを使えばOKですので、OGL DEVさんのコードがそのまま活用できます。顔については、FBX SDKでコンバートするときに、データが壊れるのかわかりませんが、一部の顔のパーツのTransformが壊れるため、ハードコーディングで下記のような顔のTransformを与えています。ただし、まつ毛のEL_DEF Meshについては、顔に関連づいているため、単位行列のTransformになります。

    // UnityChanの顔のBoneのTransform
    glm::mat4 meshMat = {
        {0.0157026369f, 0.0416036099f, 0.999011219f, 0.f},
        {0.990548074f, 0.136814535f, 0.00987200066f, 0.f},
        {-0.136268482f, 0.989723265f, -0.0433587097f, 0.f},
        {-128.945908f, -16.3280373f, -1.34681416f, 1.f}
    };

UniyChanのアニメーションでは、Bind poseも一緒に入っているので、それが描画されないようにする必要があります。Assimpで読み込んだ場合、Animation内の時刻が0から1までの期間がBind poseの状態を取ります。なので、以下のように書いて、0から1までの期間を除外しています。しかし、このように書いても、アニメーション間を移行するときになぜかBind poseが途中で発生してしまいます。何かしらの改善が必要です。

        float animTicks = mAnimation->GetAnimTicks(mAnimTime, mAnimIdx);
        mAnimTicks = fmod(animTicks, mDuration - 1);
        mAnimTicks++;
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0