2022年6月29日水曜日

DirectX12 Skeletal Animation With Assimp

DirectX11で3Dライブラリを用意していたが、DirectX12で作り直すことにした。

経緯はDirectX11で作るときにも11にするか12にするか検討したが、12はハードルが高いということだったので手っ取り早く11で作ってみた。
作ってからほかが忙しくなり、それからしばらく半年?1年くらい経って、ライブラリ開発を再開しようとしたら何がなにやらわからなくなっていた。

というわけで11のライブラリは捨てて12で作り直しすることにした。
11と同じことができるようになるまで1ヶ月ぐらいかかったがなんとかできた。
ここから更にメッシュデータの表示に手をだして見ることに。
DirectX3ぐらいのときにスキンメッシュについては全く理解できなかったので、敬遠していたが「DirectX 12の魔導書 3Dレンダリングの基礎からMMDモデルを踊らせるまで」という本を買って、pmdのロードからvmdのロード、FK、IK、表示までを作ってみた。
FKまではうまく行っているように見えるが、IKの部分でクリーチャーが出現する。

本の誤植もひどく、10回ぐらいソースを見直して著者のgithubと見比べながら直したが、そもそも著者のソースの中にこっちが正しいはずなのになんで?みたいなコメントもあり信用できなくなった。PCのkindleも使いづらく、まともにスクロールも検索もできない。
著者のソースはライブラリを別途用意しないとコンパイルできなかったのでずっとソースだけ参照していたが、どうしてもうまくいかないのでライブラリも準備してコンパイルを通して実行してみたところ、著者のプログラムでも全く同じ結果になっていた。
それはいくらやっても無理なはずだ・・・。

そもそもpmxならまだしもpmdが表示できるようになっても意味はなく、blenderとかで用意できる汎用的なファイルフォーマットはないのかと調べたところfbxがいいらしいということがわかった。

今度はfbxの読み込みライブラリについて調べていくとassimpというものにたどり着いた。
このライブラリを使えば、fbxだけでなくいくつかのフォーマットを読み込むことができるのでこのライブラリを使ってみることにした。

オープンソースのバージョン管理に疲れたので、NuGetに対応してないと適用をしたくない病にかかっていたが、問題なくあった。ただし、個人ではなく公式のものがいいので、Assimp_native、Assimp_native_4.1、Assimp_native_v142が候補だが、Assimp_nativeはVer4.0.1で他のVer4.1よりも古い。Assimp_native_v142はvs2019用で、ラインタイムを別途用意しないと動かない。一番いいのは開発しているvs2022用のv143だが、存在せず。Assimp_native_4.1はv140だが、ランタイムがwin11にデフォルトで入ってるっぽくしばらくはこれを使ってみることにする。
assimpの最新バージョンは5.2.4(2022/6/29時点)らしいので、どこかで新しくしたい。
v143がないので最悪自分でビルドか・・・

次にこれを使ったサンプルを探すとこんなサイトを見つけた。
やってることはまさにこれで、このチュートリアルで勉強してみることにした。
モデルの読み込み自体はassimpがやってくれるので特に問題はなかったが、フラグがいまいちよくわからなかった。
最終的なフラグはこんな感じ
		tSStr sSFile = sFile ;	// ここはsjis
		auto nFlag = aiProcess_ConvertToLeftHanded ;
		nFlag |= aiProcess_Triangulate ;
		nFlag |= aiProcess_JoinIdenticalVertices ;
		nFlag |= aiProcess_CalcTangentSpace ;
		nFlag |= aiProcess_GenSmoothNormals ;
		nFlag |= aiProcess_GenUVCoords ;
		nFlag |= aiProcess_TransformUVCoords ;
		nFlag |= aiProcess_RemoveRedundantMaterials ;
		nFlag |= aiProcess_OptimizeMeshes ;
		nFlag |= aiProcess_LimitBoneWeights ;
		auto pScene = Importer.ReadFile( sSFile.Ptr(), nFlag ) ;
assimp内の文字列はutf-8っぽいんだけど、ReadFileにわたす文字はutf-8ではなくsjisみたい。DirectXを使うならaiProcess_ConvertToLeftHandedを指定して、左手系にする必要がある。OpenGLは右手系みたい。
マニュアルを見ると、このフラグはおすすめ、効果的みたいな感じでどれをつけていいか悩む。読み込んだあとデータを加工してくれるようでそれなりに時間がかかるのでOn/Offできるようにしよう。

頂点データ、インデックスデータ、マテリアルを読み込んで描画する分にはすぐにできた。
両手を広げたおっさんが表示されるようになった。
いざアニメーションに取り掛かろうとしたけど、アニメーションデータがない。確実に入っているはずのデータを読み込んでもアニメーションデータがない。
他のサイトを見たとき、aiProcess_PreTransformVerticesが指定されているサンプルがあって、それをつけてたんだけどこれをつけてるとアニメーションデータが削除されるみたい。罠だった。

