Unity의 HP Reverb G2 컨트롤러

HP 모션 컨트롤러는 새로운 유형의 Windows Mixed Reality 컨트롤러입니다. 사용 가능한 입력 집합이 약간 다른 모든 동일한 추적 기술:

  • 터치 패드는 오른쪽 컨트롤러의 경우 A와 B, 왼쪽 컨트롤러의 경우 X와 Y의 두 단추로 대체되었습니다.
  • 이제 손아귀는 누른 상태와 누르지 않음 상태의 단추 대신 0.0에서 1.0 사이의 값 스트림을 게시하는 트리거입니다.

기존 Windows 및 Unity API를 통해 새 입력에 액세스할 수 없으므로 전용 Microsoft.MixedReality.Input UPM 패키지가 필요합니다.

중요

이 패키지의 클래스는 기존 Windows 및 Unity API를 대체하지 않고 보완합니다. 클래식 Windows Mixed Reality 컨트롤러와 HP 모션 컨트롤러 모두에서 일반적으로 사용할 수 있는 기능은 기존 API를 사용하여 동일한 코드 경로를 통해 액세스할 수 있습니다. 새 입력만 추가 Microsoft.MixedReality.Input 패키지를 사용해야 합니다.

HP 모션 컨트롤러 개요

Microsoft.MixedReality.Input.MotionController 는 동작 컨트롤러를 나타냅니다. 각 MotionController 인스턴스에는 XR이 있습니다. WSA. Input.InteractionSource 피어는 손수, 공급업체 ID, 제품 ID 및 버전을 사용하여 상관 관계를 지정할 수 있습니다.

InteractionManager 이벤트를 사용하여 새 InteractionSource 인스턴스를 검색하는 것과 유사하게 MotionControllerWatcher를 만들고 해당 이벤트를 구독하여 MotionController 인스턴스를 잡을 수 있습니다. MotionController의 메서드 및 속성은 단추, 트리거, 2D 축 및 엄지스틱을 포함하여 컨트롤러에서 지원하는 입력을 설명합니다. 또한 MotionController 클래스는 MotionControllerReading 클래스를 통해 입력 상태에 액세스하는 메서드를 노출합니다. MotionControllerReading 클래스는 지정된 시간에 컨트롤러 상태의 스냅샷을 나타냅니다.

Mixed Reality 기능 도구를 사용하여 Microsoft.MixedReality.Input 설치

새 Mixed Reality Feature Tool 애플리케이션을 사용하여 Microsoft.MixedReality.Input 플러그 인을 설치합니다. 설치 및 사용 지침을 따르고 Mixed Reality Toolkit 범주에서 Mixed Reality 입력 패키지를 선택합니다.

Mixed Reality Feature Tool packages window with mixed reality input highlighted

Microsoft.MixedReality.Input 사용

입력 값

MotionController는 다음 두 종류의 입력을 노출할 수 있습니다.

  • 단추 및 트리거 상태는 누른 정도를 나타내는 0.0에서 1.0 사이의 고유한 부동 소수점으로 표현됩니다.
    • 단추는 0.0(누르지 않은 경우) 또는 1.0(누를 때)만 반환할 수 있지만 트리거는 0.0(완전히 해제됨)에서 1.0(완전히 누름) 사이의 연속 값을 반환할 수 있습니다.
  • 썸스틱 상태는 X 및 Y 구성 요소가 -1.0에서 1.0 사이인 Vector2에 의해 표현됩니다.

MotionController.GetPressableInputs()를 사용하여 누른 값(단추 및 트리거) 또는 MotionController.GetXYInputs() 메서드를 반환하는 입력 목록을 반환하여 2축 값을 반환하는 입력 목록을 반환할 수 있습니다.

MotionControllerReading 인스턴스는 지정된 시간에 컨트롤러의 상태를 나타냅니다.

  • GetPressedValue() 는 단추 또는 트리거의 상태를 검색합니다.
  • GetXYValue() 는 엄지스틱의 상태를 검색합니다.

MotionController 인스턴스 및 해당 상태의 컬렉션을 유지하기 위한 캐시 만들기

먼저 MotionControllerWatcher를 인스턴스화하고 해당 MotionControllerAddedMotionControllerRemoved 이벤트에 대한 처리기를 등록하여 사용 가능한 MotionController 인스턴스의 캐시를 유지합니다. 이 캐시는 다음 코드에 설명된 대로 GameObject에 연결된 MonoBehavior여야 합니다.

