本文へスキップ

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

Lightshipの深度マップ出力を使用すると、平面やメッシュを使用せずにARシーン内にオブジェクトを動的に配置できます。 この入門ガイドでは、画面上の位置を選択し、深度出力を使用して3D空間にオブジェクトを配置する手順を説明します。

深度を使ってキューブを配置している様子

前提条件

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

ヒント

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

手順

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

  1. Main Camera を削除します。

  2. ARSessionXROrigin をHierarchyに追加し、 AR Occlusion Manager コンポーネントを XROrigin に追加します。 高精度なオクルージョンを望む場合は、 How to Set Up Real-World Occlusion(現実世界のオクルージョンを設定する)LightshipOcclusionExtension の使用方法をご覧ください。

    AR SessionとXR Origin ARオクルージョンマネージャー
  3. 深度のピッキングとプレハブの配置を処理するスクリプトを作成します。 Depth_ScreenToWorldPosition という名前を付けます。

  4. スクリプトに必要な名前空間を追加する

    using Niantic.Lightship.AR.Utilities;
    using UnityEngine;
    using UnityEngine.XR.ARFoundation;
    using UnityEngine.XR.ARSubsystems;
  5. 更新時に深度画像を取得する

    1. AROcclusionManager のシリアライズされたフィールドと、 XRCpuImage のプライベートフィールドを追加します。

      [SerializeField]
      private AROcclusionManager _occlusionManager;

      private XRCpuImage? _depthImage;
    2. 以下の手順で、新しいメソッド(UpdateImage)を作成します。

      1. XROcclusionSubsystem が有効かつ実行中であることを確認します。
      2. _occlusionManager.TryAcquireEnvironmentDepthCpuImage を呼び出して、最新の深度画像を AROcclusionManager から取得します。
      3. 古い深度画像を破棄し、新しい値をキャッシュします。
      private void UpdateImage()
      {
      if (!_occlusionManager.subsystem.running)
      {
      return;
      }

      if (_occlusionManager.TryAcquireEnvironmentDepthCpuImage(out var image))
      {
      // 古いイメージを破棄
      _depthImage?.Dispose();

      // 新しいイメージをキャッシュ
      _depthImage = image;
      }
      }
    3. 次のように、 Update コールバック内で UpdateImage メソッドを呼び出します。

      private void Update()
      {
      UpdateImage();
      }
  6. 表示マトリックスの計算: 機械学習モデルから生成される際、深度画像はセンサーに向かって配置されるため、現在の画面の向きに合わせてサンプリングする必要があります。 表示変換では、スクリーン空間からイメージ座標系への変換マッピングを利用できます。 Sample(Vector2 uv, Matrix4x4 transform) メソッドをCPU上で使用できるように、GPUテクスチャではなく XRCpuImage を使用します。

    1. 非公開の Matrix4x4 フィールドと ScreenOrientation フィールドを追加します。
      private Matrix4x4 _displayMatrix;
      private ScreenOrientation? _latestScreenOrientation;
    2. 新しいメソッド(UpdateDisplayMatrix)を作成します。
    3. スクリプトに有効な XRCpuImage がキャッシュされていることを確認します。
    4. 画面の向きが変更されたかどうかを確認して、マトリックスの再計算が必要かどうかを判断します。
    5. CameraMath.CalculateDisplayMatrix を呼び出し、スクリーン座標をイメージ座標に変換するマトリックスを計算します。
    private void UpdateDisplayMatrix()
    {
    // 有効な深度画像があることを確認
    if (_depthImage is {valid: true})
    {
    // 表示マトリックスは画面の向きが変わっている場合にのみ再計算が必要
    if (!_latestScreenOrientation.HasValue ||
    _latestScreenOrientation.Value != XRDisplayContext.GetScreenOrientation())
    {
    _latestScreenOrientation = XRDisplayContext.GetScreenOrientation();
    _displayMatrix = CameraMath.CalculateDisplayMatrix(
    _depthImage.Value.width,
    _depthImage.Value.height,
    Screen.width,
    Screen.height,
    _latestScreenOrientation.Value,
    invertVertically: true);
    }
    }
    }
    1. 次のように、 Update コールバック内で UpdateDisplayMatrix メソッドを呼び出します。
    private void Update()
    {
    ...
    UpdateDisplayMatrix();
    }
  7. 以下の手順で、タップ入力を処理するコードを設定します。

    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
    // タップで何かのアクションを行う
    }
    }
    }
  8. 深度を使ってタップポイントを3D座標に変換します。

    1. HandleTouchメソッド内で、タップが検出された際に有効な深度画像があるかどうかを確認します。

          // タップで何かのアクションを行う
      if (_depthImage.HasValue)
      {
      // 1. 視線深度をサンプリングする

      // 2. ワールド座標を取得する

      // 3. 深度マップ上にオブジェクトをスポーンする
      }
    2. screenPositionで深度画像をサンプリングして、z値を取得します。

      // 1. サンプル目の深さ
      var uv = new Vector2(screenPosition.x / Screen.width, screenPosition.y / Screen.height);
      var eyeDepth = (float) _depthImage.Value.Sample(uv, _displayMatrix);
    3. スクリプトの先頭に、 Camera フィールドを追加します。

      [SerializeField]
      private Camera _camera;
    4. これで、Unityの Camera.ScreenToWorldPoint 関数が使用されるようになります。 「HandleTouch」メソッド内でこの関数を呼び出し、screenPositionとeyeDepthをworldPositionsに変換します。

      // 2. ワールド座標を取得
      var worldPosition =
      _camera.ScreenToWorldPoint(new Vector3(screenPosition.x, screenPosition.y, eyeDepth));
    5. ワールド空間のこの位置にGameObjectをスポーンします。

    6. スクリプトの先頭に GameObject フィールドを追加します。

      [SerializeField]
      private GameObject _prefabToSpawn;
    7. このプレハブのコピーをこの位置にインスタンス化します。

      // 3. 深度マップ上にオブジェクトをスポーンする
      Instantiate(_prefabToSpawn, worldPosition, Quaternion.identity);
  9. HandleTouchUpdate メソッドの最後に追加します。

        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 = _depthImage.Value.Sample<float>(uv, _displayMatrix);

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

    //デプスマップ上にモノをスポーンする
    Instantiate(_prefabToSpawn, worldPosition, Quaternion.identity);
    }.
    }
    }
  10. Depth_ScreenToWorldPosition スクリプトを、 XROrigin のコンポーネントとして、 ** Hierarchy **に追加します:

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

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

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

    深度を使ってキューブを配置している様子
  14. 正常に動作しない場合は、上記の手順を再度確認し、以下のスクリプトと比較してください。

