Unity VR 开发教程 OpenXR+XR Interaction Toolkit(九)根据不同物体匹配对应的抓取手势

news2025/1/18 20:14:16

文章目录

  • 📕教程说明
  • 📕前置准备
  • 📕HandData 脚本存储手部数据
  • 📕制作预设手势
  • 📕手势匹配脚本 GrabHandPose
    • ⭐完整代码
    • ⭐需要保存的数据
    • ⭐得知什么时候开始抓取和取消抓取
    • ⭐将手势数据赋予手部模型
    • ⭐平滑变化手势
    • ⭐开始抓取和取消抓取触发的事件函数
  • 📕镜像手势
  • 📕编辑器面板关键物体一览

在这里插入图片描述

往期回顾:
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
Unity VR 开发教程 OpenXR+XR Interaction Toolkit (六)手与物品交互(触摸、抓取)
Unity VR 开发教程 OpenXR+XR Interaction Toolkit(七)射线抓取
Unity VR 开发教程 OpenXR+XR Interaction Toolkit(八)手指触控 Poke Interaction

往期教程中,我们学习了 VR 中的抓取功能,当时抓取的姿势仅仅是使用了简单的动画,和按下手柄 Grip 键触发的手部动画是一样的。但是如果想要提升游戏的沉浸感,抓取不同形状、不同大小的物体应该具有不同的抓取姿势,比如抓一个球和抓一根棍子可能会用不同的姿势。那么这篇教程,我将介绍如何实现抓取一个物体时手部呈现出与之匹配的抓取姿势。

其中一种思路就是对手部模型的 Animation Controller 进行修改。需要准备好手部抓取的动画和触发条件,当手部准备抓取的时候,禁用原来根据手柄输入改变手部姿势的动画控制器,然后通过判断是什么物体,切换到与之匹配的抓取动画。但是这种方法会导致随着抓取的姿势越来越多,Animation Controller 的动画状态会越来越多,而且代码中也要添加对物体类型的判断,导致可扩展性较差。

那么这篇教程提供另外一种思路,就是我们提前设置好抓取一个物体需要的姿势,即调整手部模型各个关节的位置和旋转角度,我们称它为预设手势。然后当手部准备抓取的时候,禁用原来根据手柄输入改变手部姿势的动画控制器,并将手部各关节的位置和旋转角度设置成和预设手势一样,这样我们的手部就会呈现出抓取该物体所匹配的姿势,然后松开物体时还原成默认的手势,并开启手部动画控制器,让手势由动画控制。简单来说,为了根据不同物体匹配不同的抓取姿势,我们只需记录好抓取姿势每个关节的位置和旋转角度,然后在实际抓取的时候将记录好的数据赋予我们的手部模型。使用这种方法,随着抓取的姿势越来越多,我们无需对脚本进行更改,只需要制作不同的预设手势,在可拓展性上会好一些。


📕教程说明

使用的 Unity 版本: 2021.3.5

使用的 VR 头显: Oculus Quest 2

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

项目源码(持续更新):https://github.com/YY-nb/Unity_XRInteractionToolkit2.3.2_Demo

前期的配置:环境配置参考教程一,手部模型参考教程二,手部模型的动画使用这篇教程(Unity VR 开发教程 OpenXR+XR Interaction Toolkit 番外(一)用 Grip 键, Trigger 键和摇杆控制手部动画)中的配置,也就是当玩家按下手柄的 Trigger 键时,手部会呈现出食指向前指的姿态,以便对其他物体进行触控交互。本篇教程的场景基于上一篇教程搭建的场景进行延伸,也就是沿用了之前教程里所配置的移动、抓取、用射线与 UI 进行交互,手指触控等功能。

本篇教程参考自油管教程:https://www.youtube.com/watch?v=JdspLj4fZlI 和 https://www.youtube.com/watch?v=TW3eAJqWCDU,加入了个人的理解。

最终实现的效果:

在这里插入图片描述


📕前置准备

因为我沿用了上篇教程的场景,目前我的 XR Origin 物体的层级是这样的:

在这里插入图片描述

因为这篇教程还是讲的抓取,所以对抓取功能不熟悉的小伙伴可以先学习我的近距离抓取和远距离抓取的教程:

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

VR 中的交互分为发起交互的对象(Interactor)和可交互的对象(Interactable)。抓取方面我们只会需要这些物体(带有 XR Direct Interactor 和 XR Ray Interactor)加上手部模型:

在这里插入图片描述

然后我们还需要 Interactable,我这里用了一个枪的模型,添加上碰撞体刚体,还有 XR Grab Interactable 脚本:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

这样枪就能被抓取了,我们的前置准备也到此为止。


📕HandData 脚本存储手部数据

根据之前的思路,我们需要记录抓取姿势的相关数据。因此我们新建一个脚本,名为 HandData:

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

