Unity VR开发教程 OpenXR+XR Interaction Toolkit 传送

Posted YY-nb

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity VR开发教程 OpenXR+XR Interaction Toolkit 传送相关的知识,希望对你有一定的参考价值。

文章目录


往期回顾:
Unity VR开发教程 OpenXR+XR Interaction Toolkit (一) 安装和配置
Unity VR开发教程 OpenXR+XR Interaction Toolkit (二) 手部动画
Unity VR开发教程 OpenXR+XR Interaction Toolkit (三) 转向和移动

上一篇教程中,我们学习了如何用手柄来控制转向和持续移动。在 VR 应用中,除了持续移动还有另一种比较常见的移动方式,那就是 “传送”,即让玩家直接到达想要传送的地点 。相较于持续移动,传送给人带来的晕动感不会那么强烈。本篇教程,我们一起来学习如何实现传送功能。


📕教程说明

使用的 Unity 版本: 2020.3.36

使用的 VR 头显: Oculus Quest 2

教程使用的 XR Interaction Toolkit 版本:2.1.1

前期的配置:环境配置参考教程一,手部模型参考教程二

项目源码(持续更新):https://github.com/YY-nb/Unity_XRInteractionToolkit_Tutorial2022/tree/master

最终实现的效果:向前推动手柄的摇杆然后释放,实现人物的传送。


📕添加触发传送的脚本

传送其实也是交互的一种。而交互的过程一般需要两个对象,一个是可交互的对象(Interactable),一个是发起交互的对象(Interactor,一般是玩家自己)。传送的过程也是如此,需要可传送的区域和触发传送的人。现在,我们先来介绍与触发传送相关的脚本。

Locomotion System:可以直接控制场景中的 XR Origin(在 VR 中就是我们自己的身体),从而实现 VR 中人物的运动。

Teleportation Provider:负责传送功能。

XR Ray Interactor:通过射线检测实现与物体的远距离交互,在传送时可以配合可视化的射线使用。

Line Renderer:用于渲染传送时的射线

XR Interactor Line Visual:搭配 Line Renderer 和 XR Ray Interacter,使准备传送的射线可视化。

另外 XR Controller(Action-based) 这个脚本也是必备的,它用于跟踪手柄的姿态和处理手柄输入和动作的绑定。而我们的传送功能也是通过手柄的按键触发的。(这个系列中的每一篇教程都会用到它,这篇教程再次强调它是因为待会儿要对这个脚本上的一些设置做些修改)

如果你跟着此系列教程的前三篇做了一遍,那么这些脚本其实就已经添加好了。我这里直接将上一篇教程中的大部分场景沿用下来,此时 Hierarchy 窗口如下图所示:

不过这里还是再简要地提一下:最底下的 Locomotion System 游戏物体可以直接在 Hierarchy 面板中的 XR 下创建。

添加了这个游戏物体就会自带 Teleportation Provider 和 Locomotion System 脚本。

而 XR Controller,XR Ray Interactor,Line Renderer 和 XR Interactor Line Visual 脚本在 XR Origin 下的 LeftHand Controller 和 RightHand Controller 上。

虽然这些脚本可以让 Unity 自动帮我们创建好,但是还是希望大家能够记住需要用到哪些脚本,以及这些脚本大致的作用,以便日后能够根据我们的需求灵活使用。

最后,我们还需要对 XR Ray Interactor 脚本进行一些操作:
在 Inspector 面板中将 Keep Selected Target 取消勾选。否则当你选中了一个传送区域后,你无法切换到另一个传送区域。

接下来,我们来介绍一下传送需要的可交互对象。


📕添加传送区域脚本

⭐Teleportation Area 脚本

我们可以在地面(在本教程的场景中是 Plane)添加 Teleportation Area 脚本。
注:地面必须要有碰撞体,且碰撞体不能设为 Is Trigger,否则无法检测到传送区域

如果没有给此脚本的 Colliders 手动赋值,那么它会找到任意一个子物体(包括自己)的碰撞体,将该碰撞体用于检测是否选中了传送区域,如果触发传送的射线射到了碰撞体上,则视为选中了传送区域。

这时候我们可以运行一下程序。可以看到手部会发出一条直线。当直线射到地面上时,射线颜色会变成白色,因为这时候我们选中了传送区域;当直线没有射到地面上,射线的颜色会变成红色。

