さて、前回は…
プラグインでWPFウィンドウ表示ができることを確認し
オーディオデータを入力に FFT 解析結果を表示するプラグインを作成しました。
今回はプラグイン拡張で深度画像の3D表示を行います。
既存の SharpDX 関連のプログラムをソース管理に追加し、プラグイン用に少々アレンジします。
まずは簡単なプラグインを作ります。
ウィンドウを表示し、その中に3Dオブジェクトを描画するというものです。
今から一年以上前に071話にて、下図のとおり
計測点を3D表示し、その点群を処理して Kinect ケースの姿勢推定を実現しました。
このときの対応を見直し、今回の目的を実現したいと思います。
071話では何をしていたのか詳細に確認します。
…
ポイントスプライトの描画については詳細は何も示していないですね。
だれでも簡単にできるという雰囲気で紹介されていますが、全然簡単じゃないです。
3D描画のハードルは高いと思います。(当時の自分は色々勉強中の身だったので一般的ではないです。)
ということで具体的に示しながら、当時の作業内容を紐解いていきます。
まずShaprDXが必要です。
2014.6現在の最新である 2.6.0 をローカルに引き、これを VisualStudioOnline 管理とします。
環境準備を続けているわけで、このあたりをしっかりメモしていきましょう。
(今から2年前の当時は 2.1.0 だったんですね、懐かしいです。)
VisualStudioOnline でソース管理する話ですが
ストアアプリ開発には、マップドライブを利用するとうまく動作しません。(管理者権限の関係)
C: ドライブでないとパスが見つからないとか言われるので VisualStudioOnline のマップ先は
C: ドライブの下としました。(C:\Work\<ProjectName> のパスでマップしました。)
さて、そこに External フォルダを作り SharpDX フォルダを掘って
2.6.0 フォルダを作成して、その中に SharpDX-SDK-2.6.0.exe を配置
直下に展開するように指示書を付けておき管理します。
これから作成する複数のプロジェクトは SharpDX を参照する場合、この展開後のパスを利用するようにし
環境が変わっても、C:ドライブとWorkパスを変えていなければビルドできる仕組みとします。
プロジェクトごとに参照する DLL や lib を引くようにするのが良いとも思いましたが
パッケージをバージョンフォルダにインストールすれば完了というやり方ならば、プロジェクト間で
共通化できますし、何よりドライブを圧迫しませんのでこの方式を選びました。
C:ドライブとWorkパスを同じにするルールを守ってもらえれば困ることはないはずです。
ポイントスプライトとは関係ないですが、作業を一つ一つ記録していきます。
さて、過去に作成したプロジェクトを VisualStudio 2013 対応して、動作確認していきます。
外部ライブラリとして BulletSharp も使っていました。
こちらも External に追加します。
Kimberlite2 に関しては PropertyTools も使っていますね。
これも External に追加してひとまず移行完了です。
新しい環境でもビルド、実行できるか確認します。
BulletSharp 2.81
SharpDX 2.6.0
PropertyTools 2014.1.11.1
を使っています。
モバイル開発環境である Surface Pro 2 で動作確認できました。
ビルド、実行、3D描画、オブジェクト選択、プロパティ変更、物理演算など特に問題ないようです。
整理している時に確認しましたが、Kimberlite2 はエディタプロジェクトであり、下記のクラスライブラリを利用します。
KimberliteScene 物理演算、3D描画処理全般担当
ColladaBinary モデルデータアクセスとファイルへの読み書き担当
KimberliteNodeTree 描画ツリー関連、ツリー要素を触って描画ノードを直接編集できます。
(KimberliteSceneと切り離し不可、本当は切り離したかった、切り離せなかった)
KimberliteBinary 物理演算用データ全般、描画オフセット情報などへのアクセス、ファイルへの読み書き担当
そして嬉しい事にこれらを利用したベースとなるプロジェクト KimberliteFramework が用意されています。
実装はなく、自由に様々なアプリケーションへと変化させることができる WPF プロジェクトです。
まずはこのプロジェクトを使ってポイントスプライトの描画を実現してみます。
(今回も、やはり詳細表示にならない気がしてきました…が頑張ります。)
まずはシェーダーファイルを確認してみましょう。
GPUに計算させるプログラムが書かれたエフェクトファイルのことです。
詳細はかれこれ3年ほど前に何回か書いています。(031話とか033話とか043話とかTipsDX0001です。)
軽く読みなおしてみました。
…書いてあるとおりです。
シェーダファイル自体は簡単な3D数学の知識があれば大丈夫です。
Kimberlite2 でどうやって実現しているか見てみましたが、ある ColladaBinary ファイルにエフェクトを含ませて
このエフェクトを利用して、各種変数を更新しながら描画しています。
動けば良いだけのコーディングをしている様子だったので、もう一度使いやすいように書きなおしてみます。
まずは KimberliteFramework がどういうものか把握してみましょう。
(そこから…)
描画と物理演算の機能はクラスライブラリが提供してくれます。
下記は描画先の Image をウィンドウに配置した図です。
XAML では Kimberlite 名前空間の DirectXImage という Image を配置します。
コードビハインドは次の通り
BaseWindow がマウスによる視点移動を色々担当してくれます。
名前がこれで良いのか、とにかく DirectXImage を描画するならばマウスで視点移動くらいは提供しておかないと
ユーザーとしてもそこからは作りたくないので、ひとまず Window として最低限の機能として用意しています。
注意する必要があるコードは次のとおり。
ハイライトした行の内容(シーン)を描画します。
シーンは IScene を継承したクラスであれば何でもよく、たとえば BaseScene は Kimberlite の Scene です。
(名前変えます、KimberliteScene とします。)
シーンとは世界のようなもので、複数の世界を用意し、それぞれの世界を描画することが可能です。
DXImage.Scene プロパティを切り替えることで、描画する世界の切り替えが可能となっています。
Debug ボタンは使い方を示す上で用意されているもので、押した時に実行されるコードは次のとおりです。
よい方法とは言えないかもしれませんが、現在描画中のシーンが KimberliteScene であることを確認し
PhysicsFramework にアクセスして指定シェイプの剛体追加を行なうというものです。
実行結果は下図のとおり
さて、KimberliteFramework についてわかってきたところで
Scene.cs に機能を追加していきながら、Intel CREATIVE の距離画像の3D表示を行います。
現在の Scene.cs は下記の通り、何も書かれていない状態です。
ポイントスプライト用のシェーダコードを作成したいと思います。
シェーダを書きながら結果を確認できるのが Kimberlite2 の良い所なので
Kimberlite2 を起動します。
別途 Blender を起動して、Monkey の頂点だけオブジェクトを作成します。
Blender から FBX で書き出し、これを FBXConverter で Collada ファイルに変換します。
Collada ファイルを独自のフォーマットに変換して Kimberlite2 で読み込みます。
…変換に失敗しました。
詳細を追います。
単に出力先フォルダが作られていないだけでした。
出力先フォルダを作成するよう修正、結果コンバートに成功し、変換後のファイルを読み込むことを確認しました。
エフェクトファイルを作成するにはマテリアルが必要なので Blender にてマテリアルを Monkey に適用します。
おや?FBXConverterを介すとマテリアルがうまく作れません。
それに直接 dae を出力するとどうも、法線情報が含まれていない様子…どうなっちゃっているのでしょう?
詳細を調べます。
解決しました。
頂点情報のみの場合は、頂点位置のみで、法線情報は出力されない仕様のようです。
また FBXConverter を用いて変換した Collada は lines ではなく triangles としてプリミティブを出力します。
既存の ColladaToBinary は polylist のみに対応していたので、最初読み込めないだけでした。
triangles に対応することで、このように正しい位置に monkey の頂点描画ができるようになりました。
ポイントスプライトを作成するため、まずは現在描画しているシェーダコードを見てみましょう。
コメントが特に無いですが、このコードは Kimberlite が自動生成したシェーダコードです。
まずは距離に応じて色を変える作りを入れてみます。
Z軸に対し近くが赤、遠くが青となるようにグラデーションをかけます。
結果は次のとおりです。
後付でシェーダパラメータとプロパティを結び付けたいものです。
この発言の意味を具体的に示します。
例えば現在の時点でマテリアルカラーがこれに当たります。
下記の通りマテリアルカラー変数をシェーダコードに定義し、ピクセルシェーダで直接利用します。
シェーダにこうした変数を利用していることを、システムに伝えるため、エフェクト情報に下記の通り
float4 のマテリアルカラーがあることを示します。
StringTable がシーンに一つ作られており、そこの文字列を参照するID(インデックスですが)がここには打たれています。
シーンの StringTable は次のとおり
では、システム側は何をしているのか?
自分でも思い出せないので、詳細を追ってみます。
ノードツリーとか言いながら、ノード以外の ViewModel がツリーには並んでいます。
ひとまずノードだけで親から子ノードへ下りながら再帰します。
ノード変数の更新は、ノード単位、例えばノードの姿勢行列とかです。
ノードがメッシュの場合に描画するのですが、メッシュには複数のプリミティブが存在します。
プリミティブの存在単位はマテリアル数と同じと考えることができます。
例えばBoxの6面全てに別マテリアルを割り当てると、メッシュのプリミティブ数は 6 となり
ここでは6回ループしながら、エフェクト変数を更新しつつ、プリミティブの描画を行います。
一つ驚くかもしれませんが、プリミティブインデックスはシーンで一意なIDとして機能します。
では、メッシュからプリミティブを複数参照する場合どのようにアクセスするかですが、上記コードの通り連番でアクセスできます。
UpdatePrimitiveVariables にて、マテリアルカラーの更新を行っているはずなので、中身を明らかにします。
マテリアルがあれば、マテリアルのディフューズテクスチャフラグをチェック
なければシーンのマテリアル情報からディフューズカラーを取り、何やら変数IDを指定して設定をシーンにお願いしています。
この関数を機能拡張するとして、SetEffectVariable が何を行っているのか確認します。
一つ関数を飛ばしていますが、SetEffectVariable が SetVariable を呼び出しています。
プリミティブとエフェクトは一対一で対応付いているので、プリミティブインデックスからシーンのエフェクトを引っ張ってこれます。
(注意したいのがインデックスは同一ではないです、エフェクトはマテリアルと読み替えることができます。)
エフェクトに対して、予め用意しておいた ReservedEffectVariables に対して値を設定しています。
今のところ、予約変数と2Dテクスチャのセマンティクスにのみ対応しています。
これが現在プリミティブが持っているエフェクト変数に対して走査が行われるようになれば、目的を達成できそうです。
そして、これらの予約変数を作成している処理が次のとおり。
結構このサイトを閲覧している方から、質問メールをもらうのですが
とりわけこのエフェクト作成についての質問が多いです。
上記コードが回答なのですが、このコードはエフェクト情報からエフェクトとその入力レイアウト、エフェクト変数アクセサ
予約変数に関しては現在のパラメータで初期化するという操作を行っています。
こういうの見ていると色々なことが頭を駆け巡りますが、必要な計算、必要な更新のことを考えると
ノードツリーの描画フローでは一度GPU側の変数を更新したら、次にCPU側のパラメータを変更するまでは
更新は不要になります。
今はマテリアルカラーを必ず更新するフローになっていますので、結構低速となり得るコードです。
そういったギミックを入れていきます。
…脱線してますね。
一応エフェクト変数まわりの扱い、改善点、拡張内容などが見えてきたところで、この話は次回に行おうと思います。
ポイントスプライトに話を戻します。
ポイントスプライトを行なうためには、ジオメトリシェーダを使うのが手っ取り早いです。
頂点位置を入力に、カメラ姿勢行列からビルボード用の頂点を4つ作り、三角形を2つカメラに向けて作成します。
あとはワールド位置のZ座標よりカラーを決定するだけです。
上記の考えに沿って書いたシェーダコードを次に示します。
このコードを適用すると次の描画結果を得ます。
WorldView を掛けた位置情報、つまりカメラからの距離に比例して、スプライトの辺の長さを調整するようにしました。
全てのスプライトは距離に影響されず常に同じ大きさで描画されます。
シーンの文字列ブロックを次のとおり編集
シェーダコードの予約変数は次のとおり
はい、ポイントスプライトについて詳細を示しました。
上記シェーダコードを参考に書けば問題ないですね。
(今回は詳細を示せました。)
あとはセンサからもらった3D位置を入力にポイントスプライトを描画する仕組みを入れてみたいと思います。
ひとまずオーディオプラグインを参考にしたいと思ったので、どのようなインタフェースを用意すれば良いのか確認します。
基本的には必要な情報を渡し初期化、終了処理を行なう関数を用意する。
また入力データを渡してそれを処理する関数を用意する。
ちょっと設計の話になりますが距離画像だけを入力としてもらいたいプラグインもあれば
カラー画像だけを入力にもらいたいプラグインもあるわけで
処理的にはどちらも、または音声までも一緒にもらいたいプラグインもあります。
初期化処理に必要な情報はそれぞれ異なるので、どうしたものか…
プラグインに種類を作り、これをプラグインに教えてもらう形はどうでしょうか?
プラグインを使う側はその宣言を頼りに初期化処理を行うのです。
少し気になる技術を確かめてみます。
リフレクションについてです。
Assembly にカスタムアトリビュートでを定義して
指定したプロパティを取得できることを確認しました。
これを使って、まずはプラグインアセンブリかどうか判断することにします。
共通のプラグイン用アトリビュートとプラグイン基底クラスを提供するアセンブリを用意して
プラグインはそれぞれのタイプに合った基底クラスを継承して作成することにします。
すると結構簡単にプラグインクラスを扱えるのではないでしょうか?
設計なのか構想なのか、とにかくイメージしたものを形にしてみようと思います。
こうしたい、あーしたいをユースケースにて
誰が何をどうするに書き換え
登場した単語をオブジェクトとして切り分けるとクラスやその属性が見えてきます。
概念設計を行った所、早速実装してみましょう。
せっかく EA 買ったので、クラス図からソースファイルの作成をやってみたいと思います。
先ほどのクラス図のクラス名などを実装仕様にしました。
ワンボタンで次のようなクラスコードを構築してくれます。(これは嬉しい!)
設計図に書き込んでいたコメントも良い感じで書き込んだ状態でソースコードを構築してくれますし
ステレオタイプでプロパティであることを設定しておけばプロパティとして属性を書いてくれます。
おまけにクラス間の集約について記述すると、そこからメンバも自動生成してくれる優しさ。
半日から、一日に相当するめんどくさい実装作業があっという間に完了します。
EnterpriseArchitect 買ってよかったなと思います。
さて、実装を埋めて思い通りにプラグインを管理できるのか試してみます。
しかし今回の話は長いなぁ…勉強することがいっぱいです。
プラグイン機構を実装しました。
今までと基本的には何も変わりませんが、任意のプラグインを選択
デバイスストリームに対して割り当てることが出来るようになります。
では、距離画像処理用のプラグインを作ってみましょう。
ここまで距離画像の情報は取得できること、表示できることを確認しています。
距離画像データではなく、3D点雲がデータとして欲しい所なので、具体的な取得方法を確認します。
と、結構前のことになりますが081話で調べていました。
思い出したことに planes のインデックス 0~3 のうち 2 はカラーカメラの画像座標でした。
0 は、距離値でしたが、残り 1, 3 は?
あとで確認します。
あ、記述を見つけました。
The depth map, which contains three planes, unless otherwise specified in ImageOption.
The first plane contains a depth map; the second plane contains the infrared map; and the third plane contains the UV map.
つまり 0 が深度画像、1 が赤外線画像、2 がカラーUV値です。3 はありません。
オプションで without 指定ができるので、キャプチャが高速になるとか?
上記は COLOR_FORMAT_DEPTH の話ですが、もう一つ COLOR_FORMAT_VERTICES について見てみましょう。
The first plane contains the vertices in fixed-point integers;
the second plane contains the infrared map; and the third plane contains the UV map.
つまり 0 が整列された点整数情報群、1 が赤外線画像、2 がカラーUV値です。3 はありません。
説明が足りないな、一応整数値x3として取り出そうとは思います。
画像に情報を書き込んでどうなるか見てみましょう。
確認した所、short x 3 が画素数連続する byte 配列であることを確認
どうもカメラ座標系における x, y, z の値が mm 単位で格納されている様子でした。
もう不要になりつつありますが、デバイスからプロジェクションを得るコードを以下に示します。
では、距離画像処理用のプラグインを作成して
このお話の主目的であるを3D点雲表示を行ってみます。
その前に確認したいこととして、距離値の単位をmmから更にあげられるかどうかですが
変更しても変更しても device.QueryProperty(PXCMCapture.Device.Property.PROPERTY_DEPTH_UNIT, out value); の結果は
1000[μm]固定でした。ということで short 配列固定でプラグインに情報を渡すことに迷わなくて済みます。
プラグインインタフェースは次のとおり
これを継承して、KimberliteFramework 内にプラグインクラスを作成します。
うーん手が止まりますね、今のところ 変換が不要な Color 画像はそのまま byte 配列から画像に変換して表示しており
変換が必要なDepth, Vertices は、そのピクセルの距離値を求めて、Gray8 に変換しています。
YUY2 フォーマットも入力としてあり得るので BGR に変換して表示しています。
オーディオプラグインが機能しているのは、中間にアナライザが入って、short 配列や float 配列を構築しているからです。
画像処理も同様に中間にアナライザを入れて、必ず BGR や Gray, Vertices float 配列にするべきかと考えます。
そうですね、書いていてこのクッションは必要だと考えます。
BGR 変換や Gray8 変換は既に構築されているので、ビデオのアナライザも用意します。
はい、用意しました。
一つ Logicool の結構いいカメラから byte 配列を取得しようとするとクラッシュする問題がありましたが
仕方なく memoryStream に bitmap を save する方法で解決しました。(かなり低速になりました?)
アナライザが現在画像表示用の byte 配列を持っている状態です。
機能追加で距離点群の float x3 配列をアナライザが持つようにしてみます。
以前 Kinnect の BoxTracking でやったようなので、ソースを参考にします。
確認しました。そこでは float[] に XYZ 座標を詰めて SharpDX.DataStream.WriteRange で GPU に送っていました。
はい、ひとまずプラグインをフォルダに置けば、これを実行するように作り終えました。
計測した距離点群の3D表示は少々ハードルが高いので、画像を簡単に処理するフィルタプラグインを試しに用意します。
はい、3x3のガウスフィルタをカラーRGB画像にかけられるようになることを確認しました。
引き続き、3D点群表示に向けて作業します。
ちょっとフィルタリング処理が激重だったので、マルチスレッド化してみました。
CPU負荷は分散されたのですが、フレームレートは期待通りまでは改善しませんでした。
結果はVGA解像度のカラーが画像に 7x7 のガウシアンフィルタをかけて 0.4 [s] ほど
リアルタイムのカメラ映像にフィルタ処理をかけるって結構面白いですよね。
もっと高速化するならば GPGPU を使いますが、今回の画像処理は試験的なものなのでここまで
4500万回 float 演算していますし、こんなものでしょうか?
引き続き、3D点群表示に向けて作業します。
しかし、開発はどのようにファイル依存して作るのが良いのでしょう?
一旦 Kimberlite をパッケージ化してどこかに配置するのが良いのでしょうか?
今の KimberliteFramework は…
まぁ色々あって、ローカルパッケージ作成ルールを設けました。
このように Package フォルダの中にツール名、ラベル名でフォルダを切り、パッケージを .zip 圧縮して提出します。
今回 KimberliteFramework を元に作成したプラグインはこのパッケージを参照するように修正
さらにローカルで起動することを確認し、クラスライブラリ化、プラグイン化を行いました。
プラグイン機能から呼び出した結果が次のとおり(まだ距離点群は表示していません)
いやはや、依存関係やターゲットプラットフォームなどを気にしないとここまで来られないので
それなりに知識を持った人じゃないとけっこう実行時エラーで泣くことになりそうです。
(多分大学院にいた頃の自分では数十分じゃ無理…)
あとは、プラグインが毎回渡してくる float[] を GPU に転送して、この話の中盤で作成したエフェクトを実行してみましょう。
そんなシーンを作成します。
もう大変ですね、この辺り…
KimberliteFramework からデバイスにアクセスして、シェーダ変数に情報を送る処理など
どうするの?といった疑問に答えるドキュメントをコメントに用意しました。
これに伴い Kimberlite 側もいくつか修正、ローカルパッケージも更新しました。
改めて、参照パッケージ更新後のプラグインに修正を加えます。(疲れた…)
ひとまず実装完了です。
初動確認します。
おや?何もかも遠く、そして青いだけ…そして1,2分放置するとコードを示さずクラッシュしました。
青い件は入力単位が[mm]であることが理由ですね、単位を[m]に直します。
1,2分放置してもクラッシュしなくなりました。
シェーダは一度コンパイルしたものを渡せば、解決する模様ですね。(なんか危ないな…)
あ、10分ほどしてクラッシュしました。(まぁそうですよね)
さて、単位を m に直しても次の結果になります。
デバッグします。
真っ先に疑ったのはシェーダコードの不備です。
まったく同じ VS, GS, PS を実行し、別のオブジェクトの頂点を利用します。
問題無いですね。
入力データを使って、深度画像を表示しているので入力データが誤っているとも思えないのですが…
あ!そうか、であれば入力レイアウトと入力データが噛み合っていないのかもしれません。
固定の入力レイアウトを用意してみます。
おそらくこれが原因のはず。
シェーダコードも一部 float3 入力となるよう書き換えます。
結果は次のとおり
形は正しい…けれど、ずいぶんとカメラに近い認識になっていますね。
(白色は 0.1m よりカメラに近いことを意味するのだが…倍率間違えたかな?)
もう少しシェーダコード修正します。
あと、クラッシュしなくなりました。
やっぱり倍率間違えていました。
きっちり単位を m に直した結果がこちら
あれ?またクラッシュしました。
クラッシュしたコードが示されないのではっきりとはわかりませんでしたが
デバイス操作を排他的に行えていないことが原因と考えるようになり
DirectXImage の方にロック、アンロック機能を追加し
デバイス操作を行う一連の作業をロック中に行なうようにしました。
結果、1時間実行してもクラッシュしないことを確認。
解決したようです。
ひとまずプラグインを使って 3D点群表示が行えることを確認しました。
いや、長かったですね。
次回の冒頭でまとめを行います。
お疲れ様でした。
次回は途中で出てきた Kimberlite2 の機能を拡張させるお話を予定。
2014/06/03 初記。
2014/07/23 更新。