はじめに
こんにちは、ユーゴです。今回は、NReal Light (Dev Kit)で開発をしていた時に得た知見を共有したいと思います。
NRealで簡単に指の曲げ具合を判別できないものだろうかと思い調べてみたものの、いい記事が見つからずに自前実装しました。今回は、実装方法について紹介します。
なお、HoloLensやMeta Questのハンドトラッキングでも応用が効くアルゴリズムとなっております。
問題
NRealで指の曲げ具合、ひいては手の形(ハンドサイン)を取得したいです。しかし、指の曲げ具合を取得するAPIがありません。
要件
以下のように、指をそれぞれ「開いている(1)」〜「閉じている(0)」と線形的に数値を取得したいとします。
解決策
以下のように、指の関節の角度は180°〜90°と変化するため、「平均180° → 1」「平均90° → 0」と計算します。
親指は根本の関節1つをなくして計算した方が精度が取れそうです。
コードと導入
以下がコードです。確認用にDebug.LogがUpdateに入っているので、不要なら消してください。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using NRKernal;
[Serializable]
public class HandShape
{
[Range(0, 1)] public float thumb;
[Range(0, 1)] public float index;
[Range(0, 1)] public float middle;
[Range(0, 1)] public float ring;
[Range(0, 1)] public float pinky;
}
public class HandShapeManager : MonoBehaviour
{
//<Variables>ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
//<Variables(Singleton)>ーーーーーーーーーーーーーーーーーーーーー
static HandShapeManager instance;
public static HandShapeManager Instance
{
get
{
if (instance == null) instance = FindObjectOfType<HandShapeManager>() as HandShapeManager;
return instance;
}
}
//<Variables(Inspector)>ーーーーーーーーーーーーーーーーーーーーー
//右手か左手か選ぶ
[SerializeField] HandEnum handEnum;
//<Variables(General)>ーーーーーーーーーーーーーーーーーーーーー
[HideInInspector] public HandShape handShape = new HandShape();
//<Methods>ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
void Update()
{
GetFingerValues(handEnum);
Debug.Log($"thumb:{handShape.thumb:F1}, index:{handShape.index:F1}, middle:{handShape.middle:F1}, ring:{handShape.ring:F1}, pinky:{handShape.pinky:F1}");
}
void GetFingerValues(HandEnum _handEnum)
{
HandState handState = NRInput.Hands.GetHandState(_handEnum);
handShape.thumb = GetValue_Thumb(handState);
handShape.index = GetValue_Index(handState);
handShape.middle = GetValue_Middle(handState);
handShape.ring = GetValue_Ring(handState);
handShape.pinky = GetValue_Pinky(handState);
}
float GetValue_Thumb(HandState handState)
{
Pose wrist = handState.GetJointPose(HandJointID.Wrist);
Pose pose_proximal = handState.GetJointPose(HandJointID.ThumbProximal);
Pose pose_distal = handState.GetJointPose(HandJointID.ThumbDistal);
Pose pose_tip = handState.GetJointPose(HandJointID.ThumbTip);
float angle1 = Vector3.Angle(wrist.position - pose_proximal.position, pose_distal.position - pose_proximal.position);
float angle2 = Vector3.Angle(pose_proximal.position - pose_distal.position, pose_tip.position - pose_distal.position);
float value = (angle1 + angle2) / 2;
value = Mathf.Clamp01(value / 90f - 1);
return value;
}
float GetAvarageValue(Pose pose_wrist, Pose pose_proximal, Pose pose_middle, Pose pose_distal, Pose pose_tip)
{
float angle1 = Vector3.Angle(pose_wrist.position - pose_proximal.position, pose_middle.position - pose_proximal.position);
float angle2 = Vector3.Angle(pose_proximal.position - pose_middle.position, pose_distal.position - pose_middle.position);
float angle3 = Vector3.Angle(pose_middle.position - pose_distal.position, pose_tip.position - pose_distal.position);
float value = (angle1 + angle2 + angle3) / 3;
value = Mathf.Clamp01(value / 90f - 1);
return value;
}
float GetValue_Index(HandState handState)
{
Pose pose_wrist = handState.GetJointPose(HandJointID.Wrist);
Pose pose_proximal = handState.GetJointPose(HandJointID.IndexProximal);
Pose pose_middle = handState.GetJointPose(HandJointID.IndexMiddle);
Pose pose_distal = handState.GetJointPose(HandJointID.IndexDistal);
Pose pose_tip = handState.GetJointPose(HandJointID.IndexTip);
return GetAvarageValue(pose_wrist, pose_proximal, pose_middle, pose_distal, pose_tip);
}
float GetValue_Middle(HandState handState)
{
Pose pose_wrist = handState.GetJointPose(HandJointID.Wrist);
Pose pose_proximal = handState.GetJointPose(HandJointID.MiddleProximal);
Pose pose_middle = handState.GetJointPose(HandJointID.MiddleMiddle);
Pose pose_distal = handState.GetJointPose(HandJointID.MiddleDistal);
Pose pose_tip = handState.GetJointPose(HandJointID.MiddleTip);
return GetAvarageValue(pose_wrist, pose_proximal, pose_middle, pose_distal, pose_tip);
}
float GetValue_Ring(HandState handState)
{
Pose pose_wrist = handState.GetJointPose(HandJointID.Wrist);
Pose pose_proximal = handState.GetJointPose(HandJointID.RingProximal);
Pose pose_middle = handState.GetJointPose(HandJointID.RingMiddle);
Pose pose_distal = handState.GetJointPose(HandJointID.RingDistal);
Pose pose_tip = handState.GetJointPose(HandJointID.RingTip);
return GetAvarageValue(pose_wrist, pose_proximal, pose_middle, pose_distal, pose_tip);
}
float GetValue_Pinky(HandState handState)
{
Pose pose_wrist = handState.GetJointPose(HandJointID.Wrist);
Pose pose_proximal = handState.GetJointPose(HandJointID.PinkyProximal);
Pose pose_middle = handState.GetJointPose(HandJointID.PinkyMiddle);
Pose pose_distal = handState.GetJointPose(HandJointID.PinkyDistal);
Pose pose_tip = handState.GetJointPose(HandJointID.PinkyTip);
return GetAvarageValue(pose_wrist, pose_proximal, pose_middle, pose_distal, pose_tip);
}
}
手順
(1) 適当なシーンに「NRCameraRig」「NRInput」を配置
(2) NRInputの「Input Source Type」を「Hands」にする
(3) 空のオブジェクトを作り、それに「HandShapeManager」をアタッチ
(4) HandShapeManagerで取得する値を、右手か左手か選べます。シングルトンなので、両方取得したい場合はちょっとだけプログラムを書き換える必要があります。2個置いても無理です。
HandShapeインスタンスを2個作って、決め打ちでGetFingerValuesに値を入れるくらいです。
(5) 必要なクラスから、以下のように取得します。
//親指の曲げ具合
float thumbValue = HandShapeManager.Incetance.handShape.thumb;
ちなみに
手の形のサンプルをいくつか用意して、どれに近いかユークリッド距離で求めるとかもできます。三平方の定理5次元ベクトル版をやればいいだけです。コードはしんどいので書いてません。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameLogic : MonoBehaviour
{
[SerializeField] HandShape goo_shape;//グー
[SerializeField] HandShape choki_shape;//チョキ
[SerializeField] HandShape par_shape;//パー
}
まとめ
いかがだったでしょうか。今回はアルゴリズム解説寄りの記事でした。
xRが普及する時代はもう間近に迫っていると考えています。xR開発時代に向けて、このアルゴリズムが役に立てばと思います。
このように、アルゴリズムの紹介といった中級者〜上級者向けの内容から、Unityなどの基本的な操作など初心者向けの記事まで幅広く扱っていきます。
この記事がお役に立ちましたら、LGTM・ストック・フォローの方よろしくお願いいたします!