クリックして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 Camera _camera;

[SerializeField]
private GameObject _prefabToSpawn;

private Matrix4x4 _displayMatrix;
private XRCpuImage? _depthImage;
private ScreenOrientation? _latestScreenOrientation;

private void Update()
{
UpdateImage();
UpdateDisplayMatrix();
HandleTouch();
}

private void OnDestroy()
{
// キャッシュしていた深度画像を破棄
_depthImage?.Dispose();
}

private void UpdateImage()
{
if (!_occlusionManager.subsystem.running)
{
return;
}

if (_occlusionManager.TryAcquireEnvironmentDepthCpuImage(out var image))
{
// 古い深度画像を破棄
_depthImage?.Dispose();

// 新しい深度画像をキャッシュ
_depthImage = image;
}
}

private void UpdateDisplayMatrix()
{
// 有効な深度画像があることを確認
if (_depthImage is {valid: true})
{
// 表示マトリックスは画面の向きが変わっている場合にのみ再計算が必要
if (!_latestScreenOrientation.HasValue ||
_latestScreenOrientation.Value != XRDisplayContext.GetScreenOrientation())
{
_latestScreenOrientation = XRDisplayContext.GetScreenOrientation();
_displayMatrix = CameraMath.CalculateDisplayMatrix(
_depthImage.Value.width,
_depthImage.Value.height,
Screen.width,
Screen.height,
_latestScreenOrientation.Value,
invertVertically: true);
}
}
}

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 is {valid: true})
{
// カメラ深度をサンプリングする
var uv = new Vector2(screenPosition.x / Screen.width, screenPosition.y / Screen.height);
var eyeDepth = _depthImage.Value.Sample<float>(uv, _displayMatrix);

// ワールド座標を取得する
var worldPosition =
_camera.ScreenToWorldPoint(new Vector3(screenPosition.x, screenPosition.y, eyeDepth));

// 深度マップ上にオブジェクトをスポーンする
Instantiate(_prefabToSpawn, worldPosition, Quaternion.identity);
}
}
}
}

詳細情報

この入門ガイドは、 ObjectDetectionSemantics と組み合わせることで、3D空間内でのオブジェクトの位置を確認する方法を理解することもできます。

深度とオブジェクト検出を使ってキュ�ーブを配置する様子