자습서: 인터페이스 및 사용자 지정 모델
이 자습서에서는 다음을 하는 방법을 알아볼 수 있습니다.
- 프로젝트에 Mixed Reality Toolkit 추가
- 모델 상태 관리
- 모델 수집을 위한 Azure Blob Storage 구성
- 렌더링할 모델 업로드 및 처리
필수 조건
- 이 자습서는 자습서: 원격으로 렌더링된 모델 보기를 기반으로 합니다.
MRTK(Mixed Reality Toolkit) 시작
MRTK(Mixed Reality Toolkit)는 혼합 현실 환경을 빌드하는 데 사용되는 플랫폼 간 도구 키트입니다. 상호 작용 및 시각화 기능에 MRTK 2.8.3을 사용합니다.
MRTK를 가져오는 공식 가이드에는 수행할 필요가 없는 몇 가지 단계가 포함되어 있습니다. 다음 세 단계만 필요합니다.
- Mixed Reality 기능 도구(MRTK 가져오기)를 통해 'Mixed Reality Toolkit/Mixed Reality Toolkit Foundation' 버전 2.8.3을 프로젝트에 가져옵니다.
- MRTK(MRTK 구성)의 구성 마법사를 실행합니다.
- 현재 장면에 MRTK를 추가합니다(장면에 추가). 자습서에서 제안된 프로필 대신 여기에 ARRMixedRealityToolkitConfigurationProfile을 사용합니다.
이 자습서에서 사용하는 자산 가져오기
이 챕터부터 대부분의 재질에 사용되는 기본 모델-보기-컨트롤러 패턴을 구현합니다. 패턴의 모델 파트는 Azure Remote Rendering 관련 코드 및 Azure Remote Rendering과 관련된 상태 관리입니다. 패턴의 보기 및 컨트롤러 파트는 MRTK 자산과 사용자 지정 스크립트를 사용하여 구현됩니다. 여기서 구현하는 보기 컨트롤러 없이 이 자습서의 모델을 사용할 수도 있습니다. 이렇게 분리하면 이 자습서의 코드를 개발자의 애플리케이션에 쉽게 통합할 수 있습니다. 그러면 애플리케이션이 디자인 패턴의 보기 컨트롤러 파트를 넘겨받습니다.
MRTK가 도입되면서, 이제 여러 스크립트, prefab 및 자산을 프로젝트에 추가하여 상호 작용 및 시각적 피드백을 지원할 수 있습니다. 자습서 자산이라고 하는 이러한 자산은 Unity 자산 패키지에 번들로 제공되며, '\Unity\TutorialAssets\TutorialAssets.unitypackage'의 Azure Remote Rendering GitHub에 포함되어 있습니다.
- 다운로드하면 알려진 위치에 Zip 파일의 압축이 풀리는 경우 Azure Remote Rendering git 리포지토리를 복제하거나 다운로드합니다.
- Unity 프로젝트에서 자산 -> 패키지 가져오기 -> 사용자 지정 패키지를 선택합니다.
- 파일 탐색기에서 Azure Remote Rendering 리포지토리를 복제하거나 압축을 푼 디렉터리로 이동한 다음, Unity ->TutorialAssets -> TutorialAssets.unitypackage에 있는
.unitypackage
를 선택합니다. - 가져오기 단추를 선택하여 패키지의 내용을 프로젝트로 가져옵니다.
- Unity 편집기의 위쪽 메뉴 모음에서 Mixed Reality Toolkit -> 유틸리티 -> 경량 렌더링 파이프라인에 대한 MRTK 표준 셰이더 업그레이드를 선택하고 표시되는 메시지에 따라 셰이더를 업그레이드합니다.
MRTK 및 자습서 자산이 설정되면 올바른 프로필이 선택되었는지 다시 확인합니다.
- 장면 계층 구조에서 MixedRealityToolkit GameObject를 선택합니다.
- 검사기의 MixedRealityToolkit 구성 요소 아래에서 구성 프로필을 ARRMixedRealityToolkitConfigurationProfile로 전환합니다.
- Ctrl+S를 눌러 변경 내용을 저장합니다.
이 단계에서는 주로 기본 HoloLens 2 프로필을 사용하여 MRTK를 구성합니다. 제공된 프로필은 다음과 같은 방법으로 미리 구성됩니다.
- 프로파일러를 끕니다(9를 눌러 프로파일러 켜기/끄기 또는 디바이스에서는 음성으로 "프로파일러 표시/숨기기"라고 말하기).
- 시선 응시 커서를 끕니다.
- Unity 마우스 클릭을 사용하도록 설정합니다. 이렇게 하면 시뮬레이션된 손 대신 마우스를 사용하여 MRTK UI 요소를 클릭할 수 있습니다.
앱 메뉴 추가
이 자습서에 나오는 보기 컨트롤러는 대부분 구체적 클래스 대신 추상 기본 클래스에 대해 작동합니다. 이 패턴은 Azure Remote Rendering에 대해 학습할 수 있도록 도와주면서도 더 많은 유연성을 제공하고 보기 컨트롤러를 자동으로 제공할 수 있습니다. 편의상 RemoteRenderingCoordinator 클래스에는 추상 클래스가 없으며 해당 보기 컨트롤러는 구체적 클래스에 대해 직접 작동합니다.
이제 장면에 prefab AppMenu를 추가하여 현재 세션 상태에 대한 시각적 피드백을 받을 수 있습니다. AppMenu는 사용자가 ARR에 연결하도록 애플리케이션에 권한을 부여하는 데 사용하는 모달 패널도 제공합니다.
Assets/RemoteRenderingTutorial/Prefabs/AppMenu에서 AppMenu prefab을 찾습니다.
AppMenu prefab을 장면으로 끌어 놓습니다.
TMP 가져오기 대화 상자가 표시되면 프롬프트에 따라 TMP Essentials를 가져옵니다. 그런 다음, 예제 및 엑스트라가 필요하지 않으므로 가져오기 대화 상자를 닫습니다.
AppMenu는 세션에 연결하는 것에 동의하도록 자동으로 연결되어 모달을 제공하도록 구성되어 있으므로, 이전에 배치된 바이패스를 제거할 수 있습니다. RemoteRenderingCoordinator GameObject의 On Requesting Authorization 이벤트에서 '-' 단추를 눌러 이전에 구현한 권한 부여에 대한 바이패스를 제거합니다.
.
Unity 편집기에서 재생을 눌러 보기 컨트롤러를 테스트합니다.
MRTK가 구성되었으므로, 이제 편집기에서 WASD 키를 사용하여 보기 위치를 변경하고 마우스 오른쪽 단추를 누른 채로 마우스를 이동하여 보기 방향을 변경할 수 있습니다. 장면 주위를 약간 "이동"하여 컨트롤의 느낌을 살펴보세요.
디바이스에서 손바닥을 들어올려 AppMenu를 소환할 수 있습니다. Unity 편집기에서는 바로 가기 키 'M'을 사용합니다.
메뉴가 보이지 않으면 'M' 키를 눌러 메뉴를 소환합니다. 메뉴는 쉽게 조작할 수 있도록 카메라 근처에 배치됩니다.
AppMenu는 AppMenu 오른쪽에 권한 부여를 위한 UI 요소를 제공합니다. 이제부터는 이 UI 요소를 사용하여 원격 렌더링 세션을 관리하도록 앱에 권한을 부여해야 합니다.
Unity 재생을 중지하고 자습서를 계속 진행합니다.
모델 상태 관리
상태 추적, 이벤트에 응답, 이벤트 실행 및 구성을 위한 RemoteRenderedModel이라는 새 스크립트가 필요합니다. 기본적으로 RemoteRenderedModel은 모델 데이터의 원격 경로를 modelPath
에 저장합니다. RemoteRenderingCoordinator에서 상태 변경을 수신 대기하면서 정의하는 모델을 자동으로 로드 또는 언로드해야 하는지 확인합니다. RemoteRenderedModel이 연결된 GameObject는 원격 콘텐츠의 로컬 부모입니다.
RemoteRenderedModel 스크립트는 자습서 자산에 포함된 BaseRemoteRenderedModel을 구현합니다. 이 연결을 사용하면 원격 모델 보기 컨트롤러를 스크립트에 바인딩할 수 있습니다.
RemoteRenderingCoordinator와 동일한 폴더에 RemoteRenderedModel이라는 새 스크립트를 만듭니다. 전체 내용을 다음 코드로 바꿉니다.
// 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; using UnityEngine.Events; public class RemoteRenderedModel : BaseRemoteRenderedModel { public bool AutomaticallyLoad = true; private ModelState currentModelState = ModelState.NotReady; [SerializeField] [Tooltip("The friendly name for this model")] private string modelDisplayName; public override string ModelDisplayName { get => modelDisplayName; set => modelDisplayName = value; } [SerializeField] [Tooltip("The URI for this model")] private string modelPath; public override string ModelPath { get => modelPath.Trim(); set => modelPath = value; } public override ModelState CurrentModelState { get => currentModelState; protected set { if (currentModelState != value) { currentModelState = value; ModelStateChange?.Invoke(value); } } } public override event Action<ModelState> ModelStateChange; public override event Action<float> LoadProgress; public override Entity ModelEntity { get; protected set; } public UnityEvent OnModelNotReady = new UnityEvent(); public UnityEvent OnModelReady = new UnityEvent(); public UnityEvent OnStartLoading = new UnityEvent(); public UnityEvent OnModelLoaded = new UnityEvent(); public UnityEvent OnModelUnloading = new UnityEvent(); public UnityFloatEvent OnLoadProgress = new UnityFloatEvent(); public void Awake() { // Hook up the event to the Unity event LoadProgress += (progress) => OnLoadProgress?.Invoke(progress); ModelStateChange += HandleUnityStateEvents; } private void HandleUnityStateEvents(ModelState modelState) { switch (modelState) { case ModelState.NotReady: OnModelNotReady?.Invoke(); break; case ModelState.Ready: OnModelReady?.Invoke(); break; case ModelState.Loading: OnStartLoading?.Invoke(); break; case ModelState.Loaded: OnModelLoaded?.Invoke(); break; case ModelState.Unloading: OnModelUnloading?.Invoke(); break; } } private void Start() { //Attach to and initialize current state (in case we're attaching late) RemoteRenderingCoordinator.CoordinatorStateChange += Instance_CoordinatorStateChange; Instance_CoordinatorStateChange(RemoteRenderingCoordinator.instance.CurrentCoordinatorState); } /// <summary> /// Listen for state changes on the coordinator, clean up this model's remote objects if we're no longer connected. /// Automatically load if required /// </summary> private void Instance_CoordinatorStateChange(RemoteRenderingCoordinator.RemoteRenderingState state) { switch (state) { case RemoteRenderingCoordinator.RemoteRenderingState.RuntimeConnected: CurrentModelState = ModelState.Ready; if (AutomaticallyLoad) LoadModel(); break; default: UnloadModel(); break; } } private void OnDestroy() { RemoteRenderingCoordinator.CoordinatorStateChange -= Instance_CoordinatorStateChange; UnloadModel(); } /// <summary> /// Asks the coordinator to create a model entity and listens for coordinator state changes /// </summary> [ContextMenu("Load Model")] public override async void LoadModel() { if (CurrentModelState != ModelState.Ready) return; //We're already loaded, currently loading, or not ready to load CurrentModelState = ModelState.Loading; ModelEntity = await RemoteRenderingCoordinator.instance?.LoadModel(ModelPath, this.transform, SetLoadingProgress); if (ModelEntity != null) CurrentModelState = ModelState.Loaded; else CurrentModelState = ModelState.Error; } /// <summary> /// Clean up the local model instances /// </summary> [ContextMenu("Unload Model")] public override void UnloadModel() { CurrentModelState = ModelState.Unloading; if (ModelEntity != null) { var modelGameObject = ModelEntity.GetOrCreateGameObject(UnityCreationMode.DoNotCreateUnityComponents); Destroy(modelGameObject); ModelEntity.Destroy(); ModelEntity = null; } if (RemoteRenderingCoordinator.instance.CurrentCoordinatorState == RemoteRenderingCoordinator.RemoteRenderingState.RuntimeConnected) CurrentModelState = ModelState.Ready; else CurrentModelState = ModelState.NotReady; } /// <summary> /// Update the Unity progress event /// </summary> /// <param name="progressValue"></param> public override void SetLoadingProgress(float progressValue) { LoadProgress?.Invoke(progressValue); } }
간단하게 말해서, RemoteRenderedModel은 모델을 로드하는 데 필요한 데이터(여기서는 SAS 또는 builtin:// URI)를 보유하고 원격 모델 상태를 추적합니다. 모델을 로드할 시간이 되면 RemoteRenderingCoordinator에서 LoadModel
메서드가 호출되고, 모델을 포함하는 엔터티가 참조 및 언로드를 위해 반환됩니다.
테스트 모델 로드
테스트 모델을 다시 로드하여 새 스크립트를 테스트하겠습니다. 이 테스트의 경우 스크립트를 포함하고 테스트 모델의 부모가 되려면 게임 개체가 필요하며 모델을 포함하는 가상 단계도 필요합니다. 스테이지는 WorldAnchor를 사용하여 실제 세계를 기준으로 고정된 상태로 유지됩니다. 모델 자체를 나중에 계속 이동할 수 있도록 고정 스테이지를 사용합니다.
장면에서 빈 게임 개체를 새로 만들고 이름을 ModelStage로 지정합니다.
ModelStage에 World Anchor 구성 요소 추가
장면에서 ModelStage의 자식으로 빈 게임 개체를 새로 만들고 이름을 TestModel로 지정합니다.
TestModel에 RemoteRenderedModel 스크립트를 추가합니다.
Model Display Name
및Model Path
를 각각 "TestModel" 및 "builtin://Engine"으로 채웁니다.카메라 앞에 있는 TestModel 개체를 x = 0, y = 0, z = 3 위치에 배치합니다.
AutomaticallyLoad가 켜져 있는지 확인합니다.
Unity 편집기에서 재생을 눌러 애플리케이션을 테스트합니다.
앱에서 세션을 만들고, 세션에 연결하고, 모델을 자동으로 로드할 수 있도록 연결 단추를 클릭하여 권한을 부여합니다.
애플리케이션의 상태가 진행되는 동안 콘솔을 시청합니다. 일부 상태에서는 완료하는 데 다소 시간이 걸릴 수 있으며 잠깐 진행률 업데이트가 없을 수 있습니다. 최종적으로 로드되는 모델의 로그가 표시되고 직후에 장면에 렌더링된 테스트 모델이 표시됩니다.
검사기에서 변환을 통해 또는 장면 보기에서 TestModel GameObject를 이동하고 회전하고 게임 보기에서 변환을 관찰해 보세요.
Azure 및 사용자 지정 모델 수집에서 Blob Storage 프로비저닝
이제 모델을 로드해 볼 수 있습니다. 이렇게 하려면 Azure에서 Blob Storage를 구성하고, 모델을 업로드 및 변환한 다음, RemoteRenderedModel 스크립트를 사용하여 모델을 로드해야 합니다. 현재 로드할 모델이 없는 경우 사용자 지정 모델 로드 단계를 안전하게 건너뛸 수 있습니다.
빠른 시작: 렌더링하기 위한 모델 변환에 지정된 단계를 따릅니다. 이 자습서에서는 빠른 시작 샘플 앱에 새 모델 삽입 섹션을 건너뜁니다. 수집한 모델의 SAS(공유 액세스 서명) URI가 있으면 계속합니다.
사용자 지정 모델 로드 및 렌더링
장면에 빈 GameObject를 새로 만들고 사용자 지정 모델과 비슷하게 이름을 지정합니다.
새로 만든 GameObject에 RemoteRenderedModel 스크립트를 추가합니다.
Model Display Name
에 적절한 모델 이름을 입력합니다.Model Path
에는 Azure 및 사용자 지정 모델 수집에서 Blob Storage 프로비전 단계에서 만든 모델의 SAS(공유 액세스 서명) URI를 입력합니다.카메라 앞에 있는 GameObject를 x = 0, y = 0, z = 3 위치에 배치합니다.
AutomaticallyLoad가 켜져 있는지 확인합니다.
Unity 편집기에서 재생을 눌러 애플리케이션을 테스트합니다.
콘솔은 세션이 연결되면 현재 세션 상태와 모델 로드 진행률 메시지를 표시합니다.
장면에서 사용자 지정 모델 개체를 제거합니다. 이 자습서에 가장 적합한 환경은 테스트 모델을 사용하는 것입니다. ARR에서 여러 모델이 지원되지만, 이 자습서는 한 번에 하나의 원격 모델을 가장 잘 지원하도록 작성되었습니다.
다음 단계
이제 개발자 고유의 모델을 Azure Remote Rendering에 로드하고 애플리케이션에서 모델을 볼 수 있습니다. 다음으로, 모델을 조작하는 방법을 알아보겠습니다.