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

news/2024/9/17 19:00:22/ 标签: unity, vr, 交互

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

需求

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

演示视频

在这里插入图片描述

资源工程附件

(评论区回复:VR交互

实现方案

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

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

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 framevoid 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视图中的射线渲染

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 RendererlineRenderer.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 轴向上
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 轴向上
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 轴正方向创建刚体,同时数据处理如下:

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));
}


http://www.ppmy.cn/news/1507263.html

相关文章

C#实现代理服务器

在C#中实现一个简单的代理服务器&#xff0c;可以使用System.Net.Sockets命名空间下的TcpListener类来监听客户端的连接请求&#xff0c;并使用TcpClient来处理与客户端的通信。以下是一个简单的代理服务器示例&#xff1a; using System; using System.IO; using System.Net;…

MySQL:表的设计原则和聚合函数

所属专栏&#xff1a;MySQL学习 &#x1f48e;1. 表的设计原则 1. 从需求中找到类&#xff0c;类对应到数据库中的实体&#xff0c;实体在数据库中表现为一张一张的表&#xff0c;类中的属性对应着表中的字段 2. 确定类与类的对应关系 3. 使用SQL去创建具体的表 范式&#xff1…

【微信小程序开发】——奶茶点餐小程序的制作(二)

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;开发者-曼亿点 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 曼亿点 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a…

【MySQL】1.MySQL基本操作

目录 一、MySQL数据库登陆 1、设置环境变量 2、cmd命令登陆数据库 二、基本操作语法 1、显示数据库——SHOW 2、使用/选择数据库——USE 3、删除——DROP 4、创建——CREATE 5、查看表结构——DESC 6、数据操作——增删改查 &#xff08;1&#xff09;增/插入&#…

03创建型设计模式——抽象工厂模式

一、抽象工厂模式简介 抽象工厂模式是所有形态的工厂模式中最为抽象和具有一般性的。抽象工厂模式可以向客户端提供一个接口&#xff0c;使得客户端在不必指定产品的具体类型的情况下&#xff0c;能够创建多个产品族的产品对象。例如现实生活中&#xff0c;水果的种类繁多&…

Python:协程 - 快速创建三步骤

asyncio 协程快速创建三步骤&#xff1a; 1、编写协程函数。 2、创建协程对象。 3、加入事件循环。 import asyncioasync def do_somethings():# 1、定义协程函数print(1)await asyncio.sleep(0.5)async def do_another():print(2)await asyncio.sleep(0.2)async def do_other…

haproxy七层代理

目录 一、haproxy简介 二、haproxy实验 1.环境部署 2.haproxy的基本部署方法及负载均衡的实现 2.1安装软件 2.2haproxy的基本配置 3.haproxy的全局配置参数及日志分离 3.1多线程设定 3.2自定义日志 4.haproxy-proxies中的常用配置参数 4.1设置backup --- sorryserver…

TCP如何建立长连接

文章目录 TCP建立长连接长连接和短连接长连接的优势TCP KEEPALIVE 心跳包心跳检测步骤 断线重连断线重连函数实现 实例服务端客户端程序功能演示效果 TCP建立长连接 长连接和短连接 长连接是指不论TCP的客户端和服务器之间是否有数据传输&#xff0c;都保持建立的TCP连接&…

CSS优先级,没你想的那么简单!全面介绍影响CSS优先级的各类因素

简介 CSS的中文名称叫做“层叠样式表”&#xff0c;其中的层叠就是指根据各类优先级规则来处理冲突的样式。层叠是CSS的一个重要特性&#xff0c;优先级也是CSS学习中一项非常重要的内容。 提到CSS优先级&#xff0c;我们首先会想到各类的选择器&#xff0c;例如ID选择器&…

学习记录——day28 信号量集

目录 一、信号量集 1、信号量集的API函数接口 二、 将信号量集函数再次封装 1、sem.h 2、sem.c 三、使用信号量集完成共享内存的进程同步 1、发送端 2、接收端 一、信号量集 信号量集&#xff0c;其实就是无名信号量的集合&#xff0c;主要用于完整多个进程间的同步问题.…

