Решил в отпуске изучить разработку под Vive в Unity3D. Погуглил парочку примеров и начал пробовать, но почему то не работало. Начав подробней разбираться и обнаружил, что Valve выкатили недавно обновление плагина для Unity3D — новая, сильно переделанная версия. В ней появилось парочка принципиальных новшеств, которые сделали старые tutorial'ы не актуальными. Решил написать новый



Нам понадобится Unity >= 5.4.0 и новый плагин SteamVR Plugin (GitHub)


В самом плагине есть три полезных для ознакомления pdf'ки


\Assets\SteamVR\SteamVR Unity Plugin.pdf
\Assets\SteamVR\SteamVR Unity Plugin — Input System.pdf
\Assets\SteamVR\InteractionSystem\InteractionSystem.pdf


И два примера:


\Assets\SteamVR\Simple Sample.unity
\Assets\SteamVR\InteractionSystem\Samples\Interactions_Example.unity


Плагин поддерживает режим имитации — он включается если не включен шлем



Ну а теперь по шагам;


  1. Создадим новый проект по шаблону 3D с плагином SteamVR Plugin


  2. Соглашаемся с настройками



  3. Теперь ключевой момент — нужно настроить управление. Выбираем пункт меню Window\SteamVR Imput



  4. Unity спросит про отсутствующий actions.json и предложит скопировать файл примера (он лежит в \Assets\SteamVR\Input\ExampleJSON) — советую согласиться.



    По json-файлам видно, что плагин рассчитан не только на Vive, но и на Oculus и Windows MR, а также новые контролеры knuckle. С этим связанны основные изменения.


  5. В открывшемся окне достаточно нажать "Save and generate"



  6. Теперь нужно добавить Игрока (Player из \Assets\SteamVR\InteractionSystem\Core\Prefabs) в Сцену и удалить Main Camera



    что бы работал режим имитации — он должен быть включён в свойствах Игрока



    Ещё полезно скопировать папку \Assets\SteamVR\InteractionSystem\Core\Icons в \Assets и переименовать её в Gizmos


  7. Можно нажимать Play — но только фальшивая рука в имитации работать не будет, нужно скопировать скрипт от сюда и повесить его на Player->NoSteamVRFallbackObjects->FallbackHand



Код скрипта для фальшивой руки
using System.Collections.Generic;
using UnityEngine;
using Valve.VR;

public class VrSimulatorHandFixer156 : SteamVR_Behaviour_Pose
{
    Valve.VR.InteractionSystem.Hand _hand;
    protected override void Start()
    {

        base.Start();
        _hand = this.gameObject.GetComponent<Valve.VR.InteractionSystem.Hand>();
        _hand.handType = SteamVR_Input_Sources.RightHand;

            GameObject broHand = GameObject.Instantiate(_hand.gameObject);
            Destroy(broHand.GetComponent<VrSimulatorHandFixer156>());
            broHand.SetActive(false);
        _hand.otherHand = broHand.GetComponent<Valve.VR.InteractionSystem.Hand>();
        _hand.otherHand.handType = SteamVR_Input_Sources.LeftHand;

        var spoofMouse = new SpoofMouseAction();
        _hand.grabGripAction = spoofMouse;
        spoofMouse.InitializeDictionariesExposed(_hand.handType);

        this.poseAction = new Poser_SteamVR_Action_Pose();
    }

    protected override void OnEnable()
    {

    }

    protected override void Update()
    {
        _hand.grabGripAction.UpdateValue(SteamVR_Input_Sources.RightHand);
    }

    protected override void OnDisable()
    {
    }

    protected override void CheckDeviceIndex()
    {
    }

    //----------------------------------------------------------------------------------------------
    class SpoofMouseAction : SteamVR_Action_Boolean
    {
        public SpoofMouseAction()
        {

        }

        public void InitializeDictionariesExposed(SteamVR_Input_Sources source)
        {
            try
            {
                InitializeDictionaries(source);
            }
            catch(System.Exception e)
            {
            }
        }

        protected override void InitializeDictionaries(SteamVR_Input_Sources source)
        {
            base.InitializeDictionaries(source);

            onStateDown.Add(source, null);
            onStateUp.Add(source, null);
            actionData.Add(source, new InputDigitalActionData_t());
            lastActionData.Add(source, new InputDigitalActionData_t());
        }

