アドカレ
KENTOのひとりアドカレ14日目の記事です。
https://qiita.com/advent-calendar/2025/kento
環境情報
| ツール/SDK | バージョン |
|---|---|
| Unity | 6000.0.62f1 |
| Meta XR Core SDK | 81.0.0 |
| Open XR Plugin | 1.16.0 |
| XR Interaction Toolkit | 3.0.9 |
| XR Hands | 1.7.1 |
事前準備は以下の通りです。
デモ
グッドポーズで赤色、グッドじゃないけど親指を立てていれば黄色に変わるサンプルです。

カスタムジェスチャーの仕組み
ジェスチャーの設定はアセットファイルに逃して定義し、コード側でその値を判定してジェスチャー確認を行う仕組みになっています。
アセットファイルはCreate -> XR -> Hand Interactions -> Hand Pose、Hand Shapeと辿れば作成可能です。

Hand Shapeは任意の手の"形"になっているかを判定する値を持ちます。一方で、Hand Poseは任意の手の"形"になっているかに加えて、手の向きも判定する値を持ちます。
つまり、Hand Shapeは向きに関わらずジェスチャー認識を行うための値を保持するアセットです。ジェスチャーの向きも重要な意味を持つ場合においてはHand Poseが適任です。例えば👍と👎では全く意味が異なるジェスチャーになるので、そういう時にはHand Poseで向きも判定するようにしてあげると良いです。
では、実際にアセットファイルを見ていきます。Hand ShapeとHand Poseのサンプルアセットは以下のパスにあります。
Assets/Samples/XR Hands/1.7.1/Gestures/Examples/Hand Shapes
Assets/Samples/XR Hands/1.7.1/Gestures/Examples/Hand Poses
Hand Shapeの例として、Thumb Signal Hand Shapeを見ていきます。まずは親指の値です。画像の通り、2つの値が設定されています。Full Curlは曲がり具合の設定で、"指全体を曲げるかどうか"の設定値です。2つ目のSpreadは指の広がりを表す値です。これにより、隣の指との離れ具合を条件に追加できます。

Spreadは隣接する指の広がり具合のことですが、親指は人差し指、人差し指は中指、のように親指の方向から小指の方向における隣の指が対象です。即ち、小指におけるSpreadの設定は無視されます。
ThresholdはTargetの値に対して上限下限の閾値を設定できます。例えば、Targetが0.8、Thresholdのlower(左側の値)0.2、upper(右側の値)が0.1の際、0.6~0.9が認識範囲になります。
次はHand Poseです。Thumbs Upを見ていきます。先ほどのHand Shapeがここで使われています。

