자습서: 모델 조작

이 자습서에서는 다음을 하는 방법을 알아볼 수 있습니다.

  • 원격으로 렌더링된 모델 주위에 시각적 개체 및 조작 경계 추가
  • 이동, 회전 및 크기 조정
  • 공간 쿼리를 사용하는 Raycast
  • 원격으로 렌더링된 개체에 대한 간단한 애니메이션 추가

필수 조건

  • 이 자습서는 자습서: 인터페이스 및 사용자 지정 모델을 기반으로 합니다.

원격 개체 경계 쿼리 및 로컬 경계에 적용

원격 개체와 상호 작용하려면 먼저 상호 작용하는 로컬 표현이 필요합니다. 개체 경계원격 개체의 빠른 조작에 유용합니다. 원격 범위는 ARR에서 로컬 Entity를 참조로 사용하여 쿼리할 수 있습니다. 모델이 원격 세션에 로드된 후 범위가 쿼리됩니다.

모델의 범위는 x, y, z 축에 대해 정의된 가운데 및 크기를 가진 Unity의 BoxCollider와 마찬가지로 전체 모델을 포함하는 상자에 의해 정의됩니다. 실제로 Unity의 BoxCollider 를 사용하여 원격 모델의 범위를 나타냅니다.

  1. RemoteRenderedModel과 동일한 디렉터리에 새 스크립트를 만들고 이름을 RemoteBounds로 지정합니다.

  2. 스크립트의 내용을 다음 코드로 바꿉니다.

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

    참고 항목

    Visual Studio에서 기능 'X'를 주장하는 오류가 표시되는 경우 C# 6에서 사용할 수 없습니다. 언어 버전 7.0 이상은 사용하세요. 이러한 오류는 무시해도 됩니다. 이는 Unity의 솔루션 및 프로젝트 생성과 관련이 있습니다.

    이 스크립트는 BaseRemoteRenderedModel을 구현하는 스크립트와 동일한 GameObject에 추가되어야 합니다. 이 경우 RemoteRenderedModel을 의미합니다. 이전 스크립트와 마찬가지로 이 초기 코드는 원격 경계와 관련된 모든 상태 변경, 이벤트 및 데이터를 처리합니다.

    구현 할 메서드는 QueryBounds가 하나뿐입니다. QueryBounds 는 범위를 비동기적으로 가져오고 쿼리 결과를 가져와 로컬 BoxCollider에 적용합니다.

    QueryBounds 메서드는 간단합니다. 쿼리를 원격 렌더링 세션으로 보내고 결과를 기다립니다.

  3. QueryBounds 메서드를 다음 완료된 메서드로 바꿉니다.

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

    쿼리 결과를 검사 성공했는지 확인합니다. 그렇다면 BoxCollider에서 허용할 수 있는 형식으로 반환된 범위를 변환하고 적용합니다.

이제 RemoteBounds 스크립트가 RemoteRenderedModel과 동일한 게임 개체에 추가되면 필요한 경우 BoxCollider가 추가되고 모델이 상태에 Loaded 도달하면 경계가 자동으로 쿼리되고 BoxCollider적용됩니다.

  1. 이전에 만든 TestModel GameObject를 사용하여 RemoteBounds 구성 요소를 추가합니다.

  2. 스크립트가 추가되어 있는지 확인합니다.

    Add RemoteBounds component

  3. 애플리케이션을 다시 실행합니다. 모델이 로드된 직후 원격 개체의 경계가 표시됩니다. 다음과 같은 값이 표시됩니다.

    Screenshot that shows the remote object bounds example.

이제 Unity 개체의 정확한 범위로 구성된 로컬 BoxCollider 가 있습니다. 범위를 사용하면 로컬로 렌더링된 개체에 사용하는 것과 동일한 전략을 사용하여 시각화 및 상호 작용을 수행할 수 있습니다. 예를 들어 변환, 물리학 등을 변경하는 스크립트입니다.

이동, 회전 및 크기 조정

