Dela via


Självstudie: Förfina material, belysning och effekter

I den här guiden får du lära dig att:

  • Markera och beskriva modeller och modellkomponenter
  • Tillämpa olika material på modeller
  • Segmentera genom modeller med klippta plan
  • Lägga till enkla animeringar för fjärrrederade objekt

Förutsättningar

Markering och utläggning

Att ge visuell feedback till användaren är en viktig del av användarupplevelsen i alla program. Azure Remote Rendering tillhandahåller visuella feedbackmekanismer via Åsidosättningar av hierarkiskt tillstånd. Åsidosättningarna för hierarkiskt tillstånd implementeras med komponenter som är kopplade till lokala instanser av modeller. Vi har lärt oss hur du skapar dessa lokala instanser i Synkronisera fjärrobjektdiagrammet i Unity-hierarkin.

Först skapar vi en omslutning runt komponenten HierarchicalStateOverrideComponent . HierarchicalStateOverrideComponent är det lokala skriptet som styr åsidosättningarna på fjärrentiteten. Självstudietillgångarna innehåller en abstrakt basklass med namnet BaseEntityOverrideController, som vi utökar för att skapa omslutningen.

  1. Skapa ett nytt skript med namnet EntityOverrideController och ersätt innehållet med följande kod:

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

LocalOverride huvudsakliga jobb är att skapa en länk mellan sig själv och dess RemoteComponent. Med LocalOverride kan vi sedan ange tillståndsflaggor för den lokala komponenten, som är bundna till fjärrentiteten. Åsidosättningarna och deras tillstånd beskrivs på sidan Åsidosättningar för hierarkiskt tillstånd .

Den här implementeringen växlar bara ett tillstånd i taget. Det är dock fullt möjligt att kombinera flera åsidosättningar på enskilda entiteter och skapa kombinationer på olika nivåer i hierarkin. Om du till exempel kombinerar Selected och SeeThrough på en enskild komponent får den en disposition samtidigt som den blir transparent. Om du anger åsidosättningen av rotentiteten Hidden till ForceOn när en underordnad entitet åsidosätts ForceOff döljs allt utom det underordnade med åsidosättningenHidden.

Om du vill tillämpa tillstånd på entiteter kan vi ändra remoteEntityHelper som skapades tidigare.

  1. Ändra klassen RemoteEntityHelper för att implementera den abstrakta klassen BaseRemoteEntityHelper . Den här ändringen tillåter användning av en vykontrollant som anges i självstudietillgångarna. Den bör se ut så här när den ändras:

    public class RemoteEntityHelper : BaseRemoteEntityHelper
    
  2. Åsidosätt de abstrakta metoderna med hjälp av följande kod:

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

Den här koden säkerställer att en EntityOverrideController-komponent läggs till i målentiteten och anropar sedan en av växlingsmetoderna. Om du vill kan du anropa dessa hjälpmetoder på TestModel GameObject genom att lägga till RemoteEntityHelper som ett återanrop till OnRemoteEntityClicked händelsen på RemoteRayCastPointerHandler-komponenten .

Återanrop till pekare

Nu när dessa skript har lagts till i modellen, när de är anslutna till körningen, bör AppMenu-vykontrollanten ha ytterligare gränssnitt aktiverade för att interagera med EntityOverrideController-skriptet . Gå till menyn Modellverktyg för att se upplåst vykontrollanter.

Nu bör TestModel GameObjects komponenter se ut ungefär så här:

Testmodell med ytterligare skript

Här är ett exempel på att stapla åsidosättningar på en enda entitet. Vi använde Select och Tint för att tillhandahålla både en kontur och färg:

Välj testmodellton

Klippa ut planer

Klipp ut plan är en funktion som kan läggas till i alla fjärrentiteter. Oftast skapar du en ny fjärrentitet som inte är associerad med några nätdata för att lagra komponenten för klippplanet. Klippplanets position och orientering bestäms av positionen och orienteringen för den fjärrentitet som det är kopplat till.

