基于光学动捕定位下的Unity-VR手柄交互

Unity VR 场景手柄交互实现方案

需求

在已创建好的 Unity VR 场景中,接入游戏手柄,通过结合动捕系统与 VRPN,建立刚体,实时系统获取到手柄的定位数据与按键数据,通过编写代码实现手柄的交互逻辑,实现手柄抓起物体移动放置。

演示视频

资源工程附件

(评论区回复:VR交互)

实现方案

1. 控制抓取物体的平移运动(不考虑旋转)

仅控制抓取物体的前后上下左右方向的平移运动,不考虑手柄的旋转

csharp 复制代码
using UnityEngine;
using UVRPN.Core;

public class HandleInteraction : MonoBehaviour
{
    public LayerMask targetLayer; // 目标物体所在的层
    public float rayLength = 5f; // 射线的长度
    private GameObject selectedObject; // 当前选中的物体
    private Vector3 offset; // 物体与手柄的相对位置
    private bool isHoldingObject = false; // 是否正在抓取物体的标志
    private VRPN_Button vrpnButton; // VRPN_Button 组件的引用

    void Start()
    {
        vrpnButton = GetComponent<VRPN_Button>();
        // 或者,如果 VRPN_Button 组件在其他 GameObject 上
        // vrpnButton = GameObject.Find("GameObjectName").GetComponent<VRPN_Button>();
    }

    // Update is called once per frame
    void Update()
    {
        // 发射射线
        Ray ray = new Ray(transform.position, -transform.forward);
        RaycastHit hit;

        // 如果射线击中了目标层上的物体
        if (Physics.Raycast(ray, out hit, rayLength, targetLayer))
        {
            Debug.DrawLine(ray.origin, hit.point, Color.red); // 绘制射线(仅在 Scene 视图中可见)

            // 检测手柄按钮是否被按下
            if (vrpnButton.ButtonDown)
            {
                // 抓取物体
                selectedObject = hit.collider.gameObject;
                offset = selectedObject.transform.position - transform.position;
                isHoldingObject = true;
            }
        }

        // 如果当前有选中的物体,并且手柄按钮仍然被按住
        if (isHoldingObject && vrpnButton.ButtonHold)
        {
            // 移动物体,使其保持与手柄的相对位置
            selectedObject.transform.position = transform.position + offset;
        }

        // 如果手柄按钮被松开
        if (isHoldingObject && vrpnButton.ButtonUp)
        {
            // 放置物体
            isHoldingObject = false;
            selectedObject = null;
        }
    }
}

2. 考虑手柄的旋转处理

将选中物体与手柄作为整体,同时增加重力响应与Game视图中的射线渲染

csharp 复制代码
using UnityEngine;
using UVRPN.Core;

public class HandleMove : MonoBehaviour
{
    public LayerMask targetLayer; // 目标物体所在的层
    public float rayLength = 5f; // 射线的长度
    public float lineWidth = 0.01f; // 线条宽度    
    private GameObject selectedObject; // 当前选中的物体
    private Vector3 originalLocalPosition; // 物体相对于手柄的初始局部位置
    private Quaternion originalLocalRotation; // 物体相对于手柄的初始局部旋转
    private VRPN_Button vrpnButton; // VRPN_Button 组件的引用
    private Rigidbody rigidbody; // 选中物体的刚体组件
    private LineRenderer lineRenderer; // Game视图中的射线渲染
    
    void Start()
    {
        vrpnButton = GetComponent<VRPN_Button>();
        lineRenderer = GetComponent<LineRenderer>();
        // 设置 LineRenderer 的宽度
        lineRenderer.startWidth = lineWidth;
        lineRenderer.endWidth = lineWidth;
        lineRenderer.enabled = false;
    }

