您现在访问的是微软AZURE全球版技术文档网站,若需要访问由世纪互联运营的MICROSOFT AZURE中国区技术文档网站,请访问 https://docs.azure.cn.

教程:优化材料、照明和效果Tutorial: Refining materials, lighting, and effects

在本教程中,你将了解如何执行以下操作:In this tutorial, you learn how to:

  • 突出显示和大纲显示模型和模型组件Highlight and outline models and model components
  • 将不同材料应用于模型Apply different materials to models
  • 使用剪切平面,穿过模型进行切片Slice through models with cut planes
  • 为远程渲染对象添加简单动画Add simple animations for remotely rendered objects

先决条件Prerequisites

突出显示和大纲显示Highlighting and outlining

向用户提供视觉反馈是任何应用程序中用户体验的重要组成部分。Providing visual feedback to the user is an important part of the user experience in any application. Azure 远程渲染通过分层状态替代提供视觉反馈机制。Azure Remote Rendering provides visual feedback mechanisms through Hierarchical state overrides. 分层状态替代通过附加到模型本地实例的组件来实现。The hierarchical state overrides are implemented with components attached to local instances of models. 我们已通过将远程对象图同步到 Unity 层次结构中中的介绍了解了如何创建这些本地实例。We learned how to create these local instances in Synchronizing the remote object graph into the Unity hierarchy.

首先,我们将围绕 HierarchicalStateOverrideComponent 组件创建一个包装器。First, we'll create a wrapper around the HierarchicalStateOverrideComponent component. HierarchicalStateOverrideComponent 是控制远程实体上替代操作的本地脚本。The HierarchicalStateOverrideComponent is the local script that controls the overrides on the remote entity. Tutorial Assets 包含一个名为 BaseEntityOverrideController 的抽象基类,我们将对其进行扩展以创建包装器 。The Tutorial Assets include an abstract base class called BaseEntityOverrideController, which we'll extend to create the wrapper.

  1. 创建名为 EntityOverrideController 的新脚本,并将其内容替换为以下代码:Create a new script named EntityOverrideController and replace its contents with the following code:

    // 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 的主要工作是在自身与其 RemoteComponent 之间建立链接。LocalOverride's main job is to create a link between itself and its RemoteComponent. 然后,我们可以通过 LocalOverride 在本地组件上设置绑定到远程实体的状态标志。The LocalOverride then allows us to set state flags on the local component, which are bound to the remote entity. 分层状态替代页中对这些替代及其状态进行了描述。The overrides and their states are described in the Hierarchical state overrides page.

此实现一次只切换一个状态。This implementation just toggles one state at a time. 但是,完全有可能合并单个实体上的多个替代并在层次结构的不同级别上创建合并。However, it's entirely possible to combine multiple overrides on single entities and to create combinations at different levels in the hierarchy. 例如,在单个组件上合并 SelectedSeeThrough 可以向组件提供一个边框,同时还可以使它透明。For example, combining Selected and SeeThrough on a single component would give it an outline while also making it transparent. 或者,将根实体的 Hidden 替代设置为 ForceOn,同时将子实体的 Hidden 替代设置为 ForceOff,这样会隐藏除具有替代内容的子项以外的所有内容。Or, setting the root entity Hidden override to ForceOn while making a child entity's Hidden override to ForceOff would hide everything except for the child with the override.

若要将状态应用于实体,可以修改先前创建的 RemoteEntityHelper。To apply states to entities, we can modify the RemoteEntityHelper created previously.

  1. 修改 RemoteEntityHelper 类以实现 BaseRemoteEntityHelper 抽象类 。Modify the RemoteEntityHelper class to implement the BaseRemoteEntityHelper abstract class. 此修改将允许使用 Tutorial Assets 中提供的视图控制器。This modification will allow the use of a view controller provided in the Tutorial Assets. 修改后,它应如下所示:It should look like this when modified:

    public class RemoteEntityHelper : BaseRemoteEntityHelper
    
  2. 使用以下代码重写抽象方法:Override the abstract methods using the following code:

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

此代码可确保将 EntityOverrideController 组件添加到目标实体,然后调用其中一个切换方法。This code ensures an EntityOverrideController component is added to the target Entity, then it calls one of the toggle methods. 如果需要,可以在 TestModel GameObject 上,通过添加 RemoteEntityHelper 作为对 RemoteRayCastPointerHandler 组件上 OnRemoteEntityClicked 事件的回调来调用这些帮助程序方法 。If desired, on the TestModel GameObject, calling these helper methods can be done by adding the RemoteEntityHelper as a callback to the OnRemoteEntityClicked event on the RemoteRayCastPointerHandler component.

