Samouczek: udoskonalanie materiałów, oświetlenia i efektów

Ten samouczek zawiera informacje na temat wykonywania następujących czynności:

  • Wyróżnianie i konspektowanie modeli i składników modelu
  • Stosowanie różnych materiałów do modeli
  • Fragmentowanie modeli z płaszczyznami wycięcia
  • Dodawanie prostych animacji dla zdalnie renderowanych obiektów

Wymagania wstępne

Wyróżnianie i zwijanie

Przekazywanie wizualnej opinii użytkownikowi jest ważną częścią środowiska użytkownika w dowolnej aplikacji. Usługa Azure Remote Rendering udostępnia wizualne mechanizmy przesyłania opinii za pośrednictwem przesłonięć stanu hierarchicznego. Przesłonięcia stanu hierarchicznego są implementowane ze składnikami dołączonymi do lokalnych wystąpień modeli. Dowiedzieliśmy się, jak utworzyć te wystąpienia lokalne podczas synchronizowania zdalnego grafu obiektów w hierarchii aparatu Unity.

Najpierw utworzymy otokę wokół składnika HierarchicalStateOverrideComponent . HierarchicalStateOverrideComponent jest skryptem lokalnym, który kontroluje przesłonięcia w jednostce zdalnej. Zasoby samouczka zawierają abstrakcyjną klasę bazową o nazwie BaseEntityOverrideController, którą rozszerzymy w celu utworzenia otoki.

  1. Utwórz nowy skrypt o nazwie EntityOverrideController i zastąp jego zawartość następującym kodem:

    // 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);
            }
        }
    }
    

Głównym zadaniem localOverride jest utworzenie połączenia między samym sobą a jego RemoteComponent. Następnie localOverride umożliwia ustawienie flag stanu w składniku lokalnym, które są powiązane z jednostką zdalną. Przesłonięcia i ich stany są opisane na stronie Przesłonięcia stanu hierarchicznego .

Ta implementacja przełącza jeden stan naraz. Można jednak całkowicie połączyć wiele przesłonięć w pojedynczych jednostkach i utworzyć kombinacje na różnych poziomach w hierarchii. Na przykład połączenie Selected i SeeThrough na jednym składniku dałoby mu konspekt, jednocześnie czyniąc go przezroczystym. Można też ustawić ForceOn zastąpienie jednostki Hidden głównej na wartość podczas zastępowania jednostki Hidden podrzędnej, aby ForceOff ukryć wszystkie elementy z wyjątkiem elementu podrzędnego z przesłonięciami.

Aby zastosować stany do jednostek, możemy zmodyfikować utworzony wcześniej element RemoteEntityHelper .

  1. Zmodyfikuj klasę RemoteEntityHelper , aby zaimplementować klasę abstrakcyjną BaseRemoteEntityHelper . Ta modyfikacja umożliwi korzystanie z kontrolera widoku udostępnionego w temacie Zasoby samouczka. Po zmodyfikowaniu powinna wyglądać następująco:

    public class RemoteEntityHelper : BaseRemoteEntityHelper
    
  2. Zastąpij metody abstrakcyjne przy użyciu następującego kodu:

    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);
        }
    }
    

Ten kod zapewnia dodanie składnika EntityOverrideController do jednostki docelowej, a następnie wywołuje jedną z metod przełączania. W razie potrzeby w obiekcie TestModel GameObject można wywołać te metody pomocnicze, dodając element RemoteEntityHelper jako wywołanie zwrotne do OnRemoteEntityClicked zdarzenia w składniku RemoteRayCastPointerHandler .

Wywołania zwrotne wskaźnika

Po dodaniu tych skryptów do modelu po nawiązaniu połączenia ze środowiskiem uruchomieniowym kontroler widoku AppMenu powinien mieć włączone dodatkowe interfejsy umożliwiające interakcję ze skryptem EntityOverrideController . Zapoznaj się z menu Narzędzia modelu , aby wyświetlić odblokowane kontrolery widoku.

W tym momencie składniki obiektu GameObject modelu TestModel powinny wyglądać mniej więcej tak:

Testowanie modelu za pomocą dodatkowych skryptów

Oto przykład przesłonięć stosu dla pojedynczej jednostki. Użyliśmy Select elementów i Tint w celu zapewnienia zarówno konturu, jak i kolorowania:

Wybór odcienia modelu testowego

Wycięte płaszczyzny

Płaszczyzny wycinania to funkcja, którą można dodać do dowolnej jednostki zdalnej. Najczęściej tworzy się nową jednostkę zdalną, która nie jest skojarzona z żadnymi danymi siatki do przechowywania składnika płaszczyzny cięcia. Położenie i orientacja płaszczyzny wycięcia są określane przez położenie i orientację jednostki zdalnej, do którą jest dołączony.