    void Update()
    {
        // 发射射线
        Ray ray = new Ray(transform.position, -transform.forward);
        RaycastHit hit;

        // 如果射线击中了目标层上的物体
        if (Physics.Raycast(ray, out hit, rayLength, targetLayer))
        {
            Debug.DrawLine(ray.origin, hit.point, Color.red); // 绘制射线(仅在 Scene 视图中可见)

            // 启用 Line Renderer 并设置位置
            lineRenderer.enabled = true;
            lineRenderer.SetPosition(0, ray.origin);
            lineRenderer.SetPosition(1, hit.point);
            
            // 检测手柄按钮是否被按下
            if (vrpnButton.ButtonDown && !selectedObject)
            {
                // 抓取物体
                selectedObject = hit.collider.gameObject;
                selectedObject.transform.SetParent(transform, true); // 将物体设置为手柄的子对象

                // 禁用物理引擎响应
                rigidbody = selectedObject.GetComponent<Rigidbody>();
                if (rigidbody)
                {
                    rigidbody.isKinematic = true;
                }

                // 记录物体相对于手柄的初始局部位置和旋转
                originalLocalPosition = selectedObject.transform.localPosition;
                originalLocalRotation = selectedObject.transform.localRotation;
            }
        }
        else
        {
            // 如果射线没有击中任何物体,禁用 Line Renderer
            lineRenderer.enabled = false;
        }
        
        // 如果手柄按钮被松开
        if (vrpnButton.ButtonUp && selectedObject)
        {
            // 放置物体
            selectedObject.transform.SetParent(null); // 移除物体的父对象
            selectedObject.transform.position = transform.TransformPoint(originalLocalPosition); // 重置物体的世界位置
            selectedObject.transform.rotation = transform.rotation * originalLocalRotation; // 重置物体的世界旋转
            selectedObject = null;

            // 启用物理引擎响应
            if (rigidbody)
            {
                rigidbody.isKinematic = false;
            }
        }
    }
}

3. 正确处理 VRPN 的坐标转换关系

坐标系转换的原理

已知动捕坐标系(右手)与 Unity 坐标系(左手),转动到同一视角将坐标系对齐如下:

3.1 动捕 Y-UP

即两坐标系 X 轴反向。

3.2 动捕 Z-UP

即 Z 与 Y 对调。

注意:转换的方式不止一种,这里选择比较方便的处理方式。

3.3 解决坐标系转换问题
1.通过 VRPN 反转设置
  1. 若动捕 Y 轴向上

  2. 若动捕Z轴向上

    不支持

2.通过修改 VRPN 脚本(编辑 VRPN_NativeBridge.cs),实现坐标系转换
  1. 若动捕 Y 轴向上
csharp 复制代码
internal static Vector3 TrackerPos(string address, int channel)
{
    return new Vector3(
        -(float)vrpnTrackerExtern(address, channel, 0, Time.frameCount),
        (float)vrpnTrackerExtern(address, channel, 1, Time.frameCount),
        (float)vrpnTrackerExtern(address, channel, 2, Time.frameCount));
}

internal static Quaternion TrackerQuat(string address, int channel)
{
    return new Quaternion(
        -(float)vrpnTrackerExtern(address, channel, 3, Time.frameCount),
        (float)vrpnTrackerExtern(address, channel, 4, Time.frameCount),
        (float)vrpnTrackerExtern(address, channel, 5, Time.frameCount),
        -(float)vrpnTrackerExtern(address, channel, 6, Time.frameCount));
}
  1. 若动捕 Z 轴向上
csharp 复制代码
internal static Vector3 TrackerPos(string address, int channel)
{
    return new Vector3(
        (float)vrpnTrackerExtern(address, channel, 1, Time.frameCount),
        (float)vrpnTrackerExtern(address, channel, 2, Time.frameCount),
        -(float)vrpnTrackerExtern(address, channel, 0, Time.frameCount));
}

