前言
最近在做一个工作需要用到PICO4 Enterprise VR头盔里的眼动追踪功能,但是遇到了如下问题:
- 在Unity里面没法串流调试眼动追踪功能,根本获取不到Device,只能将整个场景build成APK,安装到头盔里,才能在代码里调用眼动追踪功能。这使得每次修改代码都要打包一次apk安装到头盔里,十分不方便,难以调试。
- PICO VR 官方提供的Eye Tracking教程里,获取到的眼睛朝向和位置是相对于Head这个位置的,而不是相对于XR Origin下的Camera的位置,这使得API不能直接拿来使用。
- Unity在引入PICO VR眼动跟踪代码后,编译时点击"build and run"后,会报错“
NullReferenceException: Object reference not set to an instance of an object
”
鉴于以上问题网络上均没有人进行解答,以及个人没能得到PICO官方的技术支持情况下(三周内发了2封技术工单邮件+2次催线上客服),遂打算自己捣鼓一下写篇教程。
(点名批评PICO官方的技术支持不回复邮件的问题,明明就几个特别简单的问题,但是官方承诺的3-5个工作日内回复并没有做到,等了三周一封邮件也没回,这个过程还问了客服,客服表示会催技术人员回复,但等了一周半也没看到,放弃了orz)
更新补充:据说在PICO企业版官网提交企业版工单能得到官方较快的回复。
1. 如何在代码里使用眼动跟踪,并转换成世界坐标 或 摄像机坐标系
首先,检查眼动追踪是否能正常工作:
void Start()
{
CheckEyeTracking();
}
private void CheckEyeTracking()
{
//Start PICO Eye Tracking Service
TrackingStateCode trackingState;
trackingState = (TrackingStateCode)PXR_MotionTracking.WantEyeTrackingService();
Debug.Log("告知PICO想要眼动跟踪服务 trackingState: " + trackingState.ToString());
//Check Eye Tracking able or not
EyeTrackingMode eyeTrackingMode = EyeTrackingMode.PXR_ETM_NONE;
bool supported = false;
int supportedModesCount = 0;
trackingState = (TrackingStateCode)PXR_MotionTracking.GetEyeTrackingSupported(ref supported, ref supportedModesCount, ref eyeTrackingMode);
Debug.Log("检查是否有眼动跟踪功能 trackingState: "+ trackingState.ToString()+" code "+ supported);
// Get Eye Tracking State
bool tracking = true;
EyeTrackingState eyeTrackingState = new EyeTrackingState();
trackingState = (TrackingStateCode)PXR_MotionTracking.GetEyeTrackingState(ref tracking, ref eyeTrackingState);
Debug.Log("获取当前眼动跟踪状态 trackingState: " + trackingState.ToString());
// Start Eye Tracking
//这里要严谨点的话,应该要加上if条件判断是否有眼动跟踪功能,再选择开启该功能
EyeTrackingStartInfo info = new EyeTrackingStartInfo();
info.needCalibration = 1;
info.mode = eyeTrackingMode;
trackingState = (TrackingStateCode)PXR_MotionTracking.StartEyeTracking(ref info);
Debug.Log("开始眼动跟踪状态 trackingState: " + trackingState.ToString());
//Get Eye Tracking Data
//获取眼动跟踪数据
EyeTrackingDataGetInfo eyeData = new EyeTrackingDataGetInfo();
eyeData.displayTime = 0;
eyeData.flags = EyeTrackingDataGetFlags.PXR_EYE_DEFAULT
| EyeTrackingDataGetFlags.PXR_EYE_POSITION
| EyeTrackingDataGetFlags.PXR_EYE_ORIENTATION;
EyeTrackingData eyeTrackingData = new EyeTrackingData();
trackingState = (TrackingStateCode)PXR_MotionTracking.GetEyeTrackingData(ref eyeData, ref eyeTrackingData);
Debug.Log("eyeData: "+ eyeData.ToString() + " TrackingData: " + eyeTrackingData.ToString());
}
接下来,每帧都获取眼动数据,并将眼睛位置和眼睛朝向数据转换成世界坐标
public Transform origin;//在Unity主界面把XR Origin拖进来即可
private Matrix4x4 headPoseMatrix,originPoseMatrix;
private Vector3 combineEyeGazeVector, combineEyeGazeOriginOffset, combineEyeGazeOrigin;
private Vector3 combineEyeGazeVectorInWorldSpace, combineEyeGazeOriginInWorldSpace;
void Update()
{
GetEyeTrackingData();
}
private void GetEyeTrackingData()
{
//获取head的局部转世界矩阵,该矩阵点乘head局部坐标系下坐标,则能转换为世界坐标系下的坐标
PXR_EyeTracking.GetHeadPosMatrix(out headPoseMatrix);
//获取双眼(取中间位置)位于Head坐标系下的朝向信息GazeVector
PXR_EyeTracking.GetCombineEyeGazeVector(out combineEyeGazeVector);
//获取双眼(取中间位置)位于Head坐标系下的位置信息GazePoint
PXR_EyeTracking.GetCombineEyeGazePoint(out combineEyeGazeOrigin);
//获取眼睛的世界坐标
combineEyeGazeOriginInWorldSpace = originPoseMatrix.MultiplyPoint(headPoseMatrix.MultiplyPoint(combineEyeGazeOrigin));
//获取眼睛的朝向信息
combineEyeGazeVectorInWorldSpace = originPoseMatrix.MultiplyVector(headPoseMatrix.MultiplyVector(combineEyeGazeVector));
//highlighArea是我添加的一个手电筒高亮区域,能让用户更直观地查看眼睛看向位置
highlighArea.transform.position = combineEyeGazeOriginInWorldSpace ;
//LookRotation默认是以z轴旋转,如果要以y轴旋转的话可以在后面加上 ,Vector3.up
highlighArea.transform.rotation = Quaternion.LookRotation(combineEyeGazeVectorInWorldSpace);
/* RaycastHit hit;
if (Physics.Raycast(highlighArea.transform.position, combineEyeGazeVectorInWorldSpace, out hit, 1000f))
{
highlighArea.transform.LookAt(hit.point);
}*/
}
简易效果图(透明圈处是一个作为highlighArea的圆锥区域):
至此,便已经能获取到PICO VR里眼动追踪的位置和朝向信息。
2. "Build and Run"报错
引入EyeTracking代码后,发现Build and Run时会报以下错误:
这是因为run的话会导致Unity找不到可供眼动追踪的设备,导致编译报错。
解决方法,只点击Build,而不是Build and Run:
Build完后,打开Pico Developer Center,依次点击"应用管理"->“安装包”->“安装本地应用”:
选择build好的apk即可,后续便能直接在眼镜里运行:
3.眼镜内调试的Trick
对于这种要build到眼镜里才能调试的功能,可以利用Unity提高的UI->Text组件进行调试。
即,在Unity场景下创建一个UI->Text对象,然后把对象拖到下面脚本的GazeDebugText变量上,便能在眼镜里看到相应数据输出了
public TMP_Text GazeDebugText;
private void GetEyeTrackingData()
{
GazeDebugText.text = combineEyeGazeOrigin.ToString("F3");//F3是指精确到小数点后3位
}
效果:
4.如何在Unity里串流调用眼动追踪功能
B站视频:使用PICO头显在VR串流环境下进行眼动追踪
我个人是在看了这个视频后才知道PICO VR也能在Unity下串流使用眼动跟踪功能,但我没有进一步深入探究,据该视频UP主表示,PICO neo3 pro eye是可以串流的,该方法理应也适用于其他头盔(带有企业串流功能的头盔)。
在此处也表达下对UP主的感谢,在后台私信耐心细致替我解答问题。
想要串流使用pico的眼动追踪功能,需要下载特定的串流软件和PICO系统版本。
4.1 串流工具准备
PC端我们要下载最新版本的PICO企业版串流工具(普通版的串流工具不知道可不可以):
在PICO端上则是自动保持串流软件更新到最新版本即可。
在PC端下载好“企业串流”软件后,点击"设置"->“通用”:
把需要的功能都勾选上(默认是关闭状态):
business streaming安装后且打开眼动追踪功能后,sdk里会有一个用qt写的测试程序,可以试试看串流后眼动跟踪是否正常,测试程序不需要用steam,但是对头显的固件版本有要求:
点击get eye tracking(记住,一定要戴上头盔的情况下,盲点这个按钮,不如眼睛都不在头盔里面自然捕获不到数据),正常的话会显示如下数据:
如果这里显示ET Data 是 invalid的,那证明你没有戴上头盔后再点击get eye tracking,因为头盔没能捕捉到瞳孔数据,记住一定要先戴上头盔再盲点击按钮。
4.2 企业串流+OpenVR+SteamVR进行Unity开发
众所周知,任意VR头盔都能用SteamVR+OpenVR来开发VR应用,我们的PICO眼镜也可以,这里的“企业串流”只起到了传输EyeTrackingData的作用。我们如果要用到眼动追踪串流调试的话,就需要以OpenXR+SteamVR的方式进行开发,详细教程可以参考:知乎-Pico基于Unity XR Interaction Toolkit开发SteamVR串流应用 如果能正常串流到Unity界面,则我们进入下一步代码开发。
4.3 代码部分
接下来便是关键的部分,我们要在代码里引入企业版的PICO SDK并初始化(在你要用到Eye tracking代码最开始的地方引入即可,):
//引入 企业版串流SDK
private const string PXR_PLATFORM_DLL = "BStreamingSDK.dll";
//对 企业版串流SDK 进行初始化
[DllImport(PXR_PLATFORM_DLL, CallingConvention = CallingConvention.Cdecl)]
private static extern int BStreamingSDK_Init(IntPtr userData);
[DllImport(PXR_PLATFORM_DLL, CallingConvention = CallingConvention.Cdecl)]
public static extern int BStreamingSDK_Deinit();
此处的SDK跟我们平常开发PICO用的PXR SDK不一样,是为串流功能专门要用的SDK,该SDK会随business streaming一起安装,不需要你特定指明路径。而且当我们将项目build成apk安装包之后,调用的SDK是正常使用的,不用担心冲突问题等。
接下来便是获取眼部追踪数据/手势追踪数据即可:
//眼动追踪
[DllImport(PXR_PLATFORM_DLL, CallingConvention = CallingConvention.Cdecl)]
private static extern int BStreamingSDK_GetEyeTrackingData(ref PxrEyeTrackingData etdata);
//手势追踪
[DllImport(PXR_PLATFORM_DLL, CallingConvention = CallingConvention.Cdecl)]
private static extern int BStreamingSDK_GetHandTrackerAimState(HandType hand, ref HandAimState aimState);
[DllImport(PXR_PLATFORM_DLL, CallingConvention = CallingConvention.Cdecl)]
private static extern int BStreamingSDK_GetHandTrackerJointLocations(HandType hand, ref HandJointLocations jointLocations);
此外,注意这里的PxrEyeTrackingData
并不是SDK自带的类,而是需要我们自己在代码里创建的类,然后将其传入官方提供的函数来获取数据(手势追踪的Hand AimState等也是如此要自己创建类,详细部分可以向官方发送企业工单获取技术人员帮助),该类的定义如下所示:
public struct PxrEyeTrackingData
{
public int leftEyePoseStatus;
public int rightEyePoseStatus;
public int combinedEyePoseStatus;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
public float[] leftEyeGazePoint;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
public float[] rightEyeGazePoint;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
public float[] combinedEyeGazePoint;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
public float[] leftEyeGazeVector;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
public float[] rightEyeGazeVector;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
public float[] combinedEyeGazeVector;
public float leftEyeOpenness;
public float rightEyeOpenness;
public float leftEyePupilDilation;
public float rightEyePupilDilation;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
public float[] leftEyePositionGuide;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
public float[] rightEvePositionGuide;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
public float[] foveatedGazeDirection;
public int foveatedGazeTrackingState;
}
后续要注意到的点是,串流SDK里 CombinedEyeGazeVector Z轴是要取反的,与PICO Integration SDK不同:
PxrEyeTrackingData etData = new PxrEyeTrackingData();
var result = BStreamingSDK_GetEyeTrackingData(ref etData);
headPoseMatrix = Matrix4x4.TRS(userView.transform.position, userView.transform.rotation, Vector3.one);
var eyePoseStatus = etData.combinedEyePoseStatus;
var gazePosition = new Vector3(etData.combinedEyeGazePoint[0], etData.combinedEyeGazePoint[1], etData.combinedEyeGazePoint[2]);
var gazeVector = new Vector3(etData.combinedEyeGazeVector[0], etData.combinedEyeGazeVector[1], -etData.combinedEyeGazeVector[2]);
combineEyeGazeOrigin = gazePosition;
combineEyeGazeVector = gazeVector;
combineEyeGazeOriginInWorldSpace = headPoseMatrix.MultiplyPoint(combineEyeGazeOrigin);
combineEyeGazeVectorInWorldSpace = headPoseMatrix.MultiplyVector(combineEyeGazeVector);
highlightArea.position = combineEyeGazeOriginInWorldSpace;
highlightArea.rotation = Quaternion.LookRotation(combineEyeGazeVectorInWorldSpace);
如果经过测试后,eye tracking data什么的没有问题,后续在Unity串流过程中使用Eye Tracking Dat并基于OpenXR进行应用调试与开发。