Tutorial: Manipular modelos

Neste tutorial, ficará a saber como:

  • Adicionar limites visuais e de manipulação em torno de modelos compostos remotamente
  • Mover, rodar e dimensionar
  • Raycast com consultas espaciais
  • Adicionar animações simples para objetos compostos remotamente

Pré-requisitos

Consultar limites de objetos remotos e aplicar-se a limites locais

Para interagir com objetos remotos, precisamos de uma representação local com a qual interagir primeiro. Os limites de objetos são úteis para manipulação rápida de um objeto remoto. Os limites remotos podem ser consultados a partir do ARR, utilizando a Entidade local como referência. Os limites são consultados depois de o modelo ter sido carregado para a sessão remota.

Os limites de um modelo são definidos pela caixa que contém todo o modelo, tal como o BoxCollider do Unity, que tem um centro e tamanho definidos para os eixos x, y, z. Na verdade, vamos utilizar o BoxCollider do Unity para representar os limites do modelo remoto.

  1. Crie um novo script no mesmo diretório que RemoteRenderedModel e dê-lhe o nome RemoteBounds.

  2. Substitua o conteúdo do script pelo seguinte código:

    // Copyright (c) Microsoft Corporation. All rights reserved.
    // Licensed under the MIT License. See LICENSE in the project root for license information.
    
    using Microsoft.Azure.RemoteRendering;
    using Microsoft.Azure.RemoteRendering.Unity;
    using System;
    using UnityEngine;
    
    [RequireComponent(typeof(BaseRemoteRenderedModel))]
    public class RemoteBounds : BaseRemoteBounds
    {
        //Remote bounds works with a specific remotely rendered model
        private BaseRemoteRenderedModel targetModel = null;
    
        private RemoteBoundsState currentBoundsState = RemoteBoundsState.NotReady;
    
        public override RemoteBoundsState CurrentBoundsState
        {
            get => currentBoundsState;
            protected set
            {
                if (currentBoundsState != value)
                {
                    currentBoundsState = value;
                    BoundsStateChange?.Invoke(value);
                }
            }
        }
    
        public override event Action<RemoteBoundsState> BoundsStateChange;
    
        public void Awake()
        {
            BoundsStateChange += HandleUnityEvents;
            targetModel = GetComponent<BaseRemoteRenderedModel>();
    
            targetModel.ModelStateChange += TargetModel_OnModelStateChange;
            TargetModel_OnModelStateChange(targetModel.CurrentModelState);
        }
    
        private void TargetModel_OnModelStateChange(ModelState state)
        {
            switch (state)
            {
                case ModelState.Loaded:
                    QueryBounds();
                    break;
                default:
                    BoundsBoxCollider.enabled = false;
                    CurrentBoundsState = RemoteBoundsState.NotReady;
                    break;
            }
        }
    
        // Create an async query using the model entity
        async private void QueryBounds()
        {
            //Implement me
        }
    }
    

    Nota

    Se vir um erro no Visual Studio a afirmar que a Funcionalidade "X" não está disponível no C# 6. Utilize o idioma versão 7.0 ou superior. Estes erros podem ser ignorados com segurança. Isto está relacionado com a Geração de Projetos e Soluções do Unity.

    Este script deve ser adicionado ao mesmo GameObject que o script que implementa BaseRemoteRenderedModel. Neste caso, significa RemoteRenderedModel. Semelhante aos scripts anteriores, este código inicial processa todas as alterações de estado, eventos e dados relacionados com os limites remotos.

    Só resta um método para implementar: QueryBounds. QueryBounds obtém os limites de forma assíncrona, obtém o resultado da consulta e aplica-o ao BoxCollider local.

    O método QueryBounds é simples: envie uma consulta para a sessão de composição remota e aguarde o resultado.

  3. Substitua o método QueryBounds pelo seguinte método concluído:

    // Create a query using the model entity
    async private void QueryBounds()
    {
        var remoteBounds = targetModel.ModelEntity.QueryLocalBoundsAsync();
        CurrentBoundsState = RemoteBoundsState.Updating;
        await remoteBounds;
    
        if (remoteBounds.IsCompleted)
        {
            var newBounds = remoteBounds.Result.toUnity();
            BoundsBoxCollider.center = newBounds.center;
            BoundsBoxCollider.size = newBounds.size;
            BoundsBoxCollider.enabled = true;
            CurrentBoundsState = RemoteBoundsState.Ready;
        }
        else
        {
            CurrentBoundsState = RemoteBoundsState.Error;
        }
    }
    

    Verificamos o resultado da consulta para ver se foi bem-sucedido. Se sim, converta e aplique os limites devolvidos num formato que o BoxCollider possa aceitar.

