基本力觉反馈教程
在本教程中,为了再现与球体等静态物体接触时的物理特性,我们将创建一个同时包含刚度和阻尼的基本力觉模拟。 完成本教程后,您将制作出一个能够感知球体存在,并根据不同的力觉体验调整刚度和阻尼特性的模拟。
前言
本教程的核心任务是开发一个函数,用于计算与同时具有刚度和阻尼特性的球体接触时产生的力。 在此背景下,刚度起着类似弹簧的作用,压缩得越厉害,产生的力就越大。 另一方面,阻尼代表了物体运动所受到的阻力,移动得越快,产生的阻力就越大。
场景设置
入门指南请按照说明设置 Haptic Rig。此时,触觉起源 位置、旋转和缩放分别 (0, 0, 0)、(1, 1, 1) 请确认已设置为。
接下来,具有以下属性的 Sphere 创建一个名为……的球体。
- 职位:
(0, 0, -0.1)(距设备前方约10厘米) - 规模:
(0.2, 0.2, 0.2)(相当于直径20厘米的球体)
力反馈脚本
球体 在 GameObject 上,SphereForceFeedback.cs 添加一个名为“Inverse3 ”的新 C# 脚本。
该脚本会计算球体与光标接触时作用于光标上的力,同时考虑刚度和阻尼。
使用以下属性初始化该脚本:
[SerializeField]
private Inverse3 inverse3;
[Range(0, 800)]
public float stiffness = 300f;
[Range(0, 3)]
public float damping = 1f;
private Vector3 _ballPosition;
private float _ballRadius;
private float _cursorRadius;
仅当光标进入球体内部时才需要进行力计算。这样可以再现触碰具有既定刚度和阻尼的物理对象时的触感。
ForceCalculation 该方法负责处理此操作,并同时考虑光标的位置和速度:
private Vector3 ForceCalculation(Vector3 cursorPosition, Vector3 cursorVelocity, float cursorRadius,
Vector3 otherPosition, float otherRadius)
{
var force = Vector3.zero;
var distanceVector = cursorPosition - otherPosition;
var distance = distanceVector.magnitude;
var penetration = otherRadius + cursorRadius - distance;
if (penetration > 0)
{
// Normalize the distance vector to get the direction of the force
var normal = distanceVector.normalized;
// Calculate the force based on penetration
force = normal * penetration * stiffness;
// Apply damping based on the cursor velocity
force -= cursorVelocity * damping;
}
return force;
}
private void OnDeviceStateChanged(object sender, Inverse3EventArgs args)
{
var inverse3 = args.DeviceController;
// Calculate the ball force
var force = ForceCalculation(inverse3.CursorLocalPosition, inverse3.CursorLocalVelocity,
_cursorRadius, _ballPosition, _ballRadius);
inverse3.SetCursorLocalForce(force);
}
Awake 在该方法中,为了设置场景数据 _ballPosition、_ballRadius、_cursorRadius 初始化:
private void SaveSceneData()
{
var t = transform;
_ballPosition = t.position;
_ballRadius = t.lossyScale.x / 2f;
_cursorRadius = inverse3.Cursor.Radius;
}
private void Awake()
{
SaveSceneData();
}
OnEnable 方法与 OnDisable 在各个方法中 OnDeviceStateChanged 请注册和取消注册回调,并在交互过程中正确处理力反馈:
protected void OnEnable()
{
inverse3.DeviceStateChanged += OnDeviceStateChanged;
}
protected void OnDisable()
{
inverse3.DeviceStateChanged -= OnDeviceStateChanged;
}
游戏体验
Inverse3 请按住光标,进入“播放模式”,然后尝试触摸球体。 您可以感知到球体的存在,并可通过 Unity 的“检查器”调整其刚度和阻尼特性。这样,您就能切身感受到与虚拟对象的交互。

源文件
该示例的完整场景及相关文件均可从 Unity 的“包管理器”中的“Tutorials”示例中导入。
SphereForceFeedback.cs
/*
* Copyright 2024 Haply Robotics Inc. All rights reserved.
*/
using Haply.Inverse.DeviceControllers;
using Haply.Inverse.DeviceData;
using UnityEngine;
namespace Haply.Samples.Tutorials._2_BasicForceFeedback
{
public class SphereForceFeedback : MonoBehaviour
{
public Inverse3Controller inverse3;
[Range(0, 800)]
// Stiffness of the force feedback.
public float stiffness = 300f;
[Range(0, 3)]
public float damping = 1f;
private Vector3 _ballPosition;
private float _ballRadius;
private float _cursorRadius;
/// <summary>
/// Stores the cursor and sphere transform data for access by the haptic thread.
/// </summary>
private void SaveSceneData()
{
var t = transform;
_ballPosition = t.position;
_ballRadius = t.lossyScale.x / 2f;
_cursorRadius = inverse3.Cursor.Radius;
}
/// <summary>
/// Saves the initial scene data cache.
/// </summary>
private void Awake()
{
inverse3 ??= FindObjectOfType<Inverse3Controller>();
inverse3.Ready.AddListener((device, args) => SaveSceneData());
}
/// <summary>
/// Subscribes to the DeviceStateChanged event.
/// </summary>
private void OnEnable()
{
inverse3.DeviceStateChanged += OnDeviceStateChanged;
}
/// <summary>
/// Unsubscribes from the DeviceStateChanged event.
/// </summary>
private void OnDisable()
{
inverse3.DeviceStateChanged -= OnDeviceStateChanged;
inverse3.Release();
}
/// <summary>
/// Calculates the force based on the cursor's position and another sphere position.
/// </summary>
/// <param name="cursorPosition">The position of the cursor.</param>
/// <param name="cursorVelocity">The velocity of the cursor.</param>
/// <param name="cursorRadius">The radius of the cursor.</param>
/// <param name="otherPosition">The position of the other sphere (e.g., ball).</param>
/// <param name="otherRadius">The radius of the other sphere.</param>
/// <returns>The calculated force vector.</returns>
private Vector3 ForceCalculation(Vector3 cursorPosition, Vector3 cursorVelocity, float cursorRadius,
Vector3 otherPosition, float otherRadius)
{
var force = Vector3.zero;
var distanceVector = cursorPosition - otherPosition;
var distance = distanceVector.magnitude;
var penetration = otherRadius + cursorRadius - distance;
if (penetration > 0)
{
// Normalize the distance vector to get the direction of the force
var normal = distanceVector.normalized;
// Calculate the force based on penetration
force = normal * penetration * stiffness;
// Apply damping based on the cursor velocity
force -= cursorVelocity * damping;
}
return force;
}
/// <summary>
/// Event handler that calculates and send the force to the device when the cursor's position changes.
/// </summary>
/// <param name="sender">The Inverse3 data object.</param>
/// <param name="args">The event arguments containing the device data.</param>
private void OnDeviceStateChanged(object sender, Inverse3EventArgs args)
{
var inverse3 = args.DeviceController;
// Calculate the ball force
var force = ForceCalculation(inverse3.CursorLocalPosition, inverse3.CursorLocalVelocity,
_cursorRadius, _ballPosition, _ballRadius);
inverse3.SetCursorLocalForce(force);
}
}
}