Bagikan melalui


Tutorial: Menyempurnakan bahan, pencahayaan, dan efek

Dalam tutorial ini, Anda mempelajari cara:

  • Menyoroti dan menguraikan model dan komponen model
  • Menerapkan bahan yang berbeda ke model
  • Mengiris model dengan bidang potong
  • Menambahkan animasi sederhana untuk objek yang dirender dari jarak jauh

Prasyarat

Menyoroti dan menambah garis

Memberikan umpan balik visual kepada pengguna adalah bagian penting dari pengalaman pengguna dalam aplikasi apa pun. Azure Remote Rendering menyediakan mekanisme umpan balik visual melalui Penimpaan status Hierarkis. Penimpaan status hierarkis diimplementasikan dengan komponen yang melekat pada instans model lokal. Kami belajar cara membuat instans lokal ini dalam Menyinkronkan grafik objek jarak jauh ke dalam hierarki Unity.

Pertama, kita akan membuat pembungkus di sekitar komponen HierarchicalStateOverrideComponent. HierarchicalStateOverrideComponent adalah skrip lokal yang mengontrol penimpaan pada entitas jarak jauh. Aset Tutorial mencakup kelas dasar abstrak yang disebut BaseEntityOverrideController, yang akan kami perluas untuk membuat pembungkus.

  1. Buat skrip baru bernama EntityOverrideController dan ganti kontennya dengan kode berikut:

    // Copyright (c) Microsoft Corporation. All rights reserved.
    // Licensed under the MIT License. See LICENSE in the project root for license information.
    
    using Microsoft.Azure.RemoteRendering;
    using Microsoft.Azure.RemoteRendering.Unity;
    using System;
    using UnityEngine;
    
    public class EntityOverrideController : BaseEntityOverrideController
    {
        public override event Action<HierarchicalStates> FeatureOverrideChange;
    
        private ARRHierarchicalStateOverrideComponent localOverride;
        public override ARRHierarchicalStateOverrideComponent LocalOverride
        {
            get
            {
                if (localOverride == null)
                {
                    localOverride = gameObject.GetComponent<ARRHierarchicalStateOverrideComponent>();
                    if (localOverride == null)
                    {
                        localOverride = gameObject.AddComponent<ARRHierarchicalStateOverrideComponent>();
                    }
    
                    var remoteStateOverride = TargetEntity.Entity.FindComponentOfType<HierarchicalStateOverrideComponent>();
    
                    if (remoteStateOverride == null)
                    {
                        // if there is no HierarchicalStateOverrideComponent on the remote side yet, create one
                        localOverride.Create(RemoteManagerUnity.CurrentSession);
                    }
                    else
                    {
                        // otherwise, bind our local stateOverride component to the remote component
                        localOverride.Bind(remoteStateOverride);
    
                    }
                }
                return localOverride;
            }
        }
    
        private RemoteEntitySyncObject targetEntity;
        public override RemoteEntitySyncObject TargetEntity
        {
            get
            {
                if (targetEntity == null)
                    targetEntity = gameObject.GetComponent<RemoteEntitySyncObject>();
                return targetEntity;
            }
        }
    
        private HierarchicalEnableState ToggleState(HierarchicalStates feature)
        {
            HierarchicalEnableState setToState = HierarchicalEnableState.InheritFromParent;
            switch (LocalOverride.RemoteComponent.GetState(feature))
            {
                case HierarchicalEnableState.ForceOff:
                case HierarchicalEnableState.InheritFromParent:
                    setToState = HierarchicalEnableState.ForceOn;
                    break;
                case HierarchicalEnableState.ForceOn:
                    setToState = HierarchicalEnableState.InheritFromParent;
                    break;
            }
    
            return SetState(feature, setToState);
        }
    
        private HierarchicalEnableState SetState(HierarchicalStates feature, HierarchicalEnableState enableState)
        {
            if (GetState(feature) != enableState) //if this is actually different from the current state, act on it
            {
                LocalOverride.RemoteComponent.SetState(feature, enableState);
                FeatureOverrideChange?.Invoke(feature);
            }
    
            return enableState;
        }
    
        public override HierarchicalEnableState GetState(HierarchicalStates feature) => LocalOverride.RemoteComponent.GetState(feature);
    
        public override void ToggleHidden() => ToggleState(HierarchicalStates.Hidden);
    
        public override void ToggleSelect() => ToggleState(HierarchicalStates.Selected);
    
        public override void ToggleSeeThrough() => ToggleState(HierarchicalStates.SeeThrough);
    
        public override void ToggleTint(Color tintColor = default)
        {
            if (tintColor != default) LocalOverride.RemoteComponent.TintColor = tintColor.toRemote();
            ToggleState(HierarchicalStates.UseTintColor);
        }
    
        public override void ToggleDisabledCollision() => ToggleState(HierarchicalStates.DisableCollision);
    
        public override void RemoveOverride()
        {
            var remoteStateOverride = TargetEntity.Entity.FindComponentOfType<HierarchicalStateOverrideComponent>();
            if (remoteStateOverride != null)
            {
                remoteStateOverride.Destroy();
            }
    
            if (localOverride == null)
                localOverride = gameObject.GetComponent<ARRHierarchicalStateOverrideComponent>();
    
            if (localOverride != null)
            {
                Destroy(localOverride);
            }
        }
    }
    