指针回调

现在已经将这些脚本添加到模型中,当与运行时连接时,AppMenu 视图控制器应会再启用一个接口,以便与 EntityOverrideController 脚本进行交互 。Now that these scripts have been added to the model, once connected to the runtime, the AppMenu view controller should have additional interfaces enabled to interact with the EntityOverrideController script. 查看“模型工具”菜单以查看未锁定的视图控制器。Check out the Model Tools menu to see the unlocked view controllers.

此时,TestModel GameObject 的组件应如下所示:At this point, your TestModel GameObject's components should look something like this:

具有其他脚本的测试模型

下面是在单个实体上堆叠替代的示例。Here is an example of stacking overrides on a single entity. 我们使用 SelectTint 来提供边框和着色:We used Select and Tint to provide both an outline and coloring:

测试模型色彩选择

剪切平面Cut planes

剪切平面是可以添加到任何远程实体的功能。Cut planes are a feature that can be added to any remote entity. 最常见的方案是,创建一个与任何网格数据都不关联的新远程实体来保存剪切平面组件。Most commonly, you create a new remote entity that's not associated with any mesh data to hold the cut plane component. 剪切平面的位置和方向取决于其附加到的远程实体的位置和方向。The position and orientation of the cut plane are determined by the position and orientation of the remote entity it's attached to.

我们将创建一个脚本,该脚本将自动创建远程实体、添加剪切平面组件并同步本地对象与剪切平面实体的转换。We'll create a script that automatically creates a remote entity, adds a cut plane component, and syncs the transform of a local object with the cut plane entity. 然后,可以使用 CutPlaneViewController 将剪切平面包装在一个接口中,从而允许我们对其进行操作。Then, we can use the CutPlaneViewController to wrap the cut plane in an interface that will allow us to manipulate it.

  1. 创建一个名为 RemoteCutPlane 的新脚本,并将其代码替换为以下内容:Create a new script named RemoteCutPlane and replace its code with the code below:

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

    该代码扩展了 Tutorial Assets 中包含的 BaseRemoteCutPlane 类 。This code extends the BaseRemoteCutPlane class included in the Tutorial Assets. 与远程渲染的模型相似,此脚本附加并侦听远程协调器中的 RemoteRenderingState 更改。Similarly to the remotely rendered model, this script attaches and listens for RemoteRenderingState changes from the remote coordinator. 当协调器达到 RuntimeConnected 状态时,它将按预期自动尝试连接。When the coordinator reaches the RuntimeConnected state, it will try to automatically connect if it's supposed to. 我们还将跟踪一个 CutPlaneComponent 变量。There's also a CutPlaneComponent variable we'll be tracking. 这是与远程会话中的剪切平面同步的 Azure 远程渲染组件。This is the Azure Remote Rendering component that syncs with the cut plane in the remote session. 让我们看看需要执行哪些操作来创建剪切平面。Let's take a look at what we need to do to create the cut plane.

  2. CreateCutPlane() 方法替换为下面的完整版本:Replace the CreateCutPlane() method with the completed version below:

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

    此处我们将创建一个远程实体,并将其绑定到本地 GameObject。Here we're creating a remote entity and binding it to a local GameObject. 我们通过将 SyncEveryFrame 设置为 true,确保远程实体将其“转换”同步到本地转换。We ensure that the remote entity will have its transform synced to the local transform by setting SyncEveryFrame to true. 然后,使用 CreateComponent 调用将 CutPlaneComponent 添加到远程对象。Then, we use the CreateComponent call to add a CutPlaneComponent to the remote object. 最后,我们使用在 MonoBehaviour 顶部定义的设置来配置剪切平面。Finally, we configure the cut plane with the settings defined at the top of the MonoBehaviour. 让我们看看通过实现 DestroyCutPlane() 方法来清理剪切平面需要执行哪些操作。Let's see what it takes to clean up a cut plane by implementing the DestroyCutPlane() method.

  3. DestroyCutPlane() 方法替换为下面的完整版本:Replace the DestroyCutPlane() method with the completed version below:

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

由于远程对象非常简单,并且我们只清理远程端(并保留本地对象),因此,只需在远程对象上调用 Destroy 并清除对其的引用即可。Since the remote object is fairly simple and we're only cleaning up the remote end (and keeping our local object), it's straightforward to just call Destroy on the remote object and clear our reference to it.

