Physik-basierte Kraftrückkopplung [Experimentell]
Bei früheren Beispielen wurden primitive Objekte wie eine Ebene oder eine Kugel gerendert, bei denen die Kraftrückkopplung Berechnungen aufgrund ihrer Einfachheit von Hand kodiert wurden. Wenn die Objekte komplexer werden, werden manuelle Berechnungen mühsam und kompliziert. Physik-Engines wie SOFA, IMSTK und CHAI3D sind sind stark für haptische Anwendungen optimiert, so dass sie bei Frequenzen über 1000 Hz arbeiten können realistische haptische Simulationen zu erstellen, aber sie müssen in Unity integriert werden. Unity hat eine eingebaute Physik-Engine, die für einfache Simulationen verwendet werden kann. Dieser Artikel zeigt, wie man eine haptische Szene erstellt und die Physik-Engine in eine haptische Simulation mit kinematischen und nicht-kinematischen Objekten.
Einführung
Unity hat zwei Arten von RigidBodies: kinematische und nicht-kinematische. Kinematische Objekte werden gesteuert durch Skripte gesteuert, wie der Cursor in den vorherigen Beispielen. Im Gegensatz dazu werden nicht-kinematische Körper durch Kollisionen und die daraus resultierenden Kräfte gesteuert. Die Herausforderung liegt im Austausch von Kraftdaten zwischen dem Benutzer und dem nicht-kinematischen Objekt, da weder die Krafteingabe noch die Kraftausgabe direkt gemessen werden können. direkt.
Um dieses Problem zu umgehen, verwenden wir ConfigurableJoints, um eine virtuelle Kupplung zu erstellen, die ein kinematisches und ein nicht-kinematisches Objekt mit einem virtuellen Gestänge aus einer Feder und einem Dämpfer verbindet. Wenn ein nicht-kinematisches Objekt mit einem anderen nicht-kinematischen Objekt kollidiert, wird es wird es an der Bewegung gehindert, aber das kinematische Objekt bewegt sich weiter, dehnt die Feder und erzeugt eine Kraft. Wir können die resultierende Kraft direkt zum Rendern der Objekte verwenden.
Szene einrichten
Die Umsetzung beruht auf der Verwendung von zwei Objekten im Tandem:
- Das Cursor-Objekt (kinematisch), das der Cursor-Position Ihres Geräts entspricht
- Der PhysicEffector (nicht-kinematisch), der mit dem Cursor Objekt über ein festes Gelenk verbunden ist.
Die von Ihrem Gerät erzeugte Kraft ist relativ zum Abstand zwischen diesen beiden Objekten, so dass dass, wenn das PhysicsEffector-Objekt durch ein anderes Objekt in der Szene blockiert wird, eine entgegengesetzte Kraft proportional zum Abstand vom Cursor-Objekt erzeugt wird.
- Fügen Sie einen haptischen Faden und einen Cursor hinzu, wie in der Schnellstartanleitung gezeigt.
- Erstellen Sie einen Arbeitsbereich, wie in Arbeitsbereichsskalierung und -platzierung gezeigt.
- Erstellen Sie eine Kugel namens Physikeffektor unter Haptischer Arbeitsbereich mit den denselben Transformationswerten wie Cursor.
- Fügen Sie verschiedene 3D-Objekte mit Kollidern in die Szene ein.
- Fügen Sie einen Würfel mit einem starren Körper hinzu, dessen Schwerkraft aktiviert ist und der eine Masse von
1000
.
optional: Sie können die integrierte
SpatialMappingWideframe
Material über PhysicEffector zu sehen, wie sich die Kugel aufgrund der Reibung dreht, wenn man sie über die Oberfläche bewegt Reibung.
Einfache haptische Physikschleife
Hinzufügen einer neuen C#-Skript genannt. SimplePhysicsHapticEffector.cs
zum
PhysicEffector Spielobjekt. Die Quelle für dieses Skript ist unten angegeben.
using Haply.HardwareAPI.Unity;
using UnityEngine;
public class SimplePhysicsHapticEffector : MonoBehaviour
{
// Thread safe scene data
private struct AdditionalData
{
public Vector3 physicEffectorPosition;
}
public bool forceEnabled;
[Range(0, 800)]
public float stiffness = 400f;
[Range(0, 3)]
public float damping = 1;
private HapticThread m_hapticThread;
private void Awake ()
{
// Find the HapticThread object before the first FixedUpdate() call.
m_hapticThread = FindObjectOfType<HapticThread>();
// Create the physics link between the physic effector and the device cursor
AttachCursor( m_hapticThread.avatar.gameObject );
}
private void OnEnable ()
{
// Run haptic loop with AdditionalData method to get initial values
if (m_hapticThread.isInitialized)
m_hapticThread.Run(ForceCalculation, GetAdditionalData());
else
m_hapticThread.onInitialized.AddListener(() => m_hapticThread.Run(ForceCalculation, GetAdditionalData()) );
}
private void FixedUpdate () =>
// Update AdditionalData
m_hapticThread.SetAdditionalData( GetAdditionalData() );
// Attach the current physics effector to the device end-effector with a fixed joint
private void AttachCursor (GameObject cursor)
{
// Add a kinematic rigidbody to the cursor.
var rbCursor = cursor.GetComponent<Rigidbody>();
if ( !rbCursor )
{
rbCursor = cursor.AddComponent<Rigidbody>();
rbCursor.useGravity = false;
rbCursor.isKinematic = true;
}
// Add a non-kinematic rigidbody to self
if ( !gameObject.GetComponent<Rigidbody>() )
{
var rb = gameObject.AddComponent<Rigidbody>();
rb.useGravity = false;
}
// Connect self to the cursor rigidbody
if ( !gameObject.GetComponent<FixedJoint>() )
{
var joint = gameObject.AddComponent<FixedJoint>();
joint.connectedBody = rbCursor;
}
}
// Method used by HapticThread.Run(ForceCalculation) and HapticThread.GetAdditionalData()
// to synchronize the physic effector position information between the physics thread and the haptic thread.
private AdditionalData GetAdditionalData ()
{
AdditionalData additionalData;
additionalData.physicEffectorPosition = transform.localPosition;
return additionalData;
}
// Calculate the force to apply based on the distance between the two effectors.
private Vector3 ForceCalculation ( in Vector3 position, in Vector3 velocity, in AdditionalData additionalData )
{
if ( !forceEnabled )
{
return Vector3.zero;
}
var force = additionalData.physicEffectorPosition - position;
force *= stiffness;
force -= velocity * damping;
return force;
}
}
Mit dieser Einstellung sollten Sie in der Lage sein, jedes Objekt in der Szene zu fühlen, wobei schwerere Objekte mehr Widerstand bieten. Widerstand bieten.
Probleme:
- Das Gefühl von Reibung/Widerstand wird durch den Unterschied in der Update Aktualisierungsfrequenz zwischen der Physik-Engine von Unity (zwischen 60Hz und 120Hz) und dem Haptik Thread (~1000Hz). Dieser Unterschied bedeutet, dass der Physikeffektor immer der wahren Position des Cursors hinterherhinkt, was zu Kräften führt, die eher einer Stufenfunktion ähneln, anstatt kontinuierlich zu sein.
- Keine echte Haptik bei sich bewegenden Objekten.
Lösungen:
- Verringern Sie den Wert von
ProjectSettings.FixedTimestep
so nah an0.001
wie möglich. Diese Änderung wird erhebliche Auswirkungen auf die Leistungen bei komplexen Szenen. - Kräfte nur dann anwenden, wenn es zu Kollisionen kommt (siehe das nächste Beispiel)
- Verwenden Sie eine Physik-/Haptik-Engine eines Drittanbieters wie (TOIA, SOFA, etc...) als Middleware zwischen der Physik-Engine von Unity und der haptischen Schleife, um die Kontaktpunkte mit einer höheren Frequenz zu simulieren.
Erweiterte haptische Physikschleife
In diesem Beispiel werden wir:
-
Verwenden Sie den Ausgang der Kollisionserkennung, um das Gefühl von Reibung/Widerstand zu vermeiden zu vermeiden, wenn der Effektor keinen Kontakt mit einem Objekt hat.
-
Ein ConfigurableJoint mit einem Limit, einer Feder und einem Dämpfer als Verbindung zwischen den zwei Effektoren anstelle eines FixedJoints. Dies ermöglicht uns die Verwendung von Unitys PhysicsMaterials zu verwenden, um verschiedene Reibungswerte für die Objekte in der Szene zu setzen und die Masse eines beweglichen Objekts durch das Force-Feedback zu spüren.
Auf der Physikalischer Effektor Spielobjekt, ersetzen Sie die
VereinfachtePhysikHaptikEffektoren Skriptkomponente durch eine neue C#-Skript
genannt. AdvancedPhysicsHapticEffector.cs
using Haply.HardwareAPI.Unity;
using System.Collections.Generic;
using UnityEngine;
public class AdvancedPhysicsHapticEffector : MonoBehaviour
{
/// Thread-safe scene data
private struct AdditionalData
{
public Vector3 physicEffectorPosition;
public bool isTouching;
}
public bool forceEnabled;
[Range(0, 800)]
public float stiffness = 400f;
[Range(0, 3)]
public float damping = 1;
private HapticThread m_hapticThread;
// Apply forces only when we're colliding with an object which prevents feeling
// friction/drag while moving through the air.
public bool collisionDetection = true;
private List<Collider> m_Touched = new();
private void Awake ()
{
// Find the HapticThread object before the first FixedUpdate() call.
m_hapticThread = FindObjectOfType<HapticThread>();
// Create the physics link between the physic effector and the device cursor
AttachCursor( m_hapticThread.avatar.gameObject );
}
private void OnEnable ()
{
// Run haptic loop with AdditionalData method to get initial values
if (m_hapticThread.isInitialized)
m_hapticThread.Run(ForceCalculation, GetAdditionalData());
else
m_hapticThread.onInitialized.AddListener(() => m_hapticThread.Run(ForceCalculation, GetAdditionalData()) );
}
private void FixedUpdate () =>
// Update AdditionalData with the latest physics data.
m_hapticThread.SetAdditionalData( GetAdditionalData() );
/// Attach the current physics effector to the device end-effector with a joint
private void AttachCursor (GameObject cursor)
{
// Add a kinematic rigidbody to the cursor.
var rbCursor = cursor.AddComponent<Rigidbody>();
rbCursor.useGravity = false;
rbCursor.isKinematic = true;
// Add a non-kinematic rigidbody to self.
var rb = gameObject.AddComponent<Rigidbody>();
rb.useGravity = false;
rb.drag = 80f; // stabilize spring connection
// Connect self with the cursor rigidbody via a spring/damper joint and a locked rotation.
var joint = gameObject.AddComponent<ConfigurableJoint>();
joint.connectedBody = rbCursor;
joint.anchor = joint.connectedAnchor = Vector3.zero;
joint.axis = joint.secondaryAxis = Vector3.zero;
// Limit linear movements.
joint.xMotion = joint.yMotion = joint.zMotion = ConfigurableJointMotion.Limited;
// Configure the limit, spring and damper
joint.linearLimit = new SoftJointLimit()
{
limit = 0.001f
};
joint.linearLimitSpring = new SoftJointLimitSpring()
{
spring = 500000f,
damper = 10000f
};
// Lock the rotation to prevent the sphere from rolling due to friction with the material which will
// improve the force-feedback feeling.
joint.angularXMotion = joint.angularYMotion = joint.angularZMotion = ConfigurableJointMotion.Locked;
// Set the first collider which handles collisions with other game objects.
var sphereCollider = gameObject.GetComponents<SphereCollider>();
sphereCollider.material = new PhysicMaterial {
dynamicFriction = 0,
staticFriction = 0
};
// Set the second collider as a trigger that is a bit larger than our first collider. It will be used to
// detect when our effector is moving away from an object it was touching.
var trigger = gameObject.AddComponent<SphereCollider>();
trigger.isTrigger = true;
trigger.radius = sphereCollider.radius * 1.08f;
}
// Method used by HapticThread.Run(ForceCalculation) and HapticThread.GetAdditionalData()
// to synchronize the physic effector position information between the physics thread and the haptic thread
private AdditionalData GetAdditionalData ()
{
AdditionalData additionalData;
additionalData.physicEffectorPosition = transform.localPosition;
additionalData.isTouching = collisionDetection && m_Touched.Count > 0;
return additionalData;
}
// Calculate the force to apply based on the distance between the two effectors
private Vector3 ForceCalculation ( in Vector3 position, in AdditionalData additionalData )
{
if ( !forceEnabled || (collisionDetection && !additionalData.isTouching) )
{
// Don't compute forces if there are no collisions which prevents feeling drag/friction while moving through air.
return Vector3.zero;
}
var force = additionalData.physicEffectorPosition - position;
force *= stiffness;
return force;
}
private void OnCollisionEnter ( Collision collision )
{
if ( forceEnabled && collisionDetection && !m_Touched.Contains( collision.collider ) )
{
// Store the object that our effector is touching.
m_Touched.Add( collision.collider );
}
}
private void OnTriggerExit ( Collider other )
{
if ( forceEnabled && collisionDetection && m_Touched.Contains( other ) )
{
// Remove the object when our effector moves away from it.
m_Touched.Remove( other );
}
}
}
Quelldateien
Die endgültige Szene und alle zugehörigen Dateien, die in diesem Beispiel verwendet werden, können über den Paketmanager von Unity importiert werden Manager importiert werden.
Zusätzliche Merkmale:
- Wählen Sie zwischen dem vereinfachten und dem erweiterten Physikeffektor, indem Sie die Taste
1
oder2
Schlüssel. - Kontrolle der Haptik Framerate mit dem
LEFT
/RIGHT
Tasten. - Kontrolle der Framerate der Physik mit dem
UP
/DOWN
Tasten. - Umschalten auf Kollisionserkennung mit dem
C
Schlüssel. - Umschalten auf Kraftrückkopplung mit dem
SPACE
Schlüssel. - Eine vorbereitete Szene mit statischen und dynamischen Objekten, die unterschiedliche Masse und PhysicsMaterialien haben.
- Ein UI zur Anzeige der Eigenschaften von berührten Objekten (statische/dynamische Reibung, Masse, Widerstand...)