最近有一个项目是用Steam VR开发的,里面部分场景是用VRTK框架做的,还有一部分是用SteamVR SDK自带的Player预制直接开发的。

这样本身没有问题,因为最终都是通过SteamVR SDK处理的,VRTK也管理好了SteamVR的逻辑,并且支持动态切换,比如切换成Oculus的。

然后现在遇到一个问题,还有一个项目是用Unity自带的XR开发的,Package Manager导入XR相关的插件实现的。

XR和Steam VR项目合并问题插图 

需要将XR开发的项目移植到Steam VR项目来,然后事情就开始了。

SteamVR的场景可以运行,通过Pico以及Quest串流还有htc头盔都能正常识别,手柄也能控制。

但是XR场景就出现问题了,头盔无法识别。

经过一步步排查,发现是XR Plug-in Management这里需要设置不同的XRLoader。

XR和Steam VR项目合并问题插图(1)

而SteamVR是OpenVR Loader,而XR是OpenXR,因为OpenVR Loader在前,所以激活的是OpenVR Loader,这也是为什么SteamVR场景可以运行而XR场景不行。

我们看看unity的源代码是怎么写的,发现这里面是有activeLoader的概念,也就是一次只能一个Loader运行。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.UIElements;
using UnityEngine.Serialization;
using UnityEngine.XR.Management;
[assembly: InternalsVisibleTo("Unity.XR.Management.Tests")]
[assembly: InternalsVisibleTo("Unity.XR.Management.EditorTests")]
namespace UnityEngine.XR.Management
{
/// 
/// Class to handle active loader and subsystem management for XR. This class is to be added as a
/// ScriptableObject asset in your project and should only be referenced by the 
/// instance for its use.
///
/// Given a list of loaders, it will attempt to load each loader in the given order. The first
/// loader that is successful wins and all remaining loaders are ignored. The loader
/// that succeeds is accessible through the  property on the manager.
///
/// Depending on configuration the  instance will automatically manage the active loader
/// at correct points in the application lifecycle. The user can override certain points in the active loader lifecycle
/// and manually manage them by toggling the  and 
/// properties. Disabling  implies the the user is responsible for the full lifecycle
/// of the XR session normally handled by the  instance. Toggling this to false also toggles
///  false.
///
/// Disabling  only implies that the user is responsible for starting and stopping
/// the  through the  and  APIs.
///
/// Automatic lifecycle management is executed as follows
///
/// * Runtime Initialize -> . The loader list will be iterated over and the first successful loader will be set as the active loader.
/// * Start -> . Ask the active loader to start all subsystems.
/// * OnDisable -> . Ask the active loader to stop all subsystems.
/// * OnDestroy -> . Deinitialize and remove the active loader.
/// 
public sealed class XRManagerSettings : ScriptableObject
{
[HideInInspector]
bool m_InitializationComplete = false;
#pragma warning disable 414
// This property is only used by the scriptable object editing part of the system and as such no one
// directly references it. Have to manually disable the console warning here so that we can
// get a clean console report.
[HideInInspector]
[SerializeField]
bool m_RequiresSettingsUpdate = false;
#pragma warning restore 414
[SerializeField]
[Tooltip("Determines if the XR Manager instance is responsible for creating and destroying the appropriate loader instance.")]
[FormerlySerializedAs("AutomaticLoading")]
bool m_AutomaticLoading = false;
/// 
/// Get and set Automatic Loading state for this manager. When this is true, the manager will automatically call
///  and  for you. When false 
/// is also set to false and remains that way. This means that disabling automatic loading disables all automatic behavior
/// for the manager.
/// 
public bool automaticLoading
{
get { return m_AutomaticLoading; }
set { m_AutomaticLoading = value; }
}
[SerializeField]
[Tooltip("Determines if the XR Manager instance is responsible for starting and stopping subsystems for the active loader instance.")]
[FormerlySerializedAs("AutomaticRunning")]
bool m_AutomaticRunning = false;
/// 
/// Get and set automatic running state for this manager. When set to true the manager will call 
/// and  APIs at appropriate times. When set to false, or when  is false
/// then it is up to the user of the manager to handle that same functionality.
/// 
public bool automaticRunning
{
get { return m_AutomaticRunning; }
set { m_AutomaticRunning = value; }
}
[SerializeField]
[Tooltip("List of XR Loader instances arranged in desired load order.")]
[FormerlySerializedAs("Loaders")]
List m_Loaders = new List();
// Maintains a list of registered loaders that is immutable at runtime.
[SerializeField]
[HideInInspector]
HashSet m_RegisteredLoaders = new HashSet();
/// 
/// List of loaders currently managed by this XR Manager instance.
/// 
/// 
/// Modifying the list of loaders at runtime is undefined behavior and could result in a crash or memory leak.
/// Use  to retrieve the currently ordered list of loaders. If you need to mutate
/// the list at runtime, use , , and
/// .
/// 
[Obsolete("'XRManagerSettings.loaders' property is obsolete. Use 'XRManagerSettings.activeLoaders' instead to get a list of the current loaders.")]
public List loaders
{
get { return m_Loaders; }
#if UNITY_EDITOR
set { m_Loaders = value; }
#endif
}
/// 
/// A shallow copy of the list of loaders currently managed by this XR Manager instance.
/// 
/// 
/// This property returns a read only list. Any changes made to the list itself will not affect the list
/// used by this XR Manager instance. To mutate the list of loaders currently managed by this instance,
/// use , , and/or .
/// 
public IReadOnlyList activeLoaders => m_Loaders;
/// 
/// Read only boolean letting us know if initialization is completed. Because initialization is
/// handled as a Coroutine, people taking advantage of the auto-lifecycle management of XRManager
/// will need to wait for init to complete before checking for an ActiveLoader and calling StartSubsystems.
/// 
public bool isInitializationComplete
{
get { return m_InitializationComplete; }
}
///
/// Return the current singleton active loader instance.
///
[HideInInspector]
public XRLoader activeLoader { get; private set; }
/// 
/// Return the current active loader, cast to the requested type. Useful shortcut when you need
/// to get the active loader as something less generic than XRLoader.
/// 
///
/// Requested type of the loader
///
/// The active loader as requested type, or null.
public T ActiveLoaderAs() where T : XRLoader
{
return activeLoader as T;
}
/// 
/// Iterate over the configured list of loaders and attempt to initialize each one. The first one
/// that succeeds is set as the active loader and initialization immediately terminates.
///
/// When complete  will be set to true. This will mark that it is safe to
/// call other parts of the API. This does not guarantee that init successfully created a loader. For that
/// you need to check that ActiveLoader is not null.
///
/// Note that there can only be one active loader. Any attempt to initialize a new active loader with one
/// already set will cause a warning to be logged and immediate exit of this function.
///
/// This method is synchronous and on return all state should be immediately checkable.
///
/// If manual initialization of XR is being done, this method can not be called before Start completes
/// as it depends on graphics initialization within Unity completing.
/// 
public void InitializeLoaderSync()
{
if (activeLoader != null)
{
Debug.LogWarning(
"XR Management has already initialized an active loader in this scene." +
" Please make sure to stop all subsystems and deinitialize the active loader before initializing a new one.");
return;
}
foreach (var loader in currentLoaders)
{
if (loader != null)
{
if (CheckGraphicsAPICompatibility(loader) && loader.Initialize())
{
activeLoader = loader;
m_InitializationComplete = true;
return;
}
}
}
activeLoader = null;
}
/// 
/// Iterate over the configured list of loaders and attempt to initialize each one. The first one
/// that succeeds is set as the active loader and initialization immediately terminates.
///
/// When complete  will be set to true. This will mark that it is safe to
/// call other parts of the API. This does not guarantee that init successfully created a loader. For that
/// you need to check that ActiveLoader is not null.
///
/// Note that there can only be one active loader. Any attempt to initialize a new active loader with one
/// already set will cause a warning to be logged and immediate exit of this function.
///
/// Iteration is done asynchronously and this method must be called within the context of a Coroutine.
///
/// If manual initialization of XR is being done, this method can not be called before Start completes
/// as it depends on graphics initialization within Unity completing.
/// 
///
/// Enumerator marking the next spot to continue execution at.
public IEnumerator InitializeLoader()
{
if (activeLoader != null)
{
Debug.LogWarning(
"XR Management has already initialized an active loader in this scene." +
" Please make sure to stop all subsystems and deinitialize the active loader before initializing a new one.");
yield break;
}
foreach (var loader in currentLoaders)
{
if (loader != null)
{
if (CheckGraphicsAPICompatibility(loader) && loader.Initialize())
{
activeLoader = loader;
m_InitializationComplete = true;
yield break;
}
}
yield return null;
}
activeLoader = null;
}
/// 
/// Attempts to append the given loader to the list of loaders at the given index.
/// 
/// 
/// The  to be added to this manager's instance of loaders.
/// 
/// 
/// The index at which the given  should be added. If you set a negative or otherwise
/// invalid index, the loader will be appended to the end of the list.
/// 
/// 
/// true if the loader is not a duplicate and was added to the list successfully, false
/// otherwise.
/// 
/// 
/// This method behaves differently in the Editor and during runtime/Play mode. While your app runs in the Editor and not in
/// Play mode, attempting to add an  will always succeed and register that loader's type
/// internally. Attempting to add a loader during runtime/Play mode will trigger a check to see whether a loader of
/// that type was registered. If the check is successful, the loader is added. If not, the loader is not added and the method
/// returns false.
/// 
public bool TryAddLoader(XRLoader loader, int index = -1)
{
if (loader == null || currentLoaders.Contains(loader))
return false;
#if UNITY_EDITOR
if (!EditorApplication.isPlaying && !m_RegisteredLoaders.Contains(loader))
m_RegisteredLoaders.Add(loader);
#endif
if (!m_RegisteredLoaders.Contains(loader))
return false;
if (index = currentLoaders.Count)
currentLoaders.Add(loader);
else
currentLoaders.Insert(index, loader);
return true;
}
/// 
/// Attempts to remove the first instance of a given loader from the list of loaders.
/// 
/// 
/// The  to be removed from this manager's instance of loaders.
/// 
/// 
/// true if the loader was successfully removed from the list, false otherwise.
/// 
/// 
/// This method behaves differently in the Editor and during runtime/Play mode. During runtime/Play mode, the loader
/// will be removed with no additional side effects if it is in the list managed by this instance. While in the
/// Editor and not in Play mode, the loader will be removed if it exists and
/// it will be unregistered from this instance and any attempts to add it during
/// runtime/Play mode will fail. You can re-add the loader in the Editor while not in Play mode.
/// 
public bool TryRemoveLoader(XRLoader loader)
{
var removedLoader = true;
if (currentLoaders.Contains(loader))
removedLoader = currentLoaders.Remove(loader);
#if UNITY_EDITOR
if (!EditorApplication.isPlaying && !currentLoaders.Contains(loader))
m_RegisteredLoaders.Remove(loader);
#endif
return removedLoader;
}
/// 
/// Attempts to set the given loader list as the list of loaders managed by this instance.
/// 
/// 
/// The list of s to be managed by this manager instance.
/// 
/// 
/// true if the loader list was set successfully, false otherwise.
/// 
/// 
/// This method behaves differently in the Editor and during runtime/Play mode. While in the Editor and not in
/// Play mode, any attempts to set the list of loaders will succeed without any additional checks. During
/// runtime/Play mode, the new loader list will be validated against the registered  types.
/// If any loaders exist in the list that were not registered at startup, the attempt will fail.
/// 
public bool TrySetLoaders(List reorderedLoaders)
{
var originalLoaders = new List(activeLoaders);
#if UNITY_EDITOR
if (!EditorApplication.isPlaying)
{
registeredLoaders.Clear();
currentLoaders.Clear();
foreach (var loader in reorderedLoaders)
{
if (!TryAddLoader(loader))
{
TrySetLoaders(originalLoaders);
return false;
}
}
return true;
}
#endif
currentLoaders.Clear();
foreach (var loader in reorderedLoaders)
{
if (!TryAddLoader(loader))
{
currentLoaders = originalLoaders;
return false;
}
}
return true;
}
private bool CheckGraphicsAPICompatibility(XRLoader loader)
{
GraphicsDeviceType deviceType = SystemInfo.graphicsDeviceType;
List supportedDeviceTypes = loader.GetSupportedGraphicsDeviceTypes(false);
// To help with backward compatibility, if the compatibility list is empty we assume that it does not implement the GetSupportedGraphicsDeviceTypes method
// Therefore we revert to the previous behavior of building or starting the loader regardless of gfx api settings.
if (supportedDeviceTypes.Count > 0 && !supportedDeviceTypes.Contains(deviceType))
{
Debug.LogWarning(String.Format("The {0} does not support the initialized graphics device, {1}. Please change the preffered Graphics API in PlayerSettings. Attempting to start the next XR loader.", loader.name, deviceType.ToString()));
return false;
}
return true;
}
/// 
/// If there is an active loader, this will request the loader to start all the subsystems that it
/// is managing.
///
/// You must wait for  to be set to true prior to calling this API.
/// 
public void StartSubsystems()
{
if (!m_InitializationComplete)
{
Debug.LogWarning(
"Call to StartSubsystems without an initialized manager." +
"Please make sure wait for initialization to complete before calling this API.");
return;
}
if (activeLoader != null)
{
activeLoader.Start();
}
}
/// 
/// If there is an active loader, this will request the loader to stop all the subsystems that it
/// is managing.
///
/// You must wait for  to be set to tru prior to calling this API.
/// 
public void StopSubsystems()
{
if (!m_InitializationComplete)
{
Debug.LogWarning(
"Call to StopSubsystems without an initialized manager." +
"Please make sure wait for initialization to complete before calling this API.");
return;
}
if (activeLoader != null)
{
activeLoader.Stop();
}
}
/// 
/// If there is an active loader, this function will deinitialize it and remove the active loader instance from
/// management. We will automatically call  prior to deinitialization to make sure
/// that things are cleaned up appropriately.
///
/// You must wait for  to be set to tru prior to calling this API.
///
/// Upon return  will be rest to false;
/// 
public void DeinitializeLoader()
{
if (!m_InitializationComplete)
{
Debug.LogWarning(
"Call to DeinitializeLoader without an initialized manager." +
"Please make sure wait for initialization to complete before calling this API.");
return;
}
StopSubsystems();
if (activeLoader != null)
{
activeLoader.Deinitialize();
activeLoader = null;
}
m_InitializationComplete = false;
}
// Use this for initialization
void Start()
{
if (automaticLoading && automaticRunning)
{
StartSubsystems();
}
}
void OnDisable()
{
if (automaticLoading && automaticRunning)
{
StopSubsystems();
}
}
void OnDestroy()
{
if (automaticLoading)
{
DeinitializeLoader();
}
}
// To modify the list of loaders internally use `currentLoaders` as it will return a list reference rather
// than a shallow copy.
// TODO @davidmo 10/12/2020: remove this in next major version bump and make 'loaders' internal.
internal List currentLoaders
{
get { return m_Loaders; }
set { m_Loaders = value; }
}
// To modify the set of registered loaders use `registeredLoaders` as it will return a reference to the
// hashset of loaders.
internal HashSet registeredLoaders
{
get { return m_RegisteredLoaders; }
}
}
}

