openvr_survivor第一期开发活动:复位与定位追踪

开发简介

  • 开发主题:VR复位和定位追踪.
  • 参与人员:helen,ice,bikasuo.(VR开发者QQ群:538874606)
  • 项目代码
  • gitter讨论链接
    分享一下我们对于这方面问题的一些看法,文档可能存在一些错误和不足,希望能够得到大家的批评和建议,希望与大家一起将这部分内容做的更加完善,持续更新.可以随意复制使用,方便的话注明一下出处,不胜感谢!

复位

  • 复位介绍
    玩VR游戏的人经常能发现这样的现象,你坐在一个固定的位置上,开始游戏时,你可能是正对着游戏的正前方:
    openvr_survivor第一期开发活动:复位与定位追踪_第1张图片
    激情几分钟后,在回到这个画面时,面对相同的方向,实际显示的画面已经偏离了原来的方向,例如:
    openvr_survivor第一期开发活动:复位与定位追踪_第2张图片
    这是一个典型的陀螺仪漂移的情况,为了消除这异常情况,需要进行一次复位操作,使面面重新回到最初的方向上;或则当你在玩VR游戏时,移动头部调整到画面为正方向上时,你面对的方向是北,但是你想朝着方向为南的地方玩,这时需要在南方向上触发一个复位,将画面正方向”拉”过来;或则添加一些特殊功能,例如某些定位方案只有180度范围的定位,扩展定位方案从180—>360度,需要做”一键转身的功能”.目前市面上的各种VR设备都会提供复位的操作. 那么我们就来聊聊VR的复位功能.
    复位包括,姿态(rotation)/位置(position)的复位.
    姿态的复位:将当前姿态设置为新的原点,这个原点是指在应用的正面方向(多数情况为Yaw为0度的方向,这里暂时也只讨论Yaw为0度的正方向情况,openvr_survivor支持任意Yaw角度复位),**姿态的复位目前多数平台都只对Yaw方向执行复位操作,Pitch和Roll参与复位操作,因为磁力计能确保Yaw方向正确,而没有其他传感器能作为Pitch和Roll的准确位置参考,复位流程如下:
    openvr_survivor第一期开发活动:复位与定位追踪_第3张图片
    位置的复位:将当前位置设置为新的原点,这个原点是指位置为(0,0,0)的位置,这个比较好理解,在steamVR里是地上绿色圈圈的中心.目前位置定位的方案大多都是绝对坐标,没有累计漂移的问题,一般不需要复位位置.
  • 如何实现复位功能
    实现复位的操作,可以在平台层\应用层\硬件驱动层做这个操作,不论在哪层实现,实现起来的方法都一样,都是对被追踪物体的四元素进行处理.这里,关于复位功能的讨论,个人认为是在平台层实现复位的方法,然后向硬件驱动层和应用层分别提供接口来统一管理比较好,但steamVR目前的情况是没有统一,比较混乱另外复位的算法在几大平台都只提供接口,而不开放源代码,后来自己把复位功能实现,觉得这些并不复杂(花点时间大家基本都能实现),VR目前这行业处于技术爬坡,并且不挣钱的时候,没必要把这些功能封闭起来,浪费大部分开发者的时间,所以创建开源openvr_survivor项目,把一些VR基础技术实现,并分享,和更多的开发者一起推动VR的发展,额,想的有点多~.回归正题,复位功能实现的思路,之前在openvr的issue里讨论过.
    按下复位按键或APP里的复位按钮(在触发前最好保持静止状态,这样yaw的角度会比较稳定),第一步先获取当前yaw角度值:

    然后在每次上报物体四元素之前,对yaw做偏移:

    对应的主要代码:
//get yaw offset
m_dRecenterYawOffset = simple_math::GetYawDegree(m_OriginRotation)

