ことれいのもり

【実装編③】OpenSiv3Dでスキニング済みメッシュを描画する方法

はじめに

この記事では、OpenSiv3DとAssimpを使って、FBXモデルのアニメーションを動かす方法を、具体的なコードと共に解説します。


前編の「解説編」では、アニメーションを動かすための流れを紹介しました。

後編の「実践編」では、さらに3回に分けて、OpenSiv3Dで実装する方法を紹介します。


前編:【解説編】AssimpでFBXアニメーションを動かすための理論的な手順まとめ

第1回目:【実装編①】OpenSiv3DでFBXモデルを読み込んでアニメーション準備する方法

第2回目:【実装編②】OpenSiv3Dでスキニングとアニメーションさせる方法

第3回目(今回):【実装編③】OpenSiv3Dでスキニング済みメッシュを描画する方法


この記事は実装編の第3回目です。

今回は「MeshWrapper」クラスを中心に、スキニングされたメッシュの描画について解説します。


なお、このコードは私が考えた物で、OpenSiv3Dを使ってUnityの様なコンポーネント構造を想定したものです。

Assimpを使ってゲームエンジンにFBXモデルを取り込もうとしている方にとって参考になると思います。

記事の中では流れに沿って関数ごとに紹介しますが、コードの全文が見たい方は、記事最後をご覧ください。

前提知識

解説編、実装編①②の内容を踏まえて実装していきます。

以前の記事を見ていない人は、先に見ておくことをオススメします。


ここで、以前の記事の内容を簡単におさらいしておきます。


Assimpを使ってFBX形式のモデルをアニメーションする手順

① Assimpを使ってFBXファイルを読み込む

② メッシュとボーンの情報を抽出する

③ アニメーションを更新する

④ スキニング処理(ボーンの変形処理)をする

⑤ メッシュを更新する


このうち実装編1では、①、③~⑤の処理をFBXModelComponentクラスに記述しました。

実装編2では、②・③・④の中身をSkeletonクラスに記述しました。

MeshWrapperクラスとは?

MeshWrapperクラスは、OpenSiv3Dの Mesh を使いやすくまとめたラッパークラスです。

主な役割は、毎フレーム更新される頂点データを受け取り、MeshDataを再構築し、描画することです。


FBXのアニメーションでは、ボーンに基づいて頂点を動かす「スキニング」という処理が行なわれます。

スキニング処理では、まず SkinnedVertex 構造体に一度的なデータを保存しています。

スキニングが終わった段階で、Vertex3D(OpenSiv3Dの型)に変換されてからMeshWrapperに渡されます。

この話は次の項目「FBXModelComponentとの繋がり」でコードを交えて解説します。


解説編の手順で言うと、

⑤ メッシュを更新する

この処理を担当するのが MeshWrapper クラスです。

FBXModelComponentとの繋がり

ここで、実装編①との繋がりについて説明しておきます。

FBXModelComponentでは、フレームごとにスキニング後の頂点データを生成しています。

これを、FBXModelComponentのUpdateMeshData関数経由で、MeshWrapperクラスに渡します。


void FBXModelComponent::UpdateMeshData()
{
    std::vector<Vertex3D> skinnedMeshVertices;
    skinnedMeshVertices.reserve(skinnedVertices.size());

    for (const auto& vertex : skinnedVertices)
    {
        Vertex3D skinnedVertex;
        skinnedVertex.pos = vertex.pos;
        skinnedVertex.normal = vertex.normal;
        skinnedVertex.tex = vertex.tex;

        skinnedMeshVertices.push_back(skinnedVertex);
    }

    // =======================
    // ここでMeshWrapperに渡す!
    // =======================
    if (meshWrapper)
    {
        meshWrapper->UpdateMeshData(skinnedMeshVertices, indices);
    }
}

更新処理(UpdateMeshData)

MeshWrapperクラスのUpdateMeshData関数では、渡された頂点データを元にOpenSiv3Dで扱えるMesh型を生成します。

このとき、前回のフレームと今回送られてきたフレームの頂点データを比較し、変化がある場合のみ更新します。

これにより、更新が必要がどうかを判定することで、無駄を減らします。


void MeshWrapper::UpdateMeshData(const std::vector<Vertex3D>& vertices, const std::vector<TriangleIndex32>& indices)
{

    if (currentVertices != vertices || currentIndices != indices)
    {
        currentVertices = vertices;
        currentIndices = indices;
        meshData = MeshData(currentVertices, currentIndices);
        mesh = Mesh(meshData);
    }
    else
    {
        Console << U"同一なのでスキップ";
    }
}

頂点・インデックスの比較(演算子オーバーロード)

さきほど、「前回のフレームと今回のフレームの頂点データを比較する」と説明しました。

これを「=, !=」の演算子で比較したい場合、演算子オーバーロードを実装する必要があります。


namespace s3d {

    inline bool operator==(const Vertex3D& lhs, const Vertex3D& rhs)
    {
        return lhs.pos == rhs.pos && lhs.normal == rhs.normal && lhs.tex == rhs.tex;
    }

    inline bool operator!=(const Vertex3D& lhs, const Vertex3D& rhs)
    {
        return !(lhs == rhs);
    }

    inline bool operator==(const TriangleIndex32& a, const TriangleIndex32& b)
    {
        return a.i0 == b.i0 && a.i1 == b.i1 && a.i2 == b.i2;
    }

    inline bool operator!=(const TriangleIndex32& a, const TriangleIndex32& b)
    {
        return !(a == b);
    }
}


このコードを書くことで、Vertex3D 及び TriangleIndex32 同士の比較が可能になります。

描画処理(Draw)

Draw関数は、渡された変換行列(Mat4x4)とテクスチャを使ってメッシュを描画します。

テクスチャが存在しない場合は、そのままメッシュを描画します。

ここが MeshWrapper クラスの核です。


void MeshWrapper::Draw(const Mat4x4& transform, const Optional<Texture>& texture) const
{
    if (texture)
    {
        mesh.draw(transform, *texture);
    }
    else
    {
        mesh.draw(transform);
    }
}

 コード全文

今回紹介したコードを含んだ全てのコードは、Gistにおいてあります。

参考にしてください。

GitHub Gist

おわりに

ここまで見てくださった方、お疲れ様でした。

これで、OpenSiv3DでAssimpを使ってアニメーション再生ができるようになりました。

ここまでの記事を見た上で、メインとなるFBXModelComponentクラスを見直してみると、より理解が深まるのではないでしょうか。


アニメーションを再生したいだけなのに、思った以上に複雑な処理が必要で驚いたかもしれません。

当たり前に動いている仕組みでも、自分で実装しようとすると本当に難しいですよね。

でもきっと、新しく理解できたことは自分の力になっているはずです!

これらの記事を通して、少しでも解決のヒントになれば幸いです。


記事一覧

前編:【解説編】AssimpでFBXアニメーションを動かすための理論的な手順まとめ

第1回目:【実装編①】OpenSiv3DでFBXモデルを読み込んでアニメーション準備する方法

第2回目:【実装編②】OpenSiv3Dでスキニングとアニメーションさせる方法

第3回目(今回):【実装編③】OpenSiv3Dでスキニング済みメッシュを描画する方法