Could not find Hand Subsystem

Hi there,

since the oob solution for two pinch input (second pinch is only detected when hand is moved) isn’t working really well for us, i started experimenting with custom hand tracking.

Sadly I can not get Unitys MixedReality Scene from the poly spatial samples to work. I get a LogError saying Could not find Hand Subsystem from PinchSpawn.cs:91.

I tried to test the input with the XR Device Simulator in Editor, just wanted to see if I can get reliable pinches.

Thanks for any help!

1 Like

I struggled a bit with this awhile back, and ended up modeling my code after the HandsVisualizer that comes with the XRHands package. I have an entire working sample app I can share, but it needs updating to the latest release. Here’s an excerpt of the main bits that access the hand subsystem and expose the positions of each joint:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Hands;
using UnityEngine.InputSystem;

public class SNHandTracking_XRHands : SNHandTracking

   XRHandSubsystem _handSys = null;
   bool _leftHandTracking = false;
   bool _rightHandTracking = false;

   Vector3[] _jointPosLeft = new Vector3[(uint)SNHandJoint.JOINTS_MAX];
   Vector3[] _jointPosRight = new Vector3[(uint)SNHandJoint.JOINTS_MAX];
   Quaternion[] _jointRotLeft = new Quaternion[(uint)SNHandJoint.JOINTS_MAX];
   Quaternion[] _jointRotRight = new Quaternion[(uint)SNHandJoint.JOINTS_MAX];

   void Awake()
      //make input system work faster?
      InputSystem.settings.SetInternalFeatureFlag("USE_OPTIMIZED_CONTROLS", true);
      #endif // ENABLE_INPUT_SYSTEM

      for (int i = 0; i < _jointPosLeft.Length; i++)
         _jointPosLeft[i] =;
      for (int i = 0; i < _jointPosRight.Length; i++)
         _jointPosRight[i] =;
      for (int i = 0; i < _jointRotLeft.Length; i++)
         _jointRotLeft[i] = Quaternion.identity;
      for (int i = 0; i < _jointRotRight.Length; i++)
         _jointRotRight[i] = Quaternion.identity;

   public override bool GetIsTracking(Hand h)
      return (h == Hand.Left) ? _leftHandTracking : _rightHandTracking;

   public override Vector3 GetPosition(Hand h, SNHandJoint j) 
      if (h == Hand.Left)
         return _jointPosLeft[(uint)j];
         return _jointPosRight[(uint)j];
   public override Quaternion GetRotation(Hand h, SNHandJoint j) 
      if (h == Hand.Left)
         return _jointRotLeft[(uint)j];
         return _jointRotRight[(uint)j];

   void _OnTrackingAcquired(XRHand hand)
      if (hand.handedness == Handedness.Left)
         _leftHandTracking = true;
      else if (hand.handedness == Handedness.Right)
         _rightHandTracking = true;

      Debug.Log("TRACKING ACQUIRED for hand " + hand.handedness.ToString() +  " at " + Time.time);

   void _OnTrackingLost(XRHand hand)
      if (hand.handedness == Handedness.Left)
         _leftHandTracking = false;
      else if (hand.handedness == Handedness.Right)
         _rightHandTracking = false;

      Debug.Log("TRACKING LOST for hand " + hand.handedness.ToString() + " at " + Time.time);

   void _OnUpdatedHands(XRHandSubsystem subsystem, XRHandSubsystem.UpdateSuccessFlags updateSuccessFlags, XRHandSubsystem.UpdateType updateType)

   void _SubscribeHandSubsystem()
      if (_handSys == null)

      _handSys.trackingAcquired += _OnTrackingAcquired;
      _handSys.trackingLost += _OnTrackingLost;
      _handSys.updatedHands += _OnUpdatedHands;

      Debug.Log("SUBSCRIBE hand subsystem at " + Time.time);

   void _UnsubscribeHandSubsystem()
      if (_handSys == null)

      _handSys.trackingAcquired -= _OnTrackingAcquired;
      _handSys.trackingLost -= _OnTrackingLost;
      _handSys.updatedHands -= _OnUpdatedHands;

      Debug.Log("UNSUBSCRIBE hand subsystem at " + Time.time);

   void _RefreshData(XRHandSubsystem subsystem)
      if (subsystem == null)

      _rightHandTracking = subsystem.rightHand.isTracked;
      _leftHandTracking = subsystem.leftHand.isTracked;

      _UpdateHandData(subsystem.leftHand, ref _jointPosLeft, ref _jointRotLeft);
      _UpdateHandData(subsystem.rightHand, ref _jointPosRight, ref _jointRotRight);

      //Debug.Log("UPDATE hand subsystem at " + Time.time);

   void _UpdateHandData(XRHand hand, ref Vector3[] posData, ref Quaternion[] rotData)
      Pose rootPose = hand.rootPose;

      for(int i = 0; i < posData.Length; i++)
         SNHandJoint hj = (SNHandJoint)i;
         XRHandJointID xrj = _ToXRHandJoint(hj);

         var handJoint = hand.GetJoint(xrj);
         Pose pose;
         handJoint.TryGetPose(out pose);
         if(pose != null)
            //Pose t = pose.GetTransformedBy(rootPose);
            //posData[i] = t.position;
            //rotData[i] = t.rotation;

            //Debug.Log("Update Joint " + hj.ToString() + " pos(" + pose.position + ") finalPos(" + posData[i] + ") at " + Time.time);
            posData[i] = pose.position;
            rotData[i] = pose.rotation;

   public override bool GetIsJointSupported(SNHandJoint j)
      if ((j == SNHandJoint.Thumb0) || (j == SNHandJoint.Pinky0))
         return false;

      return true;

   //convert from our enum to their enum
   XRHandJointID _ToXRHandJoint(SNHandJoint j)
         case SNHandJoint.Wrist:     return XRHandJointID.Wrist;
         case SNHandJoint.Thumb0:    return XRHandJointID.Palm; //technically should be trapezium bone
         case SNHandJoint.Thumb1:    return XRHandJointID.ThumbMetacarpal;
         case SNHandJoint.Thumb2:    return XRHandJointID.ThumbProximal;
         case SNHandJoint.Thumb3:    return XRHandJointID.ThumbDistal;
         case SNHandJoint.ThumbTip:  return XRHandJointID.ThumbTip;
         case SNHandJoint.Index1:    return XRHandJointID.IndexProximal;
         case SNHandJoint.Index2:    return XRHandJointID.IndexIntermediate;
         case SNHandJoint.Index3:    return XRHandJointID.IndexDistal;
         case SNHandJoint.IndexTip:  return XRHandJointID.IndexTip;
         case SNHandJoint.Middle1:   return XRHandJointID.MiddleProximal;
         case SNHandJoint.Middle2:   return XRHandJointID.MiddleIntermediate;
         case SNHandJoint.Middle3:   return XRHandJointID.MiddleDistal;
         case SNHandJoint.MiddleTip: return XRHandJointID.MiddleTip;
         case SNHandJoint.Ring1:     return XRHandJointID.RingProximal;
         case SNHandJoint.Ring2:     return XRHandJointID.RingIntermediate;
         case SNHandJoint.Ring3:     return XRHandJointID.RingDistal;
         case SNHandJoint.RingTip:   return XRHandJointID.RingTip;
         case SNHandJoint.Pinky0:    return XRHandJointID.LittleMetacarpal;
         case SNHandJoint.Pinky1:    return XRHandJointID.LittleProximal;
         case SNHandJoint.Pinky2:    return XRHandJointID.LittleIntermediate;
         case SNHandJoint.Pinky3:    return XRHandJointID.LittleDistal;
         case SNHandJoint.PinkyTip:  return XRHandJointID.LittleTip;
         default:  return XRHandJointID.EndMarker;

   static readonly List<XRHandSubsystem> s_SubsystemsReuse = new List<XRHandSubsystem>();
   protected override void Update()

      //got hand subsystem, will handle everything in its callbacks
      if (_handSys != null && _handSys.running)

      var foundRunningHandSubsystem = false;
      for (var i = 0; i < s_SubsystemsReuse.Count; ++i)
         var handSubsystem = s_SubsystemsReuse[i];
         if (handSubsystem.running)
            _handSys = handSubsystem;
            Debug.Log("Found hand subsystem at " + Time.time);
            foundRunningHandSubsystem = true;

      if (!foundRunningHandSubsystem)




Thanks for sharing!