Vi skapar ett skript som automatiskt skapar en fjärrentitet, lägger till en komponent för klippt plan och synkroniserar transformering av ett lokalt objekt med entiteten klipp ut plan. Sedan kan vi använda CutPlaneViewController för att omsluta klippplanet i ett gränssnitt som gör att vi kan manipulera det.

  1. Skapa ett nytt skript med namnet RemoteCutPlane och ersätt koden med koden nedan:

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

    Den här koden utökar klassen BaseRemoteCutPlane som ingår i självstudietillgångarna. På samma sätt som den fjärrrenderade modellen bifogar RemoteRenderingState och lyssnar det här skriptet efter ändringar från fjärrkoordinatorn. När koordinatorn når RuntimeConnected tillståndet försöker den ansluta automatiskt om det är tänkt. Det finns också en CutPlaneComponent variabel som vi kommer att spåra. Det här är Den Azure Remote Rendering komponent som synkroniseras med det klippta planet i fjärrsessionen. Låt oss ta en titt på vad vi behöver göra för att skapa klippplanet.

  2. CreateCutPlane() Ersätt metoden med den färdiga versionen nedan:

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

    Här skapar vi en fjärrentitet och binder den till en lokal GameObject. Vi ser till att fjärrentiteten har sin transformering synkroniserad till den lokala transformeringen genom att ange SyncEveryFrame till true. Sedan använder vi anropet CreateComponent för att lägga till en CutPlaneComponent i fjärrobjektet. Slutligen konfigurerar vi klippplanet med de inställningar som definierats överst i MonoBehaviour. Nu ska vi se vad som krävs för att rensa ett klippt plan genom att implementera DestroyCutPlane() metoden.

  3. DestroyCutPlane() Ersätt metoden med den färdiga versionen nedan:

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

Eftersom fjärrobjektet är ganska enkelt och vi bara rensar fjärrdelen (och behåller vårt lokala objekt) är det enkelt att bara anropa Destroy fjärrobjektet och rensa vår referens till det.

AppMenu innehåller en vystyrenhet som automatiskt ansluts till ditt klippta plan och gör att du kan interagera med det. Det krävs inte att du använder AppMenu eller någon av vykontrollanterna, men de ger en bättre upplevelse. Testa nu klippplanet och dess vystyrenhet.

  1. Skapa en ny, tom GameObject i scenen och ge den namnet CutPlane.

  2. Lägg till RemoteCutPlane-komponenten i CutPlane GameObject.

    Konfiguration av komponent för klippplan

  3. Tryck på Spela upp i Unity-redigeraren för att läsa in och ansluta till en fjärrsession.

  4. Med hjälp av MRTK:s handsimulering tar du tag i och roterar (håll Ctrl för att rotera) CutPlane för att flytta den runt scenen. Titta på den i TestModel för att visa interna komponenter.

Klipp ut planexempel

Konfigurera fjärrbelysningen

Fjärråtergivningssessionen stöder ett fullständigt spektrum av belysningsalternativ. Vi skapar skript för Sky Texture och en enkel karta för två Unity-ljustyper som ska användas med fjärråtergivning.

Sky-struktur

Det finns ett antal inbyggda Kubkartor att välja mellan när du ändrar himlens struktur. Dessa läses in i sessionen och tillämpas på himlens struktur. Det är också möjligt att läsa in i dina egna texturer för att använda som ett himmelsljus.