원격으로 렌더링된 개체의 이동, 회전 및 크기 조정은 다른 Unity 개체와 동일하게 작동합니다. LateUpdate 메서드의 RemoteRenderingCoordinator는 현재 활성 세션에서 Update를 호출합니다. Update에서 수행하는 작업의 일부는 로컬 모델 엔터티 변환을 원격 대응 항목과 동기화하는 것입니다. 원격으로 렌더링된 모델을 이동, 회전 또는 크기 조정하려면 원격 모델을 나타내는 GameObject의 변환을 이동, 회전 또는 크기 조정하기만 하면 됩니다. 여기서는 RemoteRenderedModel 스크립트가 연결된 부모 GameObject의 변환을 수정합니다.

이 자습서에서는 MRTK를 개체 상호 작용에 사용합니다. 개체를 이동, 회전 및 크기 조정하는 MRTK 관련 구현의 대부분은 이 자습서의 범위를 벗어납니다. AppMenu 내의 Model Tools(모델 도구) 메뉴에는 미리 구성된 모델 보기 컨트롤러가 있습니다.

  1. 이전에 만든 TestModel GameObject가 장면에 있는지 확인합니다.
  2. AppMenu 프리팹이 장면에 있는지 확인합니다.
  3. Unity의 Play(재생) 단추를 눌러 장면을 재생하고, AppMenu 내에서 Model Tools(모델 도구) 메뉴를 엽니다. View controller

AppMenu에는 모델과 바인딩하기 위한 뷰 컨트롤러를 구현하는 하위 메뉴 모델 도구가 있습니다. RemoteBounds 구성 요소가 GameObject에 포함되어 있으면 보기 컨트롤러에서 BoundingBox 구성 요소를 추가합니다. 이는 BoxCollider를 사용하여 경계 상자를 개체 주위에 렌더링하는 MRTK 구성 요소입니다. 손 조작을 담당하는 ObjectManipulator입니다. 이러한 스크립트를 결합하면 원격으로 렌더링된 모델을 이동, 회전 및 크기 조정할 수 있습니다.

  1. 마우스를 게임 패널로 이동하고 안쪽을 클릭하여 포커스를 지정합니다.

  2. MRTK의 손 시뮬레이션을 사용하여 왼쪽 Shift 키를 길게 누릅니다.

  3. 손 광선이 테스트 모델을 가리키도록 시뮬레이션된 손을 조종합니다.

    Pointed hand ray

  4. 마우스 왼쪽 단추를 클릭한 채 모델을 끌어서 이동합니다.

원격으로 렌더링된 콘텐츠가 경계 상자와 함께 이동하는 것을 볼 수 있습니다. 경계 상자와 원격 콘텐츠 간에 약간의 지연이 있을 수 있습니다. 이 지연은 인터넷 대기 시간 및 대역폭에 따라 달라집니다.

원격 모델의 광선 캐스팅 및 공간 쿼리

모델 주위의 상자 collider는 전체 모델과 상호 작용하는 데 적합하지만, 모델의 개별 부분과 상호 작용하는 데는 충분하지 않습니다. 이 문제를 해결하기 위해 원격 광선 캐스팅을 사용할 수 있습니다. 원격 광선 캐스팅은 원격 장면으로 광선을 캐스팅하고 로컬에서 적중 결과를 반환하기 위해 Azure Remote Rendering에서 제공하는 API입니다. 이 기법을 사용하여 큰 모델의 자식 엔터티를 선택하거나 위치, 보통 표면 및 거리와 같은 적중 결과 정보를 얻을 수 있습니다.

테스트 모델에는 쿼리하고 선택할 수 있는 여러 하위 엔터티가 있습니다. 지금은 선택한 엔터티의 이름이 Unity 콘솔에 출력됩니다. 선택한 엔터티를 강조 표시하려면 재질, 조명 및 효과 장을 확인합니다.