public class MotionControllerStateCache : MonoBehaviour 
{ 
    /// <summary> 
    /// Internal helper class which associates a Motion Controller 
    /// and its known state 
    /// </summary> 
    private class MotionControllerState 
    { 
        /// <summary> 
        /// Construction 
        /// </summary> 
        /// <param name="mc">motion controller</param>` 
        public MotionControllerState(MotionController mc) 
        { 
            this.MotionController = mc; 
        } 

        /// <summary> 
        /// Motion Controller that the state represents 
        /// </summary> 
        public MotionController MotionController { get; private set; } 
        … 
    } 

    private MotionControllerWatcher _watcher; 
    private Dictionary<Handedness, MotionControllerState> 
        _controllers = new Dictionary<Handedness, MotionControllerState>(); 

    /// <summary> 
    /// Starts monitoring controller's connections and disconnections 
    /// </summary> 
    public void Start() 
    { 
        _watcher = new MotionControllerWatcher(); 
        _watcher.MotionControllerAdded += _watcher_MotionControllerAdded; 
        _watcher.MotionControllerRemoved += _watcher_MotionControllerRemoved; 
        var nowait = _watcher.StartAsync(); 
    } 

    /// <summary> 
    /// Stops monitoring controller's connections and disconnections 
    /// </summary> 
    public void Stop() 
    { 
        if (_watcher != null) 
        { 
            _watcher.MotionControllerAdded -= _watcher_MotionControllerAdded; 
            _watcher.MotionControllerRemoved -= _watcher_MotionControllerRemoved; 
            _watcher.Stop(); 
        } 
    }

    /// <summary> 
    /// called when a motion controller has been removed from the system: 
    /// Remove a motion controller from the cache 
    /// </summary> 
    /// <param name="sender">motion controller watcher</param> 
    /// <param name="e">motion controller </param> 
    private void _watcher_MotionControllerRemoved(object sender, MotionController e) 
    { 
        lock (_controllers) 
        { 
            _controllers.Remove(e.Handedness); 
        } 
    }

    /// <summary> 
    /// called when a motion controller has been added to the system: 
    /// Remove a motion controller from the cache 
    /// </summary> 
    /// <param name="sender">motion controller watcher</param> 
    /// <param name="e">motion controller </param> 
    private void _watcher_MotionControllerAdded(object sender, MotionController e) 
    { 
        lock (_controllers) 
        { 
            _controllers[e.Handedness] = new MotionControllerState(e); 
        } 
    } 
} 

폴링으로 새 입력 읽기

MonoBehavior 클래스의 Update 메서드 중에 MotionController.TryGetReadingAtTime을 통해 알려진 각 컨트롤러의 현재 상태를 읽을 수 있습니다. DateTime.Now를 타임스탬프 매개 변수로 전달하여 컨트롤러의 최신 상태를 읽도록 합니다.

public class MotionControllerStateCache : MonoBehaviour 
{ 
    … 

    private class MotionControllerState 
    {
        … 

        /// <summary> 
        /// Update the current state of the motion controller 
        /// </summary> 
        /// <param name="when">time of the reading</param> 
        public void Update(DateTime when) 
        { 
            this.CurrentReading = this.MotionController.TryGetReadingAtTime(when); 
        } 

        /// <summary> 
        /// Last reading from the controller 
        /// </summary> 
        public MotionControllerReading CurrentReading { get; private set; } 
    } 

    /// <summary> 
    /// Updates the input states of the known motion controllers 
    /// </summary> 
    public void Update() 
    { 
        var now = DateTime.Now; 

        lock (_controllers) 
        { 
            foreach (var controller in _controllers) 
            { 
                controller.Value.Update(now); 
            } 
        } 
    } 
} 

컨트롤러의 Handedness를 사용하여 컨트롤러의 현재 입력 값을 잡을 수 있습니다.

public class MotionControllerStateCache : MonoBehaviour 
{ 
    … 
    /// <summary> 
    /// Returns the current value of a controller input such as button or trigger 
    /// </summary> 
    /// <param name="handedness">Handedness of the controller</param> 
    /// <param name="input">Button or Trigger to query</param> 
    /// <returns>float value between 0.0 (not pressed) and 1.0 
    /// (fully pressed)</returns> 
    public float GetValue(Handedness handedness, ControllerInput input) 
    { 
        MotionControllerReading currentReading = null; 

        lock (_controllers) 
        { 
            if (_controllers.TryGetValue(handedness, out MotionControllerState mc)) 
            { 
                currentReading = mc.CurrentReading; 
            } 
        } 

        return (currentReading == null) ? 0.0f : currentReading.GetPressedValue(input); 
    } 

