HTC Vive开发学习——VRTK(1)

本系列是基于Unity和HTC Vive开发虚拟现实应用的学习笔记。Unity环境下Vive开发必用的插件是SteamVR Plugin,除此之外,计划参考一些开源项目和免费的Asset Store插件,暂时涉及的包括:VRTK和Hover UI。

VRTK包括基于SteamVR Plugin实现的各种组件,除了提供一个Vive开发的辅助工具,更重要的是,它演示了如何使用SteamVR Plugin在Unity环境下开发Vive应用,具有很高的学习价值。我们的学习过程首先从VRTK的Demo开始。

(一)002_Controller_Events

该Demo提供了一个与Vive Controller(以下简称:手柄)交互的基础脚本VRTK_ControllerEvents,以及一个演示脚本(VRTK_ControllerEvents_ListenerExample)。演示了如何编写与控制器交互的脚本程序。

1. VRTK_ControllerEvents脚本

该脚本基于.NET事件模式,对于手柄的几乎全部交互操作,封装了一个事件发布者类脚本,并挂载在[CameraRig]对象下使用。首先,我通过该脚本再一次系统地学习了如何开发.NET事件模式程序。概念上,事件等同于一个多播委托,代码上则包含一个固定的模式,我将它归纳为:

(1)事件消息类型定义:事件发布者需要进行更改并通知所有订阅者的数据,即数据源类型。通常要继承EventArgs,但新的.NET框架可以不必(VRTK的代码属于后者)。

(2)事件委托定义:起到了定义该事件委托格式的作用,几个约束包括,返回值一定是void、第一个参数一定为object类型、第二个参数为(1)步骤中定义的消息类型。事实上,这步代码真正的作用就是给该事件绑定(1)中定义的用户消息类型。

(3)定义事件:通过event关键字,给(2)中的事件委托起一个名称,实质上这个步骤起到的是命名作用。

(4)发布者方法:发布者最后需要决定的,即是在什么时候发布消息、以及如何发布消息。对于该案例脚本,在MonoBehaviour的Update函数中进行更新是合理的策略,该脚本提供了一个EmitAlias统一各种手柄交互事件的发布形式。

(5)订阅者:订阅者不需要知道事件的实现细节,它们只需要对该事件类的事件成员(通常设为public)进行操作即可,包括添加和删除自己定义的委托方法。

其次,VRTK是基于SteamVR Plugin实现的,必然要用到其中的接口。该脚本实现的核心在于SteamVR_Controller.Device(以下简称Device类型)类型。该类型代表了一个Vive设备,目前Vive还只包括两个手柄和一个头盔,从SteamVR的代码上看,Vive是有增加追踪设备的计划的。这里,VRTK_ControllerEvents脚本被挂载到[CameraRig]下的一个[Controller](left/right)子物体下,那么Device类型成员就被赋值为该手柄。Device类的接口为我们提供了通过手柄来交互的方式。手柄都包括哪些交互元素呢?在SteamVR Plugin中又提供了哪些接口呢?

HTC Vive开发学习——VRTK(1)_第1张图片

通过阅读Device类的代码,我们可以将手柄交互元素和接口总结为以下几个方面:

(1)位置和速度:手柄运动的线速度和角速度,接口:velocity和angularVelocity。位置,接口:transform。

(2)按键操作:各种GetPressXX/GetTouchXX

(3)按键内容:对于触摸板(TouchPad),可以获取Axis、Angle等信息;对于扳机,可以获取勾动扳机的幅度,比如可以用来判断误操作。接口:GetAxis等。

2. VRTK_ControllerEvents_ListenerExample脚本

这个是订阅者脚本,这里给事件绑定的响应是一系列的控制台打印操作。在后续的开发中,我们可以从这个Demo的执行效果来观察,Vive手柄各种交互操作的输出结果是什么,并以此来设计具体应用中的交互逻辑。

(二)003_Controller_SimplePointer

这个案例中,按Grip键应该能弹出一个射线,碰到物体会出现一个指针。但貌似几个脚本都没有检测Grip按键,因此效果看不出来。通过仔细阅读VRTK_ControllerEvents脚本,我发现错误在于下面这行代码:

if (device.GetTouchDown(SteamVR_Controller.ButtonMask.Grip))

Device接口有两种,GetPressXX或者GetTouchXX,从语义上也比较好理解Press即是按下而Touch是触摸。后者仅仅对支持触摸方式的交互元素有效(触摸板和扳机),而对于Grip按键,显然只支持Press操作,因此这行代码无法检测Grip按键(事实上,ApplicationMenu按键的处理也存在这个问题。Trigger按键的Press操作并没有处理,可以根据后续需求按照VRTK_ControllerEvents脚本的实现方式,添加对应的事件实现交互操作。)为了解决这个问题,我又仔细看了一下VRTK_ControllerEvents这个脚本,它在VRTK中的地位十分重要,是后面所有基本功能的基础。因此我给代码加了一些必要注释,方便后面回顾。

namespace VRTK
{
    using UnityEngine;
    using System.Collections;

    //定义事件消息格式
    public struct ControllerInteractionEventArgs
    {
        public uint controllerIndex; //控制器索引
        public float buttonPressure; //按键幅度
        public Vector2 touchpadAxis; //触摸板Axis
        public float touchpadAngle;  //触摸板Angle
    }

    //定义事件委托
    public delegate void ControllerInteractionEventHandler(object sender, ControllerInteractionEventArgs e);

    public class VRTK_ControllerEvents : MonoBehaviour
    {
        public enum ButtonAlias //给按钮起一些别名
        {
            Trigger,
            Grip,
            Touchpad_Touch,
            Touchpad_Press,
            Application_Menu
        }

        public ButtonAlias pointerToggleButton = ButtonAlias.Grip;
        public ButtonAlias grabToggleButton = ButtonAlias.Trigger;
        public ButtonAlias useToggleButton = ButtonAlias.Trigger;
        public ButtonAlias menuToggleButton = ButtonAlias.Application_Menu;

        public int axisFidelity = 1;

        //公共按键状态标志位:方便其它脚本进行查询
        public bool triggerPressed = false;
        public bool triggerAxisChanged = false;
        public bool applicationMenuPressed = false;
        public bool touchpadPressed = false;
        public bool touchpadTouched = false;
        public bool touchpadAxisChanged = false;
        public bool gripPressed = false;

        public bool pointerPressed = false;
        public bool grabPressed = false;
        public bool usePressed = false;
        public bool menuPressed = false;

        //定义各种事件
        public event ControllerInteractionEventHandler TriggerPressed;
        public event ControllerInteractionEventHandler TriggerReleased;

        public event ControllerInteractionEventHandler TriggerAxisChanged;

        public event ControllerInteractionEventHandler ApplicationMenuPressed;
        public event ControllerInteractionEventHandler ApplicationMenuReleased;

        public event ControllerInteractionEventHandler GripPressed;
        public event ControllerInteractionEventHandler GripReleased;

        public event ControllerInteractionEventHandler TouchpadPressed;
        public event ControllerInteractionEventHandler TouchpadReleased;

        public event ControllerInteractionEventHandler TouchpadTouchStart;
        public event ControllerInteractionEventHandler TouchpadTouchEnd;

        public event ControllerInteractionEventHandler TouchpadAxisChanged;

        public event ControllerInteractionEventHandler AliasPointerOn;
        public event ControllerInteractionEventHandler AliasPointerOff;

        public event ControllerInteractionEventHandler AliasGrabOn;
        public event ControllerInteractionEventHandler AliasGrabOff;

        public event ControllerInteractionEventHandler AliasUseOn;
        public event ControllerInteractionEventHandler AliasUseOff;

        public event ControllerInteractionEventHandler AliasMenuOn;
        public event ControllerInteractionEventHandler AliasMenuOff;


        //设备相关
        private uint controllerIndex; //设备索引:获取具体设备
        private SteamVR_TrackedObject trackedController; //跟踪:获取controllerIndex
        private SteamVR_Controller.Device device; //设备对象:Device类提供了一系列获取设备信息的接口

