Tutorial: Refinar materiais, iluminação e efeitos

Neste tutorial, ficará a saber como:

  • Realçar e destacar modelos e componentes de modelos
  • Aplicar materiais diferentes a modelos
  • Segmentar modelos com planos cortados
  • Adicionar animações simples para objetos compostos remotamente

Pré-requisitos

Realce e destaque

Fornecer feedback visual ao utilizador é uma parte importante da experiência do utilizador em qualquer aplicação. O Azure Remote Rendering fornece mecanismos de feedback visual através de Substituições de estado hierárquico. As substituições de estado hierárquico são implementadas com componentes ligados a instâncias locais de modelos. Aprendemos a criar estas instâncias locais em Sincronizar o grafo de objeto remoto na hierarquia do Unity.

Primeiro, vamos criar um wrapper em torno do componente HierarchicalStateOverrideComponent . HierarchicalStateOverrideComponent é o script local que controla as substituições na entidade remota. Os Recursos do Tutorial incluem uma classe base abstrata chamada BaseEntityOverrideController, que vamos expandir para criar o wrapper.

  1. Crie um novo script com o nome EntityOverrideController e substitua os respetivos conteúdos pelo seguinte código:

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

A tarefa principal da LocalOverride é criar uma ligação entre si e o respetivo RemoteComponent. Em seguida, a LocalOverride permite-nos definir sinalizadores de estado no componente local, que estão vinculados à entidade remota. As substituições e os respetivos estados estão descritos na página Substituições de estado hierárquico .

Esta implementação apenas ativa um estado de cada vez. No entanto, é totalmente possível combinar várias substituições em entidades individuais e criar combinações em diferentes níveis na hierarquia. Por exemplo, combinar Selected e SeeThrough num único componente daria-lhe um destaque, ao mesmo tempo que o tornava transparente. Em alternativa, definir a substituição da entidade Hidden raiz como ForceOn ao efetuar a substituição ForceOff de Hidden uma entidade subordinada como ocultaria tudo, exceto para o menor com a substituição.

Para aplicar estados a entidades, podemos modificar o RemoteEntityHelper criado anteriormente.

  1. Modifique a classe RemoteEntityHelper para implementar a classe abstrata BaseRemoteEntityHelper . Esta modificação permitirá a utilização de um controlador de vista fornecido nos Recursos do Tutorial. Deverá ter o seguinte aspeto quando modificado:

    public class RemoteEntityHelper : BaseRemoteEntityHelper
    
  2. Substitua os métodos abstratos com o seguinte código:

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

Este código garante que um componente EntityOverrideController é adicionado à Entidade de destino e, em seguida, chama um dos métodos de alternar. Se assim o desejar, no TestModel GameObject, pode chamar estes métodos auxiliares ao adicionar o RemoteEntityHelper como uma chamada de retorno ao OnRemoteEntityClicked evento no componente RemoteRayCastPointerHandler .

Chamadas de retorno de ponteiro

Agora que estes scripts foram adicionados ao modelo, uma vez ligados ao runtime, o controlador de vista AppMenu deve ter interfaces adicionais ativadas para interagir com o script EntityOverrideController . Consulte o menu Ferramentas de Modelo para ver os controladores de vista desbloqueados.

Neste momento, os componentes do TestModel GameObject devem ter um aspeto semelhante ao seguinte:

Modelo de Teste com scripts adicionais

Eis um exemplo de substituições de pilha numa única entidade. Utilizámos Select e Tint para fornecer um destaque e uma coloração:

Seleção de tonalidade de Modelo de Teste

Cortar planos

Os planos cortados são uma funcionalidade que pode ser adicionada a qualquer entidade remota. Normalmente, cria uma nova entidade remota que não está associada a quaisquer dados de malha para conter o componente de plano cortado. A posição e a orientação do plano de corte são determinadas pela posição e orientação da entidade remota à qual está anexada.

Vamos criar um script que cria automaticamente uma entidade remota, adiciona um componente de plano cortado e sincroniza a transformação de um objeto local com a entidade de plano cortado. Em seguida, podemos utilizar o CutPlaneViewController para moldar o plano cortado numa interface que nos permitirá manipulá-lo.

  1. Crie um novo script com o nome RemoteCutPlane e substitua o respetivo código pelo código abaixo:

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

    Este código expande a classe BaseRemoteCutPlane incluída nos Recursos do Tutorial. Da mesma forma que o modelo composto remotamente, este script anexa e escuta RemoteRenderingState as alterações do coordenador remoto. Quando o coordenador chegar ao RuntimeConnected estado, tentará ligar automaticamente, se for suposto. Também existe uma CutPlaneComponent variável que vamos controlar. Este é o componente do Azure Remote Rendering que sincroniza com o plano cortado na sessão remota. Vamos ver o que temos de fazer para criar o plano de corte.

  2. Substitua o CreateCutPlane() método pela versão concluída abaixo:

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

    Aqui, estamos a criar uma entidade remota e a enlace-la a um GameObject local. Garantimos que a entidade remota terá a transformação sincronizada com a transformação local ao definir SyncEveryFrame como true. Em seguida, utilizamos a CreateComponent chamada para adicionar um CutPlaneComponent ao objeto remoto. Por fim, configuramos o plano de corte com as definições definidas na parte superior do MonoBehaviour. Vamos ver o que é preciso para limpar um plano cortado ao implementar o DestroyCutPlane() método .

  3. Substitua o DestroyCutPlane() método pela versão concluída abaixo:

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

