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

2022年4月21日木曜日

Moveの動作

class T
{
public :
	T( void ) { mLogV( L"Constructor") ; }
	T( const T & o ) { mLogV( L"Copy Constructor") ; }
	T( T && o ) { mLogV( L"Move Constructor") ; }

	T &			operator = ( const T & o ) { mLogV( L"Copy Operator") ; return *this ; }
	T &			operator = ( T && o ) { mLogV( L"Move Operator") ; return *this ; }

	static
	T			Func1( tV ) { T o ; return o ; }				// Make系関数 自動でmove扱いになる
	T			Func2( tV ) { return *this ; }					// コピー
	T &			Func3( tV ) { return *this ; }					// 参照 呼び出し元でstd::moveにすると、move扱いに。
//	T &&		FuncX( tV ) { return *this ; }					// コンパイルエラー
	T &&		Func4( tV ) { return std::move( *this ) ; }		// move
//	T &&		FuncX( tV ) { T o ; return o ; }				// コンパイルエラー
	T &&		Func5( tV ) { T o ; return std::move( o ) ; }	// move Func1の強制版。通常ここまでやる必要なし
	static
	T			Func6( T o ) { return o ; }						// 引数は渡し方により、copy、moveを操作可能 returnはmove
	static
	T			Func7( T & o ) { return o ; }					// 引数は参照 returnはcopy
	static
	T			Func8( T && o ) { return o ; }					// 引数はmove returnはmove

} ;
    T o ;							// Constructor

    T o1 = o ;						// Copy Constructor
    T o2 = std::move( o ) ;			// Move Constructor
    T o3 = o.Func1() ;				// Constructor - Move Constructor
    T o4 = o.Func2() ;				// Copy Constructor
    T o5 = o.Func3() ;				// Copy Constructor
    T o6 = std::move( o.Func3()) ;	// Move Constructor
    T o7 = o.Func4() ;				// Move Constructor
    T o8 = o.Func5() ;				// Constructor - Move Constructor

    o = o1;							// Copy Operator
    o1 = std::move( o );			// Move Operator
    o3 = o.Func1() ;				// Constructor - Move Constructor - Move Operator
    o4 = o.Func2() ;				// Copy Constructor - Move Operator
    o5 = o.Func3() ;				// Copy Operator
    o6 = std::move( o.Func3()) ;	// Move Operator
    o7 = o.Func4() ;				// Move Operator
    o8 = o.Func5() ;				// Constructor - Move Operator

	o = T::Func6( o ) ;				// Copy Constructor - Move Constructor - Move Operator
	o = T::Func6( std::move( o )) ;	// Move Constructor - Move Constructor - Move Operator
	o = T::Func6( T()) ;			// Constructor - Move Constructor - Move Operator

	o = T::Func7( o ) ;				// Copy Constructor - Move Operator
//	o = T::Func7( std::move( o )) ;	// コンパイルエラー
//	o = T::Func7( T()) ;			// コンパイルエラー

//	o = T::Func8( o ) ;				// コンパイルエラー
	o = T::Func8( std::move( o )) ;	// Move Constructor - Move Operator
	o = T::Func8( T()) ;			// Constructor - Move Constructor - Move Operator

2019年11月29日金曜日

Luaのテーブル上限

luaのスクリプト形式で永続化情報を出力して、luaに再読み込みさせると
「function at line 1 has more than 65536 constants」
というエラーが返ってきた。

ネットでテーブルの上限を調べてみても、メモリ次第という答えしか見つからなかった。

テストプログラムで実験してみると、配列の要素数、キーの数には制限がなさそうだけど
テーブル要素が65536を超えると、前述のエラーが出る。

o = {
{a=1},
{a=1},
{a=1},
.
. 全部で65536個
.
{a=1},
}
エラー。
oと中身の65536個を足すと65537個になる。中身が65535個ならエラーにならない。

1変数内の最大数が65536までなのかと思い下記のコードを実行
o = {
{a=1},
.
. 全部で35535個
.
{a=1},
}
o2 = {
{a=1},
.
. 全部で30000個
.
{a=1},
}
エラー。
oと中身の35535個、o2と中身の30000個で65537個。
oの中身を35534個にすればエラーにならない。

グローバルスコープで宣言できる数が、65536までらしい。
function x()
o = {
{a=1},
.
. 全部で65535個
.
{a=1},
}
end
g = {
{a=1},
.
. 全部で65533個
.
{a=1},
}
OK。

x関数のスコープ内はoと中身で65536個、グローバルスコープはgと中身で65534個+関数で65536個と数えるらしい。関数は2個扱い?
gの要素数を65534個にするとエラーになる。
x関数内にo2を増やしても、g側の要素数には影響しなかった為、x関数内の変数の数は関係ないようだ。

関数を増やしてみる。
function x()
.
.
.
end
function y()
.
.
.
end
g = {
{a=1},
.
. 全部で65531個
.
{a=1},
}
OK。

x関数、y関数、gと中身の65531個で65536個。
gの中身を65532個にするとエラーになる。
関数を3個に増やした場合、gの要素を65529個にすればエラーにならない。
やはり関数は2個と数えるようだ。

関数を要素に入れてみる。
g = {
{a=1},
.
. 全部で65533個
.
{a=1},
f = function() return {} end
}
エラー。
なぜか、関数を要素に入れる場合は3個で数えるようだ。
gの要素を65532個にするとエラーでなくなる。

