DirectX での手とモーション コントローラー
Note
この記事は、従来のWinRTネイティブAPIに関連します。 新しいネイティブ アプリ プロジェクトの場合は、OpenXR API を使用することをお勧めします。
Windows Mixed Reality では、手とモーション コントローラーの入力は、どちらも Windows.UI.Input.Spatial 名前空間にある空間入力APIを介して処理されます。 これにより、選択 の押下などの通常のアクションを、手とモーションコントローラーの両方で同様に簡単に処理できるようになります。
作業の開始
Windows Mixed Reality で空間入力にアクセスするには、SpatialInteractionManager インターフェイスから始めます。 通常はアプリの起動中に SpatialInteractionManager::GetForCurrentView を呼び出することで、このインターフェイスにアクセスできます。
using namespace winrt::Windows::UI::Input::Spatial;
SpatialInteractionManager interactionManager = SpatialInteractionManager::GetForCurrentView();
SpatialInteractionManager の役割は、入力のソースを表す SpatialInteractionSource へのアクセスを提供することです。 システムで利用できる SpatialInteractionSource は 3 種類あります。
- 手は、検出されたユーザーの手を表します。 手ソースは、HoloLens での基本的なジェスチャーから、HoloLens 2での完全な多関節ハンド トラッキングまで、デバイスに基づいてさまざまな機能を提供します。
- コントローラーは、対になっているモーション コントローラーを表します。 モーション コントローラーは、選択トリガー、メニュー ボタン、把持ボタン、タッチパッド、サムスティックなど、さまざまな機能を提供できます。
- 音声 は、ユーザーの音声読み上げシステムで検出されたキーワードを表します。 たとえば、このソースは、ユーザーが「選択」と言うたびに、選択押下命令を導入して解放します。
ソースのフレームごとのデータは、SpatialInteractionSourceState インターフェイスで表されます。 アプリケーションでイベント駆動のモデルとポーリング ベースのモデルのどちらを使用するかに応じて、このデータにアクセスする方法は異なります。
イベント駆動型入力
SpatialInteractionManager では、アプリがリッスンできる多数のイベントが提供されています。 その一例として、SourcePressed、[SourceReleased、SourceUpdated などが挙げられます。
たとえば、次のコードは、MyApp::OnSourcePressed というイベント ハンドラーを SourcePressed イベントにフックします。 これにより、アプリは任意のタイプの対話式操作ソースでの押下を検出できるようになります。
using namespace winrt::Windows::UI::Input::Spatial;
auto interactionManager = SpatialInteractionManager::GetForCurrentView();
interactionManager.SourcePressed({ this, &MyApp::OnSourcePressed });
この押下イベントは、押下が発生した時点で対応する SpatialInteractionSourceState と共に、非同期的にアプリに送信されます。 アプリまたはゲームエンジンは、入力処理ルーチンで処理をすぐに開始したり、イベント データをキューに入れたりする場合があります。 ここで、選択ボタンが押されたかどうかを確認する、SourcePressed イベントのイベント ハンドラー関数を示します。
using namespace winrt::Windows::UI::Input::Spatial;
void MyApp::OnSourcePressed(SpatialInteractionManager const& sender, SpatialInteractionSourceEventArgs const& args)
{
if (args.PressKind() == SpatialInteractionPressKind::Select)
{
// Select button was pressed, update app state
}
}
上記のコードは、デバイスでのプライマリ アクションに対応する「選択」押下のみを確認します。 例として、HoloLens でのエアタップの実行や、モーション コントローラーでのトリガーを引くことなどが挙げられます。 「選択」押下は、ターゲットとしているホログラムをアクティブにするというユーザーの意図を表します。 SourcePressed イベントは、さまざまなボタンやジェスチャーに対して発生します。それらのケースをテストするには SpatialInteractionSource の他のプロパティをご確認ください。
ポーリング ベースの入力
SpatialInteractionManager を使用して、フレームごとに入力の現在の状態をポーリングすることもできます。 これを行うには、フレームごとに GetDetectedSourcesAtTimestamp を呼び出します。 この関数は、アクティブな SpatialInteractionSource ごとに 1 つの SpatialInteractionSourceState を含む配列を返します。 つまり、アクティブなモーション コントローラーごとに 1 つ、トラッキング対象の手ごとに 1 つ、「選択」コマンドが最近発された場合の音声に対して 1 つです。 その後、各 SpatialInteractionSourceState のプロパティを調べて、アプリケーションへの入力を駆動できます。
ここで、ポーリング方法を使用して「選択」アクションを確認する方法の一例を示します。 prediction 変数は、HolographicFrame から取得できる HolographicFramePrediction オブジェクトを表します。
using namespace winrt::Windows::UI::Input::Spatial;
auto interactionManager = SpatialInteractionManager::GetForCurrentView();
auto sourceStates = m_spatialInteractionManager.GetDetectedSourcesAtTimestamp(prediction.Timestamp());
for (auto& sourceState : sourceStates)
{
if (sourceState.IsSelectPressed())
{
// Select button is down, update app state
}
}
各 SpatialInteractionSource は ID を有し、その ID を使用して新しいソースを識別し、既存のソースをフレーム間で相互に関連付けることができます。 手は FOV を出入りするたびに新しい ID を取得しますが、コントローラー ID はセッションの持続時間中、固定されたままです。 SourceDetected や SourceLost など、SpatialInteractionManager におけるイベントを使用して、手がデバイスのビューを出入りするとき、あるいはモーション コントローラーがオン/オフになっている、またはペアリング/ペアリング解除されたときに反応させることができます。
予測ポーズと履歴ポーズ
GetDetectedSourcesAtTimestamp には、タイムスタンプ パラメーターがあります。 これにより、状態やポーズのデータを予測または履歴として要求することができ、空間的対話式操作を他の入力ソースと相関させることができます。 たとえば、現在のフレームで手の位置をレンダリングするときに、HolographicFrame によって提供される予測タイムスタンプを渡すことができます。 これにより、システムは手の位置を前方予測して、レンダリングされたフレーム出力と厳密に位置合わせし、知覚される遅延を最小限に抑えることができるようになります。
ただし、そのような予測ポーズでは、対話式操作ソースによるターゲット設定に最適なポインティング レイは生成されません。 たとえば、モーション コントローラーのボタンが押されたときに、そのイベントが Bluetooth を介してオペレーティング システムにバブル アップするまでに最大 20 ミリ秒かかる場合があります。 同様に、ユーザーが手のジェスチャーを行った後、システムがそのジェスチャーを検出し、アプリがそれをポーリングする前に、ある程度の時間が経過する可能性があります。 アプリが状態の変化をポーリングする前に、その対話式操作をターゲットにするために使用される頭部と手のポーズは、実際には過去に発生したものです。 現在の HolographicFrame のタイムスタンプを GetDetectedSourcesAtTimestamp に渡すことでターゲットを設定する場合、フレームが表示される時点で代わりにターゲット設定レイに対してポーズを前方予測します。これは、将来 20 ミリ秒以上かかる可能性があります。 この将来のポーズは対話式操作ソースのレンダリングには適しますが、ユーザーのターゲット設定が過去に発生したため、対話式操作のターゲット設定のための時間の問題が複雑になります。
幸い、SourcePressed、[SourceReleased、SourceUpdated のイベントは、各入力イベントに関連付けられた過去の State を提供します。 これには、TryGetPointerPose を介して利用できる過去の頭部および手のポーズと、このイベントと関連付けるために他の API に渡すことができる過去の Timestamp が直接含まれます。
それにより、各フレームで手とコントローラーを使用してレンダリングおよびターゲット設定を行うときのベスト プラクティスは次のようになります。
- 各フレームの手/コントローラーによるレンダリングでは、アプリは現在のフレームの光子時間で各対話式操作ソースの前方予測されたポーズをポーリングする必要があります。 フレームごとに GetDetectedSourcesAtTimestamp を呼び出し、HolographicFrame::CurrentPrediction によって提供される予測タイムスタンプを渡すことで、すべての対話式操作ソースをポーリングできます。
- 押下または解放時に手/コントローラーのターゲット設定では、アプリは押下/解放されたイベントを処理し、そのイベントの過去の頭部または手のポーズに基づくレイキャスティングを行う必要があります。 SourcePressed または SourceReleased イベントを処理し、イベント引数から State プロパティを取得し、次に TryGetPointerPose メソッドを呼び出すことによって、このターゲット設定レイを取得します。
デバイス間入力プロパティ
SpatialInteractionSource API は、幅広い機能を備えたコントローラーおよびハンド トラッキング システムをサポートします。 これらの機能の多くは、異なるタイプのデバイス間で共通します。 たとえば、ハンド トラッキングとモーション コントローラーは、どちらも「選択」アクションと 3D 位置を提供します。 この API は、可能な限り、これらの共通機能を SpatialInteractionSource の同じプロパティにマップします。 これにより、アプリケーションはさまざまな入力タイプをより簡単にサポートできるようになります。 次の表は、サポートされるプロパティと、入力タイプ間でそれらを比較する方法を示します。
プロパティ | 説明 | HoloLens (第1世代) のジェスチャー | モーション コントローラー | 多関節手 |
---|---|---|---|---|
SpatialInteractionSource::Handedness | 右手または左手/コントローラー | サポートされていません | サポートされています | サポートされています |
SpatialInteractionSourceState::IsSelectPressed | プライマリ ボタンの現在の状態 | エアタップ | トリガー | 緩和されたエアタップ(垂直のピンチ) |
SpatialInteractionSourceState::IsGrasped | グラブ ボタンの現在の状態 | サポートされていません | グラブ ボタン | ピンチまたは閉じた手 |
SpatialInteractionSourceState::IsMenuPressed | メニュー ボタンの現在の状態 | サポートされていません | メニューボタン | サポートされていません |
SpatialInteractionSourceLocation::Position | コントローラー上の手または把持位置の XYZ 位置付け | 手のひらの位置付け | 把持ポーズの位置 | 手のひらの位置付け |
SpatialInteractionSourceLocation::Orientation | コントローラー上の手または把持ポーズの向きを表す四元数 | サポートされていません | 把持ポーズの向き | 手のひらの向き |
SpatialPointerInteractionSourcePose::Position | ポインティングレイの原点 | サポートされていません | サポートされています | サポートされています |
SpatialPointerInteractionSourcePose::ForwardDirection | ポインティングレイの方向 | サポートされていません | サポートされています | サポートされています |
上記のプロパティの一部は、すべてのデバイスで利用できるわけではなく、API はこれをテストする手段を提供します。 たとえば、SpatialInteractionSource::IsGraspSupported プロパティを調べて、ソースが把持アクションを提供するかどうかを判断できます。
把持ポーズとポインティング ポーズ
Windows Mixed Reality は、さまざまなフォーム ファクターのモーション コントローラーをサポートします。 また、多関節ハンド トラッキング システムもサポートします。 手の位置と、アプリがユーザーの手にある物をポインティングしたりレンダリングしたりするために使用する必要がある自然な「前方」方向とのリレーションシップは、これらのシステムのすべてによってそれぞれ異なります。 このすべてをサポートするために、ハンド トラッキングとモーション コントローラーの両方に 2 種類の 3D ポーズがあります。 1 つ目は、ユーザーの手の位置を表す把持ポーズです。 2 つ目は、ユーザーの手またはコントローラーから発するポインティング レイを表すポインティングポーズです。 ユーザーの手、またはユーザーが手に持っている物、たとえば剣や銃などをレンダリングしたい場合、把持ポーズを使用します。 ユーザーがUIを**ポインティングしているときなど、コントローラーまたは手からレイキャスティングしたい場合、ポインティングポーズを使用します。
把持ポーズには、SpatialInteractionSourceState::Properties::TryGetLocation(...) によってアクセスできます。これは、次のように定義されます。
- 把持位置:コントローラーを自然に握ったときの手のひらの中心点。握った手の中心に来るよう左右に調整されます。
- 把持の向きの右軸:手を完全に開いて 5 本の指が平らになるポーズをとるときの手のひらに垂直なレイ(左の手のひらからは前方、右の手のひらからは後方)。
- グリップの向きの前方向の軸: (コントローラーを持つように) 手を部分的に閉じたとき、親指以外の指で形成される筒形を通って "前方" をポイントする光線。
- 把持の向きの上方向の軸:右方向と前方向の定義によって暗黙的に指定された上方向の軸。
ポインターポーズには、SpatialInteractionSourceState::Properties::TryGetLocation(...)::SourcePointerPose または SpatialInteractionSourceState::TryGetPointerPose(...)::TryGetInteractionSourcePose によってアクセスできます。
コントローラー固有の入力プロパティ
コントローラーの場合、SpatialInteractionSource には追加機能を備えたController プロパティがあります。
- HasThumbstick:true の場合、コントローラーにはサムスティックがあります。 SpatialInteractionSourceState の ControllerProperties プロパティを調べて、サムスティックの x 値と y 値 (ThumbstickXとThumbstickY)、および押下状態 (IsThumbstickPressed) を取得します。
- HasTouchpad:true の場合、コントローラーにはタッチパッドがあります。 SpatialInteractionSourceState の ControllerProperties プロパティを調べて、タッチパッドの x 値と y 値 (TouchpadX と TouchpadY) を取得し、ユーザーがパッドをタッチしているかどうか (IsTouchpadTouched) と、タッチパッドを押しているかどうか (IsTouchpadPressed) を確認します。
- SimpleHapticsController:コントローラーのSimpleHapticsControllerAPI を使用すると、コントローラーの触覚機能を検査でき、また、触覚フィードバックを制御することもできます。
タッチパッドとサムスティックの範囲は、両軸(下から上、左から右)ともに -1 から 1 です。 SpatialInteractionSourceState::SelectPressedValue プロパティを使用してアクセスされるアナログトリガーの範囲は 0 から 1 です。 値 1 は、値が true の IsSelectPressed と相関し、その他の値は、値が false の IsSelectPressed と相関します。
多関節ハンドトラッキング
Windows Mixed Reality API は、HoloLens 2 などで使用される多関節ハンド トラッキングを完全にサポートします。 多関節ハンド トラッキングを使用して、アプリケーションに直接操作モデルおよびポイントとコミット入力モデルを実装できます。 また、完全なカスタム対話式操作を作成するためにも使用できます。
手の骨格
多関節ハンド トラッキングは、さまざまなタイプの対話式操作を可能にする 25 関節の骨格を提供します。 骨格には、人差し指、中指、薬指、小指の 5 つの関節、親指の 4 つの関節、手首の 1 つの関節があります。 手首の関節は階層の基底となります。 次の図は骨格のレイアウトを示します。
ほとんどの場合、各関節には、それが表す骨に基づいて名前が付けられます。 各関節には 2 つの骨があるため、その場所にある子骨に基づいて各関節に名前を付けるという規定が使用されます。 子骨は、手首から遠い方の骨と定義されます。 たとえば、「Index Proximal (人差し指近位)」関節には、人差し指近位骨の開始位置と、その骨の向きが含まれます。 骨の終了位置は含まれません。 必要な場合は、階層内の次の関節である「Index Intermediate (人差し指中間)」関節から取得します。
階層構造の 25 個の関節に加えて、システムは手のひらの関節も提供します。 通常、手のひらは骨格構造の一部とは見なされません。 これは、手の全体的な位置と向きを取得するための便利な方法としてのみ提供されます。
関節ごとに次の情報が提供されます。
名前 | 説明 |
---|---|
[位置] | いずれかの要求された座標系で利用できる関節の 3D 位置。 |
Orientation | いずれかの要求された座標系で利用できる骨の 3D 方位。 |
Radius | 関節位置にある皮膚の表面までの距離。 指の幅に依存する直接対話式操作または視覚化を調整する際に役立ちます。 |
精度 | この関節の情報に対するシステムの信頼度に関するヒントを提供します。 |
手の骨格データには、SpatialInteractionSourceState の関数によってアクセスできます。 この関数は TryGetHandPose と呼ばれ、HandPose というオブジェクトを返します。 ソースが多関節ハンドをサポートしない場合、この関数は null を返します。 HandPose を取得すると、関心のあるの関節の名前によって TryGetJoint を呼び出すことで、現在の関節データを取得できます。 このデータは、JointPose 構造として返されます。 次のコードは、人差し指の先端の位置を取得します。 変数 currentState は、SpatialInteractionSourceState のインスタンスを表します。
using namespace winrt::Windows::Perception::People;
using namespace winrt::Windows::Foundation::Numerics;
auto handPose = currentState.TryGetHandPose();
if (handPose)
{
JointPose joint;
if (handPose.TryGetJoint(desiredCoordinateSystem, HandJointKind::IndexTip, joint))
{
float3 indexTipPosition = joint.Position;
// Do something with the index tip position
}
}
ハンド メッシュ
多関節ハンド トラッキング API は、完全に変形可能な三角形のハンドメッシュを可能にします。 このメッシュは、手の骨格と共にリアルタイムで変形することができ、視覚化技術や高度な物理技術に役立ちます。 ハンドメッシュにアクセスするには、まず、SpatialInteractionSource で TryCreateHandMeshObserverAsync を呼び出することで、HandMeshObserver オブジェクトを作成する必要があります。 これは、ソースごとに 1 回だけ行う必要があり、通常は初めて表示するときに行います。 つまり、手が FOV に入るたびに、この関数を呼び出して HandMeshObserver オブジェクトを作成します。 これは非同期関数であるため、ここで同時実行に少し対処する必要があります。 利用可能になると、GetTriangleIndices を呼び出すことで、HandMeshObserver オブジェクトに三角形のインデックス バッファーを要求できます。 インデックスはフレームごとに変更されないため、一度取得すると、ソースの有効期間中キャッシュすることができます。 インデックスは時計回りの順に提供されます。
次のコードは、デタッチされた std::thread をスピンアップしてメッシュ オブザーバーを作成し、メッシュ オブザーバーが利用可能になるとインデックス バッファーを抽出します。 これは、トラッキング対象の手を表す SpatialInteractionSourceState のインスタンスである currentState という変数から始まります。
using namespace Windows::Perception::People;
std::thread createObserverThread([this, currentState]()
{
HandMeshObserver newHandMeshObserver = currentState.Source().TryCreateHandMeshObserverAsync().get();
if (newHandMeshObserver)
{
unsigned indexCount = newHandMeshObserver.TriangleIndexCount();
vector<unsigned short> indices(indexCount);
newHandMeshObserver.GetTriangleIndices(indices);
// Save the indices and handMeshObserver for later use - and use a mutex to synchronize access if needed!
}
});
createObserverThread.detach();
デタッチされたスレッドの開始は、非同期呼び出しを処理するオプションの1つにすぎません。 代わりに、C++/WinRT でサポートされる新しい co_await 機能を使用することもできます。
HandMeshObserver オブジェクトを取得すると、対応する SpatialInteractionSource がアクティブである間、それを保持する必要があります。 次に、フレームごとに、GetVertexStateForPose を呼び出し、頂点が必要なポーズを表す HandPose インスタンスを渡すことで、手を表す最新の頂点バッファーを要求できます。 バッファー内の各頂点には、位置と法線があります。 ここでは、ハンド メッシュの現在の頂点セットを取得する方法の一例を示します。 前と同様に、currentState 変数は SpatialInteractionSourceState のインスタンスを表します。
using namespace winrt::Windows::Perception::People;
auto handPose = currentState.TryGetHandPose();
if (handPose)
{
std::vector<HandMeshVertex> vertices(handMeshObserver.VertexCount());
auto vertexState = handMeshObserver.GetVertexStateForPose(handPose);
vertexState.GetVertices(vertices);
auto meshTransform = vertexState.CoordinateSystem().TryGetTransformTo(desiredCoordinateSystem);
if (meshTransform != nullptr)
{
// Do something with the vertices and mesh transform, along with the indices that you saved earlier
}
}
骨格の関節とは異なり、ハンドメッシュ API は頂点の座標系の指定を可能にしません。 代わりに、HandMeshVertexState は、頂点を提供する座標系を指定します。 その後、TryGetTransformTo を呼び出し、必要な座標系を指定することで、メッシュ変換を取得できます。 頂点を操作するたびに、このメッシュ変換を使用する必要があります。 特に、レンダリングの目的でのみメッシュを使用する場合、このアプローチによって CPU のオーバーヘッドが削減されます。
視線とコミットの複合ジェスチャー
特に HoloLens (第1世代) で視線入力モデルとコミット入力モデルを使用するアプリケーションでは、空間入力 API は、「select」イベントの上にビルドされた複合ジェスチャーを有効にするために使用できるオプションの SpatialGestureRecognizer を提供します。 SpatialInteractionManager からホログラムの SpatialGestureRecognizer に対話式操作をルーティングすることで、アプリは、押下と解放を手動で処理しなくても、手、音声、空間入力デバイスの間で、タップ、保持、操作およびナビゲーションのイベントを一様に検出できます。
SpatialGestureRecognizer は、要求されたジェスチャー セット間でのみ最小限の曖昧さ回避を行います。 たとえば、タップのみを要求する場合、ユーザーは好きなだけ指を押し続ける可能性があり、タップが依然として発生します。 タップと保持の両方を要求する場合、指で押下してから約 1 秒後にジェスチャーは保持になり、タップは発生しなくなります。
SpatialGestureRecognizer を使用するには、SpatialInteractionManager の InteractionDetected イベントを処理し、そこで晒された SpatialPointerPose をつかみます。 このポーズからユーザーの頭部の視線レイを使用して、ユーザーの周囲のホログラムや表面メッシュと交差させ、ユーザーが対話しようとしているものを判断します。 次に、CaptureInteraction メソッドを使用して、イベント引数の SpatialInteraction をターゲットホログラムの SpatialGestureRecognizer にルーティングします。 これにより、作成時にそのレコグナイザーに設定された SpatialGestureSettings に従うか、または TrySetGestureSettings によって、その対話式操作の解釈が開始されます。
HoloLens (第1世代) では、対話式操作とジェスチャーは、手の位置付けでレンダリングまたは対話するのではなく、ユーザーの頭部の視線からターゲット設定を導出する必要があります。 対話式操作が開始されると、操作またはナビゲーション ジェスチャーと同様に、手の相対的なモーションを使用してジェスチャーを制御できます。