Tugas utama LocalOverrideadalah menciptakan hubungan antara dirinya sendiri dan RemoteComponentnya. LocalOverride kemudian memungkinkan kita untuk menetapkan bendera status pada komponen lokal, yang terikat ke entitas jarak jauh. Penimpaan dan statusnya dijelaskan di halaman Penimpaan status hierarkis.

Implementasi ini hanya mengaktifkan satu status dalam satu waktu. Namun, sangat mungkin untuk menggabungkan beberapa penimpaan pada satu entitas dan membuat kombinasi pada tingkat yang berbeda dalam hierarki. Misalnya, menggabungkan Selected dan SeeThrough pada satu komponen akan memberikan garis besar sekaligus membuatnya transparan. Atau, mengatur penimpaan Hidden entitas akar ke ForceOn saat membuat penimpaan entitas anak Hidden ke ForceOff untuk menyembunyikan semuanya kecuali untuk anak dengan penimpaan.

Untuk menerapkan status ke entitas, kita dapat memodifikasi RemoteEntityHelper yang dibuat sebelumnya.

  1. Ubah kelas RemoteEntityHelper untuk mengimplementasikan kelas abstrak BaseRemoteEntityHelper. Modifikasi ini akan memungkinkan penggunaan kontroler tampilan yang disediakan dalam Aset Tutorial. Ini akan terlihat seperti ini ketika dimodifikasi:

    public class RemoteEntityHelper : BaseRemoteEntityHelper
    
  2. Menimpa metode abstrak menggunakan kode berikut:

    public override BaseEntityOverrideController EnsureOverrideComponent(Entity entity)
    {
        var entityGameObject = entity.GetOrCreateGameObject(UnityCreationMode.DoNotCreateUnityComponents);
        var overrideComponent = entityGameObject.GetComponent<EntityOverrideController>();
        if (overrideComponent == null)
            overrideComponent = entityGameObject.AddComponent<EntityOverrideController>();
        return overrideComponent;
    }
    
    public override HierarchicalEnableState GetState(Entity entity, HierarchicalStates feature)
    {
        var overrideComponent = EnsureOverrideComponent(entity);
        return overrideComponent.GetState(feature);
    }
    
    public override void ToggleHidden(Entity entity)
    {
        var overrideComponent = EnsureOverrideComponent(entity);
        overrideComponent.ToggleHidden();
    }
    
    public override void ToggleSelect(Entity entity)
    {
        var overrideComponent = EnsureOverrideComponent(entity);
        overrideComponent.ToggleSelect();
    }
    
    public override void ToggleSeeThrough(Entity entity)
    {
        var overrideComponent = EnsureOverrideComponent(entity);
        overrideComponent.ToggleSeeThrough();
    }
    
    public Color TintColor = new Color(0.0f, 1.0f, 0.0f, 0.1f);
    public override void ToggleTint(Entity entity)
    {
        var overrideComponent = EnsureOverrideComponent(entity);
        overrideComponent.ToggleTint(TintColor);
    }
    
    public override void ToggleDisableCollision(Entity entity)
    {
        var overrideComponent = EnsureOverrideComponent(entity);
        overrideComponent.ToggleHidden();
    }
    
    public override void RemoveOverrides(Entity entity)
    {
        var entityGameObject = entity.GetOrCreateGameObject(UnityCreationMode.DoNotCreateUnityComponents);
        var overrideComponent = entityGameObject.GetComponent<EntityOverrideController>();
        if (overrideComponent != null)
        {
            overrideComponent.RemoveOverride();
            Destroy(overrideComponent);
        }
    }
    

Kode ini memastikan komponen EntityOverrideController ditambahkan ke Entitas target, kemudian memanggil salah satu metode bolak-balik. Jika diinginkan, pada TestModel GameObject, memanggil metode pembantu ini dapat dilakukan dengan menambahkan RemoteEntityHelper sebagai panggilan balik ke acara OnRemoteEntityClicked pada komponen RemoteRayCastPointerHandler.

Panggilan balik pointer

Karena skrip ini telah ditambahkan ke model, setelah terhubung ke runtime, pengontrol tampilan AppMenu seharusnya memiliki antarmuka tambahan yang diaktifkan untuk berinteraksi dengan skrip EntityOverrideController. Lihat menu Alat Model untuk melihat pengontrol tampilan yang tidak terkunci.

Pada titik ini, komponen TestModel GameObject Anda akan terlihat seperti ini:

Model Uji dengan skrip tambahan

Berikut adalah contoh penimpaan tumpukan pada satu entitas. Kami menggunakan Select dan Tint untuk memberikan garis besar dan pewarnaan:

Pilih warna Model Uji

Bidang potong

Bidang potong adalah fitur yang dapat ditambahkan ke entitas jarak jauh apa pun. Anda biasanya membuat entitas jarak jauh baru yang tidak terkait dengan data jala apa pun untuk menahan komponen bidang potong. Posisi dan orientasi bidang potong ditentukan oleh posisi dan orientasi entitas jarak jauh yang melekat padanya.

