物理学に基づくフォース・フィードバック【実験的
これまでの例では、平面や球体のようなプリミティブなオブジェクトをレンダリングしていた。 計算を手作業でコーディングしていた。オブジェクトが複雑になればなるほど、手作業での計算は面倒で複雑になる。 計算は面倒で複雑になります。SOFA、IMSTK、CHAI3Dのような物理エンジンは、ハプティックアプリケーションに最適化されています。 ハプティックアプリケーション用に最適化されているため、1000Hzを超える周波数で実行することができます。 現実的なハプティックシミュレーションを作成できますが、Unityに統合する必要があります。Unityには 物理エンジンが組み込まれており、簡単なシミュレーションに使用できます。この記事では、ハプティックシーン シーンを構築し、キネマティックと非キネマティックオブジェクトを使用したハプティックシミュレーションに物理エンジンを組み込む方法を紹介します。 オブジェクトを使います。
はじめに
Unityにはキネマティックとノンキネマティックの2種類のRigidBodiesがあります。キネマティックオブジェクトは スクリプトによって制御されます。一方、ノンキネマティックボディは 衝突とその結果生じる力によって制御されます。課題は、ユーザーと非キネマティックオブジェクトの間で力データを交換することにある。 力の入力も出力も直接測定できないためです。 直接測定することができないからです。
この問題を回避するために、ConfigurableJointsを使用して、キネマティックオブジェクトとノンキネマティックオブジェクトをバネとダンパーで構成される仮想リンクで結合する仮想カップリングを作成します。 キネマティックオブジェクトとノンキネマティックオブジェクトを、バネとダンパーで構成された仮想リンケージで結合します。 事実上、非キネマティックオブジェクトが他の非キネマティックオブジェクトと衝突すると、そのオブジェクトは移動を妨げられます。 キネマティック・オブジェクトは動き続け、バネを伸ばし、力を生み出す。 力が発生する。オブジェクトのレンダリングには、この力を直接使うことができます。
シーン設定
この実装は、2つのオブジェクトを同時に使用することに依存している:
- デバイスのカーソル位置に合わせるカーソルオブジェクト(キネマティック
- PhysicEffector(非キネマティック)。 オブジェクトにリンクされます。
デバイスによってレンダリングされる力は、これら2つのオブジェクト間の距離に対する相対的なものになります。 PhysicsEffectorオブジェクトがシーン内の別のオブジェクトによってブロックされると、Cursorオブジェクトからの距離に比例した反対の力が発生します。 カーソルオブジェクトからの距離に比例した力が生成されます。
- クイックスタートガイドにあるように、触覚スレッドと カーソルを追加します。
- ワークスペースのスケーリングと配置」に示すように、ワークスペースを作成します。
- Haptic Workspaceの下に、Physics EffectorというSphereを、Cursorと同じトランスフォーム値で作成します。 を作成します。
- シーンにコライダーで様々な3Dオブジェクトを追加。
- 剛体、重力が有効で、質量が以下のキューブを追加する。
1000
.
オプション:内蔵の
SpatialMappingWideframe
素材 物理エフェクタ 球体を動かすと、摩擦によって球体がどのように回転するか見てみよう。 摩擦
シンプルな物理ハプティック・ループ
新規追加 C#スクリプト という SimplePhysicsHapticEffector.cs
に対する
物理エフェクタ ゲーム・オブジェクトを生成する。このスクリプトのソースを以下に示す。
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;
}
}
このセットアップでは、シーン内の各オブジェクトを感じることができるはずだ。 抵抗を感じることができるはずです。
問題だ:
- 摩擦/ドラッグの感覚は、Unityの物理エンジン(60Hzから120Hzの間)とハプティクス(触覚)の更新周波数の違いによって引き起こされます。 Unityの物理エンジン(60Hz~120Hz)とハプティックスレッド(~1000Hz)の間の更新周波数の違いによるものです。 スレッド(~1000Hz)との間の更新頻度の違いによるものです。この違いは、物理エフェクタが常に 物理エフェクタは常にカーソルの真の位置より遅れていることを意味します。 ステップ関数のような力になります。
- 動いている物体に本物のハプティクスはない。
解決策
- の価値を下げる
ProjectSettings.FixedTimestep
に近い。0.001
可能な限り 可能である。この変更は、複雑なシーンの演技に大きな影響を与える。 シーンに大きな影響を与える。 - 衝突が起きたときだけ力を加える(次の例を参照)
- unityの物理エンジンとハプティックループの間のミドルウェアとして、サードパーティの物理/ハプティックエンジン(TOIA、SOFAなど)を使用する。 unityの物理エンジンとハプティックループの間のミドルウェアとして使ってください。 より高い周波数で接触点をシミュレートします。
より高度な物理ハプティック・ループ
この例では、次のようにする:
-
衝突検出出力を使用して、エフェクタがオブジェクトに接触していないときの摩擦/ドラッグの感覚を回避します。 エフェクタがオブジェクトに接触していないときの摩擦/ドラッグの感覚を避けるために、衝突検出出力を使用します。
-
2つのエフェクタ間のリンクとして、リミット、スプリング、ダンパーを備えたコンフィギュラブルジョイント。 FixedJointの代わりに、2つのエフェクタ間のリンクとしてリミットとスプリングとダンパーを持つConfigurableJointを使用します。これにより、Unityの PhysicsMaterialsを使用して、シーン内のオブジェクトに異なる摩擦値を設定できます。 シーンのオブジェクトに異なる摩擦値を設定し、フォースフィードバックを通して移動可能なオブジェクトの質量を感じることができます。
について 物理エフェクター ゲームオブジェクトを
簡易物理学ハプティクスエフェクタ スクリプトコンポーネントを新しい C#スクリプト
という 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 );
}
}
}
ソースファイル
この例で使用する最終的なシーンとすべての関連ファイルは、Unityのパッケージマネージャ マネージャーからインポートできます。
その他の特徴
- を押して、簡易または高度な物理エフェクターを選択します。
1
または2
キーだ。 - をコントロールする。 ハプティクス・フレームレート を持つ。
LEFT
/RIGHT
キーだ。 - をコントロールする。 物理フレームレート を持つ。
UP
/DOWN
キーだ。 - トグル 衝突検知 を持つ。
C
キーだ。 - トグル フォース・フィードバック を持つ。
SPACE
キーだ。 - 異なる質量を持つ静的オブジェクトと動的オブジェクトが用意されたシーン とPhysicsMaterialsを持ちます。
- タッチしたオブジェクトのプロパティを表示するUI(静的/動的摩擦、 質量、ドラッグ...)