AppMenu 包含一个视图控制器,该控制器自动附加到剪切平面,并可与之进行交互。The AppMenu includes a view controller that will automatically attach to your cut plane and allow you to interact with it. 不需要使用 AppMenu 或任何视图控制器,但是它们可以带来更好的体验。It's not required that you use AppMenu or any of the view controllers, but they make for a better experience. 现在,测试剪切平面及其视图控制器。Now test the cut plane and its view controller.

  1. 在场景中创建新的空 GameObject 并将其命名为 CutPlane。Create a new, empty GameObject in the scene and name it CutPlane.

  2. 将 RemoteCutPlane 组件添加到 CutPlane GameObject 。Add the RemoteCutPlane component to the CutPlane GameObject.

    剪切平面组件配置

  3. 在 Unity 编辑器中按“播放”以加载并连接到远程会话。Press Play in the Unity Editor to load and connect to a remote session.

  4. 使用 MRTK 的手势模拟,抓取并旋转(按 Ctrl 旋转)CutPlane,使其在场景中移动。Using MRTK's hand simulation, grab and rotate (hold Ctrl to rotate) the CutPlane to move it around the scene. 观察它切分至 TestModel 内部并显示内部组件。Watch it slice into the TestModel to reveal internal components.

剪切平面示例

配置远程照明Configuring the remote lighting

远程渲染会话支持丰富完善的照明选项The remote rendering session supports a full spectrum of lighting options. 我们将为天空纹理创建脚本,并为两种 Unity 光线类型创建一个简单贴图,以用于远程渲染。We'll create scripts for the Sky Texture and a simple map for two Unity light types to use with remote rendering.

天空纹理Sky Texture

更改天空纹理时,有许多内置的立体贴图可供选择。There are a number of built-in Cubemaps to choose from when changing the sky texture. 这些贴图加载到会话中,并应用于天空纹理。These are loaded into the session and applied to the sky texture. 也可以加载自己的纹理,作为天空光线使用。It's also possible to load in your own textures to use as a sky light.

我们将创建一个 RemoteSky 脚本,该脚本含有一系列内置的、“加载参数”形式的可用立体贴图。We'll create a RemoteSky script that has a list of the built-in available Cubemaps in the form of load parameters. 然后,我们将允许用户选择并加载其中一个。Then, we'll allow the user to select and load one of the options.

  1. 创建名为 RemoteSky 的新脚本,并将其全部内容替换为以下代码:Create a new script named RemoteSky and replace its entire contents with the code below:

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

    这段代码最重要的部分只有几行:The most important part of this code is just a few lines:

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

    此处我们通过将要使用的纹理从内置 Blob 存储加载到会话中来获取对它的引用。Here, we get a reference to the texture to use by loading it into the session from the built-in blob storage. 然后,我们只需要将该纹理分配给会话的 SkyReflectionTexture 即可应用它。Then, we only need to assign that texture to the session's SkyReflectionTexture to apply it.

  2. 在场景中创建空 GameObject 并将其命名为 SkyLight。Create an empty GameObject in your scene and name it SkyLight.

  3. 将 RemoteSky 脚本添加到 SkyLight GameObject 。Add the RemoteSky script to your SkyLight GameObject.

    可以通过使用 AvailableCubemaps 中定义的字符串键之一调用 SetSky 来切换天空光线。Switching between sky lights can be done by calling SetSky with one of the string keys defined in AvailableCubemaps. AppMenu 中内置的视图控制器将自动创建按钮并挂钩其事件,以使用各自的键调用 SetSkyThe view controller built into AppMenu automatically creates buttons and hooks up their events to call SetSky with their respective key.

  4. 在 Unity 编辑器中按“播放”并授权连接。Press Play in the Unity Editor and authorize a connection.

  5. 将本地运行时连接到远程会话后,请导航至“AppMenu”->“会话工具”->“远程天空”,以浏览不同的天空选项,看看它们可以对 TestModel 提供什么效果 。After connecting the local runtime to a remote session, navigate AppMenu -> Session Tools -> Remote Sky to explore the different sky options and see how they affect the TestModel.

场景光线Scene Lights

