البرنامج التعليمي: مواد التكرير والإضاءة والمؤثرات

في هذا البرنامج التعليمي، تتعلم كيفية:

  • إبراز وتحديد مخططات الطرازات التفصيلية ومكونات النموذج
  • تطبيق مواد مختلفة على النماذج
  • استكشِف النماذج مع مستويات القطع
  • إضافة حركات بسيطة للعناصر المعروضة عن بُعد

المتطلبات الأساسية

إبراز وتحديد المخططات التفصيلية

يُعتبر توفير ملاحظات مرئية للمستخدم جزءًا مهمًا من تجربة المستخدم في أي تطبيق. توفر Azure Remote Rendering آليات ملاحظات مرئية عبر تجاوزات الحالة الهرمية. يتم تطبيق تجاوزات الحالة الهرمية مع المكونات المرفقة بالمثيلات المحلية للنماذج. تعلمنا كيفية إنشاء هذه المثيلات المحلية في مزامنة الرسم البياني العنصر البعيد في التسلسل الهرمي للوحدة.

أولًا، سننشئ غلافًا حول مكون HierarchicalStateOverrideComponent. إن HierarchicalStateOverrideComponent هو النسخة المحلية التي تتحكم بالتجاوزات على الكيان البعيد. تتضمن أصول البرنامج التعليمي فئة أساسية مجردة تُسمى BaseEntityOverrideController، والتي سنوسعها لإنشاء الغلاف.

  1. إنشاء برنامج نصي جديد يُسمَّى EntityOverrideController واستبدال محتوياته مع التعليمات البرمجية التالية:

    // 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بعد ذلك بوضع أعلام الحالة على المكوّن المحلي المرتبط بالكيان البعيد. يتم وصف التجاوزات وحالاتها في صفحة تجاوزات الحالة الهرمية.

يبدّل هذا التطبيق حالة واحدة في كل مرة. ومع ذلك، فمن الممكن تمامًا الجمع بين تجاوزات متعددة على كيانات واحدة وإنشاء مجموعات على مستويات مختلفة في التسلسل الهرمي. فعلى سبيل المثال، فإن الجمع بين Selected وSeeThrough على عنصر واحد من شأنه أن يعطيه مخططًا تفصيليًا مع جعله شفافًا أيضًا. أو، ضبط تجاوز كيان الجذر Hidden إلى ForceOn أثناء عمل تجاوز Hidden لكيان فرعي ForceOff سيخفي كل شيء باستثناء الكيان الفرعي.

لتطبيق الحالات على الكيانات، يمكننا تعديل RemoteEntityHelper التي تم ابتكارها مسبقًا.

  1. تعديل فئة RemoteEntityHelper لتطبيق فئة تجريدية لـBaseRemoteEntityHelper. سيسمح هذا التعديل باستخدام وحدة تحكم بالعرض متوفرة في أصول البرنامج التعليمي. يجب أن يبدو هكذا عند التعديل:

    public class RemoteEntityHelper : BaseRemoteEntityHelper
    
  2. تجاوز الطرق المجردة باستخدام التعليمات البرمجية التالية:

    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 إلى الكيان الهدف، ثم يطلب إحدى طرق التبديل. إن رغبت في ذلك، على TestModel GameObject، فإن طلب طرق المساعد هذه يمكن القيام به عن طريق إضافة RemoteEntityHelper كاستدعاء OnRemoteEntityClicked إلى الحدث على مكون RemoteRayCastPointerHandler.

استدعاءات المؤشر

الآن وقد تمت إضافة هذه البرامج النصية إلى النموذج، بمجرد توصيلها بوقت التشغيل، يجب أن تتضمن وحدة التحكم بعرض AppMenu واجهات إضافية مُمكَّنة للتفاعل مع البرنامج النصيEntityOverrideController. تحقَّق من قائمة أدوات النموذج لرؤية وحدات التحكم بالعرض غير المؤمنة.

عند هذه النقطة، يجب أن تبدو مكونات TestModel GameObject هكذا:

نموذج الاختبار مع نصوص إضافية

فيما يلي مثال لتكديس التجاوزات على كيان واحد. استخدمنا SelectوTint لتوفير كل من المخطط والتلوين:

حدد لون نموذج الاختبار

مستويات القطع

مستويات القطع هي ميزة يمكن إضافتها إلى أي كيان بعيد. الأكثر شيوعًا، تنشئ كيانًا بعيدًا جديدًا غير مقترن بأي بيانات شبكة للاحتفاظ بمكون مستوى القطع. يتم تحديد موضع واتجاه مستوى القطع وفق موضع واتجاه الكيان البعيد المرفق به.