//DoOrientationRecenter
inline vr::HmdQuaternion_t DoOrientationRecenter(const vr::HmdQuaternion_t quaternion_origin,const double yaw_offset){
       double yaw_degree_new,yaw_degree_origin;
       vr::HmdVector3d_t degree;
       vr::HmdQuaternion_t quaternion_dest;
       //get origin yaw from quaternion
       degree = simple_math::QuaternionToEulerDegree(quaternion_origin);
       yaw_degree_origin = degree.v[0];
       LOG_EVERY_N(INFO,5 * 60) << "DoOrientationRecenter[0]:quat(" << quaternion_origin.w << "," << quaternion_origin.x << ","
               << quaternion_origin.y << "," << quaternion_origin.z << "),degree(" << degree.v[0] << "," << degree.v[1] << ","
               << degree.v[2] << "),yaw_offset=" << yaw_offset;

       //recenter yaw ,get yaw_degree_new
       yaw_degree_new = yaw_degree_origin - yaw_offset;
       yaw_degree_origin = degree.v[0];
       LOG_EVERY_N(INFO,5 * 60) << "DoOrientationRecenter[0]:quat(" << quaternion_origin.w << "," << quaternion_origin.x << ","
               << quaternion_origin.y << "," << quaternion_origin.z << "),degree(" << degree.v[0] << "," << degree.v[1] << ","
               << degree.v[2] << "),yaw_offset=" << yaw_offset;

       //recenter yaw ,get yaw_degree_new
       yaw_degree_new = yaw_degree_origin - yaw_offset;
       if(yaw_degree_new > 180){
               yaw_degree_new= -360 + yaw_degree_new;
       }else if(yaw_degree_new < -180){
               yaw_degree_new = 360 + yaw_degree_new;
       }
       //transform degree to quaternion
       degree.v[0] = yaw_degree_new;
       quaternion_dest = simple_math::DegreeEulerToQuaternion(degree);

       LOG_EVERY_N(INFO,5 * 60) << "DoOrientationRecenter[1]:quat(" << quaternion_dest.w << "," << quaternion_dest.x << ","
               << quaternion_dest.y << "," << quaternion_dest.z << "),degree(" << degree.v[0] << "," << degree.v[1] << ","
               << degree.v[2] << "),yaw_offset=" << yaw_offset;
       return quaternion_dest;
}

详细见openvr_survivor的提交:

commit 72ec5ee7a0d22aceb79991a185763982bc09aed6
Author: HelenXR .com>
Date:   Fri Jul 14 15:57:16 2017 +0800

实际验证可行,这个复位方法比较容易理解,当然这个复位转换应该有一个更快的操作方法:

quaternion(new) = quaternion(ori) * Rotation matrix.

后续有时间会用GLM库来实现这个转换.

定位追踪

之前学习整理过一个关于定位追踪的资料,详见VR定位追踪,这里就直接进入主题.我们主要是选择了国内目前比较成熟的定位追踪方案,一个是NOLO,另外一个是Ximmerse.两种方案技术路径不同,NOLO是激光+声波,Ximmerse是类似PSVR的可见光定位,它们提供的是相同的功能:6DOF的头部追踪和6DOF的手柄.有了定位追踪,带来了更好的VR体验.

NOLO

NOLO提供的是一种激光定位+声波定位的方案,类似HTC VIVE,它包含一个基站,一个头盔定位器(6DOF),两个手柄控制器(6DOF).激光定位原理(激光定位原理,之前翻译过Hypereal开源的激光定位文档,想了解的,点击这里)上单基站相对于双基站的情况,定位精度会低一些(双基站可以达到0.5mm),单基站能达到2mm的精度,在精度上问题不大,最大的缺点是不能实现360度追踪,只能达到180度,还是蛮遗憾的(文章最后提供一个单基站扩展为360的方案”一键转身”功能).
NOLO定位参数:
1. 定位基站
尺寸:81*41*71mm
功耗:400 ma(2000mAh)
续航:4小时
充电:USB充电(1.5小时)
支持USB数据传输
2. 头盔定位器
尺寸:105*30*53mm
功耗:100 ma
续航:手机供电
支持USB数据传输
3. 交互手柄
尺寸:146*40*40mm
功耗:120 ma(1000mAh)
续航:USB充电(1.5小时)
支持USB数据传输
支持滑动触摸和点触摸
支持可调节震动
4. 定位参数
定位范围:FOV100°5.3m(以基站为原点)
定位精度:<2mm
定位刷新率:60hz
定位延时: <20ms

基于NOLO SDK开发,对应修改代码,见如下提交:

commit ba2a6c68e88480b60ce244894f6ba25dec541118
Author: HelenXR 
Date:   Fri Aug 25 18:42:29 2017 +0800
    add six dof module:nolo.

Ximmerse

Ximmerse提供的是一种类似PSVR的可见光追踪的方案.它包含一个双目摄像头,一个头盔定位器(6DOF),两个手柄控制器(6DOF).定位精度2mm,缺点也是只有180度范围定位,相比NOLO刷新率高一些(ximmerse:90,NOLO:60),使用起来更加流畅一些,但定位距离和范围没有NOLO的大,同样存在只有180度追踪范围的尴尬情况.
Ximmerse定位参数:
openvr_survivor第一期开发活动:复位与定位追踪_第4张图片
基于Ximmerse SDK开发,对应修改代码,见如下提交:

commit 3416a43d25e48b2acdf0120cb8d530e656606be0
Author: HelenXR 
Date:   Mon Aug 21 16:25:04 2017 +0800
    add six dof module:ximmerse.

3个功能代码说明

在处理两种定位方案过程中,基于SDK开发还是比较顺利的,这里有2个有点意思的数学问题

  • TouchPad区域识别
    在定位方案SDK中都可以通过接口获取到TouchPad的x,y坐标,TouchPad的是一个圆盘(半径为1.0),如下图所示:
    openvr_survivor第一期开发活动:复位与定位追踪_第5张图片

    分为up,down,left,right区域,每次手触摸在触摸板上时,都可以读取到一组x,y的值,如何通过x,y判断按下的区域是哪一个?
    区域”UP”:黑色直线(y=x)左侧与蓝色直线(y=-x)右侧,以及半径为1的圆相交的区域.
    黑色直线左侧:y>x
    蓝色直线右侧:y>-x
    xy在圆之内:因触摸板上报的x,y坐标值一定会在圆盘之内,不需要处理.
    up区域:-y < x < y
    同理可以得出其他三个区域:
    down区域:y < x < -y
    left区域:x < y < -x
    right区域:-x < y < x
    对应代码:

vr::EVRButtonId CHandControllerDevice::GetDPadButton(float float_x,float float_y){
    if(float_x > 1.0 || float_x < -1.0
    ||float_y > 1.0 || float_y < -1.0){
        LOG(WARNING) << "GetDPadButton[" << m_cControllerRole << "]: error postion(" << float_x << "," << float_y << ")";
        return vr::k_EButton_Max;
    }

    int x = float_x * 10000.0f,y = float_y * 10000.0f;
    //UP:-y
    if(x > -y && x < y){
        LOG_EVERY_N(INFO,1 * 90) << "GetDPadButton[" << m_cControllerRole << "]:k_EButton_DPad_Up";
        return vr::k_EButton_DPad_Up;
    }
    //DOWN:y
    if(x > y && x < -y){
        LOG_EVERY_N(INFO,1 * 90) << "GetDPadButton[" << m_cControllerRole << "]:k_EButton_DPad_Down";
        return vr::k_EButton_DPad_Down;
    }
    //LEFT:x
    if(y > x && y < -x){
        LOG_EVERY_N(INFO,1 * 90) << "GetDPadButton[" << m_cControllerRole << "]:k_EButton_DPad_Left";
        return vr::k_EButton_DPad_Left;
    }
    //RIGHT:-x
    if(y > -x && y < x){
        LOG_EVERY_N(INFO,1 * 90) << "GetDPadButton[" << m_cControllerRole << "]:k_EButton_DPad_Right";
        return vr::k_EButton_DPad_Right;
    }

    LOG_EVERY_N(INFO,1 * 90) << "GetDPadButton[" << m_cControllerRole << "]:unknown region(" << x << "," << y << "),float(" << float_x << "," << "" << float_y <<")";
    return vr::k_EButton_Max;
}
  • 一键转身
    因为NOLO和Ximmerse的定位方案都只有180度,当你面对基站/双目摄像头时,可以操作前方180度的区域,无法操作到背后的区域,一键转身,实现对背后区域的操作,项目里以双击menu/back按键来实现一键转身.一键转身时需要考虑头部信息(6DOF)和手柄信息(6DOF)的转换.
    头部位置信息包括姿态(rotation)和位置(position),当一键转身后,头部的姿态需要旋转180度,位置点绕自身旋转180度.
    头部姿态旋转按公式q(dest) = q(rotate) * q(ori) 即可.
    头部位置旋转麻烦一些:头部位置点,看做一个三维矢量,然后将矢量移到原点,之后进行旋转180度操作,旋转后的矢量移回最初的位置.
    手柄信息的处理与头部信息处理基本一致,要注意,手柄位置信息需要绕头部信息进行旋转操作.
    代码如下:
            vr::HmdQuaternion_t quaternion_rotate = HmdQuaternion_Init( 0, 0, 1, 0 );
            vr::HmdQuaternion_t quaternion_origin = HmdQuaternion_Init( m_Pose.qRotation.w,m_Pose.qRotation.x,m_Pose.qRotation.y,m_Pose.qRotation.z);
            //rotate rotation
            m_Pose.qRotation = glm_adapter::QuaternionMultiplyQuaternion(quaternion_rotate,m_Pose.qRotation);
            //rotate position                                                                                   