Uma vez que o objeto remoto é bastante simples e estamos apenas a limpar a extremidade remota (e a manter o nosso objeto local), é simples chamar Destroy o objeto remoto e limpar a nossa referência ao mesmo.

O AppMenu inclui um controlador de vista que será automaticamente anexado ao plano cortado e permite-lhe interagir com o mesmo. Não é necessário que utilize o AppMenu ou qualquer um dos controladores de vista, mas estes permitem uma melhor experiência. Agora, teste o plano de corte e o controlador de vista.

  1. Crie um Novo GameObject vazio na cena e dê-lhe o nome CutPlane.

  2. Adicione o componente RemoteCutPlane ao CutPlane GameObject.

    Configuração do componente Cortar Plano

  3. Prima Reproduzir no Editor do Unity para carregar e ligar a uma sessão remota.

  4. Ao utilizar a simulação manual do MRTK, agarre e rode (mantenha premida a tecla Ctrl para rodar) o CutPlane para movê-lo ao redor da cena. Veja-o a segmentar-se no TestModel para revelar componentes internos.

Exemplo de Cortar Plano

Configurar a iluminação remota

A sessão de composição remota suporta um espetro completo de opções de iluminação. Vamos criar scripts para a Textura do Céu e um mapa simples para dois tipos de luz unity a utilizar com composição remota.

Textura do Céu

Existem vários Cubos incorporados à escolha ao alterar a textura do céu. Estes são carregados para a sessão e aplicados à textura do céu. Também é possível carregar as suas próprias texturas para usar como uma luz do céu.

Vamos criar um script RemoteSky que tem uma lista dos Cubos disponíveis incorporados sob a forma de parâmetros de carga. Em seguida, vamos permitir que o utilizador selecione e carregue uma das opções.

  1. Crie um novo script com o nome RemoteSky e substitua todo o respetivo conteúdo pelo código abaixo:

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

    A parte mais importante deste código são apenas algumas linhas:

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

    Aqui, obtemos uma referência à textura a utilizar ao carregá-la para a sessão a partir do armazenamento de blobs incorporado. Em seguida, só precisamos de atribuir essa textura à sessão SkyReflectionTexture para aplicá-la.

  2. Crie um GameObject vazio na sua cena e dê-lhe o nome SkyLight.

  3. Adicione o script RemoteSky ao seu GameObject do SkyLight .

    Pode alternar entre luzes do céu ao chamar SetSky com uma das teclas de cadeia definidas em AvailableCubemaps. O controlador de vista incorporado no AppMenu cria automaticamente botões e liga os eventos para chamar SetSky com a respetiva chave.

  4. Prima Reproduzir no Editor do Unity e autorize uma ligação.

  5. Depois de ligar o runtime local a uma sessão remota, navegue em AppMenu –> Ferramentas de Sessão –> Céu Remoto para explorar as diferentes opções do sky e ver como afetam o TestModel.

Luzes de Cena

As luzes de cena remotas incluem: ponto, local e direcional. À semelhança do Plano de Corte que criámos acima, estas luzes de cena são entidades remotas com componentes ligados às mesmas. Uma consideração importante quando acende a sua cena remota está a tentar combinar a iluminação na sua cena local. Esta estratégia nem sempre é possível porque muitas aplicações do Unity para a HoloLens 2 não utilizam composição física para objetos compostos localmente. No entanto, a um determinado nível, podemos simular a iluminação predefinida mais simples do Unity.

  1. Crie um novo script com o nome RemoteLight e substitua o respetivo código pelo código abaixo:

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

    Este script cria diferentes tipos de luzes remotas consoante o tipo de luz local do Unity a que o script está anexado. A luz remota duplicará a luz local na sua posição, rotação, cor e intensidade. Sempre que possível, a luz remota também definirá configuração adicional. Esta não é uma combinação perfeita, uma vez que as luzes do Unity não são luzes PBR.

  2. Localize o DirectionalLight GameObject na sua cena. Se tiver removido o DirectionalLight predefinido da sua cena: na barra de menus superior, selecione GameObject -> Light -> DirectionalLight para criar uma nova luz na sua cena.

  3. Selecione DirectionalLight GameObject e, com o botão Adicionar Componente , adicione o script RemoteLight .

  4. Uma vez que este script implementa a classe BaseRemoteLightbase, pode utilizar o controlador de vista AppMenu fornecido para interagir com a luz remota. Navegue para AppMenu –> Ferramentas de Sessão –> Luz Direcional.

    Nota

    A IU no AppMenu foi limitada a uma única luz direcional para simplificar. No entanto, continua a ser possível e incentivado a adicionar luzes de ponto e a anexar-lhes o script RemoteLight . Essas luzes adicionais podem ser modificadas ao editar as propriedades da luz unity no editor. Terá de sincronizar manualmente as alterações locais à luz remota com o menu de contexto RemoteLight no inspetor:

    Sincronização manual de luz remota

  5. Prima Reproduzir no Editor do Unity e autorize uma ligação.

  6. Depois de ligar o runtime a uma sessão remota, posicione e aponte para a câmara (utilize WASD e clique com o botão direito do rato em + movimento do rato) para ter o controlador de vista de luz direcional na vista.

  7. Utilize o controlador de vista de luz remota para modificar as propriedades da luz. Utilizando a simulação manual do MRTK, agarre e rode (mantenha premida a tecla Ctrl para rodar) a luz direcional para ver o efeito na iluminação da cena.

    Luz direcional