سننشئ برنامجًا نصيًا ينشئ تلقائيًا كيانًا بعيدًا، ويضيف مكون مستوى القطع، ويزامن تحويل كائن محلي مع كيان مستوى القطع. ثم، يمكننا استخدام CutPlaneViewController لتغليف مستوى القطع في واجهة من شأنها أن تسمح لنا بمعالجته.

  1. أنشِئ نصًا جديدًا تحت اسم AADAuthentication واستبدِل الرمز الخاص به بما يلي:

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

    يوسّع هذا الرمز فئة BaseRemoteCutPlane المدرجة في أصول البرنامج التعليمي. وبالمثل إلى النموذج المقدم عن بعد، هذا البرنامج النصي تعلق ويستمع لتغييرات RemoteRenderingState من منسق بعيد. عندما يصل المنسق إلى RuntimeConnectedالحالة، سيحاول الاتصال تلقائيًا إن كان من المفترض أن يصل. هناك أيضًا CutPlaneComponentمتغير سنقوم بتعقبه. هذا هو مكوّن Azure Remote Rendering الذي يتزامن مع مستوى القطع في الجلسة البعيدة. دعونا نلقي نظرة على ما نحتاج إلى القيام به لإنشاء مستوى القطع.

  2. استبدِل CreateCutPlane()الطريقة بالإصدار المكتمل أدناه:

    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 محلي. نضمن أن الكيان البعيد سيزامن تحويله مع التحويل المحلي عن طريق الإعدادSyncEveryFrame إلىtrue. ثم نستخدم CreateComponentالاتصال لإضافةCutPlaneComponent الكائن البعيد. وأخيرًا، نضبط مستوى القطع مع الإعدادات المحددة في الجزء العلوي من MonoBehaviour. دعونا نرى ما يلزم لتنظيف مستوى القطع من خلال تطبيقDestroyCutPlane() هذه الطريقة.

  3. استبدِل DestroyCutPlane()الطريقة بالإصدار المكتمل أدناه:

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

بما أن الكائن البعيد بسيط إلى حد ما ونحن ننظف الطرف البعيد فقط (ونحافظ على كائننا المحلي)، فمن السهل الاتصال بـ Destroy بالكائن البعيد ومسح إشارتنا إليه.

تتضمن AppMenu وحدة تحكم بالعرض التي ستلتصق تلقائيًا بمستوى القطع لديك وتسمح لك بالتفاعل معها. ليس مطلوبًا منك استخدام AppMenu أو أي من وحدات التحكم بالعرض، ولكنها وُضعت لتجربة أفضل. اختبِر الآن مستوى القطع ووحدة التحكم بالعرض الخاصة به.

  1. أنشِئ GameObject فارغًا جديدًا في المشهد وأطلِق عليه اسم CutPlane.

  2. أضِف مكون RemoteCutPlane إلى GameObject CutPlane.

    تكوين مكوّن مستوى القطع

  3. اضغَط على تشغيل في محرر الوحدة لتحميل جلسة تعمل عن بعد والاتصال بها.

  4. باستخدام محاكاة اليد MRTK، التقِط وقم بتدوير (اضغَط على Ctrl لتدوير) CutPlane لتحريكه في جميع أنحاء المشهد. شاهِده يدخل إلى TestModel ليكشف عن المكونات الداخلية.

مثال عن مستوى القطع

تكوين الإضاءة التي تعمل عن بعد

تدعم جلسة العرض عن بعد مجموعة كاملة من خيارات الإضاءة. سننشئ نصوصًا لـSky Texture وخريطة بسيطة لنوعي ضوء الوحدة لاستخدامها مع العرض عن بعد.

Sky Texture

هناك عدد من Cubemaps المُدرجة للاختيار من بينها عند تغيير بنية السماء. يتم تحميل هذه في الجلسة وتطبيقها على بنية السماء. من الممكن أيضًا التحميل في بنياتك الخاصة لاستخدامها كضوء سماء.