        private Vector2 touchpadAxis = Vector2.zero;
        private Vector2 triggerAxis = Vector2.zero;

        public virtual void OnTriggerPressed(ControllerInteractionEventArgs e)
        {
            if (TriggerPressed != null)
                TriggerPressed(this, e);
        }

        public virtual void OnTriggerReleased(ControllerInteractionEventArgs e)
        {
            if (TriggerReleased != null)
                TriggerReleased(this, e);
        }

        public virtual void OnTriggerAxisChanged(ControllerInteractionEventArgs e)
        {
            if (TriggerAxisChanged != null)
                TriggerAxisChanged(this, e);
        }

        public virtual void OnApplicationMenuPressed(ControllerInteractionEventArgs e)
        {
            if (ApplicationMenuPressed != null)
                ApplicationMenuPressed(this, e);
        }

        public virtual void OnApplicationMenuReleased(ControllerInteractionEventArgs e)
        {
            if (ApplicationMenuReleased != null)
                ApplicationMenuReleased(this, e);
        }

        public virtual void OnGripPressed(ControllerInteractionEventArgs e)
        {
            if (GripPressed != null)
                GripPressed(this, e);
        }

        public virtual void OnGripReleased(ControllerInteractionEventArgs e)
        {
            if (GripReleased != null)
                GripReleased(this, e);
        }

        public virtual void OnTouchpadPressed(ControllerInteractionEventArgs e)
        {
            if (TouchpadPressed != null)
                TouchpadPressed(this, e);
        }

        public virtual void OnTouchpadReleased(ControllerInteractionEventArgs e)
        {
            if (TouchpadReleased != null)
                TouchpadReleased(this, e);
        }

        public virtual void OnTouchpadTouchStart(ControllerInteractionEventArgs e)
        {
            if (TouchpadTouchStart != null)
                TouchpadTouchStart(this, e);
        }

        public virtual void OnTouchpadTouchEnd(ControllerInteractionEventArgs e)
        {
            if (TouchpadTouchEnd != null)
                TouchpadTouchEnd(this, e);
        }

        public virtual void OnTouchpadAxisChanged(ControllerInteractionEventArgs e)
        {
            if (TouchpadAxisChanged != null)
                TouchpadAxisChanged(this, e);
        }

        public virtual void OnAliasPointerOn(ControllerInteractionEventArgs e)
        {
            if (AliasPointerOn != null)
                AliasPointerOn(this, e);
        }

        public virtual void OnAliasPointerOff(ControllerInteractionEventArgs e)
        {
            if (AliasPointerOff != null)
                AliasPointerOff(this, e);
        }

        public virtual void OnAliasGrabOn(ControllerInteractionEventArgs e)
        {
            if (AliasGrabOn != null)
                AliasGrabOn(this, e);
        }

        public virtual void OnAliasGrabOff(ControllerInteractionEventArgs e)
        {
            if (AliasGrabOff != null)
                AliasGrabOff(this, e);
        }

        public virtual void OnAliasUseOn(ControllerInteractionEventArgs e)
        {
            if (AliasUseOn != null)
                AliasUseOn(this, e);
        }

        public virtual void OnAliasUseOff(ControllerInteractionEventArgs e)
        {
            if (AliasUseOff != null)
                AliasUseOff(this, e);
        }

        public virtual void OnAliasMenuOn(ControllerInteractionEventArgs e)
        {
            if (AliasMenuOn != null)
                AliasMenuOn(this, e);
        }

        public virtual void OnAliasMenuOff(ControllerInteractionEventArgs e)
        {
            if (AliasMenuOff != null)
                AliasMenuOff(this, e);
        }

        //创建事件消息
        ControllerInteractionEventArgs SetButtonEvent(ref bool buttonBool, bool value, float buttonPressure)
        {
            buttonBool = value;
            ControllerInteractionEventArgs e;
            e.controllerIndex = controllerIndex;
            e.buttonPressure = buttonPressure;
            e.touchpadAxis = device.GetAxis();

            float angle = Mathf.Atan2(e.touchpadAxis.y, e.touchpadAxis.x) * Mathf.Rad2Deg;
            angle = 90.0f - angle;
            if (angle < 0)
                angle += 360.0f;

            e.touchpadAngle = angle;

            return e;
        }