Utworzymy skrypt, który automatycznie tworzy jednostkę zdalną, dodaje składnik płaszczyzny wycięcia i synchronizuje przekształcenie obiektu lokalnego z jednostką płaszczyzny cięcia. Następnie możemy użyć kontrolki CutPlaneViewController do opakowania płaszczyzny wycięcia w interfejsie, który pozwoli nam go manipulować.

  1. Utwórz nowy skrypt o nazwie RemoteCutPlane i zastąp jego kod poniższym kodem:

    // 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
        }
    }
    

    Ten kod rozszerza klasę BaseRemoteCutPlane zawartą w temacie Zasoby samouczka. Podobnie jak w przypadku zdalnie renderowanego modelu, ten skrypt dołącza i nasłuchuje RemoteRenderingState zmian z koordynatora zdalnego. Gdy koordynator osiągnie RuntimeConnected stan, spróbuje automatycznie nawiązać połączenie, jeśli ma zostać nawiązane. Istnieje również zmienna CutPlaneComponent , która będzie śledzona. Jest to składnik usługi Azure Remote Rendering, który synchronizuje się z płaszczyzną wycinania w sesji zdalnej. Przyjrzyjmy się temu, co musimy zrobić, aby utworzyć płaszczyznę cięcia.

  2. Zastąp metodę CreateCutPlane() ukończoną wersją poniżej:

    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;
    }
    

    W tym miejscu tworzymy jednostkę zdalną i wiążemy ją z lokalnym obiektem GameObject. Upewniamy się, że jednostka zdalna będzie miała jej przekształcenie zsynchronizowane z przekształceniem lokalnym, ustawiając wartość SyncEveryFrame .true Następnie używamy wywołania CreateComponent , aby dodać obiekt do CutPlaneComponent obiektu zdalnego. Na koniec skonfigurujemy płaszczyznę wycinania z ustawieniami zdefiniowanymi u góry modułu MonoBehaviour. Zobaczmy, czego potrzeba, aby oczyścić płaszczyznę cięcia przez zaimplementowanie DestroyCutPlane() metody .

  3. Zastąp metodę DestroyCutPlane() ukończoną wersją poniżej:

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

Ponieważ obiekt zdalny jest dość prosty i czyścimy tylko zdalny koniec (i utrzymujemy nasz obiekt lokalny), wystarczy wywołać Destroy obiekt zdalny i wyczyścić nasze odwołanie do niego.

Aplikacja AppMenu zawiera kontroler widoku, który będzie automatycznie dołączany do płaszczyzny cięcia i umożliwia interakcję z nim. Nie jest to wymagane, aby używać narzędzia AppMenu ani żadnego z kontrolerów widoków, ale zapewniają lepsze środowisko pracy. Teraz przetestuj płaszczyznę cięcia i jego kontroler widoku.

  1. Utwórz nowy, pusty obiekt GameObject w scenie i nadaj mu nazwę CutPlane.

  2. Dodaj składnik RemoteCutPlane do obiektu CutPlane GameObject.

    Konfiguracja składnika płaszczyzny wycinania

  3. Naciśnij pozycję Odtwórz w edytorze aparatu Unity, aby załadować sesję zdalną i nawiązać z nią połączenie.

  4. Korzystając z symulacji ręcznej zestawu narzędzi MRTK, chwytaj i obracaj (przytrzymaj klawisze Ctrl, aby obrócić) cutPlane, aby poruszać się po scenie. Obejrzyj fragment w modelu TestModel , aby wyświetlić składniki wewnętrzne.

Przykład płaszczyzny wycinania

Konfigurowanie oświetlenia zdalnego

Sesja zdalnego renderowania obsługuje pełne spektrum opcji oświetlenia. Utworzymy skrypty dla struktury Sky Texture i prostą mapę dla dwóch typów światła aparatu Unity do użycia z renderowaniem zdalnym.

Tekstura nieba

Istnieje wiele wbudowanych map modułów do wyboru podczas zmieniania tekstury nieba. Są one ładowane do sesji i stosowane do tekstury nieba. Można również załadować własne tekstury do użycia jako światło nieba.

