Esercitazione: Perfezionamento di materiali, illuminazione ed effetti
In questa esercitazione verranno illustrate le procedure per:
- Evidenziare e strutturare i modelli e i relativi componenti
- Applicare materiali diversi ai modelli
- Sezionare i modelli con i piani di taglio
- Aggiungere animazioni semplici per oggetti sottoposti a rendering remoto
Prerequisiti
- Questa esercitazione si basa sull'argomento Esercitazione: Manipolazione dei modelli.
Evidenziazione e struttura
Una parte importante dell'esperienza utente di tutte le applicazioni consiste nel fornire un feedback visivo all'utente. Rendering remoto di Azure prevede meccanismi di feedback visivo tramite gli override degli stati gerarchici. Gli override degli stati gerarchici vengono implementati con i componenti collegati a istanze locali dei modelli. La creazione di queste istanze locali viene descritta nell'argomento Sincronizzazione del grafico di oggetti remoto nella gerarchia di Unity.
Prima di tutto, verrà creato un wrapper intorno all componente HierarchicalStateOverrideComponent. Il componente HierarchicalStateOverrideComponent è lo script locale che controlla gli override nell'entità remota. Gli asset dell'esercitazione includono una classe di base astratta denominata BaseEntityOverrideController, che verrà estesa per creare il wrapper.
Creare un nuovo script denominato EntityOverrideController e sostituirne l'intero contenuto con il codice seguente:
// 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); } } }
Il compito principale di LocalOverride consiste nel creare un collegamento tra se stesso e il relativo componente RemoteComponent
. LocalOverride quindi consente di impostare i flag di stato sul componente locale, che sono associati all'entità remota. Gli override e i relativi stati vengono descritti nella pagina Override degli stati gerarchici.
Questa implementazione imposta un solo stato alla volta. Tuttavia, è possibile combinare più override in singole entità e creare combinazioni a livelli diversi nella gerarchia. Se ad esempio si combinano Selected
e SeeThrough
in un singolo componente, quest'ultimo ottiene una struttura e diventa anche trasparente. Oppure, se si impostano l'override Hidden
dell'entità radice su ForceOn
e l'override Hidden
dell'entità figlio su ForceOff
, verrà nascosto tutto tranne l'entità figlio con l'override.
Per applicare gli stati alle entità, è possibile modificare la classe RemoteEntityHelper creata in precedenza.
Modificare la classe RemoteEntityHelper per implementare la classe astratta BaseRemoteEntityHelper. Questa modifica consentirà l'uso di un controller di visualizzazione fornito negli asset dell'esercitazione. Dopo la modifica si otterrà quanto segue:
public class RemoteEntityHelper : BaseRemoteEntityHelper
Eseguire l'override dei metodi astratti usando il codice seguente:
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); } }
Questo codice assicura che venga aggiunto un componente EntityOverrideController all'entità di destinazione, quindi chiama uno dei metodi di attivazione. Se lo si desidera, nel GameObject TestModel la chiamata a questi metodi helper può essere eseguita aggiungendo RemoteEntityHelper come callback all'evento OnRemoteEntityClicked
nel componente RemoteRayCastPointerHandler.
Ora che questi script sono stati aggiunti al modello, dopo la connessione al runtime, il controller di visualizzazione AppMenu dovrebbe includere altre interfacce abilitate per l'interazione con lo script EntityOverrideController. Per vedere i controller di visualizzazione sbloccati, aprire il menu Model Tools (Strumenti del modello).
A questo punto, i componenti del GameObject TestModel avranno un aspetto simile al seguente:
Ecco un esempio di override raggruppati in una singola entità. Sono stati usati Select
e Tint
per fornire una struttura e una colorazione:
Piani di taglio
I piani di taglio sono una funzionalità che è possibile aggiungere a qualsiasi entità remota. Nella maggior parte dei casi, si crea una nuova entità remota non associata a dati di mesh in cui contenere il componente piano di taglio. La posizione e l'orientamento del piano tagliato sono determinati dalla posizione e dall'orientamento dell'entità remota a cui è collegato.
Verrà creato uno script che crea automaticamente un'entità remota, aggiunge un componente piano di taglio e sincronizza la trasformazione di un oggetto locale con l'entità del piano di taglio. Quindi, è possibile usare CutPlaneViewController per eseguire il wrapping del piano di taglio in un'interfaccia che consentirà di modificarlo.
Creare un nuovo script denominato RemoteCutPlane e sostituirne il codice con quello seguente:
// 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 } }
Questo codice estende la classe BaseRemoteCutPlane inclusa negli asset dell'esercitazione. Analogamente al modello sottoposto a rendering remoto, questo script collega e resta in ascolto di modifiche di
RemoteRenderingState
da parte del coordinatore remoto. Quando il coordinatore raggiunge lo statoRuntimeConnected
, proverà a connettersi automaticamente, se previsto. È disponibile anche una variabileCutPlaneComponent
che verrà tracciata. Si tratta del componente di Rendering remoto di Azure che esegue la sincronizzazione con il piano di taglio nella sessione remota. Ecco le procedure per creare il piano di taglio.Sostituire il metodo
CreateCutPlane()
con la versione completa seguente: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; }
Viene creata un'entità remota che viene quindi associata a un GameObject locale. Per assicurare che la trasformazione dell'entità remota verrà sincronizzata con la trasformazione locale, impostare
SyncEveryFrame
sutrue
. Usare quindi la chiamata aCreateComponent
per aggiungereCutPlaneComponent
all'oggetto remoto. Infine, configurare il piano di taglio con le impostazioni definite all'inizio di MonoBehaviour. Ecco la procedura per pulire un piano di taglio implementando il metodoDestroyCutPlane()
.Sostituire il metodo
DestroyCutPlane()
con la versione completa seguente:public override void DestroyCutPlane() { if (remoteCutPlaneComponent == null) return; //Nothing to do! remoteCutPlaneComponent.Owner.Destroy(); remoteCutPlaneComponent = null; CutPlaneReady = false; }
Poiché l'oggetto remoto è piuttosto semplice e viene pulita solo l'estremità remota, mantenendo l'oggetto locale, è possibile chiamare semplicemente Destroy
sull'oggetto remoto e cancellare il riferimento a tale oggetto.
AppMenu include un controller di visualizzazione che si collegherà automaticamente al piano di taglio con cui sarà possibile interagire. Non è necessario usare AppMenu o uno qualsiasi dei controller di visualizzazione, ma offrono un'esperienza migliore. Testare ora il piano di taglio e il controller di visualizzazione.
Creare un nuovo GameObject vuoto nella scena e assegnare il nome CutPlane.
Aggiungere il componente RemoteCutPlane al GameObject CutPlane.
Premere Play nell'editor di Unity per caricare e connettersi a una sessione remota.
Usando la simulazione della mano di MRTK, afferrare e ruotare (tenere premuto CTRL per ruotare) l'oggetto CutPlane per spostarlo nella scena. Notare che seziona TestModel per esporne i componenti interni.
Configurazione dell'illuminazione remota
La sessione di rendering remoto supporta un'ampia gamma di opzioni di illuminazione. Verranno creati gli script per la trama del cielo e una semplice mappa per due tipi di luce di Unity da usare con il rendering remoto.
Trama del cielo
Per cambiare la trama del cielo, sono disponibili diverse mappe cubi tra cui scegliere. Vengono caricate nella sessione e applicate alla trama del cielo. È anche possibile caricare trame personalizzate da usare come luce del cielo.
Verrà creato uno script RemoteSky che include un elenco delle mappe cubi predefinite disponibili sotto forma di parametri di caricamento. Quindi, si consentirà all'utente di selezionare e caricare una delle opzioni.
Creare un nuovo script denominato RemoteSky e sostituirne l'intero contenuto con il codice seguente:
// 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"); } } }
La parte più importante di questo codice è costituita da poche righe:
//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;
Qui si ottiene un riferimento alla trama da usare caricandola nella sessione dall'archivio BLOB predefinito. Quindi, è necessario assegnare tale trama all'oggetto
SkyReflectionTexture
della sessione per applicarla.Creare un GameObject vuoto nella scena e assegnare il nome SkyLight.
Aggiungere lo script RemoteSky al GameObject SkyLight.
Il passaggio tra luci del cielo può essere eseguito chiamando
SetSky
con una delle chiavi di stringa definite inAvailableCubemaps
. Il controller di visualizzazione integrato in AppMenu crea automaticamente pulsanti e associa i relativi eventi per chiamareSetSky
con la rispettiva chiave.Premere Play nell'editor di Unity e autorizzare una connessione.
Dopo aver connesso il runtime locale a una sessione remota, passare ad AppMenu -> Strumenti sessione -> Sky remoto per esplorare le diverse opzioni del cielo e vedere come influiscono su TestModel.
Luci della scena
Le luci della scena remota includono la luce puntiforme, spot e direzionale. Analogamente al piano di taglio creato in precedenza, queste luci della scena sono entità remote con componenti collegati. Una considerazione importante da fare per l'illuminazione della scena remota è il tentativo di abbinare l'illuminazione nella scena locale. Questa strategia non è sempre possibile perché molte applicazioni Unity per HoloLens 2 non usano il rendering fisico per gli oggetti sottoposti a rendering locale. Tuttavia, a un certo livello, è possibile simulare l'illuminazione predefinita più semplice di Unity.
Creare un nuovo script denominato RemoteLight e sostituirne il codice con quello seguente:
// 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(); } }
Questo script crea diversi tipi di luci remote a seconda del tipo di luce di Unity locale a cui è collegato. La luce remota duplicherà la luce locale per quanto riguarda la posizione, la rotazione, il colore e l'intensità. Laddove possibile, la luce remota imposterà anche una configurazione aggiuntiva. Non si tratta di una corrispondenza perfetta poiché le luci di Unity non sono luci di rendering fisico.
Trovare il GameObject DirectionalLight nella scena. Se hai rimosso il valore predefinito DirectionalLight dalla scena: dalla barra dei menu in alto selezionare GameObject - Light - Light ->> DirectionalLight per creare una nuova luce nella scena.
Selezionare il GameObject DirectionalLight e usare il pulsante Add Component (Aggiungi componente) per aggiungere lo script RemoteLight.
Poiché questo script implementa la classe di base
BaseRemoteLight
, è possibile usare il controller di visualizzazione AppMenu fornito per interagire con la luce remota. Passare a AppMenu - Strumenti sessione ->> Luce direzionale.Nota
L'interfaccia utente in AppMenu è stata limitata a una singola luce direzionale per semplicità. Tuttavia, è comunque possibile e consigliabile aggiungere luci puntiformi e collegarvi lo script RemoteLight. Queste luci aggiuntive possono essere cambiate modificando le proprietà della luce di Unity nell'editor. Sarà necessario sincronizzare manualmente le modifiche locali con la luce remota usando il menu di scelta rapida RemoteLight nella finestra di controllo:
Premere Play nell'editor di Unity e autorizzare una connessione.
Dopo la connessione del runtime a una sessione remota, posizionare e puntare la videocamera (usare i tasti WASD e fare clic con il pulsante destro del mouse + spostamento del mouse) per avere in primo piano il controller di visualizzazione della luce direzionale.
Usare il controller di visualizzazione della luce remota per modificare le proprietà della luce. Usando la simulazione della mano di MRTK, afferrare e ruotare (tenere premuto CTRL per ruotare) la luce direzionale per vedere l'effetto sull'illuminazione della scena.
Materiali di modifica
È possibile modificare i materiali sottoposti a rendering remoto per aggiungere altri effetti visivi, ottimizzare gli oggetti visivi dei modelli sottoposti a rendering o fornire altro feedback agli utenti. Esistono molti modi e molti motivi per modificare un materiale. Qui viene illustrato come cambiare il colore dell'albedo di un materiale, oltre alla rugosità e alla trama metallica del materiale sottoposto a rendering fisico.
Nota
In molti casi, se è possibile implementare una funzionalità o un effetto usando HierarchicalStateOverrideComponent, è consigliabile procedere in questo modo invece di modificare il materiale.
Verrà creato uno script che accetta un'entità di destinazione e configura alcuni oggetti OverrideMaterialProperty
per cambiare le proprietà del materiale dell'entità di destinazione. Per iniziare, ottenere MeshComponent dell'entità di destinazione, che contiene un elenco di materiali usati nel mesh. Per semplicità, verrà usato solo il primo materiale trovato. Questa strategia semplice può non riuscire molto facilmente a seconda del modo in cui è stato creato il contenuto, quindi è consigliabile adottare un approccio più complesso per selezionare il materiale appropriato.
Dal materiale è possibile accedere a valori comuni come l'albedo. È necessario prima di tutto eseguire il cast dei materiali nel tipo appropriato, PbrMaterial
o ColorMaterial
, per recuperare i relativi valori, come nel metodo GetMaterialColor. Una volta ottenuto un riferimento al materiale desiderato, è sufficiente impostare i valori e Rendering remoto di Azure eseguirà la sincronizzazione tra le proprietà del materiale locale e il materiale remoto.
Creare uno script denominato EntityMaterialController e sostituirne l'intero contenuto con il codice seguente:
// 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; } }
Il tipo OverrideMaterialProperty
deve essere sufficientemente flessibile da consentire la modifica di alcuni altri valori dei materiali, se necessario. Il tipo OverrideMaterialProperty
tiene traccia dello stato di un override, mantiene il valore precedente e quello nuovo e usa un delegato per impostare l'override. Per un esempio, vedere ColorOverride
:
ColorOverride = new OverrideMaterialProperty<Color>(
GetMaterialColor(targetMaterial), //The original value
targetMaterial, //The target material
ApplyMaterialColor); //The action to take to apply the override
Viene creato un nuovo oggetto OverrideMaterialProperty
in cui l'override eseguirà il wrapping del tipo Color
. Al momento della creazione dell'override, viene fornito il colore corrente o originale. Viene anche fornito un materiale di Rendering remoto di Azure su cui agire. Infine, viene fornito un delegato che applicherà l'override. Il delegato è un metodo che accetta un materiale e il tipo di Rendering remoto di Azure di cui l'override esegue il wrapping. Questo metodo è la parte più importante per comprendere il modo in cui Rendering remoto di Azure regola i valori dei materiali.
ColorOverride
usa il metodo ApplyMaterialColor
per eseguire le operazioni seguenti:
private void ApplyMaterialColor(ARRMaterial material, Color color)
{
if (material.MaterialSubType == MaterialType.Color)
((ColorMaterial)material).AlbedoColor = color.toRemoteColor4();
else
((PbrMaterial)material).AlbedoColor = color.toRemoteColor4();
}
Questo codice accetta un materiale e un colore. Verifica il tipo di materiale che viene quindi ne esegue il cast per applicare il colore.
RoughnessOverride
e MetalnessOverride
funzionano in modo analogo, usando i metodi ApplyRoughnessValue
e ApplyMetalnessValue
per svolgere le proprie attività.
Successivamente, verrà testato il controller di materiali.
- Aggiungere lo script EntityMaterialController al GameObject TestModel.
- Premere Play in Unity per avviare la scena e connettersi a Rendering remoto di Azure.
- Dopo aver connesso il runtime a una sessione remota e caricato il modello, passare a AppMenu - Strumenti modello ->> Modifica materiale
- Selezionare un'entità dal modello usando le mani simulate per fare clic su TestModel.
- Verificare che il controller di visualizzazione materiale (AppMenu-Model> Tools-Edit> Material) sia stato aggiornato all'entità di destinazione.
- Usare il controller di visualizzazione dei materiali per modificare il materiale nell'entità di destinazione.
Poiché viene modificato solo il primo materiale della mesh, la modifica potrebbe non essere visibile. Usare l'override gerarchico SeeThrough per verificare se il materiale che si sta modificando si trova all'interno della mesh.
Passaggi successivi
Congratulazioni! A questo punto sono state implementate tutte le funzionalità di base di Rendering remoto di Azure. Nel capitolo successivo verrà illustrato come proteggere Rendering remoto di Azure e Archiviazione BLOB. Saranno i primi passaggi per rilasciare un'applicazione commerciale che usa Rendering remoto di Azure.