동적 장면에서 힘의 피드백에 관한 튜토리얼
이 가이드는 기본 햅틱 피드백 튜토리얼을 바탕으로, 사용자가 움직이는 오브젝트에서 오는 햅틱 피드백을 느낄 수 있도록 Unity에서 역동적인 상호작용을 시뮬레이션하는 방법을 설명합니다. 이 시나리오는 햅틱 피드백을 구현하기 위해 높은 주파수의 업데이트가 필요함을 보여줍니다. 이 주파수는 Unity의 일반적인 시각적 렌더링 업데이트 빈도를 훨씬 뛰어넘는 수준입니다.
소개
특히 역동적인 장면에서 설득력 있는 햅틱 경험을 제공하려면 1kHz 이상의 주파수로 계산을 수행하는 것이 필수적입니다. 이는 대개 60Hz 정도에서 작동하는 일반적인 게임 업데이트 루크와 극명한 대조를 이룹니다. 여기서 과제는 게임의 메인 루크와 병행하여 이러한 고주파수 업데이트를 처리하는 동시에, 일관되고 정확한 힘 피드백을 유지하기 위해 스레드 간에 안전한 데이터 교환을 보장하는 것입니다.
기본 힘 피드백 설정 확장
다음 장면에서 시작하세요. 기본 힘 피드백 튜토리얼. 동적 동작을 구현하기 위해 스크립트를 수정할 것입니다. SphereForceFeedback 구체의 움직임에 반응하고 움직이는 물체와의 상호작용을 시뮬레이션할 수 있도록 하기 위해서입니다.
주요 변경 사항
- 객체의 동적 이동: 사용자의 입력이나 미리 정의된 이동 패턴에 따라 Sphere 객체의 위치와 속도를 업데이트하는 로직을 구현하세요.
- 스레드 간 안전한 데이터 교환 : 다음을 사용하십시오.
ReaderWriterLockSlim메인 스레드와 햅틱 스레드 간에 공유되는 데이터에 대한 동시 액세스를 관리하기 위해. - 힘 계산의 조정 : 메서드를 수정하세요
SphereForceFeedback.ForceCalculation구체의 속도를 고려하여, 위치와 움직임을 모두 바탕으로 한 현실감 있는 피드백을 제공하기 위함입니다.
동적 상호작용
움직이는 구체를 시뮬레이션하려면, 메서드 내에서 그 위치를 수동으로 업데이트하거나 Update, 또는 키보드 입력이나 기타 상호작용에 따라 이 객체의 이동을 제어하는 별도의 컴포넌트를 사용할 수도 있습니다.
이 예제에서는 객체의 이름을 구체 en 움직이는 공 그리고 해당 컴포넌트를 추가하겠습니다 MovingObject 예시에 포함된 튜토리얼.
원작: ForceCalculation 운동을 위해
이제 힘 피드백 계산 시 ‘Moving Ball’ 객체의 속도를 고려하여, 상호작용의 위치와 속도에 따라 힘을 조정해야 합니다. 이를 통해 상호작용의 역동적인 특성을 반영하는, 더욱 미묘하고 사실적인 햅틱 감각을 제공할 수 있습니다.
- 메서드 매개변수를 추가하세요
Vector3otherVelocity - Remplacez
force -= cursorVelocity * dampingparforce -= (cursorVelocity - otherVelocity) * damping
private Vector3 ForceCalculation(Vector3 cursorPosition, Vector3 cursorVelocity, float cursorRadius,
Vector3 otherPosition, Vector3 otherVelocity, 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;
// Calculate the relative velocity
var relativeVelocity = cursorVelocity - otherVelocity;
// Apply damping based on the relative velocity
force -= relativeVelocity * damping;
}
return force;
}
스레드 간 안전한 데이터 교환
물체가 실시간으로 이동하고 상호작용하는 역동적인 장면에서는, 동시 액세스로 인한 데이터 손상이 발생하지 않도록 햅틱 피드백 계산이 최신 데이터를 기반으로 이루어지도록 보장하는 것이 필수적입니다. 이때 스레드 간 안전한 데이터 교환이 필수적입니다.
스레드 간 안전한 데이터 교환의 핵심 개념
- 스레드를 위한 안전한 메커니즘 : 사용하세요
ReaderWriterLockSlim데이터에 대한 동시 액세스를 관리하기 위함입니다. 이를 통해 여러 번의 읽기 작업이나 단일 쓰기 작업을 허용함으로써 데이터 무결성을 보장합니다. - 데이터 읽기 및 쓰기 :
- 강의: 햅틱 스레드는 데이터 업데이트에 간섭을 주지 않도록, 읽기 잠금 상태 하에서 물체의 위치와 속도를 읽습니다.
- 쓰기: 메인 스레드에 의한 객체 데이터 업데이트는 쓰기 잠금 상태에서 수행되므로, 데이터의 일관성을 해칠 수 있는 동시 읽기 또는 쓰기가 방지됩니다.
Unity에서의 구현
-
씬 데이터 구조체: 스레드 간 안전한 연산을 용이하게 하기 위해, 씬에 필요한 모든 데이터를 포함하는 구조체를 정의합니다. 이 구조체에는 Moving Ball과 커서의 위치, 속도, 반지름이 포함됩니다. 이 데이터 구조체는 스레드 간 안전한 데이터 교환의 기반이 됩니다.
private struct SceneData{public Vector3 ballPosition;public Vector3 ballVelocity;public float ballRadius;public float cursorRadius;}private SceneData _cachedSceneData; -
잠금 장치 초기화 : ~의 인스턴스
ReaderWriterLockSlimest는 씬 데이터에 대한 액세스를 관리하기 위해 초기화됩니다. 이 잠금 장치는 여러 스레드가 데이터를 동시에 읽을 수 있도록 하거나, 단일 스레드가 데이터를 독점적으로 잠그고 쓰기 작업을 수행할 수 있도록 하여, 동시 작업 시 데이터 무결성을 보장합니다.private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); -
쓰기 잠금 상태에서 캐시에 쓰기 : 방법
SaveSceneData쓰기 잠금 상태에서 장면 데이터를 업데이트합니다. 이를 통해 한 스레드가 데이터를 업데이트하는 동안 다른 스레드는 읽거나 쓸 수 없게 되어, 경합 상태를 방지하고 일관성을 보장합니다.private void SaveSceneData(){_cacheLock.EnterWriteLock();try{var t = transform;_cachedSceneData.ballPosition = t.position;_cachedSceneData.ballRadius = t.lossyScale.x / 2f;_cachedSceneData.cursorRadius = inverse3.Cursor.Radius;_cachedSceneData.ballVelocity = _movableObject.Velocity;}finally{_cacheLock.ExitWriteLock();}} -
읽기 잠금 장치를 사용한 캐시 내 읽기 : 방법
GetSceneData씬 데이터를 읽기 잠금 상태로 가져옵니다. 이를 통해 여러 스레드가 쓰기 작업에 방해받지 않고 동시에 데이터를 읽을 수 있으므로, 햅틱 피드백 계산이 가장 최신의 씬 데이터를 기반으로 수행됩니다.private SceneData GetSceneData(){_cacheLock.EnterReadLock();try{return _cachedSceneData;}finally{_cacheLock.ExitReadLock();}} -
메인 스레드에서 데이터 업데이트 : 방법
FixedUpdate메인 스레드에서 씬 데이터를 주기적으로 업데이트해야 합니다. 이를 통해 햅틱 피드백 계산이 가장 최신 데이터에 접근할 수 있게 되어, 씬의 동적인 특성을 반영할 수 있습니다.private void FixedUpdate(){SaveSceneData();} -
업데이트된 데이터를 활용한 힘 계산 적용 : 콜백 함수 내에서
OnDeviceStateChanged, 힘 계산은 스레드 안전 방식에 따라 확보된 최신 씬 데이터를 기반으로 수행됩니다. 이를 통해 씬의 역동적인 상호작용에 대해 정확하고 반응성이 뛰어난 힘 피드백이 보장됩니다.var sceneData = GetSceneData();var force = ForceCalculation(device.CursorLocalPosition, device.CursorLocalVelocity, sceneData.cursorRadius,sceneData.ballPosition, sceneData.ballVelocity, sceneData.ballRadius);
게임 플레이
스크립트의 이러한 개선 사항 덕분에 사용자는 씬 내에서 능동적으로 움직이는 구체와 상호작용할 수 있습니다. 햅틱 피드백은 구체의 궤적에 따라 동적으로 조정되어, 더욱 몰입감 있고 풍부한 촉각적 경험을 제공합니다.