سننشئ نصRemoteSky يحتوي على قائمة من Cubemaps المُدرجة المتوفرة في شكل معلمات تحميل. ثم، سنسمح للمستخدم بتحديد أحد الخيارات وتحميله.

  1. أنشِئ برنامجًا نصيًا جديدًا يُسمَّى AADAuthentication واستبدِل محتوياته الكاملة بالرمز أدناه:

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

    الجزء الأكثر أهمية من هذا الرمز هو بضعة أسطر وحسب:

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

    هنا، نحصل على مرجع للبنية المراد استخدامها عن طريق تحميلها في الجلسة من تخزين النقطة المدمج. عندئذٍ، نحتاج فقط إلى تخصيص هذه البنية للدورةSkyReflectionTexture لتطبيقها.

  2. أنشِئ GameObject فارغة في مشهدك الخاص وأطلِق عليها اسم SkyLight.

  3. أضِف نص RemoteSky إلى GameObject SkyLight الخاص بك.

    يمكن التبديل بين أضواء السماء عن طريق الاتصالSetSky بأحد مفاتيح السلسلة المحددة فيAvailableCubemaps. تقوم وحدة التحكم في العرض المضمنة في AppMenu تلقائيا بإنشاء أزرار وربط أحداثها للاتصال SetSky بالمفتاح الخاص بها.

  4. اضغَط على تشغيل في محرر الوحدة واسمَح بإجراء اتصال.

  5. بعد توصيل وقت التشغيل المحلي بجلسة عمل بعيدة، انتقل إلى AppMenu -> أدوات الجلسة -> السماء البعيدة لاستكشاف خيارات السماء المختلفة ومعرفة كيفية تأثيرها على TestModel.

أضواء المشهد

تشمل أضواء المشهد عن بعد: النقطة، البقعة، والاتجاه. على غرار مستوى القطع الذي أنشأنا أعلاه، أضواء المشهد هذه هي كيانات نائية مع مكونات مرفقة بها. هناك اعتبار مهم عند إضاءة مشهدك عن بعد وهو محاولة ليتناسب مع الإضاءة في مشهدك المحلي. هذه الاستراتيجية غير ممكنة دائمًا؛ لأن العديد من تطبيقات Unity HoloLens 2 لا تستخدم العرض المادي للكائنات المعروضة محليًا. مع ذلك، إلى مستوى معين، يمكننا محاكاة الإضاءة الافتراضية الأبسط في Unity.

  1. أنشِئ برنامجًا نصيًا جديدًا يُسمَّى RemoteLight واستبدِل رمزه بالرمز أدناه:

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

    ينشئ هذا النص أنواعًا مختلفة من الأضواء التي تعمل عن بعد وفق نوع ضوء الوحدة المحلي المرفق بالنص. سوف يكرر الضوء الذي يعمل عن بعد الضوء المحلي في: الموضع، والدوران، واللون، والكثافة. حيثما أمكن، سيقوم الضوء الذي يعمل عن بعد أيضًا بتعيين تكوين إضافي. هذا ليس تطابقًا مثاليًا؛ لأن أضواء الوحدة ليست أضواء العرض القائمة على أساس مادي.

  2. اعثر على GameObject DirectionalLight في مشهدك الخاص. إذا قمت بإزالة DirectionalLight الافتراضي من المشهد الخاص بك: من شريط القوائم العلوي حدد GameObject -> Light -> DirectionalLight لإنشاء ضوء جديد في المشهد الخاص بك.

  3. حدِّد GameObject DirectionalLight وباستخدام الزر أضِف مكون النص RemoteLight.

  4. لأن هذا البرنامج النصي ينفذ الفئة الأساسية BaseRemoteLight، يمكنك استخدام وحدة التحكم بعرضAppMenuالمتوفرة للتفاعل مع الضوء الذي يعمل عن بعد. انتقل إلى AppMenu -> أدوات الجلسة -> الضوء الاتجاهي.

    ملاحظة

    اقتصرت واجهة المستخدم فيAppMenuعلى ضوء اتجاهي واحد من أجل البساطة. مع ذلك، لا يزال من الممكن ويوصى بإضافة أضواء نقطة وإرفاق النص RemoteLight بها. يمكن تعديل هذه الأضواء الإضافية عن طريق تحرير خصائص ضوء الوحدة في المحرر. ستحتاج إلى مزامنة التغييرات المحلية للضوء الذي يعمل عن بعد يدويًا باستخدام قائمة سياق RemoteLight في الفاحص:

    مزامنة يدوية للضوء الذي يعمل عن بعد

  5. اضغَط على تشغيل في محرر الوحدة واسمَح بإجراء اتصال.

  6. بعد توصيل وقت التشغيل الخاص بك بجلسة تعمل عن بعد، ثبِّت ووجِّه الكاميرا (استخدِم WASD وانقر على زر الماوس الأيمن + الماوس التحرك) لتحصل على تحكم بعرض الضوء الاتجاهي في العرض.

  7. استخدِم وحدة التحكم بعرض الضوء التي تعمل عن بعد لتعديل خصائص الضوء. باستخدام محاكاة اليد MRTK، امسك الضوء الاتجاهي وقم بتدويره (اضغَط على Ctrl للتدوير) لمعرفة التأثير على إضاءة المشهد.

    ضوء اتجاهي

مواد التحرير