glm_adapter::PointAroundPointRotate(quaternion_rotate,m_Pose.vecPosition,m_dHmdPositionWhenTurnAround,m_Pose.vecPosition);

//其中PointAroundPointRotate代码如下:
    void PointAroundPointRotate(const vr::HmdQuaternion_t quaternion_rotate, const double point_origin[3], const double point_center[3], double point_dest[3]) {
        glm::tquat tquat_rotate(quaternion_rotate.w, quaternion_rotate.x, quaternion_rotate.y, quaternion_rotate.z);
        glm::tvec3 tvec3_origin(point_origin[0], point_origin[1], point_origin[2]), tvec3_center(point_center[0], point_center[1], point_center[2]),tvec3_dest;
        tvec3_dest = tquat_rotate * (tvec3_origin - tvec3_center) + tvec3_center;
        point_dest[0] = tvec3_dest.x;
        point_dest[1] = tvec3_dest.y;
        point_dest[2] = tvec3_dest.z;
    }    
  • 360度方案扩展
    因为NOLO和Ximmerse的定位方案都只有180度,当你面对基站/双目摄像头时,可以操作前方180度的区域,无法操作到背后的区域,虽然通过一键转身功能可以将视角强制转到后面,但这样带来的用户体验和沉静感都或多或少
    会受到影响,而如果把NOLO或Ximmerse的基站/双目摄像头放在头顶上,那么360度的操作将变得可行。因为考虑到基站/双目摄像头的最佳识别范围是在1.5米-2.5米,或2米-4.5米,那么考虑到我们手离地的位置为0.7米
    那么0.7+2米是目前最佳的放置高度,然后将原先的-z轴变为现在的y轴,原先的y轴变为现在的z轴。
    具体的代码如下:(考虑到用户是否需要用这个功能,配置中可以修改m_bTopCamera开启,m_bCameraHeight放置高度)
    if (m_bTopCamera) {
    glm::vec4 vecPosition = glm::vec4((double)hmdPos[0], (double)-hmdPos[2], (double)hmdPos[1], 1.0f);
    glm::mat4 rts = glm::mat4(1.0);
    rts = glm::translate(rts, glm::vec3(0, m_bCameraHeight, 0));
    vecPosition = rts * vecPosition;
    m_Pose.vecPosition[0] = vecPosition.x;
    m_Pose.vecPosition[1] = vecPosition.y;
    m_Pose.vecPosition[2] = vecPosition.z;
    }
    对应提交:
commit 7bf5f86d408c871fdb1936edce1d0000747180b6

最后

感谢ice,bikasuo,我们一起用空闲时间完成开发活动,这是一个开始,我们希望更多的人可以加入到我们当中来,VR还处于很初期的阶段,我们通过努力,也许可以给VR的发展带来一些好的东西,分享的过程也是一个很好的互相学习的过程,openvr_survivor下一期活动再会:-).

参考资料:

  1. OSVR reset yaw
  2. libovr
  3. openvr_issues_1
  4. openvr_issues_2
  5. reset_camera
  6. openvr_advanced_settings
  7. nolo_windows_sdk

你可能感兴趣的:(我的开源项目)