事情变的有趣起来,我们知道了这样的原理之后,那鱼蛋我就想着尝试下,在Runtime里动态切换行吧,SteamVR场景切换到OpenVR Loader,而XR场景切换到OpenXR,代码如下。

using System.Collections.Generic;
using Unity.XR.OpenVR;
using UnityEngine;
using UnityEngine.XR.Management;
using UnityEngine.XR.OpenXR;
namespace EgoGame
{
/// 
/// 该类有问题,废弃了
/// 
public class AutoXRLoader:MonoBehaviour
{
public List xrLoaders;
public List vrLoaders;
public bool isXR;
private void Awake()
{
SetLoader(isXR);
}
private void OnDestroy()
{
SetLoader(!isXR);
}
void SetLoader(bool xr)
{
//不这样,会频繁的退出loader,VR会没画面
if (xr && XRGeneralSettings.Instance.Manager.activeLoader is OpenXRLoader)
{
return;
}
if (!xr && XRGeneralSettings.Instance.Manager.activeLoader is OpenVRLoader)
{
return;
}
var loaders = xr ? xrLoaders : vrLoaders;
Debug.Log("切换Loader:" + xr+"=="+XRGeneralSettings.Instance.Manager.activeLoader);
XRGeneralSettings.Instance.Manager.DeinitializeLoader();
XRGeneralSettings.Instance.Manager.TrySetLoaders(loaders);
XRGeneralSettings.Instance.Manager.InitializeLoaderSync();
XRGeneralSettings.Instance.Manager.StartSubsystems();
}
}
}

果然奏效了,XR场景能在头盔里识别并运行了,手柄也能控制。但是,切到SteamVR场景就出现了问题,Steam VR SDK报错了,报错提示有另一个应用在使用SteamVR。

XR和Steam VR项目合并问题插图(2)

最后的结果就是,没法实现动态切换XR或VR,如果看到此处的人,有办法请告诉我,我尝试了两天用了各种办法,都没法做到。

最后推荐大家开发VR应用不要直接用SteamVR SDK或XR SDK或Oculus SDK开发,而是用那些集成的插件,如VR Interaction Framework、VRTK等,这样在多个VR设备也能快速部署。

本站无任何商业行为
个人在线分享 » XR和Steam VR项目合并问题
E-->