using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Android; using System.Runtime.InteropServices; using TouchlessA3D; using System; using System.Xml; using System.Text; namespace Ximmerse.XR.InputSystems { /// /// Hand tracking module implementor on touchless 3D SDK. /// internal class HandTrackingT3D : I_HandleTrackingModule { private readonly static int width = 1440; private readonly static int height = 1080; //264 /// /// Image data array. /// private Color32[] imgData = new Color32[width * height]; private bool m_IsModuleEnabled; /// /// RGB camera texture to be passed to native plugin. /// private WebCamTexture cameraTexture; private GCHandle imageHandle; public bool IsModuleEnabled { get => m_IsModuleEnabled; } private Engine touchlessEngine; Transform handTrackingAnchor; Transform mainCamera; Matrix4x4 hand_local_2_world; /// /// Raw hand track info by native plugin. /// HandTrackingInfo handTrackInfo; /// /// 上一个合法帧的hand track info. /// HandTrackingInfo previousValidFrameHandTrackInfo; /// /// Gets the hand track info /// public HandTrackingInfo HandleTrackInfo { get => handTrackInfo; } public bool IsTrackings { get => handTrackInfo.IsTracking; } private readonly object lockObj = new object(); GestureEvent locked_gestureEvent; public void DisableModule() { #if DEVELOPMENT_BUILD try { #endif if (!m_IsModuleEnabled) { return; } m_IsModuleEnabled = false; handTrackInfo.Dispose(); previousValidFrameHandTrackInfo.Dispose(); //cameraTexture.Stop(); //cameraTexture = null; XimmerseXR.RequestStopRGBCamera(); touchlessEngine = null; imageHandle.Free(); #if DEVELOPMENT_BUILD } catch (Exception e) { Debug.LogException(e); } #endif } /// /// A 3 x 3 float array to calibrate RGB camera. /// static float[,] CalibrationMatrix = null; /// /// A 8 array to identify the distortion coefficients of the RGB camera. /// static float[] DistortionCoefficients = null; /// /// XML file path contains the rgb camera parameter. /// const string kRGBCalibrationXMLPath = "/backup/rgb_vio_camera_params.xml"; static void ParseRGBCameraParams() { try { XmlDocument xmlDoc = new XmlDocument(); // Create an XML document object xmlDoc.Load(kRGBCalibrationXMLPath); // Load the XML document from the specified file XmlNodeList RGBCamMats = xmlDoc.GetElementsByTagName("RGBCamMat"); ParseCalibrationMatrix(RGBCamMats, out CalibrationMatrix); XmlNodeList RGBDistCoeff = xmlDoc.GetElementsByTagName("RGBDistCoeff"); ParseDistortionCoefficients(RGBDistCoeff, out DistortionCoefficients); #if DEVELOPMENT_BUILD StringBuilder buffer = new StringBuilder(); buffer.AppendFormat("CalibrationMatrix : "); for (int row = 0; row < 3; row++) for (int col = 0; col < 3; col++) { buffer.AppendFormat("{0} ", CalibrationMatrix[col, row]); } buffer.AppendFormat("\r\nDistortionCoefficients : "); foreach (var coefficient in DistortionCoefficients) { buffer.AppendFormat("{0} ", coefficient); } #endif } catch (Exception exc) { Debug.LogErrorFormat("ParseRGBCameraParams error : {0}", exc.Message); Debug.LogException(exc); } } /// /// Reads XML element and output a calibrationMatrix (3x3) array /// /// /// private static void ParseCalibrationMatrix(XmlNodeList RGBCamMats, out float[,] calibrationMatrix) { calibrationMatrix = new float[3, 3]; int row = 0, col = 0; try { XmlElement dataElement = RGBCamMats.Item(0)["data"]; var textInArray = dataElement.InnerText.Trim().Split(' '); for (int i = 0; i < textInArray.Length; i++) { string t = textInArray[i]; if (float.TryParse(t, out float value)) { calibrationMatrix[row, col] = value; col++; if (col >= 3) { col = 0; row++; } } } } catch (Exception exc) { Debug.LogException(exc); } } /// /// Reads XML element and output a distortion cofficient (8) array /// /// /// private static void ParseDistortionCoefficients(XmlNodeList RGBDistCoeff, out float[] DistortionCofficients) { DistortionCofficients = new float[8]; int col = 0; try { XmlElement dataElement = RGBDistCoeff.Item(0)["data"]; var textInArray = dataElement.InnerText.Trim().Split(' '); for (int i = 0; i < textInArray.Length; i++) { string t = textInArray[i]; if (float.TryParse(t, out float value)) { DistortionCofficients[col] = value; col++; if (col >= 8) { break; } } } } catch (Exception exc) { Debug.LogException(exc); } } public bool EnableModule(InitializeHandTrackingModuleParameter initParameter) { if (DistortionCoefficients == null || CalibrationMatrix == null) { ParseRGBCameraParams(); } #if DEVELOPMENT_BUILD try { #endif if (m_IsModuleEnabled) { Debug.Log("HandTrackingT3D module already activate."); return false; } handTrackingAnchor = initParameter.TrackingAnchor; if (!Permission.HasUserAuthorizedPermission(Permission.Camera)) { Permission.RequestUserPermission(Permission.Camera); } //WebCamDevice[] devices = WebCamTexture.devices; //if (devices.Length == 0) // return false; //for (int i = 0; i < devices.Length; i++) //{ // var curr = devices[i]; // if (curr.isFrontFacing == true) // { // //RhinoX using front facing camera // cameraTexture = new WebCamTexture(curr.name, width, height, 60); // break; // } //} //cameraTexture.Play(); XimmerseXR.RequestOpenRGBCamera(width, height); cameraTexture = XimmerseXR.RGBCameraTexture; imageHandle = GCHandle.Alloc(imgData, GCHandleType.Pinned); string uniqueID = SystemInfo.deviceUniqueIdentifier; string storageLocation = Application.persistentDataPath; //var calibration = new NativeCalibration(); //var calibration = new Calibration(cameraTexture.width, cameraTexture.height, // CalibrationMatrix, DistortionCoefficients); var calibration = new Calibration(1440, 1080, CalibrationMatrix, DistortionCoefficients); //var calibration = new Calibration(width * 4f, height * 4f, CalibrationMatrix, DistortionCoefficients); touchlessEngine = new Engine(uniqueID, storageLocation, calibration, OnTouchlessEvent); handTrackInfo = new HandTrackingInfo() { ThumbFinger = new RawFingerTrackingInfo(3) { bendnessRangeMin = 0.7f, bendnessRangeMax = 0.9f, }, IndexFinger = new RawFingerTrackingInfo(4) { bendnessRangeMin = 0.1f, bendnessRangeMax = 0.9f, }, MiddleFinger = new RawFingerTrackingInfo(4) { bendnessRangeMin = 0.1f, bendnessRangeMax = 0.9f, }, RingFinger = new RawFingerTrackingInfo(4) { bendnessRangeMin = 0.1f, bendnessRangeMax = 0.9f, }, LittleFinger = new RawFingerTrackingInfo(4) { bendnessRangeMin = 0.1f, bendnessRangeMax = 0.9f, }, }; previousValidFrameHandTrackInfo = new HandTrackingInfo() { ThumbFinger = new RawFingerTrackingInfo(3) { bendnessRangeMin = 0.7f, bendnessRangeMax = 0.9f, }, IndexFinger = new RawFingerTrackingInfo(4) { bendnessRangeMin = 0.1f, bendnessRangeMax = 0.9f, }, MiddleFinger = new RawFingerTrackingInfo(4) { bendnessRangeMin = 0.1f, bendnessRangeMax = 0.9f, }, RingFinger = new RawFingerTrackingInfo(4) { bendnessRangeMin = 0.1f, bendnessRangeMax = 0.9f, }, LittleFinger = new RawFingerTrackingInfo(4) { bendnessRangeMin = 0.1f, bendnessRangeMax = 0.9f, }, }; m_IsModuleEnabled = true; Debug.Log("T3D hand tracking is now active."); return true; #if DEVELOPMENT_BUILD } catch (Exception e) { Debug.LogException(e); } return false; #endif } private bool isvalid; private bool isTracking; int frame = 0; public void Optimize() { isvalid = handTrackInfo.IsValid; if (isvalid) { frame = 0; handTrackInfo.IsTracking = isvalid; } else { frame++; if (frame > 5) { handTrackInfo.IsTracking = isvalid; } } } /// /// Call per frame , in main thread. /// public void Tick() { #if DEVELOPMENT_BUILD try { #endif //备份当前帧 if (handTrackInfo.IsValid) { previousValidFrameHandTrackInfo.CopyFrom(handTrackInfo); } //对于50ms之前的合法帧,由于时间过长, 需要抛弃 else if (previousValidFrameHandTrackInfo.IsValid) { if (new TimeSpan(handTrackInfo.Timestamp - previousValidFrameHandTrackInfo.Timestamp).TotalMilliseconds > 50d) { previousValidFrameHandTrackInfo.IsValid = false; } } handTrackInfo.IsValid = false;//reset hand track info before native plugin feedback if (!m_IsModuleEnabled || null == cameraTexture || !cameraTexture.didUpdateThisFrame) { return; } //GestureEvent syncedEvent = null; lock (lockObj) { if (locked_gestureEvent != null) { ParseGestureEvent(locked_gestureEvent); //syncedEvent = new GestureEvent(locked_gestureEvent); //locked_gestureEvent = null; } } Optimize(); ////Debug.LogFormat("HandTrackingT3D.Tick : {0}, {1}", syncedEvent != null, syncedEvent != null && syncedEvent.skeletonValid); //if (null != syncedEvent) //{ // ParseGestureEvent(locked_gestureEvent); //} //update the tracking anchor local to world matrix hand_local_2_world = handTrackingAnchor.localToWorldMatrix; cameraTexture.GetPixels32(imgData); using (var frame = new Frame(imageHandle.AddrOfPinnedObject(), cameraTexture.width * 4, cameraTexture.width, cameraTexture.height, System.DateTimeOffset.Now.ToUnixTimeMilliseconds(), FrameRotation.ROTATION_NONE, true)) { touchlessEngine.handleFrame(frame); } #if DEVELOPMENT_BUILD } catch (Exception e) { Debug.LogException(e); } #endif } /// /// Convert T3D skeleton to hand track info : /// /// private void ParseGestureEvent(GestureEvent args) { Skeleton t3dSkel = args.skeleton; if (handTrackInfo.IsValid && (!args.skeletonValid || t3dSkel == null)) { Debug.Log("Hand tracking become invalid !"); } if (t3dSkel == null) { return; } handTrackInfo.IsValid = args.skeletonValid; handTrackInfo.Timestamp = System.DateTime.Now.Ticks; //update raw hand tracking info data handTrackInfo.Handness = args.handedness == HandednessType.LEFT_HAND ? HandnessType.Left : HandnessType.Right; handTrackInfo.ThumbFinger.Positions[0] = hand_local_2_world.MultiplyPoint3x4(t3dSkel.points[SkeletonPointsID.THUMB2]); handTrackInfo.ThumbFinger.Positions[1] = hand_local_2_world.MultiplyPoint3x4(t3dSkel.points[SkeletonPointsID.THUMB3]); handTrackInfo.ThumbFinger.Positions[2] = hand_local_2_world.MultiplyPoint3x4(t3dSkel.points[SkeletonPointsID.THUMB4]); handTrackInfo.IndexFinger.Positions[0] = hand_local_2_world.MultiplyPoint3x4(t3dSkel.points[SkeletonPointsID.INDEX1]); handTrackInfo.IndexFinger.Positions[1] = hand_local_2_world.MultiplyPoint3x4(t3dSkel.points[SkeletonPointsID.INDEX2]); handTrackInfo.IndexFinger.Positions[2] = hand_local_2_world.MultiplyPoint3x4(t3dSkel.points[SkeletonPointsID.INDEX3]); handTrackInfo.IndexFinger.Positions[3] = hand_local_2_world.MultiplyPoint3x4(t3dSkel.points[SkeletonPointsID.INDEX4]); handTrackInfo.MiddleFinger.Positions[0] = hand_local_2_world.MultiplyPoint3x4(t3dSkel.points[SkeletonPointsID.MIDDLE1]); handTrackInfo.MiddleFinger.Positions[1] = hand_local_2_world.MultiplyPoint3x4(t3dSkel.points[SkeletonPointsID.MIDDLE2]); handTrackInfo.MiddleFinger.Positions[2] = hand_local_2_world.MultiplyPoint3x4(t3dSkel.points[SkeletonPointsID.MIDDLE3]); handTrackInfo.MiddleFinger.Positions[3] = hand_local_2_world.MultiplyPoint3x4(t3dSkel.points[SkeletonPointsID.MIDDLE4]); handTrackInfo.RingFinger.Positions[0] = hand_local_2_world.MultiplyPoint3x4(t3dSkel.points[SkeletonPointsID.RING1]); handTrackInfo.RingFinger.Positions[1] = hand_local_2_world.MultiplyPoint3x4(t3dSkel.points[SkeletonPointsID.RING2]); handTrackInfo.RingFinger.Positions[2] = hand_local_2_world.MultiplyPoint3x4(t3dSkel.points[SkeletonPointsID.RING3]); handTrackInfo.RingFinger.Positions[3] = hand_local_2_world.MultiplyPoint3x4(t3dSkel.points[SkeletonPointsID.RING4]); handTrackInfo.LittleFinger.Positions[0] = hand_local_2_world.MultiplyPoint3x4(t3dSkel.points[SkeletonPointsID.PINKY1]); handTrackInfo.LittleFinger.Positions[1] = hand_local_2_world.MultiplyPoint3x4(t3dSkel.points[SkeletonPointsID.PINKY2]); handTrackInfo.LittleFinger.Positions[2] = hand_local_2_world.MultiplyPoint3x4(t3dSkel.points[SkeletonPointsID.PINKY3]); handTrackInfo.LittleFinger.Positions[3] = hand_local_2_world.MultiplyPoint3x4(t3dSkel.points[SkeletonPointsID.PINKY4]); handTrackInfo.UpdateProperties(); //Debug.LogFormat("bendness thumb = {0}, index = {1}, middle = {2}, ring = {3}, little = {4}", handTrackInfo.ThumbFinger.bendness, handTrackInfo.IndexFinger.bendness, //handTrackInfo.MiddleFinger.bendness, handTrackInfo.RingFinger.bendness, handTrackInfo.LittleFinger.bendness); Vector3 wristPos = hand_local_2_world.MultiplyPoint3x4(t3dSkel.points[SkeletonPointsID.WRIST]); Vector3 wristToRing = handTrackInfo.RingFinger.Positions[0] - wristPos; Vector3 wristToMiddle = handTrackInfo.MiddleFinger.Positions[0] - wristPos; Vector3 ringToMiddle = handTrackInfo.MiddleFinger.Positions[0] - handTrackInfo.RingFinger.Positions[0]; handTrackInfo.PalmPosition = wristPos + wristToRing * 0.45f + ringToMiddle * 0.43f; handTrackInfo.PalmScale = Vector3.one * wristToMiddle.magnitude * 0.86f; if (previousValidFrameHandTrackInfo.IsValid) { handTrackInfo.PalmDeltaPosition = handTrackInfo.PalmPosition - previousValidFrameHandTrackInfo.PalmPosition; handTrackInfo.PalmVelocity = handTrackInfo.PalmDeltaPosition / (float)new TimeSpan(handTrackInfo.Timestamp - previousValidFrameHandTrackInfo.Timestamp).TotalSeconds; } Vector3 crs = Vector3.Cross(wristToRing, wristToMiddle); //Make palm normal always facing UP upon the palm surface: if (args.handedness == HandednessType.LEFT_HAND) { crs = -crs; } handTrackInfo.PalmRotation = Quaternion.LookRotation(wristToRing, crs); handTrackInfo.PalmNormal = crs.normalized; //Calculate local position to main camera: if (!mainCamera) { mainCamera = Camera.main.transform; } if (mainCamera && mainCamera.parent) { if (mainCamera.parent) { handTrackInfo.PalmLocalPosition = mainCamera.parent.InverseTransformPoint(handTrackInfo.PalmPosition); handTrackInfo.PalmLocalRotation = Quaternion.Inverse(mainCamera.parent.rotation) * handTrackInfo.PalmRotation; handTrackInfo.PalmLocalNormal = mainCamera.parent.InverseTransformVector(handTrackInfo.PalmNormal); } else { handTrackInfo.PalmLocalPosition = mainCamera.InverseTransformPoint(handTrackInfo.PalmPosition); handTrackInfo.PalmLocalRotation = Quaternion.Inverse(mainCamera.rotation) * handTrackInfo.PalmRotation; handTrackInfo.PalmLocalNormal = mainCamera.InverseTransformVector(handTrackInfo.PalmNormal); } } //Debug.LogFormat("Get valid hand track frame at time: {0}, is valid: {3} palm point: {1}, thumb point: {2}", // handTrackInfo.Timestamp, handTrackInfo.PalmPosition, handTrackInfo.ThumbFinger.Positions[0], handTrackInfo.IsValid); handTrackInfo.NativeGestureType = (int)args.type; //Update gesture enum: if (handTrackInfo.IsValid == false) { handTrackInfo.gestureFistOpenHand = GestureType_Fist_OpenHand.None; handTrackInfo.gestureGrisp = GestureType_Grisp.None; handTrackInfo.NativeGestureType = -1; } else { //gesture type of open hand / grisp : if (handTrackInfo.NativeGestureType == (int)TouchlessA3D.GestureType.HAND || handTrackInfo.NativeGestureType == (int)TouchlessA3D.GestureType.OPEN_HAND) { handTrackInfo.gestureFistOpenHand = GestureType_Fist_OpenHand.Opened; handTrackInfo.gestureGrisp = GestureType_Grisp.GrispOpen; } //Fist - close hand and grasp clsoed if (handTrackInfo.NativeGestureType == (int)TouchlessA3D.GestureType.CLOSED_HAND) { handTrackInfo.gestureFistOpenHand = GestureType_Fist_OpenHand.Fist; handTrackInfo.gestureGrisp = GestureType_Grisp.GraspClosed; } //Grisp pinch: if (handTrackInfo.NativeGestureType == (int)TouchlessA3D.GestureType.CLOSED_PINCH) { handTrackInfo.gestureGrisp = GestureType_Grisp.GraspClosed; } } } /// /// Callback on native event. /// /// /// private void OnTouchlessEvent(object sender, GestureEvent args) { //Debug.LogFormat("OnTouchlessEvent : {0}", args.skeletonValid); lock (lockObj) { locked_gestureEvent = args; } } } }