    /// <summary> 
    /// Returns the current value of a controller input such as button or trigger 
    /// </summary> 
    /// <param name="handedness">Handedness of the controller</param> 
    /// <param name="input">Button or Trigger to query</param> 
    /// <returns>float value between 0.0 (not pressed) and 1.0 
    /// (fully pressed)</returns> 
    public float GetValue(UnityEngine.XR.WSA.Input.InteractionSourceHandedness handedness, ControllerInput input) 
    { 
        return GetValue(Convert(handedness), input); 
    } 

    /// <summary> 
    /// Returns a boolean indicating whether a controller input such as button or trigger is pressed 
    /// </summary> 
    /// <param name="handedness">Handedness of the controller</param> 
    /// <param name="input">Button or Trigger to query</param> 
    /// <returns>true if pressed, false if not pressed</returns> 
    public bool IsPressed(Handedness handedness, ControllerInput input) 
    { 
        return GetValue(handedness, input) >= PressedThreshold; 
    } 
} 

예를 들어 InteractionSource의 아날로그 파악 값을 읽으려면 다음을 수행합니다.

/// Read the analog grasp value of all connected interaction sources 
void Update() 
{ 
    … 
    var stateCache = gameObject.GetComponent<MotionControllerStateCache>(); 
    foreach (var sourceState in InteractionManager.GetCurrentReading()) 
    { 
        float graspValue = stateCache.GetValue(sourceState.source.handedness, 
            Microsoft.MixedReality.Input.ControllerInput.Grasp);
        … 
    }
} 

새 입력에서 이벤트 생성

프레임당 한 번씩 컨트롤러의 상태를 폴링하는 대신 모든 상태 변경을 이벤트로 처리할 수 있으므로 프레임보다 적게 지속되는 가장 빠른 작업도 처리할 수 있습니다. 이 접근 방식이 작동하려면 모션 컨트롤러의 캐시가 마지막 프레임 이후 컨트롤러에 의해 게시된 모든 상태를 처리해야 합니다. 이 작업은 MotionController에서 검색된 마지막 MotionControllerReading의 타임스탬프를 저장하고 MotionController.TryGetReadingAfterTime()을 호출하여 수행할 수 있습니다.

private class MotionControllerState 
{ 
    … 
    /// <summary> 
    /// Returns an array representng buttons which are pressed 
    /// </summary> 
    /// <param name="reading">motion controller reading</param> 
    /// <returns>array of booleans</returns> 
    private bool[] GetPressed(MotionControllerReading reading) 
    { 
        if (reading == null) 
        { 
            return null; 
        } 
        else 
        { 
            bool[] ret = new bool[this.pressableInputs.Length]; 
            for (int i = 0; i < pressableInputs.Length; ++i) 
            { 
                ret[i] = reading.GetPressedValue(pressableInputs[i]) >= PressedThreshold; 
            } 

            return ret; 
        } 
    } 

    /// <summary> 
    /// Get the next available state of the motion controller 
    /// </summary> 
    /// <param name="lastReading">previous reading</param> 
    /// <param name="newReading">new reading</param> 
    /// <returns>true is a new reading was available</returns> 
    private bool GetNextReading(MotionControllerReading lastReading, out MotionControllerReading newReading) 
    { 
        if (lastReading == null) 
        { 
            // Get the first state published by the controller 
            newReading = this.MotionController.TryGetReadingAfterSystemRelativeTime(TimeSpan.FromSeconds(0.0)); 
        } 
        else 
        { 
            // Get the next state published by the controller 
            newReading = this.MotionController.TryGetReadingAfterTime(lastReading.InputTime); 
        } 

        return newReading != null; 
    } 

    /// <summary> 
    /// Processes all the new states published by the controller since the last call 
    /// </summary> 
    public IEnumerable<MotionControllerEventArgs> GetNextEvents() 
    {
        MotionControllerReading lastReading = this.CurrentReading; 
        bool[] lastPressed = GetPressed(lastReading); 
        MotionControllerReading newReading; 
        bool[] newPressed; 

        while (GetNextReading(lastReading, out newReading)) 
        { 
            newPressed = GetPressed(newReading); 

            // If we have two readings, compare and generate events 
            if (lastPressed != null) 
            { 
                for (int i = 0; i < pressableInputs.Length; ++i) 
                { 
                    if (newPressed[i] != lastPressed[i]) 
                    { 
                        yield return new MotionControllerEventArgs(this.MotionController.Handedness, newPressed[i], this.pressableInputs[i], newReading.InputTime); 
                    } 
                } 
            } 

            lastPressed = newPressed; 
            lastReading = newReading; 
        } 

        // No more reading 
        this.CurrentReading = lastReading; 
    } 
} 