internal static Quaternion TrackerQuat(string address, int channel)
{
    return new Quaternion(
        (float)vrpnTrackerExtern(address, channel, 3, Time.frameCount),
        (float)vrpnTrackerExtern(address, channel, 5, Time.frameCount),
        (float)vrpnTrackerExtern(address, channel, 4, Time.frameCount),
        -(float)vrpnTrackerExtern(address, channel, 6, Time.frameCount));
}
3.4 创建刚体时的朝向,下面以Y轴向上为例进行讲解通用的处理方式
  1. 打开VR_box的空间交互场景

  2. 选中手柄,按W键进行Move状态,点击工具栏上的Toggle Tool Handle Rotation,选择Local

  3. 观察确定场景中手柄道具的朝向,此时手柄物体指向自身的Z轴负方向

  4. 编辑VRPN_Tracker的设置,勾选local(由于此物体为顶层物体无父对象,其world=local, 这里通用处理)

  5. 在动捕系统中放置实体手柄,由于此时动捕Z轴与UnityZ轴(世界坐标系)重合,朝向Z轴的负方向创建刚体

  6. 参考3.1坐标系转换的关系,对vrpn参数进行设置,其中position反转X,rotation反转Y与Z,轴向反转一次,左右手法则角度再反一次,所以X的rotation不变

4. 使用说明

  1. 解压工程附件(Unity_Joystick_Demo V1.0.zip),打开 Assets->Scenes->VR_box.unity
  2. 运行动捕软件,完成坐标系的标定,与蓝牙手柄的连接,并启用 VRPN,选择刚体类型,设置数据单位为米。
  3. 在动捕软件中创建手柄对应的刚体,其中手柄朝向参考 3.4。
  4. 配置 VRPN_TrackerVRPN_Button 组件,设置正确的 Tracker 名称。
  5. 运行 Unity 程序,测试不同方向的移动与旋转是否正常,按下手柄按键,观察是否有打印输出。

思考题

若动捕坐标系如下,其中 X 轴正方向指向显示器/墙面,对应着 Unity 中手柄前方的交互场景,如何处理转换关系?

参考答案

手柄面向 X 轴正方向创建刚体,同时数据处理如下:

csharp 复制代码
internal static Vector3 TrackerPos(string address, int channel)
{
    return new Vector3(
        (float)vrpnTrackerExtern(address, channel, 1, Time.frameCount),
        (float)vrpnTrackerExtern(address, channel, 2, Time.frameCount),
        -(float)vrpnTrackerExtern(address, channel, 0, Time.frameCount));
}

internal static Quaternion TrackerQuat(string address, int channel)
{
    return new Quaternion(
        (float)vrpnTrackerExtern(address, channel, 4, Time.frameCount),
        (float)vrpnTrackerExtern(address, channel, 5, Time.frameCount),
        -(float)vrpnTrackerExtern(address, channel, 3, Time.frameCount),
        -(float)vrpnTrackerExtern(address, channel, 6, Time.frameCount));
}

相关推荐
谢泽浩17 分钟前
Unity 给模型贴上照片
unity·游戏引擎
z2014z17 分钟前
Unity Resource System 优化笔记
unity·游戏引擎
王维志23 分钟前
Unity 高亮插件HighlightPlus介绍
unity·游戏引擎
没那么特别的特别1 小时前
Axios基本语法和前后端交互
交互
zaizai10071 小时前
我的demo保卫萝卜中的技术要点
unity
菌菌巧乐兹3 小时前
Unity 百度AI实现无绿幕拍照抠像功能(详解版)
人工智能·百度·unity
孑么17 小时前
C# 委托与事件 观察者模式
开发语言·unity·c#·游戏引擎·游戏程序
wangduqiang74719 小时前
unity的学习
学习·unity·游戏引擎
@Sunset...20 小时前
Unity程序基础框架
unity·游戏引擎
咩咩觉主1 天前
Unity实战案例全解析 :PVZ 植物脚本分析
unity·游戏引擎