Skip to main content
Version: 2.1.1

Basic Force-Feedback Tutorial

This tutorial guides you through creating a basic haptic simulation that incorporates both stiffness and damping, simulating the physical properties of contact with a static object, such as a sphere. By the end of this tutorial, you'll have a simulation that allows you to feel the presence of a sphere and adjust its stiffness and damping properties for different haptic experiences.

Introduction

The core challenge of this tutorial is to develop a function capable of calculating the forces resulting from contact with a sphere that exhibits both stiffness and damping. Stiffness in this context acts like a spring, generating more force the more it is compressed. Damping, on the other hand, represents an object's resistance to movement, offering more resistance the faster it is moved through.

Scene Setup

Begin by setting up a Haptic Rig as outlined in the Quick Start Guide, ensuring the Haptic Origin's position, rotation, and scale are set to (0, 0, 0) and (1, 1, 1) respectively.

Then, create a sphere named Sphere with the following properties:

  • Position: (0, 0, -0.1) (approximately 10cm in front of the device)
  • Scale: (0.2, 0.2, 0.2) (corresponding to a 20cm diameter sphere)

Force Feedback Script

Add a new C# script to the Sphere GameObject named SphereForceFeedback.cs. This script will calculate the forces applied to the Inverse3 cursor upon contact with the sphere, taking into account both stiffness and damping. Initialize the script with the following properties:

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

The force calculation should only occur when the cursor penetrates the sphere, simulating the sensation of touching a physical object with defined stiffness and damping. The ForceCalculation method will be responsible for this, considering both the cursor's position and velocity:

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(Inverse3 device)
{
// Calculate the ball force
var force = ForceCalculation(device.CursorLocalPosition, device.CursorLocalVelocity,
_cursorRadius, _ballPosition, _ballRadius);

device.CursorSetLocalForce(force);
}

In the Awake method, initialize _ballPosition, _ballRadius, and _cursorRadius to set up the scene data:

private void SaveSceneData()
{
var t = transform;
_ballPosition = t.position;
_ballRadius = t.lossyScale.x / 2f;

_cursorRadius = inverse3.Cursor.Model.transform.lossyScale.x / 2f;
}

private void Awake()
{
SaveSceneData();
}

Ensure to register and unregister the OnDeviceStateChanged callback in the OnEnable and OnDisable methods, respectively, to properly handle the force feedback during interaction:

protected void OnEnable()
{
inverse3.DeviceStateChanged += OnDeviceStateChanged;
}

protected void OnDisable()
{
inverse3.DeviceStateChanged -= OnDeviceStateChanged;
}

Gameplay Experience

While holding the Inverse3 cursor, enter Play Mode and attempt to touch the sphere. You should be able to feel the sphere's presence and manipulate its stiffness and damping properties through the Unity Inspector, providing a tangible sense of interaction with a virtual object.

cursor hit sphere

Source files

The complete scene and all associated files for this example are available for import from the Tutorials sample in the Unity Package Manager.

SphereForceFeedback.cs

/*
* Copyright 2024 Haply Robotics Inc. All rights reserved.
*/

using Haply.Inverse.Unity;
using UnityEngine;

namespace Haply.Samples.Tutorials._2_BasicForceFeedback
{
public class SphereForceFeedback : MonoBehaviour
{
// must assign in inspector
public Inverse3 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.Model.transform.lossyScale.x / 2f;
}

/// <summary>
/// Saves the initial scene data cache.
/// </summary>
private void Awake()
{
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;
}

/// <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="device">The Inverse3 device instance.</param>
private void OnDeviceStateChanged(Inverse3 device)
{
// Calculate the ball force
var force = ForceCalculation(device.CursorLocalPosition, device.CursorLocalVelocity,
_cursorRadius, _ballPosition, _ballRadius);

device.CursorSetLocalForce(force);
}
}
}