Vi skapar ett RemoteSky-skript som har en lista över de inbyggda tillgängliga Kubkartor i form av inläsningsparametrar. Sedan låter vi användaren välja och läsa in något av alternativen.

  1. Skapa ett nytt skript med namnet RemoteSky och ersätt hela innehållet med koden nedan:

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

    Den viktigaste delen av den här koden är bara några rader:

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

    Här får vi en referens till den struktur som ska användas genom att läsa in den i sessionen från den inbyggda bloblagringen. Sedan behöver vi bara tilldela den strukturen till sessionens SkyReflectionTexture för att tillämpa den.

  2. Skapa en tom GameObject i din scen och ge den namnet SkyLight.

  3. Lägg till RemoteSky-skriptet i SkyLight GameObject.

    Växling mellan himmelsljus kan göras genom att anropa SetSky med en av strängnycklarna som definierats i AvailableCubemaps. Vykontrollanten som är inbyggd i AppMenu skapar automatiskt knappar och ansluter sina händelser för att anropa SetSky med respektive nyckel.

  4. Tryck på Spela upp i Unity-redigeraren och auktorisera en anslutning.

  5. När du har anslutit den lokala körningen till en fjärrsession navigerar du i AppMenu –> Sessionsverktyg –> Fjärrhimmel för att utforska de olika himmelsalternativen och se hur de påverkar TestModel.

Scenljus

Fjärrscenljus är: punkt, plats och riktning. På samma sätt som det klippta planet som vi skapade ovan är dessa scenljus fjärrentiteter med komponenter som är kopplade till dem. Ett viktigt övervägande när du tänder din fjärrscen är att försöka matcha belysningen i din lokala scen. Den här strategin är inte alltid möjlig eftersom många Unity-program för HoloLens 2 inte använder fysiskt baserad återgivning för lokalt renderade objekt. Men på en viss nivå kan vi simulera Unitys enklare standardbelysning.

  1. Skapa ett nytt skript med namnet RemoteLight och ersätt dess kod med koden nedan:

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

    Det här skriptet skapar olika typer av fjärrljus beroende på vilken typ av lokalt Unity-ljus skriptet är kopplat till. Fjärrljuset duplicerar det lokala ljuset i dess position, rotation, färg och intensitet. När det är möjligt ställer fjärrlampan också in ytterligare konfiguration. Detta är inte en perfekt matchning, eftersom Unity-lampor inte är PBR-lampor.

  2. Hitta DirectionalLight GameObject i din scen. Om du har tagit bort standardinställningen DirectionalLight från din scen: välj GameObject - Light ->> DirectionalLight i den översta menyraden för att skapa ett nytt ljus i din scen.

  3. Välj DirectionalLight GameObject och lägg till RemoteLight-skriptet med knappen Lägg till komponent.

  4. Eftersom det här skriptet implementerar basklassen BaseRemoteLightkan du använda den angivna AppMenu-vykontrollanten för att interagera med fjärrbelysningen. Gå till AppMenu –> Sessionsverktyg –> Riktningsljus.

    Anteckning

    Användargränssnittet i AppMenu har begränsats till ett enda riktningsljus för enkelhetens skull. Det är dock fortfarande möjligt och uppmuntras att lägga till punktlampor och koppla RemoteLight-skriptet till dem. Dessa ytterligare lampor kan ändras genom att redigera egenskaperna för Unity-ljuset i redigeraren. Du måste synkronisera de lokala ändringarna till fjärrbelysningen manuellt med hjälp av snabbmenyn RemoteLight i inspektören:

    Manuell manuell synkronisering av fjärrljus

  5. Tryck på Spela upp i Unity-redigeraren och auktorisera en anslutning.

  6. När du har anslutit din körning till en fjärrsession, placera och sikta kameran (använd WASD och högerklicka + musen flytta) för att ha den riktade ljusvystyrenheten i sikte.

  7. Använd fjärrljuskontrollanten för att ändra ljusets egenskaper. Använd MRTK:s handsimulering, ta tag i och rotera (håll Ctrl för att rotera) riktningsljuset för att se effekten på scenens belysning.

    Riktningsljus

Redigera material