يمكن تعديل المواد التي يتم عرضها عن بعد لتوفير مؤثرات بصرية إضافية، أو ضبط المرئيات للنماذج المعروضة، أو توفير ملاحظات إضافية للمستخدمين. هناك العديد من الطرق والأسباب لتعديل المادة. هنا، سنوضح لك كيفية تغيير لون البياض للمادة وتغيير خشونة مادة PBR ونوعية معدنها.

ملاحظة

في كثير من الحالات، إن كان يمكن تطبيق ميزة أو تأثير باستخدام HierarchicalStateOverrideComponentفمن المثالي استخدام ذلك بدلًا من تعديل المادة.

سننشئ نصًا يقبل كيانًا مستهدفًا ويهيئ بعض عناصر OverrideMaterialProperty القليلة لتغيير خصائص مادة الكيان المستهدف. نبدأ من الحصول على MeshComponent للكيان المستهدف، والذي يحتوي على قائمة من المواد المستخدمة على الشبكة. للتبسيط، سنستخدم أول مادة تم العثور عليها. يمكن أن تفشل هذه الاستراتيجية الساذجة بسهولة كبيرة اعتمادًا على كيفية تأليف المحتوى؛ لذلك من المحتمل أن ترغب في اتباع نهج أكثر تعقيدًا لتحديد المواد المناسبة.

من المادة، يمكننا الوصول إلى القيم المشتركة مثل البياض. أولاً، تحتاج المواد إلى أن يتم جمعها في نوعها المناسب، PbrMaterial أو ColorMaterial، لاستعادة قيمها، كما رأينا في طريقة GetMaterialColor. وبمجرد أن يكون لدينا مرجع للمواد المطلوبة، فقط قم بتعيين القيم، وستعالج ARR المزامنة بين خصائص المواد المحلية والمواد التي تعمل عن بُعد.

  1. أنشِئ نصًا جديدًا تحت اسم EntityOverrideController واستبدِل محتوياته بالرمز التالي:

    // 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 مرنًا بما يكفي للسماح بتغيير بعض القيم المادية الأخرى، إن رغبت في ذلك. يتتبّع النوع OverrideMaterialProperty حالة التجاوز، ويحافظ على القيمة القديمة والجديدة، ويستخدم مفوضًا لتعيين التجاوز. كمثال، انظر إلى ColorOverride:

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

هذا هو إنشاء OverrideMaterialProperty جديد حيث سيغلف التجاوز النوع Color. نقدم اللون الحالي أو الأصلي في وقت إنشاء التجاوز. كما نعطيها مادة ARR للعمل على أساسها. وأخيرًا، يتم توفير المفوض الذي سيطبّق التجاوز. المفوض هو طريقة تقبل مادة ARR ونوع أغلفة التجاوز. هذه الطريقة هي أهم جزء من فهم كيفية ضبط ARR لقيم المواد.

ColorOverrideيستخدم الطريقة للقيامApplyMaterialColor بعمله:

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

يقبل هذا الرمز مادة ولونًا. فإنه يتحقق لمعرفة أي نوع من المواد هي ثم يقولب المادة لتطبيق اللون.

RoughnessOverride وMetalnessOverride يعملان بالمثل - باستخدام ApplyRoughnessValue وطرق ApplyMetalnessValue للقيام بعملهما.

بعد ذلك، لنختبر وحدة التحكم بالمواد.

  1. أضِف نص EntityMaterialController إلى GameObject TestModel الخاص بك.
  2. اضغَط على تشغيل في Unity لبدء المشهد والاتصال بـ ARR.
  3. بعد توصيل وقت التشغيل بجلسة عمل بعيدة وتحميل النموذج، انتقل إلى AppMenu -> Model Tools -> Edit Material
  4. حدِّد كيانًا من النموذج باستخدام الأيدي المقلّدة للنقر على TestModel.
  5. تأكد من تحديث وحدة تحكم عرض المواد (AppMenu-Model> Tools-Edit> Material) إلى الكيان المستهدف.
  6. استخدِم وحدة التحكم بعرض المادة لضبط المادة على الكيان المستهدف.

بما أننا نقوم بتعديل المادة الأولى من الشبكة فقط، فقد لا ترى المادة تتغير. استخدِم تجاوز التسلسل الهرمي SeeThrough لمعرفة إذا ما كانت المادة التي تقوم بتغييرها داخل الشبكة.

مثال تحرير المادة

الخطوات التالية

تهانينا! طبّقت الآن كافة الوظائف الأساسية لـAzure Remote Rendering. في الفصل التالي، سنتعرف على تأمين ميزة Azure Remote Rendering وتخزين Blob. ستكون هذه الخطوات الأولى لإصدار تطبيق تجاري يستخدم Azure Remote Rendering.