アプリをごりごり作るよりも前にデバッグ情報などを実機で見やすくする仕組みをつくりました。かたりぃなです。
結論
従来手法のビルボードはHoloLensではそのまま使うのは少し難ありです。HoloLensが提供している機能を使ってビルボードをレンダリングするのが良い。
ビルボードって?
この用語が本当に正しいのかどうか疑問ではありますが、概要だけ。 一言で言うなら「常にカメラのほうを向くポリゴン」です。
どういうところで使うの?
3Dのゲームを遊んだことがある人なら一度は見かけたことがあると思います。 「常にカメラのほうを向いていなければいけない」ポリゴンというのは多々あって、例えば次のようなものが考えられます。
これらは一枚の板ポリゴンにテクスチャを張って、そのポリゴンが必ずカメラのほうを向くように設定することで実現できます。
どうやって実現するの?
カメラのほうを向くポリゴンを作る方法は、まず最も単純な方法としてはカメラ座標系でX,Y平面にポリゴンを書くだけで実現できると思います。 先の例でのメニューやツールバーなどはこれで解決ですが、応用してパーティクルを使った演出をする場合にちょっと不便で汎用性に欠けます。
というのもパーティクルはワールド空間上の特定の位置に配置されるオブジェクト(火の粉だったり、煙だったりのエフェクト)であるので、ワールド座標系で指定したいものです。
こうして考えると、カメラ座標系で直接ポリゴンを描くのではなく、ワールド座標系で座標を指定したうえでポリゴンが常にカメラのほうを向くようにしたいです。
3Dゲームなんか向けのサイトを見てみると、カメラの回転行列からの逆行列をオブジェクトの行列の回転行列成分にセットするという方法が一般的のようです。 というわけで、HoloLensのカメラ行列からビルボードを作れないか試してみます。
HoloLensのレンダリングパイプライン
カメラ行列からの逆行列を求めてそれを使うというアプローチですが、HoloLensで同様の手法を使うことはできなさそうです。 3Dモデルをレンダリングするとき、のカメラ行列の設定ですが、VisualStudioが生成するスケルトンではこうなっていました。
// DX::CameraResources::UpdateViewProjectionBuffer関数 DX::ViewProjectionConstantBuffer viewProjectionConstantBufferData; bool viewTransformAcquired = viewTransformContainer != nullptr; if (viewTransformAcquired) { // Otherwise, the set of view transforms can be retrieved. HolographicStereoTransform viewCoordinateSystemTransform = viewTransformContainer->Value; viewProjectionConstantBufferData.viewProjection[0] = viewCoordinateSystemTransform.Left * cameraProjectionTransform.Left; viewProjectionConstantBufferData.viewProjection[1] = viewCoordinateSystemTransform.Right * cameraProjectionTransform.Right; } // 以降、viewProjectionConstantBufferDataをUpdateSubresourceでVertexShaderのconstant bufferに転送する
constant bufferを参照するHLSLシェーダはこうなっています。
cbuffer ViewProjectionConstantBuffer : register(b1) { float4x4 viewProjection[2]; }; // 略 VertexShaderOutput main(VertexShaderInput input){ // 略 // Correct for perspective and project the vertex position onto the screen. pos = mul(pos, viewProjection[idx]); output.pos = (min16float4)pos;
何やってるのかの前にAR/VRの基本的な話です。
HMDを使って3Dレンダリングするとき、左右のレンズに少しだけ異なる映像を出すことで立体感を出すという手法があります。(=ステレオグラム) 人間の目が少し離れた位置についていて~と話が長くなりそうなので、ここでは「そういう手法がある」程度にとどめておきます。
HoloLensも同じ理屈で、左右のレンズに同じものをレンダリングしているわけではなく、左レンズ用と右レンズ用の映像をレンダリングします。 HolographicStereoTransformというクラスが左レンズと右レンズそれぞれの透視投影変換とビュー行列をもっているので、これを引っ張ってくるわけですね。
それぞれのレンズ用にレンダリングするためにDrawIndexedInstancedを使って左レンズ用インスタンスと右レンズ用インスタンスでレンダリングします。 テンプレートプロジェクトではシェーダーの入力パラメータにinstIdというのがいますが、これがインスタンスのIDというわけですね。
HolographicStereoTransform struct - UWP app developer | Microsoft Docs
なので、従来手法の「カメラの回転要素の逆行列を3Dモデルの回転行列に使う」というアプローチをそのまま流用しようとしても、モデルの回転行列を左右レンズそれぞれに生成する必要があって、それはあんまりです。
HoloLensの座標系で考える
そもそもHoloLensではスクリーン中央に常にカーソルがレンダリングされています。 このカーソルは視線の先の空間マッピングされたオブジェクト上に出るのですが、これと同じことができればいいわけです。
公式マニュアルに座標系のことが書いてありました。
これを真似していけば良さそうです。というかできました。
デバイスの相対座標と絶対座標
絶対座標と相対座標というと誤解しそうですが、概念として
と解釈できます。 HoloLensのプロモーションなどで空間上に何かのオブジェクトを配置しているケースでは上記の絶対座標を用います。 相対座標はドラゴンボールのスカウターや名探偵コナンの探偵メガネのように装着者からみた座標系として考えられます。
デバイス相対座標を取得する
まず、VisualStudioのHoloLensテンプレートプロジェクトでは、座標系として絶対座標(ワールド座標系)を使っています。 Windows::Perception::Spatial::SpatialStationaryFrameOfReferenceがそれです。
上記リンク先の記事を読みつつ、相対座標系に置き換えます。 Windows::Perception::Spatial::SpatialLocatorAttachedFrameOfReferenceがデバイスからの相対座標を扱うための参照です。
型を置き換えてもこのままではインタフェースが違うのでうまくいきません。 SpatialLocatorAttachedFrameOfReferenceでSpatialCoordinateSystem^を拾うには
GetStationaryCoordinateSystemAtTimestamp(prediction->Timestamp);
とします。
HoloLensの姿勢を取得する
これでHoloLensの相対座標系で色々できるようになりました。 姿勢(視線の向き)などは
SpatialPointerPose^ pointerPose = SpatialPointerPose::TryGetAtTimestamp(currentCoordinateSystem, prediction->Timestamp);
で拾えます。
公式マニュアルはこちら。
SpatialPointerPose class - UWP app developer | Microsoft Docs
pointerPoseからは次のようにして頭の位置、向きなどが拾えるようです。
pointerPose->Head->Position
pointerPose->Head->ForwardDirection
// etc.
あとはマニュアルどおりやればビルボードが出ました。
手っ取り早く解決したい(サンプルコードを持ってくる)
手っ取り早くサンプルコードをコピペして済ませる方法もありました。
https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/HolographicFaceTracking
HoloLens実機ない人のために動作概要の解説
- HoloLensのカメラで実世界のカメラ映像を取得(media capture)
- Microsoftの顔認識APIに映像を入力
- 顔として認識できる領域があれば、そこを切り取ってDirectXテクスチャを作る
- 切り取った顔領域をビルボードとして画面中央より少し左よりの位置にレンダリング
- 顔領域と思われる領域の空間の少し上部に3Dモデルをレンダリング
- 文字列をテクスチャとして生成する(direct2d)
- 顔が認識できないもしくはカメラがない(エミュレータ)の場合は前述の文字列を書き込んだテクスチャをビルボードをレンダリング
です。 顔検出は置いといて、エミュレータで動かしても「no avalilavle camera」のビルボードは出るので、必要箇所を抜粋すれば良いかと。 試しに該当する機能を持ってきてみると、期待通りビルボードが表示できました。
ちょっと詰まったポイントがあったので2点ほど。
- シェーダーがコンパイルできない
- 何も表示されない
以下、サンプルコード引用時のトラブルシューティングです。
サンプルコード引用したシェーダーのコンパイルに失敗する
GeometryShaderを新たに作ってVisualStudioでコンパイルすると、こんなエラーが出ます。
warning X3554: unknown attribute maxvertexcount, or attribute invalid for this statement error X3514: 'main': input parameter 'input' cannot have a geometry specifier
これはVisualStudioプロジェクトに後から追加したhlslファイルはコンパイラの設定が不足していることによるものです。 maxvertexcountはgeometryShaderでのみ使えるアトリビュートなので、コンパイラに「これはgeometryShaderですよ」と教えてあげる必要があります。
新しいhlslファイルをソシューションに組み込んだ直後の設定はこうなっています。 この図ではシェーダーの種類が指定されていません。何シェーダを作るつもりなんでしょうね。
例えばGeometryShaderならこういう形で指定します。pixelShaderやVertexShaderも同様。 シェーダーレベルは環境に合わせて適切なものを選択します。
サンプルコード引用したが何も表示されない
VisualStudio2015の生成するHoloLens用Direct3Dのテンプレートと、上記サンプルコードでは、行列の型が違っています。
テンプレートではDirectX::XMFLOAT4X4を行列の型として使っていますが、サンプルコードではWindows::Foundation::Numerics::float4x4です。 全体で統一が取れていればいいので、どちらかに合わせてしまいましょう。
たぶん内部構造が少し違うのかなと推測していますが、追うの面倒なので「やっぱ型は合わせたほうがいいよね」程度で。
文字列をテクスチャとして生成できているか謎
そもそも基本的な機能が動いていないとお話になりませんよね。 例えばテクスチャがすべて黒だった(=何も表示されていないように見えていただけ)なんて悲しい思いはしたくないものです。
テクスチャが生成されていない/テクスチャの参照が失敗しているかもと不安な場合は、pixelshaderでテクスチャサンプラを使わずに単色出力を試すと幸せになれます。
たとえば
// return min16float4(rgbChannel.Sample(defaultSampler, input.texCoord)); return float4(1.f, 1.f, 1.f, 1.f); // RGBA . Aは0に近いほど透明. 1は不透過
で、白色単色のビルボードが出るので、最初はこうしておけば問題切り分けに役立ちます。
感想と今後の展望
とりあえずビルボードをレンダリングできるようになりました。 HoloLensの座標系も少し詳しくなりました。
あとはARやるための仕組みをごりごりと書いていきたいところですが、 もう少しコード整理して、いつでもデバッグ用に組み込める形にしてから次のステップに移りたいと思います。
それでは今回はこれにて。