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 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 VR开发教程 OpenXR+XR Interaction Toolkit (三) 转向和移动
Unity VR开发教程 OpenXR+XR Interaction Toolkit (四) 传送
Unity VR开发教程 OpenXR+XR Interaction Toolkit (五) UI

交互一般需要两个对象:一个是可交互的对象(Interactable),一个是发起交互的对象(Interactor,一般是玩家自己)。本系列教程中的传送功能也是交互的一种方式,可传送的地面是可交互的对象,手部发出的传送射线是发起交互的对象。而这篇教程将要介绍的是如何在 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

最终实现的效果:




📕VR 交互的类型

VR 中的交互一般分为三种类型:

  • Hover (悬停) :一般指的是发起交互的对象停留在可交互对象的交互区域。以手与物品交互为例,假设物品表面为可交互区域,当手触摸到物品(悬停在物品的可交互区域),则视为触发了 Hover。
  • Grab(抓取):这个概念好理解,就是把物品抓起来。
  • Use(使用):“使用” 是基于 “抓取” 的。有时候我们可以继续使用正在抓取的物体,触发它的一些特性。比如抓取一把枪视为 Grab,然后按下枪的扳机发射子弹则视为 Use。

接下来我会详细讲解这三种类型的用法。那么首先我们需要拥有可交互对象和发起交互的对象,让交互的条件成立,然后再具体实现交互的类型。


📕发起交互的对象(Interactor)

⭐XR Direct Interactor 脚本

因为本教程场景中的 XR Origin 沿用了上一篇 UI 教程中的游戏物体,所以我们先回顾一下 XR Origin 目前的结构:

类似的,我们很容易想到在 LeftHand Controller 和 RightHand Controller 下创建子物体,然后添加 XR Controller (Action-based) 脚本(关闭 Enable Input Tracking)和专门负责抓取的脚本。

为了让双手成为发起交互的对象,我们需要用到 XR Direct Interactor 脚本。

但是,如果把子物体上 XR Controller 的 Tracking 关闭,同个物体上的 XR Direct Interactor 将失去作用。这是因为抓取的时候也是需要判断手部的姿态,然而关闭了追踪与其产生了矛盾。

因为作为父物体的 LeftHand/RightHand Controller 上的 XR Controller 开启了追踪,所以我们只能将 XR Direct Interactor 挂载到父物体上。

XR Interaction Toolkit 2.3.0 及以上版本可以采用的做法:
2.3.0 版本也可以用上面介绍的方法,不过它新增了一个 XR Interaction Group 脚本(https://docs.unity3d.com/Packages/com.unity.xr.interaction.toolkit@2.3/manual/xr-interaction-group.html),可以使我们 XR Origin 的层级看起来更有条理。使用方法也可以参考开发包中的样例场景,我这边再简单说明一下。

如果要使用 XR Interaction Group,我们就可以在 LeftHand Controller 和 RightHand Controller 下分别创建 Direct Interactor 子物体,将 XR Direct Interactor 和 Sphere Collider 移到这个子物体上,由 Direct Interactor 物体单独处理手部的交互,层级看起来更加清晰:

我们在作为父物体的 LeftHand Controller 和 RightHand Controller 上分别添加 XR Interaction Group 脚本,然后在 Starting Group Members 添加 Interactor:

这就相当于把原来与发起交互有关的两个组件从父物体挪到了子物体上,由父物体的 XR Interaction Group 统一控制子物体上的 Interactor。可能有的小伙伴会有疑问:为什么这时子物体无需添加 XR Controller 呢?

因为父物体的 XR Controller 绑定的 InputAction 和手部发起交互的 Input Action 是一样的。并且父物体的 XR Interaction Group 充当了 Interactor 的管理者,让原本父物体上的 XR Controller 统一控制组内的 Interactor。有了 XR Interaction Group 脚本,我们就可以用一个物体的 XR Controller 集中控制多个物体的 Interactor。

不过像传送这种 XR Controller 绑定的 Input Action 和父物体略微有些不同的情况,就单独在子物体上添加 XR Controller(Action-based),并且关闭 Enable Input Tracking,让它不受 XR Interaction Group 管理。

⭐添加可交互区域

XR Direct Interactor 需要一个 Trigger 类型的碰撞体,作为可交互的区域,并且这个碰撞体需要和 XR Direct Interactor 挂载到同一个游戏物体上。因此我这边直接在 LeftHand Controller 和 RightHand Controller 上分别添加一个 Sphere Collider,调整 Radius,并且把 Is Trigger 勾选。那么当可交互对象进入这个可交互区域后,就可以进行交互。


📕可交互的对象(Interactable)

首先我们创建一个可交互的物品,我这边用红色方块来表示。

⭐添加刚体

因为手与物品的交互基本上是基于物理效果的,所以我们要为可交互的物体添加刚体。

⭐XR Simple Interactable 脚本

最简单的可交互脚本就是 XR Simple Interactable,但是它没有自带抓取的功能。

我们在方块上添加 XR Simple Interactable 脚本:

虽然 XR Simple Interactable 的官方文档没有说明需要添加刚体,但是实测后发现物体有了刚体后,XR Simple Interactable 脚本才会生效

另外要注意的是,挂载可交互脚本的游戏物体还需要一个碰撞体。游戏运行后,这个碰撞体会自动赋给 XR Simple Interactable 的 Colliders 数组。

⭐Interactable Events

为了让 XR Simple Interactable 的效果可视化,我们可以添加几个功能:
1)当手触碰到方块时,方块的颜色发生改变。而这个功能也是 VR 交互方式中很常见的 Hover,即悬停在物品的可交互区域。
2)触碰到方块后,按下手柄 Grip 键,方块的颜色变成蓝色。
3)触碰到方块后,按住手柄的 Grip 键,再按下手柄的 Trigger 键,方块的颜色变回红色。