Fjärråtergivet material kan ändras för att ge ytterligare visuella effekter, finjustera visuella objekt för renderade modeller eller ge ytterligare feedback till användarna. Det finns många sätt och många skäl att ändra ett material. Här visar vi hur du ändrar ett materials albedofärg och ändrar ett PBR-materials grovhet och metallighet.

Anteckning

I många fall, om en funktion eller effekt kan implementeras med hjälp av en HierarchicalStateOverrideComponent, är det idealiskt att använda den i stället för att ändra materialet.

Vi skapar ett skript som accepterar en målentitet och konfigurerar några OverrideMaterialProperty objekt för att ändra egenskaperna för målentitetens material. Vi börjar med att hämta målentitetens MeshComponent, som innehåller en lista över material som används i nätet. För enkelhetens skull använder vi bara det första material som hittades. Den här naiva strategin kan misslyckas mycket enkelt beroende på hur innehållet skapades, så du skulle förmodligen vilja använda en mer komplex metod för att välja lämpligt material.

Från materialet kan vi komma åt vanliga värden som albedo. Först måste materialen gjutas i lämplig typ, PbrMaterial eller ColorMaterial, för att hämta sina värden, enligt metoden GetMaterialColor . När vi har en referens till önskat material anger du bara värdena så hanterar ARR synkroniseringen mellan de lokala materialegenskaperna och fjärrmaterialet.

  1. Skapa ett skript med namnet EntityMaterialController och ersätt dess innehåll med följande kod:

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

Typen OverrideMaterialProperty bör vara tillräckligt flexibel för att några andra materialvärden ska kunna ändras, om så önskas. Typen OverrideMaterialProperty spårar tillståndet för en åsidosättning, behåller det gamla och nya värdet och använder ett ombud för att ange åsidosättningen. Som ett exempel kan du titta på ColorOverride:

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

Detta skapar en ny OverrideMaterialProperty där åsidosättningen omsluter typen Color. Vi anger aktuell eller ursprunglig färg när åsidosättningen skapas. Vi ger det också ett ARR-material att agera på. Slutligen tillhandahålls ett ombud som tillämpar åsidosättningen. Ombudet är en metod som accepterar ett ARR-material och typen åsidosättningsomslutningar. Den här metoden är den viktigaste delen av att förstå hur ARR justerar materialvärden.

ColorOverride använder ApplyMaterialColor metoden för att utföra sitt arbete:

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

Den här koden accepterar ett material och en färg. Det kontrollerar för att se vilken typ av material det är sedan gör en gjutning av materialet för att applicera färgen.

Och RoughnessOverrideMetalnessOverride fungerar på liknande sätt – med metoderna ApplyRoughnessValue och ApplyMetalnessValue för att utföra sitt arbete.

Nu ska vi testa materialkontrollanten.

  1. Lägg till EntityMaterialController-skriptet i TestModel GameObject.
  2. Tryck på Spela upp i Unity för att starta scenen och ansluta till ARR.
  3. När du har anslutit körningen till en fjärrsession och inläsning av modellen går du till AppMenu –> Modellverktyg –> Redigera material
  4. Välj en entitet från modellen med hjälp av de simulerade händerna för att klicka på TestModel.
  5. Bekräfta att materialvisningskontrollanten (AppMenu-Model> Tools-Edit> Material) har uppdaterats till målentiteten.
  6. Använd materialvisningskontrollanten för att justera materialet på den aktuella entiteten.

Eftersom vi bara ändrar det första materialet i nätet kanske du inte ser materialet förändras. Använd den hierarkiska åsidosättningen SeeThrough för att se om materialet du ändrar finns i nätet.

Exempel på materialredigering

Nästa steg

Grattis! Nu har du implementerat alla kärnfunktioner i Azure Remote Rendering. I nästa kapitel får du lära dig hur du skyddar din Azure-Remote Rendering och Blob Storage. Det här är de första stegen för att släppa ett kommersiellt program som använder Azure Remote Rendering.