関数を2つ入れてみる。
g = {
{a=1},
.
. 全部で65531個
.
{a=1},
f = function() return {} end,
f2 = function() return {} end
}
なぜか、2つ目は2個でエラーにならない。
3つ目も同様で2個扱い。
最初の関数定義だけは、3つで数えるらしい。

最後定義されている関数を入れてみる。
function x() return {} end
g = {
{a=1},
.
. 全部で65531個
.
{a=1},
f = x
}
定義されている関数を追加する場合は1つ目の関数だとしても2つ計算となった。

2017年7月19日水曜日

オーナードローでサブメニューの▶を描画する方法

メニューのオーナードローをした時、どうしても制御できないのがメニューのボーダ部分とサブメニューの▶の部分
いろいろ調べて▶の部分が制御ができるようになった。

オーナードローをしても、枠の灰色の線とSubの隣の▶の色が環境依存で、指定が出来ない。
▶については、通常黒で、選択すると反転して白になる。
左図のような黒背景だと▶が見えない。
DrawItemイベントでは描画しておらず、その後に勝手に描画されるからSetTextColorとか適用されないかな?と思ったけど無視される。

やり方は、ExcludeClipRectでメニュー項目全体をクリップすると、勝手に描画されるのを抑制できるので、あとは自分で描画するだけ。

通常状態でも白で表示出来るようになった。

2017年5月16日火曜日

VS2017のAllocConsoleでUnicode(UTF-16)出力

std::wcout.imbue(std::locale("Japanese")) ;
AllocConsole() ;
FILE * pF = _wfdopen( h, L"w" ) ;
fclose( stdout ) ;
*stdout = *pF;
setvbuf( stdout, NULL, _IONBF, 0 ) ;

VS2010では上記のコードで、wcout、wprintfを使ってコンソール出力が出来た。

VS2017でコンパイルすると、コンパイルは通るが実行すると落ちる。
いろいろ調べてみた結果、_wfreopen(freopen)でstdoutを"CONOUT$"で開き直すとコンソールと紐付けは出来るようだ。

でも、printfすると落ちる。
wprintfすると表示されなかったり、表示されるが16進文字だったり、文字化けしていたり。
wcoutは日本語は表示されるが、"\n"、endlの部分で落ちる。
coutはやってない。
唯一、表示されるのがWriteConsole関数。

試してみたのは下記のキーワード
wcout.imbue
_wsetlocale(setlocale)
setvbuf
_setmode
wcout.rdbuf
SetConsoleOutputCP
sync_with_stdio

どうやってもまともに表示されない。

色々やっているとwcoutで落ちるのは、rdbufが準備できていない?感じで、rdbufを入れ替える方法を見つけたのでwcout以外は全部捨てて対応したコードが下記。

class Out {
private :
class Buf : public std::wstreambuf {
private :
HANDLE h ;
public :
virtual int_type overflow( int_type c = EOF ) override {
if( c == EOF ) return c ;
wchar_t sBuf[] = { c, '\0' } ;
DWORD nW ;
WriteConsole( this->h, sBuf, 1, &nW, NULL ) ;
return c ;
}
Buf( void ) {
this->h = GetStdHandle( STD_OUTPUT_HANDLE ) ;
}
} oBuf ;
std::wstreambuf * pOld ;
public:
Out( void ) { this->pOld = std::wcout.rdbuf( &oBuf ) ; }
~Out( void ) { std::wcout.rdbuf( this->pOld ) ; }
} ;

AllocConsoleした後に、Outクラスのインスタンスを作ると、wcoutのバッファがoverflow関数に1文字ずつ渡ってくるらしい。内部では唯一まともに動くWriteConsoleを呼び出す。

2013年6月24日月曜日

Chrome Secure Shellでペーストの方法

Ctrl+VとかAlt+Vとか試してみるもペーストできない。

スクリーン上の文字列を選択すると、画面の真ん中に
Selection Copied
と表示される。

これが表示されるということはクリップボードの値を貼り付けることが出来るはず。

マニュアルを調べてみたら
Ctrl+Shift+V
だって。

2013年6月4日火曜日

Zlib 1.2.8

zlibのバージョンを1.2.8に上げた所、プログラムが動かなくなった。
デバッグすると、初期化(deflateInit)でエラーが返ってきている。
エラーはZ_STREAM_ERROR。

ネットで検索すると、引数の圧縮レベルが間違っているという。
指定している値はZ_DEFAULT_COMPRESSION(-1)で問題ないはず。
試しに0とか6とか試してみてもダメ。

deflate.cの中までデバッグしてみると、渡している構造体(z_stream)のzallocをチェックしている所でエラーになっている。この値がNULLの場合、Z_SOLOのコンパイルスイッチが定義されていると即エラー。
何らかのアロケート関数を渡す必要があるみたい。
Z_SOLOを定義して無ければzcallocという関数が自動で設定される。

ライブラリのサイズが減ったと思ったら、このへんの関数を削る設定がデフォルトになってたみたい。
元々300KBぐらいで、削減して260KBぐらいになってた。
Windowsデスクトップアプリでは大したサイズではないので、zlibのプロジェクトからZ_SOLOを消してコンパイルし直した所、今までと同じ動作に戻った。