以上的三个功能也分别模拟了 VR 中常见的三种交互方式:悬停(Hover)、抓取(Grab)、使用(Use)。但是因为 XR Simple Interactable 脚本的局限性,我们不能真正将物品抓起来,而只能模拟 “抓取” 发生的事件。

这时候就要用到 XR Simple Interactable 脚本中的 Interactable Events,里面包含了交互时会发生的一些事件,可以看到里面有一个 Hover Entered,也就是开始悬停在可交互区域触发的事件。然后我们可以手动设置更改方块的材质,我这边想让方块被触碰后变成黄色。

(也许有小伙伴会注意到 First Hover Entered。它的作用是只能某个手占用了相应的事件,比如一只手 Hover 的时候,另一个手过来 Hover 就不会触发这个事件。后面我们会看到的 First Select Entered 也是类似的道理)

然后,我们要实现的第二个和第三个功能分别对应了 Select Entered 和 Activated 这两个事件。同样地,我们在 Inspector 面板中手动绑定事件:

看到 Select 和 Activate,是不是觉得有些眼熟呢?我们打开 XR Interaction Toolkit 中自带的输入配置文件 XRI Default Input Actions:

可以看到 XRI LeftHand Interaction 或者 XRI RightHand Interaction 下就有 Select 和 Activate。Select 动作绑定的是 “按下 Grip 键” 这个操作,Activate 动作绑定的是 “按下 Trigger 键” 这个操作。

而 Interaction Events 中的 Select 和 Activate 使用的就是 XRI Default Input Actions 配置文件中的这两个动作,并且要注意的是:Activate 动作必须要以 Select 动作的发生为前提,Interactable Events 中的所有事件都是以“与可交互对象发生了交互”为前提。因此,当我们的手触碰到方块后按下手柄的 Grip 键,视为发生了 Select 动作,触发了 “方块的颜色变成蓝色” 这个事件;在手触碰到方块后按下手柄的 Grip 键的前提下,继续按下 Trigger 键,视为发生了 Activate 动作,触发了 “方块的颜色变成红色” 这个事件。

这时候,也许有人会有疑问:在上一篇 UI Demo 中,我们也有用到 Activate 这个动作,作用是按下 Trigger 键与 UI 进行交互。虽然 XR Controller 中的 Select Action 绑定的是 Select 这个动作,正常来说得先按下Grip 键才能触发 Select 动作。可是实际上当 UI 射线射到 UI 上时,射线的颜色变成了白色,说明此时已经进入了选中的状态。我们为什么不需要先按下 Grip 键呢?

这是因为 Canvas 上的 Tracked Device Graphic Raycaster 脚本的特性。这个脚本能让 UI 被射线响应,当射线射到 UI 上时,自动进入选中的状态,也就是触发了 Select 动作。然后在 Select 动作发生的前提下,我们按下 Trigger 键就能与 UI 进行交互。因此 UI Demo 中的 Activate 也是以 Select 为前提。


⭐XR Grab Interactable 脚本