        public override void UpdateValue(SteamVR_Input_Sources inputSource)
        {
            lastActionData[inputSource] = actionData[inputSource];
            tempActionData.bState = Input.GetMouseButton(0) || Input.GetMouseButtonDown(0);
            tempActionData.bChanged = Input.GetMouseButtonDown(0) || Input.GetMouseButtonUp(0);
            tempActionData.bActive = true;
            //tempActionData.fUpdateTime
            //tempActionData.act

            actionData[inputSource] = tempActionData;
            changed[inputSource] = tempActionData.bChanged;
            active[inputSource] = tempActionData.bActive;
            activeOrigin[inputSource] = tempActionData.activeOrigin;
            updateTime[inputSource] = Time.time;// tempActionData.fUpdateTime;

            if (changed[inputSource])
                lastChanged[inputSource] = Time.time;

            if (onStateDown[inputSource] != null && GetStateDown(inputSource))
                onStateDown[inputSource].Invoke(this);

            if (onStateUp[inputSource] != null && GetStateUp(inputSource))
                onStateUp[inputSource].Invoke(this);

            if (onChange[inputSource] != null && GetChanged(inputSource))
                onChange[inputSource].Invoke(this);

            if (onUpdate[inputSource] != null)
                onUpdate[inputSource].Invoke(this);

            if (onActiveChange[inputSource] != null && lastActionData[inputSource].bActive != active[inputSource])
                onActiveChange[inputSource].Invoke(this, active[inputSource]);
        }
    }
        class Poser_SteamVR_Action_Pose : SteamVR_Action_Pose
    {
        public override bool GetActive(SteamVR_Input_Sources inputSource)
        {
            return false;
        }
    }   
}

В режиме VR при включенных контролерах — они будут видны



  1. Мы добавили VR, но даже перемещаться не можем. В плагине есть реализация телепортации
    для её включения нужно добавить в сцену Teleporting из \Assets\SteamVR\InteractionSystem\Teleport\Prefabs
    а также расставить TeleportPoint от туда же



    Телепортироватся можно и в имитации — клавишей T.


  2. Можно создать поверхность телепортации — Создаём поверхность (plane) и вешаем на неё скрипт TeleportArea.cs из \Assets\SteamVR\InteractionSystem\Teleport\Scripts



  3. Попробуем взаимодействие с объектами — создадим Cube и повесим на него скрипт Interactable.cs из \Assets\SteamVR\InteractionSystem\Core\Scripts
    теперь он подсвечивается, но с ним ничего не происходит



  4. Нам нужно прописать взаимодействие — создадим для Cube новый скрипт



Код для взаимодействия
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Valve.VR.InteractionSystem;

public class NewBehaviourScript : MonoBehaviour {

    private Hand.AttachmentFlags attachmentFlags = Hand.defaultAttachmentFlags & (~Hand.AttachmentFlags.SnapOnAttach) & (~Hand.AttachmentFlags.DetachOthers) & (~Hand.AttachmentFlags.VelocityMovement);

    private Interactable interactable;

    // Use this for initialization
    void Start () {
        interactable = this.GetComponent<Interactable>();
    }

    private void HandHoverUpdate(Hand hand)
    {
        GrabTypes startingGrabType = hand.GetGrabStarting();
        bool isGrabEnding = hand.IsGrabEnding(this.gameObject);

        if (startingGrabType != GrabTypes.None)
        {
            // Call this to continue receiving HandHoverUpdate messages,
            // and prevent the hand from hovering over anything else
            hand.HoverLock(interactable);

            // Attach this object to the hand
            hand.AttachObject(gameObject, startingGrabType, attachmentFlags);
        }
        else if (isGrabEnding)
        {
            // Detach this object from the hand
            hand.DetachObject(gameObject);

            // Call this to undo HoverLock
            hand.HoverUnlock(interactable);

        }
    }
}


Более подробно про взаимодействие можно посмотреть в примерах к плагину, в частности в \Assets\SteamVR\InteractionSystem\Samples\Scripts\InteractableExample.cs


А мы дальше попробуем сделать то, чего в примерах нет — добавить новые действия


  1. Откроем скрипт Player.cs и добавим поля


        [SteamVR_DefaultAction("PlayerMove", "default")]
        public SteamVR_Action_Vector2 a_move;
    
        [SteamVR_DefaultAction("PlayerRotate", "default")]
        public SteamVR_Action_Vector2 a_rotate;
    
        [SteamVR_DefaultAction("MenuClick", "default")]
        public SteamVR_Action_Boolean a_menu;

    Допустимые типы для возвращаемых значений можно посмотреть в \Assets\SteamVR\Input