먼저 원격 광선 캐스트 쿼리 주위에 정적 래퍼를 만들어 보겠습니다. 이 스크립트는 Unity 공간에서 위치와 방향을 허용하고, 원격 광선 캐스팅에서 허용하는 데이터 형식으로 변환하며, 결과를 반환합니다. 스크립트에서 RayCastQueryAsync API를 사용합니다.

  1. RemoteRayCaster라는 새 스크립트를 만들고 해당 내용을 다음 코드로 바꿉니다.

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

    참고 항목

    Unity에는 RaycastHit라는 클래스가 있고 Azure Remote Rendering에는 RayCastHit라는 클래스가 있습니다. 대문자 C 는 컴파일 오류를 방지하기 위한 중요한 차이점입니다.

    RemoteRayCaster는 원격 광선을 현재 세션으로 캐스팅하기 위한 공통 액세스 지점을 제공합니다. 좀 더 구체적으로 말하자면, 다음에 MRTK 포인터 처리기를 구현하겠습니다. 스크립트는 이 스크립트가 IMixedRealityPointerHandler Mixed Reality Pointer 이벤트를 수신 대기하도록 MRTK에 지시하는 인터페이스를 구현합니다.

  2. RemoteRayCastPointerHandler라는 새 스크립트를 만들고 코드를 다음 코드로 바꿉다.

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

RemoteRayCastPointerHandlerOnPointerClicked 메서드는 상자 충돌체와 같이 충돌체에서 포인터가 '클릭'할 때 MRTK에 의해 호출됩니다. 그런 다음 포인터 PointerDataToRemoteRayCast 의 결과를 점과 방향으로 변환하기 위해 호출됩니다. 그런 다음 해당 지점과 방향을 사용하여 원격 세션에서 원격 광선을 캐스팅합니다.

Bounds updated

클릭 시 광선 캐스팅에 대한 요청을 보내는 것은 원격 개체를 쿼리하기 위한 효율적인 전략입니다. 그러나 커서가 모델 자체가 아니라 상자 충돌체와 충돌하기 때문에 이상적인 사용자 환경은 아닙니다.

원격 세션에서 광선을 더 자주 캐스팅하는 새 MRTK 포인터를 만들 수도 있습니다. 이 방법은 더 복잡한 방법이지만 사용자 환경이 더 좋습니다. 이 전략은 이 자습서의 범위를 벗어나지만 이 방법의 예는 ARR 샘플 리포지토리에 있는 쇼케이스 앱에서 확인할 수 있습니다.

RemoteRayCastPointerHandler에서 광선 캐스팅이 성공적으로 완료되면 Unity 이벤트에서 적중 Entity 이 내보내집니다OnRemoteEntityClicked. 해당 이벤트에 응답하기 위해 해당 이벤트를 수락 Entity 하고 이에 대한 작업을 수행하는 도우미 스크립트를 만듭니다. 먼저 디버그 로그에 이름을 Entity 인쇄하는 스크립트를 가져옵니다.

  1. RemoteEntityHelper라는 새 스크립트를 만들고 해당 내용을 아래로 바꿉니다.

    // 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. 이전에 만든 TestModel GameObject에서 RemoteRayCastPointerHandler 구성 요소 및 RemoteEntityHelper 구성 요소를 모두 추가합니다.

  3. EntityToDebugLog 이벤트에 메서드를 할당합니다OnRemoteEntityClicked. 이벤트의 출력 형식과 메서드의 입력 형식이 일치하면 Unity의 동적 이벤트 후크를 사용하여 이벤트 값을 메서드에 자동으로 전달할 수 있습니다.

    1. 새 콜백 필드 만들기 Add callback
    2. 원격 엔터티 도우미 구성 요소를 개체 필드로 끌어서 부모 GameObject를 참조합니다.Assign object
    3. EntityToDebugLog 콜백으로 할당Assign callback
  4. Unity 편집기에서 재생을 눌러 장면을 시작하고, 원격 세션에 연결하고, 테스트 모델을 로드합니다.

  5. MRTK의 손 시뮬레이션을 사용하여 왼쪽 Shift 키를 길게 누릅니다.

  6. 손 광선이 테스트 모델을 가리키도록 시뮬레이션된 손을 조종합니다.

  7. 길게 클릭하여 OnPointerClicked 이벤트를 실행하는 에어 탭을 시뮬레이션합니다.

  8. 자식 엔터티의 이름이 선택된 로그 메시지에 대한 Unity 콘솔을 관찰합니다. 예: Child entity example