public class HandData : MonoBehaviour
{
    public enum HandModelType { Left, Right}
    public HandModelType handType;
    public Animator animator;
    public Transform root; //手部模型的根物体    
    public Transform[] fingerBones; //手部模型的每个关节
}

解释一下这个脚本。首先我们需要知道是哪只手在抓取,所以这里设置了一个枚举类型来区分左右手。因为使用抓取手势时无需让 Animator 来控制手指弯曲,所以需要一个手部模型 Animator 的引用。在我们设置抓取手势的时候,首先要设置整个手部模型附在抓取物体上的位置和旋转角度,然后设置每个关节的位置和旋转角度,这样物体看起来才会被抓在手上,所以我们需要手部模型的根物体和每个关节的引用。

写完这个脚本,我们把它分别添加到 XR Origin 下的左手和右手模型上,然后设置好每个变量的引用:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

手部关节引用的对应关系如下(以右手为例,当然大家需要根据自己的手部模型来添加):

在这里插入图片描述


📕制作预设手势

接下来我们把 XR Origin 下的左右手模型做成预制体 Prefab,然后在场景中添加这两个 Prefab,并且作为枪模型的子物体,我们称它为预设手部模型,如下图所示。预设手部模型要和原来的手部模型做个区分。这个时候,场景中有两对手部模型,一对作为 XR Origin 层级下的物体,是由手柄控制的双手;一对作为枪模型的子物体,是已经做好抓取姿势的预设手部模型

在这里插入图片描述

在这里插入图片描述

然后找到预设手部模型的 Animator 移除掉,并且删除它的 HandData 脚本。因为预设手部模型不需要通过手柄输入来控制手部动画:

在这里插入图片描述

现在,我们就可以先制作右手的预设手势。调整预设手部模型的位置,旋转角度,以及每个关节的位置和旋转角度,让它呈现握枪的姿势:

在这里插入图片描述

接下来我们要做的,就是让 VR 中的双手在抓取这个物体的时候呈现我们设置好的这个姿势。


📕手势匹配脚本 GrabHandPose

我们需要写一个脚本将预设手势的数据赋给手部模型。我这里新建了一个脚本,名为 GrabHandPose,然后把它挂载到可抓取物体上,我这里就是挂载到枪的游戏物体上, Inspector 面板赋完值后如下图所示,下面我会具体讲解。

在这里插入图片描述

⭐完整代码

先给出完整代码,接下来我会进行具体的讲解。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
#if UNITY_EDITOR
using UnityEditor;
#endif

public class GrabHandPose : MonoBehaviour
{
    public HandData rightHandPose;
    public HandData leftHandPose;
    public bool smoothTransition; //是否开启平滑变化手势
    public float poseTransitionDuration = 0.2f; //平滑变化手势的时间

    private XRGrabInteractable grabInteractable;
    private Vector3 startingHandPosition;
    private Vector3 finalHandPosition;
    private Quaternion startingHandRotation;
    private Quaternion finalHandRotation;
    private Quaternion[] startingFingerRotations;
    private Quaternion[] finalFingerRotations;

    // Start is called before the first frame update
    void Start()
    {
        grabInteractable = GetComponent<XRGrabInteractable>();
        grabInteractable.selectEntered.AddListener(SetupPose);
        grabInteractable.selectExited.AddListener(UnsetPose);
        //隐藏预制姿势的手部模型
        rightHandPose.gameObject.SetActive(false);
        leftHandPose.gameObject.SetActive(false);
    }