Agora, quando o script RemoteBounds é adicionado ao mesmo objeto de jogo que o RemoteRenderedModel, é adicionado um BoxCollider se necessário e quando o modelo atinge o seu Loaded estado, os limites serão automaticamente consultados e aplicados ao BoxCollider.

  1. Com o TestModel GameObject criado anteriormente, adicione o componente RemoteBounds .

  2. Confirme que o script foi adicionado.

    Adicionar componente RemoteBounds

  3. Execute novamente a aplicação. Pouco depois de o modelo ser carregado, verá os limites do objeto remoto. Verá algo semelhante aos valores abaixo:

    Captura de ecrã que mostra o exemplo de limites de objeto remoto.

Agora, temos um BoxCollider local configurado com limites precisos no objeto Unity. Os limites permitem a visualização e interação com as mesmas estratégias que utilizaríamos para um objeto composto localmente. Por exemplo, scripts que alteram a Transformação, a física e muito mais.

Mover, rodar e dimensionar

Mover, rodar e dimensionar objetos compostos remotamente funciona da mesma forma que qualquer outro objeto do Unity. O RemoteRenderingCoordinator, no seu LateUpdate método, está a chamar Update na sessão atualmente ativa. Parte do que Update faz é sincronizar transformações de entidades de modelo local com as respetivas congéneres remotas. Para mover, rodar ou dimensionar um modelo composto remotamente, só precisa de mover, rodar ou dimensionar a transformação do GameObject que representa o modelo remoto. Aqui, vamos modificar a transformação do GameObject principal que tem o script RemoteRenderedModel anexado ao mesmo.

Este tutorial está a utilizar o MRTK para interação de objetos. A maior parte da implementação específica do MRTK para mover, rodar e dimensionar um objeto está fora do âmbito deste tutorial. Existe um controlador de vista de modelo que vem pré-configurado no AppMenu, no menu Ferramentas de Modelo .

  1. Confirme que o TestModel GameObject criado anteriormente está no local.
  2. Certifique-se de que a pré-base appMenu está no local.
  3. Prima o botão Reproduzir do Unity para reproduzir a cena e abrir o menu Ferramentas de Modelo dentro do AppMenu. Ver controlador

O AppMenu tem um submenu Ferramentas de Modelo que implementa um controlador de vista para enlace com o modelo. Quando o GameObject contém um componente RemoteBounds , o controlador de vista adicionará um componente BoundingBox , que é um componente MRTK que compõe uma caixa delimitadora à volta de um objeto com um BoxCollider. Um ObjectManipulator, que é responsável pelas interações entre mãos. Estes scripts combinados permitir-nos-ão mover, rodar e dimensionar o modelo composto remotamente.

  1. Mova o rato para o painel Jogo e clique no interior do mesmo para lhe dar foco.

  2. Com a simulação de mão do MRTK, prima sem soltar a tecla Shift esquerda.

  3. Guie a mão simulada para que o raio da mão aponte para o modelo de teste.

    Raio de mão pontiagudo

  4. Mantenha premido o botão esquerdo do rato e arraste o modelo para movê-lo.

Deverá ver o conteúdo composto remotamente mover-se juntamente com a caixa delimitadora. Poderá notar algum atraso ou atraso entre a caixa delimitadora e o conteúdo remoto. Este atraso dependerá da latência e largura de banda da Internet.

Ray cast e consultas espaciais de modelos remotos