Kami akan membuat skrip yang secara otomatis membuat entitas jarak jauh, menambahkan komponen bidang potong, dan menyinkronkan transformasi objek lokal dengan entitas bidang potong. Kemudian, kita dapat menggunakan CutPlaneViewController untuk membungkus bidang potong dalam antarmuka yang dapat dimanipulasi.

  1. Buat skrip baru bernama RemoteCutPlane dan ganti kodenya dengan kode di bawah ini:

    // Copyright (c) Microsoft Corporation. All rights reserved.
    // Licensed under the MIT License. See LICENSE in the project root for license information.
    
    using Microsoft.Azure.RemoteRendering;
    using Microsoft.Azure.RemoteRendering.Unity;
    using System;
    using UnityEngine;
    
    public class RemoteCutPlane : BaseRemoteCutPlane
    {
        public Color SliceColor = new Color(0.5f, 0f, 0f, .5f);
        public float FadeLength = 0.01f;
        public Axis SliceNormal = Axis.NegativeY;
    
        public bool AutomaticallyCreate = true;
    
        private CutPlaneComponent remoteCutPlaneComponent;
        private bool cutPlaneReady = false;
    
        public override bool CutPlaneReady 
        { 
            get => cutPlaneReady;
            set 
            { 
                cutPlaneReady = value;
                CutPlaneReadyChanged?.Invoke(cutPlaneReady);
            }
        }
    
        public override event Action<bool> CutPlaneReadyChanged;
    
        public UnityBoolEvent OnCutPlaneReadyChanged = new UnityBoolEvent();
    
        public void Start()
        {
            // Hook up the event to the Unity event
            CutPlaneReadyChanged += (ready) => OnCutPlaneReadyChanged?.Invoke(ready);
    
            RemoteRenderingCoordinator.CoordinatorStateChange += RemoteRenderingCoordinator_CoordinatorStateChange;
            RemoteRenderingCoordinator_CoordinatorStateChange(RemoteRenderingCoordinator.instance.CurrentCoordinatorState);
        }
    
        private void RemoteRenderingCoordinator_CoordinatorStateChange(RemoteRenderingCoordinator.RemoteRenderingState state)
        {
            switch (state)
            {
                case RemoteRenderingCoordinator.RemoteRenderingState.RuntimeConnected:
                    if (AutomaticallyCreate)
                        CreateCutPlane();
                    break;
                default:
                    DestroyCutPlane();
                    break;
            }
        }
    
        public override void CreateCutPlane()
        {
            //Implement me
        }
    
        public override void DestroyCutPlane()
        {
            //Implement me
        }
    }
    

    Kode ini memperluas kelas BaseRemoteCutPlane yang termasuk dalam Aset Tutorial. Demikian pula dengan model yang dirender dari jarak jauh, skrip ini melampirkan dan mendengarkan perubahan RemoteRenderingState dari koordinator jarak jauh. Ketika koordinator mencapai status RuntimeConnected, koordinator tersebut akan mencoba untuk secara otomatis tersambung, jika diperlukan. Ada juga variabel CutPlaneComponent yang akan kita lacak. Ini adalah komponen Azure Remote Rendering yang disinkronkan dengan bidang potong di sesi jarak jauh. Mari kita lihat apa yang perlu kita lakukan untuk membuat bidang potong.

  2. Ganti metode CreateCutPlane() dengan versi lengkap di bawah ini:

    public override void CreateCutPlane()
    {
        if (remoteCutPlaneComponent != null)
            return; //Nothing to do!
    
        //Create a root object for the cut plane
        var cutEntity = RemoteRenderingCoordinator.CurrentSession.Connection.CreateEntity();
    
        //Bind the remote entity to this game object
        cutEntity.BindToUnityGameObject(this.gameObject);
    
        //Sync the transform of this object so we can move the cut plane
        var syncComponent = this.gameObject.GetComponent<RemoteEntitySyncObject>();
        syncComponent.SyncEveryFrame = true;
    
        //Add a cut plane to the entity
        remoteCutPlaneComponent = RemoteRenderingCoordinator.CurrentSession.Connection.CreateComponent(ObjectType.CutPlaneComponent, cutEntity) as CutPlaneComponent;
    
        //Configure the cut plane
        remoteCutPlaneComponent.Normal = SliceNormal;
        remoteCutPlaneComponent.FadeColor = SliceColor.toRemote();
        remoteCutPlaneComponent.FadeLength = FadeLength;
        CutPlaneReady = true;
    }
    

    Di sini kita membuat entitas jarak jauh dan mengikatnya ke GameObject lokal. Kami memastikan bahwa entitas jarak jauh akan memiliki transformasi yang disinkronkan ke transformasi lokal dengan mengatur SyncEveryFrame ke true. Kemudian, kami menggunakan panggilan CreateComponent untuk menambahkan CutPlaneComponent ke objek jarak jauh. Akhirnya, kami mengonfigurasi bidang potong dengan pengaturan yang ditentukan di bagian atas MonoBehaviour. Mari kita lihat apa yang diperlukan untuk membersihkan bidang potong dengan menerapkan metode DestroyCutPlane().

  3. Ganti metode DestroyCutPlane() dengan versi lengkap di bawah ini:

    public override void DestroyCutPlane()
    {
        if (remoteCutPlaneComponent == null)
            return; //Nothing to do!
    
        remoteCutPlaneComponent.Owner.Destroy();
        remoteCutPlaneComponent = null;
        CutPlaneReady = false;
    }
    

Karena objek jarak jauh cukup sederhana dan kami hanya membersihkan ujung terpencil (dan menjaga objek lokal kami), mudah untuk hanya memanggil Destroy pada objek jarak jauh dan membersihkan referensi kami untuk objek tersebut.