按下手柄的 Grip 键,传送就会生效。至于为什么是按下 Grip 键才能传送,在稍后的 “向前推动手柄摇杆实现传送” 这一部分会进行详细地说明。


⭐Teleportation Anchor 脚本

Teleportation Anchor 可以理解成传送的一个目标点,它能使玩家传送到一个指定的位置和角度。

为了演示,我创建了一个新的 Plane,然后创建一个空物体叫做 “Teleportation Anchor",作为传送目标点,然后创建一个它的 Cylinder 子物体来表示可传送的区域,并且将 Cylinder 的碰撞体用于判断是否选中了传送区域。接着给 Teleportation Anchor 游戏物体添加 Teleportation Anchor 脚本。

为了演示能传送到特定角度的功能,我将传送目标点的旋转角度调了一下,让它 z 轴角度和相机 z 轴角度不相同。


相较于 Teleportation Area,Teleportation Anchor 多了一个重要的变量:Teleport Anchor Transform。这个变量就决定了传送的目标点是哪一个,默认是这个脚本所挂载的那个游戏物体,当然你也可以自定义一个传送点。

这个时候我们试着运行程序,可以发现,当手部的射线射到我们设定的圆柱体上时,射线颜色才会变成白色,说明我们选中了传送区域。然后我们按下手柄的 Grip 键,我们就会被传送到圆柱体的那个位置,准确来说就是我们提前设定的 Teleportation Anchor 的位置(可能会有一点点的偏差),尝试多次传送到 Teleportation Anchor,每次传送的位置都是非常相近的,至少都能传送到我们指定的区域。

不过我们可以发现,虽然每次传送的 Position 是相近的,但是传送后人物面朝向的角度却还是传送前面朝向的那个角度。而我们的需求是传送后面朝向指定的角度,也就是传送后眼睛看向的方向和 Teleportation Anchor 的 z 轴所指向的方向一样,那这要怎么实现呢?

回到 Teleportation Anchor 脚本,找到 Match Orientation,改成 Target Up and Forward,意思是传送到目标点后的角度以目标点正上方为 y 轴,目标点正前方为 z 轴,这就与我们的需求匹配了。

现在我们再运行一下程序看看效果:

现在就成功传送到了特定的位置,特定的角度。


📕向前推动手柄摇杆实现传送

值得注意的是,目前是只有按下手柄的 Grip 键,传送才会生效。那么是在什么地方定义了 “按下 Grip 键开始传送” 呢?

我们来看 XR Controller 这个脚本(以左手为例),原因就出在这个 Select Action 当中:

我们打开这个 Reference,可以看到 Select 这个动作绑定的是 “Grip 键按下” 这个操作

但是,按照大多数 VR 游戏的习惯,传送一般是向前推动手柄摇杆的时候被激活,然后释放摇杆进行触发,那么要怎么更改成这种方式来触发传送呢?

