但是因为我们没有添加手部的模型,所以手柄的位置是空荡荡的,如下图:
而 VR 世界里怎么能看不见自己的手呢? 所以本篇教程就来介绍如何在我们的 VR 世界中添加手部模型,以及如何用手柄的按键来控制手部模型的动作。
使用的 Unity 版本: 2020.3.36
使用的 VR 头显: Oculus Quest 2
前期的配置:见教程一
项目源码(持续更新):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。
我们新建一个脚本,挂载到手部模型上,代码如下:
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
接下来,我们可以运行一下程序,正常来说,这时候手的动画能够成功运行了。但实际上,你应该会感觉此时的效果离我们想象中的还有一些差距,所以我们还需要做一些微调。
版本一的脚本运用了 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 就大功告成了!快去试一试用手柄控制虚拟世界中的双手吧!