AppMenu menyertakan pengontrol tampilan yang akan secara otomatis terpasang ke bidang potong Anda dan memungkinkan Anda untuk berinteraksi dengannya. Anda tidak diharuskan menggunakan AppMenu atau pengontrol tampilan apa pun, tetapi fitur ini memberikan pengalaman yang lebih baik. Sekarang, uji bidang potong dan pengontrol tampilannya.

  1. Buat GameObject kosong baru di adegan dan beri nama CutPlane.

  2. Tambahkan komponen RemoteCutPlane ke CutPlane GameObject.

    Konfigurasi komponen Bidang Potong

  3. Tekan Putar di Editor Unity untuk memuat dan menyambungkan ke sesi jarak jauh.

  4. Dengan simulasi tangan MRTK, ambil dan putar (tahan Ctrl untuk memutar) CutPlane untuk memindahkannya di sekitar adegan. Tonton bidang dipotong ke dalam TestModel untuk mengungkapkan komponen internal.

Contoh Bidang Potong

Mengonfigurasi pencahayaan jarak jauh

Sesi penyajian jarak jauh mendukung spektrum penuh opsi pencahayaan. Kami akan membuat skrip untuk Tekstur Langit dan peta sederhana untuk dua jenis cahaya Unity untuk digunakan dengan penyajian jarak jauh.

Tekstur Langit

Ada sejumlah Cubemap bawaan yang bisa dipilih saat mengubah tekstur langit. Ini dimuat ke dalam sesi dan diterapkan pada tekstur langit. Anda juga dapat memuat di tekstur Anda sendiri untuk digunakan sebagai cahaya langit.

Kami akan membuat skrip RemoteSky yang memiliki daftar Cubemap bawaan yang tersedia dalam bentuk parameter beban. Kemudian, kami akan mengizinkan pengguna untuk memilih dan memuat salah satu opsi.

  1. Buat skrip baru bernama RemoteSky dan ganti seluruh isinya dengan kode di bawah ini:

    // Copyright (c) Microsoft Corporation. All rights reserved.
    // Licensed under the MIT License. See LICENSE in the project root for license information.
    
    using Microsoft.Azure.RemoteRendering;
    using System;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class RemoteSky : BaseRemoteSky
    {
        public override Dictionary<string, LoadTextureFromSasOptions> AvailableCubemaps => builtInTextures;
    
        private bool canSetSky;
        public override bool CanSetSky
        {
            get => canSetSky;
            set
            {
                canSetSky = value;
                CanSetSkyChanged?.Invoke(canSetSky);
            }
        }
    
        private string currentSky = "DefaultSky";
        public override string CurrentSky
        {
            get => currentSky;
            protected set
            {
                currentSky = value;
                SkyChanged?.Invoke(value);
            }
        }
    
        private Dictionary<string, LoadTextureFromSasOptions> builtInTextures = new Dictionary<string, LoadTextureFromSasOptions>()
        {
            {"Autoshop",new LoadTextureFromSasOptions("builtin://Autoshop", TextureType.CubeMap)},
            {"BoilerRoom",new LoadTextureFromSasOptions("builtin://BoilerRoom", TextureType.CubeMap)},
            {"ColorfulStudio",new LoadTextureFromSasOptions("builtin://ColorfulStudio", TextureType.CubeMap)},
            {"Hangar",new LoadTextureFromSasOptions("builtin://Hangar", TextureType.CubeMap)},
            {"IndustrialPipeAndValve",new LoadTextureFromSasOptions("builtin://IndustrialPipeAndValve", TextureType.CubeMap)},
            {"Lebombo",new LoadTextureFromSasOptions("builtin://Lebombo", TextureType.CubeMap)},
            {"SataraNight",new LoadTextureFromSasOptions("builtin://SataraNight", TextureType.CubeMap)},
            {"SunnyVondelpark",new LoadTextureFromSasOptions("builtin://SunnyVondelpark", TextureType.CubeMap)},
            {"Syferfontein",new LoadTextureFromSasOptions("builtin://Syferfontein", TextureType.CubeMap)},
            {"TearsOfSteelBridge",new LoadTextureFromSasOptions("builtin://TearsOfSteelBridge", TextureType.CubeMap)},
            {"VeniceSunset",new LoadTextureFromSasOptions("builtin://VeniceSunset", TextureType.CubeMap)},
            {"WhippleCreekRegionalPark",new LoadTextureFromSasOptions("builtin://WhippleCreekRegionalPark", TextureType.CubeMap)},
            {"WinterRiver",new LoadTextureFromSasOptions("builtin://WinterRiver", TextureType.CubeMap)},
            {"DefaultSky",new LoadTextureFromSasOptions("builtin://DefaultSky", TextureType.CubeMap)}
        };
    
        public UnityBoolEvent OnCanSetSkyChanged;
        public override event Action<bool> CanSetSkyChanged;
    
        public UnityStringEvent OnSkyChanged;
        public override event Action<string> SkyChanged;
    
        public void Start()
        {
            // Hook up the event to the Unity event
            CanSetSkyChanged += (canSet) => OnCanSetSkyChanged?.Invoke(canSet);
            SkyChanged += (key) => OnSkyChanged?.Invoke(key);
    
            RemoteRenderingCoordinator.CoordinatorStateChange += ApplyStateToView;
            ApplyStateToView(RemoteRenderingCoordinator.instance.CurrentCoordinatorState);
        }
    
        private void ApplyStateToView(RemoteRenderingCoordinator.RemoteRenderingState state)
        {
            switch (state)
            {
                case RemoteRenderingCoordinator.RemoteRenderingState.RuntimeConnected:
                    CanSetSky = true;
                    break;
                default:
                    CanSetSky = false;
                    break;
            }
        }
    
        public override async void SetSky(string skyKey)
        {
            if (!CanSetSky)
            {
                Debug.Log("Unable to set sky right now");
                return;
            }
    
            if (AvailableCubemaps.ContainsKey(skyKey))
            {
                Debug.Log("Setting sky to " + skyKey);
                //Load the texture into the session
                var texture = await RemoteRenderingCoordinator.CurrentSession.Connection.LoadTextureFromSasAsync(AvailableCubemaps[skyKey]);
    
                //Apply the texture to the SkyReflectionSettings
                RemoteRenderingCoordinator.CurrentSession.Connection.SkyReflectionSettings.SkyReflectionTexture = texture;
                SkyChanged?.Invoke(skyKey);
            }
            else
            {
                Debug.Log("Invalid sky key");
            }
        }
    }
    

    Bagian terpenting dari kode ini hanyalah beberapa baris:

    //Load the texture into the session
    var texture = await RemoteRenderingCoordinator.CurrentSession.Connection.LoadTextureFromSasAsync(AvailableCubemaps[skyKey]);
    
    //Apply the texture to the SkyReflectionSettings
    RemoteRenderingCoordinator.CurrentSession.Connection.SkyReflectionSettings.SkyReflectionTexture = texture;
    

    Di sini, kita mendapatkan referensi tekstur untuk digunakan dengan memuatnya ke dalam sesi dari penyimpanan blob bawaan. Kemudian, kita hanya perlu menetapkan tekstur itu ke SkyReflectionTexture sesi untuk menerapkannya.

  2. Buat GameObject kosong di adegan Anda dan beri nama SkyLight.

  3. Tambahkan skrip RemoteSky ke SkyLight GameObject Anda.

    Beralih di antara cahaya langit dapat dilakukan dengan memanggil SetSky dengan salah satu tombol string yang ditentukan dalam AvailableCubemaps . Pengontrol tampilan yang disertakan dalam AppMenu secara otomatis membuat tombol dan menghubungkan peristiwa mereka untuk dipanggil SetSky dengan kunci masing-masing.

  4. Tekan Putar di Editor Unity dan otorisasi koneksi.

  5. Setelah menyambungkan runtime lokal ke sesi jarak jauh, navigasikan AppMenu -> Session Tools -> Remote Sky untuk menjelajahi berbagai opsi langit dan melihat bagaimana pengaruhnya terhadap TestModel.