    public void SetupPose(BaseInteractionEventArgs arg)
    {
        
        XRBaseControllerInteractor interactor = arg.interactorObject as XRBaseControllerInteractor; 
        if(interactor != null)
        {
            //找到挂载HandData的物体,这里可以根据自己项目层级的实际情况进行修改
            HandData handData = interactor.transform.parent.GetComponentInChildren<HandData>();
            handData.animator.enabled = false;
            //确认是左手抓取还是右手抓取,并设置对应手部的数据,初始手势数据为实际手部模型的数据,变化后的手势数据为预设的抓取该物体的手势
            if(handData.handType == HandData.HandModelType.Right)
            {
                SetHandDataValues(handData, rightHandPose);
            }
            else
            {
                SetHandDataValues(handData, leftHandPose);
            }
            
            if (smoothTransition)
            {
                StartCoroutine(SetHandDataRouting(handData, finalHandPosition, finalHandRotation, finalFingerRotations, startingHandPosition, startingHandRotation, startingFingerRotations));
            }
            else
            {
                SetHandData(handData, finalHandPosition, finalHandRotation, finalFingerRotations);
            }
            
        }
    }
    public void UnsetPose(BaseInteractionEventArgs arg)
    {
        XRBaseControllerInteractor interactor = arg.interactorObject as XRBaseControllerInteractor;
        if (interactor != null)
        {
            HandData handData = interactor.transform.parent.GetComponentInChildren<HandData>();
            handData.animator.enabled = true;
            if (smoothTransition)
            {
                StartCoroutine(SetHandDataRouting(handData, startingHandPosition, startingHandRotation, startingFingerRotations, finalHandPosition, finalHandRotation, finalFingerRotations));
            }
            else
            {
                SetHandData(handData, startingHandPosition, startingHandRotation, startingFingerRotations);
            }
            
        }
    }
    /// <summary>
    /// 初始化变化前和变化后的手势相关数据
    /// </summary>
    /// <param name="h1">实际手部模型</param>
    /// <param name="h2">抓取物体匹配的手势</param>
    public void SetHandDataValues(HandData h1, HandData h2)
    {
        //startingHandPosition = h1.root.localPosition;
        //finalHandPosition = h2.root.localPosition;              
        startingHandPosition = new Vector3(h1.root.localPosition.x / h1.root.localScale.x, 
            h1.root.localPosition.y / h1.root.localScale.y, h1.root.localPosition.z / h1.root.localScale.z);
        finalHandPosition = new Vector3(h2.root.localPosition.x / h2.root.localScale.x,
            h2.root.localPosition.y / h2.root.localScale.y, h2.root.localPosition.z / h2.root.localScale.z);

        startingHandRotation = h1.root.localRotation;
        finalHandRotation = h2.root.localRotation;

        startingFingerRotations = new Quaternion[h1.fingerBones.Length];
        finalFingerRotations = new Quaternion[h1.fingerBones.Length];

        for(int i = 0; i < h1.fingerBones.Length; i++)
        {
            startingFingerRotations[i] = h1.fingerBones[i].localRotation;
            finalFingerRotations[i] = h2.fingerBones[i].localRotation;   
        }
    }

    public void SetHandData(HandData h, Vector3 newPosition, Quaternion newRotation, Quaternion[] newBonesRotation)
    {
        h.root.localPosition = newPosition;
        h.root.localRotation = newRotation;
        for(int i=0;i<newBonesRotation.Length; i++)
        {
            h.fingerBones[i].localRotation = newBonesRotation[i];
        }
    }
    public IEnumerator SetHandDataRouting(HandData h, Vector3 newPosition, Quaternion newRotation, Quaternion[] newBonesRotation, Vector3 startingPosition, Quaternion startingRotation, Quaternion[] startingBonesRotation)
    {
        float timer = 0;
        while(timer < poseTransitionDuration)
        {
            float lerpTime = timer / poseTransitionDuration;
            Vector3 p = Vector3.Lerp(startingPosition, newPosition, lerpTime);
            Quaternion r = Quaternion.Lerp(startingRotation, newRotation, lerpTime);
            h.root.localPosition = p;
            h.root.localRotation = r;
            for(int i = 0; i < newBonesRotation.Length; i++)
            {
                h.fingerBones[i].localRotation = Quaternion.Lerp(startingBonesRotation[i], newBonesRotation[i], lerpTime);
            }
            timer += Time.deltaTime;
            yield return null;
        }
        h.root.localPosition = newPosition;
        h.root.localRotation = newRotation;
        for (int i = 0; i < newBonesRotation.Length; i++)
        {
            h.fingerBones[i].localRotation = newBonesRotation[i];
        }
    }
#if UNITY_EDITOR
    [MenuItem("Tools/Mirror Right Hand Pose")]
    public static void MirrorRightPose()
    {
        GrabHandPose handpose = Selection.activeGameObject.GetComponent<GrabHandPose>();
        handpose.MirrorPose(handpose.leftHandPose, handpose.rightHandPose);
    }
    [MenuItem("Tools/Mirror Left Hand Pose")]
    public static void MirrorLeftPose()
    {
        GrabHandPose handpose = Selection.activeGameObject.GetComponent<GrabHandPose>();
        handpose.MirrorPose(handpose.rightHandPose, handpose.leftHandPose);
    }
#endif
    /// <summary>
    /// 镜像手势,在Unity编辑器中使用
    /// </summary>
    /// <param name="poseToMirror">镜像后得到的手势</param>
    /// <param name="poseUsedToMirror">镜像之前的手势</param>
    public void MirrorPose(HandData poseToMirror, HandData poseUsedToMirror)
    {
        Vector3 mirroredPosition = poseUsedToMirror.root.localPosition;
        mirroredPosition.x *= -1;

        Quaternion mirroredQuaternion = poseUsedToMirror.root.localRotation;
        mirroredQuaternion.y *= -1;
        mirroredQuaternion.z *= -1;

        poseToMirror.root.localPosition = mirroredPosition;
        poseToMirror.root.localRotation = mirroredQuaternion;

        for(int i = 0; i < poseUsedToMirror.fingerBones.Length; i++)
        {
            poseToMirror.fingerBones[i].localRotation = poseUsedToMirror.fingerBones[i].localRotation;
        }
    }
}