Utworzymy skrypt RemoteSky zawierający listę wbudowanych map modułów w postaci parametrów ładowania. Następnie zezwolimy użytkownikowi na wybranie i załadowanie jednej z opcji.

  1. Utwórz nowy skrypt o nazwie RemoteSky i zastąp całą jego zawartość poniższym kodem:

    // 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");
            }
        }
    }
    

    Najważniejszą częścią tego kodu jest tylko kilka wierszy:

    //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;
    

    W tym miejscu uzyskujemy odwołanie do tekstury do użycia przez załadowanie jej do sesji z wbudowanego magazynu obiektów blob. Następnie musimy przypisać tę teksturę tylko do sesji SkyReflectionTexture , aby ją zastosować.

  2. Utwórz pusty obiekt GameObject w scenie i nadaj mu nazwę SkyLight.

  3. Dodaj skrypt RemoteSky do obiektu SkyLight GameObject.

    Przełączanie między światłami nieba można wykonać przez wywołanie SetSky jednego z kluczy ciągu zdefiniowanych w pliku AvailableCubemaps. Kontroler widoku wbudowany w aplikację AppMenu automatycznie tworzy przyciski i podłącza swoje zdarzenia do wywołania SetSky przy użyciu odpowiedniego klucza.

  4. Naciśnij pozycję Odtwórz w edytorze aparatu Unity i autoryzuj połączenie.

  5. Po nawiązaniu połączenia lokalnego środowiska uruchomieniowego z sesją zdalną przejdź do pozycji AppMenu —> Narzędzia sesji —> Remote Sky , aby zapoznać się z różnymi opcjami nieba i zobaczyć, jak wpływają one na model TestModel.

Światła sceny

Zdalne światła sceny obejmują: punkt, punkt i kierunkowy. Podobnie jak w przypadku utworzonej powyżej płaszczyzny wycinania, te światła sceny są jednostkami zdalnymi z dołączonymi do nich składnikami. Ważną kwestią podczas oświetlenia sceny zdalnej jest próba dopasowania oświetlenia do sceny lokalnej. Ta strategia nie zawsze jest możliwa, ponieważ wiele aplikacji aparatu Unity dla HoloLens 2 nie używa renderowania fizycznego dla lokalnie renderowanych obiektów. Jednak na pewnym poziomie możemy symulować prostsze domyślne oświetlenie aparatu Unity.

  1. Utwórz nowy skrypt o nazwie RemoteLight i zastąp jego kod poniższym kodem:

    // 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();
        }
    }
    

    Ten skrypt tworzy różne typy świateł zdalnych w zależności od typu lokalnego aparatu Unity światło, do których jest dołączony skrypt. Zdalne światło zduplikuje lokalne światło w jego pozycji, rotacji, koloru i intensywności. Jeśli to możliwe, zdalne światło ustawi również dodatkową konfigurację. To nie jest idealne dopasowanie, ponieważ światła Aparatu Unity nie są światłami PBR.

  2. Znajdź obiekt DirectionalLight GameObject w scenie. Jeśli usunięto domyślną kontrolkę DirectionalLight ze sceny: na górnym pasku menu wybierz pozycję GameObject -> Light -> DirectionalLight , aby utworzyć nowe światło w scenie.

  3. Wybierz obiekt DirectionalLight GameObject i za pomocą przycisku Dodaj składnik dodaj skrypt RemoteLight .

  4. Ponieważ ten skrypt implementuje klasę BaseRemoteLightbazową , można użyć dostarczonego kontrolera widoku AppMenu do interakcji ze światłem zdalnym. Przejdź do pozycji AppMenu —> Narzędzia sesji —> światło kierunkowe.

    Uwaga

    Interfejs użytkownika w aplikacji AppMenu został ograniczony do pojedynczego światła kierunkowego dla uproszczenia. Jednak nadal jest możliwe i zachęcamy do dodawania świateł punktowych i dołączania do nich skryptu RemoteLight . Te dodatkowe światła można modyfikować, edytując właściwości światła aparatu Unity w edytorze. Należy ręcznie zsynchronizować zmiany lokalne ze światłem zdalnym przy użyciu menu kontekstowego RemoteLight w inspektorze:

    Zdalna synchronizacja ręczna światła

  5. Naciśnij pozycję Odtwórz w edytorze aparatu Unity i autoryzuj połączenie.

  6. Po połączeniu środowiska uruchomieniowego z sesją zdalną umieść aparat i wyceluj w kamerę (użyj funkcji WASD i kliknij prawym przyciskiem myszy w celu wyświetlenia kontrolera widoku światła kierunkowego.

  7. Użyj zdalnego kontrolera widoku światła, aby zmodyfikować właściwości światła. Korzystając z symulacji ręcznej zestawu narzędzi MRTK, chwytaj i obracaj (przytrzymaj klawisze Ctrl, aby obrócić) światło kierunkowe, aby zobaczyć efekt oświetlenia sceny.

    Światło kierunkowe

Materiały do edycji

Zdalnie renderowane materiały można modyfikować, aby zapewnić dodatkowe efekty wizualne, dostosować wizualizacje renderowanych modeli lub przekazać dodatkową opinię użytkownikom. Istnieje wiele sposobów i wiele powodów modyfikacji materiału. W tym miejscu pokażemy, jak zmienić kolor albedo materiału i zmienić szorstkość i metalowość materiału PBR.

Uwaga

W wielu przypadkach, jeśli można zaimplementować funkcję lub efekt przy użyciu elementu HierarchicalStateOverrideComponent, najlepszym rozwiązaniem jest użycie tej funkcji zamiast modyfikowania materiału.

Utworzymy skrypt, który akceptuje jednostkę docelową i konfiguruje kilka OverrideMaterialProperty obiektów w celu zmiany właściwości materiału jednostki docelowej. Zaczynamy od pobrania elementu docelowego MeshComponent jednostki zawierającej listę materiałów używanych w siatce. Dla uproszczenia użyjemy tylko pierwszego znalezionego materiału. Ta naiwna strategia może się bardzo łatwo nie powieść w zależności od sposobu tworzenia zawartości, więc prawdopodobnie zechcesz podjąć bardziej złożone podejście do wyboru odpowiedniego materiału.

Z materiału możemy uzyskać dostęp do typowych wartości, takich jak albedo. Najpierw należy rzutować materiały w odpowiednim typie PbrMaterial lub ColorMaterial, aby pobrać ich wartości, jak pokazano w metodzie GetMaterialColor . Gdy będziemy mieli odwołanie do żądanego materiału, po prostu ustawimy wartości, a ARR będzie obsługiwać synchronizację między właściwościami lokalnego materiału a materiałem zdalnym.

  1. Utwórz skrypt o nazwie EntityMaterialController i zastąp jego zawartość następującym kodem:

    // 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;
        }
    }
    

