Files
Ipseity-Demo/Assets/Wwise/OpenXR/Runtime/AkWwiseOpenXRMotionFeature.cs
T
2026-03-23 14:28:21 +01:00

211 lines
8.2 KiB
C#

/*******************************************************************************
The content of this file includes portions of the proprietary AUDIOKINETIC Wwise
Technology released in source code form as part of the game integration package.
The content of this file may not be used without valid licenses to the AUDIOKINETIC
Wwise Technology.
Note that the use of the game engine is subject to the Unity Terms of Service
at https://unity3d.com/legal/terms-of-service.
License Usage
Licensees holding valid licenses to the AUDIOKINETIC Wwise Technology may use
this file in accordance with the end user license agreement provided with the
software or, alternatively, in accordance with the terms contained
in a written agreement between you and Audiokinetic Inc.
Copyright (c) 2026 Audiokinetic Inc.
*******************************************************************************/
#if AK_ENABLE_OPENXR
using UnityEngine;
using UnityEngine.XR.OpenXR;
using UnityEngine.XR.OpenXR.Features;
using AK.Wwise.Unity.Logging;
using UnityEngine.InputSystem;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.XR.OpenXR.Features;
#endif
namespace AK.Wwise.Unity.OpenXR
{
/// <summary>
/// Enables Wwise Motion haptics support for Meta Quest devices via OpenXR.
/// This feature bridges Unity's OpenXR plugin with the Wwise AkMotion plugin.
/// </summary>
#if UNITY_EDITOR
[OpenXRFeature(
UiName = "Wwise OpenXR Motion",
BuildTargetGroups = new[] { BuildTargetGroup.Android, BuildTargetGroup.Standalone },
Company = "Audiokinetic",
Desc = "Wwise Motion haptics support via OpenXR for Meta Quest devices.",
DocumentationLink = "https://www.audiokinetic.com/",
OpenxrExtensionStrings = "XR_FB_haptic_pcm",
Version = "1.0.0",
FeatureId = featureId
)]
#endif
public class AkWwiseOpenXRMotionFeature : OpenXRFeature
{
public const string featureId = "com.audiokinetic.wwise.openxr.motion";
// Event that fires when OpenXR Motion is initialized and ready.
public static event System.Action OnOpenXRMotionInitialized;
private static bool s_isInitialized = false;
public static bool IsInitialized => s_isInitialized;
// Unity creates one feature instance per BuildTargetGroup (Android, Standalone),
// but only the active build target's instance receives OpenXR callbacks (OnInstanceCreate, OnSessionCreate).
// We set s_instance in OnInstanceCreate() to identify which instance has valid OpenXR handles.
private static AkWwiseOpenXRMotionFeature s_instance = null;
// OpenXR handles
private ulong m_xrInstance = 0;
private ulong m_xrSession = 0;
private ulong m_xrHapticAction = 0;
private System.IntPtr m_xrGetInstanceProcAddr = System.IntPtr.Zero;
public static AkWwiseOpenXRMotionFeature Instance
{
get
{
if (s_instance == null)
{
s_instance = OpenXRSettings.Instance?.GetFeature<AkWwiseOpenXRMotionFeature>();
}
return s_instance;
}
}
protected override void OnDisable()
{
if (s_instance == this) // Only clear s_instance if we're the active instance
{
s_instance = null;
}
base.OnDisable();
}
protected override System.IntPtr HookGetInstanceProcAddr(System.IntPtr func)
{
m_xrGetInstanceProcAddr = func;
return base.HookGetInstanceProcAddr(func);
}
// Called when an OpenXR instance is created. Captures the XrInstance handle.
// Only called for the active build target's feature instance.
protected override bool OnInstanceCreate(ulong xrInstance)
{
m_xrInstance = xrInstance;
s_instance = this; // This instance is the active one - set as singleton
return base.OnInstanceCreate(xrInstance);
}
protected override void OnInstanceDestroy(ulong xrInstance)
{
m_xrInstance = 0;
base.OnInstanceDestroy(xrInstance);
}
// Called when an OpenXR session is created. Captures the XrSession handle.
protected override void OnSessionCreate(ulong xrSession)
{
m_xrSession = xrSession;
InputSystem.onAfterUpdate -= OnInputSystemAfterUpdate;
InputSystem.onAfterUpdate += OnInputSystemAfterUpdate;
base.OnSessionCreate(xrSession);
}
protected override void OnSessionDestroy(ulong xrSession)
{
InputSystem.onAfterUpdate -= OnInputSystemAfterUpdate;
m_xrSession = 0;
m_xrHapticAction = 0;
base.OnSessionDestroy(xrSession);
}
// Will be called until the Input System's controller is ready. Then we unregister the callback.
static void OnInputSystemAfterUpdate()
{
if (AkUnitySoundEngine.IsInitialized())
{
if (Instance && Instance.SetWwiseMotionOpenXREnvironment())
{
InputSystem.onAfterUpdate -= OnInputSystemAfterUpdate;
}
}
}
// This method fails if the Unity Input System is not ready and can be called again on the next frame.
public bool SetWwiseMotionOpenXREnvironment()
{
if (m_xrInstance == 0 || m_xrSession == 0)
{
WwiseLogger.Error("[AkWwiseOpenXRMotionFeature] XrInstance or XrSession are invalid.");
return false;
}
// Getting the haptic action from any of the two controller is valid since it resolves to the same XrAction.
if (!TryGetHapticActionHandle("<XRController>{LeftHand}/haptic") && !TryGetHapticActionHandle("<XRController>{RightHand}/haptic"))
{
return false;
}
if (!AkWwiseOpenXRMotionBridge.Initialize(m_xrGetInstanceProcAddr, m_xrInstance, m_xrSession, m_xrHapticAction))
{
WwiseLogger.Error("[AkWwiseOpenXRMotionFeature] Failed to initialize AkMotion bridge");
return false;
}
WwiseLogger.Log("[AkWwiseOpenXRMotionFeature] Wwise Motion OpenXR integration initialized successfully!");
s_isInitialized = true;
OnOpenXRMotionInitialized?.Invoke();
return true;
}
// Retrieves the XrAction handle by piggybacking on the controller profile's existing haptic action.
// It will return false silently while waiting for the Input System to bind controls.
private bool TryGetHapticActionHandle(string bindingPath)
{
bool gotAction = false;
// This relies on a controller profile (e.g., Meta Quest Touch Pro Controller Profile) being enabled.
// Note: Any of the hand bindings (LeftHand or RightHand) resolves to the controller profile's unified
// haptic output, which automatically applies to both controllers. This is handled by the OpenXR runtime.
var hapticAction = new UnityEngine.InputSystem.InputAction(
name: "WwiseMotionHaptic",
type: UnityEngine.InputSystem.InputActionType.PassThrough,
binding: bindingPath
);
hapticAction.Enable();
// The control count can be 0 when Unity's Input System is not ready yet. This could happen because the controller profile
// is not loaded, the controllers are not detected or action's path is not bound. We need to try again later.
if (hapticAction.controls.Count > 0)
{
m_xrHapticAction = GetAction(hapticAction); // Get the native XrAction handle from Unity OpenXR
if (m_xrHapticAction != 0)
{
gotAction = true;
}
else
{
WwiseLogger.Error("[AkWwiseOpenXRMotionFeature] Could not get XrAction with control bound.");
}
}
hapticAction.Disable(); // Does not need to be enabled once we have the XrAction handle.
return gotAction;
}
}
}
#endif // AK_ENABLE_OPENXR