⭐需要保存的数据

抓取物体的时候我们要把手部模型的姿势变成预设手势的样子,然后松开物体的时候要让手部模型恢复成原来的样子。因此我们需要记录两组数据,一组代表原来的手部模型,一组代表预设手势。然后在实际操作过程中在合适的时候将数据赋予手部模型,让手部模型呈现出对应的姿势。

因为我们之前用 HandData 脚本记录了手部的一些数据,所以我们需要 HandData 类的引用:

 public HandData rightHandPose;
 public HandData leftHandPose;

为了让手部呈现出相应的姿势,手部模型本身的位置和旋转角度,以及每个关节的位置和旋转角度尤为重要。我们可以从 HandData 类中获取这些数据,分为两组存储起来,一组是没抓取时手部模型的相关数据,一组是抓取时手部模型呈现出预设手势的相关数据。因此,这些需要存储的数据可以用几个变量来表示:

//手部模型根物体的localPosition
private Vector3 startingHandPosition; 
private Vector3 finalHandPosition;
//手部模型根物体的localRotation
private Quaternion startingHandRotation;
private Quaternion finalHandRotation;
//手部模型各关节的localRotation
private Quaternion[] startingFingerRotations;
private Quaternion[] finalFingerRotations;

其中,startingxxx表示的是未抓取时手势的数据,finalxxx表示的是抓取时手势的数据。另外注意看我的注释,这些变量记录的是 localPosition 或 localRotation。为什么记录的是 local,也就是相对于父物体的变换呢?因为手部模型是 XR Origin 层级下的子物体,手部的每个关节在 Unity Hierarchy 面板中也是作为子物体的存在,每个子物体的 Inspector 面板中所显示的 position 和 rotation 是相对于它父物体的坐标,也就是本地坐标,如下图所示:

在这里插入图片描述

之前在制作预设手势的时候,我们更改的是如上图中用红框框出的这些数值,用来修改每个子物体的位置和旋转,那时候改变的就是本地坐标,也就是相对于父物体的位置和旋转角度。我们希望的是在抓取的时候让手部模型根物体和每个关节的 Inspector 面板上的 Position 和 Rotation 的数值和我们预设的一样,这样手部模型就能呈现我们预设的抓取姿势。所以我们需要更改的是本地坐标,我们存储的是 localPosition 和 localRotation。

接下来,我们需要一个方法来初始化用于存储数据的这些变量:

 /// <summary>
    /// 初始化变化前和变化后的手势相关数据
    /// </summary>
    /// <param name="h1">实际手部模型</param>
    /// <param name="h2">抓取物体匹配的手势</param>
    public void SetHandDataValues(HandData h1, HandData h2)
    {
        //startingHandPosition = h1.root.localPosition;
        //finalHandPosition = h2.root.localPosition;              
        startingHandPosition = new Vector3(h1.root.localPosition.x / h1.root.localScale.x, 
            h1.root.localPosition.y / h1.root.localScale.y, h1.root.localPosition.z / h1.root.localScale.z);
        finalHandPosition = new Vector3(h2.root.localPosition.x / h2.root.localScale.x,
            h2.root.localPosition.y / h2.root.localScale.y, h2.root.localPosition.z / h2.root.localScale.z);

        startingHandRotation = h1.root.localRotation;
        finalHandRotation = h2.root.localRotation;

        startingFingerRotations = new Quaternion[h1.fingerBones.Length];
        finalFingerRotations = new Quaternion[h1.fingerBones.Length];

        for(int i = 0; i < h1.fingerBones.Length; i++)
        {
            startingFingerRotations[i] = h1.fingerBones[i].localRotation;
            finalFingerRotations[i] = h2.fingerBones[i].localRotation;   
        }
    }

如前面解释的,startingxxx表示的是未抓取时手势的数据,finalxxx表示的是抓取时手势的数据,所以 h1 代表的是默认状态下手部模型的手势数据,h2 代表的是抓取这个物体所匹配的预设手势数据。这里需要注意的是,在初始化 startingHandPosition 和 finalHandPosition,也就是初始化手部模型根物体相对于父物体的位置时,每个轴的值还需要除以一个 localScale

startingHandPosition = new Vector3(h1.root.localPosition.x / h1.root.localScale.x, h1.root.localPosition.y / h1.root.localScale.y, h1.root.localPosition.z / h1.root.localScale.z);
finalHandPosition = new Vector3(h2.root.localPosition.x / h2.root.localScale.x,h2.root.localPosition.y / h2.root.localScale.y, h2.root.localPosition.z / h2.root.localScale.z);

这是考虑到了父物体缩放的影响。在我的场景中,枪的模型进行了缩放:

在这里插入图片描述

当一个物体受到父对象的缩放影响时,它的局部位置(即相对于父对象的位置)也会被缩放。为了获得与世界坐标一致的位置,我们需要将局部位置除以局部缩放。 在Unity中,localPosition 表示物体相对于父对象的局部位置,localScale 表示父对象的局部缩放。将 localPosition 除以 localScale 可以将物体的局部位置转换为与世界坐标一致的位置。这样做的目的是为了确保我们在处理物体位置时,不受父对象的缩放影响,以便在整个场景中准确地定位物体的位置。

⭐得知什么时候开始抓取和取消抓取

我们需要在开始抓取和取消抓取触发的时候将相应的手势数据赋予手部模型。因此我们可以利用 XR Grab Interactable 脚本中的事件,selectedEntered 表示开始抓取,selectExited 表示取消抓取,并且在 Start 方法里为事件绑定触发时调用的方法:

 	void Start()
    {
        grabInteractable = GetComponent<XRGrabInteractable>();
        grabInteractable.selectEntered.AddListener(SetupPose);
        grabInteractable.selectExited.AddListener(UnsetPose);
        //隐藏预制姿势的手部模型
        rightHandPose.gameObject.SetActive(false);
        leftHandPose.gameObject.SetActive(false);
    }

另外,可抓取物体的预设手势模型在游戏运行过程中是不需要被看到的,我们只需传输预设手势上的数据,所以可以将它们隐藏。

⭐将手势数据赋予手部模型

在开始抓取和取消抓取的时候,我们需要将相应的手势数据赋予手部模型。我们可以统一写一个方法来表示:

    public void SetHandData(HandData h, Vector3 newPosition, Quaternion newRotation, Quaternion[] newBonesRotation)
    {
        h.root.localPosition = newPosition;
        h.root.localRotation = newRotation;
        for(int i=0; i<newBonesRotation.Length; i++)
        {
            h.fingerBones[i].localRotation = newBonesRotation[i];
        }
    }

开始抓取时,我们调用这个方法:

SetHandData(handData, finalHandPosition, finalHandRotation, finalFingerRotations);

这里的 handData 参数是实际手部模型的 HandData,finalxxx 代表的是预设手势。因为 HandData 脚本持有手部模型的根物体和关节的引用,所以修改它们的数据就能直接影响到手部模型的手势,让手势成为预设抓取手势。

取消抓取,同样调用这个方法,只不过传入的参数略有不同:

SetHandData(handData, startingHandPosition, startingHandRotation, startingFingerRotations);

startxxx 代表的是未抓取时的默认手势,所以这样能够复原手势。

⭐平滑变化手势

刚刚介绍的变化手势的方法是一下子将数据赋予手部模型,让手部模型 “瞬间” 变化姿势。但是有时候,我们想要平滑地变化手势。大家可以观察下面的动图感受它们的区别。

瞬间变化:

在这里插入图片描述

平滑变化:

在这里插入图片描述

可以看到平滑变化下手指的弯曲会有一个过渡的过程。
我的脚本中也有对平滑变化的支持。这个功能可以借助协程和插值 Lerp 来实现。

 public bool smoothTransition; //是否开启平滑变化手势
 public float poseTransitionDuration = 0.2f; //平滑变化手势的时间
	public IEnumerator SetHandDataRouting(HandData h, Vector3 newPosition, Quaternion newRotation, Quaternion[] newBonesRotation, Vector3 startingPosition, Quaternion startingRotation, Quaternion[] startingBonesRotation)
    {
        float timer = 0;
        while(timer < poseTransitionDuration)
        {
            float lerpTime = timer / poseTransitionDuration;
            Vector3 p = Vector3.Lerp(startingPosition, newPosition, lerpTime);
            Quaternion r = Quaternion.Lerp(startingRotation, newRotation, lerpTime);
            h.root.localPosition = p;
            h.root.localRotation = r;
            for(int i = 0; i < newBonesRotation.Length; i++)
            {
                h.fingerBones[i].localRotation = Quaternion.Lerp(startingBonesRotation[i], newBonesRotation[i], lerpTime);
            }
            timer += Time.deltaTime;
            yield return null;
        }
        h.root.localPosition = newPosition;
        h.root.localRotation = newRotation;
        for (int i = 0; i < newBonesRotation.Length; i++)
        {
            h.fingerBones[i].localRotation = newBonesRotation[i];
        }
    }

因为 Lerp 方法需要传入的三个参数 Vector3 a, Vector3 b, float t,得到的值等于 a + (b - a) * t,所以要想平滑变化,t 是一样会逐渐变大的值。所以这里用 timer / poseTransitionDuration,当 timer 大等于 poseTransitionDuration 时,直接将最终目标的值赋给 handData。