其实在 Input Action Asset 里,就有相关的动作。我们找到 XRI LeftHand Locomotion 的 Teleport Select 或者 Teleport Mode Activate,这两个动作都绑定了 Primary2DAxis,它表示的是摇杆的坐标位置(把摇杆的推动范围看作一个坐标系,不动摇杆的时候摇杆位于原点,推动摇杆后摇杆在 x 和 y 轴上的位置会发生偏移。

它们的区别可以看界面最右边的 Binding Properties

Teleport Select:

Teleport Mode Activate:

可以观察上面两幅图用红框标出的部分,最显著的区别是这个 Directions。
Teleport Select 是 Everything,意为摇杆推向任何方向都能激活传送。
Teleport Mode Activate 是 North,意为摇杆向北(向前)推才能激活传送。

因为我们想要通过向前推动摇杆来激活传送,所以我们将要选择的是 Teleport Mode Activate。然后,我们把 XR Controller 中的 Select Action 和 Select Action Value 都换成 Teleport Mode Activate

接下来可以运行程序,选中了传送区域后,向前推动摇杆,再松开摇杆,人物就能够传送了!😊


📕让传送的射线变成曲线

在大多数游戏中,传送时的射线是曲线,或者是贝塞尔曲线。我们可以更改 XR Ray Interactor 脚本的配置来实现这个需求。

我们可以选择 Projectile Curve 或者 Bezier Curve,它们都能实现曲线的效果,只不过底层原理会有些不同。我这边选择 Projectile Curve,然后将 Velocity 减小到了 8,Velocity 越大,曲线射到的距离就越远。感兴趣的小伙伴也可以调试一下曲线的其他数值以及贝塞尔曲线的各个数值,调出适合自己的曲线。

然后运行程序,可以看到射线变成了曲线:


📕在射线末端添加辅助瞄准区域

英文的说法是添加 Reticle,Reticle 有十字线、瞄准线的意思,大家可以联想瞄准镜的样子。

在大部分 VR 游戏中,传送的射线末端一般会有一个圆形的区域,也就是这个 Reticle,用于辅助瞄准,这样让传送看起来更直观。

那么我们就来简单地制作一个 Reticle。我创建一个 Cylinder 来表示 Reticle,然后更改缩放值,移除掉它的碰撞体

⭐法一:在 XR Interactor Line Visual 脚本上添加 Reticle

然后把这个游戏物体或者它的预制体拖到 XR Interactor Line Visual 的 Reticle 当中。注意,一个 Reticle 物体只能绑定一个 XR Interactor Line Visual 脚本,如果想让两只手的传送都具有 Reticle,需要把这个 Reticle 复制一份,然后将复制的 Reticle 拖给另一边手柄控制器的 XR Interactor Line Visual 脚本中。

因为大部分 VR 游戏是只用一边手柄来触发传送,所以我这里先规定左手负责传送,因此我把右手的传送功能关闭,只需把 XR Ray Interactor 脚本关闭就行了。那么我们来看一下实际的效果:

射线末端这个圆形的区域,就是我们自己添加的 Reticle。

⭐法二:在 Teleportation Area 或者 Teleportation Anchor 脚本上添加 Reticle

如图所示, 将我们准备好的 Reticle 赋到 Custom Reticle 处,最终呈现的效果和法一是一样的:


📕实现向前推动摇杆才能显示传送射线

大部分 VR 游戏中,只有向前推动手柄的摇杆,才会显示传送的射线,然后释放手柄的摇杆,传送射线消失。

但是目前为止我们的 Demo 是在程序一运行的时候就会显示传送的射线。不过因为射线是由 XR Ray Interactor 控制的,所以改进的思路其实也比较简单,就是我们可以自定义一个脚本,去控制 XR Ray Interacter 脚本的开启和关闭。因为射线的打开和关闭是实时检测的,所以我们应该每一帧去判断手柄是否触发了传送,以及一个传送的过程是否结束


2023.1.18更新:
此时 XR Interaction Toolkit 已经更新到了 2.3.0 pre 版本。经尝试,控制 XR Ray Interacter 脚本的开启和关闭的思路已经失效,即使能控制射线的打开和关闭,但是无法触发传送。因此,为了能向上兼容,我们换一种思路:去控制 XR Ray Interacter 脚本所在游戏物体的显示和隐藏


因为输入的动作是基于 Input System的,所以需要一些 Input System 方面的知识。
脚本代码如下:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.XR.Interaction.Toolkit;

public class TeleportationController : MonoBehaviour

    public InputActionProperty m_teleportModeActivate;
    public InputActionProperty m_teleportModeCancel;

    private InputAction teleportModeActivate;
    private InputAction teleportModeCancel;

    public XRRayInteractor teleportInteractor;

    
    void Start()
    
        teleportModeActivate = m_teleportModeActivate.action;
        teleportModeCancel = m_teleportModeCancel.action;
        EnableAction();
    

  

    private void OnDestroy()
    
        DisableAction();

    

    void Update()
    
        if (CanEnterTeleport())
        
            SetTeleportController(true);
            return;
        
        if (CanExitTeleport())
        
            SetTeleportController(false);
            return;
        
    

    private void SetTeleportController(bool isEnable)
    
        if (teleportInteractor != null)
        
            teleportInteractor.gameObject.SetActive(isEnable);
        
        
    
    private bool CanEnterTeleport()
    
        bool isTriggerTeleport = teleportModeActivate != null && teleportModeActivate.triggered;
        bool isCancelTeleport = teleportModeCancel != null && teleportModeCancel.triggered;
        return isTriggerTeleport && !isCancelTeleport; //判断是否触发传送且没有按下取消传送的键
    
    private bool CanExitTeleport()
    
        bool isCancelTeleport = teleportModeCancel != null && teleportModeCancel.triggered;
        bool isReleaseTeleport = teleportModeActivate != null && teleportModeActivate.phase == InputActionPhase.Waiting;
        return isCancelTeleport || isReleaseTeleport; //判断是否按下取消传送的键或者释放了之前推动的摇杆
    
    private void EnableAction()
    
        if (teleportModeActivate != null && teleportModeActivate.enabled)
        
            teleportModeActivate.Enable();
        
    
    private void DisableAction()
    
        if (teleportModeActivate != null && teleportModeActivate.enabled)
        
            teleportModeActivate.Disable();
        
    



脚本解释:

  • Input Action Asset 中有一个 Teleport Mode Cancel 的动作,绑定的是“按下 Grip 键”这个操作,也就是按下手柄的 Grip 键可以取消传送,我们也将这个功能加入脚本。

  • 触发传送(显示传送射线)的条件:向前推动手柄摇杆,并且没有按下 Grip 键
  • 不显示传送射线的条件:按下 Grip 键或者手柄的摇杆处于原始的位置(向前推动摇杆并且释放最终也是回到了原始位置)

因为是控制游戏物体的显隐,所以 Teleportation Controller 脚本和 XR Ray Interactor 脚本不能挂载到同一个物体上。否则游戏物体隐藏时无法调用 Teleportation Controller 的功能。

因此,我们稍微改变一下 XR Origin 游戏物体的层级结构:

首先在 LeftHand Controller 或者 RightHand Controller 下创建 Teleport Interactor 物体(这取决于你设定哪只手负责传送,我这边为了日后拓展,干脆先在两边上都添加上这个物体,实际上我设定的是左手负责传送),先把 Teleport Interactor 物体隐藏掉,它的显示与隐藏会由我们刚写的 Teleport Controller 脚本进行控制。然后将之前教程中与传送有关的脚本挪到 Teleport Interactor 游戏物体上。

接下来我们要在 LeftHand Controller 上添加两个脚本(因为我这边设定左手负责传送),如下图所示:

解释一下,就是把刚刚写的 Teleportation Controller 脚本挂载到 LeftHand Controller 上,将 Input Action Asset 里的 Teleport Mode Activate 和 Teleport Mode Cancel 赋上。把手部的模型作为 LeftHand Controller 的子物体。需要注意的是,手部模型的父物体必须要有 XR Controller (Action-based) 脚本,且要开启 Enable Input Tracking 。因为需要用到设备输入的游戏物体需要 XR Controller 脚本的支持,并且打开 Tracking 才能正确追踪手柄控制器的姿态,从而让作为子物体的手部模型能跟着手柄运动,所以我们需要给 LeftHand Controller 物体额外添加一个 XR Controller(Action-based)脚本,其中的动作配置先用默认的就行,与之前负责传送的 XR Controller 不同。

这个时候,LeftHand Controller 和 Teleport Interactor 都有 XR Controller (Action-based) 脚本,如果我们运行程序,会发现传送射线的起始点位置不对。这是因为父子物体上的 XR Controller 都开启了 Tracking 导致的冲突。因此如下图所示,我们需要点击子物体 Teleport Interactor 上的 XR Controller (Action-based),把 Enable Input Tracking 取消勾选。

而且以后在使用其他功能的时候,只要父子物体都有挂载 XR Controller (Action-based) 脚本,我们都需要关闭子物体 XR Controller (Action-based) 上的 Enable Input Tracking

注:我们在 Teleportation Controller 脚本中赋值的两个 Input Action Reference 作用仅仅是为了显示和隐藏射线,真正触发传送的还是 Teleport Interactor 物体上 XR Controller (Action-based) 中的 Select Action(我们之前已经设置好了)

然后我们可以运行一下程序,这时候,只有向前推动摇杆才会显示传送的射线。

但是,此刻需要关注一下选用了哪种方式在射线末端添加 Reticle。如果选用的是在 XR Interactor Line Visual 脚本上添加 Reticle,那么你会发现这个 Reticle 在没有触发传送的时候会固定在场景中的某个位置,而我们希望的是和传送射线一样,只有向前推动摇杆的时候显示 Reticle。

其中一种方式是修改脚本,获取 Reticle 的引用,然后在 SetTeleportController 方法中去控制 Reticle 的显示和隐藏。

不过,如果选用的是在 Teleportation Area 或者 Teleportation Anchor 脚本上添加 Reticle,无需修改脚本。只要把 Reticle 做成预制体赋给 Custom Reticle,在射线选中可传送区域后,就会在可传送区域的碰撞体表面生成 Reticle。

但是此时还有一个小问题,如果你照着这篇教程制作了 Teleportation Anchor,就会发现射到圆柱体表面的时候也会显示 Reticle

这看起来有一些奇怪,因为传送的 Reticle 一般是显示在传送区域的上表面。不过,这个圆柱体只是我在本篇教程的前半段演示用的。在实际的开发中, Teleportation Anchor 一般都是地面上的某一块区域,碰撞体也应该是高度非常小的一块,这个圆柱体也一般是透明的,类似于光柱的效果。因此在实际的项目中需要调试 Teleprtation Anchor 所检测的碰撞体的高度,然后 Reticle 能够显示在目标区域的上表面。

我这边将原来的圆柱体改成方体,因为方体的碰撞体更好调试一点。那么方体的上表面面积就代表了用于演示 Teleportation Anchor 的可传送区域。

最终效果:

📕美化传送射线的方法

如果你想美化传送射线,可以更改 XR Interactor Line Visual 和 Line Renderer 脚本的配置。

先来看 XR Interactor Line Visual 脚本:

常用的有调整射线宽度(Line Width),选中传送区域时射线的颜色(Valid Color Gradient),未选中传送区域时射线的颜色(Invalid Color Gradient)

而 Line Renderer 是 Unity 中原有的组件,每个设置具体是什么意思可以参考官方文档。不过在美化传送射线上经常用的是更改射线的材质,如果你想让射线的材质效果看起来更好看,可以自定义材质,然后更改下图中标出的这个部分:

那么本篇教程的内容就到此为止啦!大家快去制作属于自己的传送功能吧!🌹

Unity VR开发教程 OpenXR+XR Interaction Toolkit 手部动画

文章目录


往期回顾:
Unity VR开发教程 OpenXR+XR Interaction Toolkit (一) 安装和配置
上一篇教程我们成功安装和配置了 Unity OpenXR+XR Interaction Toolkit 开发 VR 的环境,最终将 VR 头显和电脑进行串流后,能通过头显看到 Unity 中的场景,并且头部、手柄的位移和转动也能准确定位。

但是因为我们没有添加手部的模型,所以手柄的位置是空荡荡的,如下图:

而 VR 世界里怎么能看不见自己的手呢?🤔 所以本篇教程就来介绍如何在我们的 VR 世界中添加手部模型,以及如何用手柄的按键来控制手部模型的动作。


📕教程说明

使用的 Unity 版本: 2020.3.36

使用的 VR 头显: Oculus Quest 2

教程使用的 XR Interaction Toolkit 版本:2.1.1(此教程尽量考虑了向上兼容,如果有过期的地方,欢迎大家指出)

前期的配置:见教程一

项目源码(持续更新):https://github.com/YY-nb/Unity_XRInteractionToolkit_Tutorial2022/tree/master

最终实现的手部动画:按下手柄的 Trigger 键让大拇指和食指做出捏合的动画,按下手柄的 Grip 键让整只手做出抓握的动画。了解原理后也可以自定义手部动画和手柄按键的对应关系,做出适合自己需求的手部动画。

效果图:

注:本篇博客的内容参考自油管大佬的这个视频 https://m.youtube.com/watch?v=8PCNNro7Rt0 ,相当于基于这个教程整理的一篇学习笔记。


📕第一步:导入手部模型

我使用的是 Oculus Hand Unity 资源包,里面自带了手部模型的动画。这里提供一个百度云盘的下载链接:

链接:https://pan.baidu.com/s/15Y03XzgMUf7TQO_060zWyg?pwd=1voo
提取码:1voo

还有一个 CSDN 资源的链接:

Oculus Hands VR手部模型Unity资源包 (含有动画)

接下来我会简单分析一下这个资源包,以及手部动画制作的思路。如果你用了其他模型,并且想制作自己的手部动画,那么也可以用类似的思路进行制作。

下载完毕后就是一个 Unity 的 Package,然后我们要在 Unity 中导入这个资源,点击菜单栏的
Assets -> Import Package -> Custom Package,导入成功后可以看到 Unity 中多出了这个文件夹:

注:这个资源包的模型需要用到 URP 渲染管线如果项目是普通的 3D 项目,需要将项目升级为 URP,升级方法可以自行搜索,网上也有很多相关的教程。

然后我们简单地看一下这个资源包, Prefabs 里就是左手和右手的模型:

关键是 Animations 文件夹,这个资源包已经帮我们做好了一些手部的动画,包含了 Animator Controller 和相应的 Animation:

然后我们打开左手的 Animator Controller 看一看,发现里面有一个动画混合树:

控制动画的 Parameter 包含了 float 类型的 Grip 和 Trigger,代表了按下手柄的 Grip 键和 Trigger 键。为什么是 float 类型呢?因为这两个手柄的按键会有一个按下的程度,按下的程度不同,手部的动画也会有所不同。比如轻微按下 Grip 键,手做出微微抓取的姿势;完全按下 Grip 键,手做出紧紧抓握的姿势。

然后打开 Blend Tree ,可以看到这是一个 2D 混合树:

这边就两个重要的动画,一个是捏合(hand_pinch_anim),一个是抓握(hand_first)。我们可以点击下图中的这些动画文件查看具体的动画:

我这边打开一个 l_hand_pinch_anim,具体面板见下图:

第二个关键帧的手部模型姿势如下:

因此上图就是完全按下 Trigger 键后手部应该有的姿势。因为这个资源包里的动画是只读的,所以我们没法对这些动画进行修改。如果对已有的手部动画不满意,或者导入的是其他不带动画的手部模型,我们可以模仿它的思路,自定义手部的姿势。感兴趣的小伙伴可以自己创建手部的 Animation ,在第二个关键帧去修改手部模型各个关节的旋转角度,如下图的这些节点:


那么这个 Oculus Hands 资源包的讲解差不多就到这。我们也可以模仿它的做法去自定义 Animator Controller 和 相应的手部动画。当然也可以不用 2D 混合树去混合动画,因为很多时候我们会用三个按键去控制手的动画(大部分 VR 应用是这样),那么这时候也可以用动画分层的思想,为每个按键分配一个动画层,然后将状态间的切换进行连线。


📕第二步:将手部模型拖入场景

首先依照本系列教程的第一篇配置好场景:

然后把左手和右手的模型分别作为 LeftHand Controller 和 RightHand Controller 的子物体:

在手的 Prefab 中加上对应的 Animator Controller:

这个时候我们可以试着运行一下程序,我们可以发现手部模型已经可以跟随手柄运动了,但是按下手柄的按键后手没有任何反应。因此,我们需要写个脚本去控制 Animator。


📕第三步:编写脚本控制手部动画(版本一:基于 Input System,推荐做法)

我们新建一个脚本,挂载到手部模型上,代码如下:

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

public class AnimateHandController : MonoBehaviour

    public InputActionProperty pinchActionProperty;
    public InputActionProperty gripActionProperty;
    private InputAction pinchAction;
    private InputAction gripAction;
    private Animator animator;
    // Start is called before the first frame update
    void Start()
    
        pinchAction = pinchActionProperty.action;
        gripAction = gripActionProperty.action;
        animator = GetComponent<Animator>();
    

    // Update is called once per frame
    void Update()
    
        float triggerValue = pinchAction.ReadValue<float>();
        animator.SetFloat("Trigger", triggerValue);

        float gripValue = gripAction.ReadValue<float>();
        animator.SetFloat("Grip", gripValue);
    


注意点:

1)需要 using UnityEngine.InputSystem,因为我们按键绑定的动作已经在默认 Input Action Asset 中配置好了(这个东西是在配置环境的时候创建的),所以会用到 InputSystem 的一些知识。

