UpDate:--/--/--次回作用にXファイルのアニメーション再生ルーチンをワンスキンに対応させたので公開しておきます。ワンスキンって何ぞやということに関しては、こことかこことかこことかで調べましょう。以前の記事『DirectX8のアニメーションな話』をベースにしているので、まだ見ていない場合はそちらを先にご覧下さい。時代はシェーダーによる実装に移りつつありますが、ここではソフトウェア処理で行います。代わりに前のプログラムからの変更点はわずかです。ついでにTransformマトリクスの扱いの改善と行列キーの対応、メモリからの読み込み対応、ハンガリー記法をやめた(笑)等、細かい修正を行ってます。ワンスキンのXファイルを眺めて、親子付けのXファイルとの違いを拾っていきましょう。いくつかテンプレートが追加されているので、MSDNと照らし合わせながら見ていきます。XSkinMeshHeader、ここでボーンの全般的な設定が記述されます。次にVertexDuplicationIndices、これはメッシュ内のどの頂点が互いに重複しているかという情報を保持しています。頂点の重複状態を表すインデックス値この2つは内部的に使用されるだけのようなので軽く流しちゃいましょう。スキニングで中心になるデータはSkinWeightsで、こいつはMeshに属してます。一つのMeshの中に複数のボーンが存在可能です。メッシュの頂点をボーン空間に変換するトランスフォーム行列でもってこいつらはD3DXLoadSkinMeshFromXofで読み取られます。親子付けXファイル読み込みで使っていたD3DXLoadMeshFromXofに比べてppBoneNames、ppBoneTransformsが増えてます。ppMesh);ppBoneNames->GetBufferPointer()でtransformNodeNameが読み取れる。ppBoneTransforms->GetBufferPointer()でmatrixOffsetが読み取れる。自体はFrameで表現されます。実は、ボーンのアニメーション自体は親子付けとまったく同じ処理が行われます。ボーンは実体のない仮想的な存在なので、持っているデータは初期姿勢をあらわすFrameTransformMatrixとアニメーションを指定するAnimationSetだけです。以上Xファイル観察結果でした。後はアニメーションしているボーンに対してポリゴンを肉付けすればOK。頂点に対する複数のボーンのウエイト値を足し合わせると1になります。各ボーンの姿勢行列を線形補完しているだけって匂いがプンプンしますね。理屈は実にしょーもないんで、あとはDirectX環境でどう実装するかだけです。そのままでは表示できません。そこで登場するのがGenerateSkinnedMesh関数とUpdateSkinnedMesh関数です。pMesh);GenerateSkinnedMesh関数を使ってLPD3DXSKINMESHからLPD3DXMESHを取得します。食わせるとボーン変形後のLPD3DXMESHを返してくれます。求められます、んー楽勝ですね。姿勢行列はCD3DFrameクラスの中に存在するのでボーン名transformNodeNameを手がかりに検索すれば簡単に手に入ります。error.と言って怒られてしまい、動いてくれません。テキスト形式でXファイルを作ることで対処しています。いい事ですがこのエラーメッセージから察するに、Xファイルパーサーはyaccで書いてあるみたいですね。ExportオプションのFrameStepsをある程度小さくすることをお勧めします。いって小さくしすぎるとデータが巨大になるので、適当な見切りも必要です。各行列がまとめられているので当然)キーフレーム間の補間が不可能なので再生速度を落とした時、動きがカクついてしまいます。この辺も含めてアニメーションデータの特性に応じた良いセッティングを選ぶ必要があります。.NETで書いてあるので、VC6以下の人は自分でプロジェクトを作り直してください。あと外部依存ファイルに対するパスは自分で直してください。毎度のことですが、エラーチェックは最低限であり、このサンプルの使用によるいかなる不幸も当方では責任を負いかねます。UpDate:--/--/--無知とは罪である。Windowsプログラム始めたころ、CreatePenでペンを作成してDeleteObjectを呼ばないプログラムを平気な顔をして書いていたものです。挙句の果てにはウインドウ右上のボタン類の表示が数字に化けてびっくりした事があります。今回のお話はC++でDLLを書くというネタ。以前CでDLLを書いた事があったので、余裕をかましていたらひどい目にあいました。そんなわけで参考文献、これとか、これとか、これを読みながら覚書を書いておきます。知らない事があるかもしれないので信用しきってはいけません。無知とは恐ろしい事なのです。メモリ管理メモリの確保、解放に関して注意すべきことがあります。簡単な事で、「DLL内で確保したメモリはDLL内で解放しなければならない」ということです。newやmallocのルーチンがアプリケーションとDLLで異なっている可能性があるからです。コンパイラが違えば当然ですし、リリースビルドとデバッグビルドでもリンクされるランタイムが異なるので危険です。命名規則C++の関数名はコンパイラによって勝手に書き換えられます。hogehogeクラスのmoemoe関数はVCを使うと?moemoe@hogehoge@@UAE_NXZといった感じになります。manglingというらしいんですが、この作業によってC++のオーバーロードの実装なんかが可能になっています。この名前の変換はコンパイラ毎に異なっています。.defが各コンパイラ間で使いまわせるのか謎です。関数が増えるとそれだけ関数名の指定をしなければいけないので大変です。いえ、自作のプログラムの場合DLLもそれを使うプログラムもVCだからと割り切ってしまうなら、__declspec(dllexport)を使うことで.defがいらなくなります。a;a.Func();}こんな感じでしょうか。__declspec(dllimport)#endifってな風にマクロでごまかすのも手かもしれません。"DLLEXPORT"を指定すればOK。DLLTest.cppをコンパイルするとexeとlibができて再度関数がエクスポートされるという不思議な事になります。それでも動くみたいですが。クラス宣言に__declspec(dllexport)みたいな妙なものが付くのが嫌な場合は真面目に.defを書く事になります。mangling後の関数名をdefに書き写し、そのdefをプロジェクトに組み込んで再度コンパイルといった流れです。DLLのバージョンアップDLL化してうれしい事の一つに、アプリケーションを一切変更することなく、新しいDLLに差し替えるだけで性能を上げることができるという事があります。x[30000];Func();};とかヘッダの構成を変えてしまうと危険です。古いヘッダを前提としたアプリケーションではメモリアクセス違反を引き起こしてしまいます。MFCのDLLがMFC42.DLLみたいにバージョンごとにファイル名を変えているのはこのためです。これでは古いアプリケーションでは古いDLLを使う事になるので、バージョンアップの恩恵を受ける事ができません。manglingのことを気にしなくていい代わりに、エクスポート序数をこちらの思い通りに制御できません。@1と書く時の@に続く数のことですが、関数名とこの数を常に同じになるようにしておけば、DLLのバージョンを上げてもアプリケーション側の変更は必要ありません。__declspec(dllexport)を利用するとDLLの変更の度にアプリケーションを再コンパイルする必要が出てくるわけです。インターフェースクラスさすがに、クラス定義を変更できないのはつらいので、実装クラスの上にインターフェースクラスをかぶせて誤魔化します。Test;として実装クラスの名前を導入するというテクニックは知らなかったですね。*p;として(Test*)p->Func();といったvoidポインタのキャストでなんとかなるんですが。このようなラッパーを用意する事で実装は隠蔽されるので、安心してDLLのバージョンアップが出来ます。Testクラスの関数が増えるたびにITestクラスの関数を増やしていかなければならないのは大変なので、インターフェースクラスを純粋仮想関数にしてこれを実装クラスに継承させます。ITest{public:Func();};こうすることでIDLL.cppが不要になって手間がかかりません。ついでに仮想関数にアクセスするためのテーブル(vtbl)の構造がWindows上のコンパイラなら同じらしいので、コンパイラ間の差異を吸収することが出来ます。良い事づくめに思えますが、インターフェースクラスが純粋仮想関数になってしまったのでクラスのインスタンスを作る事が出来ません。Test();}というインスタンス生成用の関数を用意します。this;}と、自爆用の関数を追加します。CreateInstance();pTest->Func();pTest->Delete();}こんな感じで使います。ちなみに.libを生成する事でCreateInstanceをそのまま使う事ができますが、代わりにDLLを任意の場所に置くことができません。私の知る限りでは.libを使ったDLLの暗黙的リンクを行うとDLLをsystemディレクトリや、実行ファイルのある場所におかなければなりません。どこにDLLを置いても使えるようにしたいので、.libが不要なLoadLibraryによるDLLの明示的リンクを行いましょう。manglingされると鬱陶しいので.defもあわせて作る必要があります。あと注意しなくてはならないのは、IDLL.hを必ずDLLをビルドしたときのものとアプリからDLL使うときのものとで一致させなければならない事です。仮想関数を追加するのはもちろん、宣言する順番を入れ替えるのも不可です。IDLL.hはvtblの構造そのものなので、このフォーマットが崩れたら最後、まともに動作しません。DLLを作ってる最中に色々試行錯誤していると、ついうっかりIDLL.hを揃えるのを忘れてしまう事があるので気をつけましょう。そんなこんなで、それなりな感じのDLLのフレームワークはこちら。いやはや、DLL内のクラスを使うのがこんなに大変だとは思いませんでした。DLLのバージョンアップその2を追加。UpDate:02/07/09Windowsでプログラムを書くにあたって頭の痛いことの一つに環境依存性があります。このマシンでは動くのにあっちのマシンでは動かないといったことは日常茶飯事です。OS、CPU、メモリ、ドライブ、グラフィックカード、etc...原因は山のようにあって、いいかげん嫌になってきます。結局、この手の問題に対処するには長年の経験と勘によるしかないような気がします。マシンの環境を洗いざらい調べないことには始まりません。環境に依存することがあるんですね。そのあたりのノウハウを含めて、役に立つような立たないような環境チェッカーを作ってみました。使わないでGetProcAddressで関数ポインタとして取得することが挙げられます。しないと、その関数がシステムのDLLに含まれていないOSではプログラムが不正終了してしまいます。それでは一つずつ解説しましょう。【OS】MSDNに判定法が出ています。【メモリ】GlobalMemoryStatusを呼ぶだけです。4G(場合によっては2G)を超えるメモリが搭載されていると正しい値を返しません。その時はGlobalMemoryStatusExを使えば良いのですが、この関数はWin2000以降用です。【ブートモード】GetSystemMetricsを呼ぶだけです。【ユーザー名】GetUserNameを呼ぶだけです。【ドライブ】ドライブの存在チェックはGetLogicalDrives、ドライブ種別はGetDriveTypeボリューム名等ドライブ情報はGetVolumeInformationを呼びます。知りたい情報の容量については、Win95ではGetDiskFreeSpaceExが使えない、NTはGetDiskFreeSpaceExA(ASCII版)が失敗するらしい、98ではGetDiskFreeSpaceExW(Unicode版)が失敗する、とややこしいことになっています。OS毎に使えるものを選択しなければなりません。GetDiskFreeSpaceは容量2Gまでという制限があります。ダイアログを抑制します。【ネットワーク】WinSockを使います。うまくいかないのでver1.1で作ります。gethostname、gethostbynameでホスト名とIPアドレスが取得できます。【実行ファイルパス】GetModuleFileNameを呼ぶだけです。【CPU】cpuidというアセンブラ命令を使います。この命令は486の後期辺りのCPUから実装されているもので、CPUの情報を山盛りで教えてくれます。問題は山盛り過ぎて大変ということなんですが、頑張ってプログラムにしてみました。今回のメインです、情報源はここ。CPU周りに関してはWCPUIDクラスのチェックをしてます。level(Crusoe用)まで使っています。クロックを計測するにはrdtscという命令を利用します。一度rdtscを呼んでからしばらく時間がたった頃を見計らってrdtscを再度呼び出してその差分からクロックを計算します。2回のrdtsc呼び出しの間にどれだけ時間を置くかで計測精度が変わります。ソースは人様のを流用。【グラフィック】デバイスドライバー名の取得にはEnumDisplayDevicesを使います。MSDNではWin2000以降となってますが実は98でも使えたりします、NTは??。失敗することがあるので、代わりにGetDeviceCapsを呼びます。周波数を知るにはEnumDisplaySettingsを使うしかないのですが、95系では取得できません。こんな感じでいくらでも調べることができるので、きりがありません。上に挙げたものがメジャーなものでしょう。あとは必要に応じて追加してください。EnumDisplayDevicesをNT(SP6まで上げても)で使おうとするとランタイムエラーが出るみたいです。新しい奴だと2000以降対応になっています。NTでは使わない方が無難ですね。OSVERSIONINFOEXの構造体メンバwProductTypeはVC6のヘッダには入ってない模様。VC7を使うのが吉です。UpDate:--/--/--まずはこちらのデモをご覧ください。見ると結構インパクトがあります。HDDを整理していて出てきたこのデモに触発されて今回はテキスト動画を普通の動画で作るという無駄極まりない事にチャレンジです。フォントデータを当てはめていけばOK。その画像を再びAVIとして書き出せば完成です。Interface)の高レベル関数を使ってやってたんですが一見非常に簡単で良さそうな感じでした。MCIWndCreateでウインドウをWS_VISIBLEで作ってMCIWndOpen、MCIWndPlayしてやるだけでいきなり動画が再生されます。MCIWndCreateの返値がHWNDを返すので、これに対してGetDCしてやれば画面をキャプチャできます。一コマずつキャプチャしたいならMCIWndSeekでシークするだけですね。ひとつは再生しているウインドウに、ほかのウインドウが重なると、キャプチャ画像にそのウインドウが入ってしまう点。ひとつはMCIWndSeekの動作が非同期臭い?点です。これは使えないってことで他のAPIを探すとMCI系でもう少し低レベルなAPI群があったのでそちらを使うことにしました。頭にAVIという頭文字がついてる一連の関数はその名の通り、AVIファイルを操作するためのものなのですが、ファイルを操作するだけで映像は出してくれません。今回の場合は単なるコンバータなのでそれはそれで構いません。ついでにMCI系のコマンドはCodecを勝手に処理してくれるので、こちらが圧縮フォーマットを気にする必要がありません。これまた使い方は簡単でAVIFileOpenしてAVIStreamGetFrameするだけでフレーム画像へのポインタが取得できます。画像を加工後、AVIに書き出すときは同様にAVIFileOpenしてAVIFileCreateStream、AVIStreamSetFormat、AVIStreamWriteするだけです。最後にAVIStreamRelease、AVIFileReleaseをちゃんと呼んであげないと正常なAVIファイルにならないので注意。今回のソースはこちら。ソースの動画が24bitカラーであることを仮定しているので注意。それ以外の色モードで使うときは自分でなんとかしてやって下さい。font.bmpがフォントデータになっているので、このファイルは必須です。これも24bitカラーに決め打ちしてます。出力は無圧縮AVIなのでHDDの容量を食います。音声はコンバートしないので、適当なツールで結合する必要があります。手抜きで申し訳ないです。サンプル動画はこちら。サンプルの内容に対する突っ込みは無しです。『動きが大きい』『アップが多い』というのが良いと思われます。手持ちの画像を漁ったらこれしかなかったと言う訳です。余談ですがカラーからモノクロに落とすときはRx0.30+Gx0.59+Bx0.11を計算してあげます。人間の目にはRGBの明るさがそれぞれ違って感じるらしいですね。UpDate:02/07/03そんなわけでOggVorbis用のサウンドドライバを書いてます。ライブラリが正式リリース前のものなのでドキュメントの手抜きが散見されますね。vorbis_synthesis系の解説が無いような...、デコードの中心部分なのに。3月中旬から怒涛の勢いで本家のライブラリがバージョンアップを繰り返しているので、ドキュメントの方もじきに整理されてくるのかもしれません。libの差し替えだけで問題ないと思われるのでOggVorbisドライバーのコードを公開しておきます。しょぼいですが、サンプルソースってことで、ご愛嬌。この記事からプロジェクトがVisualStudio.NET用になっているので注意。VC6以下のコンパイラではプロジェクトを自分で作り直してください。インクルードパスに.\includeを含め、ランタイムライブラリをマルチスレッドに、デバッグリリース時にはライブラリLIBCMTDを無視するようにすればOKです。ドライバ本体だけで使いたい方はこちらにDLL化したものがあります。マルチスレッドなプログラムなので、他のプログラムに組み込んでも特に再生処理を意識する必要はありません。おまけで多重再生OK。フェードといったところ。ボリューム調節はできるんですが、waveOutSetVolumeを使うとどうやらWindowsすべてのボリュームを変更してしまうようです。荒業としてはデコードしたPCMデータをひとつずつ手作業でスケーリングしてやればいいんですが、そんな大量の割り算したら速度的に悲しいことになるかも。16段階でいいならビットシフトという手もあるんですけどね。waveOut系で実装するのはあまり宜しくないのかも知れません。秒間44.1k回も割り算をしたら大変かと思ったら、そうでもなかったです。少なくともCeleron300クラスでは誤差の範囲内でした。[edi-4]と、FPUに投げちゃってるんですよね。それも乗算命令に置き換えられてます。ついでにFPUとCPUが並列に処理されていれば遅くならないわけです。[ebx+10096]になるというだけのことでした。いうわけでソースを修正したついでにフェード関数を追加しました。フェード周りがいまいちだったので修正しました。バグ修正版に差し替え。