跳转至正文

基础力反馈教程

本教程将演示如何模拟与球体等静态物体接触时的物理特性,并指导您创建一个同时考虑刚度(stiffness)和阻尼(damping)的基本触觉模拟。 完成本教程后,您将能够感知球体的存在,并完成一个可调整刚度和阻尼属性以实现多种触觉体验的模拟。

简介​

本教程的核心任务是开发一个函数,用于计算与同时具有刚度和阻尼特性的球体接触时产生的力。 在此背景下,刚度像弹簧一样起作用:压缩得越多,产生的力就越大。 而阻尼则表示对物体运动的阻力,物体运动得越快,产生的阻力就越大。

场景设置​

入门指南如文中所述,首先设置 Haptic Rig, 触觉起源其位置、旋转和缩放分别 (0, 0, 0)和 (1, 1, 1)请确认是否已设置为该选项。

然后,具有以下属性的 Sphere请生成一个名为 的球体:

  • 位置: (0, 0, -0.1) (距设备前端约10厘米)
  • 比例: (0.2, 0.2, 0.2) (相当于直径20厘米的球体)

力反馈脚本​

球体 在 GameObject 上 SphereForceFeedback.cs请添加一个名为的新 C# 脚本。 该脚本在与球体接触时,会综合考虑刚度和阻尼,计算作用在Inverse3 光标上的力。 请使用以下属性初始化该脚本:

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