远程光线包括:点光、投射光和定向光。Remote scene lights include: point, spot, and directional. 与上面创建的“剪切平面”相似,这些场景光是具有附加组件的远程实体。Similar to the Cut Plane we created above, these scene lights are remote entities with components attached to them. 为远程场景提供照明时的一个重要注意事项是,应尝试匹配本地场景中的照明。An important consideration when lighting your remote scene is attempting to match the lighting in your local scene. 此策略并非总是可行的,因为 HoloLens 2 的许多 Unity 应用程序都不对本地渲染的对象使用基于物理的渲染。This strategy isn't always possible because many Unity applications for the HoloLens 2 do not use physically-based rendering for locally rendered objects. 但在某种程度上,我们可以模拟 Unity 的较简单的默认照明。However, to a certain level, we can simulate Unity's simpler default lighting.

  1. 创建一个名为 RemoteLight 的新脚本,并将其代码替换为以下内容:Create a new script named RemoteLight and replace its code with the code below:

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

    此脚本创建不同类型的远程光,具体取决于脚本附加到的本地 Unity 光线的类型。This script creates different types of remote lights depending on the type of local Unity light the script is attached to. 远程光线将复制本地光线的位置、旋转、颜色和强度。The remote light will duplicate the local light in its: position, rotation, color, and intensity. 如果可能,远程光线还设置其他配置。Where possible, the remote light will also set additional configuration. 这并不是精确匹配,因为 Unity 光线并非 PBR 光线。This isn't a perfect match, since Unity lights are not PBR lights.

  2. 在场景中查找 DirectionalLight GameObject。Find the DirectionalLight GameObject in your scene. 如果已从场景中删除默认的 DirectionalLight:请从顶部菜单栏中选择“GameObject”->“Light”->“DirectionalLight”以在场景中创建新的光线。If you've removed the default DirectionalLight from your scene: from the top menu bar select GameObject -> Light -> DirectionalLight to create a new light in your scene.

  3. 选择 DirectionalLight GameObject,然后使用“添加组件”按钮添加 RemoteLight 脚本 。Select the DirectionalLight GameObject and using the Add Component button, add the RemoteLight script.

  4. 由于此脚本实现了基类 BaseRemoteLight,因此可以使用提供的 AppMenu 视图控制器与远程光线进行交互。Because this script implements the base class BaseRemoteLight, you can use the provided AppMenu view controller to interact with the remote light. 导航到“AppMenu”->“会话工具”->“定向光”。Navigate to AppMenu -> Session Tools -> Directional Light.

    备注

    为简单起见,AppMenu 中的 UI 仅限于单个定向光。The UI in AppMenu has been limited to a single directional light for simplicity. 然而,我们依然建议添加点光,并将 RemoteLight 脚本附加到其上。However, it's still possible and encouraged to add point lights and attach the RemoteLight script to them. 可以通过在编辑器中编辑 Unity 光线的属性来修改这些附加光线。Those additional lights can be modified by editing the properties of the Unity light in the editor. 将需要使用检查器中的 RemoteLight 上下文菜单手动同步对远程光线的本地更改:You will need to manually sync the local changes to the remote light using the RemoteLight context menu in the inspector:

    远程光线手动同步

  5. 在 Unity 编辑器中按“播放”并授权连接。Press Play in the Unity Editor and authorize a connection.

  6. 将运行时连接到远程会话后,请定位并对准照相机(使用 WASD 并右键单击 + 移动鼠标)以确保视野中出现定向光视图控制器。After connecting your runtime to a remote session, position and aim your camera (use WASD and right click + mouse move) to have the directional light view controller in view.

  7. 使用远程光视图控制器修改光线的属性。Use the remote light view controller to modify the light's properties. 使用 MRTK 的手势模拟,抓取并旋转(按 Ctrl 旋转)定向光,查看该操作对场景光线的影响。Using MRTK's hand simulation, grab and rotate (hold Ctrl to rotate) the directional light to see the effect on the scene's lighting.

    定向光

编辑材料Editing materials

可以修改远程渲染的材料以提供其他视觉效果,可以微调渲染模型的视觉对象或向用户提供其他反馈。Remotely rendered materials can be modified to provide additional visual effects, fine-tune the visuals of rendered models, or provide additional feedback to users. 修改材料有多种方式和原因。There are many ways and many reasons to modify a material. 此处,我们将向你展示如何更改材料的反照率颜色以及如何更改 PBR 材料的粗糙度和金属性。Here, we will show you how to change a material's albedo color and change a PBR material's roughness and metalness.

备注

在许多情况下,如果可以使用 HierarchicalStateOverrideComponent 来实现功能或效果,则最好使用它而不是修改材料。In many cases, if a feature or effect can be implemented using a HierarchicalStateOverrideComponent, it's ideal to use that instead of modifying the material.

我们将创建一个脚本,该脚本接受目标实体并配置一些 OverrideMaterialProperty 对象以更改目标实体材料的属性。We'll create a script that accepts a target Entity and configures a few OverrideMaterialProperty objects to change the properties of the target Entity's material. 我们首先获取目标实体的 MeshComponent,其中包含一系列网格上使用的材料。We start by getting the target Entity's MeshComponent, which contains a list of materials used on the mesh. 为简单起见,我们只使用找到的第一个材料。For simplicity, we'll just use the first material found. 这一简单的策略可能会因内容的创作方式而非常容易失败,因此你可能需要采用更复杂的方法来选择合适的材料。This naive strategy can fail very easily depending on how the content was authored, so you'd likely want to take a more complex approach to select the appropriate material.

