【実装編③】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クラスを見直してみると、より理解が深まるのではないでしょうか。
アニメーションを再生したいだけなのに、思った以上に複雑な処理が必要で驚いたかもしれません。
当たり前に動いている仕組みでも、自分で実装しようとすると本当に難しいですよね。
でもきっと、新しく理解できたことは自分の力になっているはずです!
これらの記事を通して、少しでも解決のヒントになれば幸いです。