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 反转设置
-
若动捕 Y 轴向上
-
若动捕Z轴向上
不支持
2.通过修改 VRPN 脚本(编辑 VRPN_NativeBridge.cs
),实现坐标系转换
- 若动捕 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));
}
- 若动捕 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轴向上为例进行讲解通用的处理方式
-
打开VR_box的空间交互场景
-
选中手柄,按W键进行Move状态,点击工具栏上的Toggle Tool Handle Rotation,选择Local
-
观察确定场景中手柄道具的朝向,此时手柄物体指向自身的Z轴负方向
-
编辑VRPN_Tracker的设置,勾选local(由于此物体为顶层物体无父对象,其world=local, 这里通用处理)
-
在动捕系统中放置实体手柄,由于此时动捕Z轴与UnityZ轴(世界坐标系)重合,朝向Z轴的负方向创建刚体
-
参考3.1坐标系转换的关系,对vrpn参数进行设置,其中position反转X,rotation反转Y与Z,轴向反转一次,左右手法则角度再反一次,所以X的rotation不变
4. 使用说明
- 解压工程附件(
Unity_Joystick_Demo V1.0.zip
),打开Assets->Scenes->VR_box.unity
。 - 运行动捕软件,完成坐标系的标定,与蓝牙手柄的连接,并启用 VRPN,选择刚体类型,设置数据单位为米。
- 在动捕软件中创建手柄对应的刚体,其中手柄朝向参考 3.4。
- 配置
VRPN_Tracker
与VRPN_Button
组件,设置正确的 Tracker 名称。 - 运行 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));
}