在材料中,可以访问反照率等常用值。From the material, we can access common values like albedo. 首先,需要将材料强制转换为适当的类型(PbrMaterialColorMaterial),以检索其值,如 GetMaterialColor 方法所示。First the materials need to be cast in their appropriate type, PbrMaterial or ColorMaterial, to retrieve their values, as seen in the GetMaterialColor method. 只要引用了所需的材料,只需设置这些值,ARR 就会执行本地材料属性与远程材料之间的同步。Once we have a reference to the desired material, just set the values, and ARR will handle the syncing between the local material properties and the remote material.

  1. 创建名为 EntityMaterialController 的脚本,并将其内容替换为以下代码:Create a script named EntityMaterialController and replace its contents with the following code:

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

OverrideMaterialProperty 类型应具有足够的灵活性,以允许根据需要更改其他一些材料值。The OverrideMaterialProperty type should be flexible enough to allow for a few other material values to be changed, if desired. OverrideMaterialProperty 类型跟踪某个替代的状态,维护新旧值,并使用委托来设置替代。The OverrideMaterialProperty type tracks the state of an override, maintains the old and new value, and uses a delegate to set the override. ColorOverride 为例:As an example, look at the ColorOverride:

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

这将创建一个新的 OverrideMaterialProperty,其中替代将包装类型 ColorThis is creating a new OverrideMaterialProperty where the override will wrap the type Color. 在创建替代时,提供当前或原始颜色。We provide the current or original color at the time the override is created. 并为它指定要操作的 ARR 材料。We also give it an ARR material to act on. 最后,提供一个将应用替代的委托。Finally, a delegate is provided that will apply the override. 委托是一种接受 ARR 材料和替代包装的类型的方法。The delegate is a method that accepts an ARR material and the type the override wraps. 此方法是了解 ARR 如何调整材料值的最重要的部分。This method is the most important part of understanding how ARR adjusts material values.

ColorOverride 使用 ApplyMaterialColor 方法来完成其工作:The ColorOverride uses the ApplyMaterialColor method to do its work:

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

此代码接受材料和颜色。This code accepts a material and a color. 它会检查材料的类型,然后对材料进行强制转换以应用颜色。It checks to see what kind of material it is then does a cast of the material to apply the color.

RoughnessOverrideMetalnessOverride 的工作方式相似,即使用 ApplyRoughnessValueApplyMetalnessValue 方法进行工作。The RoughnessOverride and MetalnessOverride work similarly - using the ApplyRoughnessValue and ApplyMetalnessValue methods to do their work.

接下来,让我们测试材料控制器。Next, let's test the material controller.

  1. 将 EntityMaterialController 脚本添加到 TestModel GameObject 。Add the EntityMaterialController script to your TestModel GameObject.
  2. 在 Unity 中按下“播放”以启动场景并连接到 ARR。Press Play in Unity to start the scene and connect to ARR.
  3. 将运行时连接到远程会话并加载模型后,导航到“AppMenu”->“模型工具”->“编辑材料”After connecting your runtime to a remote session and loading the model, navigate to AppMenu -> Model Tools -> Edit Material
  4. 使用模拟手势单击“TestModel”,从模型中选择一个实体。Select an Entity from the model by using the simulated hands to click the TestModel.
  5. 确认材料视图控制器(“AppMenu”->“模型工具”->“编辑材料”)已更新到目标实体。Confirm that the material view controller (AppMenu->Model Tools->Edit Material) has updated to the targeted Entity.
  6. 使用材料视图控制器调整目标实体上的材料。Use the material view controller to adjust the material on the targeted Entity.

由于我们只修改了网格的第一种材料,因此你可能看不出材料的变化。Since we're only modifying the first material of the mesh, you may not see the material changing. 使用分层替代 SeeThrough 来查看要更改的材料是否在网格内。Use the hierarchical override SeeThrough to see if the material you're changing is inside the mesh.

材料编辑示例

后续步骤Next steps

祝贺你!Congratulations! 现在,你已经实现了 Azure 远程渲染的所有核心功能。You've now implemented all the core functionality of Azure Remote Rendering. 下一章将介绍如何保护 Azure 远程渲染和 Blob 存储。In the next chapter, we'll learn about securing your Azure Remote Rendering and Blob storage. 这将是发布使用 Azure 远程渲染的商业应用程序的第一步。These will be the first steps to releasing a commercial application that uses Azure Remote Rendering.