И метод Update:


        private void Update()
        {
            bool st = a_menu.GetStateDown(SteamVR_Input_Sources.Any);
            if (st)
            {
                this.transform.position = new Vector3(0, 0, 0);
            }
            else
            {
                Camera camera = this.GetComponentInChildren<Camera>();
                Quaternion cr = Quaternion.Euler(0, 0, 0);
                if (camera != null)
                {
                    Vector2 r = a_rotate.GetAxis(SteamVR_Input_Sources.RightHand);
                    Quaternion qp = this.transform.rotation;
                    qp.eulerAngles += new Vector3(0, r.x, 0);
                    this.transform.rotation = qp;
                    cr = camera.transform.rotation;
                }
                Vector2 m = a_move.GetAxis(SteamVR_Input_Sources.LeftHand);
                m = Quaternion.Euler(0, 0, -cr.eulerAngles.y) * m;
                this.transform.position += new Vector3(m.x / 10, 0, m.y / 10);
            }
        }

Полный код Player.cs
//======= Copyright (c) Valve Corporation, All rights reserved. ===============
//
// Purpose: Player interface used to query HMD transforms and VR hands
//
//=============================================================================

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

namespace Valve.VR.InteractionSystem
{
    //-------------------------------------------------------------------------
    // Singleton representing the local VR player/user, with methods for getting
    // the player's hands, head, tracking origin, and guesses for various properties.
    //-------------------------------------------------------------------------
    public class Player : MonoBehaviour
    {
        [Tooltip( "Virtual transform corresponding to the meatspace tracking origin. Devices are tracked relative to this." )]
        public Transform trackingOriginTransform;

        [Tooltip( "List of possible transforms for the head/HMD, including the no-SteamVR fallback camera." )]
        public Transform[] hmdTransforms;

        [Tooltip( "List of possible Hands, including no-SteamVR fallback Hands." )]
        public Hand[] hands;

        [Tooltip( "Reference to the physics collider that follows the player's HMD position." )]
        public Collider headCollider;

        [Tooltip( "These objects are enabled when SteamVR is available" )]
        public GameObject rigSteamVR;

        [Tooltip( "These objects are enabled when SteamVR is not available, or when the user toggles out of VR" )]
        public GameObject rig2DFallback;

        [Tooltip( "The audio listener for this player" )]
        public Transform audioListener;

        public bool allowToggleTo2D = true;

        [SteamVR_DefaultAction("PlayerMove", "default")]
        public SteamVR_Action_Vector2 a_move;

        [SteamVR_DefaultAction("PlayerRotate", "default")]
        public SteamVR_Action_Vector2 a_rotate;

        [SteamVR_DefaultAction("MenuClick", "default")]
        public SteamVR_Action_Boolean a_menu;

        //-------------------------------------------------
        // Singleton instance of the Player. Only one can exist at a time.
        //-------------------------------------------------
        private static Player _instance;
        public static Player instance
        {
            get
            {
                if ( _instance == null )
                {
                    _instance = FindObjectOfType<Player>();
                }
                return _instance;
            }
        }

        //-------------------------------------------------
        // Get the number of active Hands.
        //-------------------------------------------------
        public int handCount
        {
            get
            {
                int count = 0;
                for ( int i = 0; i < hands.Length; i++ )
                {
                    if ( hands[i].gameObject.activeInHierarchy )
                    {
                        count++;
                    }
                }
                return count;
            }
        }

        //-------------------------------------------------
        // Get the i-th active Hand.
        //
        // i - Zero-based index of the active Hand to get
        //-------------------------------------------------
        public Hand GetHand( int i )
        {
            for ( int j = 0; j < hands.Length; j++ )
            {
                if ( !hands[j].gameObject.activeInHierarchy )
                {
                    continue;
                }

                if ( i > 0 )
                {
                    i--;
                    continue;
                }

                return hands[j];
            }

            return null;
        }

        //-------------------------------------------------
        public Hand leftHand
        {
            get
            {
                for ( int j = 0; j < hands.Length; j++ )
                {
                    if ( !hands[j].gameObject.activeInHierarchy )
                    {
                        continue;
                    }

                    if ( hands[j].handType != SteamVR_Input_Sources.LeftHand)
                    {
                        continue;
                    }

                    return hands[j];
                }

                return null;
            }
        }

        //-------------------------------------------------
        public Hand rightHand
        {
            get
            {
                for ( int j = 0; j < hands.Length; j++ )
                {
                    if ( !hands[j].gameObject.activeInHierarchy )
                    {
                        continue;
                    }

                    if ( hands[j].handType != SteamVR_Input_Sources.RightHand)
                    {
                        continue;
                    }

                    return hands[j];
                }

                return null;
            }
        }

        //-------------------------------------------------
        // Get Player scale. Assumes it is scaled equally on all axes.
        //-------------------------------------------------

        public float scale
        {
            get
            {
                return transform.lossyScale.x;
            }
        }