给可交互的物体添加上这个脚本,就能实现真正的抓取。因为抓取对应的是 XRI Default Input Actions 配置文件中的 Select 动作,而 Select 动作绑定的是 “按下手柄的 Grip 键” 这个操作,所以当手部靠近可交互物体时,按下手柄的 Grip 键就能抓取物体。

注:要想使用 XR Grab Interactable 脚本,必须给物体添加刚体组件。不过即使之前没有刚体,添加 XR Grab Interactable 脚本也会自动给物体加上刚体。

⚡Movement Type(Instantaneous, Kinematic,Velocity Tracking )

在 XR Grab Interactable 脚本中,比较重要的是三种 Movement Type:Instantaneous, Kinematic 和 Velocity Tracking

为了以示区分,我这边创建了三个挂载 XR Grab Interactable 脚本的方块,红色方块对应 Instantaneous,黑色方块对应 Kinematic,绿色方块对应 Velocity Tracking


Instantaneous:
物体的移动位置和姿态完全跟随了 Interactor(手)的移动。它是通过在每一帧更新 Position 和 Rotation 进行移动,所以看上去物体跟随手部的移动是几乎没有延迟的。但是这种移动方式没有运用物理刚体的效果(即使物体上有刚体)。此时抓取的物体会穿过带有碰撞体的桌子,并且和其他刚体方块的碰撞效果也不是基于物理的。

Kinematic:
通过 Kinematic Rigidbody(运动刚体)进行移动,跟随手部移动的过程中会有一些延迟。移动过程中物体不受力和碰撞的作用。所以此时物体触碰其他碰撞体不会受到反作用力,比如物体还是能穿过带有碰撞体的桌子。但是可以对其他刚体(Kinematic Rigidbody 除外)施加物理效果,比如此时移动的 Kinematic 方块能够推动其他放置在桌子上的方块。

Velocity Tracking:
通过设置刚体的速度和角速度进行移动。跟随手部移动的过程中会有一些延迟。移动过程中带有刚体的物理效果,比如会和带有碰撞体的桌子发生碰撞,也可以对其他刚体产生力的效果。

⚡Attach Transform 抓取点

XR Grab Interactable 脚本中有一个 Attach Transform 变量可以赋值,作为物体的抓取点。如果没有赋值,将默认以物体的 position 作为抓取点。

这个变量有时候很有用。比如抓取一把枪,那么物品的抓取点应该位于枪柄上。
现在我们造一把简易的枪,来看看 Attach Transform 怎么使用,当然大家也可以用自己的模型资源。

然后添加碰撞体,刚体以及 XR Grab Interactable 脚本。这里我先把 Movement Type 设为 Instantaneous。此时如果运行程序,会发现抓取枪的抓取点不是我们想要的样子。

这时候,Attach Transform 就派上了用场。我们可以在枪的游戏物体下创建一个子物体,叫做 Attach Point。


然后将 Attach Point 赋给 Attach Transform:

接下来我们运行程序,动态调整 Attach Point 的 Position 和 Rotation,直到手能握住枪柄,复制 Attach Point 的 Transform组件,退出程序后将其粘贴至 Attach Point 原来的 Transform 组件。



现在,抓取点的效果看起来稍微好了一点,大家也可以自行优化。但是这里还有一个问题,刚刚的 Attach Point 对应的是右手的抓取点,如果我用左手抓取枪,会发现 Attach Point 的位置是不对的。具体的解决方法我会放在下一个部分的 “优化一:左右手抓取” 进行详细说明。

值得注意的是,仅设置 Attach Transform 只是粗略地抓取。因为它在物理效果上显得不是很真实,可以看到物体和手之间还是有穿模的现象。如果要实现精细地抓取,会稍微麻烦一点,今后也会出相应的教程进行详细说明。

关于 XR Grab Interactable 脚本的其他变量设置,大家可以参考官方文档:
https://docs.unity3d.com/Packages/com.unity.xr.interaction.toolkit@2.1/manual/xr-grab-interactable.html

⚡代码实现 Use 功能(制作简易手枪)

接着上面枪的例子,我们要怎么实现抓起枪后按下手柄 Trigger 键进行射击呢?这个功能实际上就是 VR 交互中的 Use 功能。

联想 XR Simple Interactable 和 Interactable Events 的部分,因为 XR Grab Interactable 脚本中也有一模一样的 Interactable Events,所以我们可以在 Inspector 面板中绑定 Activate 触发时的事件。不过,现在我想展示如何用代码来进行 Interactable Events 事件的绑定。

🔍核心脚本

我们创建一个脚本,叫做 GunController,把它挂载到枪的游戏物体上。

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

