MetaQuestでオブジェクトマニピュレーション（XRI版）

## 0. 本記事の内容

本記事では**XR Interaction Toolkit**を使用してMeta Questで3Dオブジェクト（立方体）をつかんで操作するマニピュレーションを実現する方法を紹介します。手やコントローラでオブジェクトを掴み、移動・回転・スケール変更を行う基本的な操作を学習できます。

GitHubで公開している[サンプル](https://github.com/TakashiYoshinaga/MetaXR-SDK-Samples)のXRI_Samplesプロジェクト、02-Manipulationまたは02-Manipulation-ARシーンでも動作を確認できます。

[![XRI Manipulation Demo](https://img.youtube.com/vi/atliCICwqI4/0.jpg)](https://www.youtube.com/watch?v=atliCICwqI4)

*この動画はAR版ですが記事の内容はAR/VR両対応です

なお本記事は下記で作成したシーンを編集してマニピュレーション機能を追加します。VRまたはAR機能が設定済みのシーンをご用意ください。

**[VR版]**

[MetaQuestでオブジェクトを表示（XRI版）](https://qiita.com/Tks_Yoshinaga/items/47d1cc29250879cff214)

**[AR版]**

[MetaQuestのパススルーを使ったAR表示（XRI版）](https://qiita.com/Tks_Yoshinaga/items/9afb32731f457caabb67)

## 1. シーンの複製

前回の記事で作成したシーンを編集することも可能ですが、既存のシーンを破壊せずにマニピュレーション機能を試すため、シーンを複製する方法を紹介します。不要な場合は読み飛ばしてください。

- 前回までに作成したVR版またはAR版のシーンを開く
- **File -> Save As...**をクリックして現在のシーンを新しい名前で保存  
  *本記事では**Manipulation**とします
- Hierarchyに表示されるシーン名が**Manipulation**になっていることを確認

## 2. オブジェクトの接触検知を有効化

オブジェクトをつかんで操作するには手やコントローラとオブジェクトとの接触を検知する必要があります。ここで解説するオブジェクトとの接触検知に関する手順は、Quest開発に限らずUnityでの開発に共通する内容となります。

**[Colliderの確認]**

- Hierarchyで操作対象のオブジェクト（**Cube**）を選択
- Inspectorで表示されるCubeの詳細情報に**Box Collider**が適用されていることを確認  
  *Cube以外のオブジェクトの場合は、各オブジェクトに合わせたColliderを使用  
  *自作のモデルの場合は手作業でColliderを追加

**[Rigidbodyの追加と設定]**

- Inspector下方の**Add Component**ボタンをクリック
- 検索領域に**Rigidbody**と入力
- 表示された候補の中から**Rigidbody**を選択
- InspectorにRigidbodyが追加されたことを確認し、下記のように変更

| 設定項目 | 設定値 | 説明 |
|----------|--------|------|
| **Use Gravity** | OFF | 重力の影響を無効化 |
| **Is Kinematic** | ON | 物理演算を無効化 |

**重要：** 接触検知にはColliderとRigidbodyが必要になるので、必ず対象となるオブジェクトにセットしてください。

## 3. つかんで操作できる属性の追加

ここではオブジェクトを「つかめる」ようにする、「移動可能」にするという2種類の属性を追加します。

**[Interactableコンポーネントの追加]**

- HierarchyでCubeを選択
- **Add Component**をクリック後、**XRGrabInteractable**で検索
- 候補の中から**XR Grab Interactable**を選択  

これだけで基本的なマニピュレーション（つかんで移動）が動作します。なおコントローラを使ってオブジェクトをつかむ場合は中指の**グリップボタン**を使用します。

ただし、現状の設定では手やコントローラでつかんだときにオブジェクトの向きが変更されてしまいます。つかんだ瞬間の位置や向きをキープする場合は下記を追加で設定します。

- **Cube**をクリック
- Inspectorで**Use Dynamic Attach**を**ON**

**[操作内容のON/OFF（オプション）]**

**XR Grab Interactable**コンポーネント内の下記の項目のチェックを切り替えることで、マニピュレーションによる値の変化の可否を設定できます。

| 設定項目 | 説明 |
|----------|------|
| **Track Position** | 位置の変更を有効/無効 |
| **Track Rotation** | 回転の変更を有効/無効 |
| **Track Scale** | スケールの変更を有効/無効 |

*スケール変更を有効化するには次の操作も必要

## 4. スケール変更の設定（オプション）

両手で操作をする際にオブジェクトのスケールを変更したい場合は下記の設定も行ってください。

**[基本設定]**

- HierarchyでCubeを選択
- Inspectorで**XR Grab Interactable**を見つける
- **Select Mode**を**Multiple**に変更
- **Add Component**をクリック後、**XRGeneral**で検索
- **XR General Grab Transformer**を追加
- **Allow Two Handed Scaling**を**ON**  
  
<br>
  
**[スケーリング時の回転制御（オプション）]**

スケーリング中にオブジェクトを回転させたくない場合は下記のスクリプトを作成して追加してください。

```csharp
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
using UnityEngine.XR.Interaction.Toolkit.Interactables;

public class TwoHandsScaleController : MonoBehaviour
{
    [SerializeField]
    private XRGrabInteractable grabInteractable;

    void Start()
    {
        if (grabInteractable == null)
            grabInteractable = GetComponent<XRGrabInteractable>();

        if (grabInteractable != null)
        {
            grabInteractable.selectEntered.AddListener(OnSelectEntered);
            grabInteractable.selectExited.AddListener(OnSelectExited);
        }
    }

    private void OnSelectEntered(SelectEnterEventArgs args)
    {
        CheckGrabCount();
    }

    private void OnSelectExited(SelectExitEventArgs args)
    {
        CheckGrabCount();
    }

    private void CheckGrabCount()
    {
        int grabCount = grabInteractable.interactorsSelecting.Count;
        
        if (grabCount >= 2)
        {
            Debug.Log("Grabbed with both hands!");
            OnTwoHandGrab();
        }
        else if (grabCount == 1)
        {
            Debug.Log("Grabbed with one hand");
            OnSingleHandGrab();
        }
        else
        {
            Debug.Log("Not grabbed");
            OnNoGrab();
        }
    }

    private void OnTwoHandGrab()
    {
        // 両手でつかんだ時の制御
        // スケーリングを有効にし、回転制御を調整
        grabInteractable.trackRotation = false; // 両手操作時は回転追跡を無効
        grabInteractable.trackPosition = false; // 必要に応じて位置追跡も無効
    }

    private void OnSingleHandGrab()
    {
        // 片手でつかんだ時の制御
        // 現在は特別な動作は不要
        grabInteractable.trackRotation = true;
        grabInteractable.trackPosition = true;
    }

    private void OnNoGrab()
    {
        // つかんでいない時の制御
        // デフォルト状態に復元
        grabInteractable.trackRotation = true; // 回転追跡を有効
        grabInteractable.trackPosition = true; // 位置追跡を有効
    }

    void OnDestroy()
    {
        if (grabInteractable != null)
        {
            grabInteractable.selectEntered.RemoveListener(OnSelectEntered);
            grabInteractable.selectExited.RemoveListener(OnSelectExited);
        }
    }
}
```

**[スクリプトの適用]**

- 上記のスクリプトを**TwoHandsScaleController.cs**として保存
- **Cube**オブジェクトにこのスクリプトをアタッチ
- **Grab Interactable**フィールドに同じオブジェクトの**XR Grab Interactable**コンポーネントを設定

## 5. 動作確認

**[基本操作の確認]**

- アプリを実行し、コントローラの**グリップボタン**または手でオブジェクトをつかむ
- オブジェクトが手やコントローラの動きに追従することを確認
- 両手でつかんだ場合、スケール変更が可能であることを確認

**[操作方法]**

| 操作 | 方法 |
|------|------|
| **つかむ** | グリップボタン（コントローラ）/ 手を閉じる（ハンドトラッキング） |
| **移動** | つかんだ状態で手やコントローラを移動 |
| **回転** | つかんだ状態で手やコントローラを回転 |
| **スケール** | 両手でつかんで手の距離を変更 |

## 6. 次のステップ

ここまでの操作でコントローラや手につかまれた場合にオブジェクト（Cube）の位置・角度・スケールが変更されるという設定が完了しました。次はUIの操作方法についても紹介します。

[ボタンを例にしたUnityのUI操作（XRI版）](https://qiita.com/Tks_Yoshinaga/items/d27570cd338cef31ea27)

## 7. XR Interaction Toolkitに関する記事一覧はこちら

[XR Interaction ToolkitではじめようQuestアプリ開発](https://qiita.com/Tks_Yoshinaga/items/9e691eb679fff982a1c9)