127. Go反射基本原理

文章目录 反射基础 - go 的 interface 是怎么存储的&#xff1f;iface 和 eface 的结构体定义&#xff08;runtime/iface.go&#xff09;&#xff1a;_type 是什么&#xff1f;itab 是什么&#xff1f; 反射对象 - reflect.Type 和 reflect.Value反射三大定律Elem 方法reflect.…

【数据结构】三、栈和队列:6.链队列、双端队列、队列的应用(树的层次遍历、广度优先BFS、先来先服务FCFS)

文章目录 2.链队列2.1初始化&#xff08;带头结点&#xff09;不带头结点 2.2入队&#xff08;带头结点&#xff09;2.3出队&#xff08;带头结点&#xff09;❗2.4链队列c实例 3.双端队列考点:输出序列合法性栈双端队列 队列的应用1.树的层次遍历2.图的广度优先遍历3.操作系统…

【Kubernetes】Service 概念与实战

Service 概念与实战 1.通过 Service 向外部暴露 Pod2.Service 的多端口设置3.集群内部的 DNS 服务4.无头 Service 在 Kubernetes 中部署的应用可能对应一个或者多个 Pod&#xff0c;而每个 Pod 又具有独立的 IP 地址。Service&#xff08;服务&#xff09;能够为一组功能相同的…

大数据-72 Kafka 高级特性 稳定性-事务 (概念多枯燥) 定义、概览、组、协调器、流程、中止、失败

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

Linux中安装MYSQL数据库

文章目录 一、MYSQL数据库介绍1.1、MySQL数据库的基本概述1.2、MySQL数据库的主要特性1.3、MySQL数据库的技术架构与组件1.4、MySQL数据库的应用与扩展性1.5、MySQL数据库的许可模式与开源生态 二、MySQL Workbench和phpMyAdmin介绍2.1、MySQL Workbench介绍2.2、phpMyAdmin介绍…

【学习笔记】Day 9

一、进度概述 1、inversionnet_train 试运行——成功 二、详情 1、inversionnet_train 试运行 在经历了昨天的事故后&#xff0c;今天最终成功运行了 inversionnet_train&#xff0c;运行结果如下&#xff1a; 经观察&#xff0c;最开始 loss 值大概为 0.5 左右 随着训练量的增…

使用Selenium调试Edge浏览器的常见问题与解决方案

背景介绍 在当今互联网时代&#xff0c;网页爬虫已经成为数据获取的重要手段。而Selenium作为一款功能强大的自动化测试工具&#xff0c;被广泛应用于网页爬取任务中。虽然Chrome浏览器是Selenium用户的常见选择&#xff0c;但在某些工作环境中&#xff0c;我们可能需要使用Ed…

Ubuntu24.04设置国内镜像软件源

参考文章&#xff1a; Ubuntu24.04更换源地址&#xff08;新版源更换方式&#xff09; - 陌路寒暄 一、禁用原来的软件源 Ubuntu24.04 的源地址配置文件发生改变&#xff0c;不再使用以前的 sources.list 文件&#xff0c;升级 24.04 之后&#xff0c;该文件内容变成了一行注…

牛客-热身小游戏

题目链接&#xff1a;热身小游戏 第一种写法&#xff1a;线段树 介绍第二种写法&#xff1a;并查集 对于一些已经查询过的点&#xff0c;我们可以往后跳&#xff0c;进行路径压缩&#xff0c;他们的父亲为下一个点。 a数组记录[ l , r ] 之间的乘积&#xff0c;初始值为1。…

haproxy知识点整理

haproxy知识点整理 haproxy七层代理负载均衡什么是负载均衡为什么使用负载均衡 负载均衡类型四层负载均衡七层负载均衡四层和七层的区别 环境搭建:客户端(client)haproxy服务器两台服务器hapserver1hapserver2 简单的haproxy负载均衡 haproxy的基本配置信息global配置proxies配…