Pico眼动追踪是Pico4一代新功能,本来想着官方应该会给一些比较好用的案例来帮助开发者理解,没想到暗藏玄机、破绽百出。
结合网上两个教学来帮助其他开发者解决眼动开发的天坑。
参考文献
https://zhuanlan.zhihu.com/p/678956811
https://developer-cn.picoxr.com/document/unity/eye-tracking/
Unity 2022.3.17f1c1
Pico SDK 2.5.3
只需要导入PXR_Manager就行,勾选Eye Tracking和Eye Tracking Calibration(自动校准)

这是让场景中方块跟随眼球转动的代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.XR.PXR;
public class EyeTrackingManager : MonoBehaviour
{
private bool trackingSupported = false;
private bool isTracking = false;
private TrackingStateCode trackingState;
private EyeTrackingData eyeTrackingData;
private Transform cameraOffset;
private Transform playerTransform;
private const float rayLength = 14f;
public GameObject Cube;
private void CheckEyeTrackingCondition()
{
trackingState = (TrackingStateCode)PXR_MotionTracking.WantEyeTrackingService();
if (trackingState == TrackingStateCode.PXR_MT_SUCCESS)
{
int supportedModesCount = 0;
EyeTrackingMode eyeTrackingMode = EyeTrackingMode.PXR_ETM_NONE;
trackingState = (TrackingStateCode)PXR_MotionTracking.GetEyeTrackingSupported(ref trackingSupported, ref supportedModesCount, ref eyeTrackingMode);
if (trackingSupported)
{
EyeTrackingStartInfo info = new EyeTrackingStartInfo();
info.needCalibration = 1;
info.mode = EyeTrackingMode.PXR_ETM_BOTH;
}
else
{
Debug.LogError("Eye tracking is not supported on this device.");
}
}
else
{
Debug.LogError("Fail to launch eye track function");
}
}
// Start is called before the first frame update
private void Start()
{
CheckEyeTrackingCondition();
cameraOffset = GameObject.FindGameObjectWithTag("XRCamera").transform;
playerTransform = GameObject.FindGameObjectWithTag("XROrigin").transform;
}
// Update is called once per frame
private void Update()
{
if (!trackingSupported)
{
return;
}
EyeTrackingState eyeTrackingState = new EyeTrackingState();
trackingState = (TrackingStateCode)PXR_MotionTracking.GetEyeTrackingState(ref isTracking, ref eyeTrackingState);
if (isTracking)
{
EyeTrackingDataGetInfo getInfo = new EyeTrackingDataGetInfo();
getInfo.displayTime = 0;
getInfo.flags = EyeTrackingDataGetFlags.PXR_EYE_DEFAULT | EyeTrackingDataGetFlags.PXR_EYE_POSITION | EyeTrackingDataGetFlags.PXR_EYE_ORIENTATION;
trackingState = (TrackingStateCode)PXR_MotionTracking.GetEyeTrackingData(ref getInfo, ref eyeTrackingData);
// EyeTrackingData中存放Pico运行时的眼动数据。
// eyeDatas[0],eyeDatas[1]分别是左眼和右眼单独的数据,只有企业版能够读取,故数据为空。
// eyeDatas[2]则为两者混合后的数据
//相机的世界坐标
Transform root = cameraOffset.transform;
PxrPose eyeData = eyeTrackingData.eyeDatas[2].pose;
// Pico官方填错了眼动数据
// 眼球相较于相机的位置
Vector3 position = new Vector3(eyeData.orientation.y, eyeData.orientation.z, -eyeData.orientation.w);
// 眼球相较于相机的旋转
Quaternion rotation = new Quaternion(-eyeData.position.x, -eyeData.position.y, eyeData.position.z, eyeData.orientation.x);
// 眼球的世界坐标
var origin = root.position + position;
Vector3 point = origin + playerTransform.rotation * rotation * Vector3.forward * rayLength;
Cube.transform.position = point;
}
}
} 要获取眼动数据并执行交互,我们大概需要的数据有:
当前设备支不支持眼动
眼球的世界坐标
眼球当前的朝向(基于相机旋转)
没什么好说的,官方示例
private void CheckEyeTrackingCondition()
{
trackingState = (TrackingStateCode)PXR_MotionTracking.WantEyeTrackingService();
if (trackingState == TrackingStateCode.PXR_MT_SUCCESS)
{
int supportedModesCount = 0;
EyeTrackingMode eyeTrackingMode = EyeTrackingMode.PXR_ETM_NONE;
trackingState = (TrackingStateCode)PXR_MotionTracking.GetEyeTrackingSupported(ref trackingSupported, ref supportedModesCount, ref eyeTrackingMode);
if (trackingSupported)
{
EyeTrackingStartInfo info = new EyeTrackingStartInfo();
info.needCalibration = 1;
info.mode = EyeTrackingMode.PXR_ETM_BOTH;
}
else
{
Debug.LogError("Eye tracking is not supported on this device.");
}
}
else
{
Debug.LogError("Fail to launch eye track function");
}
} 这里是通过传入了一个EyeTrackingData的实例到PXR_MotionTracking.GetEyeTrackingData这个函数里获取的眼动数据。具体的数据存在eyeTrackingData.eyeDatas这个数组里。
这个eyeDatas数组,长度为3,其中eyeDatas[0]存的是左目数据,eyeDatas[1]存的是右目数据,eyeDatas[2]存的是双目数据。而目前只有Pico4企业版才可以获得单目数据,故下文用的眼动数据都是双目数据。
EyeTrackingDataGetInfo getInfo = new EyeTrackingDataGetInfo();
getInfo.displayTime = 0;
getInfo.flags = EyeTrackingDataGetFlags.PXR_EYE_DEFAULT | EyeTrackingDataGetFlags.PXR_EYE_POSITION | EyeTrackingDataGetFlags.PXR_EYE_ORIENTATION;
trackingState = (TrackingStateCode)PXR_MotionTracking.GetEyeTrackingData(ref getInfo, ref eyeTrackingData);