2)我们需要将 Input Action Asset 中的动作和我们创建的这个脚本进行绑定,分别需要按下 Trigger 键的动作和按下 Grip 键的动作。这里我使用了 InputActionProperty 这个类,既可以引用 Input Action Asset 中已经存在的 Action,也可以自己新建 Action,Inspector 面板里显示如下:

我们直接用已有的 Action,所以勾选 Use Reference:

那么我们需要将什么东西赋给这些 Reference 呢?我们可以打开默认的 Input Action Asset(如下):

这里以左手为例(见下图):

找到 XRI LeftHand Interaction 下的 Select Value 和 Activate Value,可以看到 Select Value 代表了 grip 按键, Activate Value 代表了 trigger 按键。那为什么是选 Value 呢?这和 Animator Controller 中的 Parameter 是一样的道理,我们需要根据按键按下的程度,来决定动画播放的程序。

那么接下来就是找到我们想要的 InputActionReference,右手也是同理。


3)读取手部按键按下的程度使用 InputAction 类的 ReadValue<T>() 这个方法,而使用 InputActionProperty 类中的 action 属性可以获取它的 InputAtion。所以现在,你应该能够看懂上面的这一段代码了吧。

接下来,我们可以运行一下程序,正常来说,这时候手的动画能够成功运行了。但实际上,你应该会感觉此时的效果离我们想象中的还有一些差距,所以我们还需要做一些微调。