Pencahayaan Adegan

Pencahayaan adegann jarak jauh meliputi: titik, spot, dan arah. Mirip dengan Bidang Potong yang kami buat di atas, pencahayaan adegan ini adalah entitas jarak jauh dengan komponen yang melekat padanya. Pertimbangan penting saat menerangi adegan jarak jauh Anda adalah mencoba mencocokkan pencahayaan di adegan lokal Anda. Strategi ini tidak selalu bisa dilakukan karena banyak aplikasi Unity untuk HoloLens 2 tidak menggunakan penyajian berbasis fisik untuk objek yang dirender secara lokal. Namun, pada tingkat tertentu, kita dapat mensimulasikan pencahayaan default Unity yang lebih sederhana.

  1. Buat skrip baru bernama RemoteLight dan ganti kodenya dengan kode di bawah ini:

    // Copyright (c) Microsoft Corporation. All rights reserved.
    // Licensed under the MIT License. See LICENSE in the project root for license information.
    
    using Microsoft.Azure.RemoteRendering;
    using Microsoft.Azure.RemoteRendering.Unity;
    using System;
    using UnityEngine;
    
    [RequireComponent(typeof(Light))]
    public class RemoteLight : BaseRemoteLight
    {
        public bool AutomaticallyCreate = true;
    
        private bool lightReady = false;
        public override bool LightReady 
        {
            get => lightReady;
            set
            {
                lightReady = value;
                LightReadyChanged?.Invoke(lightReady);
            }
        }
    
        private ObjectType remoteLightType = ObjectType.Invalid;
        public override ObjectType RemoteLightType => remoteLightType;
    
        public UnityBoolEvent OnLightReadyChanged;
    
        public override event Action<bool> LightReadyChanged;
    
        private Light localLight; //Unity Light
    
        private Entity lightEntity;
        private LightComponentBase remoteLightComponent; //Remote Rendering Light
    
        private void Awake()
        {
            localLight = GetComponent<Light>();
            switch (localLight.type)
            {
                case LightType.Directional:
                    remoteLightType = ObjectType.DirectionalLightComponent;
                    break;
                case LightType.Point:
                    remoteLightType = ObjectType.PointLightComponent;
                    break;
                case LightType.Spot:
                case LightType.Area:
                    //Not supported in tutorial
                case LightType.Disc:
                    // No direct analog in remote rendering
                    remoteLightType = ObjectType.Invalid;
                    break;
            }
        }
    
        public void Start()
        {
            // Hook up the event to the Unity event
            LightReadyChanged += (ready) => OnLightReadyChanged?.Invoke(ready);
    
            RemoteRenderingCoordinator.CoordinatorStateChange += RemoteRenderingCoordinator_CoordinatorStateChange;
            RemoteRenderingCoordinator_CoordinatorStateChange(RemoteRenderingCoordinator.instance.CurrentCoordinatorState);
        }
    
        public void OnDestroy()
        {
            lightEntity?.Destroy();
        }
    
        private void RemoteRenderingCoordinator_CoordinatorStateChange(RemoteRenderingCoordinator.RemoteRenderingState state)
        {
            switch (state)
            {
                case RemoteRenderingCoordinator.RemoteRenderingState.RuntimeConnected:
                    if (AutomaticallyCreate)
                        CreateLight();
                    break;
                default:
                    DestroyLight();
                    break;
            }
        }
    
        public override void CreateLight()
        {
            if (remoteLightComponent != null)
                return; //Nothing to do!
    
            //Create a root object for the light
            if(lightEntity == null)
                lightEntity = RemoteRenderingCoordinator.CurrentSession.Connection.CreateEntity();
    
            //Bind the remote entity to this game object
            lightEntity.BindToUnityGameObject(this.gameObject);
    
            //Sync the transform of this object so we can move the light
            var syncComponent = this.gameObject.GetComponent<RemoteEntitySyncObject>();
            syncComponent.SyncEveryFrame = true;
    
            //Add a light to the entity
            switch (RemoteLightType)
            {
                case ObjectType.DirectionalLightComponent:
                    var remoteDirectional = RemoteRenderingCoordinator.CurrentSession.Connection.CreateComponent(ObjectType.DirectionalLightComponent, lightEntity) as DirectionalLightComponent;
                    //No additional properties
                    remoteLightComponent = remoteDirectional;
                    break;
    
                case ObjectType.PointLightComponent:
                    var remotePoint = RemoteRenderingCoordinator.CurrentSession.Connection.CreateComponent(ObjectType.PointLightComponent, lightEntity) as PointLightComponent;
                    remotePoint.Radius = 0;
                    remotePoint.Length = localLight.range;
                    //remotePoint.AttenuationCutoff = //No direct analog in Unity legacy lights
                    //remotePoint.ProjectedCubeMap = //No direct analog in Unity legacy lights
    
                    remoteLightComponent = remotePoint;
                    break;
                default:
                    LightReady = false;
                    return;
            }
    
            // Set the common values for all light types
            UpdateRemoteLightSettings();
    
            LightReady = true;
        }
    
        public override void UpdateRemoteLightSettings()
        {
            remoteLightComponent.Color = localLight.color.toRemote();
            remoteLightComponent.Intensity = localLight.intensity;
        }
    
        public override void DestroyLight()
        {
            if (remoteLightComponent == null)
                return; //Nothing to do!
    
            remoteLightComponent.Destroy();
            remoteLightComponent = null;
            LightReady = false;
        }
    
        [ContextMenu("Sync Remote Light Configuration")]
        public override void RecreateLight()
        {
            DestroyLight();
            CreateLight();
        }
    
        public override void SetIntensity(float intensity)
        {
            localLight.intensity = Mathf.Clamp(intensity, 0, 1);
            UpdateRemoteLightSettings();
        }
    
        public override void SetColor(Color color)
        {
            localLight.color = color;
            UpdateRemoteLightSettings();
        }
    }
    

    Skrip ini membuat berbagai jenis pencahayaan jarak jauh tergantung pada jenis lampu Unity lokal yang melekat pada skrip. Lampu jarak jauh akan menduplikasi lampu lokal dalam: posisi, rotasi, warna, dan intensitas. Jika memungkinkan, lampu jarak jauh juga akan mengatur konfigurasi tambahan. Ini bukan pasangan yang cocok karena lampu Unity bukanlah lampu PBR.

  2. Temukan DirectionalLight GameObject di adegan Anda. Jika Anda telah menghapus DirectionalLight default dari adegan Anda: dari bilah menu atas pilih GameObject -> Light -> DirectionalLight untuk membuat lampu baru di adegan Anda.

  3. Pilih DirectionalLight GameObject dan dengan tombol Tambah Komponen, tambahkan skrip RemoteLight.

  4. Karena skrip ini mengimplementasikan kelas dasar BaseRemoteLight, Anda dapat menggunakan pengontrol tampilan AppMenu yang disediakan untuk berinteraksi dengan lampu jarak jauh. Navigasi ke AppMenu -> Alat Sesi -> Cahaya Arah.

    Catatan

    UI di AppMenu telah dibatasi pada satu lampu arah untuk kesederhanaan. Namun, Anda mungkin bisa dan sebaiknya menambahkan lampu titik dan memasang skrip RemoteLight pada lampu tersebut. Lampu tambahan tersebut dapat dimodifikasi dengan mengedit sifat-sifat lampu Unity di editor. Anda perlu menyinkronkan perubahan lokal secara manual ke lampu jarak jauh menggunakan menu konteks RemoteLight di inspektur:

    Sinkronisasi manual lampu jarak jauh

  5. Tekan Putar di Editor Unity dan otorisasi koneksi.

  6. Setelah menyambungkan runtime Anda ke sesi jarak jauh, posisikan dan arahkan kamera Anda (gunakan WASD dan klik kanan + gerakan mouse) untuk melihat pengontrol tampilan lampu arah.

  7. Gunakan pengontrol tampilan cahaya jarak jauh untuk memodifikasi properti lampu. Dengan simulasi tangan MRTK, ambil dan putar (tahan Ctrl untuk memutar) lampu arah untuk melihat efek pada pencahayaan adegan.

    Lampu arah