然后在 Inspector 面板中,可以设置是否开启平滑变化和变化过渡的时间:

在这里插入图片描述

⭐开始抓取和取消抓取触发的事件函数

前面铺垫了这么多,最后我们要来完善开始抓取和取消抓取触发的事件函数。

grabInteractable.selectEntered.AddListener(SetupPose);
grabInteractable.selectExited.AddListener(UnsetPose);
	public void SetupPose(BaseInteractionEventArgs arg)
    {        
        XRBaseControllerInteractor interactor = arg.interactorObject as XRBaseControllerInteractor; 
        if(interactor != null)
        {
            //找到挂载HandData的物体,这里可以根据自己项目层级的实际情况进行修改
            HandData handData = interactor.transform.parent.GetComponentInChildren<HandData>();
            handData.animator.enabled = false;
            //确认是左手抓取还是右手抓取,并设置对应手部的数据,初始手势数据为实际手部模型的数据,变化后的手势数据为预设的抓取该物体的手势
            if(handData.handType == HandData.HandModelType.Right)
            {
                SetHandDataValues(handData, rightHandPose);
            }
            else
            {
                SetHandDataValues(handData, leftHandPose);
            }
            
            if (smoothTransition)
            {
                StartCoroutine(SetHandDataRouting(handData, finalHandPosition, finalHandRotation, finalFingerRotations, startingHandPosition, startingHandRotation, startingFingerRotations));
            }
            else
            {
                SetHandData(handData, finalHandPosition, finalHandRotation, finalFingerRotations);
            }
            
        }
    }
    public void UnsetPose(BaseInteractionEventArgs arg)
    {
        XRBaseControllerInteractor interactor = arg.interactorObject as XRBaseControllerInteractor;
        if (interactor != null)
        {
            HandData handData = interactor.transform.parent.GetComponentInChildren<HandData>();
            handData.animator.enabled = true;
            if (smoothTransition)
            {
                StartCoroutine(SetHandDataRouting(handData, startingHandPosition, startingHandRotation, startingFingerRotations, finalHandPosition, finalHandRotation, finalFingerRotations));
            }
            else
            {
                SetHandData(handData, startingHandPosition, startingHandRotation, startingFingerRotations);
            }
            
        }
    }

这里有几个点需要解释一下。首先是这句代码:

XRBaseControllerInteractor interactor = arg.interactorObject as XRBaseControllerInteractor;

arg 表示的是 Interactable,也就是可抓取的物体,它能获取与它进行交互的 Interactor。因为我们的抓取分为近距离抓取和射线抓取,分别对应 XR Direct Interactor 和 XR Ray Interactor。而 XRBaseControllerInteractor 是这两个 Interactor 的基类。

剩下的代码就简单了,基本上按照我们最初的实现思路走。开始抓取的时候,将手部模型的 Animator 禁用掉,获取手部模型的 HandData 脚本,根据交互的是左手还是右手初始化需要保存的数据,然后将预设手势的数据赋予手部模型。取消抓取的时候开启手部模型的 Animator,将手部模型的手势复原。


📕镜像手势

在前文中我先设置了右手的预设抓取手势,还剩左手的预设抓取手势需要设置。但是因为左右手的手势是呈镜像的,所以我们可以用代码来镜像另一边手的预设手势,免得对一个个关节进行手动调整。

这里我们需要对 Unity 编辑器进行拓展,在编辑器中通过按下一个按钮使左手呈镜像的姿势,因此我们需要 UnityEditor 这个命名空间。但是这里又有一个问题,引用了 UnityEditor 命名空间的脚本无法被打包。所以我们可以用预处理指令来解决:

#if UNITY_EDITOR
using UnityEditor;
#endif

这样只有在 Unity 编辑器中才会引用这个命名空间。然后我们编写镜像手势的代码:

#if UNITY_EDITOR
    [MenuItem("Tools/Mirror Right Hand Pose")]
    public static void MirrorRightPose()
    {
        GrabHandPose handpose = Selection.activeGameObject.GetComponent<GrabHandPose>();
        handpose.MirrorPose(handpose.leftHandPose, handpose.rightHandPose);
    }
    [MenuItem("Tools/Mirror Left Hand Pose")]
    public static void MirrorLeftPose()
    {
        GrabHandPose handpose = Selection.activeGameObject.GetComponent<GrabHandPose>();
        handpose.MirrorPose(handpose.rightHandPose, handpose.leftHandPose);
    }
