HoloLens (第 1 世代) 空間 230: 空間マッピング
重要
Mixed Reality Academy のチュートリアルは、HoloLens (第 1 世代)、Unity 2017、Mixed Reality イマーシブ ヘッドセットを考慮して設計されています。 そのため、それらのデバイスの開発に関するガイダンスを引き続き探している開発者のために、これらのチュートリアルをそのまま残しておくことが重要だと考えています。 これらのチュートリアルは、HoloLens 2 に使用される最新のツールセットや対話式操作を反映して更新されることは "ありません"。また、最新バージョンの Unity には対応していない可能性があります。 これらは、サポートされているデバイス上で継続して動作するように、保守されます。 HoloLens 2 向けには、新しいチュートリアル シリーズが投稿されています。
空間マッピングは、ホログラムに環境について教えることによって、現実世界と仮想世界を組み合わせます。 MR 空間 230 (プロジェクト Planetarium) では、次の方法について学習します。
- 環境をスキャンし、HoloLens から開発用コンピューターにデータを転送します。
- シェーダーを探索し、それらを使用して空間を視覚化する方法を学習します。
- メッシュ処理を使用して、部屋のメッシュを単純な平面に分割します。
- MR の基本 101 で学習した配置手法を超えて、ホログラムを環境内のどこに配置できるかに関するフィードバックを提供します。
- オクルージョン効果を探索します。これにより、ホログラムが現実世界の物体の背後にある場合でも、X 線ビジョンで表示できます。
デバイス サポート
コース | HoloLens | イマーシブ ヘッドセット |
---|---|---|
MR 空間 230:空間マッピング | ✔️ |
開始する前に
前提条件
- 正しいツールがインストールされている構成済みの Windows 10 PC。
- 基本的な C# プログラミングの能力。
- MR 基本 101 を完了している必要があります。
- 開発用に構成された HoloLens デバイス。
プロジェクト ファイル
- プロジェクトに必要なファイルをダウンロードします。 Unity 2017.2 以降が必要です。
- デスクトップなどのアクセスしやすい場所にファイルをアーカイブ解除します。
Note
ダウンロードする前にソース コードを確認する場合は、GitHub で参照できます。
メモ
- コード内のブレークポイントにヒットするには、Visual Studio で、[ツール] -> [オプション] -> [デバッグ] の下にある [マイ コードのみを有効にする] を無効 (''オフ'') にする必要があります。
Unity のセットアップ
- Unity を起動します。
- [New] (新規) を選択して新しいプロジェクトを作成します。
- プロジェクトに「Planetarium」という名前を付けます。
- [3D] 設定が選択されていることを確認します。
- [プロジェクトの作成] をクリックします。
- Unity が起動したら、[Edit] (編集) > [Project Settings] (プロジェクト設定) > [Player] (プレーヤー) の順に移動します。
- [Inspector] (インスペクター) パネルで、緑色の [Windows Store] (Windows ストア) アイコンを探して選択します。
- [Other Settings] (その他の設定) を展開します。
- [Rendering] (レンダリング) セクションで、[Virtual Reality Supported] (仮想現実のサポート) オプションを選択します。
- [Virtual Reality SDKs] (仮想現実 SDK) のリストに、[Windows Holographic]が表示されるのを確認します。 ない場合は、一覧の一番下にある + ボタンを選択し、[Windows Holographic] を選択します。
- [Publishing Settings] (公開の設定) を展開します。
- [Capabilities] (機能) セクションで、次の設定を確認します。
- InternetClientServer
- PrivateNetworkClientServer
- Microphone
- SpatialPerception
- [Edit] (編集) > [Project Settings] (プロジェクト設定) > [Quality] (品質) に移動します。
- [Inspector] (インスペクター) パネルの [Windows Store] (Windows ストア) アイコンの下にある [Default] (既定) 行の下にある黒いドロップダウン矢印を選択し、既定の設定を [Very Low] (非常に低い) に変更します。
- [Assets] (資産) > [Import Package] (パッケージのインポート) > [Custom Package] (カスタム パッケージ) をクリックします。
- ...\HolographicAcademy-Holograms-230-SpatialMapping\Starting フォルダーに移動します。
- [Planetarium.unitypackage] をクリックします。
- [開く] をクリックします。
- [Import Unity Package] (Unity パッケージのインポート) ウィンドウが表示されたら、[Import] (インポート) ボタンをクリックします。
- Unity が、このプロジェクトを完了するために必要なすべての資産をインポートするのを待ちます。
- [Hierarchy] (階層) パネルで [Main Camera] (メイン カメラ) を削除します。
- [Project] (プロジェクト) パネルの HoloToolkit-SpatialMapping-230\Utilities\Prefabs フォルダーで、[Main Camera] (メイン カメラ) オブジェクトを見つけます。
- [Main Camera] (メイン カメラ) プレハブを [Hierarchy] (階層) パネルにドラッグ アンド ドロップします。
- [Hierarchy] (階層) パネルで [Directional Light] (指向性ライト) オブジェクトを削除します。
- [Project] (プロジェクト) パネルの Holograms フォルダーで、カーソル オブジェクトを探します。
- カーソル プレハブ & 階層にドラッグ アンド ドロップします。
- [Hierarchy] (階層) パネルで、[Cursor] (カーソル) オブジェクトを選択します。
- [Inspector] (インスペクター) パネルで、[Layer] (レイヤー) ドロップ ダウンをクリックして、[Edit Layers...] (レイヤーの編集) を選択します。
- [User Layer 31] (ユーザー レイヤー 31) に「SpatialMapping」という名前を付けます。
- [File] (ファイル) >[Save Scene As] (名前を付けてシーンを保存) を選択して、新しいシーンを保存します。
- [New Folder] (新しいフォルダー) をクリックした後、フォルダーに「Scenes」という名前を付けます。
- このファイルに「Planetarium」という名前を付けて、Scenes フォルダーに保存します。
第 1 章 - スキャン
目標
- SurfaceObserver についてと、その設定がエクスペリエンスとパフォーマンスに与える影響について学習します。
- 部屋のメッシュを収集する部屋のスキャン エクスペリエンスを作成します。
手順
- [Project] (プロジェクト) パネルの HoloToolkit-SpatialMapping-230\SpatialMapping\Prefabs フォルダーで、[SpatialMapping] プレハブを見つけます。
- SpatialMapping プレハブ & [階層] パネルにドラッグ アンド ドロップします。
ビルドとデプロイ (第 1 部)
- Unity で、[File] (ファイル) > [Build Settings] (ビルド設定) の順に選択します。
- [Add Open Scenes] (開いているシーンを追加) をクリックして、Planetarium シーンをビルドに追加します。
- [プラットフォーム] リストで [ユニバーサル Windows プラットフォーム] を選択し、[プラットフォームの切り替え] をクリックします。
- [SDK] を [Universal 10] に、[UWP Build Type] (UWP ビルドの種類) を [D3D] に設定します。
- [Unity C# プロジェクト] をオンにします。
- [ビルド] をクリックします。
- 「App」という名前の新しいフォルダーを作成します。
- [App] フォルダーを 1 回クリックします。
- [フォルダーの選択] ボタンをクリックします。
- Unity のビルドが完了すると、エクスプローラー ウィンドウが表示されます。
- App フォルダーをダブルクリックして開きます。
- Planetarium.sln をダブルクリックして、Visual Studio でプロジェクトを読み込みます。
- この Visual Studio 上部のツール バーを使用して、構成を [リリース] に変更します。
- プラットフォームを [x86] に変更します。
- [ローカル コンピューター] の右側にあるドロップダウン矢印をクリックし、[リモート コンピューター] を選択します。
- デバイスの IP アドレスを [アドレス] フィールドに入力し、認証モードを [ユニバーサル (暗号化されていないプロトコル)] に設定します。
- [デバッグ] > [デバッグなしで開始] の順にクリックするか、Ctrl + F5 キーを押します。
- ビルドとデプロイの状態については、Visual Studio の [出力] パネルを確認します。
- アプリがデプロイされたら、部屋を歩き回ります。 周囲のサーフェスが黒と白のワイヤーフレーム メッシュで覆われているのを確認できます。
- 周囲をスキャンします。 必ず壁、天井、床を見ます。
ビルドとデプロイ (第 2 部)
次に、空間マッピングがパフォーマンスに与える影響を確認しましょう。
- Unity で [Window] (ウィンドウ) > [Profiler] (プロファイラー) を選択します。
- [Add Profiler] (プロファイラーの追加) > [GPU] をクリックします。
- [Active Profiler Enter IP]\(><アクティブ プロファイラー IP の入力\) を>クリックします。
- ここで HoloLens の IP アドレスを入力します。
- [接続] をクリックします。
- GPU がフレームをレンダリングするのに要するミリ秒数を確認します。
- デバイスでアプリケーションの実行を停止します。
- Visual Studio に戻って SpatialMappingObserver.cs を開きます。 これは、Assembly-CSharp (ユニバーサル Windows) プロジェクトの HoloToolkit\SpatialMapping フォルダーにあります。
- Awake() 関数を見つけて、次のコード行を追加します: TrianglesPerCubicMeter = 1200;
- プロジェクトをデバイスに再デプロイし、プロファイラーを再接続します。 フレームをレンダリングするためのミリ秒数の変化を観察します。
- デバイスでアプリケーションの実行を停止します。
Unity での保存と読み込み
最後に、部屋のメッシュを保存して Unity に読み込みます。
- Visual Studio に戻って、以前のセクションで Awake() 関数に追加した TrianglesPerCubicMeter の行を削除します。
- プロジェクトをデバイスに再デプロイします。 この時点で、立法メートルあたり 500 の三角形で実行されているはずです。
- ブラウザーを開き、HoloLens の IP アドレスを入力して、Windows デバイス ポータルに移動します。
- 左側のパネルで [3D View] (3D ビュー) オプションを選択します。
- [Surface reconstruction] (サーフェスの認識) で、[Update] (更新) ボタンを選択します。
- 表示ウィンドウに、HoloLens でスキャンした領域が表示されるのを確認します。
- 部屋のスキャンを保存するには、[Save] (保存) をクリックします。
- Downloads フォルダーを開き、保存されたルーム モデル SRMesh.obj を探します。
- SRMesh.obj を、Unity プロジェクトの Assets フォルダーにコピーします。
- Unity で、SpatialMapping オブジェクトを [Hierarchy] (階層) パネルで選択します。
- [Object Surface Observer (Script)] (オブジェクト サーフェス オブザーバー (スクリプト)) コンポーネントを見つけます。
- [Room Model] (ルーム モデル) プロパティの右側にある円をクリックします。
- SRMesh オブジェクトを見つけて選択し、ウィンドウを閉じます。
- [Room Model] (ルーム モデル) プロパティが [Inspector] (インスペクター) パネルで [SRMesh] に設定されていることを確認します。
- [Play] (再生) ボタンを押して Unity のプレビュー モードを開始します。
- SpatialMapping コンポーネントは、Unity で使用できるように、保存された部屋モデルからメッシュを読み込みます。
- シーン ビューに切り替えて、ワイヤーフレーム シェーダーで表示される部屋モデルをすべて表示します。
- [Play] (再生) ボタンを再び押してプレビュー モードを終了します。
注: 次回 Unity でプレビュー モードを開始した場合は、既定で保存されたルーム メッシュが読み込まれます。
第 2 章 - 視覚化
目標
- シェーダーの基本について説明します。
- 周囲を視覚化します。
手順
- Unity の [Hierarchy] (階層) パネルで、[SpatialMapping] オブジェクトを選択します。
- [Inspector] (インスペクター) パネルで、[Spatial Mapping Manager (Script)] (空間マッピング マネージャー (スクリプト)) コンポーネントを見つけます。
- [Surface Material] (サーフェス素材) プロパティの右側にある円をクリックします。
- BlueLinesOnWalls 素材を探して選択し、ウィンドウを閉じます。
- [Project] (プロジェクト) パネルの Shaders フォルダーで、[BlueLinesOnWalls] をダブルクリックして、Visual Studio でシェーダーを開きます。
- これは、単純なピクセル (頂点からフラグメント) シェーダーで、次のタスクを実行します。
- 頂点の位置をワールド空間に変換します。
- 頂点の法線をチェックして、ピクセルが垂直かどうかを判断します。
- レンダリングするピクセルの色を設定します。
ビルドと配置
- Unity に戻り、[Play] (再生) を押してプレビュー モードを開始します。
- 青い線が部屋メッシュのすべての垂直サーフェスにレンダリングされます (保存されたスキャン データから自動的に読み込まれます)。
- [Scene] (シーン) タブに切り替えて部屋のビューを調整し、Unity で部屋のメッシュ全体がどのように表示されるかを確認します。
- [Project] (プロジェクト) パネルで、Materials フォルダーを見つけて素材 BlueLinesOnWalls を選択します。
- いくつかのプロパティを変更し、Unity エディターで変更がどのように表示されるかを確認します。
- [Inspector] (インスペクター) パネルで、LineScale の値を調整して、線が太くまたは補足表示されるようにします。
- [Inspector] (インスペクター) パネルで、LinesPerMeter の値を調整して、各壁に表示される線の数を変更します。
- [Play] (再生) をクリックして、プレビュー モードを終了します。
- HoloLens をビルドして配置し、シェーダーのレンダリングが実際のサーフェスにどのように表示されるかを確認します。
Unity には優れた素材プレビュー機能がありますが、デバイスでのレンダリングを確認するのも常に良い方法です。
第 3 章 - 処理
目標
- アプリケーションで使用するために空間マッピング データを処理する手法について学習します。
- 空間マッピング データを分析して平面を検索し、三角形を削除します。
- ホログラムの配置に平面を使用します。
手順
- Unity の [Project] (プロジェクト) パネルの Holograms フォルダーで、SpatialProcessing オブジェクトを見つけます。
- SpatialProcessing オブジェクト & [階層] パネルにドラッグ アンド ドロップします。
SpatialProcessing プレハブには、空間マッピング データを処理するコンポーネントが含まれています。 SurfaceMeshesToPlanes.cs は、空間マッピング データに基づいて平面を検索して生成します。 アプリケーションでは、平面を使用して、壁、床、天井を表します。 このプレハブには、空間マッピング メッシュから頂点を削除できる RemoveSurfaceVertices.cs も含まれています。 これを使用して、メッシュに穴を作成する、または (代わりにプレーンを使用できるため) 不要になった余分な三角形を削除できます。
- Unity の [Project] (プロジェクト) パネルの Holograms フォルダーで、SpaceCollection オブジェクトを見つけます。
- SpaceCollection オブジェクトを [Hierarchy] (階層) パネルにドラッグ アンド ドロップします。
- [Hierarchy] (階層) パネルで、SpatialProcessing オブジェクトを選択します。
- [Inspector] (インスペクター) パネルで、[Play Space Manager (Script)] (空間マネージャーの再生 (スクリプト)) コンポーネントを見つけます。
- PlaySpaceManager.cs をダブルクリックして、Visual Studio で開きます。
PlaySpaceManager.cs には、アプリケーション固有のコードが含まれています。 このスクリプトに機能を追加して、次の動作を有効にします。
- スキャンの制限時間 (10 秒) を超えた後、空間マッピング データの収集を停止します。
- 次のようにして、空間マッピング データを処理します。
- SurfaceMeshesToPlanes を使用して、より単純なワールド表現を平面として作成します (壁、床、天井など)。
- RemoveSurfaceVertices を使用して、平面境界内にあるサーフェスの三角形を削除します。
- ワールドにホログラムのコレクションを生成し、ユーザーの近くの壁や床の平面に配置します。
PlaySpaceManager.cs でマークされたコーディング演習を完了するか、スクリプトを次の完成したソリューションに置き換えます。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Windows.Speech;
using Academy.HoloToolkit.Unity;
/// <summary>
/// The SurfaceManager class allows applications to scan the environment for a specified amount of time
/// and then process the Spatial Mapping Mesh (find planes, remove vertices) after that time has expired.
/// </summary>
public class PlaySpaceManager : Singleton<PlaySpaceManager>
{
[Tooltip("When checked, the SurfaceObserver will stop running after a specified amount of time.")]
public bool limitScanningByTime = true;
[Tooltip("How much time (in seconds) that the SurfaceObserver will run after being started; used when 'Limit Scanning By Time' is checked.")]
public float scanTime = 30.0f;
[Tooltip("Material to use when rendering Spatial Mapping meshes while the observer is running.")]
public Material defaultMaterial;
[Tooltip("Optional Material to use when rendering Spatial Mapping meshes after the observer has been stopped.")]
public Material secondaryMaterial;
[Tooltip("Minimum number of floor planes required in order to exit scanning/processing mode.")]
public uint minimumFloors = 1;
[Tooltip("Minimum number of wall planes required in order to exit scanning/processing mode.")]
public uint minimumWalls = 1;
/// <summary>
/// Indicates if processing of the surface meshes is complete.
/// </summary>
private bool meshesProcessed = false;
/// <summary>
/// GameObject initialization.
/// </summary>
private void Start()
{
// Update surfaceObserver and storedMeshes to use the same material during scanning.
SpatialMappingManager.Instance.SetSurfaceMaterial(defaultMaterial);
// Register for the MakePlanesComplete event.
SurfaceMeshesToPlanes.Instance.MakePlanesComplete += SurfaceMeshesToPlanes_MakePlanesComplete;
}
/// <summary>
/// Called once per frame.
/// </summary>
private void Update()
{
// Check to see if the spatial mapping data has been processed
// and if we are limiting how much time the user can spend scanning.
if (!meshesProcessed && limitScanningByTime)
{
// If we have not processed the spatial mapping data
// and scanning time is limited...
// Check to see if enough scanning time has passed
// since starting the observer.
if (limitScanningByTime && ((Time.time - SpatialMappingManager.Instance.StartTime) < scanTime))
{
// If we have a limited scanning time, then we should wait until
// enough time has passed before processing the mesh.
}
else
{
// The user should be done scanning their environment,
// so start processing the spatial mapping data...
/* TODO: 3.a DEVELOPER CODING EXERCISE 3.a */
// 3.a: Check if IsObserverRunning() is true on the
// SpatialMappingManager.Instance.
if(SpatialMappingManager.Instance.IsObserverRunning())
{
// 3.a: If running, Stop the observer by calling
// StopObserver() on the SpatialMappingManager.Instance.
SpatialMappingManager.Instance.StopObserver();
}
// 3.a: Call CreatePlanes() to generate planes.
CreatePlanes();
// 3.a: Set meshesProcessed to true.
meshesProcessed = true;
}
}
}
/// <summary>
/// Handler for the SurfaceMeshesToPlanes MakePlanesComplete event.
/// </summary>
/// <param name="source">Source of the event.</param>
/// <param name="args">Args for the event.</param>
private void SurfaceMeshesToPlanes_MakePlanesComplete(object source, System.EventArgs args)
{
/* TODO: 3.a DEVELOPER CODING EXERCISE 3.a */
// Collection of floor and table planes that we can use to set horizontal items on.
List<GameObject> horizontal = new List<GameObject>();
// Collection of wall planes that we can use to set vertical items on.
List<GameObject> vertical = new List<GameObject>();
// 3.a: Get all floor and table planes by calling
// SurfaceMeshesToPlanes.Instance.GetActivePlanes().
// Assign the result to the 'horizontal' list.
horizontal = SurfaceMeshesToPlanes.Instance.GetActivePlanes(PlaneTypes.Table | PlaneTypes.Floor);
// 3.a: Get all wall planes by calling
// SurfaceMeshesToPlanes.Instance.GetActivePlanes().
// Assign the result to the 'vertical' list.
vertical = SurfaceMeshesToPlanes.Instance.GetActivePlanes(PlaneTypes.Wall);
// Check to see if we have enough horizontal planes (minimumFloors)
// and vertical planes (minimumWalls), to set holograms on in the world.
if (horizontal.Count >= minimumFloors && vertical.Count >= minimumWalls)
{
// We have enough floors and walls to place our holograms on...
// 3.a: Let's reduce our triangle count by removing triangles
// from SpatialMapping meshes that intersect with our active planes.
// Call RemoveVertices().
// Pass in all activePlanes found by SurfaceMeshesToPlanes.Instance.
RemoveVertices(SurfaceMeshesToPlanes.Instance.ActivePlanes);
// 3.a: We can indicate to the user that scanning is over by
// changing the material applied to the Spatial Mapping meshes.
// Call SpatialMappingManager.Instance.SetSurfaceMaterial().
// Pass in the secondaryMaterial.
SpatialMappingManager.Instance.SetSurfaceMaterial(secondaryMaterial);
// 3.a: We are all done processing the mesh, so we can now
// initialize a collection of Placeable holograms in the world
// and use horizontal/vertical planes to set their starting positions.
// Call SpaceCollectionManager.Instance.GenerateItemsInWorld().
// Pass in the lists of horizontal and vertical planes that we found earlier.
SpaceCollectionManager.Instance.GenerateItemsInWorld(horizontal, vertical);
}
else
{
// We do not have enough floors/walls to place our holograms on...
// 3.a: Re-enter scanning mode so the user can find more surfaces by
// calling StartObserver() on the SpatialMappingManager.Instance.
SpatialMappingManager.Instance.StartObserver();
// 3.a: Re-process spatial data after scanning completes by
// re-setting meshesProcessed to false.
meshesProcessed = false;
}
}
/// <summary>
/// Creates planes from the spatial mapping surfaces.
/// </summary>
private void CreatePlanes()
{
// Generate planes based on the spatial map.
SurfaceMeshesToPlanes surfaceToPlanes = SurfaceMeshesToPlanes.Instance;
if (surfaceToPlanes != null && surfaceToPlanes.enabled)
{
surfaceToPlanes.MakePlanes();
}
}
/// <summary>
/// Removes triangles from the spatial mapping surfaces.
/// </summary>
/// <param name="boundingObjects"></param>
private void RemoveVertices(IEnumerable<GameObject> boundingObjects)
{
RemoveSurfaceVertices removeVerts = RemoveSurfaceVertices.Instance;
if (removeVerts != null && removeVerts.enabled)
{
removeVerts.RemoveSurfaceVerticesWithinBounds(boundingObjects);
}
}
/// <summary>
/// Called when the GameObject is unloaded.
/// </summary>
private void OnDestroy()
{
if (SurfaceMeshesToPlanes.Instance != null)
{
SurfaceMeshesToPlanes.Instance.MakePlanesComplete -= SurfaceMeshesToPlanes_MakePlanesComplete;
}
}
}
ビルドと配置
- HoloLens にデプロイする前に、Unity の [Play] (再生) ボタンを押して、再生モードを開始します。
- ルーム メッシュがファイルから読み込まれた後、空間マッピング メッシュで処理が開始されるまで 10 秒間待ちます。
- 処理が完了すると、床、壁、天井などを表す平面が表示されます。
- すべての平面が見つかったら、カメラの近くの床のテーブルに太陽系が表示されます。
- カメラの近くの壁には、2 つのポスターも表示されます。 ゲーム モードで表示されない場合は、[Scene] (シーン) タブに切り替えます。
- [Play] (再生) ボタンを再び押して再生モードを終了します。
- 通常どおりビルドして、HoloLens に配置します。
- 空間マッピング データのスキャンと処理が完了するまで待ちます。
- 平面を見てワールド内の太陽系とポスターを探してみます。
第 4 章 - 配置
目標
- ホログラムがサーフェスに合うかどうかを判断します。
- ホログラムがサーフェスに合わない場合は、ユーザーにフィードバックを提供します。
手順
- Unity の [Hierarchy] (階層) パネルで、SpatialProcessing オブジェクトを選択します。
- [Inspector] (インスペクター) パネルで、[Surface Meshes To Planes (Script)] (サーフェス メッシュから平面 (スクリプト)) コンポーネントを見つけます。
- [Draw Planes] (平面の描画) プロパティを [Nothing] (なし) に変更して、選択を解除します。
- [Draw Planes] (平面の描画) プロパティを [Wall] (壁) に変更して、壁平面のみがレンダリングされるようにします。
- [Project] (プロジェクト) パネルの Scripts フォルダー内の Placeable.cs ファイルをダブルクリックして、Visual Studio で開きます。
Placeable スクリプトが、平面の検出が完了した後に作成されたポスターと投影ボックスに既にアタッチされています。 ここで一部のコードをコメント解除すると、このスクリプトで次の操作が実行されます。
- 境界キューブの中心と四隅からレイキャストすることで、ホログラムがサーフェスに合うかどうかを判断します。
- サーフェスの法線を調べ、ホログラムに十分な滑らかさかどうかを確認します。
- ホログラムの周囲に境界キューブをレンダリングして、配置中の実際のサイズを表示します。
- ホログラムの下または後ろに影を付け、床/壁のどこに配置されるかを示します。
- ホログラムをサーフェスに配置できない場合は、影を赤でレンダリングし、配置できる場合は緑でレンダリングします。
- アフィニティがあるサーフェスの種類 (垂直または水平) に合わせて、ホログラムの向きを変更します。
- ジャンプやスナップ動作を回避するために、選択したサーフェスにホログラムをスムーズに配置します。
以下のコーディング演習のすべてのコードをコメント解除するか、Placeable.cs でこの完成したソリューションを使用します。
using System.Collections.Generic;
using UnityEngine;
using Academy.HoloToolkit.Unity;
/// <summary>
/// Enumeration containing the surfaces on which a GameObject
/// can be placed. For simplicity of this sample, only one
/// surface type is allowed to be selected.
/// </summary>
public enum PlacementSurfaces
{
// Horizontal surface with an upward pointing normal.
Horizontal = 1,
// Vertical surface with a normal facing the user.
Vertical = 2,
}
/// <summary>
/// The Placeable class implements the logic used to determine if a GameObject
/// can be placed on a target surface. Constraints for placement include:
/// * No part of the GameObject's box collider impacts with another object in the scene
/// * The object lays flat (within specified tolerances) against the surface
/// * The object would not fall off of the surface if gravity were enabled.
/// This class also provides the following visualizations.
/// * A transparent cube representing the object's box collider.
/// * Shadow on the target surface indicating whether or not placement is valid.
/// </summary>
public class Placeable : MonoBehaviour
{
[Tooltip("The base material used to render the bounds asset when placement is allowed.")]
public Material PlaceableBoundsMaterial = null;
[Tooltip("The base material used to render the bounds asset when placement is not allowed.")]
public Material NotPlaceableBoundsMaterial = null;
[Tooltip("The material used to render the placement shadow when placement it allowed.")]
public Material PlaceableShadowMaterial = null;
[Tooltip("The material used to render the placement shadow when placement it not allowed.")]
public Material NotPlaceableShadowMaterial = null;
[Tooltip("The type of surface on which the object can be placed.")]
public PlacementSurfaces PlacementSurface = PlacementSurfaces.Horizontal;
[Tooltip("The child object(s) to hide during placement.")]
public List<GameObject> ChildrenToHide = new List<GameObject>();
/// <summary>
/// Indicates if the object is in the process of being placed.
/// </summary>
public bool IsPlacing { get; private set; }
// The most recent distance to the surface. This is used to
// locate the object when the user's gaze does not intersect
// with the Spatial Mapping mesh.
private float lastDistance = 2.0f;
// The distance away from the target surface that the object should hover prior while being placed.
private float hoverDistance = 0.15f;
// Threshold (the closer to 0, the stricter the standard) used to determine if a surface is flat.
private float distanceThreshold = 0.02f;
// Threshold (the closer to 1, the stricter the standard) used to determine if a surface is vertical.
private float upNormalThreshold = 0.9f;
// Maximum distance, from the object, that placement is allowed.
// This is used when raycasting to see if the object is near a placeable surface.
private float maximumPlacementDistance = 5.0f;
// Speed (1.0 being fastest) at which the object settles to the surface upon placement.
private float placementVelocity = 0.06f;
// Indicates whether or not this script manages the object's box collider.
private bool managingBoxCollider = false;
// The box collider used to determine of the object will fit in the desired location.
// It is also used to size the bounding cube.
private BoxCollider boxCollider = null;
// Visible asset used to show the dimensions of the object. This asset is sized
// using the box collider's bounds.
private GameObject boundsAsset = null;
// Visible asset used to show the where the object is attempting to be placed.
// This asset is sized using the box collider's bounds.
private GameObject shadowAsset = null;
// The location at which the object will be placed.
private Vector3 targetPosition;
/// <summary>
/// Called when the GameObject is created.
/// </summary>
private void Awake()
{
targetPosition = gameObject.transform.position;
// Get the object's collider.
boxCollider = gameObject.GetComponent<BoxCollider>();
if (boxCollider == null)
{
// The object does not have a collider, create one and remember that
// we are managing it.
managingBoxCollider = true;
boxCollider = gameObject.AddComponent<BoxCollider>();
boxCollider.enabled = false;
}
// Create the object that will be used to indicate the bounds of the GameObject.
boundsAsset = GameObject.CreatePrimitive(PrimitiveType.Cube);
boundsAsset.transform.parent = gameObject.transform;
boundsAsset.SetActive(false);
// Create a object that will be used as a shadow.
shadowAsset = GameObject.CreatePrimitive(PrimitiveType.Quad);
shadowAsset.transform.parent = gameObject.transform;
shadowAsset.SetActive(false);
}
/// <summary>
/// Called when our object is selected. Generally called by
/// a gesture management component.
/// </summary>
public void OnSelect()
{
/* TODO: 4.a CODE ALONG 4.a */
if (!IsPlacing)
{
OnPlacementStart();
}
else
{
OnPlacementStop();
}
}
/// <summary>
/// Called once per frame.
/// </summary>
private void Update()
{
/* TODO: 4.a CODE ALONG 4.a */
if (IsPlacing)
{
// Move the object.
Move();
// Set the visual elements.
Vector3 targetPosition;
Vector3 surfaceNormal;
bool canBePlaced = ValidatePlacement(out targetPosition, out surfaceNormal);
DisplayBounds(canBePlaced);
DisplayShadow(targetPosition, surfaceNormal, canBePlaced);
}
else
{
// Disable the visual elements.
boundsAsset.SetActive(false);
shadowAsset.SetActive(false);
// Gracefully place the object on the target surface.
float dist = (gameObject.transform.position - targetPosition).magnitude;
if (dist > 0)
{
gameObject.transform.position = Vector3.Lerp(gameObject.transform.position, targetPosition, placementVelocity / dist);
}
else
{
// Unhide the child object(s) to make placement easier.
for (int i = 0; i < ChildrenToHide.Count; i++)
{
ChildrenToHide[i].SetActive(true);
}
}
}
}
/// <summary>
/// Verify whether or not the object can be placed.
/// </summary>
/// <param name="position">
/// The target position on the surface.
/// </param>
/// <param name="surfaceNormal">
/// The normal of the surface on which the object is to be placed.
/// </param>
/// <returns>
/// True if the target position is valid for placing the object, otherwise false.
/// </returns>
private bool ValidatePlacement(out Vector3 position, out Vector3 surfaceNormal)
{
Vector3 raycastDirection = gameObject.transform.forward;
if (PlacementSurface == PlacementSurfaces.Horizontal)
{
// Placing on horizontal surfaces.
// Raycast from the bottom face of the box collider.
raycastDirection = -(Vector3.up);
}
// Initialize out parameters.
position = Vector3.zero;
surfaceNormal = Vector3.zero;
Vector3[] facePoints = GetColliderFacePoints();
// The origin points we receive are in local space and we
// need to raycast in world space.
for (int i = 0; i < facePoints.Length; i++)
{
facePoints[i] = gameObject.transform.TransformVector(facePoints[i]) + gameObject.transform.position;
}
// Cast a ray from the center of the box collider face to the surface.
RaycastHit centerHit;
if (!Physics.Raycast(facePoints[0],
raycastDirection,
out centerHit,
maximumPlacementDistance,
SpatialMappingManager.Instance.LayerMask))
{
// If the ray failed to hit the surface, we are done.
return false;
}
// We have found a surface. Set position and surfaceNormal.
position = centerHit.point;
surfaceNormal = centerHit.normal;
// Cast a ray from the corners of the box collider face to the surface.
for (int i = 1; i < facePoints.Length; i++)
{
RaycastHit hitInfo;
if (Physics.Raycast(facePoints[i],
raycastDirection,
out hitInfo,
maximumPlacementDistance,
SpatialMappingManager.Instance.LayerMask))
{
// To be a valid placement location, each of the corners must have a similar
// enough distance to the surface as the center point
if (!IsEquivalentDistance(centerHit.distance, hitInfo.distance))
{
return false;
}
}
else
{
// The raycast failed to intersect with the target layer.
return false;
}
}
return true;
}
/// <summary>
/// Determine the coordinates, in local space, of the box collider face that
/// will be placed against the target surface.
/// </summary>
/// <returns>
/// Vector3 array with the center point of the face at index 0.
/// </returns>
private Vector3[] GetColliderFacePoints()
{
// Get the collider extents.
// The size values are twice the extents.
Vector3 extents = boxCollider.size / 2;
// Calculate the min and max values for each coordinate.
float minX = boxCollider.center.x - extents.x;
float maxX = boxCollider.center.x + extents.x;
float minY = boxCollider.center.y - extents.y;
float maxY = boxCollider.center.y + extents.y;
float minZ = boxCollider.center.z - extents.z;
float maxZ = boxCollider.center.z + extents.z;
Vector3 center;
Vector3 corner0;
Vector3 corner1;
Vector3 corner2;
Vector3 corner3;
if (PlacementSurface == PlacementSurfaces.Horizontal)
{
// Placing on horizontal surfaces.
center = new Vector3(boxCollider.center.x, minY, boxCollider.center.z);
corner0 = new Vector3(minX, minY, minZ);
corner1 = new Vector3(minX, minY, maxZ);
corner2 = new Vector3(maxX, minY, minZ);
corner3 = new Vector3(maxX, minY, maxZ);
}
else
{
// Placing on vertical surfaces.
center = new Vector3(boxCollider.center.x, boxCollider.center.y, maxZ);
corner0 = new Vector3(minX, minY, maxZ);
corner1 = new Vector3(minX, maxY, maxZ);
corner2 = new Vector3(maxX, minY, maxZ);
corner3 = new Vector3(maxX, maxY, maxZ);
}
return new Vector3[] { center, corner0, corner1, corner2, corner3 };
}
/// <summary>
/// Put the object into placement mode.
/// </summary>
public void OnPlacementStart()
{
// If we are managing the collider, enable it.
if (managingBoxCollider)
{
boxCollider.enabled = true;
}
// Hide the child object(s) to make placement easier.
for (int i = 0; i < ChildrenToHide.Count; i++)
{
ChildrenToHide[i].SetActive(false);
}
// Tell the gesture manager that it is to assume
// all input is to be given to this object.
GestureManager.Instance.OverrideFocusedObject = gameObject;
// Enter placement mode.
IsPlacing = true;
}
/// <summary>
/// Take the object out of placement mode.
/// </summary>
/// <remarks>
/// This method will leave the object in placement mode if called while
/// the object is in an invalid location. To determine whether or not
/// the object has been placed, check the value of the IsPlacing property.
/// </remarks>
public void OnPlacementStop()
{
// ValidatePlacement requires a normal as an out parameter.
Vector3 position;
Vector3 surfaceNormal;
// Check to see if we can exit placement mode.
if (!ValidatePlacement(out position, out surfaceNormal))
{
return;
}
// The object is allowed to be placed.
// We are placing at a small buffer away from the surface.
targetPosition = position + (0.01f * surfaceNormal);
OrientObject(true, surfaceNormal);
// If we are managing the collider, disable it.
if (managingBoxCollider)
{
boxCollider.enabled = false;
}
// Tell the gesture manager that it is to resume
// its normal behavior.
GestureManager.Instance.OverrideFocusedObject = null;
// Exit placement mode.
IsPlacing = false;
}
/// <summary>
/// Positions the object along the surface toward which the user is gazing.
/// </summary>
/// <remarks>
/// If the user's gaze does not intersect with a surface, the object
/// will remain at the most recently calculated distance.
/// </remarks>
private void Move()
{
Vector3 moveTo = gameObject.transform.position;
Vector3 surfaceNormal = Vector3.zero;
RaycastHit hitInfo;
bool hit = Physics.Raycast(Camera.main.transform.position,
Camera.main.transform.forward,
out hitInfo,
20f,
SpatialMappingManager.Instance.LayerMask);
if (hit)
{
float offsetDistance = hoverDistance;
// Place the object a small distance away from the surface while keeping
// the object from going behind the user.
if (hitInfo.distance <= hoverDistance)
{
offsetDistance = 0f;
}
moveTo = hitInfo.point + (offsetDistance * hitInfo.normal);
lastDistance = hitInfo.distance;
surfaceNormal = hitInfo.normal;
}
else
{
// The raycast failed to hit a surface. In this case, keep the object at the distance of the last
// intersected surface.
moveTo = Camera.main.transform.position + (Camera.main.transform.forward * lastDistance);
}
// Follow the user's gaze.
float dist = Mathf.Abs((gameObject.transform.position - moveTo).magnitude);
gameObject.transform.position = Vector3.Lerp(gameObject.transform.position, moveTo, placementVelocity / dist);
// Orient the object.
// We are using the return value from Physics.Raycast to instruct
// the OrientObject function to align to the vertical surface if appropriate.
OrientObject(hit, surfaceNormal);
}
/// <summary>
/// Orients the object so that it faces the user.
/// </summary>
/// <param name="alignToVerticalSurface">
/// If true and the object is to be placed on a vertical surface,
/// orient parallel to the target surface. If false, orient the object
/// to face the user.
/// </param>
/// <param name="surfaceNormal">
/// The target surface's normal vector.
/// </param>
/// <remarks>
/// The alignToVerticalSurface parameter is ignored if the object
/// is to be placed on a horizontalSurface
/// </remarks>
private void OrientObject(bool alignToVerticalSurface, Vector3 surfaceNormal)
{
Quaternion rotation = Camera.main.transform.localRotation;
// If the user's gaze does not intersect with the Spatial Mapping mesh,
// orient the object towards the user.
if (alignToVerticalSurface && (PlacementSurface == PlacementSurfaces.Vertical))
{
// We are placing on a vertical surface.
// If the normal of the Spatial Mapping mesh indicates that the
// surface is vertical, orient parallel to the surface.
if (Mathf.Abs(surfaceNormal.y) <= (1 - upNormalThreshold))
{
rotation = Quaternion.LookRotation(-surfaceNormal, Vector3.up);
}
}
else
{
rotation.x = 0f;
rotation.z = 0f;
}
gameObject.transform.rotation = rotation;
}
/// <summary>
/// Displays the bounds asset.
/// </summary>
/// <param name="canBePlaced">
/// Specifies if the object is in a valid placement location.
/// </param>
private void DisplayBounds(bool canBePlaced)
{
// Ensure the bounds asset is sized and positioned correctly.
boundsAsset.transform.localPosition = boxCollider.center;
boundsAsset.transform.localScale = boxCollider.size;
boundsAsset.transform.rotation = gameObject.transform.rotation;
// Apply the appropriate material.
if (canBePlaced)
{
boundsAsset.GetComponent<Renderer>().sharedMaterial = PlaceableBoundsMaterial;
}
else
{
boundsAsset.GetComponent<Renderer>().sharedMaterial = NotPlaceableBoundsMaterial;
}
// Show the bounds asset.
boundsAsset.SetActive(true);
}
/// <summary>
/// Displays the placement shadow asset.
/// </summary>
/// <param name="position">
/// The position at which to place the shadow asset.
/// </param>
/// <param name="surfaceNormal">
/// The normal of the surface on which the asset will be placed
/// </param>
/// <param name="canBePlaced">
/// Specifies if the object is in a valid placement location.
/// </param>
private void DisplayShadow(Vector3 position,
Vector3 surfaceNormal,
bool canBePlaced)
{
// Rotate and scale the shadow so that it is displayed on the correct surface and matches the object.
float rotationX = 0.0f;
if (PlacementSurface == PlacementSurfaces.Horizontal)
{
rotationX = 90.0f;
shadowAsset.transform.localScale = new Vector3(boxCollider.size.x, boxCollider.size.z, 1);
}
else
{
shadowAsset.transform.localScale = boxCollider.size;
}
Quaternion rotation = Quaternion.Euler(rotationX, gameObject.transform.rotation.eulerAngles.y, 0);
shadowAsset.transform.rotation = rotation;
// Apply the appropriate material.
if (canBePlaced)
{
shadowAsset.GetComponent<Renderer>().sharedMaterial = PlaceableShadowMaterial;
}
else
{
shadowAsset.GetComponent<Renderer>().sharedMaterial = NotPlaceableShadowMaterial;
}
// Show the shadow asset as appropriate.
if (position != Vector3.zero)
{
// Position the shadow a small distance from the target surface, along the normal.
shadowAsset.transform.position = position + (0.01f * surfaceNormal);
shadowAsset.SetActive(true);
}
else
{
shadowAsset.SetActive(false);
}
}
/// <summary>
/// Determines if two distance values should be considered equivalent.
/// </summary>
/// <param name="d1">
/// Distance to compare.
/// </param>
/// <param name="d2">
/// Distance to compare.
/// </param>
/// <returns>
/// True if the distances are within the desired tolerance, otherwise false.
/// </returns>
private bool IsEquivalentDistance(float d1, float d2)
{
float dist = Mathf.Abs(d1 - d2);
return (dist <= distanceThreshold);
}
/// <summary>
/// Called when the GameObject is unloaded.
/// </summary>
private void OnDestroy()
{
// Unload objects we have created.
Destroy(boundsAsset);
boundsAsset = null;
Destroy(shadowAsset);
shadowAsset = null;
}
}
ビルドと配置
- 前と同様に、プロジェクトをビルドし、HoloLens に配置します。
- 空間マッピング データのスキャンと処理が完了するまで待ちます。
- 太陽系が表示されたら、下の投影ボックスに視線入力し、選択ジェスチャを実行して動かします。 投影ボックスが選択されている間は、投影ボックスの周囲に境界キューブが表示されます。
- 頭を動かして、部屋の別の場所に視線入力します。 投影ボックスは視線入力に従います。 投影ボックスの下の影が赤に変わった場合は、ホログラムをそのサーフェスに配置することはできません。 投影ボックスの下の影が緑色に変わった場合は、別の選択ジェスチャを実行してホログラムを配置できます。
- 壁のホログラフィック ポスターの 1 つを見つけて選択し、新しい場所に移動します。 ポスターを床や壁に配置することはできません。また、動き回っても正しい向きのまま壁に維持されます。
第 5 章 - オクルージョン
目標
- ホログラムが空間マッピング メッシュによって隠されるかどうかを判断します。
- さまざまなオクルージョン手法を適用して、楽しい効果を実現します。
手順
まず、空間マッピング メッシュが、現実世界を隠さずに他のホログラムを隠すことを許可します。
- [Hierarchy] (階層) パネルで、SpatialProcessing オブジェクトを選択します。
- [Inspector] (インスペクター) パネルで、[Play Space Manager (Script)] (空間マネージャーの再生 (スクリプト)) コンポーネントを見つけます。
- [Secondary Material] (セカンダリ素材) プロパティの右側にある円をクリックします。
- [Occlusion] (オクルージョン) 素材を探して選択し、ウィンドウを閉じます。
次に、地球に特別な動作を追加して、別のホログラム (太陽など) または空間マッピング メッシュによって隠されるたびに、青くハイライトされるようにします。
- [Project] (プロジェクト) パネルの Holograms フォルダーで、SolarSystem オブジェクトを展開します。
- [Earth] (地球) をクリックします。
- [Inspector] (インスペクター) パネルで、地球の素材 (下部のコンポーネント) を探します。
- [Shader] (シェーダー) ドロップダウンで、シェーダーを [Custom] (カスタム) > [OcclusionRim] に変更します。 これにより、別のオブジェクトによって隠されるたびに、地球の周りに青いハイライトがレンダリングされます。
最後に、太陽系の惑星に対して X 線ビジョンの効果を有効にします。 次を実現するには、PlanetOcclusion.cs (Scripts\SolarSystem フォルダーにあります) を編集する必要があります。
- SpatialMapping レイヤー (部屋のメッシュと平面) によって惑星が隠されているかどうかを判断します。
- SpatialMapping レイヤーによって隠されるたびに、惑星のワイヤーフレーム表現を表示します。
- SpatialMapping レイヤーによってブロックされていないときは、惑星のワイヤーフレーム表現を非表示にします。
PlanetOcclusion.cs のコーディング演習に従うか、次のソリューションを使用します。
using UnityEngine;
using Academy.HoloToolkit.Unity;
/// <summary>
/// Determines when the occluded version of the planet should be visible.
/// This script allows us to do selective occlusion, so the occlusionObject
/// will only be rendered when a Spatial Mapping surface is occluding the planet,
/// not when another hologram is responsible for the occlusion.
/// </summary>
public class PlanetOcclusion : MonoBehaviour
{
[Tooltip("Object to display when the planet is occluded.")]
public GameObject occlusionObject;
/// <summary>
/// Points to raycast to when checking for occlusion.
/// </summary>
private Vector3[] checkPoints;
// Use this for initialization
void Start()
{
occlusionObject.SetActive(false);
// Set the check points to use when testing for occlusion.
MeshFilter filter = gameObject.GetComponent<MeshFilter>();
Vector3 extents = filter.mesh.bounds.extents;
Vector3 center = filter.mesh.bounds.center;
Vector3 top = new Vector3(center.x, center.y + extents.y, center.z);
Vector3 left = new Vector3(center.x - extents.x, center.y, center.z);
Vector3 right = new Vector3(center.x + extents.x, center.y, center.z);
Vector3 bottom = new Vector3(center.x, center.y - extents.y, center.z);
checkPoints = new Vector3[] { center, top, left, right, bottom };
}
// Update is called once per frame
void Update()
{
/* TODO: 5.a DEVELOPER CODING EXERCISE 5.a */
// Check to see if any of the planet's boundary points are occluded.
for (int i = 0; i < checkPoints.Length; i++)
{
// 5.a: Convert the current checkPoint to world coordinates.
// Call gameObject.transform.TransformPoint(checkPoints[i]).
// Assign the result to a new Vector3 variable called 'checkPt'.
Vector3 checkPt = gameObject.transform.TransformPoint(checkPoints[i]);
// 5.a: Call Vector3.Distance() to calculate the distance
// between the Main Camera's position and 'checkPt'.
// Assign the result to a new float variable called 'distance'.
float distance = Vector3.Distance(Camera.main.transform.position, checkPt);
// 5.a: Take 'checkPt' and subtract the Main Camera's position from it.
// Assign the result to a new Vector3 variable called 'direction'.
Vector3 direction = checkPt - Camera.main.transform.position;
// Used to indicate if the call to Physics.Raycast() was successful.
bool raycastHit = false;
// 5.a: Check if the planet is occluded by a spatial mapping surface.
// Call Physics.Raycast() with the following arguments:
// - Pass in the Main Camera's position as the origin.
// - Pass in 'direction' for the direction.
// - Pass in 'distance' for the maxDistance.
// - Pass in SpatialMappingManager.Instance.LayerMask as layerMask.
// Assign the result to 'raycastHit'.
raycastHit = Physics.Raycast(Camera.main.transform.position, direction, distance, SpatialMappingManager.Instance.LayerMask);
if (raycastHit)
{
// 5.a: Our raycast hit a surface, so the planet is occluded.
// Set the occlusionObject to active.
occlusionObject.SetActive(true);
// At least one point is occluded, so break from the loop.
break;
}
else
{
// 5.a: The Raycast did not hit, so the planet is not occluded.
// Deactivate the occlusionObject.
occlusionObject.SetActive(false);
}
}
}
}
ビルドと配置
- 通常どおり、アプリケーションをビルドして HoloLens に配置します。
- 空間マッピング データのスキャンと処理が完了するまで待ちます (壁に青い線が表示されます)。
- 太陽系の投影ボックスを見つけて選択し、壁の横またはカウンターの後ろにボックスを設定します。
- ポスターまたは投影ボックスを見てサーフェスの背後に隠すことで、基本的なオクルージョンを表示できます。
- 地球を探して、別のホログラムまたはサーフェスの後ろに入るたびに青いハイライト効果が出るのを確認します。
- 惑星が壁や部屋の他のサーフェスの後ろを移動するのを見ます。 X 線ビジョンになり、ワイヤーフレームのスケルトンが表示されます。
終了
おめでとうございます。 「MR 空間 230: 空間マッピング」が完了しました。
- 環境をスキャンし、Unity に空間マッピング データを読み込む方法を確認しました。
- シェーダーの基本と、素材を使用して世界を再視覚化する方法を理解しました。
- 平面を検索し、メッシュから三角形を削除するための新しい処理手法について学習しました。
- ホログラムを移動して、理にかなった方法でサーフェスに配置できました。
- さまざまなオクルージョン手法を経験し、X 線ビジョンを活用しました。