        void Awake()
        {
            trackedController = GetComponent();
        }

        void Start()
        {
            controllerIndex = (uint)trackedController.index;
            device = SteamVR_Controller.Input((int)controllerIndex);
        }

        //语义:type指定了按键操作类型,touchDown指明了事件是On/Off
        //buttonPressure指明了按键幅度,buttonBool表示更改的标志位(因此是ref类型)
        void EmitAlias(ButtonAlias type, bool touchDown, float buttonPressure, ref bool buttonBool) //发布消息
        {
            if (pointerToggleButton == type)
            {
                if (touchDown)
                {
                    pointerPressed = true;
                    OnAliasPointerOn(SetButtonEvent(ref buttonBool, true, buttonPressure));
                }
                else
                {
                    pointerPressed = false;
                    OnAliasPointerOff(SetButtonEvent(ref buttonBool, false, buttonPressure));
                }
            }

            if (grabToggleButton == type)
            {
                if (touchDown)
                {
                    grabPressed = true;
                    OnAliasGrabOn(SetButtonEvent(ref buttonBool, true, buttonPressure));
                }
                else
                {
                    grabPressed = false;
                    OnAliasGrabOff(SetButtonEvent(ref buttonBool, false, buttonPressure));
                }
            }

            if (useToggleButton == type)
            {
                if (touchDown)
                {
                    usePressed = true;
                    OnAliasUseOn(SetButtonEvent(ref buttonBool, true, buttonPressure));
                }
                else
                {
                    usePressed = false;
                    OnAliasUseOff(SetButtonEvent(ref buttonBool, false, buttonPressure));
                }
            }

            if (menuToggleButton == type)
            {
                if (touchDown)
                {
                    menuPressed = true;
                    OnAliasMenuOn(SetButtonEvent(ref buttonBool, true, buttonPressure));
                }
                else
                {
                    menuPressed = false;
                    OnAliasMenuOff(SetButtonEvent(ref buttonBool, false, buttonPressure));
                }
            }
        }

        bool Vector2ShallowEquals(Vector2 vectorA, Vector2 vectorB)
        {
            return (vectorA.x.ToString("F" + axisFidelity) == vectorB.x.ToString("F" + axisFidelity) &&
                    vectorA.y.ToString("F" + axisFidelity) == vectorB.y.ToString("F" + axisFidelity));
        }