📕第三步:编写脚本控制手部动画(版本二:基于 XR Input Subsystem)

版本一的脚本运用了 Input System 的知识。而 XR Input Subsytem 是 Unity 提供的另一种用于处理设备输入的系统,需要运用到 UnityEngine.XR 这个命名空间,以下是基于 XR Input Subsystem 实现的手部动画控制代码:

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

public class HandPresence : MonoBehaviour

    public InputDeviceCharacteristics controllerCharacteristics;    
    private InputDevice targetDevice;
    private Animator handAnimator;
    
    private void Start()
    
        handAnimator = GetComponent<Animator>();
        TryInitialize();
    


    private void TryInitialize()
    
        List<InputDevice> devices = new List<InputDevice>();

        InputDevices.GetDevicesWithCharacteristics(controllerCharacteristics, devices);
        if (devices.Count > 0)
        
            targetDevice = devices[0];
        
    

    private void UpdateHandAnimation()
    
        if (targetDevice.TryGetFeatureValue(CommonUsages.trigger, out float triggerValue))
        
            handAnimator.SetFloat("Trigger", triggerValue);
        
        else
        
            handAnimator.SetFloat("Trigger", 0);
        

        if (targetDevice.TryGetFeatureValue(CommonUsages.grip, out float gripValue))
        
            handAnimator.SetFloat("Grip", gripValue);
        
        else
        
            handAnimator.SetFloat("Grip", 0);
        
    

    private void Update()
    
        if(!targetDevice.isValid)
        
            TryInitialize();
        
        else
        
            UpdateHandAnimation();
        
    