Mengedit bahan

Bahan yang dirender dari jarak jauh dapat dimodifikasi untuk memberikan efek visual tambahan, menyempurnakan visual model yang dirender, atau memberikan umpan balik tambahan kepada pengguna. Ada banyak cara dan alasan untuk memodifikasi materi. Di sini, kami akan menunjukkan kepada Anda cara mengubah warna albedo material dan mengubah kekasaran dan logam bahan PBR.

Catatan

Dalam banyak kasus, jika fitur atau efek dapat diimplementasikan menggunakan HierarchicalStateOverrideComponent, sangat ideal untuk menggunakannya, alih-alih memodifikasi materi.

Kami akan membuat skrip yang menerima Entitas target dan mengonfigurasi beberapa objek OverrideMaterialProperty untuk mengubah properti materi Entitas target. Kita mulai dengan mendapatkan target MeshComponent Entitas yang berisi daftar bahan yang digunakan pada mesh. Untuk mempermudah, kami hanya akan menggunakan bahan pertama yang ditemukan. Strategi naif ini bisa gagal kapan saja tergantung pada bagaimana konten ditulis, jadi Anda mungkin ingin menggunakan pendekatan yang lebih kompleks untuk memilih bahan yang sesuai.

Dari bahan tersebut, kita dapat mengakses nilai-nilai bersama seperti albedo. Pertama, bahan perlu dilemparkan dalam jenis yang sesuai, PbrMaterial atau ColorMaterial, untuk mengambil nilainya, seperti yang terlihat dalam metode GetMaterialColor. Setelah kami memiliki referensi ke materi yang diinginkan, cukup atur nilainya, dan ARR akan menangani sinkronisasi antara properti bahan lokal dan bahan jarak jauh.

  1. Buat skrip baru bernama EntityOverrideController dan ganti kontennya dengan kode berikut:

    // Copyright (c) Microsoft Corporation. All rights reserved.
    // Licensed under the MIT License. See LICENSE in the project root for license information.
    
    using Microsoft.Azure.RemoteRendering;
    using Microsoft.Azure.RemoteRendering.Unity;
    using System;
    using System.Linq;
    using UnityEngine;
    // to prevent namespace conflicts
    using ARRMaterial = Microsoft.Azure.RemoteRendering.Material;
    
    public class EntityMaterialController : BaseEntityMaterialController
    {
        public override bool RevertOnEntityChange { get; set; } = true;
    
        public override OverrideMaterialProperty<Color> ColorOverride { get; set; }
        public override OverrideMaterialProperty<float> RoughnessOverride { get; set; }
        public override OverrideMaterialProperty<float> MetalnessOverride { get; set; }
    
        private Entity targetEntity;
        public override Entity TargetEntity
        {
            get => targetEntity;
            set
            {
                if (targetEntity != value)
                {
                    if (targetEntity != null && RevertOnEntityChange)
                    {
                        Revert();
                    }
    
                    targetEntity = value;
                    ConfigureTargetEntity();
                    TargetEntityChanged?.Invoke(value);
                }
            }
        }
    
        private ARRMaterial targetMaterial;
        private ARRMeshComponent meshComponent;
    
        public override event Action<Entity> TargetEntityChanged;
        public UnityRemoteEntityEvent OnTargetEntityChanged;
    
        public void Start()
        {
            // Forward events to Unity events
            TargetEntityChanged += (entity) => OnTargetEntityChanged?.Invoke(entity);
    
            // If there happens to be a remote RayCaster on this object, assume we should listen for events from it
            if (GetComponent<BaseRemoteRayCastPointerHandler>() != null)
                GetComponent<BaseRemoteRayCastPointerHandler>().RemoteEntityClicked += (entity) => TargetEntity = entity;
        }
    
        protected override void ConfigureTargetEntity()
        {
            //Get the Unity object, to get the sync object, to get the mesh component, to get the material.
            var targetEntityGameObject = TargetEntity.GetOrCreateGameObject(UnityCreationMode.DoNotCreateUnityComponents);
    
            var localSyncObject = targetEntityGameObject.GetComponent<RemoteEntitySyncObject>();
            meshComponent = targetEntityGameObject.GetComponent<ARRMeshComponent>();
            if (meshComponent == null)
            {
                var mesh = localSyncObject.Entity.FindComponentOfType<MeshComponent>();
                if (mesh != null)
                {
                    targetEntityGameObject.BindArrComponent<ARRMeshComponent>(mesh);
                    meshComponent = targetEntityGameObject.GetComponent<ARRMeshComponent>();
                }
            }
    
            meshComponent.enabled = true;
    
            targetMaterial = meshComponent.RemoteComponent.Mesh.Materials.FirstOrDefault();
            if (targetMaterial == default)
            {
                return;
            }
    
            ColorOverride = new OverrideMaterialProperty<Color>(
                GetMaterialColor(targetMaterial), //The original value
                targetMaterial, //The target material
                ApplyMaterialColor); //The action to take to apply the override
    
            //If the material is a PBR material, we can override some additional values
            if (targetMaterial.MaterialSubType == MaterialType.Pbr)
            {
                var firstPBRMaterial = (PbrMaterial)targetMaterial;
    
                RoughnessOverride = new OverrideMaterialProperty<float>(
                    firstPBRMaterial.Roughness, //The original value
                    targetMaterial, //The target material
                    ApplyRoughnessValue); //The action to take to apply the override
    
                MetalnessOverride = new OverrideMaterialProperty<float>(
                    firstPBRMaterial.Metalness, //The original value
                    targetMaterial, //The target material
                    ApplyMetalnessValue); //The action to take to apply the override
            }
            else //otherwise, ensure the overrides are cleared out from any previous entity
            {
                RoughnessOverride = null;
                MetalnessOverride = null;
            }
        }
    
        public override void Revert()
        {
            if (ColorOverride != null)
                ColorOverride.OverrideActive = false;
    
            if (RoughnessOverride != null)
                RoughnessOverride.OverrideActive = false;
    
            if (MetalnessOverride != null)
                MetalnessOverride.OverrideActive = false;
        }
    
        private Color GetMaterialColor(ARRMaterial material)
        {
            if (material == null)
                return default;
    
            if (material.MaterialSubType == MaterialType.Color)
                return ((ColorMaterial)material).AlbedoColor.toUnity();
            else
                return ((PbrMaterial)material).AlbedoColor.toUnity();
        }
    
        private void ApplyMaterialColor(ARRMaterial material, Color color)
        {
            if (material == null)
                return;
    
            if (material.MaterialSubType == MaterialType.Color)
                ((ColorMaterial)material).AlbedoColor = color.toRemoteColor4();
            else
                ((PbrMaterial)material).AlbedoColor = color.toRemoteColor4();
        }
    
        private void ApplyRoughnessValue(ARRMaterial material, float value)
        {
            if (material == null)
                return;
    
            if (material.MaterialSubType == MaterialType.Pbr) //Only PBR has Roughness
                ((PbrMaterial)material).Roughness = value;
        }
    
        private void ApplyMetalnessValue(ARRMaterial material, float value)
        {
            if (material == null)
                return;
    
            if (material.MaterialSubType == MaterialType.Pbr) //Only PBR has Metalness
                ((PbrMaterial)material).Metalness = value;
        }
    }
    