        //-------------------------------------------------
        // Get the HMD transform. This might return the fallback camera transform if SteamVR is unavailable or disabled.
        //-------------------------------------------------
        public Transform hmdTransform
        {
            get
            {
                if (hmdTransforms != null)
                {
                    for (int i = 0; i < hmdTransforms.Length; i++)
                    {
                        if (hmdTransforms[i].gameObject.activeInHierarchy)
                            return hmdTransforms[i];
                    }
                }
                return null;
            }
        }

        //-------------------------------------------------
        // Height of the eyes above the ground - useful for estimating player height.
        //-------------------------------------------------
        public float eyeHeight
        {
            get
            {
                Transform hmd = hmdTransform;
                if ( hmd )
                {
                    Vector3 eyeOffset = Vector3.Project( hmd.position - trackingOriginTransform.position, trackingOriginTransform.up );
                    return eyeOffset.magnitude / trackingOriginTransform.lossyScale.x;
                }
                return 0.0f;
            }
        }

        //-------------------------------------------------
        // Guess for the world-space position of the player's feet, directly beneath the HMD.
        //-------------------------------------------------
        public Vector3 feetPositionGuess
        {
            get
            {
                Transform hmd = hmdTransform;
                if ( hmd )
                {
                    return trackingOriginTransform.position + Vector3.ProjectOnPlane( hmd.position - trackingOriginTransform.position, trackingOriginTransform.up );
                }
                return trackingOriginTransform.position;
            }
        }

        //-------------------------------------------------
        // Guess for the world-space direction of the player's hips/torso. This is effectively just the gaze direction projected onto the floor plane.
        //-------------------------------------------------
        public Vector3 bodyDirectionGuess
        {
            get
            {
                Transform hmd = hmdTransform;
                if ( hmd )
                {
                    Vector3 direction = Vector3.ProjectOnPlane( hmd.forward, trackingOriginTransform.up );
                    if ( Vector3.Dot( hmd.up, trackingOriginTransform.up ) < 0.0f )
                    {
                        // The HMD is upside-down. Either
                        // -The player is bending over backwards
                        // -The player is bent over looking through their legs
                        direction = -direction;
                    }
                    return direction;
                }
                return trackingOriginTransform.forward;
            }
        }

        //-------------------------------------------------
        void Awake()
        {
            SteamVR.Initialize(true); //force openvr

            if ( trackingOriginTransform == null )
            {
                trackingOriginTransform = this.transform;
            }
        }

        //-------------------------------------------------
        private IEnumerator Start()
        {
            _instance = this;

            while (SteamVR_Behaviour.instance.forcingInitialization)
                yield return null;

            if ( SteamVR.instance != null )
            {
                ActivateRig( rigSteamVR );
            }
            else
            {
#if !HIDE_DEBUG_UI
                ActivateRig( rig2DFallback );
#endif
            }
        }

        private void Update()
        {
            bool st = a_menu.GetStateDown(SteamVR_Input_Sources.Any);
            if (st)
            {
                this.transform.position = new Vector3(0, 0, 0);
            }
            else
            {
                Camera camera = this.GetComponentInChildren<Camera>();
                Quaternion cr = Quaternion.Euler(0, 0, 0);
                if (camera != null)
                {
                    Vector2 r = a_rotate.GetAxis(SteamVR_Input_Sources.RightHand);
                    Quaternion qp = this.transform.rotation;
                    qp.eulerAngles += new Vector3(0, r.x, 0);
                    this.transform.rotation = qp;
                    cr = camera.transform.rotation;
                }
                Vector2 m = a_move.GetAxis(SteamVR_Input_Sources.LeftHand);
                m = Quaternion.Euler(0, 0, -cr.eulerAngles.y) * m;
                this.transform.position += new Vector3(m.x / 10, 0, m.y / 10);
            }
        }

