你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

教程:查看远程渲染的模型

在本教程中,你将了解如何执行以下操作:

  • 预配 Azure 远程渲染 (ARR) 实例
  • 创建和停止渲染会话
  • 重用现有渲染会话
  • 连接会话以及从会话断开连接
  • 将模型载入渲染会话

先决条件

本教程需要:

预配 Azure 远程渲染 (ARR) 实例

若要获取对 Azure 远程渲染服务的访问权限,首先需要创建一个帐户

创建新的 Unity 项目

提示

ARR 示例存储库 包含一个已完成了所有教程的项目,可将其用作参考。 在 Unity\Tutorial-Complete 中查找完整的 Unity 项目。

在 Unity Hub 中创建一个新项目。 本示例假设在名为 RemoteRendering 的文件夹中创建项目。

Screenshot of Unity Hub showing the Create a New Unity Project dialog. The panel 3D is selected.

包括 Azure 远程渲染和 OpenXR 包

按照说明将 Azure 远程渲染和 OpenXR 包添加到 Unity 项目。

注意

如果 Unity 在导入 OpenXR 包后显示警告对话框,询问是否为新的输入系统启用本机平台后端,则立即单击“”。 你将在后面的步骤中启用它。

配置相机

  1. 选择“主相机”节点。

  2. 右键单击“转换”组件并选择“重置”选项,打开上下文菜单:

    Screenshot of the Unity inspector for a Transform component. The context menu is opened and Reset is selected.

  3. 将“清除标志”设置为“纯色”

  4. 将“背景”设置为“黑色”(#000000),并将 alpha (A) 设置为完全透明 (0)

    Screenshot of the Unity Color wheel dialog. The color is set to 0 for all R G B A components.

  5. 将“剪裁平面”设置为“近距 = 0.1”、“远距 = 20”。 此设置意味着,渲染功能将剪裁 10 厘米以内或者 20 米以外的几何结构。

    Screenshot of the Unity inspector for a Camera component.

调整项目设置

  1. 打开“编辑”>“项目设置...”

  2. 在左侧列表菜单中,选择“质量”

    1. 将所有平台的“默认质量水平”更改为“低”。 此设置可以更高效地渲染本地内容,并且不会影响远程渲染内容的质量。

      Screenshot of the Unity Project Settings dialog. The Quality entry is selected in the list on the left. The context menu for the default quality level is opened on the right. The low entry is selected.

    注意

    在本教程的范围内,我们坚持使用 Unity 内置渲染器管道。 若要使用通用渲染器管道,请参阅 Unity 渲染器管道,了解其他设置步骤。

  3. 从左侧列表菜单中选择“XR 插件管理”

    1. 单击“安装 XR 插件管理”按钮。
    2. 选择“通用 Windows 平台设置”选项卡,以 Windows 图标表示。
    3. 选择“插件提供程序”下的“Open XR”复选框
    4. 如果一个对话框打开,要求你为新的输入系统启用本机平台后端,请选择“”。

    Screenshot of the Unity Project Settings dialog. The XR Plug-in Management entry is selected in the list on the left. The tab with the windows logo is highlighted on the right. The Open XR checkbox below it is also highlighted.

    注意

    如果禁用 Microsoft HoloLens 功能组,则项目中会缺少 Windows 混合现实 OpenXR 插件。 按照说明添加 Azure 远程渲染和 OpenXR 包以安装它。

  4. 在左侧列表菜单中,选择“OpenXR”

    1. 将“深度提交模式”设置为“深度 16 位”
    2. 将“Microsoft 手部交互配置文件”添加到“交互配置文件”。
    3. 启用以下 OpenXR 功能:
      • Azure 远程渲染
      • 手部跟踪
      • 混合现实功能
      • 运动控制器模型

    Screenshot of the Unity Project Settings dialog. The Open XR subentry is selected in the list on the left. Highlights on the right side are placed on the Depth Submission Mode, Interaction Profiles, and Open XR feature settings.

    注意

    如果没有看到所需的 OpenXR 功能列出,则项目中缺少 Windows 混合现实 OpenXR 插件。 按照说明添加 Azure 远程渲染和 OpenXR 包以安装它。

  5. 在左侧列表菜单中,选择“播放机”

    1. 选择“通用 Windows 平台设置”选项卡,以 Windows 图标表示。
    2. 展开“其他设置”
    3. 在“渲染”下,将“颜色空间”更改为“线性”,并按要求重启 Unity。
    4. 在“配置”下,将“活动输入处理”更改为“两者”,并按要求重启 Unity。 Screenshot of the Unity Project Settings dialog. The Player entry is selected in the list on the left. Highlights on the right side are placed on the tab with the Windows logo, the Color Space setting, and the Active input Handling setting.
    5. 展开“发布设置”
    6. 向下滚动到“功能”并选择:
      • InternetClient
      • InternetClientServer
      • SpatialPerception
      • PrivateNetworkClientServer(可选)。 若要将 Unity 远程调试器连接到设备,请选择此选项。
    7. 在“支持的设备系列”下,启用“Holographic”和“桌面”Screenshot of the Unity Project Settings dialog. The Player entry is selected in the list on the left. Highlights on the right side are placed on the Capabilities and the Supported Device Families settings.
  6. 关闭或停靠“项目设置”面板

  7. 打开“文件”->“生成设置”

    1. 选择“通用 Windows 平台”
    2. 配置你的设置以匹配下面的设置
    3. 按“切换平台”按钮。
      Screenshot of the Unity Build Settings dialog. The Universal Windows Platform entry is selected in the list on the left. Highlights on the right side are placed on the settings dropdown boxes and the Switch Platform button.
  8. Unity 更改平台后,请关闭生成面板。

验证项目设置

执行以下步骤来验证项目设置是否正确。

  1. 从 Unity 编辑器工具栏上的“RemoteRendering”菜单中选择“ValidateProject”菜单项 。

  2. 查看“项目验证程序”窗口是否存在错误并按需修复项目设置。

    Screenshot of the Unity Project Validator dialog. The dialog shows a list of required, recommended, and development rules that are all successful checked.

注意

如果在项目中使用 MRTK 并启用相机子系统,MRTK 将替代你应用到相机的手动更改。 这包括来自 ValidateProject 工具的修补程序。

创建一个脚本用于协调 Azure 远程渲染连接和状态

需要完成四个基本阶段才能显示远程渲染的模型,如下面的流程图所示。 必须按顺序完成每个阶段。 下一步是创建一个脚本,该脚本将管理应用程序状态并完成每个必需的阶段。

Diagram of the four stages required to load a model.

  1. 在“项目”窗格中的“资产”文件夹下,创建名为“RemoteRenderingCore”的新文件夹。 然后在“RemoteRenderingCore”中,创建名为“Scripts”的文件夹 。

  2. 创建一个名为 RemoteRenderingCoordinator 的新 C# 脚本。 项目应如下所示:

    Screenshot of Unity Project hierarchy containing the new script.

    此协调器脚本跟踪和管理远程渲染状态。 值得注意的是,其中有一些代码用于维护状态,向其他组件公开功能,触发事件以及存储不直接与 Azure 远程渲染相关的特定于应用程序的数据。 以下面的代码为起点,我们将在本教程的后面介绍和实现特定的 Azure 远程渲染代码。

  3. 在代码编辑器中打开 RemoteRenderingCoordinator 并将其全部内容替换为以下代码:

// 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 System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Events;

#if UNITY_WSA
using UnityEngine.XR.WSA;
#endif

/// <summary>
/// Remote Rendering Coordinator is the controller for all Remote Rendering operations.
/// </summary>

// Require the GameObject with a RemoteRenderingCoordinator to also have the ARRServiceUnity component
[RequireComponent(typeof(ARRServiceUnity))]
public class RemoteRenderingCoordinator : MonoBehaviour
{
    public enum RemoteRenderingState
    {
        NotSet,
        NotInitialized,
        NotAuthorized,
        NoSession,
        ConnectingToExistingRemoteSession,
        ConnectingToNewRemoteSession,
        RemoteSessionReady,
        ConnectingToRuntime,
        RuntimeConnected
    }

    public static RemoteRenderingCoordinator instance;

    // Account
    // RemoteRenderingDomain must be '<region>.mixedreality.azure.com' - if no '<region>' is specified, connections will fail
    // For most people '<region>' is either 'westus2' or 'westeurope'
    [SerializeField]
    private string remoteRenderingDomain = "westus2.mixedreality.azure.com";
    public string RemoteRenderingDomain
    {
        get => remoteRenderingDomain.Trim();
        set => remoteRenderingDomain = value;
    }

    [Header("Development Account Credentials")]
    [SerializeField]
    private string accountDomain = "<enter your account domain here>";
    public string AccountDomain
    {
        get => accountDomain.Trim();
        set => accountDomain = value;
    }

    [SerializeField]
    private string accountId = "<enter your account id here>";
    public string AccountId {
        get => accountId.Trim();
        set => accountId = value;
    }

    [SerializeField]
    private string accountKey = "<enter your account key here>";
    public string AccountKey {
        get => accountKey.Trim();
        set => accountKey = value;
    }

    // These settings are important. All three should be set as low as possible, while maintaining a good user experience
    // See the documentation around session management and the technical differences in session VM size
    [Header("New Session Defaults")]

    public RenderingSessionVmSize renderingSessionVmSize = RenderingSessionVmSize.Standard;
    public uint maxLeaseHours = 0;
    public uint maxLeaseMinutes = 20;

    [Header("Other Configuration")]

    [Tooltip("If you have a known active SessionID, you can fill it in here before connecting")]
    [SerializeField]
    private string sessionIDOverride;
    public string SessionIDOverride {
        get => sessionIDOverride.Trim();
        set => sessionIDOverride = value;
    }

    // When Automatic Mode is true, the coordinator will attempt to automatically proceed through the process of connecting and loading a model
    public bool automaticMode = true;

    public event Action RequestingAuthorization;
    public UnityEvent OnRequestingAuthorization = new UnityEvent();

    public event Action AuthorizedChanged;
    public UnityEvent OnAuthorizationChanged = new UnityEvent();
    private bool authorized;
    public bool Authorized
    {
        get => authorized;
        set
        {
            if (value == true) //This is a one-way value, once we're authorized it lasts until the app is shutdown.
            {
                authorized = value;
                AuthorizedChanged?.Invoke();
            }
        }
    }

    public delegate Task<SessionConfiguration> AccountInfoGetter();

    public static AccountInfoGetter ARRCredentialGetter
    {
        private get;
        set;
    }

    private RemoteRenderingState currentCoordinatorState = RemoteRenderingState.NotSet;
    public RemoteRenderingState CurrentCoordinatorState
    {
        get => currentCoordinatorState;
        private set
        {
            if (currentCoordinatorState != value)
            {
                currentCoordinatorState = value;
                Debug.LogFormat(LogType.Log, LogOption.NoStacktrace, null, "{0}", $"State changed to: {currentCoordinatorState}");
                CoordinatorStateChange?.Invoke(currentCoordinatorState);
            }
        }
    }

    public static event Action<RemoteRenderingState> CoordinatorStateChange;

    public static RenderingSession CurrentSession => instance?.ARRSessionService?.CurrentActiveSession;

    private ARRServiceUnity arrSessionService;

    private ARRServiceUnity ARRSessionService
    {
        get
        {
            if (arrSessionService == null)
                arrSessionService = GetComponent<ARRServiceUnity>();
            return arrSessionService;
        }
    }

    private async Task<SessionConfiguration> GetDevelopmentCredentials()
    {
        Debug.LogWarning("Using development credentials! Not recommended for production.");
        return await Task.FromResult(new SessionConfiguration(AccountDomain, RemoteRenderingDomain, AccountId, AccountKey));
    }

    /// <summary>
    /// Keep the last used SessionID, when launching, connect to this session if its available
    /// </summary>
    private string LastUsedSessionID
    {
        get
        {
            if (!string.IsNullOrEmpty(SessionIDOverride))
                return SessionIDOverride;

            if (PlayerPrefs.HasKey("LastUsedSessionID"))
                return PlayerPrefs.GetString("LastUsedSessionID");
            else
                return null;
        }
        set
        {
            PlayerPrefs.SetString("LastUsedSessionID", value);
        }
    }

    public void Awake()
    {
        //Forward events to Unity events
        RequestingAuthorization += () => OnRequestingAuthorization?.Invoke();
        AuthorizedChanged += () => OnAuthorizationChanged?.Invoke();

        //Attach to event
        AuthorizedChanged += RemoteRenderingCoordinator_AuthorizedChanged;

        if (instance == null)
            instance = this;
        else
            Destroy(this);

        CoordinatorStateChange += AutomaticMode;

        CurrentCoordinatorState = RemoteRenderingState.NotInitialized;
    }

    private void RemoteRenderingCoordinator_AuthorizedChanged()
    {
        if (CurrentCoordinatorState != RemoteRenderingState.NotAuthorized)
            return; //This isn't valid from any other state than NotAuthorized


        //We just became authorized to connect to Azure
        InitializeSessionService();
    }

    /// <summary>
    /// Automatic mode attempts to automatically progress through the connection and loading steps. Doesn't handle error states.
    /// </summary>
    /// <param name="currentState">The current state</param>
    private async void AutomaticMode(RemoteRenderingState currentState)
    {
        if (!automaticMode)
            return;

        //Add a small delay for visual effect
        await Task.Delay(1500);
        switch (currentState)
        {
            case RemoteRenderingState.NotInitialized:
                InitializeARR();
                break;
            case RemoteRenderingState.NotAuthorized:
                RequestAuthorization();
                break;
            case RemoteRenderingState.NoSession:
                JoinRemoteSession();
                break;
            case RemoteRenderingState.RemoteSessionReady:
                ConnectRuntimeToRemoteSession();
                break;
        }
    }

    /// <summary>
    /// Initializes ARR, associating the main camera
    /// Note: This must be called on the main Unity thread
    /// </summary>
    public void InitializeARR()
    {
        //Implement me
    }

    /// <summary>
    /// Create a new remote session manager
    /// If the ARRCredentialGetter is set, use it as it, otherwise use the development credentials 
    /// </summary>
    public async void InitializeSessionService()
    {
        //Implement me
    }

    /// <summary>
    /// Trigger the event for checking authorization, respond to this event by prompting the user for authentication
    /// If authorized, set Authorized = true
    /// </summary>
    public void RequestAuthorization()
    {
        RequestingAuthorization?.Invoke();
    }

    public void BypassAuthorization()
    {
        Authorized = true;
    }

    /// <summary>
    /// Attempts to join an existing session or start a new session
    /// </summary>
    public async void JoinRemoteSession()
    {
        //Implement me
    }

    public async void StopRemoteSession()
    {
        //Implement me
    }

    private async Task<bool> IsSessionAvailable(string sessionID)
    {
        bool sessionAvailable = false;
        try
        {
            RenderingSessionPropertiesArrayResult result = await ARRSessionService.Client.GetCurrentRenderingSessionsAsync();
            if (result.ErrorCode == Result.Success)
            {
                RenderingSessionProperties[] properties = result.SessionProperties;
                if (properties != null)
                {
                    sessionAvailable = properties.Any(x => x.Id == sessionID && (x.Status == RenderingSessionStatus.Ready || x.Status == RenderingSessionStatus.Starting));
                }
            }
            else
            {
                Debug.LogError($"Failed to get current rendering sessions. Error: {result.Context.ErrorMessage}");
            }
        }
        catch (RRException ex)
        {
            Debug.LogError($"Failed to get current rendering sessions. Error: {ex.Message}");
        }
        return sessionAvailable;
    }

    /// <summary>
    /// Connects the local runtime to the current active session, if there's a session available
    /// </summary>
    public async void ConnectRuntimeToRemoteSession()
    {
        //Implement me
    }

    public void DisconnectRuntimeFromRemoteSession()
    {
        //Implement me
    }

    /// <summary>
    /// The session must have its runtime pump updated.
    /// The Connection.Update() will push messages to the server, receive messages, and update the frame-buffer with the remotely rendered content.
    /// </summary>
    private void LateUpdate()
    {
        ARRSessionService?.CurrentActiveSession?.Connection?.Update();
    }

    /// <summary>
    /// Loads a model into the remote session for rendering
    /// </summary>
    /// <param name="modelPath">The model's path</param>
    /// <param name="progress">A call back method that accepts a float progress value [0->1]</param>
    /// <param name="parent">The parent Transform for this remote entity</param>
    /// <returns>An awaitable Remote Rendering Entity</returns>
    public async Task<Entity> LoadModel(string modelPath, UnityEngine.Transform parent = null, Action<float> progress = null)
    {
        //Implement me
        return null;
    }

    private async void OnRemoteSessionStatusChanged(ARRServiceUnity caller, RenderingSession session)
    {
        var properties = await session.GetPropertiesAsync();

        switch (properties.SessionProperties.Status)
        {
            case RenderingSessionStatus.Error:
            case RenderingSessionStatus.Expired:
            case RenderingSessionStatus.Stopped:
            case RenderingSessionStatus.Unknown:
                CurrentCoordinatorState = RemoteRenderingState.NoSession;
                break;
            case RenderingSessionStatus.Starting:
                CurrentCoordinatorState = RemoteRenderingState.ConnectingToNewRemoteSession;
                break;
            case RenderingSessionStatus.Ready:
                CurrentCoordinatorState = RemoteRenderingState.RemoteSessionReady;
                break;
        }
    }

    private void OnLocalRuntimeStatusChanged(ConnectionStatus status, Result error)
    {
        switch (status)
        {
            case ConnectionStatus.Connected:
                CurrentCoordinatorState = RemoteRenderingState.RuntimeConnected;
                break;
            case ConnectionStatus.Connecting:
                CurrentCoordinatorState = RemoteRenderingState.ConnectingToRuntime;
                break;
            case ConnectionStatus.Disconnected:
                CurrentCoordinatorState = RemoteRenderingState.RemoteSessionReady;
                break;
        }
    }
}

创建 Azure 远程渲染 GameObject

远程渲染协调器及其所需的脚本 (ARRServiceUnity) 都是必须附加到场景中的 GameObject 上的 MonoBehaviours。 ARR 提供了 ARRServiceUnity 脚本,用于公开 ARR 的许多功能,这些功能用于连接和管理远程会话。

  1. 在场景中创建一个新的 GameObject(Ctrl+Shift+N 或 GameObject -> Create Empty),并将其命名为 RemoteRenderingCoordinator。
  2. 将 RemoteRenderingCoordinator 脚本添加到 RemoteRenderingCoordinator GameObject 中。
    Screenshot of the Unity Add Component dialog. The search text field contains the text RemoteRenderingCoordinator.
  3. 确认 ARRServiceUnity 脚本(在检查器中显示为 Service)已自动添加到 GameObject 中 。 这里解释一下,这是将 [RequireComponent(typeof(ARRServiceUnity))] 放在 RemoteRenderingCoordinator 脚本顶部所产生的效果。
  4. 将 Azure 远程渲染凭据、帐户域和远程渲染域添加到协调器脚本:
    Screenshot of the Unity inspector of the Remote Rendering Coordinator Script. The credential input fields are highlighted.

初始化 Azure 远程渲染

现在我们有了协调器的框架,我们将从初始化远程渲染开始,实现这四个阶段中的每个阶段。

Diagram of the four stages required to load a model. The first stage

“初始化”可指示 Azure 远程渲染使用哪个相机对象进行渲染,并促使状态机进入“NotAuthorized”状态 。 此状态意味着它已初始化,但尚未获得连接会话的授权。 由于启动 ARR 会话会产生费用,因此我们需要确认用户是否希望继续。

进入 NotAuthorized 状态时,将调用 CheckAuthorization,这将调用 RequestingAuthorization 事件并确定要使用的帐户凭据(AccountInfo 在类顶部附近定义,并使用你在上述步骤中通过 Unity 检查器定义的凭据) 。

注意

ARR 不支持运行时重新编译。 在激活播放模式时修改并保存该脚本可能会导致 Unity 冻结,且需要通过任务管理器强制关闭。 在编辑脚本之前,请务必确保已停止播放模式。

  1. 将 InitializeARR 和 InitializeSessionService 的内容替换为以下代码 :

    /// <summary>
    /// Initializes ARR, associating the main camera
    /// Note: This must be called on the main Unity thread
    /// </summary>
    public void InitializeARR()
    {
        RemoteManagerUnity.InitializeManager(new RemoteUnityClientInit  (Camera.main));
    
        CurrentCoordinatorState = RemoteRenderingState.NotAuthorized;
    }
    
    /// <summary>
    /// Create a new remote session manager
    /// If the ARRCredentialGetter is set, use it as it, otherwise use  the development credentials 
    /// </summary>
    public async void InitializeSessionService()
    {
        if (ARRCredentialGetter == null)
            ARRCredentialGetter = GetDevelopmentCredentials;
    
        var sessionConfiguration = await ARRCredentialGetter.Invoke();
    
        ARRSessionService.OnSessionStatusChanged +=     OnRemoteSessionStatusChanged;
    
        try
        {
            ARRSessionService.Initialize(sessionConfiguration);
        }
        catch (ArgumentException argumentException)
        {
            Debug.LogError(argumentException.Message);
            CurrentCoordinatorState = RemoteRenderingState. NotAuthorized;
            return;
        }
    
        CurrentCoordinatorState = RemoteRenderingState.NoSession;
    }
    

为从 NotAuthorized 状态进展到 NoSession 状态,通常会向用户呈现一个模式对话框,以便他们可以选择(我们将在另一章中进行介绍)。 现在,我们会在触发 RequestingAuthorization 事件时立即调用 ByPassAuthentication 来自动绕过授权检查。

  1. 选择 RemoteRenderingCoordinator GameObject 并找到在 RemoteRenderingCoordinator 组件的检查器中公开的 OnRequestingAuthorization Unity 事件 。

  2. 按右下方的“+”添加新事件。

  3. 将组件拖到其自己的事件上,以引用其自身。 Screenshot of the Unity inspector of the Remote Rendering Coordinator Script. The title bar of the component is highlighted and an arrow connects it to the On Requesting Authorization event.

  4. 在下拉列表中,选择“RemoteRenderingCoordinator”-“>BypassAuthorization”。
    Screenshot of the On Requesting Authorization event.

创建或加入远程会话

第二个阶段是创建或加入远程渲染会话(有关渲染会话的详细信息,请参阅远程渲染会话)。

Diagram of the four stages required to load a model. The second stage

远程会话是渲染模型的地方。 JoinRemoteSession( ) 方法将尝试加入使用 LastUsedSessionID 属性跟踪的现有会话或是在 SessionIDOverride 上有已分配的活动会话 ID 的情况下加入。 SessionIDOverride 仅用于调试目的,应只在知道会话存在并要显式连接到该会话时使用。

如果没有可用的会话,则会创建新会话。 然而,创建新会话是一个相当耗时的操作。 因此,应该尝试只在需要时创建会话,并尽可能重用它们(有关管理会话的详细信息,请参阅商业就绪:会话池、计划和最佳做法)。

提示

StopRemoteSession() 将结束活动会话。 若要避免产生不必要的费用,应在不再需要会话时将其停止。

根据可用会话,状态机现在将进展到 ConnectingToNewRemoteSession 或 ConnectingToExistingRemoteSession 。 打开现有会话或创建新会话都将触发 ARRSessionService.OnSessionStatusChanged 事件,并执行我们的 OnRemoteSessionStatusChanged 方法。 理想情况下,这会使状态机进展到 RemoteSessionReady

  1. 若要加入新会话,请修改代码,将 JoinRemoteSession( ) 和 StopRemoteSession( ) 方法替换为以下完整示例内容 :
/// <summary>
/// Attempts to join an existing session or start a new session
/// </summary>
public async void JoinRemoteSession()
{
    //If there's a session available that previously belonged to us, and it's ready, use it. Otherwise start a new session.
    RenderingSessionProperties joinResult;
    if (await IsSessionAvailable(LastUsedSessionID))
    {
        CurrentCoordinatorState = RemoteRenderingState.ConnectingToExistingRemoteSession;
        joinResult = await ARRSessionService.OpenSession(LastUsedSessionID);
    }
    else
    {
        CurrentCoordinatorState = RemoteRenderingState.ConnectingToNewRemoteSession;
        joinResult = await ARRSessionService.StartSession(new RenderingSessionCreationOptions(renderingSessionVmSize, (int)maxLeaseHours, (int)maxLeaseMinutes));
    }

    if (joinResult.Status == RenderingSessionStatus.Ready || joinResult.Status == RenderingSessionStatus.Starting)
    {
        LastUsedSessionID = joinResult.Id;
    }
    else
    {
        //The session should be ready or starting, if it's not, something went wrong
        await ARRSessionService.StopSession();
        if(LastUsedSessionID == SessionIDOverride)
            SessionIDOverride = "";
        CurrentCoordinatorState = RemoteRenderingState.NoSession;
    }
}

public async void StopRemoteSession()
{
    if (ARRSessionService.CurrentActiveSession != null)
    {
        await ARRSessionService.CurrentActiveSession.StopAsync();
    }
}

如果要通过重用会话来节省时间,请确保停用 ARRServiceUnity 组件中的“自动停止会话”选项。 请记住,即使没有人连接到会话,这也会使会话保持运行状态。 会话可以持续运行 MaxLeaseTime 指定的时间长度,此时间过后,会话将被服务器关闭(可在远程渲染协调器的“新会话默认值”下修改 MaxLeaseTime 的值) 。 另一方面,如果在断开连接时自动关闭每个会话,那么每次都必须等待新会话启动,这可能是一个漫长的过程。

注意

停止会话的操作会立即生效,且无法撤消。 停止会话后,必须创建新会话,其启动开销是相同的。

将本地运行时连接到远程会话

接下来,应用程序需要将其本地运行时连接到远程会话。

Diagram of the four stages required to load a model. The third stage

应用程序还需要侦听有关运行时与当前会话之间的连接的事件;这些状态更改将在 OnLocalRuntimeStatusChanged 中进行处理。 此代码会使状态进展至 ConnectingToRuntime。 在 OnLocalRuntimeStatusChanged 中建立连接后,状态将进展至 RuntimeConnected。 连接到运行时是与协调器本身相关的最后一种状态,这意味着应用程序已完成所有通用配置,并且已准备好开始加载和渲染模型的特定于会话的工作。

  1. 将 ConnectRuntimeToRemoteSession( ) 和 DisconnectRuntimeFromRemoteSession( ) 方法替换为以下已完成的版本 。
  2. 请务必注意 Unity 方法 LateUpdate,它正在更新当前的活动会话。 这允许当前会话发送/接收消息,并通过远程会话接收的帧更新帧缓冲区。 这对 ARR 的正常运行至关重要。
/// <summary>
/// Connects the local runtime to the current active session, if there's a session available
/// </summary>
public async void ConnectRuntimeToRemoteSession()
{
    if (ARRSessionService == null || ARRSessionService.CurrentActiveSession == null)
    {
        Debug.LogError("Not ready to connect runtime");
        return;
    }

    // Connect the local runtime to the currently connected session
    // This session is set when connecting to a new or existing session

    ARRSessionService.CurrentActiveSession.ConnectionStatusChanged += OnLocalRuntimeStatusChanged;
    await ARRSessionService.CurrentActiveSession.ConnectAsync(new RendererInitOptions());
}

public void DisconnectRuntimeFromRemoteSession()
{
    if (ARRSessionService == null || ARRSessionService.CurrentActiveSession == null || ARRSessionService.CurrentActiveSession.ConnectionStatus != ConnectionStatus.Connected)
    {
        Debug.LogError("Runtime not connected!");
        return;
    }

    ARRSessionService.CurrentActiveSession.Disconnect();
    ARRSessionService.CurrentActiveSession.ConnectionStatusChanged -= OnLocalRuntimeStatusChanged;
    CurrentCoordinatorState = RemoteRenderingState.RemoteSessionReady;
}

注意

将本地运行时连接到远程会话取决于在当前活动会话上调用 Update。 如果发现应用程序的进展从未超过 ConnectingToRuntime 状态,请确保在活动会话中定期调用 Update 。

加载模型

设置好所需的基础结构后,就可以将模型加载到远程会话并开始接收帧了。

Diagram of the four stages required to load a model. The fourth stage

LoadModel 方法的作用是接受模型路径、进度处理程序和父转换。 这些参数用于将模型加载到远程会话中,向用户更新加载进度,并基于父转换确定远程渲染模型的方向。

  1. 将 LoadModel 方法完全替换为以下代码:

    /// <summary>
    /// Loads a model into the remote session for rendering
    /// </summary>
    /// <param name="modelName">The model's path</param>
    /// <param name="parent">The parent Transform for this remote entity</param>
    /// <param name="progress">A call back method that accepts a float progress value [0->1]</param>
    /// <returns>An awaitable Remote Rendering Entity</returns>
    public async Task<Entity> LoadModel(string modelPath, UnityEngine.Transform parent = null, Action<float> progress = null)
    {
        //Create a root object to parent a loaded model to
        var modelEntity = ARRSessionService.CurrentActiveSession.Connection.CreateEntity();
    
        //Get the game object representation of this entity
        var modelGameObject = modelEntity.GetOrCreateGameObject(UnityCreationMode.DoNotCreateUnityComponents);
    
        //Ensure the entity will sync its transform with the server
        var sync = modelGameObject.GetComponent<RemoteEntitySyncObject>();
        sync.SyncEveryFrame = true;
    
        //Parent the new object under the defined parent
        if (parent != null)
        {
            modelGameObject.transform.SetParent(parent, false);
            modelGameObject.name = parent.name + "_Entity";
        }
    
        //Load a model that will be parented to the entity
        var loadModelParams = new LoadModelFromSasOptions(modelPath, modelEntity);
        var loadModelAsync = ARRSessionService.CurrentActiveSession.Connection.LoadModelFromSasAsync(loadModelParams, progress);
        var result = await loadModelAsync;
        return modelEntity;
    }
    

上述代码将执行以下步骤:

  1. 创建远程实体
  2. 创建用于表示远程实体的本地 GameObject。
  3. 配置本地 GameObject 以将其状态(即“转换”)同步到每个帧的远程实体。
  4. 将模型数据从 Blob 存储加载到远程实体。
  5. 返回父实体,供以后引用。

查看测试模型

现在,我们拥有了查看远程渲染模型所需的所有代码,并完成了远程渲染必需的所有四个阶段。 现在我们需要添加一些代码来启动模型加载过程。

Diagram of the four stages required to load a model. All stages are marked as completed.

  1. 将以下代码添加到 RemoteRenderingCoordinator 类,就在 LoadModel 方法的正下方 :

    private bool loadingTestModel = false;
    [ContextMenu("Load Test Model")]
    public async void LoadTestModel()
    {
        if(CurrentCoordinatorState != RemoteRenderingState.RuntimeConnected)
        {
            Debug.LogError("Please wait for the runtime to connect before loading the test model. Try again later.");
            return;
        }
        if(loadingTestModel)
        {
            Debug.Log("Test model already loading or loaded!");
            return;
        }
        loadingTestModel = true;
    
        // Create a parent object to use for positioning
        GameObject testParent = new GameObject("TestModelParent");
        testParent.transform.position = new Vector3(0f, 0f, 3f);
    
        // The 'built in engine path' is a special path that references a test model built into Azure Remote Rendering.
        await LoadModel("builtin://Engine", testParent.transform, (progressValue) => Debug.Log($"Loading Test Model progress: {Math.Round(progressValue * 100, 2)}%"));
    }
    

    此代码创建一个 GameObject 作为测试模型的父项。 然后,它调用 LoadModel 方法来加载模型“builtin://Engine”,这是 Azure 远程渲染中内置的资产,用于测试渲染。

  2. 保存代码。

  3. 按 Unity 编辑器中的“播放”按钮以启动连接到 Azure 远程渲染并创建新会话的过程。

  4. 在“游戏”视图中不会看到太多内容,但是,控制台将显示应用程序状态的变化。 可能会进展至 ConnectingToNewRemoteSession,并保持最多五分钟。

  5. 选择 RemoteRenderingCoordinator GameObject,以在检查器中查看其附加的脚本。 观察“服务”组件在其初始化和连接步骤中的更新。

  6. 监视控制台输出 - 等待状态更改为 RuntimeConnected。

  7. 连接运行时后,右键单击检查器中的 RemoteRenderingCoordinator 以显示上下文菜单。 然后,选择上下文菜单中的“加载测试模型”选项,该选项由上面代码的 [ContextMenu("Load Test Model")] 部分添加。

    Screenshot of the Unity inspector of the Remote Rendering Coordinator Script. Highlights instruct to first right-click on the title bar and then select Load Test Model from the context menu.

  8. 监视控制台,查看传递给 LoadModel 方法的 ProgressHandler 的输出 。

  9. 查看远程渲染的模型!

注意

远程模型永远不会显示在“场景”视图中,只显示在“游戏”视图中。 这是因为 ARR 专门从“游戏”视图照相机的角度远程渲染帧,而不感知编辑器照相机(用于渲染“场景”视图)。

后续步骤

Screenshot of Unity running the project in Play mode. A car engine is rendered in the center of the viewport.

祝贺你! 你已经创建了一个基本应用程序,该应用程序能够使用 Azure 远程渲染来查看远程渲染的模型。 在下一教程中,我们将集成 MRTK 并导入我们自己的模型。