Skip to main content
Version: 2.0.0

Device Workspace Transform Tutorial

This tutorial expands on the Basic Force-Feedback tutorial by demonstrating how to adjust the position, rotation, and scaling of the Inverse3 device using its space transformation properties and methods.

Introduction

After setting up the scene for basic force feedback, you may notice that adjustments to the Haptic Origin or Haptic Controller—such as moving, scaling, or rotating—do not affect the haptic feedback as expected. The feedback still corresponds to an invisible sphere located in front of the device, unaffected by these transformations.

workspace-transform-fail

This discrepancy occurs because the force calculations use the real, non-transformed coordinates of the device's cursor. To address this, we'll utilize thread-safe cached transformation matrices provided by the Inverse3 Controller component, allowing us to apply world space transformations to the haptic feedback calculations.

Scene Setup

Begin with the scene from the Basic Force-Feedback tutorial, ensuring the Inverse3 Controller's handedness matches your device. Rotate the Haptic Controller so that the Haply logo faces the camera, and adjust the Haptic Origin scale as desired to enhance cursor range or adjust its position.

inverse3-right-handed-setup

workspace-transform-right-handed

Force Feedback Script Changes

Copy the SphereForceFeedback.cs script from the Basic Force-Feedback tutorial and make the following adjustments in the OnDeviceStateChanged callback:

  • Replace device.LocalPosition with device.Position.
  • Replace device.LocalVelocity with device.Velocity.
  • Replace device.CursorSetLocalForce(force) with device.CursorSetForce(force).
private void OnDeviceStateChanged(Inverse3 device) {
var force = ForceCalculation(device.Position, device.Velocity,
_cursorRadius, _ballPosition, _ballRadius);

device.CursorSetForce(force);
}

Gameplay Experience

Hold the Inverse3 cursor and enter Play Mode. Attempt to touch the sphere as in the previous example. You should now experience accurate haptic feedback that reflects the transformations applied to the Haptic Origin and Haptic Controller.

cursor-hit-sphere-upscaled

Source Files

The complete scene and associated files for this example can be imported from the Tutorials sample in the Unity Package Manager.

The sample scene includes additional scripts for runtime adjustments of the Haptic Controller and Haptic Origin.

SphereForceFeedback.cs

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

using Haply.Inverse.Unity;
using UnityEngine;
using UnityEngine.Serialization;

namespace Haply.Samples.Tutorials._3_DeviceSpaceTransform
{
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. Using 'device.Position' instead of 'device.LocalPosition'
// ensures the force calculation considers the device's offset and rotation in world space.
var force = ForceCalculation(device.Position, device.Velocity,
_cursorRadius, _ballPosition, _ballRadius);

// Apply the calculated force to the cursor. Using 'device.CursorSetForce' instead of
// 'device.CursorSetLocalForce' ensures that the force vector is correctly converted
// from world space to the device's local space.
device.CursorSetForce(force);
}
}
}