官方GetEyeTrackingData的API
下面这一行,我们获得了双目的眼动数据,就可以来求眼球的位置和眼球看向的方向了。
PxrPose eyeData = eyeTrackingData.eyeDatas[2].pose; 这个PxrPose是一个结构体,如下图所示

眼动数据最终的结构体
理论上来说,position应该是眼球相对于相机的位置,orientation是眼球相对于相机的旋转(四元数),就可以开始计算目视的方向了。
但!!!Pico官方代码里把这个值填错了!!填错了!!!填错了!!!!

谢谢论坛的大哥,不然现在都不知道怎么解决
所以,正确获得眼球相较于相机的位置和眼球相较于相机的旋转是这样的(注意代码里的position和orientation)
// 天杀的傻逼Pico官方填错了眼动数据
// 眼球相较于相机的位置
Vector3 position = new Vector3(eyeData.orientation.y, eyeData.orientation.z, -eyeData.orientation.w);
// 眼球相较于相机的旋转
Quaternion rotation = new Quaternion(-eyeData.position.x, -eyeData.position.y, eyeData.position.z, eyeData.orientation.x);
// 眼球的世界坐标
var origin = root.position + position; 于是,眼球的世界坐标就是 相机的世界坐标 + 眼球相对相机的位置
//相机的世界坐标
Transform root = cameraOffset.transform;
PxrPose eyeData = eyeTrackingData.eyeDatas[2].pose;
// 天杀的傻逼Pico官方填错了眼动数据
// 眼球相较于相机的位置
Vector3 position = new Vector3(eyeData.orientation.y, eyeData.orientation.z, -eyeData.orientation.w);
// 眼球相较于相机的旋转
Quaternion rotation = new Quaternion(-eyeData.position.x, -eyeData.position.y, eyeData.position.z, eyeData.orientation.x);
// 眼球的世界坐标
var origin = root.position + position; 最后由于角色不可避免地会在世界中旋转,如果角色默认朝向为Z轴,那么再最后看向的方向再乘上角色的转向方向就是当前角色转向下眼睛所看向的方向。
Vector3 point = origin + playerTransform.rotation * rotation * Vector3.forward * rayLength; 当然这是一条射线,所以设定了射线的长度为rayLength。
一切的天坑源自于官方填错了position和orientation导致它无法直接映射到Unity的position和rotation,还好论坛里有大手子解决了,不然不知道要调试多久。
尽快修复吧官方。