#endif
    /// <summary>
    /// 镜像手势,在Unity编辑器中使用
    /// </summary>
    /// <param name="poseToMirror">镜像后得到的手势</param>
    /// <param name="poseUsedToMirror">镜像之前的手势</param>
    public void MirrorPose(HandData poseToMirror, HandData poseUsedToMirror)
    {
        Vector3 mirroredPosition = poseUsedToMirror.root.localPosition;
        mirroredPosition.x *= -1;

        Quaternion mirroredQuaternion = poseUsedToMirror.root.localRotation;
        mirroredQuaternion.y *= -1;
        mirroredQuaternion.z *= -1;

        poseToMirror.root.localPosition = mirroredPosition;
        poseToMirror.root.localRotation = mirroredQuaternion;

        for(int i = 0; i < poseUsedToMirror.fingerBones.Length; i++)
        {
            poseToMirror.fingerBones[i].localRotation = poseUsedToMirror.fingerBones[i].localRotation;
        }
    }

先解释 MirrorPose 这个方法。重点是将 x 轴的 position 翻转,将 y 轴和 z 轴的旋转角度翻转。当然,这个是针对我的手部模型进行操作的。我们可以观察默认的左右手部模型。

左:

在这里插入图片描述

右:

在这里插入图片描述

我这里大概就是 x 轴 position 互为相反数,y 轴和 z 轴 rotation 互为相反数。

接下来解释 MirrorRightPose 和 MirrorLeftPose 方法。它们是编辑器拓展方法,所以也要用预处理指令来涵盖。我们只需在 Unity 的菜单栏找到:

在这里插入图片描述

上面这个层级取决于 MenuItem 中写的内容,我写的是 [MenuItem(“Tools/Mirror Right Hand Pose”)]
[MenuItem(“Tools/Mirror Left Hand Pose”)]

然后在 Hierarchy 面板中选中枪的游戏物体,点击相应的按钮就能够将手部模型进行镜像操作,比如点击 Mirror Right Hand Pose,就能将左手预设模型设置为右手的镜像姿势。

在这里插入图片描述


📕编辑器面板关键物体一览

Hierarchy 面板关键物体层级:

在这里插入图片描述

手部模型:

在这里插入图片描述
在这里插入图片描述

预设手势模型:

在这里插入图片描述
在这里插入图片描述

可抓取物体的 GrabHandPose:

在这里插入图片描述

最终效果:

在这里插入图片描述

我后面又加了个球的模型来展示:

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/755094.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux重定向符怎么用/Centos和Ubuntu怎么安装软件?Vim编辑器是啥、又怎么用/Linux权限怎么修改设置

前情提要&#xff1a;经过一段时间的沉淀&#xff0c;因为要用到Linux&#xff0c;索性就梳理总结一下Linux的基本知识&#xff01; 紧接着前文&#xff0c;有需要点击这里查看哦&#xff01;(╹▽╹) 3.10 echo命令 作用&#xff1a;在命令行内输出指定内容语法&#xff1a;…

Windows多网卡通过跃点数设置网络优先级失败解决办法

在有多个网卡的情况下&#xff0c;网络优先级往往不是自己所需的&#xff0c;默认情况Windows会自动决策出应该优先使用的最佳网络连接顺序&#xff0c;但用户也有可能需要访问某一网卡所在内网等情况&#xff0c;此时可能就无法正常访问。网上查找可以通过修改跃点数的方式手动…

XUbuntu22.04之解决蓝牙鼠标不停掉线问题(追凶过程)(一百八十五)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

python_day8_bar

初识柱状图 导包 from pyecharts.charts import Bar from pyecharts.options import *创建柱状图对象 bar Bar()添加x轴数据,注意数据格式为列表 bar.add_xaxis([中国, USA, 不列颠])添加y轴数据,注意格式&#xff1a;图例&#xff0c;列表数据&#xff0c;设置 bar.add_…

Stable Diffusion Webui 之 ControlNet使用

一、安装 1.1、插件安装 1.2、模型安装 模型安装分为预处理模型和 controlnet所需要的模型。 先安装预处理模型&#xff0c;打开AI所在的安装目录\extensions\sd-webui-controlnet\annotator,将对应的预处理模型放进对应的文件夹中即可&#xff0c; 而controlnet所需模型则…

wordpress主题zibll子比主题v7.2.2绕授权+教程

1、先说一下要准备的东西 一份子比7.1正式包&#xff0c;一台服务器&#xff0c;wp6.2.2正式包&#xff08;wordpress&#xff09;&#xff0c;一个域名 2、首先把wp上传服务器的域名根目录下&#xff0c;然后打开前台按要求填写数据库和管理员邮箱账号密码&#xff0c;php版本…

0129 进程与线程3

目录 2.进程与线程 2.4死锁 2.4部分习题 2.进程与线程 2.4死锁 2.4部分习题 1.死锁的避免是根据&#xff08;&#xff09;采取措施实现的 A.配置足够多的系统资源 B.使进程推进顺序合理 C.破坏死锁的四个必要条件之一 D.防止系统进入不安全状态 2.死锁…

HTML5和CSS3新特性