Um colisor de caixas em torno de modelos é adequado para interagir com todo o modelo, mas não é detalhado o suficiente para interagir com partes individuais de um modelo. Para resolver este problema, podemos utilizar a conversão de raios remotos. A conversão de raios remotos é uma API fornecida pelo Azure Remote Rendering para lançar raios na cena remota e devolver resultados de sucesso localmente. Esta técnica pode ser utilizada para selecionar entidades subordinadas de um modelo grande ou obter informações de resultados como posição, superfície normal e distância.

O modelo de teste tem várias sub-entidades que podem ser consultadas e selecionadas. Por agora, a seleção irá exportar o nome da Entidade selecionada para a Consola do Unity. Consulte o capítulo Materiais, iluminação e efeitos para realçar a Entidade selecionada.

Primeiro, vamos criar um wrapper estático em torno das consultas de raios remotos. Este script irá aceitar uma posição e direção no espaço do Unity, convertê-lo nos tipos de dados aceites pelo raio remoto e devolver os resultados. O script irá utilizar a RayCastQueryAsync API.

  1. Crie um novo script chamado RemoteRayCaster e substitua os respetivos conteúdos pelo seguinte código:

    // Copyright (c) Microsoft Corporation. All rights reserved.
    // Licensed under the MIT License. See LICENSE in the project root for license information.
    
    using Microsoft.Azure.RemoteRendering;
    using Microsoft.Azure.RemoteRendering.Unity;
    using System.Linq;
    using System.Threading.Tasks;
    using UnityEngine;
    
    /// <summary>
    /// Wraps the Azure Remote Rendering RayCast queries to easily send requests using Unity data types
    /// </summary>
    public class RemoteRayCaster
    {
        public static double maxDistance = 30.0;
    
        public static async Task<RayCastHit[]> RemoteRayCast(Vector3 origin, Vector3 dir, HitCollectionPolicy hitPolicy = HitCollectionPolicy.ClosestHit)
        {
            if(RemoteRenderingCoordinator.instance.CurrentCoordinatorState == RemoteRenderingCoordinator.RemoteRenderingState.RuntimeConnected)
            {
                var rayCast = new RayCast(origin.toRemotePos(), dir.toRemoteDir(), maxDistance, hitPolicy);
                var result = await RemoteRenderingCoordinator.CurrentSession.Connection.RayCastQueryAsync(rayCast);
                return result.Hits;
            }
            else
            {
                return new RayCastHit[0];
            }
        }
    
        public static async Task<Entity[]> RemoteRayCastEntities(Vector3 origin, Vector3 dir, HitCollectionPolicy hitPolicy = HitCollectionPolicy.ClosestHit)
        {
            var hits = await RemoteRayCast(origin, dir, hitPolicy);
            return hits.Select(hit => hit.HitEntity).Where(entity => entity != null).ToArray();
        }
    }
    

    Nota

    O Unity tem uma classe chamada RaycastHit e o Azure Remote Rendering tem uma classe chamada RayCastHit. A maiúscula C é uma diferença importante para evitar erros de compilação.

    RemoteRayCaster fornece um ponto de acesso comum para lançar raios remotos na sessão atual. Para ser mais específico, vamos implementar um processador de ponteiros MRTK em seguida. O script implementará a IMixedRealityPointerHandler interface, que indicará ao MRTK que pretendemos que este script ouça Mixed Reality eventos de Ponteiro.

  2. Crie um novo script chamado RemoteRayCastPointerHandler e substitua o código pelo seguinte código:

    // Copyright (c) Microsoft Corporation. All rights reserved.
    // Licensed under the MIT License. See LICENSE in the project root for license information.
    
    using Microsoft.Azure.RemoteRendering;
    using Microsoft.MixedReality.Toolkit.Input;
    using System;
    using System.Linq;
    using System.Threading.Tasks;
    using UnityEngine;
    
    public class RemoteRayCastPointerHandler : BaseRemoteRayCastPointerHandler, IMixedRealityPointerHandler
    {
        public UnityRemoteEntityEvent OnRemoteEntityClicked = new UnityRemoteEntityEvent();
    
        public override event Action<Entity> RemoteEntityClicked;
    
        public void Awake()
        {
            // Forward events to Unity events
            RemoteEntityClicked += (entity) => OnRemoteEntityClicked?.Invoke(entity);
        }
    
        public async void OnPointerClicked(MixedRealityPointerEventData eventData)
        {
            if (RemoteEntityClicked != null) //Ensure someone is listening before we do the work
            {
                var firstHit = await PointerDataToRemoteRayCast(eventData.Pointer);
                if (firstHit.success)
                    RemoteEntityClicked.Invoke(firstHit.hit.HitEntity);
            }
        }
    
        public void OnPointerDown(MixedRealityPointerEventData eventData) { }
    
        public void OnPointerDragged(MixedRealityPointerEventData eventData) { }
    
        public void OnPointerUp(MixedRealityPointerEventData eventData) { }
    
        private async Task<(bool success, RayCastHit hit)> PointerDataToRemoteRayCast(IMixedRealityPointer pointer, HitCollectionPolicy hitPolicy = HitCollectionPolicy.ClosestHit)
        {
            RayCastHit hit;
            var result = pointer.Result;
            if (result != null)
            {
                var endPoint = result.Details.Point;
                var direction = pointer.Rays[pointer.Result.RayStepIndex].Direction;
                Debug.DrawRay(endPoint, direction, Color.green, 0);
                hit = (await RemoteRayCaster.RemoteRayCast(endPoint, direction, hitPolicy)).FirstOrDefault();
            }
            else
            {
                hit = new RayCastHit();
            }
            return (hit.HitEntity != null, hit);
        }
    }
    