Jenis OverrideMaterialProperty harus cukup fleksibel agar beberapa nilai bahan lainnya dapat diubah, jika diinginkan. Jenis OverrideMaterialProperty melacak status penimpaan, mempertahankan nilai lama dan baru, serta menggunakan delegasi untuk mengatur penimpaan. Sebagai contoh, lihat ColorOverride:

ColorOverride = new OverrideMaterialProperty<Color>(
    GetMaterialColor(targetMaterial), //The original value
    targetMaterial, //The target material
    ApplyMaterialColor); //The action to take to apply the override

Ini membuat OverrideMaterialProperty baru di mana penimpaan akan membungkus jenis Color . Kami menyediakan warna saat ini atau asli saat melakukan penimpaan. Kami juga memberikan bahan ARR untuk bertindak. Akhirnya, delegasi disediakan yang akan menerapkan penimpaan. Delegasi adalah metode yang menerima bahan ARR dan jenis bungkus penimpaan. Metode ini adalah bagian terpenting dalam memahami bagaimana ARR menyesuaikan nilai bahan.

ColorOverride menggunakan metode ApplyMaterialColor untuk melakukan pekerjaannya:

private void ApplyMaterialColor(ARRMaterial material, Color color)
{
    if (material.MaterialSubType == MaterialType.Color)
        ((ColorMaterial)material).AlbedoColor = color.toRemoteColor4();
    else
        ((PbrMaterial)material).AlbedoColor = color.toRemoteColor4();
}