        void Update()
        {
            controllerIndex = (uint)trackedController.index;
            device = SteamVR_Controller.Input((int)controllerIndex);

            Vector2 currentTriggerAxis = device.GetAxis(Valve.VR.EVRButtonId.k_EButton_SteamVR_Trigger);
            Vector2 currentTouchpadAxis = device.GetAxis();

            if (Vector2ShallowEquals(triggerAxis, currentTriggerAxis))
            {
                triggerAxisChanged = false;
            }
            else
            {
                OnTriggerAxisChanged(SetButtonEvent(ref triggerAxisChanged, true, currentTriggerAxis.x));
            }

            if (Vector2ShallowEquals(touchpadAxis, currentTouchpadAxis))
            {
                touchpadAxisChanged = false;
            }
            else
            {
                OnTouchpadAxisChanged(SetButtonEvent(ref touchpadTouched, true, 1f));
                touchpadAxisChanged = true;
            }

            touchpadAxis = new Vector2(currentTouchpadAxis.x, currentTouchpadAxis.y);
            triggerAxis = new Vector2(currentTriggerAxis.x, currentTriggerAxis.y);

            //Trigger
            if (device.GetTouchDown(SteamVR_Controller.ButtonMask.Trigger)) //对于每一个按键,都有两类事件:
            {
                OnTriggerPressed(SetButtonEvent(ref triggerPressed, true, currentTriggerAxis.x)); //首先,是单纯的按键事件
                EmitAlias(ButtonAlias.Trigger, true, currentTriggerAxis.x, ref triggerPressed);   //其次,是按键别名事件
            }
            else if (device.GetTouchUp(SteamVR_Controller.ButtonMask.Trigger))
            {
                OnTriggerReleased(SetButtonEvent(ref triggerPressed, false, 0f));
                EmitAlias(ButtonAlias.Trigger, false, 0f, ref triggerPressed);
            }

            //ApplicationMenu
            if (device.GetPressDown(SteamVR_Controller.ButtonMask.ApplicationMenu))
            {
                OnApplicationMenuPressed(SetButtonEvent(ref applicationMenuPressed, true, 1f));
                EmitAlias(ButtonAlias.Application_Menu, true, 1f, ref applicationMenuPressed);
            }
            else if (device.GetPressUp(SteamVR_Controller.ButtonMask.ApplicationMenu))
            {

                OnApplicationMenuReleased(SetButtonEvent(ref applicationMenuPressed, false, 0f));
                EmitAlias(ButtonAlias.Application_Menu, false, 0f, ref applicationMenuPressed);
            }

            //Grip
            //if (device.GetTouchDown(SteamVR_Controller.ButtonMask.Grip)) //怀疑是这一行有问题,API是否发生了改变?
            if (device.GetPressDown(SteamVR_Controller.ButtonMask.Grip)) //显然对于Grip不存在touch这种操作,应该改成Press
            {
                OnGripPressed(SetButtonEvent(ref gripPressed, true, 1f));
                EmitAlias(ButtonAlias.Grip, true, 1f, ref gripPressed);
            }
            else if (device.GetPressUp(SteamVR_Controller.ButtonMask.Grip))
            {
                OnGripReleased(SetButtonEvent(ref gripPressed, false, 0f));
                EmitAlias(ButtonAlias.Grip, false, 0f, ref gripPressed);
            }

            //Touchpad Pressed
            if (device.GetPressDown(SteamVR_Controller.ButtonMask.Touchpad))
            {
                OnTouchpadPressed(SetButtonEvent(ref touchpadPressed, true, 1f));
                EmitAlias(ButtonAlias.Touchpad_Press, true, 1f, ref touchpadPressed);
            }
            else if (device.GetPressUp(SteamVR_Controller.ButtonMask.Touchpad))
            {
                OnTouchpadReleased(SetButtonEvent(ref touchpadPressed, false, 0f));
                EmitAlias(ButtonAlias.Touchpad_Press, false, 0f, ref touchpadPressed);
            }

            //Touchpad Touched
            if (device.GetTouchDown(SteamVR_Controller.ButtonMask.Touchpad))
            {
                OnTouchpadTouchStart(SetButtonEvent(ref touchpadTouched, true, 1f));
                EmitAlias(ButtonAlias.Touchpad_Touch, true, 1f, ref touchpadTouched);
            }
            else if (device.GetTouchUp(SteamVR_Controller.ButtonMask.Touchpad))
            {
                OnTouchpadTouchEnd(SetButtonEvent(ref touchpadTouched, false, 0f));
                EmitAlias(ButtonAlias.Touchpad_Touch, false, 0f, ref touchpadTouched);
            }
        }
    }
}

这里有一个点我想提一下,该脚本的事件总体包括两大类,一个是代表按键操作本身的事件,另一个是该脚本实现的抽象事件类型。对于后者,可以在Inspector中根据现有的属性进行更改,比如我们可以将发射射线的操作由Grip轻松改成TouchPad_Press!也可以自行扩展,实现我们想要的交互功能。

关于如何处理手柄交互操作的VRTK_ControllerEvents脚本我们已经基本搞清楚了,下一节我们将会详细探究003案例射线的实现原理和交互逻辑。

参考文献:

1. Github:https://github.com/giveaphuk/SteamVR_Unity_Toolkit

2. 《Htc Vive VR游戏开发实战》

3. Vive教程:https://blog.csdn.net/fcauto2012/article/details/78398171




你可能感兴趣的:(Vive开发)