Typ OverrideMaterialProperty powinien być wystarczająco elastyczny, aby w razie potrzeby można było zmienić kilka innych wartości materialnych. Typ OverrideMaterialProperty śledzi stan przesłonięcia, utrzymuje starą i nową wartość i używa delegata do ustawienia przesłonięcia. Na przykład przyjrzyj się elementowi ColorOverride:

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

Spowoduje to utworzenie nowego OverrideMaterialProperty miejsca, w którym zastąpienie będzie opakowować typ Color. Udostępniamy bieżący lub oryginalny kolor w momencie utworzenia przesłonięcia. Udostępniamy również materiał ARR do działania. Na koniec jest dostarczany delegat, który będzie stosować przesłonięcia. Delegat jest metodą, która akceptuje materiał ARR i typ przesłonięcia zawijania. Ta metoda jest najważniejszą częścią zrozumienia sposobu dostosowywania wartości materialnych przez ARR.

Metoda ColorOverride używa ApplyMaterialColor metody do wykonania swojej pracy:

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

Ten kod akceptuje materiał i kolor. Sprawdza, jakiego rodzaju materiał jest następnie rzutować materiał, aby zastosować kolor.

MetalnessOverride I RoughnessOverride działają podobnie - przy użyciu ApplyRoughnessValue metod i ApplyMetalnessValue do wykonywania swojej pracy.

Następnie przetestujmy kontroler materiału.

  1. Dodaj skrypt EntityMaterialController do obiektu TestModel GameObject.
  2. Naciśnij pozycję Odtwórz w a unity, aby uruchomić scenę i nawiązać połączenie z usługą ARR.
  3. Po nawiązaniu połączenia środowiska uruchomieniowego z sesją zdalną i załadowaniu modelu przejdź do pozycji AppMenu —> Narzędzia modelu —> Edytuj materiał
  4. Wybierz jednostkę z modelu przy użyciu symulowanych rąk, aby kliknąć model TestModel.
  5. Upewnij się, że kontroler widoku materiału (AppMenu-Model Tools-Edit>> Material) został zaktualizowany do docelowej jednostki.
  6. Użyj kontrolera widoku materiału, aby dostosować materiał w jednostce docelowej.

Ponieważ modyfikujemy tylko pierwszy materiał z siatki, może się okazać, że materiał się zmienia. Użyj hierarchicznego zastąpienia SeeThrough , aby sprawdzić, czy zmieniany materiał znajduje się wewnątrz siatki.

Przykład edycji materiału

Następne kroki

Gratulacje! Teraz zaimplementowano wszystkie podstawowe funkcje usługi Azure Remote Rendering. W następnym rozdziale dowiesz się więcej o zabezpieczaniu usługi Azure Remote Rendering i usługi Blob Storage. Będą to pierwsze kroki do wydania aplikacji komercyjnej korzystającej z usługi Azure Remote Rendering.