Kode ini menerima bahan dan warna. Kode tersebut memeriksa untuk melihat jenis bahan, kemudian melakukan transmisi bahan untuk menerapkan warna.

Cara kerja RoughnessOverride dan MetalnessOverride mirip - menggunakan metode ApplyRoughnessValue dan ApplyMetalnessValue untuk melakukan pekerjaan mereka.

Selanjutnya, mari kita uji pengontrol bahan.

  1. Tambahkan skrip EntityMaterialController ke TestModel GameObject Anda.
  2. Tekan Putar di Unity untuk memulai adegan dan menyambungkan ke ARR.
  3. Setelah menyambungkan runtime Anda ke sesi jarak jauh dan memuat model, navigasikan ke AppMenu -> Alat Model -> Edit Bahan
  4. Pilih Entitas dari model dengan menggunakan tangan yang disimulasikan untuk mengeklik TestModel.
  5. Konfirmasikan bahwa pengontrol tampilan material (AppMenu-Model> Tools-Edit> Material) telah diperbarui ke Entitas yang ditargetkan.
  6. Gunakan pengontrol tampilan bahan untuk menyesuaikan bahan pada Entitas yang ditargetkan.

Karena kami hanya memodifikasi bahan pertama dari mesh, Anda mungkin tidak melihat perubahan bahan. Gunakan penimpaan hierarki SeeThrough untuk melihat apakah materi yang Anda ubah ada di dalam mesh.

Contoh pengeditan bahan

Langkah berikutnya

Selamat! Anda sekarang telah menerapkan semua fungsionalitas inti Azure Remote Rendering. Di bab selanjutnya, Anda akan mempelajari tentang mengamankan Azure Remote Rendering dan penyimpanan Blob. Ini mencakup langkah-langkah pertama untuk merilis aplikasi komersial yang menggunakan Azure Remote Rendering.