소스 파일
이 예제와 관련된 전체 씬 및 관련 파일은 Unity 패키지 관리자의 ‘Tutorials’ 예제에서 가져올 수 있습니다.
예시 튜토리얼 스크립트를 포함합니다
MovableObject, 여러 예제에서 키보드를 통해 연결된 게임 오브젝트의 이동을 제어하는 데 사용됩니다.
SphereForceFeedback.cs
/*
* Copyright 2024 Haply Robotics Inc. All rights reserved.
*/
using System.Threading;
using Haply.Inverse.DeviceControllers;
using Haply.Inverse.DeviceData;
using Haply.Samples.Tutorials.Utils;
using UnityEngine;
namespace Haply.Samples.Tutorials._4A_DynamicForceFeedback
{
public class SphereForceFeedback : MonoBehaviour
{
// must assign in inspector
public Inverse3Controller inverse3;
[Range(0, 800)]
// Stiffness of the force feedback.
public float stiffness = 500f;
[Range(0, 3)]
public float damping = 1f;
#region Thread-safe cached data
/// <summary>
/// Represents scene data that can be updated in the Update() call.
/// </summary>
private struct SceneData
{
public Vector3 ballPosition;
public Vector3 ballVelocity;
public float ballRadius;
public float cursorRadius;
}
/// <summary>
/// Cached version of the scene data.
/// </summary>
private SceneData _cachedSceneData;
private MovableObject _movableObject;
/// <summary>
/// Lock to ensure thread safety when reading or writing to the cache.
/// </summary>
private readonly ReaderWriterLockSlim _cacheLock = new();
/// <summary>
/// Safely reads the cached data.
/// </summary>
/// <returns>The cached scene data.</returns>
private SceneData GetSceneData()
{
_cacheLock.EnterReadLock();
try
{
return _cachedSceneData;
}
finally
{
_cacheLock.ExitReadLock();
}
}
/// <summary>
/// Safely updates the cached data.
/// </summary>
private void SaveSceneData()
{
_cacheLock.EnterWriteLock();
try
{
var t = transform;
_cachedSceneData.ballPosition = t.position;
_cachedSceneData.ballRadius = t.lossyScale.x / 2f;
_cachedSceneData.cursorRadius = inverse3.Cursor.Radius;
_cachedSceneData.ballVelocity = _movableObject.Velocity;
}
finally
{
_cacheLock.ExitWriteLock();
}
}
#endregion
/// <summary>
/// Saves the initial scene data cache.
/// </summary>
private void Start()
{
inverse3 ??= FindFirstObjectByType<Inverse3Controller>();
inverse3.Ready.AddListener((device, args) => SaveSceneData());
_movableObject = GetComponent<MovableObject>();
}
/// <summary>
/// Update scene data cache.
/// </summary>
private void FixedUpdate()
{
if (inverse3.IsReady)
{
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="otherVelocity">The velocity 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, Vector3 otherVelocity, 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;
// Calculate the relative velocity
var relativeVelocity = cursorVelocity - otherVelocity;
// Apply damping based on the relative velocity
force -= relativeVelocity * 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;
var sceneData = GetSceneData();
// Calculate the moving ball force.
var force = ForceCalculation(inverse3.CursorLocalPosition, inverse3.CursorLocalVelocity, sceneData.cursorRadius,
sceneData.ballPosition, sceneData.ballVelocity, sceneData.ballRadius);
// Apply the force to the cursor.
inverse3.SetCursorLocalForce(force);
}
}
}