O método remoteRayCastPointerHandlerOnPointerClicked é chamado pelo MRTK quando um Ponteiro 'clica' num colisor, como o nosso colisor de caixas. Depois disso, PointerDataToRemoteRayCast é chamado para converter o resultado do ponteiro num ponto e direção. Esse ponto e direção são então utilizados para lançar um raio remoto na sessão remota.

Limites atualizados

O envio de pedidos para a conversão de raios ao clicar é uma estratégia eficiente para consultar objetos remotos. No entanto, não é uma experiência de utilizador ideal porque o cursor colide com o colisor de caixas e não com o próprio modelo.

Também pode criar um novo ponteiro mrtk que lança os seus raios na sessão remota com mais frequência. Embora esta seja uma abordagem mais complexa, a experiência do utilizador seria melhor. Esta estratégia está fora do âmbito deste tutorial, mas pode ver um exemplo desta abordagem na Aplicação showcase, que se encontra no repositório de exemplos do ARR.

Quando uma transmissão de raios é concluída com êxito no RemoteRayCastPointerHandler, o sucesso Entity é emitido do evento unity OnRemoteEntityClicked . Para responder a esse evento, vamos criar um script auxiliar que aceita e Entity executa uma ação no mesmo. Vamos começar por obter o script para imprimir o nome do Entity para o registo de depuração.

  1. Crie um novo script com o nome RemoteEntityHelper e substitua o respetivo conteúdo pelo seguinte:

    // 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 UnityEngine;
    
    public class RemoteEntityHelper : MonoBehaviour
    {
        public void EntityToDebugLog(Entity entity)
        {
            Debug.Log(entity.Name);
        }
    }
    
  2. No TestModel GameObject criado anteriormente, adicione o componente RemoteRayCastPointerHandler e o componente RemoteEntityHelper .

  3. Atribua o EntityToDebugLog método ao OnRemoteEntityClicked evento. Quando o tipo de saída do evento e o tipo de entrada do método corresponderem, podemos utilizar a ligação dinâmica de eventos do Unity, que transmitirá automaticamente o valor do evento para o método.

    1. Criar um novo campo de chamada de retorno Adicionar chamada de retorno
    2. Arraste o componente Auxiliar de Entidades Remotas para o campo Objeto, para referenciar o objeto principal GameObject Assign
    3. Atribuir como EntityToDebugLog chamada de retorno Atribuir chamada de retorno
  4. Prima reproduzir no Editor do Unity para iniciar a cena, ligar a uma sessão remota e carregar o modelo de teste.

  5. Ao utilizar a simulação manual do MRTK, prima sem soltar a tecla Shift à esquerda.

  6. Guie a mão simulada para que o raio da mão aponte para o modelo de teste.

  7. Clique longamente para simular um toque de ar, executando o OnPointerClicked evento.

  8. Observe a Consola do Unity para obter uma mensagem de registo com o nome da entidade subordinada selecionada. Por exemplo: Exemplo de entidade subordinada