public class GunController : MonoBehaviour

    public GameObject bullet;
    public Transform spawnPoint;
    public float fireSpeed = 40;

    void Start()
    
        XRGrabInteractable grabbable = GetComponent<XRGrabInteractable>();
        grabbable.activated.AddListener(FireBullet);
    

    private void FireBullet(ActivateEventArgs arg)
    
        GameObject spawnBullet = Instantiate(bullet,spawnPoint.position,spawnPoint.rotation);
        spawnBullet.GetComponent<Rigidbody>().velocity = spawnPoint.forward * fireSpeed;
        Destroy(spawnBullet,5);
    

其中最重要的部分就是获取 XR Grab Interactable 中的 activated 事件,然后通过 AddListener 绑定事件触发的函数

🔍制作子弹(碰撞检测方式设为 Continous Dynamic)

然后我们可以创建一个子弹的 Prefab(需要刚体和碰撞体):


这里有个小坑需要注意一下,就是我们最好要把子弹 Rigidbody 的 Collision Detection 设为 Continous Dynamic,否则因为子弹是高速运动的,有时候会检测不到和刚体的碰撞,造成子弹直接从物体中间穿过去。

🔍制作子弹发射位置

再创建一个枪的子物体,叫做 Spawn Point,作为子弹生成的位置。该物体的 z 轴箭头方向对应子弹发射的方向。


然后把子弹和 Spawn Point 赋给 Gun Controller:

这时候,枪的功能就制作好了。我们试着运行程序:

大功告成!😊

🔍优化一:左右手抓取(判断哪只手与物体交互)

到目前为止,我们还有一个问题没有被解决,就是枪的 Attach Transform 的为止只适用于右手的抓取。我们希望左右手的抓取点都是正确的,但是枪的 XR Grab Interactable 脚本只能有一个 Attach Transform。那么我们其实可以动态地去切换左右手对应的 Attach Transform

首先,我们要准备好左右手对应的 Attach Point,作为切换用的 Attach Transform:

然后,我们需要修改之前写的 GunController 脚本 ( 方法一)

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

public class GunController : MonoBehaviour

    public GameObject bullet;
    public Transform spawnPoint;
    public float fireSpeed = 40;

    private Transform leftHandAttachPoint;
    private Transform rightHandAttachPoint;
    private XRGrabInteractable grabbable;

    void Start()
    
        leftHandAttachPoint = transform.Find("LeftHand Attach Point");
        rightHandAttachPoint = transform.Find("RightHand Attach Point");
        grabbable = GetComponent<XRGrabInteractable>();
        grabbable.selectEntered.AddListener(ChangeAttachTransform);
        grabbable.activated.AddListener(FireBullet);
    

    private void ChangeAttachTransform(SelectEnterEventArgs arg)
    
        Transform interactor = arg.interactorObject.transform; 
        if (interactor.name == "LeftHand Controller")
        
            grabbable.attachTransform = leftHandAttachPoint;
        
        else if (interactor.name == "RightHand Controller")
        
            grabbable.attachTransform = rightHandAttachPoint;
        
    

    private void FireBullet(ActivateEventArgs arg)
    
        GameObject spawnBullet = Instantiate(bullet,spawnPoint.position,spawnPoint.rotation); 
        spawnBullet.GetComponent<Rigidbody>().velocity = spawnPoint.forward * fireSpeed;
        Destroy(spawnBullet,5);
    


核心思想就是给 XR Grab Interactable 的 Select Entered 事件绑定事件触发的函数,通过判断是哪一个 Interactor 来决定切换成哪一个 Attach Transform。

刚刚我们将左右手切换抓取的功能写在了枪的控制器中,但是这样代码的耦合性可能会比较高,因为除了枪,可能还会有其他的物体可以用左右手切换抓取。如果想要让左右手抓取的脚本更为通用,我们可以新建一个脚本继承 XR Grab Interactable (方法二):

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

public class XRGrabInteractableTwoAttach : XRGrabInteractable

    public Transform leftAttachTransform;
    public Transform rightAttachTransform;
    protected override void OnSelectEntered(SelectEnterEventArgs args)
    

        if(args.interactorObject.transform.CompareTag("Left Hand"))
        
            attachTransform = leftAttachTransform;
        
        else if(args.interactorObject.transform.CompareTag("Right Hand"))
        
            attachTransform = rightAttachTransform;
        
        base.OnSelectEntered(args);
    

核心思想类似,我们是重写了 XR Grab Interactable 当中的 OnSelectEntered 方法,它会在 Select Entered 事件触发时被调用。