Unity 的 XR 架构提供了不同厂商的硬件设备和输入之间的映射。虽然不同设备的设计会有些不同,但是可以检测相同的输入的动作,比如不同的手柄都能触发 “trigger”,“grip” 等动作。因此 Unity 只要检测某个输入动作是否发生,就能将这个输入动作映射到不同硬件设备的按键上,比如我按下了 Oculus 手柄的 Grip 按键,就代表输入了 “grip” 这个动作;再比如我转动 Oculus 手柄的摇杆(JoyStick)或者转动 Vive 手柄上的那个圆形触摸板(Touchpad),都可以用 “primary2DAxis” 来表示,得到的是一个 Vector2 类型的坐标,代表摇杆或触摸板位移的程度,这经常用于人物的移动当中。这样做能消除不同硬件设备的差异,和 Input System,OpenXR 的思想是类似的。

下图是 Unity 官方文档的不同厂商设备按键与动作输入的映射表,这些是 Unity 目前支持的设备:

上图中第一列的 InputFeatureUsage 类型的变量都存储在了 CommonUsages 这个静态类中,如图:

那怎么去使用这些变量呢?我们还需要了解一下其他相关的类。

Unity 的 XR 系统用 InputDevice 这个类来表示用于检测输入的硬件设备,用 InputDeviceCharacteristics 这个类来表示硬件设备的一些特征。我们回看版本二的脚本代码,在开头声明了 public InputDeviceCharacteristics controllerCharacteristics; 因此,我们可以到 Unity 编辑器的 Inspector 面板中看看这是什么东西。在此之前,我们要把这个脚本分别挂载到左手和右手的模型 Prefab 上,如图:

可以看到这边有个类似选项框的东西,有很多选项可以选择(可多选):

这些选项就包含了输入设备的一些特征。因为我们要检测手柄的输入,所以左手柄选择 Controller 和 Left,右手柄选择 Controller 和 Right。

约束了特征之后,就能够借助 InputDevices.GetDevicesWithCharacteristics 这个方法筛选出想要检测输入的设备列表。因为一个手部动画的脚本对应一个手柄输入的检测,所以返回列表的第一个元素就能得到对应的 InputDevice 类型的手柄。

然后用 InputDevice 类下的 TryGetFeatureValue 方法就能获取按键输入的值:

targetDevice.TryGetFeatureValue(CommonUsages.trigger, out float triggerValue)
targetDevice.TryGetFeatureValue(CommonUsages.grip, out float gripValue)

接下来的思路就和版本一的代码差不多了,读取到了按键的输入,我们就可以将输入的值用于 Animator。此外,最好在 Update 里判断一下当前的设备是否还有效,如果失去检测了,需要重新初始化检测的设备。

private void Update()
    
        if(!targetDevice.isValid)
        
            TryInitialize();
        
        else
        
            UpdateHandAnimation();
        
    

以上便是版本二的代码讲解,大家可以从两个版本中选择一个自己喜欢的作为手部动画的控制脚本。基于 XR Input Subsystem 的版本在 Inspector 面板中的操作更为方便,而基于 Input System 的版本在动作与输入的绑定上更加灵活。因为 XR Input Subsystem 中的动作与设备的按键是绑死了的,而 Input System 可以自定义动作与哪个按键进行绑定,比如我想按下手柄的 Trigger 键来进行抓握,可以直接在 Input Action Asset 里进行更改。但是 XR Input Subsystem 只允许按下手柄的 Grip 键来进行抓握。


