// Copyright (c) Meta Platforms, Inc. and affiliates. #include "OculusXRInputOpenXR.h" #include "Haptics/HapticFeedbackEffect_Base.h" #include "OculusXRInputExtensionPlugin.h" #include "OculusXRInputModule.h" #include "OculusXRInputXRFunctions.h" #include "OpenXR/OculusXROpenXRUtilities.h" #include "OpenXRCore.h" #include namespace OculusXRInput { FOculusXRInputModule* GetInputModule() { FOculusXRInputModule* InputModule = static_cast(&FOculusXRInputModule::Get()); if (!InputModule) { UE_LOG(LogOcInput, Error, TEXT("Failed getting Oculus XR input module.")); return nullptr; } return InputModule; } bool InstanceAndSessionAreValid( const FOculusXRInputModule* InputModule) { if (!InputModule->GetHapticsOpenXRExtension()->GetOpenXRInstance() || !InputModule->GetHapticsOpenXRExtension()->GetOpenXRSession()) { UE_LOG(LogOcInput, Error, TEXT("Failed getting OpenXR instance or session.")); return false; } return true; } int ControllerHandToHandIndex(EControllerHand Hand) { int HandIndex = -1; switch (Hand) { case EControllerHand::Left: HandIndex = 0; break; case EControllerHand::Right: HandIndex = 1; break; default: UE_LOG(LogOcInput, Error, TEXT("No action defined for %s."), *UEnum::GetValueAsString(Hand)); } return HandIndex; } XrAction LocationToXrAction(EOculusXRHandHapticsLocation Location) { const FOculusXRInputModule* InputModule = GetInputModule(); if (Location != EOculusXRHandHapticsLocation::Hand && !InputModule->GetHapticsOpenXRExtension()->IsTouchControllerProExtensionAvailable()) { UE_LOG(LogOcInput, Warning, TEXT("Touch Controller Pro extension is not available.")); return XR_NULL_HANDLE; } switch (Location) { case EOculusXRHandHapticsLocation::Hand: return InputModule->GetHapticsOpenXRExtension()->GetXrHandHapticVibrationAction(); case EOculusXRHandHapticsLocation::Thumb: return InputModule->GetHapticsOpenXRExtension()->GetXrThumbHapticVibrationAction(); case EOculusXRHandHapticsLocation::Index: return InputModule->GetHapticsOpenXRExtension()->GetXrIndexHapticVibrationAction(); default: UE_LOG(LogOcInput, Warning, TEXT("Invalid location specified (%d)"), (int32)Location); } return XR_NULL_HANDLE; } float FOculusXRInputOpenXR::GetControllerSampleRateHz(EControllerHand Hand) const { const FOculusXRInputModule* InputModule = GetInputModule(); if (!InputModule || !InstanceAndSessionAreValid(InputModule)) { return 0.f; } if (!InputModule->GetHapticsOpenXRExtension()->IsPCMExtensionAvailable()) { UE_LOG(LogOcInput, Warning, TEXT("PCM extension is not available.")); return 0.f; } const int HandIndex = ControllerHandToHandIndex(Hand); if (HandIndex == -1) { return 0.f; } XrHapticActionInfo HapticActionInfo = { XR_TYPE_HAPTIC_ACTION_INFO }; HapticActionInfo.action = InputModule->GetHapticsOpenXRExtension()->GetXrHandHapticVibrationAction(); HapticActionInfo.subactionPath = InputModule->GetHapticsOpenXRExtension()->GetXrHandsSubactionPaths()[HandIndex]; HapticActionInfo.next = nullptr; XrDevicePcmSampleRateGetInfoFB DeviceSampleRate = { XR_TYPE_DEVICE_PCM_SAMPLE_RATE_GET_INFO_FB }; const XrResult result = xrGetDeviceSampleRateFB(InputModule->GetHapticsOpenXRExtension()->GetOpenXRSession(), &HapticActionInfo, &DeviceSampleRate); if (XR_FAILED(result)) { UE_LOG(LogOcInput, Error, TEXT("xrGetDeviceSampleRateFB failed.")); return 0.f; } return DeviceSampleRate.sampleRate; } int FOculusXRInputOpenXR::GetMaxHapticDuration(EControllerHand Hand) const { const float SampleRate = GetControllerSampleRateHz(Hand); if (SampleRate == 0.f) { UE_LOG(LogOcInput, Warning, TEXT("Sample rate equals 0")); return 0; } return XR_MAX_HAPTIC_PCM_BUFFER_SIZE_FB / SampleRate; } void FOculusXRInputOpenXR::Tick(float DeltaTime) { if (ActiveHapticEffect_Left.IsValid()) { FHapticFeedbackValues LeftHaptics; const bool bPlaying = ActiveHapticEffect_Left->Update(DeltaTime, LeftHaptics); if (!bPlaying) { ActiveHapticEffect_Left->bLoop ? HapticsDesc_Left->Restart() : HapticsDesc_Left.Reset(); ActiveHapticEffect_Left->bLoop ? ActiveHapticEffect_Left->Restart() : ActiveHapticEffect_Left.Reset(); } SetHapticFeedbackValues(EControllerHand::Left, LeftHaptics, HapticsDesc_Left.Get()); } if (ActiveHapticEffect_Right.IsValid()) { FHapticFeedbackValues RightHaptics; const bool bPlaying = ActiveHapticEffect_Right->Update(DeltaTime, RightHaptics); if (!bPlaying) { ActiveHapticEffect_Right->bLoop ? HapticsDesc_Right->Restart() : HapticsDesc_Right.Reset(); ActiveHapticEffect_Right->bLoop ? ActiveHapticEffect_Right->Restart() : ActiveHapticEffect_Right.Reset(); } SetHapticFeedbackValues(EControllerHand::Right, RightHaptics, HapticsDesc_Right.Get()); } } // Tick will only get called if the object is created in FOculusXRInput::GetOculusXRInputBaseImpl(), so we do not need ETickableTickType::Conditional ETickableTickType FOculusXRInputOpenXR::GetTickableTickType() const { return ETickableTickType::Always; } TStatId FOculusXRInputOpenXR::GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(FOculusXRInputOpenXR, STATGROUP_Tickables); } void FOculusXRInputOpenXR::SetHapticFeedbackValues(EControllerHand Hand, const FHapticFeedbackValues& Values, FOculusXRHapticsDesc* HapticsDesc) { FHapticFeedbackBuffer* const HapticBuffer = Values.HapticBuffer; const bool bHasBuffer = HapticBuffer && HapticBuffer->BufferLength > 0; // UHapticFeedbackEffect_SoundWave if (bHasBuffer) { const FOculusXRInputModule* InputModule = GetInputModule(); if (!InputModule || !InstanceAndSessionAreValid(InputModule)) { return; } if (!InputModule->GetHapticsOpenXRExtension()->IsPCMExtensionAvailable()) { UE_LOG(LogOcInput, Warning, TEXT("PCM extension is not available.")); return; } int SamplesToSend = 0.036f * HapticBuffer->SamplingRate; // Related to the duration that each PCM haptic batch lasts (36ms) if (SamplesToSend == 0 || HapticBuffer->SamplesSent == HapticBuffer->BufferLength) { return; } // Makes sure we are not overloading it SamplesToSend = FMath::Min(SamplesToSend, XR_MAX_HAPTIC_PCM_BUFFER_SIZE_FB); SamplesToSend = FMath::Min(SamplesToSend, (HapticBuffer->BufferLength - HapticBuffer->SamplesSent) / 2); uint32_t SamplesConsumed = 0; std::vector PCMBuffer(SamplesToSend); for (int i = 0; i < SamplesToSend; i++) { const uint32 DataIndex = HapticBuffer->CurrentPtr + (i * 2); const int16* const RawData = reinterpret_cast(&HapticBuffer->RawData[DataIndex]); float SampleValue = (*RawData * HapticBuffer->ScaleFactor) / INT16_MAX; SampleValue = FMath::Min(1.0f, SampleValue); SampleValue = FMath::Max(-1.0f, SampleValue); PCMBuffer[i] = SampleValue; } XrHapticActionInfo HapticActionInfo = { XR_TYPE_HAPTIC_ACTION_INFO }; HapticActionInfo.action = LocationToXrAction(HapticsDesc->Location); HapticActionInfo.subactionPath = InputModule->GetHapticsOpenXRExtension()->GetXrHandsSubactionPaths()[ControllerHandToHandIndex(Hand)]; XrHapticPcmVibrationFB HapticPcmVibration = { XR_TYPE_HAPTIC_PCM_VIBRATION_FB }; HapticPcmVibration.buffer = PCMBuffer.data(); HapticPcmVibration.bufferSize = SamplesToSend; HapticPcmVibration.sampleRate = HapticBuffer->SamplingRate; HapticPcmVibration.samplesConsumed = &SamplesConsumed; HapticPcmVibration.append = HapticsDesc->bIsFirstCall ? HapticsDesc->bAppend : true; const XrResult result = xrApplyHapticFeedback(InputModule->GetHapticsOpenXRExtension()->GetOpenXRSession(), &HapticActionInfo, reinterpret_cast(&HapticPcmVibration)); if (XR_FAILED(result)) { UE_LOG(LogOcInput, Error, TEXT("xrApplyHapticFeedback failed for PCM haptics with result %s"), OpenXRResultToString(result)); } HapticsDesc->bIsFirstCall = false; HapticBuffer->CurrentPtr = FMath::Min(HapticBuffer->CurrentPtr + SamplesConsumed * 2, static_cast(HapticBuffer->BufferLength)); HapticBuffer->SamplesSent = FMath::Min(HapticBuffer->SamplesSent + SamplesConsumed * 2, static_cast(HapticBuffer->BufferLength)); } // UHapticFeedbackEffect_Curve and UHapticFeedbackEffect_Buffer else { SetHapticsByValue(Values.Frequency, Values.Amplitude, Hand, HapticsDesc ? HapticsDesc->Location : EOculusXRHandHapticsLocation::Hand); } } void FOculusXRInputOpenXR::PlayHapticEffect( UHapticFeedbackEffect_Base* HapticEffect, EControllerHand Hand, EOculusXRHandHapticsLocation Location, bool bAppend, float Scale, bool bLoop) { if (!HapticEffect) { return; } switch (Hand) { case EControllerHand::Left: ActiveHapticEffect_Left.Reset(); ActiveHapticEffect_Left = MakeShareable(new FActiveHapticFeedbackEffect(HapticEffect, Scale, bLoop)); HapticsDesc_Left.Reset(); HapticsDesc_Left = MakeShareable(new FOculusXRHapticsDesc(Location, bAppend)); break; case EControllerHand::Right: ActiveHapticEffect_Right.Reset(); ActiveHapticEffect_Right = MakeShareable(new FActiveHapticFeedbackEffect(HapticEffect, Scale, bLoop)); HapticsDesc_Right.Reset(); HapticsDesc_Right = MakeShareable(new FOculusXRHapticsDesc(Location, bAppend)); break; default: UE_LOG(LogOcInput, Warning, TEXT("Invalid hand specified (%d) for haptic feedback effect %s"), (int32)Hand, *HapticEffect->GetName()); break; } } void FOculusXRInputOpenXR::PlayAmplitudeEnvelopeHapticEffect(EControllerHand Hand, int SamplesCount, void* Samples, int InSampleRate) { const FOculusXRInputModule* InputModule = GetInputModule(); if (!InputModule || !InstanceAndSessionAreValid(InputModule)) { return; } if (!InputModule->GetHapticsOpenXRExtension()->IsAmplitudeEnvelopeExtensionAvailable()) { UE_LOG(LogOcInput, Warning, TEXT("Amplitude Envelope extension is not available.")); return; } const int MaxTimeToSend = GetMaxHapticDuration(Hand); if (MaxTimeToSend == 0) { return; } const int SampleRate = InSampleRate > 0 ? InSampleRate : GetControllerSampleRateHz(Hand); if (SamplesCount > XR_MAX_HAPTIC_AMPLITUDE_ENVELOPE_SAMPLES_FB || SamplesCount < 1) { UE_LOG(LogOcInput, Warning, TEXT("Sample count should be between 1 and %d which last %d seconds."), XR_MAX_HAPTIC_PCM_BUFFER_SIZE_FB, MaxTimeToSend); } const int AmplitudesCount = FMath::Min(SamplesCount, static_cast(XR_MAX_HAPTIC_AMPLITUDE_ENVELOPE_SAMPLES_FB)); std::vector AmplitudesToSend(AmplitudesCount); for (int i = 0; i < AmplitudesCount; i++) { float Amplitude = static_cast(Samples)[i] / 255.0f; Amplitude = FMath::Min(1.0f, Amplitude); Amplitude = FMath::Max(0.0f, Amplitude); AmplitudesToSend[i] = Amplitude; } XrHapticActionInfo HapticActionInfo = { XR_TYPE_HAPTIC_ACTION_INFO }; HapticActionInfo.action = InputModule->GetHapticsOpenXRExtension()->GetXrHandHapticVibrationAction(); HapticActionInfo.subactionPath = InputModule->GetHapticsOpenXRExtension()->GetXrHandsSubactionPaths()[ControllerHandToHandIndex(Hand)]; XrHapticAmplitudeEnvelopeVibrationFB HapticAmplitudeEnvelopeVibration = { XR_TYPE_HAPTIC_AMPLITUDE_ENVELOPE_VIBRATION_FB }; HapticAmplitudeEnvelopeVibration.duration = OculusXR::ToXrDuration(static_cast(AmplitudesCount) / SampleRate); HapticAmplitudeEnvelopeVibration.amplitudeCount = static_cast(AmplitudesCount); HapticAmplitudeEnvelopeVibration.amplitudes = AmplitudesToSend.data(); const XrResult result = xrApplyHapticFeedback(InputModule->GetHapticsOpenXRExtension()->GetOpenXRSession(), &HapticActionInfo, reinterpret_cast(&HapticAmplitudeEnvelopeVibration)); if (XR_FAILED(result)) { UE_LOG(LogOcInput, Error, TEXT("xrApplyHapticFeedback failed for amplitude envelope haptics with result %s"), OpenXRResultToString(result)); } } void FOculusXRInputOpenXR::SetHapticsByValue(float Frequency, float Amplitude, EControllerHand Hand, EOculusXRHandHapticsLocation Location) { const FOculusXRInputModule* InputModule = GetInputModule(); if (!InputModule || !InstanceAndSessionAreValid(InputModule)) { return; } const int HandIndex = ControllerHandToHandIndex(Hand); if (HandIndex == -1) { return; } XrHapticActionInfo HapticActionInfo = { XR_TYPE_HAPTIC_ACTION_INFO }; HapticActionInfo.action = LocationToXrAction(Location); HapticActionInfo.subactionPath = InputModule->GetHapticsOpenXRExtension()->GetXrHandsSubactionPaths()[ControllerHandToHandIndex(Hand)]; XrHapticVibration Vibration = { XR_TYPE_HAPTIC_VIBRATION }; Vibration.amplitude = Amplitude; Vibration.frequency = Frequency; Vibration.duration = 2000000000; // 2 second duration, this is to give enough // time for a new signal to be received without // stopping the previous vibration const XrResult Result = xrApplyHapticFeedback(InputModule->GetHapticsOpenXRExtension()->GetOpenXRSession(), &HapticActionInfo, reinterpret_cast(&Vibration)); if (XR_FAILED(Result)) { UE_LOG(LogOcInput, Error, TEXT("xrApplyHapticFeedback failed with result %s"), OpenXRResultToString(Result)); } } } // namespace OculusXRInput