Pico4Pro Unity眼动开发实战指南(如何解决天坑)
卿不菜
编辑于 2024年05月21日 20:33

Pico眼动追踪是Pico4一代新功能,本来想着官方应该会给一些比较好用的案例来帮助开发者理解,没想到暗藏玄机、破绽百出。

结合网上两个教学来帮助其他开发者解决眼动开发的天坑。

参考文献

Unity pico4 pro开发踩坑记录(1)眼动​

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(自动校准)

二、完整代码

这是让场景中方块跟随眼球转动的代码

代码块
C#
自动换行
复制代码
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;
        }
    }
}
复制成功

详细说明

要获取眼动数据并执行交互,我们大概需要的数据有:

  • 当前设备支不支持眼动

  • 眼球的世界坐标

  • 眼球当前的朝向(基于相机旋转)

检查设备是否可以使用眼动追踪

没什么好说的,官方示例

代码块
C#
自动换行
复制代码
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企业版才可以获得单目数据,故下文用的眼动数据都是双目数据。

代码块
C#
自动换行
复制代码
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

天坑之官方写的狗屎代码

下面这一行,我们获得了双目的眼动数据,就可以来求眼球的位置和眼球看向的方向了。

代码块
C#
自动换行
复制代码
PxrPose eyeData = eyeTrackingData.eyeDatas[2].pose;
复制成功

这个PxrPose是一个结构体,如下图所示

眼动数据最终的结构体

理论上来说,position应该是眼球相对于相机的位置,orientation是眼球相对于相机的旋转(四元数),就可以开始计算目视的方向了。

但!!!Pico官方代码里把这个值填错了!!填错了!!!填错了!!!!

谢谢论坛的大哥,不然现在都不知道怎么解决

所以,正确获得眼球相较于相机的位置和眼球相较于相机的旋转是这样的(注意代码里的position和orientation)

代码块
C#
自动换行
复制代码
// 天杀的傻逼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;
复制成功

于是,眼球的世界坐标就是 相机的世界坐标 + 眼球相对相机的位置

代码块
C#
自动换行
复制代码
//相机的世界坐标
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轴,那么再最后看向的方向再乘上角色的转向方向就是当前角色转向下眼睛所看向的方向。

代码块
C#
自动换行
复制代码
Vector3 point = origin + playerTransform.rotation * rotation * Vector3.forward * rayLength;
复制成功

当然这是一条射线,所以设定了射线的长度为rayLength。

三、总结

一切的天坑源自于官方填错了position和orientation导致它无法直接映射到Unity的position和rotation,还好论坛里有大手子解决了,不然不知道要调试多久。

尽快修复吧官方。