원격 개체 그래프를 Unity 계층 구조에 동기화

이 시점까지 전체 모델을 나타내는 단일 로컬 GameObject만 보았습니다. 이는 전체 모델의 렌더링 및 조작에 적합합니다. 그러나 효과를 적용하거나 특정 하위 엔터티를 조작하려면 해당 엔터티를 나타내는 로컬 GameObjects를 만들어야 합니다. 먼저 테스트 모델에서 수동으로 검색할 수 있습니다.

  1. 장면을 시작하고 테스트 모델을 로드합니다.
  2. Unity의 계층 구조에서 TestModel GameObject의 자식을 펼치고, TestModel_Entity GameObject를 선택합니다.
  3. 검사기에서 Show Children(자식 항목 표시) 단추를 클릭합니다. Show children
  4. 계층에서 자식을 계속 확장하고 큰 자식 목록이 표시될 때까지 [자식 표시]를 클릭합니다. All children

이제 수십 개의 엔터티 목록이 계층 구조를 채웁니다. 이러한 엔터티 중 하나를 선택하면 검사기에서 TransformRemoteEntitySyncObject 구성 요소가 표시됩니다. 기본적으로 각 엔터티는 모든 프레임에 자동으로 동기화되지 않으므로 로컬 변경 내용은 Transform 서버와 동기화되지 않습니다. 모든 프레임 동기화를 검사 장면 보기에서 변환을 이동, 크기 조정 또는 회전할 수 있습니다. 장면 보기에는 렌더링된 모델이 표시되지 않고 게임 보기를 시청하여 모델의 위치와 회전이 시각적으로 업데이트되는 것을 볼 수 있습니다.

동일한 프로세스를 프로그래밍 방식으로 수행할 수 있으며 특정 원격 엔터티를 수정하는 첫 번째 단계입니다.

  1. 다음 메서드도 포함하도록 RemoteEntityHelper 스크립트를 수정합니다.

    public void MakeSyncedGameObject(Entity entity)
    {
        var entityGameObject = entity.GetOrCreateGameObject(UnityCreationMode.DoNotCreateUnityComponents);
        var sync = entityGameObject.GetComponent<RemoteEntitySyncObject>();
        sync.SyncEveryFrame = true;
    }
    
  2. RemoteRayCastPointerHandler 이벤트에 OnRemoteEntityClickedMakeSyncedGameObject콜백을 추가하여 설정합니다. Additional callback

  3. MRTK의 손 시뮬레이션을 사용하여 왼쪽 Shift 키를 길게 누릅니다.

  4. 손 광선이 테스트 모델을 가리키도록 시뮬레이션된 손을 조종합니다.

  5. 길게 클릭하여 OnPointerClicked 이벤트를 실행하는 에어 탭을 시뮬레이션합니다.

  6. 계층 구조를 확인하고 확장하여 클릭한 엔터티를 나타내는 새 자식 개체를 확인합니다. GameObject representation

  7. 나중에 다른 효과의 일부로 통합할 예정이므로 테스트 후 콜백 MakeSyncedGameObject을 제거합니다.

참고 항목

모든 프레임 동기화는 변환 데이터를 동기화해야 하는 경우에만 필요합니다. 변환을 동기화하는 데 약간의 오버헤드가 있으므로 아쉽게 사용해야 합니다.

로컬 인스턴스를 만들고 자동으로 동기화하는 것이 하위 엔터티를 조작하는 첫 번째 단계입니다. 모델을 전체적으로 조작하는 데 사용한 것과 동일한 기술을 하위 엔터티에서도 사용할 수 있습니다. 예를 들어 엔터티의 동기화된 로컬 인스턴스를 만든 후 해당 범위를 쿼리하고 조작 처리기를 추가하여 사용자의 손 광선으로 이동할 수 있습니다.

다음 단계

이제 원격으로 렌더링된 모델을 조작하고 상호 작용할 수 있습니다. 다음 자습서에서는 재질 수정, 조명 변경 및 원격 렌더링 모델에 효과 적용에 대해 설명합니다.