Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Penting
Tutorial Mixed Reality Academy dirancang dengan HoloLens (generasi ke-1), Unity 2017, dan Mixed Reality Headset Imersif dalam pikiran. Dengan demikian, kami merasa penting untuk meninggalkan tutorial ini di tempat bagi pengembang yang masih mencari panduan dalam mengembangkan untuk perangkat tersebut. Tutorial ini tidak akan diperbarui dengan toolset atau interaksi terbaru yang digunakan untuk HoloLens 2 dan mungkin tidak kompatibel dengan versi Unity yang lebih baru. Mereka akan dipertahankan untuk terus bekerja pada perangkat yang didukung. Serangkaian tutorial baru telah diposting untuk HoloLens 2.
Pemetaan spasial menggabungkan dunia nyata dan dunia virtual bersama-sama dengan mengajarkan hologram tentang lingkungan. Dalam MR Spatial 230 (Project Planetarium) kita akan mempelajari cara:
- Pindai lingkungan dan transfer data dari HoloLens ke komputer pengembangan Anda.
- Jelajahi shader dan pelajari cara menggunakannya untuk memvisualisasikan ruang Anda.
- Memecah jala ruangan menjadi bidang sederhana menggunakan pemrosesan jala.
- Melampaui teknik penempatan yang kami pelajari di MR Basics 101, dan berikan umpan balik tentang di mana hologram dapat ditempatkan di lingkungan.
- Jelajahi efek oklusi, jadi ketika hologram Anda berada di belakang objek dunia nyata, Anda masih dapat melihatnya dengan visi sinar x!
Dukungan perangkat
| Kursus | HoloLens | Headset imersif |
|---|---|---|
| MR Spasial 230: Pemetaan spasial | ✔️ |
Sebelum memulai
Prasyarat
- PC Windows 10 dikonfigurasi dengan alat yang benar terinstal.
- Beberapa kemampuan pemrograman C# dasar.
- Anda seharusnya telah menyelesaikan MR Basics 101.
- Perangkat HoloLens dikonfigurasi untuk pengembangan.
File proyek
- Unduh file yang diperlukan oleh proyek. Membutuhkan Unity 2017.2 atau yang lebih baru.
- Batalkan arsip file ke desktop Anda atau lokasi lain yang mudah dijangkau.
Catatan
Jika Anda ingin melihat melalui kode sumber sebelum mengunduh, kode tersebut tersedia di GitHub.
Catatan
- "Aktifkan Hanya Kode Saya" di Visual Studio perlu dinonaktifkan (tidak dicentang) di bawah Opsi > Alat > Penelusuran Kesalahan untuk mencapai titik henti dalam kode Anda.
Penyiapan Unity
- Mulai Unity.
- Pilih Baru untuk membuat proyek baru.
- Beri nama proyek Planetarium.
- Verifikasi bahwa pengaturan 3D dipilih.
- Klik Buat Project.
- Setelah Unity diluncurkan, buka Edit > Pemutar Pengaturan > Proyek.
- Di panel Pemeriksa , temukan dan pilih ikon Bursa Windows hijau.
- Perluas Pengaturan Lain.
- Di bagian Penyajian , centang opsi Realitas Virtual yang Didukung .
- Verifikasi bahwa Windows Holographic muncul dalam daftar SDK Realitas Virtual. Jika tidak, pilih tombol + di bagian bawah daftar dan pilih Windows Holographic.
- Perluas Pengaturan Penerbitan.
- Di bagian Kapabilitas , periksa pengaturan berikut ini:
- InternetClientServer
- PrivateNetworkClientServer
- Mikrofon
- Persepsi Spasial
- Buka Edit > Kualitas Pengaturan > Proyek
- Di panel Pemeriksa , di bawah ikon Bursa Windows, pilih panah drop-down hitam di bawah baris 'Default' dan ubah pengaturan default menjadi Sangat Rendah.
- Buka Paket Kustom Impor > Aset > Paket.
- Navigasi ke folder ...\HolographicAcademy-Holograms-230-SpatialMapping\Starting .
- Klik Planetarium.unitypackage.
- Klik Buka.
- Jendela Impor Paket Unity akan muncul, klik tombol Impor .
- Tunggu Unity mengimpor semua aset yang kita perlukan untuk menyelesaikan proyek ini.
- Di panel Hierarki , hapus Kamera Utama.
- Di panel Proyek , folder HoloToolkit-SpatialMapping-230\Utilities\Prefabs , temukan objek Kamera Utama .
- Seret dan letakkan prefab Kamera Utama ke panel Hierarki .
- Di panel Hierarki , hapus objek Cahaya Arah .
- Di panel Proyek , folder Hologram , temukan objek Kursor .
- Seret & letakkan prefab Kursor ke hierarki.
- Di panel Hierarki , pilih objek Kursor .
- Di panel Pemeriksa , klik menu drop-down Lapisan dan pilih Edit Lapisan....
- Beri nama User Layer 31 sebagai "SpatialMapping".
- Simpan adegan baru: Simpan Adegan Sebagai > ...
- Klik Folder Baru dan beri nama folder Adegan.
- Beri nama file "Planetarium" dan simpan di folder Adegan .
Bab 1 - Pemindaian
Tujuan
- Pelajari tentang SurfaceObserver dan bagaimana pengaturannya memengaruhi pengalaman dan performa.
- Buat pengalaman pemindaian ruangan untuk mengumpulkan jala kamar Anda.
Petunjuk
- Di panel ProyekHoloToolkit-SpatialMapping-230\SpatialMapping\Prefabs folder, temukan prefab SpatialMapping .
- Seret & letakkan prefab SpatialMapping ke panel Hierarki .
Membangun dan Menyebarkan (bagian 1)
- Di Unity, pilih Pengaturan Build File>.
- Klik Tambahkan Adegan Terbuka untuk menambahkan adegan Planetarium ke build.
- Pilih Platform Windows Universal di daftar Platform dan klik Beralih Platform.
- Atur SDK ke Universal 10 dan UWP Build Type ke D3D.
- Periksa Proyek Unity C#.
- Klik Bangun.
- Buat Folder Baru bernama "Aplikasi".
- Klik tunggal folder Aplikasi .
- Tekan tombol Pilih Folder .
- Ketika Unity selesai membangun, jendela File Explorer akan muncul.
- Klik dua kali pada folder Aplikasi untuk membukanya.
- Klik dua kali pada Planetarium.sln untuk memuat proyek di Visual Studio.
- Di Visual Studio, gunakan toolbar atas untuk mengubah Konfigurasi menjadi Rilis.
- Ubah Platform menjadi x86.
- Klik panah drop-down di sebelah kanan 'Komputer Lokal', dan pilih Komputer Jarak Jauh.
- Masukkan alamat IP perangkat Anda di bidang Alamat dan ubah Mode Autentikasi menjadi Universal (Protokol Tidak Terenkripsi).
- Klik Debug -> Mulai Tanpa penelusuran kesalahan atau tekan Ctrl + F5.
- Tonton panel Output di Visual Studio untuk status build dan deploy.
- Setelah aplikasi Anda disebarkan, berjalanlah di sekitar ruangan. Anda akan melihat permukaan di sekitarnya ditutupi oleh jala wireframe hitam dan putih.
- Pindai lingkungan sekitarmu. Pastikan untuk melihat dinding, langit-langit, dan lantai.
Bangun dan Sebarkan (bagian 2)
Sekarang mari kita jelajahi bagaimana Pemetaan Spasial dapat memengaruhi performa.
- Di Unity, pilih Window > Profiler.
- Klik Tambahkan Profiler > GPU.
- Klik Profiler ><Aktif Masukkan IP>.
- Masukkan alamat IP HoloLens Anda.
- Klik Sambungkan.
- Amati jumlah milidetik yang diperlukan GPU untuk merender bingkai.
- Hentikan aplikasi agar tidak berjalan pada perangkat.
- Kembali ke Visual Studio dan buka SpatialMappingObserver.cs. Anda akan menemukannya di folder HoloToolkit\SpatialMapping dari proyek Assembly-CSharp (Universal Windows).
- Temukan fungsi Awake(), dan tambahkan baris kode berikut: TrianglesPerCubicMeter = 1200;
- Sebarkan kembali proyek ke perangkat Anda, lalu sambungkan kembali profiler. Amati perubahan jumlah milidetik untuk merender bingkai.
- Hentikan aplikasi agar tidak berjalan pada perangkat.
Simpan dan muat di Unity
Akhirnya, mari kita simpan jala kamar kita dan muat ke Unity.
- Kembali ke Visual Studio dan hapus garis TrianglesPerCubicMeter yang Anda tambahkan dalam fungsi Awake() selama bagian sebelumnya.
- Sebarkan ulang proyek ke perangkat Anda. Kita sekarang harus berjalan dengan 500 segitiga per meter kubik.
- Buka browser dan masukkan di HoloLens IPAddress Anda untuk menavigasi ke Portal Perangkat Windows.
- Pilih opsi Tampilan 3D di panel kiri.
- Di bawah Rekonstruksi permukaan pilih tombol Perbarui .
- Tonton sebagai area yang telah Anda pindai di HoloLens Anda muncul di jendela tampilan.
- Untuk menyimpan pemindaian ruangan Anda, tekan tombol Simpan .
- Buka folder Unduhan Anda untuk menemukan model ruang tersimpan SRMesh.obj.
- Salin SRMesh.obj ke folder Aset proyek Unity Anda.
- Di Unity, pilih objek SpatialMapping di panel Hierarki .
- Temukan komponen Pengamat Permukaan Objek (Skrip).
- Klik lingkaran di sebelah kanan properti Model Ruangan .
- Temukan dan pilih objek SRMesh lalu tutup jendela.
- Verifikasi bahwa properti Model Ruangan di panel Inspektur sekarang diatur ke SRMesh.
- Tekan tombol Putar untuk memasuki mode pratinjau Unity.
- Komponen SpatialMapping akan memuat jala dari model kamar yang disimpan sehingga Anda dapat menggunakannya di Unity.
- Beralih ke Tampilan adegan untuk melihat semua model kamar Anda yang ditampilkan dengan pemecah bingkai kawat.
- Tekan tombol Putar lagi untuk keluar dari mode pratinjau.
CATATAN: Lain kali Anda memasuki mode pratinjau di Unity, itu akan memuat jala kamar yang disimpan secara default.
Bab 2 - Visualisasi
Tujuan
- Pelajari dasar-dasar shader.
- Visualisasikan lingkungan Anda.
Petunjuk
- Di panel Hierarki Unity , pilih objek SpatialMapping .
- Di panel Inspektur, temukan komponen Pengelola Pemetaan Spasial (Skrip).
- Klik lingkaran di sebelah kanan properti Surface Material .
- Temukan dan pilih bahan BlueLinesOnWalls dan tutup jendela.
- Di folder Shaders panel Proyek, klik dua kali pada BlueLinesOnWalls untuk membuka shader di Visual Studio.
- Ini adalah shader piksel sederhana (vertex ke fragmen), yang menyelesaikan tugas-tugas berikut:
- Mengonversi lokasi puncak ke ruang dunia.
- Memeriksa normal vertex untuk menentukan apakah piksel vertikal.
- Mengatur warna piksel untuk penyajian.
Membangun dan Menyebarkan
- Kembali ke Unity dan tekan Putar untuk memasuki mode pratinjau.
- Garis biru akan dirender pada semua permukaan vertikal jala ruangan (yang secara otomatis dimuat dari data pemindaian kami yang disimpan).
- Beralih ke tab Adegan untuk menyesuaikan tampilan ruangan Anda dan melihat bagaimana seluruh jala ruangan muncul di Unity.
- Di panel Proyek , temukan folder Bahan dan pilih materi BlueLinesOnWalls .
- Ubah beberapa properti dan lihat bagaimana perubahan muncul di editor Unity.
- Di panel Pemeriksa , sesuaikan nilai LineScale untuk membuat garis tampak lebih tebal atau lebih tipis.
- Di panel Pemeriksa , sesuaikan nilai LinesPerMeter untuk mengubah berapa banyak garis yang muncul di setiap dinding.
- Klik Putar lagi untuk keluar dari mode pratinjau.
- Bangun dan sebarkan ke HoloLens dan amati bagaimana penyajian shader muncul di permukaan nyata.
Unity melakukan pekerjaan yang bagus untuk mempratinjau materi, tetapi selalu merupakan ide yang baik untuk memeriksa penyajian di perangkat.
Bab 3 - Pemrosesan
Tujuan
- Pelajari teknik untuk memproses data pemetaan spasial untuk digunakan dalam aplikasi Anda.
- Analisis data pemetaan spasial untuk menemukan bidang dan menghapus segitiga.
- Gunakan bidang untuk penempatan hologram.
Petunjuk
- Di panel Proyek Unity, folder Hologram , temukan objek SpatialProcessing .
- Seret & letakkan objek SpatialProcessing ke panel Hierarki .
Prefab SpatialProcessing mencakup komponen untuk memproses data pemetaan spasial. SurfaceMeshesToPlanes.cs akan menemukan dan menghasilkan bidang berdasarkan data pemetaan spasial. Kami akan menggunakan pesawat dalam aplikasi kami untuk mewakili dinding, lantai, dan langit-langit. Prefab ini juga mencakup RemoveSurfaceVertices.cs yang dapat menghapus simpul dari jala pemetaan spasial. Ini dapat digunakan untuk membuat lubang di jala, atau untuk menghilangkan kelebihan segitiga yang tidak lagi diperlukan (karena bidang dapat digunakan sebagai gantinya).
- Di panel Proyek Unity, folder Hologram , temukan objek SpaceCollection .
- Seret dan letakkan objek SpaceCollection ke panel Hierarki .
- Di panel Hierarki , pilih objek SpatialProcessing .
- Di panel Inspektur, temukan komponen Play Space Manager (Skrip).
- Klik dua kali pada PlaySpaceManager.cs untuk membukanya di Visual Studio.
PlaySpaceManager.cs berisi kode khusus aplikasi. Kami akan menambahkan fungsionalitas ke skrip ini untuk mengaktifkan perilaku berikut:
- Berhenti mengumpulkan data pemetaan spasial setelah kami melebihi batas waktu pemindaian (10 detik).
- Memproses data pemetaan spasial:
- Gunakan SurfaceMeshesToPlanes untuk membuat representasi dunia yang lebih sederhana sebagai bidang (dinding, lantai, langit-langit, dll).
- Gunakan RemoveSurfaceVertices untuk menghilangkan segitiga permukaan yang termasuk dalam batas bidang.
- Hasilkan kumpulan hologram di dunia dan letakkan di dinding dan bidang lantai di dekat pengguna.
Selesaikan latihan pengkodian yang ditandai di PlaySpaceManager.cs, atau ganti skrip dengan solusi jadi dari bawah ini:
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;
}
}
}
Membangun dan Menyebarkan
- Sebelum menyebarkan ke HoloLens, tekan tombol Putar di Unity untuk memasuki mode putar.
- Setelah jala ruangan dimuat dari file, tunggu selama 10 detik sebelum pemrosesan dimulai pada jala pemetaan spasial.
- Ketika pemrosesan selesai, bidang akan tampak mewakili lantai, dinding, langit-langit, dll.
- Setelah semua pesawat ditemukan, Anda akan melihat tata surya muncul di meja lantai di dekat kamera.
- Dua poster juga akan muncul di dinding di dekat kamera. Beralih ke tab Adegan jika Anda tidak dapat melihatnya dalam mode Game .
- Tekan tombol Putar lagi untuk keluar dari mode putar.
- Bangun dan sebarkan ke HoloLens, seperti biasa.
- Tunggu hingga pemindaian dan pemrosesan data pemetaan spasial selesai.
- Setelah Anda melihat pesawat, cobalah untuk menemukan tata surya dan poster di dunia Anda.
Bab 4 - Penempatan
Tujuan
- Tentukan apakah hologram akan pas di permukaan.
- Berikan umpan balik kepada pengguna saat hologram dapat/tidak dapat muat di permukaan.
Petunjuk
- Di panel Hierarki Unity , pilih objek SpatialProcessing .
- Di panel Inspektur, temukan komponen Surface Meshes To Planes (Skrip).
- Ubah properti Gambar Bidang menjadi Tidak Ada untuk menghapus pilihan.
- Ubah properti Draw Planes ke Wall, sehingga hanya bidang dinding yang akan dirender.
- Di panel Proyek , folder Skrip , klik dua kali placeable.cs untuk membukanya di Visual Studio.
Skrip Yang Dapat Ditempatkan sudah dilampirkan ke poster dan kotak proyeksi yang dibuat setelah temuan bidang selesai. Yang perlu kita lakukan adalah menghapus beberapa kode, dan skrip ini akan mencapai hal berikut:
- Tentukan apakah hologram akan pas di permukaan dengan raycasting dari tengah dan empat sudut kubus pembatas.
- Periksa permukaan normal untuk menentukan apakah cukup halus bagi hologram untuk duduk menyiram.
- Render kubus pembatas di sekitar hologram untuk menunjukkan ukuran aktualnya saat ditempatkan.
- Melemparkan bayangan di bawah/di belakang hologram untuk menunjukkan di mana ia akan ditempatkan di lantai / dinding.
- Render bayangan sebagai merah, jika hologram tidak dapat ditempatkan di permukaan, atau hijau, jika bisa.
- Arahkan kembali hologram untuk menyelaraskan dengan jenis permukaan (vertikal atau horizontal) yang memiliki afinitas.
- Tempatkan hologram dengan lancar di permukaan yang dipilih untuk menghindari perilaku melompat atau mematahkan.
Batalkan komentar semua kode dalam latihan pengodean di bawah ini, atau gunakan solusi lengkap ini di 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;
}
}
Membangun dan Menyebarkan
- Seperti sebelumnya, bangun proyek dan sebarkan ke HoloLens.
- Tunggu hingga pemindaian dan pemrosesan data pemetaan spasial selesai.
- Saat Anda melihat tata surya, tatap kotak proyeksi di bawah ini dan lakukan gerakan pilih untuk memindahkannya. Saat kotak proyeksi dipilih, kubus pembatas akan terlihat di sekitar kotak proyeksi.
- Pindahkan Anda untuk menatap ke lokasi yang berbeda di ruangan. Kotak proyeksi harus mengikuti tatapan Anda. Ketika bayangan di bawah kotak proyeksi berubah menjadi merah, Anda tidak dapat menempatkan hologram di permukaan tersebut. Saat bayangan di bawah kotak proyeksi berubah menjadi hijau, Anda dapat menempatkan hologram dengan melakukan gerakan pemilihan lainnya.
- Temukan dan pilih salah satu poster holografik di dinding untuk memindahkannya ke lokasi baru. Perhatikan bahwa Anda tidak dapat menempatkan poster di lantai atau langit-langit, dan bahwa itu tetap berorientasi dengan benar ke setiap dinding saat Anda bergerak.
Bab 5 - Oklusi
Tujuan
- Tentukan apakah hologram dihilangkan oleh jala pemetaan spasial.
- Terapkan teknik oklusi yang berbeda untuk mencapai efek yang menyenangkan.
Petunjuk
Pertama, kita akan mengizinkan jala pemetaan spasial untuk menempati hologram lain tanpa menempati dunia nyata:
- Di panel Hierarki , pilih objek SpatialProcessing .
- Di panel Inspektur, temukan komponen Play Space Manager (Skrip).
- Klik lingkaran di sebelah kanan properti Materi Sekunder .
- Temukan dan pilih bahan Oklusi dan tutup jendela.
Selanjutnya, kita akan menambahkan perilaku khusus ke Bumi, sehingga memiliki sorotan biru setiap kali terhimpun oleh hologram lain (seperti matahari), atau oleh jala pemetaan spasial:
- Di panel Proyek , di folder Hologram , perluas objek SolarSystem .
- Klik bumi.
- Di panel Inspektur , temukan bahan Bumi (komponen bawah).
- Di menu drop-down Shader, ubah shader menjadi Custom > OcclusionRim. Ini akan merender sorotan biru di sekitar Bumi setiap kali dihilangkan oleh objek lain.
Akhirnya, kita akan mengaktifkan efek visi sinar X untuk planet di tata surya kita. Kita perlu mengedit PlanetOcclusion.cs (ditemukan di folder Scripts\SolarSystem) untuk mencapai hal-hal berikut:
- Tentukan apakah planet dihilangkan oleh lapisan SpatialMapping (jala kamar dan bidang).
- Tunjukkan representasi wireframe planet setiap kali dihilangkan oleh lapisan SpatialMapping.
- Sembunyikan representasi wireframe planet ketika tidak diblokir oleh lapisan SpatialMapping.
Ikuti latihan pengkodian di PlanetOcclusion.cs, atau gunakan solusi berikut:
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);
}
}
}
}
Membangun dan Menyebarkan
- Bangun dan sebarkan aplikasi ke HoloLens, seperti biasa.
- Tunggu hingga pemindaian dan pemrosesan data pemetaan spasial selesai (Anda akan melihat garis biru muncul di dinding).
- Temukan dan pilih kotak proyeksi tata surya lalu atur kotak di samping dinding atau di belakang penghitung.
- Anda dapat melihat oklusi dasar dengan bersembunyi di belakang permukaan untuk serekan di poster atau kotak proyeksi.
- Cari bumi, harus ada efek sorotan biru setiap kali ia pergi di belakang hologram atau permukaan lain.
- Perhatikan saat planet bergerak di belakang dinding atau permukaan lain di ruangan. Anda sekarang memiliki visi sinar-x dan dapat melihat kerangka wireframe mereka!
Akhir
Selamat! Anda sekarang telah menyelesaikan MR Spasial 230: Pemetaan spasial.
- Anda tahu cara memindai lingkungan Anda dan memuat data pemetaan spasial ke Unity.
- Anda memahami dasar-dasar shader dan bagaimana bahan dapat digunakan untuk memvisualisasikan kembali dunia.
- Anda belajar teknik pemrosesan baru untuk menemukan bidang dan menghapus segitiga dari jala.
- Anda dapat memindahkan dan menempatkan hologram pada permukaan yang masuk akal.
- Anda mengalami teknik oklusi yang berbeda dan memanfaatkan kekuatan visi sinar-x!