        //-------------------------------------------------
        void OnDrawGizmos()
        {
            if ( this != instance )
            {
                return;
            }

            //NOTE: These gizmo icons don't work in the plugin since the icons need to exist in a specific "Gizmos"
            //      folder in your Asset tree. These icons are included under Core/Icons. Moving them into a
            //      "Gizmos" folder should make them work again.

            Gizmos.color = Color.white;
            Gizmos.DrawIcon( feetPositionGuess, "vr_interaction_system_feet.png" );

            Gizmos.color = Color.cyan;
            Gizmos.DrawLine( feetPositionGuess, feetPositionGuess + trackingOriginTransform.up * eyeHeight );

            // Body direction arrow
            Gizmos.color = Color.blue;
            Vector3 bodyDirection = bodyDirectionGuess;
            Vector3 bodyDirectionTangent = Vector3.Cross( trackingOriginTransform.up, bodyDirection );
            Vector3 startForward = feetPositionGuess + trackingOriginTransform.up * eyeHeight * 0.75f;
            Vector3 endForward = startForward + bodyDirection * 0.33f;
            Gizmos.DrawLine( startForward, endForward );
            Gizmos.DrawLine( endForward, endForward - 0.033f * ( bodyDirection + bodyDirectionTangent ) );
            Gizmos.DrawLine( endForward, endForward - 0.033f * ( bodyDirection - bodyDirectionTangent ) );

            Gizmos.color = Color.red;
            int count = handCount;
            for ( int i = 0; i < count; i++ )
            {
                Hand hand = GetHand( i );

                if ( hand.handType == SteamVR_Input_Sources.LeftHand)
                {
                    Gizmos.DrawIcon( hand.transform.position, "vr_interaction_system_left_hand.png" );
                }
                else if ( hand.handType == SteamVR_Input_Sources.RightHand)
                {
                    Gizmos.DrawIcon( hand.transform.position, "vr_interaction_system_right_hand.png" );
                }
                else
                {
                    /*
                    Hand.HandType guessHandType = hand.currentHandType;

                    if ( guessHandType == Hand.HandType.Left )
                    {
                        Gizmos.DrawIcon( hand.transform.position, "vr_interaction_system_left_hand_question.png" );
                    }
                    else if ( guessHandType == Hand.HandType.Right )
                    {
                        Gizmos.DrawIcon( hand.transform.position, "vr_interaction_system_right_hand_question.png" );
                    }
                    else
                    {
                        Gizmos.DrawIcon( hand.transform.position, "vr_interaction_system_unknown_hand.png" );
                    }
                    */
                }
            }
        }

        //-------------------------------------------------
        public void Draw2DDebug()
        {
            if ( !allowToggleTo2D )
                return;

            if ( !SteamVR.active )
                return;

            int width = 100;
            int height = 25;
            int left = Screen.width / 2 - width / 2;
            int top = Screen.height - height - 10;

            string text = ( rigSteamVR.activeSelf ) ? "2D Debug" : "VR";

            if ( GUI.Button( new Rect( left, top, width, height ), text ) )
            {
                if ( rigSteamVR.activeSelf )
                {
                    ActivateRig( rig2DFallback );
                }
                else
                {
                    ActivateRig( rigSteamVR );
                }
            }
        }

        //-------------------------------------------------
        private void ActivateRig( GameObject rig )
        {
            rigSteamVR.SetActive( rig == rigSteamVR );
            rig2DFallback.SetActive( rig == rig2DFallback );

            if ( audioListener )
            {
                audioListener.transform.parent = hmdTransform;
                audioListener.transform.localPosition = Vector3.zero;
                audioListener.transform.localRotation = Quaternion.identity;
            }
        }

        //-------------------------------------------------
        public void PlayerShotSelf()
        {
            //Do something appropriate here
        }
    }
}

  1. Запустим Window\SteamVR Imput, Создадим наши действия в наборе default и сохраним их, теперь выберем пункт "Open binding UI" (SteamVR должен быть запущен и как минимум один контролер включён)



  2. В браузере откроется вкладка Controller Binding — в ней нужно настроить связь наших действий с контролерами: PlayerMove мы повесим на левый TRACKPAD (не забудьте выключить Mirror Mode), PlayerRotate на правый TRACKPAD, а MenuClick повесим на клавиши Menu



  3. Закроем Controller Binding и сохраним изменения


  4. В свойствах Player свяжем новые действия



  5. Запускаем Play




Проект


Заключение


Следует отметить несколько моментов. Некоторые действия в SteamVR Imput могут заставить Unity задуматься надолго, в принципе эти изменения можно внести самому в код, а вместо использования Controller Binding можно напрямую править json-файлы, но большой риск ошибки которые сложно будет отловить.


Для более глубокого изучения плагина — полезно подробно изучить примеры, ну и конечно — читайте документацию.

Комментарии (4)


  1. viruseg
    15.10.2018 09:06

    "Погуглил парочку примеров и начал пробывать".
    Попробуйте использовать спелчекер, если уж совсем плохо с русским языком.


    1. trir Автор
      15.10.2018 10:10

      1. viruseg
        15.10.2018 10:21

        С «погуглить» как раз всё устраивает. А вот слова «пробЫвать» в русском языке не существует.
        ПопробОвать или попробЫвать, как правильно писать?


        1. trir Автор
          15.10.2018 10:39

          такие веши прнято писать в личку