アニメーションデータが参照できるようになって、ボーンデータとアニメーションデータの取り込みをしていざ表示してみるとうまくいかなかった。
このサイトはOpenGLのチュートリアルで、それをDirectXの関数に直してみたんだけど、どこが悪いのかがわからず1週間ぐらい悩むことになる。
DirectX assimp animationとか検索文字列を変えながら検索結果の全部のサイトを見てみるけど、DirectXで動くソースがあるサイトは見つからず普段あまり見ない中国のサイトで1つそのものズバリのソースを見つけた。
VisualStudioで読み込んでみると文字コードがおかしくて中国語が含まれてるのに、sjisで読み込むもんだから文字化けしまくり。コメントだけならどうせ読めないから問題ないとコンパイルしてみるも、エラーだらけ。コメント部分の後ろにソースが巻き込まれてる。全部改行を入れていってコンパイルしたらなんとかコンパイルは通ってモデルも表示されていた。これでなんとかなると思ってたら何回か実行すると起動しなくなった。メモリ関連に致命的な問題があるのか、動いていたexeが何回か動かすと動かなくなるなんて初めて体験した。でもまあ、動いたソースなので参考にはなった。

まず、assimpから取り出すaiMatrix4x4はすべて転置が必要。参考にしたソース全部がXMMatrixTransposeを使っていたが、下記のような変換関数で直接代入した。
(tDFloat44はDirectX::XMFLOAT4X4の別名)
	auto fConvToDFloat44 = []( const aiMatrix4x4 & m ) -> tDFloat44 {
		return tDFloat44(
			m.a1, m.b1, m.c1, m.d1,	// 転置
			m.a2, m.b2, m.c2, m.d2,
			m.a3, m.b3, m.c3, m.d3,
			m.a4, m.b4, m.c4, m.d4
//			m.a1, m.a2, m.a3, m.a4,	// そのまま
//			m.b1, m.b2, m.b3, m.b4,
//			m.c1, m.c2, m.c3, m.c4,
//			m.d1, m.d2, m.d3, m.d4
		) ;
	} ;
pScene->mRootNode->mTransformationの逆行列を用意する部分と、ボーン情報のaiBone.mOffsetMatrix、アニメーションノードのaiNode.mTransformationの取り出す際に上記関数で取り出す。

すべてのポイントはReadNodeHierarchy関数。
OpenGLとDirectXでは行列の掛け方が逆らしい。

位置と回転、スケールの補完したあとの行列の計算
    NodeTransformation = TranslationM * RotationM * ScalingM;
    ↓
	NodeTransformation = ScalingM * RotationM * TranslationM;

上記結果と親の行列をかけ合わせる部分
(tDMatrixはDirectX::XMMATRIXの別名)
    Matrix4f GlobalTransformation = ParentTransform * NodeTransformation
    ↓
	tDMatrix GlobalTransformation = NodeTransformation * ParentTransform ;

ボーンマトリックス計算部分    
	m_BoneInfo[BoneIndex].FinalTransformation = m_GlobalInverseTransform * GlobalTransformation * m_BoneInfo[BoneIndex].OffsetMatrix;
    ↓
	m_BoneInfo[BoneIndex].FinalTransformation = m_BoneInfo[BoneIndex].OffsetMatrix * GlobalTransformation * m_GlobalInverseTransform;

また、CalcInterpolatedScaling、CalcInterpolatedPositionの部分はDelta計算不要でXMVectorLerpで置き換えられた。
    aiVector3D Delta = End - Start;
    Out = Start + Factor * Delta;
    ↓
	return DirectX::XMVectorLerp( Start, End, Factor ) ;

CalcInterpolatedRotationはXMQuaternionSlerpで置き換えられた。
    aiQuaternion::Interpolate(Out, StartRotationQ, EndRotationQ, Factor);
    Out = StartRotationQ;
    Out.Normalize();
    ↓
	return DirectX::XMQuaternionNormalize( DirectX::XMQuaternionSlerp( StartRotationQ, EndRotationQ, Factor )) ;

VertexShaderはBoneTransformとposを掛けて、それをProjction*View*WorldかかってるTransと掛ける。
	float4x4 BoneTransform
		=  Bones[boneid[0]] * weight[0]
		+  Bones[boneid[1]] * weight[1]
		+  Bones[boneid[2]] * weight[2]
		+  Bones[boneid[3]] * weight[3]
	;
	Out.pos = mul( Trans, mul( BoneTransform, float4( pos, 1 ))) ;

一番嵌ったのは、boneidを頂点データと送り込む際、メッシュ単位でデータを作るのでメッシュ単位の頂点データのオフセットを足すのを忘れていて、これが原因で正しく描画できていなかった。
最終的にはこんな感じで動くようになった。

0 件のコメント:

コメントを投稿