本文へスキップ

深度を使って画面上の地点を現実世界の位置に変換する

Lightshipの深度マップ出力により、平面やメッシュを使わずにARシーンにオブジェクトを動的に配置できる。 このHow-Toでは、画面上のポイントを選択し、深度出力を使用して3D空間にオブジェクトを配置するプロセスについて説明します。

奥行きのあるキューブの配置

前提条件

Lightship ARを有効にしたUnityのプロジェクトが必要です。 詳細については、 ARDK 3 のインストールを参照してください。

ヒント

初めて深度を使うのであれば、 Accessing and Displaying Depth Information は、深度のより単純な使用例を提供しており始めやすいです。

手順

メインシーンがAR対応でない場合は、次のように設定します。

  1. Main Camera を削除します。

  2. 階層にARSessionと XROriginを追加し、XROriginに AR Occlusion ManagerComponentを追加します。 より高品質なオクルージョンが必要な場合は、LightshipOcclusionExtensionの使い方について、How to Set Up Real-World Occlusionを参照してください。

    AR SessionとXR OriginAR Occlusion Manager
  3. 深度のピッキングとプレハブの配置を処理するスクリプトを作成します。 Depth_ScreenToWorldPositionと名付ける。

  4. カメラ更新イベントの追加:

    1. これは、後で深度テクスチャをスクリーン空間に整列させるために、最も最近使用されたDisplayMatrixを保存します。

    2. スクリプトの先頭に、シリアライズされたARCameraManagerとプライベートなMatrix4x4フィールドを追加する。

      [SerializeField]
      private ARCameraManager _arCameraManager;

      private Matrix4x4 _displayMatrix;
    3. カメラの更新を受信するためにOnCameraFrameEventReceivedにサブスクライブします。

         ```cs
      private void OnCameraFrameEventReceived(ARCameraFrameEventArgs args)
      {
      // 画面から画像への変換をキャッシュする
      if (args.displayMatrix.HasValue) HasValue)
      {
      #if UNITY_IOS
      _displayMatrix = args.displayMatrix.Value.transpose;
      #else
      _displayMatrix = args.displayMatrix.Value;
      #endif
      }.
      }
      ```
      :::note

      iOSではDisplayMatrixをトランスポーズさせる必要がある。

