本文旨在介绍如何使用HTC Vive Pro eye,结合Unity,读取到眼动的数据。假设文章内容面向的是此前从未接触过unity以及刚上手HTC Vive Pro eye的同学(这也是我对自己的个人定位),文章会尽量写的详尽一些,如果阅读内容已经掌握,可以选择跳过。
另外,本文仅对一些不方便搜索得知的内容进行说明,如有疑问可以自行Google或者百度。
最终成果为一个简单的Demo,可以在unity的Log栏中看到输出的信息。如下图所示,右下角的信息栏中可看到读取到的眼动数据。
另外,本文支持转载,转载时请注明作者与出处。希望大家在研究的过程中可以收获到分享与交流的快乐。
如发现错误,请通过评论或是私信的方式批评指正。
感谢b站的大佬邓布利多军,在个人空间中分享过几例测试demo,本文也是在其耐心的指导下完成的。由衷的表示感谢。
unity的官方网址
选择红框中的“Get Started”
在进入的页面中,以学生身份或是个人身份,进行unity账户的注册(此处略过)。之后运行unity客户端时,需要个人身份验证才可以正常使用。
unity 下载界面
推荐使用unity hub,即unity官方提供的管理平台进行不同版本的客户端的安装。
选择想要下载的unity版本进行下载安装即可(至于安装过程中工具包的具体选择,视个人需要,笔者是在windows平台,所以选择了与windows相关的工具包)。
以下为几点个人在过程中遇到的问题:
1、通常在安装unity各版本的客户端时,由于网络问题,“Document”经常下载失败或是下载进度为零,可以直接选择不进行下载。
2、如要使用其他人的unity项目,请注意版本的问题。通常若两个项目的unity版本不一致,会发生错误。
在新建的项目中,选择“window”,在子菜单栏中选择Package Manager
在Unity Registry中选择OpenVR Plugin,点击右下方的install即可。
有两种方式安装SteamVR。一是通过网页
unity store 搜索 steam vr 后的结果,选择红框对应的插件,选择在unity中打开即可。
第二种方法近似,即在菜单栏的windows中选择Assert Store,步骤与一近似。
下载好steamVR插件后, 需要在package manager中的My Asserts中选择进行import。
到这里,基本的工作就已经做好了。
可以在steam VR官方提供的场景中进行交互测试。
HTC Vive 官方提供了SDK
安装网址
两个SDK都需要安装,根据步骤一步一步来即可。
安装好两个SDK后,可以在安装目录中,看到如此的两个文件夹。
在SDK文件夹下存放着说明文档,C、Unity、虚幻环境的demo。
此处提供一个方便查阅SDK 中 各API的网址,也是官方提供的,在下图所示路径下。
而在SRainipal文件夹中,则存放着本次实验的关键应用。具体使用方法请阅读SDK中提供的Eye_SRanipal_SDK_Guide。
在如图所示的路径下,可以找到HTC官方提供的demo package。在unity中选择该package file,进行import。
导入后,可以在Project中看到如图所示红框对应的文件夹ViceSR,选择Scenes中的Eye,双击EyeSample_v2。
之后则可以在Hierarchy中看到如此的结构。
笔者此处在Gaze Ray Sample_v2和Avatar_Fairy_V2组件的Inspector进行了勾选。unity通过红框中的对勾即可以实现各组件的随意添加与移除。
在Gaze Ray Sample v2组件中,挂载了当前脚本。
在该脚本中,修改代码,结果为下面所示。
修改后,需要启动SRanipalRuntime,同时启动SteamVR,使得HMD正常运行。点击unity中的play,即可看到最终结果。
//========= Copyright 2018, HTC Corporation. All rights reserved. ===========
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.Assertions;
namespace ViveSR
{
namespace anipal
{
namespace Eye
{
public class SRanipal_GazeRaySample_v2 : MonoBehaviour
{
public int LengthOfRay = 25;
[SerializeField] private LineRenderer GazeRayRenderer;
private static EyeData_v2 eyeData = new EyeData_v2();
private bool eye_callback_registered = false;
//此处为增加的变量
private float pupilDiameterLeft, pupilDiameterRight;
private Vector2 pupilPositionLeft, pupilPositionRight;
private float eyeOpenLeft, eyeOpenRight;
private void Start()
{
if (!SRanipal_Eye_Framework.Instance.EnableEye)
{
enabled = false;
return;
}
Assert.IsNotNull(GazeRayRenderer);
}
private void Update()
{
if (SRanipal_Eye_Framework.Status != SRanipal_Eye_Framework.FrameworkStatus.WORKING &&
SRanipal_Eye_Framework.Status != SRanipal_Eye_Framework.FrameworkStatus.NOT_SUPPORT) return;
if (SRanipal_Eye_Framework.Instance.EnableEyeDataCallback == true && eye_callback_registered == false)
{
SRanipal_Eye_v2.WrapperRegisterEyeDataCallback(Marshal.GetFunctionPointerForDelegate((SRanipal_Eye_v2.CallbackBasic)EyeCallback));
eye_callback_registered = true;
}
else if (SRanipal_Eye_Framework.Instance.EnableEyeDataCallback == false && eye_callback_registered == true)
{
SRanipal_Eye_v2.WrapperUnRegisterEyeDataCallback(Marshal.GetFunctionPointerForDelegate((SRanipal_Eye_v2.CallbackBasic)EyeCallback));
eye_callback_registered = false;
}
Vector3 GazeOriginCombinedLocal, GazeDirectionCombinedLocal;
if (eye_callback_registered)
{
if (SRanipal_Eye_v2.GetGazeRay(GazeIndex.COMBINE, out GazeOriginCombinedLocal, out GazeDirectionCombinedLocal, eyeData)) { }
else if (SRanipal_Eye_v2.GetGazeRay(GazeIndex.LEFT, out GazeOriginCombinedLocal, out GazeDirectionCombinedLocal, eyeData)) { }
else if (SRanipal_Eye_v2.GetGazeRay(GazeIndex.RIGHT, out GazeOriginCombinedLocal, out GazeDirectionCombinedLocal, eyeData)) { }
else return;
}
else
{
if (SRanipal_Eye_v2.GetGazeRay(GazeIndex.COMBINE, out GazeOriginCombinedLocal, out GazeDirectionCombinedLocal)) { }
else if (SRanipal_Eye_v2.GetGazeRay(GazeIndex.LEFT, out GazeOriginCombinedLocal, out GazeDirectionCombinedLocal)) { }
else if (SRanipal_Eye_v2.GetGazeRay(GazeIndex.RIGHT, out GazeOriginCombinedLocal, out GazeDirectionCombinedLocal)) { }
else return;
}
Vector3 GazeDirectionCombined = Camera.main.transform.TransformDirection(GazeDirectionCombinedLocal);
GazeRayRenderer.SetPosition(0, Camera.main.transform.position - Camera.main.transform.up * 0.05f);
GazeRayRenderer.SetPosition(1, Camera.main.transform.position + GazeDirectionCombined * LengthOfRay);
//以下为新增的部分
//pupil diameter 瞳孔的直径
pupilDiameterLeft = eyeData.verbose_data.left.pupil_diameter_mm;
pupilDiameterRight = eyeData.verbose_data.right.pupil_diameter_mm;
//pupil positions 瞳孔位置
//pupil_position_in_sensor_area手册里写的是The normalized position of a pupil in [0,1],给坐标归一化了
pupilPositionLeft = eyeData.verbose_data.left.pupil_position_in_sensor_area;
pupilPositionRight = eyeData.verbose_data.right.pupil_position_in_sensor_area;
//eye open 睁眼
//eye_openness手册里写的是A value representing how open the eye is,也就是睁眼程度,从输出来看是在0-1之间,也归一化了
eyeOpenLeft = eyeData.verbose_data.left.eye_openness;
eyeOpenRight = eyeData.verbose_data.right.eye_openness;
Debug.Log("左眼瞳孔直径:" + pupilDiameterLeft + " 左眼位置坐标:" + pupilPositionLeft + "左眼睁眼程度" + eyeOpenLeft);
Debug.Log("右眼瞳孔直径:" + pupilDiameterRight + " 右眼位置坐标:" + pupilPositionRight + " 左眼睁眼程度" + eyeOpenRight);
}
private void Release()
{
if (eye_callback_registered == true)
{
SRanipal_Eye_v2.WrapperUnRegisterEyeDataCallback(Marshal.GetFunctionPointerForDelegate((SRanipal_Eye_v2.CallbackBasic)EyeCallback));
eye_callback_registered = false;
}
}
private static void EyeCallback(ref EyeData_v2 eye_data)
{
eyeData = eye_data;
}
}
}
}
}
该方法有些取巧,前文提到的“邓布利多军”是通过新建脚本实现的该功能。笔者也在进一步的尝试中。
笔者从一头雾水,到如今简单的实现了该功能,要感谢官方论坛中许多位前辈,以及官方技术人员的技术支持。同时也要感谢众多曾在网络上分享技术,发表见解的前辈们。
最后,再次感谢“邓布利多军”。希望有更多未来的开发者们,加入技术分享的大家庭中。