文章目录 1.HTML5新特性1.1 概述1.2 语义化标签1.3 多媒体标签1.3.1 视频标签- video1.3.2 音频标签- audio 1.4 新增的表单元素1.5 新增表单属性 2.CSS3新特性2.1新增选择器2.1.1 属性选择器2.1.2 结构伪类选择器E:first-childE:nth-child(n)E:nth-child 与 E:nth-of-type 的区…

7个有用的Prompt参数

ChatGPT和Midjournal使得生成式人工智能的应用程序激增。当涉及到生成式AI时&#xff0c;"prompt"通常指的是作为输入给模型的初始提示或指示。它是一个短语、问题、句子或段落&#xff0c;用来引导模型生成相关的响应或文本。 在使用生成式AI模型时&#xff0c;提供…

form 校验多个表单

有的时候&#xff0c;表单需要拆开多个&#xff0c;这时候就需要校验多个表单 <template><div><div>表单1</div><div class"top"><el-form :model"form" ref"form1" :rules"rules" label-width&quo…

ylb-接口9登录短信发送

总览&#xff1a;&#xff08;总体功能与注册发送短信功能相似&#xff09; 在web模块service.impl包下&#xff0c;创建SmsCodeLoginImpl&#xff0c;实现的还是SmsService接口 package com.bjpowernode.front.service.impl;import com.alibaba.fastjson.JSONObject; impor…

2023机器人操作系统(ROS)暑期学校报名通道开启-转发-

来源请查看&#xff1a; https://mp.weixin.qq.com/s/gVr4pUG2TGT6sCcGKvVnYw 报名等请使用上面给出地址。 面向对象&#xff1a;机器人/人工智能相关专业教师/学生/工程师 要求&#xff1a;ROS零基础/中高级 费用&#xff1a;免费&#xff0c;食宿自理 时间&#xff1a;2023…

3.2 多路复用和多路分用

3.2 多路复用和多路分用 多路复用/分用分用如何工作&#xff1f;无连接分用面向连接的分用面向连接的分用&#xff1a;多线程Web服务器 多路复用/分用 分用如何工作&#xff1f; 主机接收到IP数据报(datagram) 每个数据报携带源IP地址、目的IP地址。每个数据报携带一个传输层的…

Java中字符串相关的类

目录 String类 StringBuffer类 StringBuilder类 String类 String类&#xff1a;代表字符串。Java 程序中的所有字符串字面值&#xff08;如 "abc" &#xff09;都作为此类的实例实现。 String是一个final类&#xff0c;代表不可变的字符序列。 字符串是常量&…

[论文分享]MR-MAE:重构前的模拟:用特征模拟增强屏蔽自动编码器

论文题目&#xff1a;Mimic before Reconstruct: Enhancing Masked Autoencoders with Feature Mimicking 论文地址&#xff1a;https://arxiv.org/abs/2303.05475 代码地址&#xff1a;https://github.com/Alpha-VL/ConvMAE&#xff08;好像并未更新为MR-MAE模型&#xff09; …

不容错过!10个Python自动化办公库免费送!(上)

大家好&#xff0c;这里是程序员晚枫&#xff0c;小红薯也叫这个名。 今天给大家分享一下&#xff0c;花费2周时间整理的Python自动化办公库。 本次内容涵盖了Excel、Word、PPT、PDF、微信、文件处理等所有能在办公场景实现自动化的库&#xff0c;希望能够对大家有所帮助。 提…

部署LAMP 平台(二十四)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 一、概述 二、PHP安装配置 1. PHP的作用 2. PHP安装 2.1 yum安装 2.2 PHP语言简介 三、安装 四、启动 五、书写测试页面 六、客户端访问 七、书写连接数据库页…

C++图形开发(13):得分系统

文章目录 1.定义变量2.得分系统3.分数清零系统4.输出分数5.整段代码 今天来对这个“别碰方块”的游戏进行一个完善&#xff0c;并增加一个得分系统 1.定义变量 首先是定义用于储存得分的变量&#xff1a; int score 0;2.得分系统 那么怎样才能得分呢&#xff1f; 没错&…

数据分析——AB测试应用与实战

摘要 某电商公司非常注重自己的落地页设计&#xff0c;希望通过改进设计来提高转化率。以往该公司全年转化率平均在13%左右&#xff0c;现在希望设计的新页面能够带来更高的转化率&#xff0c;希望新页面的转化率能有2%的提升&#xff0c;达到15%。在正式推出新页面之前&#…

HCIA配置命令集

目录 扩展 交换机 路由器 路由器网关配置 DHCP服务器 Telnet &#xff1a;远程登录协议 静态路由配置 动态路由 OSPF RIP NAT—网络地址转换 ACL—访问控制列表 ACL的分类&#xff1a; 配置 配置基础ACL &#xff1a; 例一&#xff1a; 例二&#xff1a; 配…