Sincronizar o grafo de objeto remoto na hierarquia do Unity

Até agora, só vimos um único GameObject local a representar todo o modelo. Isto funciona bem para a composição e manipulação de todo o modelo. No entanto, se quisermos aplicar efeitos ou manipular sub entidades específicas, teremos de criar GameObjects locais para representar essas entidades. Primeiro, podemos explorar manualmente no modelo de teste.

  1. Inicie a cena e carregue o modelo de teste.
  2. Expanda as crianças do TestModel GameObject na hierarquia do Unity e selecione o TestModel_Entity GameObject.
  3. No Inspetor, clique no botão Mostrar Crianças . Mostrar crianças
  4. Continue a expandir as crianças na hierarquia e a clicar em Mostrar Crianças até ser apresentada uma grande lista de crianças. Todas as crianças

Uma lista de dezenas de entidades irá agora preencher a hierarquia. Selecionar um deles irá mostrar os Transform componentes e RemoteEntitySyncObject no Inspetor. Por predefinição, cada entidade não é sincronizada automaticamente em cada frame, pelo que as alterações locais ao Transform não são sincronizadas com o servidor. Pode verificar Sincronizar Cada Frame e, em seguida, mover, dimensionar ou rodar a transformação na vista Cena, não verá o modelo composto na vista de cena, watch vista Jogo para ver a posição e a rotação do modelo visualmente atualizadas.

O mesmo processo pode ser feito programaticamente e é o primeiro passo para modificar entidades remotas específicas.

  1. Modifique o script RemoteEntityHelper para conter também o seguinte método:

    public void MakeSyncedGameObject(Entity entity)
    {
        var entityGameObject = entity.GetOrCreateGameObject(UnityCreationMode.DoNotCreateUnityComponents);
        var sync = entityGameObject.GetComponent<RemoteEntitySyncObject>();
        sync.SyncEveryFrame = true;
    }
    
  2. Adicione uma chamada de retorno adicional ao evento OnRemoteEntityClickedRemoteRayCastPointerHandler, definindo-o como MakeSyncedGameObject. Chamada de retorno adicional

  3. Ao utilizar a simulação manual do MRTK, prima sem soltar a tecla Shift à esquerda.

  4. Guie a mão simulada para que o raio da mão aponte para o modelo de teste.

  5. Clique longamente para simular um toque de ar, executando o OnPointerClicked evento.

  6. Verifique e expanda a Hierarquia para ver um novo objeto subordinado, representando a entidade clicada. Representação do GameObject

  7. Após o teste, remova a chamada de retorno para MakeSyncedGameObject, uma vez que vamos incorporá-la como parte de outros efeitos mais tarde.

Nota

A sincronização de cada frame só é necessária quando precisar de sincronizar os dados de transformação. Existe alguma sobrecarga na sincronização de transformações, pelo que deve ser utilizada com moderação.

Criar uma instância local e sincronizá-la automaticamente é o primeiro passo para manipular sub entidades. As mesmas técnicas que utilizámos para manipular o modelo como um todo também podem ser utilizadas nas sub entidades. Por exemplo, depois de criar uma instância local sincronizada de uma entidade, pode consultar os respetivos limites e adicionar processadores de manipulação para permitir que seja movido pelos raios de mão do utilizador.

Passos seguintes

Agora pode manipular e interagir com os seus modelos compostos remotamente! No próximo tutorial, vamos abordar a modificação de materiais, a alteração da iluminação e a aplicação de efeitos a modelos compostos remotamente.