Editar materiais

Os materiais compostos remotamente podem ser modificados para fornecer efeitos visuais adicionais, ajustar os elementos visuais dos modelos compostos ou fornecer feedback adicional aos utilizadores. Existem muitas formas e muitas razões para modificar um material. Aqui, vamos mostrar-lhe como alterar a cor do albedo de um material e alterar a espessura e metalismo de um material PBR.

Nota

Em muitos casos, se uma funcionalidade ou efeito puder ser implementado com um HierarchicalStateOverrideComponent, é ideal utilizá-lo em vez de modificar o material.

Vamos criar um script que aceita uma Entidade de destino e configura alguns OverrideMaterialProperty objetos para alterar as propriedades do material da Entidade de destino. Começamos por obter o MeshComponent da Entidade de destino, que contém uma lista de materiais utilizados na malha. Para simplificar, vamos utilizar apenas o primeiro material encontrado. Esta estratégia ingénua pode falhar muito facilmente consoante a forma como o conteúdo foi criado, pelo que é provável que queira adotar uma abordagem mais complexa para selecionar o material adequado.

A partir do material, podemos aceder a valores comuns como o albedo. Primeiro, os materiais têm de ser lançados no tipo adequado ou PbrMaterialColorMaterial, para obter os respetivos valores, conforme visto no método GetMaterialColor . Assim que tivermos uma referência ao material pretendido, basta definir os valores e o ARR processará a sincronização entre as propriedades do material local e o material remoto.

  1. Crie um script com o nome EntityMaterialController e substitua os respetivos conteúdos pelo seguinte código:

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

O OverrideMaterialProperty tipo deve ser suficientemente flexível para permitir a alteração de alguns outros valores materiais, se assim o desejar. O OverrideMaterialProperty tipo controla o estado de uma substituição, mantém o valor antigo e novo e utiliza um delegado para definir a substituição. Por exemplo, veja :ColorOverride

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

Isto está a criar um novo OverrideMaterialProperty em que a substituição irá moldar o tipo Color. Fornecemos a cor atual ou original no momento em que a substituição é criada. Também lhe damos um material ARR para agir. Por fim, é fornecido um delegado que aplicará a substituição. O delegado é um método que aceita um material ARR e o tipo que a substituição molda. Este método é a parte mais importante da compreensão de como o ARR ajusta os valores materiais.

O ColorOverride utiliza o método para fazer o ApplyMaterialColor seu trabalho:

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

Este código aceita um material e uma cor. Verifica para ver que tipo de material é e, em seguida, faz um molde do material para aplicar a cor.

O RoughnessOverride e MetalnessOverride funcionam da mesma forma - utilizando os métodos e ApplyMetalnessValue para fazer o ApplyRoughnessValue seu trabalho.

Em seguida, vamos testar o controlador de material.

  1. Adicione o script EntityMaterialController ao seu TestModel GameObject.
  2. Prima Reproduzir no Unity para iniciar a cena e ligar ao ARR.
  3. Depois de ligar o runtime a uma sessão remota e carregar o modelo, navegue para AppMenu –> Ferramentas de Modelo –> Editar Material
  4. Selecione uma Entidade no modelo com as mãos simuladas para clicar no TestModel.
  5. Confirme que o controlador de vista de material (AppMenu-Model> Tools-Edit> Material) foi atualizado para a Entidade de destino.
  6. Utilize o controlador de vista de material para ajustar o material na Entidade de destino.

Uma vez que estamos apenas a modificar o primeiro material da malha, poderá não ver o material a mudar. Utilize a substituição hierárquica SeeThrough para ver se o material que está a alterar está dentro da malha.

Exemplo de edição material

Passos seguintes

Parabéns! Implementou agora todas as principais funcionalidades do Azure Remote Rendering. No próximo capítulo, vamos aprender a proteger o seu armazenamento de blobs e Remote Rendering do Azure. Estes serão os primeiros passos para lançar uma aplicação comercial que utiliza o Azure Remote Rendering.