配置
Unity XR Toolkit + XR Hands
基础配置参考:
代码
我目前测试的的灵巧手是Curl(握紧/绷直)5个自由度(绳驱)+ Spread(侧向张开)5个自由度(电机驱动)。
Unity XR Hands自带5指curl和食指、中指、无名指的spread,拇指和小拇指需要自己写代码算
Unity的关节和其他参考:https://docs.unity3d.com/Packages/com.unity.xr.hands@1.5/manual/hand-data/xr-hand-data-model.html
直接上代码
using UnityEngine;
using UnityEngine.XR.Hands;
using UnityEngine.XR.Hands.Gestures;
using TMPro;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
public class XRHandsFlexDisplay : MonoBehaviour
{
[Header("Settings")]
[Range(0, 1)] public int handSide = 0; // 0 = Left, 1 = Right
public float sendInterval = 0.05f; // 20 Hz 默认
public TextMeshProUGUI debugText;
[Header("UDP Out")]
public string remoteIP = "192.168.50.233";
public int remotePort = 12345;
UdpClient udp;
float lastSendTime;
XRHandSubsystem handSub;
static readonly XRHandFingerID[] fids = {
XRHandFingerID.Thumb, XRHandFingerID.Index, XRHandFingerID.Middle,
XRHandFingerID.Ring, XRHandFingerID.Little };
void Start()
{
udp = new UdpClient();
udp.Connect(IPAddress.Parse(remoteIP), remotePort);
}
void Update()
{
if (handSub == null)
{
handSub = UnityEngine.XR.Management.XRGeneralSettings
.Instance.Manager.activeLoader?
.GetLoadedSubsystem<XRHandSubsystem>();
if (handSub == null) return;
}
XRHand hand = (handSide == 0) ? handSub.leftHand : handSub.rightHand;
if (!hand.isTracked) { debugText.text = "Hand not tracked"; return; }
// ---------- 1. 采集 Flex ----------
float[] flex01 = new float[5];
for (int i = 0; i < 5; ++i)
{
var s = XRFingerShapeMath.CalculateFingerShape(
hand, fids[i], XRFingerShapeTypes.FullCurl);
s.TryGetFullCurl(out flex01[i]);
}
// ---------- 2. 采集 Spread ----------
float[] spr01 = new float[5];
spr01[0] = ThumbSpreadBy3Points(hand); // Thumb
for (int i = 1; i <= 3; ++i) // Index-Ring
{
var s = XRFingerShapeMath.CalculateFingerShape(
hand, fids[i], XRFingerShapeTypes.Spread);
s.TryGetSpread(out spr01[i]);
}
spr01[4] = spr01[3]; // Little = Ring
float ringSend = spr01[3] * 0.5f; // Ring 半值
float middleSend = spr01[2] * 0.5f; // Middle 半值
// ---------- 3. UDP 发送(按节拍) ----------
if (Time.time - lastSendTime >= sendInterval)
{
byte[] pkt = new byte[10];
for (int i = 0; i < 5; ++i) pkt[i] = ToByte(flex01[i]);
pkt[5] = ToByte(spr01[0]);
pkt[6] = ToByte(spr01[1]);
pkt[7] = ToByte(middleSend);
pkt[8] = ToByte(ringSend);
pkt[9] = ToByte(spr01[4]);
udp.Send(pkt, pkt.Length);
lastSendTime = Time.time;
// ---------- 4. TMP 两行显示 ----------
var sb = new StringBuilder();
sb.Append("Flex: ");
foreach (var v in flex01) sb.Append($"{v:0.00} ");
sb.AppendLine();
sb.Append("Spread: ");
foreach (var v in spr01) sb.Append($"{v:0.00} ");
debugText.text = sb.ToString();
}
}
static byte ToByte(float v) =>
(byte)Mathf.Clamp(Mathf.RoundToInt(v * 255f), 0, 255);
float ThumbSpreadBy3Points(XRHand hand)
{
XRHandJoint tip = hand.GetJoint(XRHandJointID.ThumbDistal);
XRHandJoint root = hand.GetJoint(XRHandJointID.ThumbMetacarpal);
XRHandJoint idxP = hand.GetJoint(XRHandJointID.IndexProximal);
if (!tip.TryGetPose(out Pose a) ||
!root.TryGetPose(out Pose b) ||
!idxP.TryGetPose(out Pose c)) return 0f;
float ang = Vector3.Angle(a.position - b.position, c.position - b.position);
return Mathf.Clamp01(Mathf.InverseLerp(30f, 65f, ang));
}
void OnApplicationQuit() => udp?.Dispose();
}
接收端测试代码,python为例
#!/usr/bin/env python3
"""
UDP receiver for 10-byte hand-data packets (5 × flex, 5 × spread).
Usage
-----
$ python hand_udp_receiver.py # 监听 0.0.0.0:12345
$ python hand_udp_receiver.py -p 5555 # 改用端口 5555
"""
import socket
import argparse
def main():
ap = argparse.ArgumentParser(description="Receive 10-byte hand packets over UDP.")
ap.add_argument("-p", "--port", type=int, default=12345,
help="listen port (default: 12345)")
args = ap.parse_args()
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("0.0.0.0", args.port))
print(f"🟢 Listening on 0.0.0.0:{args.port} …")
try:
while True:
data, addr = sock.recvfrom(1024) # ≥10 bytes
if len(data) < 10:
continue # 跳过错误包
frame = list(data[:10]) # 10 byte → int list
# 一行打印:0-4 = curl, 5-9 = spread
print(" ".join(f"{v:3d}" for v in frame))
except KeyboardInterrupt:
print("\n⏹ Stopped by user.")
finally:
sock.close()
if __name__ == "__main__":
main()
定义
Byte Index | Finger / Channel | Type | Value 0 | Value 255 |
---|---|---|---|---|
0 | Thumb Curl | Flex | Thumb完全伸直 | Thumb握到掌心 |
1 | Index Curl | Flex | 食指完全伸直 | 食指握到掌心 |
2 | Middle Curl | Flex | 中指完全伸直 | 中指握到掌心 |
3 | Ring Curl | Flex | 无名指完全伸直 | 无名指握到掌心 |
4 | Little Curl | Flex | 小指完全伸直 | 小指握到掌心 |
5 | Thumb Spread | Abduction | 拇指贴近食指(对掌) | 拇指最大外展 |
6 | Index Spread | Abduction | 食指正前方(与中指平行) | 向拇指方向最大外展 |
7 | Middle Spread† | Abduction | 中指正前方(基准) | 向拇指方向½ 幅度外展(原值×0.5) |
8 | Ring Spread† | Abduction | 无名指正前方 | 向小指方向½ 幅度外展(原值×0.5) |
9 | Little Spread | Abduction | 小指正前方 | 向小指外侧(远离拇指)最大外展 = 无名指原始 spread |
† Middle/Ring Spread 在发送前乘 0.5,用于减小中指/无名指舵机动作幅度。
小拇指因为没有直接可读取的值,直接取无名指原始数据(发送的数据的两倍)。