:::

  1. 更新時にデプス画像を収集

    1. シリアライズされた`AROcclusionManagerと`プライベートな`XRCpuImage`フィールドを追加します。
    ```cs
    [SerializeField]
    private AROcclusionManager _occlusionManager;

    private XRCpuImage? _深度画像;
    ```
    :::note 奥行き画像は、機械学習モデルを90度回転させたもので、ディスプレイ変換に対してサンプリングする必要がある。 表示変換は、スクリーン空間からML座標系への変換マッピングを与える。 GPUテクスチャの代わりに`XRCpuImageが`使われているので、CPUで`Sample(Vector2 uv, Matrix4x4 transform)`メソッドが使えます。

    :::

    1. `Update`メソッドで:
    1. `XROcclusionSubsystem`が有効で実行中であることを確認する。
    1. `_occlusionManager.TryAcquireEnvironmentDepthCpuImageを呼び出して`、`AROcclusionManagerから`最新の深度画像を取得する。
    1. 古い深度画像を破棄し、新しい値をキャッシュする。

    ```cs
    void Update()
    {
    if (!_occlusionManager.subsystem.running)
    {
    return;
    }
    if (_occlusionManager.TryAcquireEnvironmentDepthCpuImage(out var image))
    {
    // 古い画像を処分する
    _depthImage?.Dispose();
    _depthImage = image;
    }
    else
    {
    return;
    }.
    }
    ```
  2. タッチ入力を処理するコードを設定する:

    1. HandleTouch "という名前のプライベート・メソッドを作成する。
    2. エディターでは、マウスのクリックを検出するために "Input.MouseDown "を使用します。
    3. 電話の場合、"Input.GetTouch"
    4. 次に、デバイスから2D screenPosition座標を取得する。
    private void HandleTouch()
    {
    // エディターではマウスクリックを使い、電話ではタッチを使いたい。
    #if UNITY_EDITOR
    if (Input.GetMouseButtonDown(0) || Input.GetMouseButtonDown(1) || Input.GetMouseButtonDown(2))
    {
    var screenPosition = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
    #else
    //タッチがないか、タッチでUI要素が選択された場合
    if (Input.touchCount <= 0)
    return;
    var touch = Input.GetTouch(0);

    // 始まったばかりのタッチだけをカウントする
    if (touch.phase == UnityEngine.TouchPhase.Began)
    {
    var screenPosition = touch.position;
    #endif
    // タッチで何かをする
    }.
    }
    }
  3. 深度を使用して、スクリーンのタッチポイントを3D座標に変換する。

    1. HandleTouchメソッドで、タッチが検出されたときに有効な深度画像をチェックする。
    ```cs
    //
    if (_depthImage.HasValue)
    {
    // 1. サンプル目の深さ

    // 2. ワールドポジションを取得

    // 3. デプスマップ上に物を産む

    }.

        ```
    1. screenPositionで深度画像をサンプリングし、z値を取得する。
    ```cs
    // 1. サンプル目の深さ
    var uv = new Vector2(screenPosition.x / Screen.width, screenPosition.y / Screen.height);
    var eyeDepth = (float) _depthImage.Value.Sample(uv, _displayMatrix);
    ```

    1. スクリプトの先頭に`Camera`フィールドを追加する:

    ```cs
    [SerializeField]
    private Camera _camera;
    ```

    1. これはUnityの [`カメラ.ScreenToWorldPoint`](https://docs.unity3d.com/ScriptReference/Camera.ScreenToWorldPoint.html)関数を使用します。 HandleTouch "内のメソッドを呼び出し、screenPositionとeyeDepthをworldPositionsに変換する。

    ```cs
    // 2. ワールドポジションの取得
    var worldPosition =
    _camera.ScreenToWorldPoint(new Vector3(screenPosition.x, screenPosition.y, eyeDepth));
    ```

    1. ワールド空間のこの位置にGameObjectをスポーンする:
    1. スクリプトの先頭に`GameObject`フィールドを追加する:

    ```cs
    [SerializeField]
    private GameObject _prefabToSpawn;
    ```

    1. このプレハブのコピーをこの位置にインスタンス化します:

    ```cs
    // 3.
    Instantiate(_prefabToSpawn, worldPosition, Quaternion.identity);
    ```
  4. Updateメソッドの最後にHandleTouchを追加する。

        private void HandleTouch()
    {
    // エディターではマウスクリックを使い、電話ではタッチを使いたい。
    #if UNITY_EDITOR
    if (Input.GetMouseButtonDown(0) || Input.GetMouseButtonDown(1) || Input.GetMouseButtonDown(2))
    {
    var screenPosition = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
    #else
    //if there is no touch or touch selects UI element
    if (Input.touchCount <= 0)
    return;
    var touch = Input.GetTouch(0);

    // only count touches that just began
    if (touch.phase == UnityEngine.TouchPhase.Began)
    {
    var screenPosition = touch.position;
    #endif
    // do something with touches
    if (_depthImage.HasValue)
    {
    // Sample eye depth
    var uv = new Vector2(screenPosition.x / Screen.width, screenPosition.y / Screen.height);
    var eyeDepth = (float) _depthImage.Value.Sample(uv, _displayMatrix);

    // Get world position
    var worldPosition =
    _camera.ScreenToWorldPoint(new Vector3(screenPosition.x, screenPosition.y, eyeDepth));

    //spawn a thing on the depth map
    Instantiate(_prefabToSpawn, worldPosition, Quaternion.identity);
    }
    }
    }
  5. Depth_ScreenToWorldPosition スクリプトを、 XROrigin のコンポーネントとして、 Hierarchy に追加します:

    1. Hierarchy ウィンドウで、 XROriginを選択し、 Inspectorで、 Add Component をクリックします。
    2. Depth_ScreenToWorldPosition スクリプトを検索し、選択します。
  6. シーンにスポーンするオブジェクトとして使用するため、 Cube を作成します:

    1. Hierarchyで右クリックし、 Create メニューで、 3D Object にマウスオーバーし、 Cubeを選択します。
    2. 新しい Cube オブジェクトを Hierarchy から Assets ウィンドウにドラッグしてプレハブを作成し、 Hierarchyから削除します。 ( Assets ウィンドウの Cube は残るはずです)。
  7. Depth_ScreenToWorldPosition スクリプトのフィールドを割り当てます:

    1. Hierarchy ウィンドウで、 XROriginを選択し、 Inspector ウィンドウで、 Depth_ScreenToWorldPosition Component を展開します。
    2. XROriginを AROcclusionManagerフィールドに割り当てる。
    3. メインカメラカメラ フィールドに割り当てます。
    4. メインカメラを ARCameraManagerフィールドに割り当てます。
    5. 新しい Cube プレハブを Prefab to Spawn フィールドに割り当てます。
    Depth_ScreenToWorldPositionエディタ・プロパティ
  8. Playbackを使用してエディター内でシーンを実行するか、Build Settingsを開き、Build and Runをクリックしてデバイスにビルドしてみてください。

    奥行きのあるキューブの配置
  9. うまくいかない場合は、上記の手順を再度確認し、以下のスクリプトと比較してください。

クリックするとDepth_ScreenToWorldPositionスクリプトが表示されます。
using Niantic.Lightship.AR.Utilities;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

public class Depth_ScreenToWorldPosition : MonoBehaviour
{
[SerializeField]
private AROcclusionManager _occlusionManager;

[SerializeField]
private ARCameraManager _arCameraManager;

[SerializeField]
private Camera _camera;

[SerializeField]
private GameObject _prefabToSpawn;

private Matrix4x4 _displayMatrix;
private XRCpuImage? _depthImage;

private void OnEnable()
{
_arCameraManager.frameReceived += OnCameraFrameEventReceived;
}

private void OnDisable()
{
_arCameraManager.frameReceived -= OnCameraFrameEventReceived;
}

private void OnCameraFrameEventReceived(ARCameraFrameEventArgs args)
{
// 画面から画像への変換をキャッシュ
if (args.displayMatrix.HasValue)
{
#if UNITY_IOS
_displayMatrix = args.displayMatrix.Value.transpose;
#else
_displayMatrix = args.displayMatrix.Value;
#endif
}.
}

void Update()
{
if (!_occlusionManager.subsystem.running)
{
return;
}
if (_occlusionManager.TryAcquireEnvironmentDepthCpuImage(out
var image))
{
// 古い画像を処分する
_depthImage?.Dispose();
_depthImage = image;
}
else
{
return;
}.

HandleTouch();
}

private void HandleTouch()
{
// エディタではマウスクリックを使用し、電話ではタッチを使用します。
#if UNITY_EDITOR
if (Input.GetMouseButtonDown(0) || Input.GetMouseButtonDown(1) || Input.GetMouseButtonDown(2))
{
var screenPosition = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
#else
//タッチがないか、タッチでUI要素が選択された場合
if (Input.touchCount <= 0)
return;
var touch = Input.GetTouch(0);

// 始まったばかりのタッチだけをカウントする
if (touch.phase == UnityEngine.TouchPhase.Began)
{
var screenPosition = touch.position;
#endif
// タッチで何かをする
if (_depthImage.HasValue)
{
// Sample eye depth
var uv = new Vector2(screenPosition.x / Screen.width, screenPosition.y / Screen.height);
var eyeDepth = (float) _depthImage.Value.Sample(uv, _displayMatrix);

// ワールドポジションを取得
var worldPosition =
_camera.ScreenToWorldPoint(new Vector3(screenPosition.x, screenPosition.y, eyeDepth));

//デプスマップ上にモノをスポーン
Instantiate(_prefabToSpawn, worldPosition, Quaternion.identity);
}.
}
}
}

詳細情報

また、このHow-ToをObjectDetectionや Semanticsと組み合わせて、3D空間のどこに何があるかを知ることもできる。

奥行きと物体検出を使ったキューブの配置