이제 캐시 내부 클래스를 업데이트했으므로 MonoBehavior 클래스는 Pressed 및 Released라는 두 개의 이벤트를 노출하고 Update() 메서드에서 발생합니다.

/// <summary> 
/// Event argument class for InputPressed and InputReleased events 
/// </summary> 
public class MotionControllerEventArgs : EventArgs 
{ 
    public MotionControllerEventArgs(Handedness handedness, bool isPressed, rollerInput input, DateTime inputTime) 
    { 
        this.Handedness = handedness; 
        this.Input = input; 
        this.InputTime = inputTime; 
        this.IsPressed = isPressed; 
    } 

    /// <summary> 
    /// Handedness of the controller raising the event 
    /// </summary> 
    public Handedness Handedness { get; private set; } 

    /// <summary> 
    /// Button pressed or released 
    /// </summary> 
    public ControllerInput Input { get; private set; } 

    /// <summary> 
    /// Time of the event 
    /// </summary> 
    public DateTime InputTime { get; private set; } 

    /// <summary> 
    /// true if button is pressed, false otherwise 
    /// </summary> 
    public bool IsPressed { get; private set; } 
} 

/// <summary> 
/// Event raised when a button is pressed 
/// </summary> 
public event EventHandler<MotionControllerEventArgs> InputPressed; 

/// <summary> 
/// Event raised when a button is released 
/// </summary> 
public event EventHandler<MotionControllerEventArgs> InputReleased; 

/// <summary> 
/// Updates the input states of the known motion controllers 
/// </summary> 
public void Update() 
{ 
    // If some event handler has been registered, we need to process all states  
    // since the last update, to avoid missing a quick press / release 
    if ((InputPressed != null) || (InputReleased != null)) 
    { 
        List<MotionControllerEventArgs> events = new <MotionControllerEventArgs>(); 

        lock (_controllers) 
        { 
            foreach (var controller in _controllers) 
            { 
                events.AddRange(controller.Value.GetNextEvents()); 
            } 
        } 
 
        // Sort the events by time 
        events.Sort((e1, e2) => DateTime.Compare(e1.InputTime, e2.InputTime)); 

        foreach (MotionControllerEventArgs evt in events) 
        { 
            if (evt.IsPressed && (InputPressed != null)) 
            { 
                InputPressed(this, evt); 
            } 
            else if (!evt.IsPressed && (InputReleased != null)) 
            { 
                InputReleased(this, evt); 
            } 
        } 
    } 
    else 
    { 
        // As we do not predict button presses and the timestamp of the next e is in the future 
        // DateTime.Now is correct in this context as it will return the latest e of controllers 
        // which is the best we have at the moment for the frame. 
        var now = DateTime.Now; 
        lock (_controllers) 
        { 
            foreach (var controller in _controllers) 
            { 
                controller.Value.Update(now); 
            } 
        } 
    } 
} 

위의 코드 예제의 구조는 등록 이벤트를 훨씬 더 쉽게 읽을 수 있게 합니다.

public InteractionSourceHandedness handedness; 
public Microsoft.MixedReality.Input.ControllerInput redButton;

// Start of the Mono Behavior: register handlers for events from cache 
void Start() 
{ 
    var stateCache = gameObject.GetComponent<MotionControllerStateCache>(); 
    stateCache.InputPressed += stateCache_InputPressed; 
    stateCache.InputReleased += stateCache_InputReleased; 
    … 
} 

// Called when a button is released 
private void stateCache_InputReleased(object sender, MotionControllerStateCache.MotionControllerEventArgs e) 
{ 
    if ((e.SourceHandedness == handedness) && (e.Input == redButton)) 
    { 
        … 
    } 
} 

// Called when a button is pressed 
private void stateCache_InputPressed(object sender, MotionControllerStateCache.MotionControllerEventArgs e) 
{ 
    if ((e.SourceHandedness == handedness) && (e.Input == redButton)) 
    { 
        … 
    } 
} 

참고 항목