然后,我们把 Gun 上的 XR Grab Interactable 脚本替换成 XRGrabInteractableTwoAttach,而之前 GunController 当中和左右手抓取相关的代码就可以删除了。

另外,我这边将左右手 Attach Transform 的赋值改为了拖动赋值,并且用 Tag 来判断是哪一个 Interactor,所以不要忘了在编辑器中进行赋值,并且给 LeftHand Controller 和 RightHand Controller 加上 Tag 哦。



最终效果:

🔍优化二:防止传送射线误触发远距离抓取(Interaction Layer Mask)

如果大家的项目中沿用了传送的功能,那么会发现一个 BUG:当向前推动手柄摇杆发射传送射线的时候,如果射线刚好射到可交互的物品上,这个物品会 “附着”在射线上,跟随射线移动,效果就和抓取一样,我们称这种现象为远距离抓取,它是由射线来控制抓取,这一部分我会在下一篇教程中进行详细说明。

因为 XR Grab Interactable 的抓取对应的是 Select 这个动作,而激活传送射线也是对应 Select ,所以此时我们是误打误撞触发了远距离抓取,因此我们要想办法让传送射线不触发远距离抓取。实现方式也很简单,我们需要对之前传送的相关组件进行一些设置。

首先我们找到负责发射传送射线的 XR Ray Interactor 脚本,将关注点放到 Interaction Layer Mask 上。

Interaction Layer Mask 相当于交互的过滤器,表示交互的层级。当发起交互的对象(Interactor) 和可交互的对象(Interactable)的 Interaction Layer Mask 至少有一个是相同的,那么它们是可以被交互的。

XR Ray Interactor 作为 Interactor,它的层级是 Everything。然后我们找到作为 Interactable 的 Teleport Area,可以看到它的层级是 Default。


因为 Default 包含在 Everything 中,所以满足 Interactor 和 Interactable 的 Interaction Layer Mask 至少有一个是相同的条件,即二者可以交互。

我们再看手与物品交互过程中的 XR Direct Interactor 和 XR Grab Interactable,它们的层级也分别是 Everything 和 Default。所以物品的 Default 也包含在传送射线的 Everything 中,传送的射线是能够与物品进行交互的。

那么我们可以单独为传送设置一个层级。我们点击 XR Ray Interactor 的 Interaction Layer Mask,再选择 Add Layer,我们新建一个 Teleport 层级:

然后将与传送有关的 XR Ray Interactor 和 Teleport Area 的 Interaction Layer Mask 改成 Teleport:

然后把 XR Direct Interactor 的 Interaction Layer Mask 改为 Default,这样用于抓取的 Interactor 和 Interactable 的层级是相同的,不会对其他的交互方式产生干扰。当然,我们也可以为抓取单独设立一个 Interaction Layer Mask。重要的是搞清楚 Interaction Layer Mask 的作用。

现在再次运行程序,这个 BUG 就消失啦!此时传送射线就只能和 Teleport Area 进行交互。😊

⚡其他功能一:将与物体接触的地方作为抓取点(Dynamic Attach)

我们之前介绍的抓取功能通过设置 Attach Transform,确定了物体被抓取后的位置与朝向。可以发现无论从物体上的哪个部位进行抓取,被抓取后的姿态都是相同的。

有时候,我们并不想要这种抓取方式,而是希望手能直接抓在抓取的部位上。XR Grab Interactable 脚本也提供了这个功能。

现在,我创建一个细长的 Cube,作为该功能的测试物体。


添加碰撞体,刚体。然后添加 XR Grab Interactable 脚本,勾选其中的 Use Dynamic Attach

勾选后会自动跳出 Match Position (匹配抓取时的 Position),Match Rotation(匹配抓取时的 Rotation),Snap To Collider(抓在物体的碰撞体上)。这些选项可以根据需求进行更改。

效果:


⭐XR Tint Interactable Visual 脚本

这个脚本可以挂载到可交互对象上,当发起交互的对象(Interactor)悬停(Hover)或者选中(Select 动作触发,对应 Input System 中设置的 Select 映射关系,一般和 “按下手柄 Grip 键” 进行绑定)可交互的对象时,能够暂时改变可交互对象的颜色。

调整 Tint Color 能够设置想要改变的颜色;勾选 Tint On Hover 能够在 Hover 的时候改变颜色;勾选 Tint On Selection 能够在 Select 的时候改变颜色。


以上是关于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示例场景 的全过程详细教程,包含两个实战案例。