📕第四步:调整场景

这时候,可能会存在这么几个问题,那么我们就来一个一个解决它们。

问题一:
手部模型的旋转角度看起来不大对。模型默认是手背朝上,但是这和现实中我们拿着手柄的角度是不同的,所以这时候的手感是比较奇怪的。

解决方法:
调整手部模型的旋转角度,我这里把左手的 Z Rotation 设为 90,把右手的 Z Rotation 设为 -90。这个时候,模型的角度如下,可以看到手的侧面朝上,这与现实中拿手柄的角度是接近的。

问题二:
VR 里手的位置会和现实世界的手有些偏移,造成使用手感较为奇怪。

解决方法:
调整手部模型的 position。我这边调的 position 如下,大家可以用来参考,测试手部模型和现实中手部的位置的偏移程度。

问题三:
手的阴影显示不正常。看到的是奇怪的形状,而不是手的阴影。

解决方法:
找到 URP 的配置文件,调整 Shadows -> Cascade Count 的值,如图所示:

我这里把值调到了 4,此时阴影显示就稍微正常了些。如果想要影子的效果更精细一点,可以试试调整 Shadows 下的其他数值。


这时候,你也许会疑惑为什么我的手没有发出射线。其实我先把它关闭了,大家可以分别在 LeftHand Controller 和 RightHand Controller 游戏物体找到 XR Ray Interactor 这个组件,然后将它关闭。


那么现在,咱们的 Demo 就大功告成了!快去试一试用手柄控制虚拟世界中的双手吧!😊

以上是关于Unity VR开发教程 OpenXR+XR Interaction Toolkit 传送的主要内容,如果未能解决你的问题,请参考以下文章

Unity VR开发教程 OpenXR+XR Interaction Toolkit (六)手与物品交互(触摸抓取)

Unity VR开发教程 OpenXR+XR Interaction Toolkit 手部动画

Unity 使用OpenXR和XR Interaction Toolkit 开发 HTCVive(Vive Cosmos)

Unity开发OpenXR | 使用 OpenXR 制作一款简单VR示例场景 的全过程详细教程,包含两个实战案例。

Unity开发OpenXR |使用 OpenXR 添加一个运动系统,实现传送抓取功能 的简单VR示例场景 的全过程详细教程

Unity开发OpenXR | 使用 OpenXR 制作一款简单VR示例场景 的全过程详细教程,包含两个实战案例。