Relative Orientationで認識対象となる手の向きを設定できます。
Target Conditionsは対象のオブジェクトに対する手の向きを設定可能です。ただ、肝心の対象のオブジェクトはというと、XRHandPose.relativeOrientation.targetTransformという呼び出し方で値をセットする必要がある上に、おそらく1つしかセットできません。なので、この設定値は中々に使いづらいと思います。(違ったらご指摘ください)
サンプルコード
Hand ShapeとHand Poseを利用するサンプルコードです。まずは前回の記事で書いたXRHandSubsystemのユーティリティークラスを拡張していきます。
参考リンク:【MetaQuest】XR Hands経由でボーン情報を扱う
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.XR.Hands;
using UnityEngine.XR.Hands.Gestures;
/// <summary>
/// XRHandSubsystemを扱いやすくするためのユーティリティクラス。
/// </summary>
public class XRHandSubsystemUtility : MonoBehaviour
{
[SerializeField] private Transform _cameraOffset;
public static XRHandSubsystemUtility Instance { get; private set; }
public XRHandSubsystem Subsystem => GetOrUpdateSubsystem();
public bool IsLeftHandTracked => Subsystem?.leftHand.isTracked ?? false;
public bool IsRightHandTracked => Subsystem?.rightHand.isTracked ?? false;
public bool IsSubsystemRunning => Subsystem?.running ?? false;
public Transform UserLocalSpace => _cameraOffset;
private XRHandSubsystem _subsystem;
private readonly List<XRHandSubsystem> _subsystemsReuse = new();
private void Awake()
{
Instance = this;
}
private XRHandSubsystem GetOrUpdateSubsystem()
{
if (_subsystem is { running: true }) return _subsystem;
SubsystemManager.GetSubsystems(_subsystemsReuse);
foreach (var handSubsystem in _subsystemsReuse.Where(handSubsystem => handSubsystem.running))
{
_subsystem = handSubsystem;
return _subsystem;
}
_subsystem = null;
return null;
}
private XRHandJoint GetJoint(Handedness handedness, XRHandJointID jointId)
{
var hand = handedness == Handedness.Left ? Subsystem.leftHand : Subsystem.rightHand;
return hand.GetJoint(jointId);
}
/// <summary>
/// ジョイントの姿勢をCameraOffset基準で取得する。
/// </summary>
public bool TryGetJointPose(Handedness handedness, XRHandJointID jointId, out Pose pose)
{
pose = Pose.identity;
var joint = GetJoint(handedness, jointId);
if (!joint.TryGetPose(out var localPose)) return false;
pose = localPose.GetTransformedBy(_cameraOffset.transform);
return true;
}
/// <summary>
/// CommonGesturesからピンチ箇所の姿勢をCameraOffset基準で取得する。
/// </summary>
public bool TryGetPinchPoseFromCommonGesture(Handedness handedness, out Pose pose)
{
var commonGestures = handedness == Handedness.Left
? Subsystem.leftHandCommonGestures
: Subsystem.rightHandCommonGestures;
var success = commonGestures.TryGetPinchPose(out var localPose);
pose = success ? localPose.GetTransformedBy(_cameraOffset.transform) : Pose.identity;
return success;
}
+ /// <summary>
+ /// 指定した手がXRHandPoseの条件を満たしているか判定する。
+ /// </summary>
+ /// <param name="handPose">判定対象のハンドポーズ</param>
+ /// <param name="handedness">判定する手(左手/右手)</param>
+ /// <returns>条件を満たしていればtrue</returns>
+ public bool CheckHandPose(XRHandPose handPose, Handedness handedness)
+ {
+ if (Subsystem == null) return false;
+
+ var hand = handedness == Handedness.Left ? Subsystem.leftHand : Subsystem.rightHand;
+ if (!hand.isTracked) return false;
+
+ var eventArgs = new XRHandJointsUpdatedEventArgs { hand = hand };
+ return handPose.CheckConditions(eventArgs);
+ }
+
+ /// <summary>
+ /// 指定した手がXRHandShapeの条件を満たしているか判定する。
+ /// </summary>
+ /// <param name="handShape">判定対象のハンドシェイプ</param>
+ /// <param name="handedness">判定する手(左手/右手)</param>
+ /// <returns>条件を満たしていればtrue</returns>
+ public bool CheckHandShape(XRHandShape handShape, Handedness handedness)
+ {
+ if (Subsystem == null) return false;
+
+ var hand = handedness == Handedness.Left ? Subsystem.leftHand : Subsystem.rightHand;
+ if (!hand.isTracked) return false;
+
+ var eventArgs = new XRHandJointsUpdatedEventArgs { hand = hand };
+ return handShape.CheckConditions(eventArgs);
+ }
}
次に、実際にアセットファイルを渡して判定するサンプルコードです。
using UnityEngine;
using UnityEngine.XR.Hands;
using UnityEngine.XR.Hands.Gestures;
public class CustomHandGestureSample : MonoBehaviour
{
[SerializeField] private Renderer _targetRenderer;
[SerializeField] private XRHandShape _handShape;
[SerializeField] private XRHandPose _handPose;
private void Update()
{
var xrHandSubsystemUtility = XRHandSubsystemUtility.Instance;
if (!xrHandSubsystemUtility.IsSubsystemRunning) return;
var hand = xrHandSubsystemUtility.Subsystem.rightHand;
if (!hand.isTracked)
{
_targetRenderer.material.color = Color.gray;
return;
}
if (xrHandSubsystemUtility.CheckHandPose(_handPose, Handedness.Right))
{
_targetRenderer.material.color = Color.red;
}
else if (xrHandSubsystemUtility.CheckHandShape(_handShape, Handedness.Right))
{
_targetRenderer.material.color = Color.yellow;
}
else
{
_targetRenderer.material.color = Color.gray;
}
}
}
なぜかCheckConditionsはXR Device Simulatorで動きません。(なんで?)
以上です。よくできていますが、やはりMetaのSDKに比べると少々便利さに欠ける機能だと感じました。

