디바이스 워크스페이스 변환 튜토리얼
이 튜토리얼은 기본 포스 피드백 튜토리얼을 확장하여, 공간 변환 속성과 메서드를 사용해 Inverse3 장치의 위치, 회전, 스케일을 조정하는 방법을 보여 줍니다.
소개
기본 포스 피드백을 위한 씬을 설정한 후 Haptic Origin 또는 Haptic Controller를 이동, 스케일링, 회전하더라도 햅틱 피드백이 예상대로 반응하지 않는 것을 확인할 수 있습니다. 피드백은 여전히 장치 앞쪽에 위치한 보이지 않는 구체에 대응하며, 이러한 변환의 영향을 받지 않습니다.

이러한 불일치는 힘 계산이 커서의 실제 변환되지 않은 좌표를 사용하기 때문에 발생합니다. 이를 해결하기 위해 Inverse3 Controller 컴포넌트가 제공하는 스레드 안전 캐시 변환 행렬을 활용하여, 햅틱 피드백 계산에 월드 공간 변환을 적용하겠습니다.
씬 설정
기본 포스 피드백 튜토리얼의 씬에서 시작하여, Inverse3 Controller의 좌우 방향(handedness)이 장치와 일치하는지 확인하십시오. Haptic Controller를 회전하여 Haply 로고가 카메라를 향하도록 하고, 커서 범위를 넓히거나 위치를 조정하고 싶다면 Haptic Origin의 스케일을 원하는 대로 조정하십시오.


포스 피드백 스크립트 변경
기본 포스 피드백 튜토리얼의 SphereForceFeedback.cs 스크립트를 복사한 다음, OnDeviceStateChanged 콜백에서 다음과 같이 수정하십시오:
device.CursorLocalPosition을device.CursorPosition으로 바꾸십시오.device.CursorLocalVelocity를device.CursorVelocity로 바꾸십시오.device.CursorSetLocalForce(force)를device.CursorSetForce(force)로 바꾸십시오.
private void OnDeviceStateChanged(object sender, Inverse3EventArgs args)
{
var inverse3 = args.DeviceController;
var force = ForceCalculation(inverse3.CursorPosition, inverse3.CursorVelocity,
_cursorRadius, _ballPosition, _ballRadius);
inverse3.SetCursorForce(force);
}
게임플레이 경험
Inverse3 커서를 잡은 상태에서 Play Mode에 진입하십시오. 이전 예제에서와 같이 구체를 만져 보십시오. 이제 Haptic Origin과 Haptic Controller에 적용된 변환이 반영된 정확한 햅틱 피드백을 경험할 수 있습니다.

소스 파일
이 예제의 전체 씬과 관련 파일은 Unity Package Manager의 Tutorials 샘플에서 가져올 수 있습니다.
샘플 씬에는 런타임 중 Haptic Controller 및 Haptic Origin을 조정하기 위한 추가 스크립트가 포함되어 있습니다.
SphereForceFeedback.cs
/*
* Copyright 2024 Haply Robotics Inc. All rights reserved.
*/
using Haply.Inverse.DeviceControllers;
using Haply.Inverse.DeviceData;
using UnityEngine;
namespace Haply.Samples.Tutorials._3_DeviceSpaceTransform
{
public class SphereForceFeedback : MonoBehaviour
{
// must assign in inspector
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 ??= FindFirstObjectByType<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. Using 'device.Position' instead of 'device.LocalPosition'
// ensures the force calculation considers the device's offset and rotation in world space.
var force = ForceCalculation(inverse3.CursorPosition, inverse3.CursorVelocity,
_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.
inverse3.SetCursorForce(force);
}
}
}