Repository: oculus-samples/Unreal-HandGameplay Branch: main Commit: 7f2ee08aec0e Files: 334 Total size: 314.4 KB Directory structure: gitextract_c23nrrk_/ ├── .gitattributes ├── .github/ │ └── ISSUE_TEMPLATE/ │ └── bug_report.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Config/ │ ├── Android/ │ │ └── AndroidEngine.ini │ ├── DefaultDeviceProfiles.ini │ ├── DefaultEditor.ini │ ├── DefaultEngine.ini │ ├── DefaultGame.ini │ └── DefaultInput.ini ├── Content/ │ └── HandGameplay/ │ ├── Audio/ │ │ ├── GrabSound.uasset │ │ ├── LAST_TestAudio_Release01.uasset │ │ ├── LAST_TestAudio_Release02.uasset │ │ ├── LAST_TestAudio_Release03.uasset │ │ ├── ReleaseSound.uasset │ │ ├── SoundAttenuationForSpatialization.uasset │ │ ├── generic_grab_01.uasset │ │ ├── generic_grab_02.uasset │ │ ├── generic_grab_03.uasset │ │ ├── generic_grab_04.uasset │ │ ├── impacts/ │ │ │ ├── sfx_impact_primary_block_01.uasset │ │ │ ├── sfx_impact_primary_block_02.uasset │ │ │ ├── sfx_impact_primary_block_03.uasset │ │ │ ├── sfx_impact_primary_block_04.uasset │ │ │ ├── sfx_impact_primary_block_05.uasset │ │ │ ├── sfx_impact_primary_block_06.uasset │ │ │ ├── sfx_impact_primary_block_07.uasset │ │ │ ├── sfx_impact_primary_block_Cue.uasset │ │ │ ├── sfx_impact_secondary_block_01.uasset │ │ │ ├── sfx_impact_secondary_block_02.uasset │ │ │ ├── sfx_impact_secondary_block_03.uasset │ │ │ ├── sfx_impact_secondary_block_04.uasset │ │ │ ├── sfx_impact_secondary_block_05.uasset │ │ │ ├── sfx_impact_secondary_block_06.uasset │ │ │ ├── sfx_impact_secondary_block_07.uasset │ │ │ ├── sfx_impact_secondary_block_08.uasset │ │ │ ├── sfx_impact_secondary_block_09.uasset │ │ │ ├── sfx_impact_secondary_block_10.uasset │ │ │ ├── sfx_impact_secondary_block_11.uasset │ │ │ ├── sfx_impact_secondary_block_12.uasset │ │ │ ├── sfx_impact_secondary_block_13.uasset │ │ │ └── sfx_impact_secondary_block_Cue.uasset │ │ ├── laser/ │ │ │ ├── LaserSound.uasset │ │ │ ├── sfx_laser_end.uasset │ │ │ ├── sfx_laser_lp.uasset │ │ │ └── sfx_laser_start.uasset │ │ ├── mus_showcase_hands01.uasset │ │ └── sfx_ambient_outdoor_forest_ambix.uasset │ ├── Blueprints/ │ │ ├── HandsCharacter.uasset │ │ └── HandsGameMode.uasset │ ├── Environment/ │ │ ├── DustMotes/ │ │ │ ├── DustMotes_mat.uasset │ │ │ ├── DustMotes_system.uasset │ │ │ ├── DustMotes_vfx.uasset │ │ │ ├── T_Epic_sub_dustParticle.uasset │ │ │ ├── vfxm_Dust.uasset │ │ │ ├── vfxm_GradientCircle.uasset │ │ │ └── vfxm_GradientCircle_Inst.uasset │ │ ├── EnvMatParms.uasset │ │ ├── HT_cube_Tex.uasset │ │ ├── snow_envi/ │ │ │ └── materials/ │ │ │ └── front_rock_snow_bc.uasset │ │ ├── summer_envi/ │ │ │ ├── back_wall_bc.uasset │ │ │ ├── back_wall_mat.uasset │ │ │ ├── back_wall_sm.uasset │ │ │ ├── cieling_bc.uasset │ │ │ ├── cieling_mat.uasset │ │ │ ├── cieling_sm.uasset │ │ │ ├── cloud.uasset │ │ │ ├── cloud_sm.uasset │ │ │ ├── clouds_bc.uasset │ │ │ ├── collision.uasset │ │ │ ├── front_rock_bc.uasset │ │ │ ├── front_rock_mat.uasset │ │ │ ├── front_rock_sm.uasset │ │ │ ├── front_statue_bc.uasset │ │ │ ├── front_statue_mat.uasset │ │ │ ├── front_statue_sm.uasset │ │ │ ├── grass_bc.uasset │ │ │ ├── grass_mat.uasset │ │ │ ├── grass_sm.uasset │ │ │ ├── left_wall_bc.uasset │ │ │ ├── left_wall_mat.uasset │ │ │ ├── left_wall_sm.uasset │ │ │ ├── main_floor_bc.uasset │ │ │ ├── main_floor_mat.uasset │ │ │ ├── main_floor_sm.uasset │ │ │ ├── right_wall_bc.uasset │ │ │ ├── right_wall_mat.uasset │ │ │ ├── right_wall_sm.uasset │ │ │ └── vines_bc.uasset │ │ ├── vfxm_WindFoliage.uasset │ │ ├── vfxm_WindGrass_Inst.uasset │ │ ├── vfxm_WindVines_Inst.uasset │ │ └── vfxsm_vinesVtxAlpha.uasset │ ├── Hands/ │ │ ├── Models/ │ │ │ ├── HandMat.uasset │ │ │ ├── OculusHand_L.uasset │ │ │ ├── OculusHand_L_Skeleton.uasset │ │ │ ├── OculusHand_R.uasset │ │ │ └── OculusHand_R_Skeleton.uasset │ │ ├── TutorialHand.uasset │ │ ├── TutorialHandLeft.uasset │ │ └── TutorialHandRight.uasset │ ├── Input/ │ │ ├── Actions/ │ │ │ ├── AvatarLeftSystemGesture.uasset │ │ │ ├── LogGestureState.uasset │ │ │ ├── LogLeftHand.uasset │ │ │ ├── LogRightHand.uasset │ │ │ ├── TogglePoseRecording.uasset │ │ │ ├── TogglePoseRecordingLeft.uasset │ │ │ └── TogglePoseRecordingRight.uasset │ │ └── InputMappingContext.uasset │ ├── Levels/ │ │ ├── HandGameplayShowcase.umap │ │ ├── HandRecognitionShowcaseArt.umap │ │ ├── HandRecognitionShowcaseAudio.umap │ │ └── HandRecognitionShowcaseVFX.umap │ ├── Props/ │ │ ├── Blocks/ │ │ │ ├── Blocks001_Throwable_sm.uasset │ │ │ ├── Blocks01_bc.uasset │ │ │ ├── Blocks01_bc_Emissive.uasset │ │ │ ├── Blocks01_nm.uasset │ │ │ ├── InteractableBrick.uasset │ │ │ ├── NS_BrickImpact.uasset │ │ │ ├── T_Epic_SUB_UV_Small_Rocks.uasset │ │ │ ├── vfxm_Blocks01_Throwable.uasset │ │ │ ├── vfxm_Blocks01_Throwable_Inst.uasset │ │ │ └── vfxm_ParticleSpriteUnlitSubUV.uasset │ │ ├── Button/ │ │ │ ├── PushButtonBP.uasset │ │ │ ├── SFX/ │ │ │ │ ├── GameConsole_button_press_in_01.uasset │ │ │ │ └── GameConsole_button_press_out_04.uasset │ │ │ ├── ToggleButtonBP.uasset │ │ │ └── testbutton_sm.uasset │ │ ├── RingWeapon/ │ │ │ ├── 2HandedBeamProp_Mesh.uasset │ │ │ ├── InteractableArtefactHandle.uasset │ │ │ ├── InteractableTwoHandedArtefact.uasset │ │ │ ├── TwoHandArtefact_StartBeam.uasset │ │ │ ├── TwoHandedBeamSystem.uasset │ │ │ ├── vfxm_2HandedBeamProp.uasset │ │ │ ├── vfxm_OrbFloating.uasset │ │ │ ├── vfxm_TwoHandedArtefactBeam.uasset │ │ │ └── vfxm_TwoHandedArtefactBeam_Inst.uasset │ │ ├── Table/ │ │ │ ├── table_bc.uasset │ │ │ ├── table_mat.uasset │ │ │ └── table_sm.uasset │ │ ├── Target/ │ │ │ ├── TargetBP.uasset │ │ │ ├── sfx_spinning_target_01_loop.uasset │ │ │ ├── target_m.uasset │ │ │ ├── target_pentagon_sm.uasset │ │ │ ├── targets_bc.uasset │ │ │ └── targets_em.uasset │ │ ├── TeleportBeacon/ │ │ │ ├── Activate_vfx.uasset │ │ │ ├── NS_Activate.uasset │ │ │ ├── TeleportBeacon.uasset │ │ │ ├── TeleportationPad.uasset │ │ │ ├── TeleportationPadMesh.uasset │ │ │ ├── vfxm_ActivateGlow_Inst.uasset │ │ │ ├── vfxm_AlphaPanner.uasset │ │ │ ├── vfxm_TeleportBeaconPointerArrow.uasset │ │ │ ├── vfxmi_TeleportBeacon.uasset │ │ │ ├── vfxmi_TeleportPointerArrowRing.uasset │ │ │ ├── vfxsm_TeleportBeacon.uasset │ │ │ └── vfxsm_TeleportPointerArrowRing.uasset │ │ └── TetherBall/ │ │ ├── Art/ │ │ │ ├── ball_bc.uasset │ │ │ ├── ball_mat.uasset │ │ │ ├── ball_sm.uasset │ │ │ ├── tether_ball_base.uasset │ │ │ ├── tether_ball_base_mat.uasset │ │ │ ├── tether_ball_base_sm.uasset │ │ │ └── tether_mat.uasset │ │ ├── SFX/ │ │ │ ├── TetherBallHit.uasset │ │ │ ├── TetherBallSoundConcurrency.uasset │ │ │ ├── TetherBall_RubberThick_Imp_01.uasset │ │ │ ├── TetherBall_RubberThick_Imp_02.uasset │ │ │ ├── TetherBall_RubberThick_Imp_03.uasset │ │ │ ├── TetherBall_RubberThick_Imp_04.uasset │ │ │ ├── TetherBall_RubberThick_Imp_05.uasset │ │ │ ├── TetherBall_RubberThick_Imp_06.uasset │ │ │ ├── TetherBall_RubberThick_Imp_07.uasset │ │ │ ├── TetherBall_RubberThick_Imp_08.uasset │ │ │ └── TetherBall_RubberThick_Imp_09.uasset │ │ ├── TetherBallBP.uasset │ │ ├── ThetherBallPhysMat.uasset │ │ ├── VFX/ │ │ │ └── spawn_tetherballBase_ps.uasset │ │ └── tether_ball_sm.uasset │ └── SharedArt/ │ ├── Blue.uasset │ ├── FlatBlue.uasset │ ├── Red.uasset │ ├── T_Caustic01.uasset │ ├── T_WorleyNoise01.uasset │ └── TextBackground.uasset ├── HandGameplay.uproject ├── LICENSE ├── Platforms/ │ └── HoloLens/ │ └── Config/ │ └── HoloLensEngine.ini ├── Plugins/ │ ├── OculusHandTools/ │ │ ├── .gitattributes │ │ ├── .gitignore │ │ ├── Config/ │ │ │ └── FilterPlugin.ini │ │ ├── Content/ │ │ │ ├── BlueprintMacros.uasset │ │ │ ├── Button/ │ │ │ │ ├── IsButtonableInterface.uasset │ │ │ │ └── PushButtonBaseBP.uasset │ │ │ ├── Fonts/ │ │ │ │ ├── OculusSans-Black.uasset │ │ │ │ ├── OculusSans-BlackItalic.uasset │ │ │ │ ├── OculusSans-BlackItalic_Font.uasset │ │ │ │ ├── OculusSans-Black_Font.uasset │ │ │ │ ├── OculusSans-Bold.uasset │ │ │ │ ├── OculusSans-BoldItalic.uasset │ │ │ │ ├── OculusSans-BoldItalic_Font.uasset │ │ │ │ ├── OculusSans-Bold_Font.uasset │ │ │ │ ├── OculusSans-DemiBold.uasset │ │ │ │ ├── OculusSans-DemiBoldItalic.uasset │ │ │ │ ├── OculusSans-DemiBoldItalic_Font.uasset │ │ │ │ ├── OculusSans-DemiBold_Font.uasset │ │ │ │ ├── OculusSans-Light.uasset │ │ │ │ ├── OculusSans-LightItalic.uasset │ │ │ │ ├── OculusSans-LightItalic_Font.uasset │ │ │ │ ├── OculusSans-Light_Font.uasset │ │ │ │ ├── OculusSans-Medium.uasset │ │ │ │ ├── OculusSans-MediumItalic.uasset │ │ │ │ ├── OculusSans-MediumItalic_Font.uasset │ │ │ │ ├── OculusSans-Medium_Font.uasset │ │ │ │ ├── OculusSans-Normal.uasset │ │ │ │ ├── OculusSans-NormalItalic.uasset │ │ │ │ ├── OculusSans-NormalItalic_Font.uasset │ │ │ │ ├── OculusSans-Normal_Font.uasset │ │ │ │ ├── OculusSans-Regular.uasset │ │ │ │ ├── OculusSans-RegularItalic.uasset │ │ │ │ ├── OculusSans-RegularItalic_Font.uasset │ │ │ │ ├── OculusSans-Regular_Font.uasset │ │ │ │ ├── OculusSans-SemiBold.uasset │ │ │ │ ├── OculusSans-SemiBoldItalic.uasset │ │ │ │ ├── OculusSans-SemiBoldItalic_Font.uasset │ │ │ │ ├── OculusSans-SemiBold_Font.uasset │ │ │ │ ├── OculusSans-Thin.uasset │ │ │ │ ├── OculusSans-ThinItalic.uasset │ │ │ │ ├── OculusSans-ThinItalic_Font.uasset │ │ │ │ ├── OculusSans-Thin_Font.uasset │ │ │ │ ├── OculusSans-Ultra.uasset │ │ │ │ ├── OculusSans-UltraItalic.uasset │ │ │ │ ├── OculusSans-UltraItalic_Font.uasset │ │ │ │ └── OculusSans-Ultra_Font.uasset │ │ │ ├── HandDebug/ │ │ │ │ ├── HandDebugActor.uasset │ │ │ │ ├── HandDebugWidget.uasset │ │ │ │ ├── STADIUM_Black-01.uasset │ │ │ │ └── WidgetInvisibleMaterial.uasset │ │ │ ├── Hands/ │ │ │ │ ├── HandsCharacterBase.uasset │ │ │ │ ├── HandsCharacterHandState.uasset │ │ │ │ ├── TeleportSelector.uasset │ │ │ │ └── TutorialHand.uasset │ │ │ ├── RespawnProps/ │ │ │ │ ├── FloorIdentifier.uasset │ │ │ │ └── RespawnFromFloor.uasset │ │ │ └── Selectors/ │ │ │ ├── AimingGlow.uasset │ │ │ ├── DefaultAimingActor.uasset │ │ │ └── DefaultAimingSphere.uasset │ │ ├── OculusHandTools.uplugin │ │ ├── README.md │ │ ├── README_HandInput.md │ │ ├── README_HandPoseRecognition.md │ │ ├── README_HandTrackingFilter.md │ │ ├── README_Interactable.md │ │ ├── README_OculusUtils.md │ │ ├── README_ThrowAssist.md │ │ └── Source/ │ │ ├── HandInput/ │ │ │ ├── CameraHandInput.cpp │ │ │ ├── CameraHandInput.h │ │ │ ├── EnumMap.h │ │ │ ├── HandInput.Build.cs │ │ │ ├── HandInput.cpp │ │ │ ├── HandInput.h │ │ │ ├── HandInputModule.cpp │ │ │ ├── HandInputModule.h │ │ │ └── QuatUtil.h │ │ ├── HandTrackingFilter/ │ │ │ ├── HandTrackingFilter.Build.cs │ │ │ ├── HandTrackingFilter.cpp │ │ │ ├── HandTrackingFilter.h │ │ │ ├── HandTrackingFilterComponent.cpp │ │ │ ├── HandTrackingFilterComponent.h │ │ │ └── QuatUtil.h │ │ ├── OculusHandPoseRecognition/ │ │ │ ├── OculusHandPoseRecognition.Build.cs │ │ │ ├── Private/ │ │ │ │ ├── FRecordHandPoseAction.h │ │ │ │ ├── FWaitForHandGestureAction.h │ │ │ │ ├── FWaitForHandPoseAction.h │ │ │ │ ├── HandGesture.cpp │ │ │ │ ├── HandGestureRecognizer.cpp │ │ │ │ ├── HandPose.cpp │ │ │ │ ├── HandPoseRecognizer.cpp │ │ │ │ ├── HandRecognitionFunctionLibrary.cpp │ │ │ │ ├── HandRecognitionGameMode.cpp │ │ │ │ ├── OculusHandPoseRecognitionModule.cpp │ │ │ │ └── PoseableHandComponent.cpp │ │ │ └── Public/ │ │ │ ├── HandGesture.h │ │ │ ├── HandGestureRecognizer.h │ │ │ ├── HandPose.h │ │ │ ├── HandPoseRecognizer.h │ │ │ ├── HandRecognitionFunctionLibrary.h │ │ │ ├── HandRecognitionGameMode.h │ │ │ ├── OculusHandPoseRecognitionModule.h │ │ │ └── PoseableHandComponent.h │ │ ├── OculusInteractable/ │ │ │ ├── OculusInteractable.Build.cs │ │ │ ├── Private/ │ │ │ │ ├── AimingActor.cpp │ │ │ │ ├── HandGrabbingComponent.cpp │ │ │ │ ├── Interactable.cpp │ │ │ │ ├── InteractableFunctionLibrary.cpp │ │ │ │ ├── InteractableSelector.cpp │ │ │ │ ├── OculusInteractableModule.cpp │ │ │ │ ├── TransformString.cpp │ │ │ │ └── TransformString.h │ │ │ └── Public/ │ │ │ ├── AimingActor.h │ │ │ ├── HandGrabbingComponent.h │ │ │ ├── Interactable.h │ │ │ ├── InteractableFunctionLibrary.h │ │ │ ├── InteractablePose.h │ │ │ ├── InteractableSelector.h │ │ │ └── OculusInteractableModule.h │ │ └── OculusThrowAssist/ │ │ ├── OculusThrowAssist.Build.cs │ │ ├── Private/ │ │ │ ├── OculusThrowAssistModule.cpp │ │ │ ├── ThrowingComponent.cpp │ │ │ └── TransformBufferComponent.cpp │ │ └── Public/ │ │ ├── OculusThrowAssistModule.h │ │ ├── ThrowingComponent.h │ │ └── TransformBufferComponent.h │ └── OculusUtils/ │ ├── OculusUtils.uplugin │ └── Source/ │ └── OculusUtils/ │ ├── OculusUtils.Build.cs │ ├── Private/ │ │ ├── ContinuousOverlapSphereComponent.cpp │ │ ├── OculusDeveloperTelemetry.cpp │ │ ├── OculusUtilsLibrary.cpp │ │ └── OculusUtilsModule.cpp │ └── Public/ │ ├── ContinuousOverlapSphereComponent.h │ ├── OculusDeveloperTelemetry.h │ ├── OculusUtilsLibrary.h │ └── OculusUtilsModule.h ├── README.md └── Source/ ├── HandGameplay/ │ ├── HandGameplay.Build.cs │ ├── HandGameplay.cpp │ ├── HandGameplay.h │ ├── HandGameplayGameModeBase.cpp │ ├── HandGameplayGameModeBase.h │ └── UpdatePermissions.xml ├── HandGameplay.Target.cs └── HandGameplayEditor.Target.cs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ *.a filter=lfs diff=lfs merge=lfs -text *.lib filter=lfs diff=lfs merge=lfs -text *.uasset filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text *.umap filter=lfs diff=lfs merge=lfs -text *.jpg filter=lfs diff=lfs merge=lfs -text ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: 🐛 Bug Report description: Report a reproducible bug or regression. title: '[BUG] ' body: - type: markdown attributes: value: Thank you for taking the time to report an issue! - type: input id: version attributes: label: Unreal Engine version placeholder: 5.3.0 validations: required: true - type: dropdown id: meta-fork-unreal attributes: label: Using the Meta fork of Unreal Engine? options: - Yes, using the Meta fork - No, using the standard Epic build validations: required: true - type: checkboxes id: where attributes: label: Where does the issue occur? options: - label: In Unreal Editor required: false - label: In Quest builds required: false - type: textarea id: description attributes: label: Description description: A clear and concise description of what the bug is. validations: required: true - type: textarea id: reproduction attributes: label: Steps to reproduce description: The list of steps that reproduce the issue. validations: required: true - type: textarea id: logs attributes: label: Logs description: | For in-editor bugs, paste the logs from the "Output Log" window in the Unreal Editor. For on-device Quest bugs, paste the output of `adb logcat -s "UE"` render: text validations: required: true - type: textarea id: extra attributes: label: Additional info description: Please provide screenshots, a video, or any other relevant information. ================================================ FILE: .gitignore ================================================ # Visual Studio 2015 user specific files .vs/ # Compiled Object files *.slo *.lo *.o *.obj # Precompiled Headers *.gch *.pch # Fortran module files *.mod # Executables *.exe *.out *.app *.ipa # These project files can be generated by the engine *.xcodeproj *.xcworkspace *.sln *.suo *.opensdf *.sdf *.VC.db *.VC.opendb # Precompiled Assets SourceArt/**/*.png SourceArt/**/*.tga # Binary Files Binaries/* Plugins/*/Binaries/* # Builds Build/* # Whitelist PakBlacklist-.txt files !Build/*/ Build/*/** !Build/*/PakBlacklist*.txt # Don't ignore icon files in Build !Build/*/**/ !Build/**/*.ico !Build/**/*.png # Built data for maps *_BuiltData.uasset # Configuration files generated by the Editor Saved/* # Compiled source files for the engine to use Intermediate/* Plugins/*/Intermediate/* # Cache files for the editor to use DerivedDataCache/* StoreAssets/ GenerateSln.ps1 Config/DefaultEditorSettings.ini Build/Android/res/drawable*/ lfs/ hooks/ .vsconfig ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. This Code of Conduct also applies outside the project spaces when there is a reasonable belief that an individual's behavior may have a negative impact on the project or its community. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing We want to make contributing to this project as easy and transparent as possible. ## Pull Requests We actively welcome your pull requests. 1. Fork the repo and create your branch from `main`. 2. If you've added code that should be tested, add tests. 3. If you've changed APIs, update the documentation. 4. Ensure the test suite passes. 5. Make sure your code lints. 6. If you haven't already, complete the Contributor License Agreement ("CLA"). ## Contributor License Agreement ("CLA") In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Facebook's open source projects. Complete your CLA here: ## Issues We use GitHub issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue. Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. In those cases, please go through the process outlined on that page and do not file a public issue. ## License By contributing, you agree that your contributions will be licensed under the LICENSE file in the root directory of this source tree. ================================================ FILE: Config/Android/AndroidEngine.ini ================================================ [DevOptions.Shaders] bNeedsShaderStableKeys=True ================================================ FILE: Config/DefaultDeviceProfiles.ini ================================================ [Oculus_Quest2 DeviceProfile] DeviceType=Android BaseProfileName=Oculus_Quest bIsVisibleForAssets=False -CVars=r.Mobile.Oculus.ForceSymmetric=1 -CVars=fx.NiagaraAllowGPUParticles=1 -CVars=FX.AllowGPUSorting=1 -CVars=r.Mobile.AdrenoOcclusionMode=1 -CVars=r.Oculus.DynamicResolution.PixelDensityMin=0.8f -CVars=r.Oculus.DynamicResolution.PixelDensityMax=1.2f -CVars=r.so.VisualizeBufferXOffset=500 -CVars=r.so.VisualizeBufferYOffset=500 +CVars=r.Mobile.Oculus.ForceSymmetric=1 +CVars=fx.NiagaraAllowGPUParticles=1 +CVars=FX.AllowGPUSorting=1 +CVars=r.Mobile.AdrenoOcclusionMode=1 +CVars=r.Oculus.DynamicResolution.PixelDensityMin=0.8 +CVars=r.Oculus.DynamicResolution.PixelDensityMax=1.2 +CVars=r.so.VisualizeBufferXOffset=500 +CVars=r.so.VisualizeBufferYOffset=500 [Meta_Quest_Pro DeviceProfile] DeviceType=Android BaseProfileName=Oculus_Quest2 bIsVisibleForAssets=False +CVars=r.Oculus.DynamicResolution.PixelDensityMin=0.8 +CVars=r.Oculus.DynamicResolution.PixelDensityMax=1.2 [Meta_Quest_3 DeviceProfile] DeviceType=Android BaseProfileName=Meta_Quest_Pro bIsVisibleForAssets=False -CVars=r.Oculus.DynamicResolution.PixelDensityMin=0.7f -CVars=r.Oculus.DynamicResolution.PixelDensityMax=1.6f +CVars=r.Oculus.DynamicResolution.PixelDensityMin=0.8 +CVars=r.Oculus.DynamicResolution.PixelDensityMax=1.2 [Meta_Quest_3S DeviceProfile] DeviceType=Android BaseProfileName=Meta_Quest_3 bIsVisibleForAssets=False +CVars=r.Oculus.DynamicResolution.PixelDensityMin=0.8 +CVars=r.Oculus.DynamicResolution.PixelDensityMax=1.2 ================================================ FILE: Config/DefaultEditor.ini ================================================ ================================================ FILE: Config/DefaultEngine.ini ================================================ [/Script/HardwareTargeting.HardwareTargetingSettings] TargetedHardwareClass=Mobile AppliedTargetedHardwareClass=Mobile DefaultGraphicsPerformance=Scalable AppliedDefaultGraphicsPerformance=Scalable [/Script/Engine.Engine] +ActiveGameNameRedirects=(OldGameName="TP_Blank",NewGameName="/Script/SHOHandRecognition") +ActiveGameNameRedirects=(OldGameName="/Script/TP_Blank",NewGameName="/Script/SHOHandRecognition") +ActiveGameNameRedirects=(OldGameName="/Script/HandPoseShowcase", NewGameName="/Script/HandGameplay") +ActiveClassRedirects=(OldClassName="TP_BlankGameModeBase",NewClassName="SHOHandRecognitionGameModeBase") [/Script/Engine.RendererSettings] r.Mobile.DisableVertexFog=True r.Shadow.CSM.MaxMobileCascades=2 r.MobileMSAA=4 r.Mobile.UseLegacyShadingModel=False r.Mobile.AllowDitheredLODTransition=False r.Mobile.AllowSoftwareOcclusion=False r.Mobile.VirtualTextures=False r.DiscardUnusedQuality=False r.AllowOcclusionQueries=True r.MinScreenRadiusForLights=0.030000 r.MinScreenRadiusForDepthPrepass=0.030000 r.MinScreenRadiusForCSMDepth=0.010000 r.PrecomputedVisibilityWarning=False r.TextureStreaming=True Compat.UseDXT5NormalMaps=False r.VirtualTextures=False r.VirtualTexturedLightmaps=False r.VRS.Enable=True r.VRS.EnableImage=False r.VT.TileSize=128 r.VT.TileBorderSize=4 r.vt.FeedbackFactor=16 r.VT.EnableCompressZlib=True r.VT.EnableCompressCrunch=False r.ClearCoatNormal=False r.AnisotropicBRDF=False r.ReflectionCaptureResolution=128 r.ReflectionEnvironmentLightmapMixBasedOnRoughness=True r.ForwardShading=True r.VertexFoggingForOpaque=True r.AllowStaticLighting=True r.NormalMapsForStaticLighting=False r.GenerateMeshDistanceFields=False r.DistanceFieldBuild.EightBit=False r.GenerateLandscapeGIData=False r.DistanceFieldBuild.Compress=False r.TessellationAdaptivePixelsPerTriangle=48.000000 r.SeparateTranslucency=False r.TranslucentSortPolicy=0 TranslucentSortAxis=(X=0.000000,Y=-1.000000,Z=0.000000) r.CustomDepth=0 r.CustomDepthTemporalAAJitter=False r.PostProcessing.PropagateAlpha=0 r.DefaultFeature.Bloom=False r.DefaultFeature.AmbientOcclusion=False r.DefaultFeature.AmbientOcclusionStaticFraction=True r.DefaultFeature.AutoExposure=False r.DefaultFeature.AutoExposure.Method=0 r.DefaultFeature.AutoExposure.Bias=1.000000 r.DefaultFeature.AutoExposure.ExtendDefaultLuminanceRange=False r.UsePreExposure=True r.EyeAdaptation.EditorOnly=False r.DefaultFeature.MotionBlur=False r.DefaultFeature.LensFlare=False r.TemporalAA.Upsampling=False r.SSGI.Enable=False r.DefaultFeature.AntiAliasing=3 r.DefaultFeature.LightUnits=1 r.DefaultBackBufferPixelFormat=4 r.Shadow.UnbuiltPreviewInGame=True r.StencilForLODDither=False r.EarlyZPass=0 r.EarlyZPassOnlyMaterialMasking=False r.DBuffer=True r.ClearSceneMethod=1 r.BasePassOutputsVelocity=False r.VertexDeformationOutputsVelocity=False r.SelectiveBasePassOutputs=False bDefaultParticleCutouts=False fx.GPUSimulationTextureSizeX=1024 fx.GPUSimulationTextureSizeY=1024 r.AllowGlobalClipPlane=False r.GBufferFormat=1 r.MorphTarget.Mode=True r.GPUCrashDebugging=False vr.InstancedStereo=True r.MobileHDR=False vr.MobileMultiView=True r.Mobile.UseHWsRGBEncoding=True vr.RoundRobinOcclusion=False vr.ODSCapture=False r.MeshStreaming=False r.WireframeCullThreshold=5.000000 r.RayTracing=False r.RayTracing.UseTextureLod=False r.SupportStationarySkylight=True r.SupportLowQualityLightmaps=True r.SupportPointLightWholeSceneShadows=True r.SupportAtmosphericFog=True r.SupportSkyAtmosphere=True r.SupportSkyAtmosphereAffectsHeightFog=False r.SkinCache.CompileShaders=False r.SkinCache.DefaultBehavior=1 r.SkinCache.SceneMemoryLimitInMB=128.000000 r.Mobile.EnableStaticAndCSMShadowReceivers=False r.Mobile.EnableMovableLightCSMShaderCulling=True r.Mobile.AllowDistanceFieldShadows=False r.Mobile.AllowMovableDirectionalLights=False r.MobileNumDynamicPointLights=0 r.MobileDynamicPointLightsUseStaticBranch=True r.Mobile.EnableMovableSpotlights=False r.GPUSkin.Support16BitBoneIndex=False r.GPUSkin.Limit2BoneInfluences=False r.SupportDepthOnlyIndexBuffers=True r.SupportReversedIndexBuffers=True r.SupportMaterialLayers=False r.LightPropagationVolume=False r.Mobile.AmbientOcclusion=False r.Mobile.AntiAliasing=3 r.MSAACount=4 r.AntiAliasingMethod=3 [/Script/EngineSettings.GameMapsSettings] EditorStartupMap=/Game/HandGameplay/Levels/HandGameplayShowcase.HandGameplayShowcase LocalMapOptions= TransitionMap=None bUseSplitscreen=False TwoPlayerSplitscreenLayout=Horizontal ThreePlayerSplitscreenLayout=FavorTop FourPlayerSplitscreenLayout=Grid bOffsetPlayerGamepadIds=False GameInstanceClass=/Script/Engine.GameInstance GameDefaultMap=/Game/HandGameplay/Levels/HandGameplayShowcase.HandGameplayShowcase ServerDefaultMap=/Engine/Maps/Entry.Entry GlobalDefaultGameMode=/Game/HandGameplay/Blueprints/HandsGameMode.HandsGameMode_C GlobalDefaultServerGameMode=None [/Script/Slate.SlateSettings] bExplicitCanvasChildZOrder=True [/Script/AndroidRuntimeSettings.AndroidRuntimeSettings] PackageName=com.samples.[PROJECT] StoreVersion=1 StoreVersionOffsetArm64=0 StoreVersionOffsetX8664=0 ApplicationDisplayName= VersionDisplayName=1.0 MinSDKVersion=32 TargetSDKVersion=32 InstallLocation=Auto bEnableLint=False bPackageDataInsideApk=True bCreateAllPlatformsInstall=False bDisableVerifyOBBOnStartUp=False bForceSmallOBBFiles=False bAllowLargeOBBFiles=False bAllowPatchOBBFile=False bAllowOverflowOBBFiles=False bUseExternalFilesDir=False bPublicLogFiles=True Orientation=Landscape MaxAspectRatio=2.100000 bUseDisplayCutout=False bAllowResizing=False bSupportSizeChanges=False bRestoreNotificationsOnReboot=False bFullScreen=True bEnableNewKeyboard=True DepthBufferPreference=Default bValidateTextureFormats=True bForceCompressNativeLibs=False bEnableAdvancedBinaryCompression=True bEnableBundle=False bEnableUniversalAPK=False bBundleABISplit=True bBundleLanguageSplit=True bBundleDensitySplit=True +ExtraApplicationNodeTags=android:allowBackup="false" ExtraApplicationSettings= ExtraActivitySettings= bAndroidVoiceEnabled=False bEnableMulticastSupport=False bPackageForMetaQuest=True bRemoveOSIG=True KeyStore= KeyAlias= KeyStorePassword= KeyPassword= bBuildForArm64=True bBuildForX8664=False bBuildForES31=False bSupportsVulkan=True bSupportsVulkanSM5=False DebugVulkanLayerDirectory=(Path="") bAndroidOpenGLSupportsBackbufferSampling=False bDetectVulkanByDefault=True bBuildWithHiddenSymbolVisibility=False bDisableStackProtector=False bDisableLibCppSharedDependencyValidation=False bSaveSymbols=False bStripShaderReflection=True bStripReflectOfAndroidShader=False bEnableGooglePlaySupport=False bUseGetAccounts=False GamesAppID= bEnableSnapshots=False bSupportAdMob=True AdMobAppID= TagForChildDirectedTreatment=TAG_FOR_CHILD_DIRECTED_TREATMENT_UNSPECIFIED TagForUnderAgeOfConsent=TAG_FOR_UNDER_AGE_OF_CONSENT_UNSPECIFIED MaxAdContentRating=MAX_AD_CONTENT_RATING_G AdMobAdUnitID= GooglePlayLicenseKey= GCMClientSenderID= bShowLaunchImage=True bAllowIMU=True bAllowControllers=True bBlockAndroidKeysOnControllers=False bControllersBlockDeviceFeedback=False AndroidAudio=Default AudioSampleRate=44100 AudioCallbackBufferFrameSize=1024 AudioNumBuffersToEnqueue=4 AudioMaxChannels=0 AudioNumSourceWorkers=0 SpatializationPlugin=Oculus Audio SourceDataOverridePlugin= ReverbPlugin=Oculus Audio OcclusionPlugin= CompressionOverrides=(bOverrideCompressionTimes=False,DurationThreshold=5.000000,MaxNumRandomBranches=0,SoundCueQualityIndex=0) CacheSizeKB=0 MaxChunkSizeOverrideKB=0 bResampleForDevice=False SoundCueCookQualityIndex=-1 MaxSampleRate=0.000000 HighSampleRate=0.000000 MedSampleRate=0.000000 LowSampleRate=0.000000 MinSampleRate=0.000000 CompressionQualityModifier=0.000000 AutoStreamingThreshold=0.000000 AndroidGraphicsDebugger=None MaliGraphicsDebuggerPath=(Path="") bEnableMaliPerfCounters=False bMultiTargetFormat_ETC2=False bMultiTargetFormat_DXT=False bMultiTargetFormat_ASTC=True TextureFormatPriority_ETC2=0.200000 TextureFormatPriority_DXT=0.600000 TextureFormatPriority_ASTC=0.900000 SDKAPILevelOverride= NDKAPILevelOverride= BuildToolsOverride= bStreamLandscapeMeshLODs=False bEnableDomStorage=False [/Script/OculusHMD.OculusHMDRuntimeSettings] HandTrackingSupport=HandsOnly FFRLevel=FFR_Medium FFRDynamic=True XrApi=LegacyOVRPlugin HandTrackingFrequency=HIGH bLateLatching=True bPhaseSync=True bAutoEnabled=True bEnableSpecificColorGamut=False ColorSpace=Quest bSupportsDash=True bCompositesDepth=True bHQDistortion=False PixelDensityMin=0.500000 PixelDensityMax=1.000000 OSSplashScreen=(FilePath="Plugins/OculusUtils/Resources/oculus_samples.png") CPULevel=2 GPULevel=3 bFocusAware=True bRequiresSystemKeyboard=False [/Script/Engine.CollisionProfile] -Profiles=(Name="NoCollision",CollisionEnabled=NoCollision,ObjectTypeName="WorldStatic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore)),HelpMessage="No collision",bCanModify=False) -Profiles=(Name="BlockAll",CollisionEnabled=QueryAndPhysics,ObjectTypeName="WorldStatic",CustomResponses=,HelpMessage="WorldStatic object that blocks all actors by default. All new custom channels will use its own default response. ",bCanModify=False) -Profiles=(Name="OverlapAll",CollisionEnabled=QueryOnly,ObjectTypeName="WorldStatic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. ",bCanModify=False) -Profiles=(Name="BlockAllDynamic",CollisionEnabled=QueryAndPhysics,ObjectTypeName="WorldDynamic",CustomResponses=,HelpMessage="WorldDynamic object that blocks all actors by default. All new custom channels will use its own default response. ",bCanModify=False) -Profiles=(Name="OverlapAllDynamic",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldDynamic object that overlaps all actors by default. All new custom channels will use its own default response. ",bCanModify=False) -Profiles=(Name="IgnoreOnlyPawn",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that ignores Pawn and Vehicle. All other channels will be set to default.",bCanModify=False) -Profiles=(Name="OverlapOnlyPawn",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Pawn",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that overlaps Pawn, Camera, and Vehicle. All other channels will be set to default. ",bCanModify=False) -Profiles=(Name="Pawn",CollisionEnabled=QueryAndPhysics,ObjectTypeName="Pawn",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Pawn object. Can be used for capsule of any playerable character or AI. ",bCanModify=False) -Profiles=(Name="Spectator",CollisionEnabled=QueryOnly,ObjectTypeName="Pawn",CustomResponses=((Channel="WorldStatic",Response=ECR_Block),(Channel="Pawn",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore),(Channel="WorldDynamic",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore),(Channel="PhysicsBody",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore),(Channel="Destructible",Response=ECR_Ignore)),HelpMessage="Pawn object that ignores all other actors except WorldStatic.",bCanModify=False) -Profiles=(Name="CharacterMesh",CollisionEnabled=QueryOnly,ObjectTypeName="Pawn",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Pawn object that is used for Character Mesh. All other channels will be set to default.",bCanModify=False) -Profiles=(Name="PhysicsActor",CollisionEnabled=QueryAndPhysics,ObjectTypeName="PhysicsBody",CustomResponses=,HelpMessage="Simulating actors",bCanModify=False) -Profiles=(Name="Destructible",CollisionEnabled=QueryAndPhysics,ObjectTypeName="Destructible",CustomResponses=,HelpMessage="Destructible actors",bCanModify=False) -Profiles=(Name="InvisibleWall",CollisionEnabled=QueryAndPhysics,ObjectTypeName="WorldStatic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="WorldStatic object that is invisible.",bCanModify=False) -Profiles=(Name="InvisibleWallDynamic",CollisionEnabled=QueryAndPhysics,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that is invisible.",bCanModify=False) -Profiles=(Name="Trigger",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Ignore),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldDynamic object that is used for trigger. All other channels will be set to default.",bCanModify=False) -Profiles=(Name="Ragdoll",CollisionEnabled=QueryAndPhysics,ObjectTypeName="PhysicsBody",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Simulating Skeletal Mesh Component. All other channels will be set to default.",bCanModify=False) -Profiles=(Name="Vehicle",CollisionEnabled=QueryAndPhysics,ObjectTypeName="Vehicle",CustomResponses=,HelpMessage="Vehicle object that blocks Vehicle, WorldStatic, and WorldDynamic. All other channels will be set to default.",bCanModify=False) -Profiles=(Name="UI",CollisionEnabled=QueryOnly,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Block),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. ",bCanModify=False) +Profiles=(Name="NoCollision",CollisionEnabled=NoCollision,bCanModify=False,ObjectTypeName="WorldStatic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore)),HelpMessage="No collision") +Profiles=(Name="BlockAll",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="WorldStatic",CustomResponses=,HelpMessage="WorldStatic object that blocks all actors by default. All new custom channels will use its own default response. ") +Profiles=(Name="OverlapAll",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldStatic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. ") +Profiles=(Name="BlockAllDynamic",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=,HelpMessage="WorldDynamic object that blocks all actors by default. All new custom channels will use its own default response. ") +Profiles=(Name="OverlapAllDynamic",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldDynamic object that overlaps all actors by default. All new custom channels will use its own default response. ") +Profiles=(Name="IgnoreOnlyPawn",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that ignores Pawn and Vehicle. All other channels will be set to default.") +Profiles=(Name="OverlapOnlyPawn",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Pawn",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Overlap)),HelpMessage="WorldDynamic object that overlaps Pawn, Camera, and Vehicle. All other channels will be set to default. ") +Profiles=(Name="Pawn",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="Pawn",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Pawn object. Can be used for capsule of any playerable character or AI. ") +Profiles=(Name="Spectator",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="Pawn",CustomResponses=((Channel="WorldDynamic",Response=ECR_Ignore),(Channel="Pawn",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Ignore),(Channel="PhysicsBody",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore),(Channel="Destructible",Response=ECR_Ignore)),HelpMessage="Pawn object that ignores all other actors except WorldStatic.") +Profiles=(Name="CharacterMesh",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="Pawn",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore),(Channel="Vehicle",Response=ECR_Ignore)),HelpMessage="Pawn object that is used for Character Mesh. All other channels will be set to default.") +Profiles=(Name="PhysicsActor",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="PhysicsBody",CustomResponses=,HelpMessage="Simulating actors") +Profiles=(Name="Destructible",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="Destructible",CustomResponses=,HelpMessage="Destructible actors") +Profiles=(Name="InvisibleWall",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="WorldStatic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="WorldStatic object that is invisible.") +Profiles=(Name="InvisibleWallDynamic",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="Visibility",Response=ECR_Ignore)),HelpMessage="WorldDynamic object that is invisible.") +Profiles=(Name="Trigger",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Visibility",Response=ECR_Ignore),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldDynamic object that is used for trigger. All other channels will be set to default.") +Profiles=(Name="Ragdoll",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="PhysicsBody",CustomResponses=((Channel="Pawn",Response=ECR_Ignore),(Channel="Visibility",Response=ECR_Ignore)),HelpMessage="Simulating Skeletal Mesh Component. All other channels will be set to default.") +Profiles=(Name="Vehicle",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName="Vehicle",CustomResponses=,HelpMessage="Vehicle object that blocks Vehicle, WorldStatic, and WorldDynamic. All other channels will be set to default.") +Profiles=(Name="UI",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName="WorldDynamic",CustomResponses=((Channel="WorldStatic",Response=ECR_Overlap),(Channel="WorldDynamic",Response=ECR_Overlap),(Channel="Pawn",Response=ECR_Overlap),(Channel="Camera",Response=ECR_Overlap),(Channel="PhysicsBody",Response=ECR_Overlap),(Channel="Vehicle",Response=ECR_Overlap),(Channel="Destructible",Response=ECR_Overlap)),HelpMessage="WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. ") +DefaultChannelResponses=(Channel=ECC_GameTraceChannel1,DefaultResponse=ECR_Ignore,bTraceType=True,bStaticObject=False,Name="Interactable") +DefaultChannelResponses=(Channel=ECC_GameTraceChannel2,DefaultResponse=ECR_Overlap,bTraceType=False,bStaticObject=False,Name="FingerTip") -ProfileRedirects=(OldName="BlockingVolume",NewName="InvisibleWall") -ProfileRedirects=(OldName="InterpActor",NewName="IgnoreOnlyPawn") -ProfileRedirects=(OldName="StaticMeshComponent",NewName="BlockAllDynamic") -ProfileRedirects=(OldName="SkeletalMeshActor",NewName="PhysicsActor") -ProfileRedirects=(OldName="InvisibleActor",NewName="InvisibleWallDynamic") +ProfileRedirects=(OldName="BlockingVolume",NewName="InvisibleWall") +ProfileRedirects=(OldName="InterpActor",NewName="IgnoreOnlyPawn") +ProfileRedirects=(OldName="StaticMeshComponent",NewName="BlockAllDynamic") +ProfileRedirects=(OldName="SkeletalMeshActor",NewName="PhysicsActor") +ProfileRedirects=(OldName="InvisibleActor",NewName="InvisibleWallDynamic") -CollisionChannelRedirects=(OldName="Static",NewName="WorldStatic") -CollisionChannelRedirects=(OldName="Dynamic",NewName="WorldDynamic") -CollisionChannelRedirects=(OldName="VehicleMovement",NewName="Vehicle") -CollisionChannelRedirects=(OldName="PawnMovement",NewName="Pawn") +CollisionChannelRedirects=(OldName="Static",NewName="WorldStatic") +CollisionChannelRedirects=(OldName="Dynamic",NewName="WorldDynamic") +CollisionChannelRedirects=(OldName="VehicleMovement",NewName="Vehicle") +CollisionChannelRedirects=(OldName="PawnMovement",NewName="Pawn") +CollisionChannelRedirects=(OldName="Hand",NewName="FingerTip") [/Script/Engine.UserInterfaceSettings] UIScaleRule=ShortestSide UIScaleCurve=(EditorCurveData=(Keys=((Time=0.256264,Value=1.000000)),DefaultValue=340282346638528859811704183484516925440.000000,PreInfinityExtrap=RCCE_Constant,PostInfinityExtrap=RCCE_Constant),ExternalCurve=None) [/Script/WindowsTargetPlatform.WindowsTargetSettings] SpatializationPlugin=Oculus Audio ReverbPlugin=Oculus Audio [/Script/OculusAudio.OculusAudioSettings] LateReverberation=True [/Script/LuminRuntimeSettings.LuminRuntimeSettings] IconModelPath=(Path="") IconPortalPath=(Path="") [Core.Log] LogHandPoseRecognition=warning [/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings] bEnablePlugin=True bAllowNetworkConnection=True SecurityToken=8FCC7359434D4251D8FC3DAF8446EBE2 bIncludeInShipping=False bAllowExternalStartInShipping=False bCompileAFSProject=False bUseCompression=False bLogFiles=False bReportStats=False ConnectionType=USBOnly bUseManualIPAddress=False ManualIPAddress= [/Script/OculusXRHMD.OculusXRHMDRuntimeSettings] SystemSplashBackground=Black bAutoEnabled=False bHorizonOSVersionOverride=False MinOSVersion=(Version=0,bLatest=True) TargetOSVersion=(Version=0,bLatest=True) XrApi=NativeOpenXR ColorSpace=P3 ControllerPoseAlignment=Default bThumbstickDpadEmulationEnabled=True OculusXRSimulatorPreferredVersion=81 bNotifyWhenNewVersionIsAvailable=True bSupportsDash=True bCompositesDepth=True bHQDistortion=False bSetActivePIEToPrimary=True bSetCVarPIEToPrimary=True bUpdateHeadPoseForInactivePlayer=False MPPoseRestoreType=Disabled bDynamicResolution=False +SupportedDevices=Quest3 +SupportedDevices=QuestPro +SupportedDevices=Quest2 +SupportedDevices=Quest3S SuggestedCpuPerfLevel=SustainedLow SuggestedGpuPerfLevel=SustainedHigh FoveatedRenderingMethod=FixedFoveatedRendering FoveatedRenderingLevel=Off bDynamicFoveatedRendering=False bSupportEyeTrackedFoveatedRendering=False bCompositeDepthMobile=False bFocusAware=True bLateLatching=False bRequiresSystemKeyboard=False HandTrackingSupport=HandsOnly HandTrackingFrequency=LOW HandTrackingVersion=Default bInsightPassthroughEnabled=False bAnchorSupportEnabled=False bAnchorSharingEnabled=False bSceneSupportEnabled=False bBoundaryVisibilitySupportEnabled=False bDefaultBoundaryVisibilitySuppressed=False bColocationSessionsEnabled=False bBodyTrackingEnabled=False BodyTrackingFidelity=Low BodyTrackingJointSet=UpperBody bEyeTrackingEnabled=False bFaceTrackingEnabled=False FaceTrackingDataSource=() bFaceTrackingVisemesEnabled=False bDeploySoToDevice=False bIterativeCookOnTheFly=False bSupportExperimentalFeatures=False ProcessorFavor=FavorEqually bTileTurnOffEnabled=False bSupportSBC=False SBCPath=files/UnrealGame/HandGameplay/HandGameplay/Saved/VulkanCache EnableWorldLock=True ================================================ FILE: Config/DefaultGame.ini ================================================ [/Script/EngineSettings.GeneralProjectSettings] ProjectID=C55AF2354CBEC3833CFBFB923FDA1579 CopyrightNotice=Copyright (c) Meta Platforms, Inc. and affiliates. bStartInVR=True [/Script/UnrealEd.ProjectPackagingSettings] StagingDirectory=(Path="../../../../../../../../QuestPackagingArea/SHO_HandGameplay/Android_ASTC") +DirectoriesToNeverCook=(Path="/Game/Developers/jasonmeisel") ================================================ FILE: Config/DefaultInput.ini ================================================ [/Script/Engine.InputSettings] -AxisConfig=(AxisKeyName="Gamepad_LeftX",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f)) -AxisConfig=(AxisKeyName="Gamepad_LeftY",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f)) -AxisConfig=(AxisKeyName="Gamepad_RightX",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f)) -AxisConfig=(AxisKeyName="Gamepad_RightY",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f)) -AxisConfig=(AxisKeyName="MouseX",AxisProperties=(DeadZone=0.f,Exponent=1.f,Sensitivity=0.07f)) -AxisConfig=(AxisKeyName="MouseY",AxisProperties=(DeadZone=0.f,Exponent=1.f,Sensitivity=0.07f)) -AxisConfig=(AxisKeyName="Mouse2D",AxisProperties=(DeadZone=0.f,Exponent=1.f,Sensitivity=0.07f)) +AxisConfig=(AxisKeyName="MagicLeap_Left_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MagicLeap_Left_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MagicLeap_Left_Trackpad_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MagicLeap_Left_Touch1_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MagicLeap_Left_Touch1_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MagicLeap_Left_Touch1_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="Mouse2D",AxisProperties=(DeadZone=0.000000,Sensitivity=0.070000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MotionController_Right_Thumbstick_Z",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MagicLeap_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MagicLeap_Right_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MagicLeap_Right_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MagicLeap_Right_Trackpad_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MagicLeap_Right_Touch1_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MagicLeap_Right_Touch1_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MagicLeap_Right_Touch1_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Left_Thumbstick",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Left_FaceButton1",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Left_Trigger",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Left_FaceButton2",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Left_IndexPointing",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Left_ThumbUp",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Right_Thumbstick",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Right_FaceButton1",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Right_Trigger",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Right_FaceButton2",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Right_IndexPointing",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Right_ThumbUp",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusHand_Left_ThumbPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusHand_Left_IndexPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusHand_Left_MiddlePinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusHand_Left_RingPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusHand_Left_PinkPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusHand_Right_ThumbPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusHand_Right_IndexPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusHand_Right_MiddlePinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusHand_Right_RingPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusHand_Right_PinkPinchStrength",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="ValveIndex_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="ValveIndex_Right_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="ValveIndex_Right_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="ValveIndex_Right_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="ValveIndex_Right_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="ValveIndex_Right_Trackpad_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="Gamepad_LeftX",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="Gamepad_LeftY",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="Gamepad_RightX",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="Gamepad_RightY",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MouseX",AxisProperties=(DeadZone=0.000000,Sensitivity=0.070000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MouseY",AxisProperties=(DeadZone=0.000000,Sensitivity=0.070000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MouseWheelAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="Gamepad_LeftTriggerAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="Gamepad_RightTriggerAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="Gamepad_Special_Left_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="Gamepad_Special_Left_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="Daydream_Left_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="Daydream_Left_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="Daydream_Right_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="Daydream_Right_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="Vive_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="Vive_Left_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="Vive_Left_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="Vive_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="Vive_Right_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="Vive_Right_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MixedReality_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MixedReality_Left_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MixedReality_Left_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MixedReality_Left_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MixedReality_Left_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MixedReality_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MixedReality_Right_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MixedReality_Right_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MixedReality_Right_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MixedReality_Right_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Left_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Left_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Left_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Right_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Right_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Right_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="ValveIndex_Left_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="ValveIndex_Left_Grip_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="ValveIndex_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="ValveIndex_Left_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="ValveIndex_Left_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="ValveIndex_Left_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="ValveIndex_Left_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="ValveIndex_Left_Trackpad_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="ValveIndex_Left_Trackpad_Touch",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="ValveIndex_Right_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="ValveIndex_Right_Grip_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MotionController_Left_Thumbstick_Z",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="MagicLeap_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Left_ThumbRest",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +AxisConfig=(AxisKeyName="OculusTouch_Right_ThumbRest",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) bAltEnterTogglesFullscreen=True bF11TogglesFullscreen=True bUseMouseForTouch=False bEnableMouseSmoothing=True bEnableFOVScaling=True bCaptureMouseOnLaunch=True bEnableLegacyInputScales=True bEnableMotionControls=True bFilterInputByPlatformUser=False bEnableInputDeviceSubsystem=True bShouldFlushPressedKeysOnViewportFocusLost=True bEnableDynamicComponentInputBinding=True bAlwaysShowTouchInterface=False bShowConsoleOnFourFingerTap=True bEnableGestureRecognizer=False bUseAutocorrect=False DefaultViewportMouseCaptureMode=CapturePermanently_IncludingInitialMouseDown DefaultViewportMouseLockMode=LockOnCapture FOVScale=0.011110 DoubleClickTime=0.200000 DefaultPlayerInputClass=/Script/EnhancedInput.EnhancedPlayerInput DefaultInputComponentClass=/Script/EnhancedInput.EnhancedInputComponent DefaultTouchInterface=None -ConsoleKeys=Tilde +ConsoleKeys=Tilde ================================================ FILE: Content/HandGameplay/Audio/GrabSound.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:b9849f17e45079ec2902ffee437cd8d186f29aafbf3e732eab5f688f0649a7e6 size 8916 ================================================ FILE: Content/HandGameplay/Audio/LAST_TestAudio_Release01.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:1978301e2d109ee5baa2b3df67445779d460a4886b9046b2677b2879cecc96b8 size 44157 ================================================ FILE: Content/HandGameplay/Audio/LAST_TestAudio_Release02.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:1cb0cf23601b97676c977ab2c9c535ca9c06717af91495ad2b71ea5545699dfd size 44136 ================================================ FILE: Content/HandGameplay/Audio/LAST_TestAudio_Release03.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:558c2159e23d0499cd3bb75743973a7b19f2267d437e7d7250901008e7a09762 size 44242 ================================================ FILE: Content/HandGameplay/Audio/ReleaseSound.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:7409697cfa348d34ead533785d90e153499d095fff519020338abfabe177226d size 7811 ================================================ FILE: Content/HandGameplay/Audio/SoundAttenuationForSpatialization.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:5a90478a20b64771a4318e6a74b3a21201b3146ff29a4a5d3fb26df42fbd8460 size 1723 ================================================ FILE: Content/HandGameplay/Audio/generic_grab_01.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:a2c245034b32b6f5ddd707597c564248464333e501f325e0ba3adda25959a5fd size 37481 ================================================ FILE: Content/HandGameplay/Audio/generic_grab_02.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:dbe114715a7464e7fd61bd705448106fd4f0ec07df42ebddfc9ff5f4ccc11666 size 36937 ================================================ FILE: Content/HandGameplay/Audio/generic_grab_03.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:6b7e28b3d1e250ff7c16318291fd001955f755b4edfb3c4af9dffa6cdf63eaba size 42133 ================================================ FILE: Content/HandGameplay/Audio/generic_grab_04.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:0757440c8431852e8b0ce3ae3bb291890c6a33a868c489588ea886bec7dea106 size 36674 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_primary_block_01.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:eaebcbd95610bc979b02ed9221ee0fc256c17dfbdbeb34cdb7d6dc10f807dbaa size 20528 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_primary_block_02.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:8bf0bb6c80d8715e2503d243e0d711c59fbce56a8d91d11869ed41e3d5982758 size 27057 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_primary_block_03.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:eb040580284faf0a36126c46039399b3d308473e974e4a6f0501ceade9a1c949 size 23010 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_primary_block_04.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:aded0c4d042fc7b92871673996a091dce6e91da0bcaf0016cfacb9cd5b391ec5 size 21826 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_primary_block_05.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:a163e311b2ccf596cdb478c3c361d8b71f5d53fd361b0e3498187a07b1b27a83 size 22654 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_primary_block_06.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:6e0d5ce075fb26cc9b81da4fbaa93a48f31193ca2960cc5c6ed5ca1c5cb64f3a size 20680 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_primary_block_07.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:3be5c781b4eabe164845d2b39182a57dbe39893c4deda2bc5de9137095762978 size 24363 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_primary_block_Cue.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:99f5e033a67b622cf4960b6d740c24fa815b14553ab999408ed280d5dd63dc87 size 13769 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_01.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:1bf1cdfc4f9bf89928f94fba5b314576505b63be63f0f51b2857f2f7fa3c4e7a size 21480 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_02.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:4b1c604e88462ea7cb419cc59aeea14e7e85dccbb0f34775ebe3b1d085ac30a1 size 25422 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_03.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:5f6decb8de7585619fd12a51ff9143e4cc0d0cdaa4c29251d953075ec8461f13 size 35536 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_04.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:388306dc74efc7cfbe22e4519635332d621133ac47daf91f433ffe6d85da5ab5 size 28496 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_05.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:4d12f15c9ab424b4ec5a0b62c61b4cbe574795f29737c06143e3907e20c22275 size 45672 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_06.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:da46fa6bccaa2960263bbda1204210a1b64b73d3de15b028ebc02333fe7eca27 size 27113 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_07.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:59e4866391a9321a713ae1a98f0690a121666d508775160fc4393e602c5f6cca size 30256 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_08.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:cc7d95a24185342728913aef8f08248a5a80f40bf5c5306a4c31849510fdf482 size 26386 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_09.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:0450e4e94724af41df543aff6656da566d8e8565dda267b428207ebdcbad9108 size 34593 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_10.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:f3c9df420ac2e410031f394c5942c9785081b1e128ffaecf2f59ac33af3c69fd size 34240 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_11.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:4ca7163f0abdd9a17ec71eb907aca2273774f0d997df26e0351543988b0636fb size 31648 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_12.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:679b16faadd419a792ba4e37e32935c9620c8c7c0500a60aed132fbe5dd0ca09 size 37212 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_13.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:234096a925df53199d57c3d2a8cab3d8eb68ad7cb6d1182affb06e5eb13d2a44 size 27979 ================================================ FILE: Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_Cue.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:880d452734472b35c3463209b2cbeebea27ccdfed1f039e2ac0d107b19a092be size 21332 ================================================ FILE: Content/HandGameplay/Audio/laser/LaserSound.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:64a611c5eb7065b1b4428957a6ea5d67ce0dbcb9774bb6744232b7bdc11c8421 size 7671 ================================================ FILE: Content/HandGameplay/Audio/laser/sfx_laser_end.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:09d282ba8ca20e06fbaeccbe4efeedc68c90bb67d01795f867959dc2afce1312 size 203679 ================================================ FILE: Content/HandGameplay/Audio/laser/sfx_laser_lp.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:3b62e80c18f39438f0be03d7923b94206a0ae96a7d8d4140da60b94a82f9a515 size 705462 ================================================ FILE: Content/HandGameplay/Audio/laser/sfx_laser_start.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:18c1bb23a20cf98917e72a067a7234256b78133655e2065bb9589e77bed0684d size 161306 ================================================ FILE: Content/HandGameplay/Audio/mus_showcase_hands01.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:8cac4df454ab7e5ec71671950148129e614bc27de9c3c7cef92d62963a2de38a size 31912467 ================================================ FILE: Content/HandGameplay/Audio/sfx_ambient_outdoor_forest_ambix.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:7e85a9b706e63552e98e5b84381b410223eb6c69ee6b50213c42cc9d2f8c79a5 size 39309174 ================================================ FILE: Content/HandGameplay/Blueprints/HandsCharacter.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:4871e75eb2308c491e320f8fe4b0f704c1afeae43a934d67bb3449afc45558a7 size 77585 ================================================ FILE: Content/HandGameplay/Blueprints/HandsGameMode.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:a71313d9a1c73496b14fc115426a21922a4412a1ee91027b1923a9e4b7bc2dd1 size 20601 ================================================ FILE: Content/HandGameplay/Environment/DustMotes/DustMotes_mat.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:0b57e575286cb77fdf3432392fe72d265dda2a5c57133fbbef80581e55a7c742 size 80610 ================================================ FILE: Content/HandGameplay/Environment/DustMotes/DustMotes_system.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:790bc72151f44604020e0c23d6f8514005023d0d1f7a3eaf931962b619a701ef size 836668 ================================================ FILE: Content/HandGameplay/Environment/DustMotes/DustMotes_vfx.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:5f0bc288750ae2cf9eaddfbaeb00ef2fb54a2ba03952c763346990753bcbbcf6 size 185051 ================================================ FILE: Content/HandGameplay/Environment/DustMotes/T_Epic_sub_dustParticle.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:a5e5b2b4fee995be51884405dfe667a5d0d22da0ff4141c38d28ca59486ada19 size 38225 ================================================ FILE: Content/HandGameplay/Environment/DustMotes/vfxm_Dust.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:381c95e433bd9090d5af77e83e4ca4613b99f41b8e1adf9421328cc075906124 size 85304 ================================================ FILE: Content/HandGameplay/Environment/DustMotes/vfxm_GradientCircle.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:52b49b3f3069e7cec68ef7684defbadd4d5a1ace9a4635befaf09f14de4757f9 size 94105 ================================================ FILE: Content/HandGameplay/Environment/DustMotes/vfxm_GradientCircle_Inst.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:e657cf3213a2d9483a4a5d7a34171808de8e2979f277df44774e522c95783e98 size 75299 ================================================ FILE: Content/HandGameplay/Environment/EnvMatParms.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:ca178d89f6d9f0498998a502f84c4cd581409b85954accac4cfe5d44196609e4 size 2336 ================================================ FILE: Content/HandGameplay/Environment/HT_cube_Tex.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:08bbb2ab01456d61878518b53c0e6a2bbf9af202c7b507c6457f41088b2d43e5 size 18267320 ================================================ FILE: Content/HandGameplay/Environment/snow_envi/materials/front_rock_snow_bc.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:baa2f367209dde9f4e46d460d859ffce0a4d33dc23500b03387025942f804bf1 size 5150032 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/back_wall_bc.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:c902e85b67af6d2cd99a5bad91fd479b4019fc2c432fdf62a1ca65fdaa04074d size 5590326 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/back_wall_mat.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:b7ce974ef29b45d8057886ad362204e46368008e8251f5fd5be7344ad2718048 size 137768 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/back_wall_sm.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:90740901bf42d83576dd7a26d3f9ef9056e6ddf6debf867b25ad30e52484cebc size 313718 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/cieling_bc.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:30f503a452aaa88c690e11d2c97e5a9484ee6968cbcb5002010401e2f7445c86 size 6142030 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/cieling_mat.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:5a7126c3a42f491ac1d7817fec8c6e54a22149118f63dbe06d137cc2f679dadb size 156936 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/cieling_sm.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:b354f382457e82b44a0ab155312f0f4325f9690fdd1e71a2b8927270757f5a09 size 152293 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/cloud.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:80f38f5943a40ad9e0905d7a17c83460d5c3db4ede8e1ea7fd74ef4b57d12eba size 109892 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/cloud_sm.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:a4ba8c1172c2f1fbd713523780fd5c0afac7f04d43f4d69e54662738971c91eb size 568262 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/clouds_bc.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:17c542e80cc6c53706b6cb9fd8109b342e2e4be5eebd39a04c61236d03af6b36 size 1814529 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/collision.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:6701718b9264456e2dab0f07b31fb5ee775cecb8b1c6c34a9d168420e718c7d3 size 145862 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/front_rock_bc.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:801cab027ce8e7aee8027265865a22365ad254c8e7aff707b7274e6a5419e90a size 7457494 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/front_rock_mat.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:7b0067ced418dd4b5f864e379cfd5b77849a659fa16ae6fc1e4912a25b458fd4 size 142811 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/front_rock_sm.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:3aaff68e03a00c02d06ef932e4aeb6c4b5e206f66c74feb055c729ddb3956e93 size 632117 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/front_statue_bc.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:4dbfe761d9130aec0383bc7c45e5e4ccef4b8e9ae00308fb4e627f111fb5654e size 7524738 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/front_statue_mat.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:96c313008f0bd1a2e80f07d48cbbe91a074b0f6ca88aaa6c0533ed6d4b4d6fb8 size 151266 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/front_statue_sm.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:e17241fc5fd27964e71773d8e5294609077f4b766d020cdcfd0da0d51ee974fd size 881118 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/grass_bc.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:c6b73bcd76fce0f7ec32f5288db40970cbf72719606e9abb5baadaf92a65ac92 size 5004470 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/grass_mat.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:bc4dbb177cb2a909979699381ef3e7a3b32d2473b71453c8b9947c06f3b3c04c size 147603 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/grass_sm.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:e9601ad317b3220937f78a66e4b5ebd68f100e111255c0140738635a5c2c68b3 size 180524 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/left_wall_bc.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:7129dcd59dc43c7dbc5ce39260bbd2befe6ef36ff2b02e2704a7214a05778ea1 size 7846747 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/left_wall_mat.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:12a70a31bb78083ae856811bd588c762950d718c6d77d332196d0c8fbbdef9fc size 148314 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/left_wall_sm.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:598d63e3ac2943e70e28ce02dd4cdc4b83a321d71c09a13d1d85e688b4cf59d8 size 392141 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/main_floor_bc.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:402e435c4cfcf9535ec4bf11a284d69dbf579b7d22e6bfb4e56991bae4dc9d4d size 8532169 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/main_floor_mat.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:6ac6712d14e4c6975797273b78278c632003f9bdac15da6081ee902d26f24f5a size 144198 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/main_floor_sm.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:31e702b6f9fc44a16d81118fbe12a141f3e9eb4b31a85ab06ccdeabfb982c6de size 200465 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/right_wall_bc.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:17098acc888a0d0fec0585d4eca19aff287bfd1affc39114fb723cd47609e5f5 size 6306874 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/right_wall_mat.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:79319e598a0a81e75a0ecd140ba4f64e843014ab5f0c392a740dcf3ea7d05538 size 134751 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/right_wall_sm.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:bb7eb1f3fc3fdfafaaed5e7c7e3122278c5a9a79d9379b7513f69952f58744ed size 523096 ================================================ FILE: Content/HandGameplay/Environment/summer_envi/vines_bc.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:8657ebe24ff6867959d9dafd4c66ca147dda405825ca00ffa31bff2bc2837126 size 1431233 ================================================ FILE: Content/HandGameplay/Environment/vfxm_WindFoliage.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:aba24694c700ed06d22b01a1b426e677d954b9f40d2d7e8cfe37503bb40b9713 size 163497 ================================================ FILE: Content/HandGameplay/Environment/vfxm_WindGrass_Inst.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:4075091fc2884277ae3265b6c4c116ae9780fc1518d4917ccb1590938846bbe3 size 142642 ================================================ FILE: Content/HandGameplay/Environment/vfxm_WindVines_Inst.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:88384ad7256f73e82b3964b58e815877e928d7cb7df691247481627b4cda2369 size 128071 ================================================ FILE: Content/HandGameplay/Environment/vfxsm_vinesVtxAlpha.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:1e506290165640ad39a6ae18e87ede4d0e61548f3d5b84b172db290406e0af4e size 310382 ================================================ FILE: Content/HandGameplay/Hands/Models/HandMat.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:6219b9c63ee1ab3b0b4675d08b30db656d0febfb61f68b066d1f03ebc8f505cd size 129357 ================================================ FILE: Content/HandGameplay/Hands/Models/OculusHand_L.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:e9d5ff1272c22f7d2bf481ca9094e57e088603281b66b17fdb4087a92b45cda8 size 500387 ================================================ FILE: Content/HandGameplay/Hands/Models/OculusHand_L_Skeleton.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:3bfd455bd71d7e72070815035bb6e042ef8e2a45ad44e88b3a9f971e029d8ef3 size 18086 ================================================ FILE: Content/HandGameplay/Hands/Models/OculusHand_R.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:3d03d878f090cd0f06bb17510e0a6040b998f9cb3511ac8b81630c80ebb7e815 size 500443 ================================================ FILE: Content/HandGameplay/Hands/Models/OculusHand_R_Skeleton.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:cd6ad521c2923a9b13bb81a24e2fd5e2cc63250ada54e9c8c2c2c85927459f1d size 18086 ================================================ FILE: Content/HandGameplay/Hands/TutorialHand.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:2ac93caef3fa82b4392c6f1929c7af3f6329fe1ff28e39304692bcd47bb1fa00 size 100097 ================================================ FILE: Content/HandGameplay/Hands/TutorialHandLeft.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:5da72c2cbcd284e2251c3567d5a1965e1a0697be2907cea3ae749e89fd6f643f size 44984 ================================================ FILE: Content/HandGameplay/Hands/TutorialHandRight.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:240c9b6594a30c1e582952db71e79964adfb3f4b5115197d80868e8e143e9ee4 size 38781 ================================================ FILE: Content/HandGameplay/Input/Actions/AvatarLeftSystemGesture.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:db687a87c065b4da5889c279bbb85bc36e4cad91b95707db1e643bb82e4da4a5 size 1400 ================================================ FILE: Content/HandGameplay/Input/Actions/LogGestureState.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:cf62c8e733b90fd4ae2807a286ca6cb695bb95634bb41ad202a13583310d903e size 1360 ================================================ FILE: Content/HandGameplay/Input/Actions/LogLeftHand.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:0bec330b1f1d1c08d7b3701fc8f67f21ac76a043af47a04908b56cba2a7970b9 size 1340 ================================================ FILE: Content/HandGameplay/Input/Actions/LogRightHand.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:4ea0afae7721ecaa22a2d57f555261c0da4c1ac5dd87e26c5b994cc76f0f2692 size 1345 ================================================ FILE: Content/HandGameplay/Input/Actions/TogglePoseRecording.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:5fdbd587886c526d16b1654bdfe3813445ca582af5448adc7a4d7d31d24008c7 size 1380 ================================================ FILE: Content/HandGameplay/Input/Actions/TogglePoseRecordingLeft.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:e05a4a6eb3d46fbe6e918301caab9797bc816222eccb7a1929cb05173c496dac size 1400 ================================================ FILE: Content/HandGameplay/Input/Actions/TogglePoseRecordingRight.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:49061cfaed6bc737b226117849187a5aabd9fba4295d2d6226eca67b9491864e size 1405 ================================================ FILE: Content/HandGameplay/Input/InputMappingContext.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:97a2825de20eb4846b544ac3a94e80cc89e01accaa8639fdd71216109ff58548 size 6734 ================================================ FILE: Content/HandGameplay/Levels/HandGameplayShowcase.umap ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:07c8e5339550f019e1c79d4660b4ce249fec3b39e5697537167e11aa18815016 size 346692 ================================================ FILE: Content/HandGameplay/Levels/HandRecognitionShowcaseArt.umap ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:117d793adc535717d693bff0e08f42552fdaf96bcfd39f5a08899d88bd7180d7 size 49341 ================================================ FILE: Content/HandGameplay/Levels/HandRecognitionShowcaseAudio.umap ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:adf30fae34a9e7df004da751fd6763529d8dc3c7b16c423d2b3396f587037960 size 11278 ================================================ FILE: Content/HandGameplay/Levels/HandRecognitionShowcaseVFX.umap ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:6299627c430c30e2cc28e28b5c0dd14d3726ee1061c186d9bd2600c7a39e00f6 size 24789 ================================================ FILE: Content/HandGameplay/Props/Blocks/Blocks001_Throwable_sm.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:400f80d39b38df00cc322fabefdcb1e5665053fb47b72c8339094b96ed759912 size 116820 ================================================ FILE: Content/HandGameplay/Props/Blocks/Blocks01_bc.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:11d069f39ac48bae24f2b1acb90bf90261733268b6c4b55bda86d1207a7dbdf0 size 373341 ================================================ FILE: Content/HandGameplay/Props/Blocks/Blocks01_bc_Emissive.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:230f528f6dcf748855b08ad6687c796c2e16a7c74d00be907363c4059c861c6a size 166015 ================================================ FILE: Content/HandGameplay/Props/Blocks/Blocks01_nm.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:55e19d90494d33137e9f044d96729702123e9a8ec837161180179b33fd3b0d89 size 448108 ================================================ FILE: Content/HandGameplay/Props/Blocks/InteractableBrick.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:fe26a7b219c4255ce56765f18d3c0e73f3fad6bd35232401e0d55eceb2efbde4 size 77988 ================================================ FILE: Content/HandGameplay/Props/Blocks/NS_BrickImpact.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:9ee79b9bd8d2f29f41e55bc3142c9ef1bcebdf9ae7a4901de5debf458ea93614 size 1201603 ================================================ FILE: Content/HandGameplay/Props/Blocks/T_Epic_SUB_UV_Small_Rocks.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:398b9586620051eb4eb594a89d885580287b8df4e1ea4dd142455f6a88daafe7 size 69776 ================================================ FILE: Content/HandGameplay/Props/Blocks/vfxm_Blocks01_Throwable.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:c3e37b2c1fbe3438dfc64c7c80d17b786774ef16e156ef5edf8397eaa3b3d78f size 145820 ================================================ FILE: Content/HandGameplay/Props/Blocks/vfxm_Blocks01_Throwable_Inst.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:35ded812adb41a0d1156c11fdaafd91a718e0c139d7040b12d287b022115a9a0 size 131607 ================================================ FILE: Content/HandGameplay/Props/Blocks/vfxm_ParticleSpriteUnlitSubUV.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:1c0eb5ad0e0562ba7a3a8e45635669050d88fd9c9a36bf9464fd0b1c37cbd145 size 99625 ================================================ FILE: Content/HandGameplay/Props/Button/PushButtonBP.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:96054da4ed1469b8b1e3488aa649caf8187cdc93b4e4d33d8ae4ab7cac42658d size 33958 ================================================ FILE: Content/HandGameplay/Props/Button/SFX/GameConsole_button_press_in_01.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:e6b03d935a18205e06a46a9c5529da3bb331ddcda0032db317eacb15568f9c04 size 78128 ================================================ FILE: Content/HandGameplay/Props/Button/SFX/GameConsole_button_press_out_04.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:ca9d8977d512d6008e945d014abb37ccbdcc1f8002ba5a357f094e6dcc5b62b1 size 67611 ================================================ FILE: Content/HandGameplay/Props/Button/ToggleButtonBP.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:83a1ddf8dd873ac663b3f2ded9debbc012bec912b4ca61c9a2ac0fc202e93f1a size 59851 ================================================ FILE: Content/HandGameplay/Props/Button/testbutton_sm.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:2937db3174474c119042cec9b802a6c6af41365998f3c26963ef5e58249b5b0a size 106713 ================================================ FILE: Content/HandGameplay/Props/RingWeapon/2HandedBeamProp_Mesh.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:dccb18897ebeecba8cfddf6722e4e1dd7e15ed7bdb10c27bdf4d11fc22ca3fae size 136727 ================================================ FILE: Content/HandGameplay/Props/RingWeapon/InteractableArtefactHandle.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:c1bd40c9c5640ff847874f06a51c53a8f503ad8666fb6333c2f5ab29043cbbec size 22921 ================================================ FILE: Content/HandGameplay/Props/RingWeapon/InteractableTwoHandedArtefact.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:44baf3b7ad3dbec11623e8aaefb097b87cb72e72848f7d7af0c2e090f92bf444 size 406172 ================================================ FILE: Content/HandGameplay/Props/RingWeapon/TwoHandArtefact_StartBeam.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:54fe03d0134e0eef428b5b742e76b4ff06dd0f8cffe38982a99ab1edc957f407 size 306728 ================================================ FILE: Content/HandGameplay/Props/RingWeapon/TwoHandedBeamSystem.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:3c945eeb2e333e3f2abf4061b754b61d40a25435471f6a36842181fdb2f90031 size 905952 ================================================ FILE: Content/HandGameplay/Props/RingWeapon/vfxm_2HandedBeamProp.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:e64d98e9fc6a558f9441454c48a616a583b085c07050689ef3120c30da3b67d2 size 129560 ================================================ FILE: Content/HandGameplay/Props/RingWeapon/vfxm_OrbFloating.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:9ceb862c2a51374ba5bdb8235a9bb61b7b2ec57674ec3b18fcc0b7fc55bbbdc1 size 129313 ================================================ FILE: Content/HandGameplay/Props/RingWeapon/vfxm_TwoHandedArtefactBeam.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:daf831d5e14cb4c01178afbf354c8c2ef13da010d29c43a437df52421c2bb272 size 120677 ================================================ FILE: Content/HandGameplay/Props/RingWeapon/vfxm_TwoHandedArtefactBeam_Inst.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:afc44e2fdae94a4ff70eaa5a7023d1d9eb8b509329912d2402c3a700ae814c00 size 86454 ================================================ FILE: Content/HandGameplay/Props/Table/table_bc.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:e8f4e092d9e979fa974073fbda3e3a85bc625ebbaa6badae2d5646f8a51107e8 size 2873280 ================================================ FILE: Content/HandGameplay/Props/Table/table_mat.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:2ee811fdfef12013624d479c0710e8c2f1c35057081889265a7783ca71569837 size 122049 ================================================ FILE: Content/HandGameplay/Props/Table/table_sm.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:05e7df80416abb00f0f907278722785ba6dd1f63c70baa037bf39c4f717d8854 size 125429 ================================================ FILE: Content/HandGameplay/Props/Target/TargetBP.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:489e960fea29eee0a5cdfdae2af6ad98429e21b83a5ecb57c307551d5ca600dd size 168606 ================================================ FILE: Content/HandGameplay/Props/Target/sfx_spinning_target_01_loop.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:0c6f7b263811dce4f7ba17c113776cf47f89b2a21c3f6770f2ffc52db455806a size 2029960 ================================================ FILE: Content/HandGameplay/Props/Target/target_m.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:0f90611a4b3546bfbe3301812624af3dfcb7e1045bf7ef17439eecf11bace8f0 size 137620 ================================================ FILE: Content/HandGameplay/Props/Target/target_pentagon_sm.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:40810460e0acd422e7f79bdc76b0f112b4fd32502fcfc35009a44895d16c6e2e size 111988 ================================================ FILE: Content/HandGameplay/Props/Target/targets_bc.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:d3c3a8e05f71a62586c6e3ebbebebb750968d28276d524d6b73e1cc3c0f02c0d size 1393767 ================================================ FILE: Content/HandGameplay/Props/Target/targets_em.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:2013bdb1db3c56115fca9bf3d8b6fc665fb6e0a055a2f431f7b3c8d437a9e0d3 size 1461838 ================================================ FILE: Content/HandGameplay/Props/TeleportBeacon/Activate_vfx.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:f5e5063e39d93d5186a072e59cb7da62b4305da0ffcfc97594da5314d8172a06 size 219044 ================================================ FILE: Content/HandGameplay/Props/TeleportBeacon/NS_Activate.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:ff6745c9df34b10b024daffc552deb880712f9abe7299bafa2f62aa0baee79dc size 1120235 ================================================ FILE: Content/HandGameplay/Props/TeleportBeacon/TeleportBeacon.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:4fb8c9859a53a30bc3b58b9d16b9af7a0ecb29c1fecb7eaa39a0baa28c674458 size 115086 ================================================ FILE: Content/HandGameplay/Props/TeleportBeacon/TeleportationPad.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:b33f746beb7471a7d51b772c0a0f8bda81df64b64935b3c0e8768e09ac458182 size 220236 ================================================ FILE: Content/HandGameplay/Props/TeleportBeacon/TeleportationPadMesh.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:3352cb0e602349b25be96f31a176edbd2a1210ca99c56e9e7bf54f11186f946c size 103637 ================================================ FILE: Content/HandGameplay/Props/TeleportBeacon/vfxm_ActivateGlow_Inst.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:d16ea1baab686e825db506f5fd3c68ee6ca5ff70c41f6dac1ae68b23d0ff14ae size 82813 ================================================ FILE: Content/HandGameplay/Props/TeleportBeacon/vfxm_AlphaPanner.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:81ce8e1f1bf4edcee75faa6f7c4859328d56762459861f6e7b485dc24ca9a78a size 141201 ================================================ FILE: Content/HandGameplay/Props/TeleportBeacon/vfxm_TeleportBeaconPointerArrow.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:305a011c5d554a9d31a16abefa2329d658cfd5f61d3de7606196d554588318ae size 119615 ================================================ FILE: Content/HandGameplay/Props/TeleportBeacon/vfxmi_TeleportBeacon.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:391b9f9f78709f79e952028b104e19fc1001c468a5ad10953a357399fdd62cfd size 126239 ================================================ FILE: Content/HandGameplay/Props/TeleportBeacon/vfxmi_TeleportPointerArrowRing.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:655832a84c389743614cfeb4a940fca36f2e36265843667c94820a721816f4a0 size 114278 ================================================ FILE: Content/HandGameplay/Props/TeleportBeacon/vfxsm_TeleportBeacon.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:e57765522db7755917a7065be479277f5f29136a1ff5753d7b30cba823aea8d0 size 148367 ================================================ FILE: Content/HandGameplay/Props/TeleportBeacon/vfxsm_TeleportPointerArrowRing.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:ac19002af2dd2c8195bb367b2364f7d0c95590d0c6a188f539ff9697e0ef74a3 size 137894 ================================================ FILE: Content/HandGameplay/Props/TetherBall/Art/ball_bc.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:c738d56d7c0aa6f3bfb999f7675dc1164e647f306e25bd9e5983bf6ce4ca2d97 size 1241517 ================================================ FILE: Content/HandGameplay/Props/TetherBall/Art/ball_mat.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:fa16847b663305a08a1dc76f5f67b11c7e61351da8260dfc6f9b5b4f8eea259e size 123074 ================================================ FILE: Content/HandGameplay/Props/TetherBall/Art/ball_sm.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:8bb4bc79fd6fe522eaa4879f07631230aa55e337103849f7938717f1356c292e size 155978 ================================================ FILE: Content/HandGameplay/Props/TetherBall/Art/tether_ball_base.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:ed7f9002f59b8e0a40025addd009ebf0887e86d55d9d35c3b590bf54373e059e size 674230 ================================================ FILE: Content/HandGameplay/Props/TetherBall/Art/tether_ball_base_mat.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:cd91b5084903baf8a0a6efe2d947cc56543c37f2dca855ce43c94a4c0392c2f0 size 118285 ================================================ FILE: Content/HandGameplay/Props/TetherBall/Art/tether_ball_base_sm.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:7cc3f92bc854e0019cb4b912527a16e8ac0df97f378acee9a45f07b350fcfcd6 size 109258 ================================================ FILE: Content/HandGameplay/Props/TetherBall/Art/tether_mat.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:2e09393041414955f2180675afffe0851f1fd35c56d4a169f633e799fa865e85 size 115800 ================================================ FILE: Content/HandGameplay/Props/TetherBall/SFX/TetherBallHit.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:3a8704b2611d072bbe8b50d5bfb27edf3971bc544d223b1a40558aacfd86d7de size 16110 ================================================ FILE: Content/HandGameplay/Props/TetherBall/SFX/TetherBallSoundConcurrency.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:622ee5df7d8e40c606792c2ca659a94263d6652e0fc98e64d9871a4f00f2116e size 1549 ================================================ FILE: Content/HandGameplay/Props/TetherBall/SFX/TetherBall_RubberThick_Imp_01.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:4d62109799fb674d78162e53011364ccce82423aa10bf1c688dc3f60252c57ba size 43153 ================================================ FILE: Content/HandGameplay/Props/TetherBall/SFX/TetherBall_RubberThick_Imp_02.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:dcadb7b0591413a37ba76c9717196dee27dc56caa6af26ffeacd3aedd183671a size 33888 ================================================ FILE: Content/HandGameplay/Props/TetherBall/SFX/TetherBall_RubberThick_Imp_03.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:026dd90f3857c99e8f4e6fdcd3a0616aabea03cb067971090e63f26b2be0d4e4 size 48030 ================================================ FILE: Content/HandGameplay/Props/TetherBall/SFX/TetherBall_RubberThick_Imp_04.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:4ea7141b208fffaa22ba03276833ad9e17c61a0f2608f2da0179196081545092 size 62567 ================================================ FILE: Content/HandGameplay/Props/TetherBall/SFX/TetherBall_RubberThick_Imp_05.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:e726ebddfd66dbf5f9f8100b80f984e42be59715a92da8ed3fb8c0c5fce24e01 size 43129 ================================================ FILE: Content/HandGameplay/Props/TetherBall/SFX/TetherBall_RubberThick_Imp_06.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:a877d19496c02e40cdbfbec6b1f1e9d155f0c1c105046ae3253eb0df06bcd1eb size 36879 ================================================ FILE: Content/HandGameplay/Props/TetherBall/SFX/TetherBall_RubberThick_Imp_07.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:5b8910e973a1db85ba729aff9a38ee42ac23b17df628ee4914935ea89d694c77 size 30504 ================================================ FILE: Content/HandGameplay/Props/TetherBall/SFX/TetherBall_RubberThick_Imp_08.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:2ef4387350baa1ee0b82432080ad64616413becffd7d7a135fde13f92f1867d1 size 27541 ================================================ FILE: Content/HandGameplay/Props/TetherBall/SFX/TetherBall_RubberThick_Imp_09.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:02a2de4ddfd2caca2f16ec72b1c0fdccfa0a90a66fe8abb6694645ad75cab26f size 39754 ================================================ FILE: Content/HandGameplay/Props/TetherBall/TetherBallBP.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:a18056945e3328042cbf984ab8551b66c4160a003a5d0a147e85130a692482ab size 200287 ================================================ FILE: Content/HandGameplay/Props/TetherBall/ThetherBallPhysMat.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:d0cb195b43e06d51342ac25f054f5785d2297c2b065e21a5361a9180cf6b6083 size 1665 ================================================ FILE: Content/HandGameplay/Props/TetherBall/VFX/spawn_tetherballBase_ps.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:ffbb463f7c2c0efcbd1d33797ac6f1726b76f0f7ecaab705ff8b63b785b5b806 size 45863 ================================================ FILE: Content/HandGameplay/Props/TetherBall/tether_ball_sm.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:8eb309b42c11125e98086c44b90a212db8287902645446cf6b8a5b57b55da609 size 162513 ================================================ FILE: Content/HandGameplay/SharedArt/Blue.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:4d29c50208c87b00d2442ba40ec3bb323115988b1017db0350a94715671bd720 size 111177 ================================================ FILE: Content/HandGameplay/SharedArt/FlatBlue.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:4adf7a3032a3f8b0b41679d9f58a37902a400eab565d8189ff9c14371e93d14e size 104258 ================================================ FILE: Content/HandGameplay/SharedArt/Red.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:ba3bd8c301cf90755b3516af72f17c786d3fc10c356ec1364ada32cfb241e5a3 size 113428 ================================================ FILE: Content/HandGameplay/SharedArt/T_Caustic01.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:c579f1a4402b4aaa42b6f080aa6acce3bebb9808e44bd53c61b7cc5d0aa4dbee size 1530727 ================================================ FILE: Content/HandGameplay/SharedArt/T_WorleyNoise01.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:31bc66ed0c48b42be7b627b057dfd357b491fc6bedd5ec8bb13a13cc9b1f8ae6 size 448233 ================================================ FILE: Content/HandGameplay/SharedArt/TextBackground.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:ceb8c1e3729c63698365449612b9c8da558cea0e5adb96a99020247afda4bd55 size 109062 ================================================ FILE: HandGameplay.uproject ================================================ { "FileVersion": 3, "EngineAssociation": "", "Category": "", "Description": "", "Modules": [ { "Name": "HandGameplay", "Type": "Runtime", "LoadingPhase": "Default", "AdditionalDependencies": [ "Engine" ] } ], "Plugins": [ { "Name": "ApexDestruction", "Enabled": true }, { "Name": "GooglePAD", "Enabled": false }, { "Name": "OculusXR", "Enabled": true, "MarketplaceURL": "com.epicgames.launcher://ue/marketplace/product/8313d8d7e7cf4e03a33e79eb757bccba", "SupportedTargetPlatforms": [ "Win64", "Android" ] }, { "Name": "OpenXRHandTracking", "Enabled": true, "SupportedTargetPlatforms": [ "Win64", "Linux", "Android" ] } ] } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) Facebook, Inc. and its affiliates. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Platforms/HoloLens/Config/HoloLensEngine.ini ================================================ [/Script/HoloLensPlatformEditor.HoloLensTargetSettings] bBuildForEmulation=False bBuildForDevice=True bUseNameForLogo=True bBuildForRetailWindowsStore=False bAutoIncrementVersion=False bShouldCreateAppInstaller=False AppInstallerInstallationURL= HoursBetweenUpdateChecks=0 bEnablePIXProfiling=False TileBackgroundColor=(B=64,G=0,R=0,A=255) SplashScreenBackgroundColor=(B=64,G=0,R=0,A=255) +PerCultureResources=(CultureId="",Strings=(PackageDisplayName="",PublisherDisplayName="",PackageDescription="",ApplicationDisplayName="",ApplicationDescription=""),Images=()) TargetDeviceFamily=Windows.Holographic MinimumPlatformVersion=10.0.18362.0 MaximumPlatformVersionTested=10.0.19041.0 MaxTrianglesPerCubicMeter=500.000000 SpatialMeshingVolumeSize=20.000000 CompilerVersion=Default Windows10SDKVersion=10.0.18362.0 +CapabilityList=internetClientServer +CapabilityList=privateNetworkClientServer +Uap2CapabilityList=spatialPerception bSetDefaultCapabilities=False SpatializationPlugin= SourceDataOverridePlugin= ReverbPlugin= OcclusionPlugin= SoundCueCookQualityIndex=-1 ================================================ FILE: Plugins/OculusHandTools/.gitattributes ================================================ *.a filter=lfs diff=lfs merge=lfs -text *.lib filter=lfs diff=lfs merge=lfs -text *.uasset filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text *.umap filter=lfs diff=lfs merge=lfs -text *.jpg filter=lfs diff=lfs merge=lfs -text ================================================ FILE: Plugins/OculusHandTools/.gitignore ================================================ # Visual Studio 2015 user specific files .vs/ # Compiled Object files *.slo *.lo *.o *.obj # Precompiled Headers *.gch *.pch # Fortran module files *.mod # Executables *.exe *.out *.app *.ipa # These project files can be generated by the engine *.xcodeproj *.xcworkspace *.sln *.suo *.opensdf *.sdf *.VC.db *.VC.opendb # Precompiled Assets SourceArt/**/*.png SourceArt/**/*.tga # Binary Files Binaries/* Plugins/*/Binaries/* # Builds Build/* # Whitelist PakBlacklist-.txt files !Build/*/ Build/*/** !Build/*/PakBlacklist*.txt # Don't ignore icon files in Build !Build/*/**/ !Build/**/*.ico !Build/**/*.png # Built data for maps *_BuiltData.uasset # Configuration files generated by the Editor Saved/* # Compiled source files for the engine to use Intermediate/* Plugins/*/Intermediate/* # Cache files for the editor to use DerivedDataCache/* StoreAssets/ GenerateSln.ps1 ================================================ FILE: Plugins/OculusHandTools/Config/FilterPlugin.ini ================================================ [FilterPlugin] ; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and ; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively. ; ; Examples: ; /README.txt ; /Extras/... ; /Binaries/ThirdParty/*.dll ================================================ FILE: Plugins/OculusHandTools/Content/BlueprintMacros.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:fd8a267cc165f554ad3caca177326a8ffdc8d28f0e465eda99986f44474f955b size 19603 ================================================ FILE: Plugins/OculusHandTools/Content/Button/IsButtonableInterface.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:bbc6041451dbced11619631f508c5bba488a8c751c6807027058155431a77e34 size 8842 ================================================ FILE: Plugins/OculusHandTools/Content/Button/PushButtonBaseBP.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:bc31619c51f959dd3d66f795acdb4b2278cecd65fe431b45bcd54c76cfa7010d size 420145 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-Black.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:32771fd4caa2a3602a864be461f4648c71c44a6525ba97cfb3f90204b77d1b1e size 158824 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-BlackItalic.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:478864f05f6d4be2278d511f832853bcc9e9ee22f8fcf3703e6bec9fc6ef29a2 size 193332 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-BlackItalic_Font.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:47c5b118bf8405524375bbcacfb6910b90c110f4ec8272f47cf6437048f50ab7 size 9053 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-Black_Font.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:ceb0a59d1241f49c24fbd1c8b1511ddeca5f630195be570de617f55655ff652c size 8641 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-Bold.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:5311a26a07352ceb5e038fde8584c38231f42f4c2926ad617dc6a53876695792 size 157470 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-BoldItalic.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:3980178ca9739b58ce89f889be02ef11a9e35332149930d70de0a3b02bd6ef88 size 190918 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-BoldItalic_Font.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:ee0e5d9b718f4dbffc4cbb075aeee3df287a41094275f130f1c4f5969d24388e size 9150 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-Bold_Font.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:60e3255a84e3abdf54526b725235cbb1d20c2e2f8ba99c502b365ce4fa4ca433 size 8676 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-DemiBold.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:5a1213f99a6198d5344c9ffdc1289f46a91bf928b845f4bf18dc956b98bf3690 size 157094 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-DemiBoldItalic.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:3ab4e717e741ce59e63e9f887f78b2b0de3e552b2cf1e43410964af97a68deb0 size 191850 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-DemiBoldItalic_Font.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:fe14c12e99f34c5de5836e7375f93de0f556b16b7b9c577d25589d77d7a5eac4 size 9168 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-DemiBold_Font.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:e5d3096fdd763e6e7f950f15cf65bf2d37d881b6eb5768628c89fb9466683717 size 8781 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-Light.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:43ffafdee0fda042fdb2c5bc68c289385982536652234ad7b2384b56392d077b size 157532 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-LightItalic.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:a2170036e9e99f4e5084cada6e4a415d032f9c02366b3ce82fc9907aef1dcf81 size 189180 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-LightItalic_Font.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:56d9fd7cb3521ef1651d623d937e052396e185dbc809f030188855f5a6962520 size 9330 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-Light_Font.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:64992b5f8cd6ea277842b76862e5dd4de494c31e3638e5a68d80a73ddf77035e size 8608 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-Medium.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:e3656a97844e3300b46ee7ac8ad48329fd1eef673c9acc701f2c051ce9cccd54 size 144430 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-MediumItalic.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:06a652b0d2208ce5bec58021f44ca7d304389e6dbe1b0b2c001bd414ffb1a9eb size 187234 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-MediumItalic_Font.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:89b71292847af0e4776c0f4679391a26144b71189555afec3e1b780320743949 size 9367 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-Medium_Font.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:f80911b61ff847747655c9e3f159df168a3b1cc4eaacb42c32149ef9c973c40d size 8835 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-Normal.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:a94395cc1f34cbc8d5b0c482ffd769450c65bed2231c985f3926d2afacc36b6f size 155734 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-NormalItalic.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:e201ffc9b185bc0edee5241cce68000b6dca445166fbc5f60132007fd2012845 size 191538 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-NormalItalic_Font.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:1218b2fd1e272348793881364a83b34cd58464103844a5a91d86848fa9d25924 size 9455 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-Normal_Font.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:0b710fa5fbdcabfcedadfbb516a918fa01f0ed6dbb93bc6f5828f03fef395008 size 8882 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-Regular.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:77691182bc12af00b5ea06e4d81ff70c7a4bed5c58be727cb8b31cb455754656 size 157876 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-RegularItalic.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:d128ad81e480e1535a2fe3f89c7aa304b1730994022c1e69cfa38121188d8c41 size 189356 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-RegularItalic_Font.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:fcedc6cbb14e81b0b1d6f017e830c8b27f1db03f35722a6c0fa355dc2aa35af7 size 9370 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-Regular_Font.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:b8fa7e6ca6a15773f0c2b3a3a6cec65a368f3c05656bb62923db5a5eb5cd373a size 8948 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-SemiBold.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:b45e9eb70fd35ecc3b1051ac503e8e41dbbab1be67c057db5c6abf0a89870055 size 139022 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-SemiBoldItalic.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:77d8797a5d6fda4c105946d782339b184b4a4fa13ecb257c1030c60ade5d2d3c size 188166 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-SemiBoldItalic_Font.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:cf4128d4ee4e48a42666ce26e30e8880d95cbb9e491162f4b9868ec1e16a37f1 size 8959 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-SemiBold_Font.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:3b6506d5c861eeb27575386fdfc5638e17999a01eca3eed100317305533de002 size 8559 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-Thin.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:660bd26d26249b97875e1dcad98c279995bcc3ae2a2f3a95a7a1df71ae2cfe96 size 74730 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-ThinItalic.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:b814c0746547695abc1958721d673c715a4783d7413024a0d336d8810afec699 size 155030 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-ThinItalic_Font.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:f9b4e8279d87af58943e723ae7776e722c210f73da2b3dba7b1b5dfba7a56dd7 size 9096 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-Thin_Font.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:35df1772e8a52af570664cb5ffcac7ccc2e5ef61672d6fd912d9d72327dfc482 size 8159 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-Ultra.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:e2cbe30fe7f907a9275178800d1cb0520348c7664e8bf14894f4232608eaf44b size 78320 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-UltraItalic.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:a6493fec0fd50c1525add29b3764f39ea0d2b00fd51eacfed41dd0d420c63247 size 164176 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-UltraItalic_Font.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:760804e0434aa3917132456e13d67ba2f2c98f6977ed08f1fc47535e707f26b6 size 9228 ================================================ FILE: Plugins/OculusHandTools/Content/Fonts/OculusSans-Ultra_Font.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:76c5a834e42047acebba52777552f1b4d1f537addb4429e68706bfc6d56ffdbd size 8511 ================================================ FILE: Plugins/OculusHandTools/Content/HandDebug/HandDebugActor.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:44406daaad17618ee0158bcb182d52ee7b13fee81306cf30f1684c1f23efbec5 size 491492 ================================================ FILE: Plugins/OculusHandTools/Content/HandDebug/HandDebugWidget.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:3aae4ad96088bd115446a24b81e1062d238947a150e564404a461ecf4f24b1ae size 808054 ================================================ FILE: Plugins/OculusHandTools/Content/HandDebug/STADIUM_Black-01.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:9cc4e46fb0b5e26007c69227011fd7b52d2ede4382c61e51a753f3c4c584f20b size 12116 ================================================ FILE: Plugins/OculusHandTools/Content/HandDebug/WidgetInvisibleMaterial.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:61ff3feacce79561a4519ee16539ee4147b974381f7b314c91606ff501728d94 size 88316 ================================================ FILE: Plugins/OculusHandTools/Content/Hands/HandsCharacterBase.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:16b0f518b3dddea779a9e1b4e6263af40c710c94dba7c00aa7e7bf3098c3a055 size 259675 ================================================ FILE: Plugins/OculusHandTools/Content/Hands/HandsCharacterHandState.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:30d88c023a9cec698c205b42103315fcdab02a8fedeeffaeb9bf853caa967d50 size 608331 ================================================ FILE: Plugins/OculusHandTools/Content/Hands/TeleportSelector.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:339afc81c650e5b098e391cd286c9d3f18ae5119f81c9f84775a447d3c957fa8 size 36134 ================================================ FILE: Plugins/OculusHandTools/Content/Hands/TutorialHand.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:d33902179993bbf5c81b590a5b0d52dc3b0e65db2ed5cf26c8d6ebfcae70d85e size 58115 ================================================ FILE: Plugins/OculusHandTools/Content/RespawnProps/FloorIdentifier.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:feb0d6df608e45ab8ccb8a769c2c5b8ed9b40f1de4610b8540abc2c8a3cea5d4 size 55755 ================================================ FILE: Plugins/OculusHandTools/Content/RespawnProps/RespawnFromFloor.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:5a93c3ecc1f9d612b2454ea506a725a575ad6c6465f79c82087d0f78c75d960d size 58532 ================================================ FILE: Plugins/OculusHandTools/Content/Selectors/AimingGlow.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:2001a56ea1f231e9ef6eed6fe806f00961f3d7ef304f84ae717e6c261cf750e1 size 80386 ================================================ FILE: Plugins/OculusHandTools/Content/Selectors/DefaultAimingActor.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:5fc11596db979534c1661daeaf72afaeff80663fa0da224a1ce47abad5322664 size 96075 ================================================ FILE: Plugins/OculusHandTools/Content/Selectors/DefaultAimingSphere.uasset ================================================ version https://git-lfs.github.com/spec/v1 oid sha256:8efa83ee2630b4e6741bd55163b0969bbc9cfe4cc30d9c03f3207d57c62bfe52 size 90949 ================================================ FILE: Plugins/OculusHandTools/OculusHandTools.uplugin ================================================ { "FileVersion": 3, "Version": 1, "VersionName": "1.1", "FriendlyName": "OculusHandTools", "Description": "", "Category": "Other", "CreatedBy": "", "CreatedByURL": "", "DocsURL": "", "MarketplaceURL": "", "SupportURL": "", "CanContainContent": true, "IsBetaVersion": false, "IsExperimentalVersion": false, "Installed": false, "SupportedTargetPlatforms": [ "Win64", "Mac", "Android" ], "Modules": [ { "Name": "OculusHandPoseRecognition", "Type": "Runtime", "LoadingPhase": "Default", "WhitelistPlatforms": [ "Win64", "Mac", "Android" ] }, { "Name": "OculusInteractable", "Type": "Runtime", "LoadingPhase": "Default", "WhitelistPlatforms": [ "Win64", "Mac", "Android" ] }, { "Name": "HandInput", "Type": "Runtime", "LoadingPhase": "Default", "WhitelistPlatforms": [ "Win64", "Mac", "Android" ] }, { "Name": "HandTrackingFilter", "Type": "Runtime", "LoadingPhase": "Default", "WhitelistPlatforms": [ "Win64", "Mac", "Android" ] }, { "Name": "OculusThrowAssist", "Type": "Runtime", "LoadingPhase": "Default", "WhitelistPlatforms": [ "Win64", "Mac", "Android" ] } ], "Plugins": [ { "Name": "OculusXR", "Enabled": true, "MarketplaceURL": "com.epicgames.launcher://ue/marketplace/product/8313d8d7e7cf4e03a33e79eb757bccba", "SupportedTargetPlatforms": [ "Win64", "Android" ] }, { "Name": "OculusUtils", "Enabled": true } ] } ================================================ FILE: Plugins/OculusHandTools/README.md ================================================ # Meta Quest Hand Tools For UE4 ## Meta Quest Hand Tracking The Meta Quest (Oculus) is the first platform to offer native hand tracking using inside-out cameras. Its hand tracking API provides raw hand bone rotations, hand location and orientation, confidence levels, and recognition of some higher-level hand poses like pinch strength and system poses. To help developers start with raw hand bone rotations, the Meta Quest DevTech team created the Meta Quest Hand Tools plugin for UE4. It supports many common hand tracking mechanics and utilities. Most mechanics are implemented as blueprints in the [Content](./Content/) folder. More details follow below. Additional mechanics and utilities are available in these C++ modules: - [HandInput module](./README_HandInput.md) - [HandPoseRecognition module](./README_HandPoseRecognition.md) - [OculusHandTrackingFilter module](./README_HandTrackingFilter.md) - [OculusInteractable module](./README_Interactable.md) - [OculusThrowAssist module](./README_ThrowAssist.md) - [OculusUtils module](./README_OculusUtils.md) ## Mechanics Implementations The Content folder contains utilities for hand tracking gameplay mechanics. Examples appear in the *Hand Gameplay Showcase*. All mechanics integrate into the *HandsCharacterHandState* component. This component is created in the construction script of *HandsCharacterBase*, which sets up references to all relevant actor components. To add these mechanics to your project, create a new blueprint class with *HandsCharacterBase* as its parent. The showcase’s *HandsCharacter* class demonstrates this. ### Throwing The *HandsCharacterHandState* class implements throwing in the "Throwing" section of its event graph. Its *Throw with Assist* function calls the *Get Throw Vector* method of the *ThrowingComponent*. This component tracks hand position over a short time to calculate an accurate, predictable velocity for thrown objects. ### Punching Punching is a simple, fun mechanic using hand tracking. In the "Fist / punching" section of the *HandsCharacterHandState* event graph, a collision sphere on the hand activates only when the hand forms a fist and no object is grabbed. The *TetherBallBP* class in the Hand Gameplay Showcase provides an example of a "punchable" object. ### Teleportation You can implement a simple teleportation system using *InteractableSelector* actors from the *OculusInteractable* module. In the "Grabbing" section of the *HandsCharacterHandState* event graph, if a grab fails, it checks for a selected teleporter and teleports the player there. Teleporter selection happens in the "Selection" section of the same graph. To restrict selection to teleporters, use the *TeleportSelector* actor instead of the standard *InteractableSelector*. Add this to your project's *DefaultEngine.ini*: ```ini [/Script/Engine.CollisionProfile] +DefaultChannelResponses=(Channel=ECC_GameTraceChannel1,DefaultResponse=ECR_Ignore,bTraceType=True,bStaticObject=False,Name="Interactable") ``` ### Pushing Buttons The *PushButtonBaseBP* class handles buttons pushed by the player’s pointer finger. The button moves with the finger and plays clicking sounds when fully pressed. Use the *OnButtonPress* event to define the button’s response. The button’s Box component uses the *FingerTip* collision channel. The *RightFingerTip* component in *HandsCharacterBase* uses the same channel. Add this to your project's *DefaultEngine.ini*: ```ini [/Script/Engine.CollisionProfile] +DefaultChannelResponses=(Channel=ECC_GameTraceChannel2,DefaultResponse=ECR_Overlap,bTraceType=False,bStaticObject=False,Name="FingerTip") ``` ### Two-Handed Aiming During *First Steps with Hands*, we found the two-handed rifle very stable and satisfying to aim. The *InteractableTwoHandedArtefact* class shows how to implement this aiming style using the grabbing system. ### Example Hands for Tutorials Use the *TutorialHand* actor to display hand poses to players. Set the pose via the [*Pose String*](./README_HandPoseRecognition.md#pose-strings) property. ## Future Improvements We hope this showcase inspires your hand tracking projects. The team looks forward to seeing your work and improvements. We are developing ways for you to share your projects with the Meta Quest community. ================================================ FILE: Plugins/OculusHandTools/README_HandInput.md ================================================ # Hand Input ## Grabbing (with Pose Override) The *CameraHandInput* component handles grabbing. It uses the *IsGripping* and *IsPinching* methods to detect various grab poses. Additional logic appears in *HandsCharacterHandState* under the "Grabbing," "Final attachment," and "Pose" sections of its event graph. To keep the hand wrapped naturally around a grabbed object, you must override the hand pose. Provide the component with a "Bone Map" to apply poses to the hand mesh. An actor must inherit from the *Interactable* class to be grabbable. Set grab pose properties using hand pose strings within this class. For an example, see the *InteractableBrick* class in the Hand Gameplay Showcase. ## Finger Stabilization The *CameraHandInput* component also stabilizes and smooths the hand skeleton. You can adjust or disable this filtering through its properties: ================================================ FILE: Plugins/OculusHandTools/README_HandPoseRecognition.md ================================================ # The OculusHandPoseRecognition Module This module includes two [USceneComponent](https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Engine/USceneComponent?application_version=5.6) subclasses. They recognize specific hand poses—defined by relative hand bone angles—and hand gestures—defined as sequences of hand poses. It also provides a library of useful blueprint utility nodes. ## 1. Hand Pose Recognizer Component The [UHandPoseRecognizer](./Source/OculusHandPoseRecognition/Public/HandPoseRecognizer.h) class is the core of the hand recognition system. It regularly polls the [Oculus Hands API](../../../../../Engine/Plugins/Runtime/Oculus/OculusVR/Source/OculusInput/Public/OculusXRInputFunctionLibrary.h) to record hand bone data when reliable. Wrist bone data comes from the motion controller component attached to the player's VR character. Recognizer attached to controller. You can use multiple recognizers per hand, each with its own set of hand poses. Group poses into one recognizer when they are mutually exclusive. For example, our showcase uses one recognizer for American Sign Language numbers and another for letters. Although ASL distinguishes the letter 'O' from the number '0', our example uses static hand poses recognized by separate recognizers to detect both simultaneously. ### Pose Strings Each recognizer defines what it can recognize as a set of bone angles. Here is the thumbs-up pose for the left hand: L T0-52-18+51 T1+13-8+30 T2+7-9-10 T3-10+21+8 I1+6-72+1 I2-3-108+1 I3+1-55-3 M1+1-77-8 M2-1-99+1 M3-6-51-8 R1-4-85-10 R2-5-100-1 R3-4-50-1 P0+15-6-25 P1+8-88+4 P2-8-94-7 P3-4-54+2 W+81+0+0 The description starts with 'L' or 'R' for left or right hand. It lists hand bones in order: 'T' (Thumb), 'I' (Index), 'M' (Middle), 'R' (Ring), 'P' (Pinky), and 'W' (Wrist). All bones except the wrist include a bone index starting at 0 for thumb and pinky, and 1 for the other fingers. Angles are three signed numbers for pitch, yaw, and roll, in degrees. You can generate new pose strings using the logger described in [Using a Hand Pose Recognizer](#using-a-hand-pose-recognizer). ### Computing Distance (Error) to Hand Pose Matching a hand to a reference pose starts by summing the squares of all angular differences. A lower sum means a better match. You decide what error value counts as a good match; this is covered in the next section. Sometimes, you may want to ignore some bones. For example, in a "gun" pose, some people use only the index finger, others use both index and middle fingers. To match both with one pose, remove the middle finger bones from the reference pose. You can also ignore specific angles (pitch, yaw, or roll) of a bone. For example, in the thumbs-up pose, you only need to know if the thumb points up or down, not its facing direction. Use +0 to ignore an angle. Use +1 or -1 to recognize angles near zero. We have realized that for some bones are more (or less) important than others for some poses. For that purpose, you can add between the bone identifier and the pitch/yaw/roll values a weight value. Here's a full example that combines weights and ignores specific fingers: L T0*3-61+1+31 T1*3+12-8+30 T2*3+6-13-10 T3*3-10+21+8 I1+16-22-1 I2+1-2-3 I3+3+2-2 R1-2-77-12 R2-5-100-1 R3-4-57-1 P0+15-6-25 P1+8-90-3 P2-5-76-8 P3-4+1+3 This left hand "gun" pose differs from the "gun shot" pose because the thumb is lifted away from the index. The thumb's weight is increased by 3. The middle finger is ignored to match both single and double-barrel poses. The wrist is ignored, so hand orientation of which way the hand is pointed does not matter. ### Error vs Confidence Level Weights and ignoring angles or fingers affect the error range. You may also want to be more lenient with some poses. To normalize this, each pose defines a maximum error value for 100% confidence (error at max confidence, EAMC). For example, if you set EAMC to 2000, any error below 2000 means full confidence (1.0). An error of 4000 corresponds to 0.5 confidence, and so on. | Error | Confidence (EAMC = 2000) | | ----- | ------------------------ | | 500 | 1.00 | | 2000 | 1.00 | | 3000 | 0.66 | | 4000 | 0.50 | | 6000 | 0.33 | | 8000 | 0.25 | Confidence levels simplify handling recognition results, but raw errors remain useful during development to set EAMC. ### Configuring a Hand Pose Recognizer In the hand pose recognizer's details panel, find the *Hand Pose Recognition* section. Recognizer configuration. Set the *side* value to recognize the left or right hand. Set it to none to disable the recognizer. The recognition interval throttles recognition frequency. The default 0s runs recognition every tick. The confidence floor sets the minimum confidence required to recognize a pose. You can set a default at the recognizer level and customize it per pose. The damping factor controls how slowly bone updates integrate per recognition interval. By default, the latest values fully replace the current state every tick. A value of 0.2 blends 80% of the latest value with the current state. You can configure an array of [poses](#pose-strings). There is no limit to the number of poses per recognizer. Each pose has a name, which need not be unique. You also set the encoded pose, custom confidence floor, and error at max confidence. ### Using a Hand Pose Recognizer The *Log Encoded Hand Pose* blueprint node outputs the current hand pose as an encoded string to the output log. The example below shows recognizers for both hands wired to input events. Log hand pose. In this example, CTRL-L and CTRL-R keyboard events trigger logging. A typical workflow is to generate hand poses, log them, then transfer or modify the strings for use in a hand pose recognizer. The *Get Recognized Hand Pose* node retrieves the current recognition state. Get recognized hand pose. This image from the Hand Pose Showcase shows the VRCharacter blueprint forwarding the recognizer's state to display on a projector slide. When a pose is recognized (confidence above the floor), you get its index, name, duration held (seconds), raw error, and confidence level. ## 2. Hand Gesture Recognizer Component The [UHandGestureRecognizer](./Source/OculusHandPoseRecognition/Public/HandGestureRecognizer.h) class handles gesture recognition. A gesture is a sequence of hand poses. Our implementation recognizes gestures only from one hand pose recognizer, which is the common case. Attach the gesture recognizer to the pose recognizer it depends on. Gesture recognizer. In this screenshot, *LeftFlickSwipeGestureRecognizer* attaches to *LeftFlickSwipePoseRecognizer*. This gesture moves the projector to the previous slide. Similar recognizers on the right hand move to the next slide. ### Defining a Gesture as a Sequence of Poses In the Hand Pose Showcase, a sci-fi movie gesture changes channels by pointing the right index and middle fingers at the screen, then flicking left. We tested other gestures but found this one easy to learn and perform. First, the static hand pose recognizer: Flick pose recognition. It recognizes two poses: pointing and flicking. Next, the gesture recognizer: Flick gesture recognition. The recognition interval throttles gesture recognition frequency. It defaults to every game tick. The skipped frames value adds delay control but is experimental and may be removed. This example recognizes one gesture, "Flick," which transitions from "Point" to "Flick." Gesture names need not be unique. *Max Transition Time* sets the maximum allowed time (seconds) between poses, including when no pose is recognized. Here, it tolerates up to 0.2 seconds between "Point" and "Flick." You can require holding a pose before continuing. For example: Point/200, Flick The user must hold "Point" for 200 milliseconds before "Flick" triggers the gesture. The *Is Looping* flag enables recognition of looping gestures, like waving your hand. ### Using a Hand Gesture Recognizer The gesture recognizer supports *force grab* and *force throw* gestures. Here is part of the grabbing code in VRCharacter: Grab gesture recognition. This node returns whether a gesture was recognized. If so, you get the gesture index, name (not necessarily unique), and direction. The gesture direction is a vector from an average location near the end of the first pose to an average location near the start of the last pose. We also experimented with gesture duration measures. The outer duration spans from the start of the first pose to the end of the last. The inner duration spans from near the end of the first pose to near the start of the last. The *Behavior* argument controls how the recognizer resets after recognizing a gesture. By default, only the recognized gesture resets. You can reset all gestures if needed. Gesture progress. The Hand Pose Showcase shows how to detect a gesture in progress. Here, the player's hand highlights when ready to flick to the previous or next slide. The "Flick" gesture is queried to check if it is in progress. ## 3. Hand Recognition Blueprint Library Pose and gesture wait nodes. ### Waiting for Hand Pose UE4 supports latent blueprint nodes that pause execution until a condition is met. The *Wait for Hand Pose* node waits for a specified UHandPoseRecognizer instance to recognize a pose. Specify *Min Pose Duration* to require the pose be held for a minimum time. If you set a non-negative *Time to Wait*, the node exits via the *Time Out* pin if no pose is recognized within that time. When a pose is recognized, execution resumes at the *Pose Seen* pin, providing the recognized pose index and name. ### Waiting for Gesture The *Wait for Hand Gesture* node waits for a gesture recognition. Specify how long to wait with *Time to Wait*. The *Behavior* argument matches that in the *GetRecognizedHandGesture* node. When a gesture is recognized, execution resumes at the *Gesture Seen* pin, providing gesture index, name, direction, and durations. If *Time to Wait* expires, the node exits via the *Time Out* pin. You can loop back into the wait node to wait again, as shown in the screenshot. ### Recording a Pose Range Pose range recording. The *Record Hand Pose* blueprint records the range of hand bone angles. In the screenshot from *HandPoseShowcase*, you start and stop recording with a keyboard key. When stopped, the node outputs a report like this (recording the [royal wave](https://www.youtube.com/watch?v=n5pkDB7zEeo)): Hand Pose Range Recorded #0 Thumb_0 pitch 11.58 [ -55.70 .. -44.12] yaw 13.78 [ -26.18 .. -12.40] roll 16.16 [ +44.29 .. +60.45] Thumb_1 pitch 14.34 [ +14.35 .. +28.70] yaw 9.21 [ -6.92 .. +2.29] roll 1.96 [ +30.00 .. +31.96] Thumb_2 pitch 1.39 [ -0.03 .. +1.37] yaw 7.99 [ -45.73 .. -37.74] roll 0.46 [ -10.10 .. -9.64] Thumb_3 pitch 2.92 [ -7.76 .. -4.84] yaw 17.45 [ -12.73 .. +4.72] roll 0.45 [ +9.26 .. +9.71] Index_1 pitch 5.86 [ +4.05 .. +9.91] yaw 10.05 [ -31.78 .. -21.73] roll 1.99 [ +0.69 .. +2.69] Index_2 pitch 0.46 [ -0.03 .. +0.43] yaw 8.70 [ -16.26 .. -7.55] roll 0.02 [ -3.04 .. -3.03] Index_3 pitch 0.17 [ +3.06 .. +3.22] yaw 5.54 [ +2.21 .. +7.75] roll 0.19 [ -1.82 .. -1.63] Middle_1 pitch 8.14 [ -1.27 .. +6.87] yaw 13.01 [ -37.69 .. -24.67] roll 4.12 [ -4.32 .. -0.20] Middle_2 pitch 0.20 [ +0.20 .. +0.40] yaw 8.38 [ -12.74 .. -4.36] roll 0.06 [ -1.39 .. -1.33] Middle_3 pitch 0.70 [ +0.11 .. +0.81] yaw 8.97 [ +0.51 .. +9.48] roll 0.95 [ -4.95 .. -4.00] Ring_1 pitch 5.89 [ -0.00 .. +5.89] yaw 18.23 [ -34.62 .. -16.39] roll 3.38 [ -9.76 .. -6.39] Ring_2 pitch 0.83 [ -0.93 .. -0.10] yaw 11.61 [ -18.00 .. -6.39] roll 0.16 [ -4.15 .. -3.99] Ring_3 pitch 0.06 [ -3.40 .. -3.34] yaw 6.10 [ -3.92 .. +2.18] roll 0.11 [ -0.60 .. -0.50] Pinky_0 pitch 0.00 [ +15.32 .. +15.32] yaw 0.00 [ -5.57 .. -5.57] roll 0.00 [ -24.89 .. -24.89] Pinky_1 pitch 10.01 [ -9.62 .. +0.39] yaw 19.92 [ -27.22 .. -7.30] roll 3.09 [ +10.10 .. +13.20] Pinky_2 pitch 1.93 [ +2.19 .. +4.11] yaw 17.13 [ -25.88 .. -8.75] roll 1.73 [ -7.25 .. -5.52] Pinky_3 pitch 0.05 [ -5.64 .. -5.59] yaw 11.15 [ -8.46 .. +2.69] roll 0.58 [ -0.06 .. +0.53] Wrist pitch 32.72 [ -7.06 .. +25.67] yaw 0.72 [-179.60 .. +179.67] roll 28.95 [ -90.53 .. -61.58] Hand pose range total square error - full=1325.39 half=331.35 quarter=82.84 ================================================ FILE: Plugins/OculusHandTools/README_HandTrackingFilter.md ================================================ # Filtered Hand Tracking The *HandTrackingFilterComponent* improves hand-tracking in games by stabilizing hand movement when tracking quality is low or lost. Attach this component to the *MotionControllerComponent* in your Character to achieve smoother hand tracking. For more details, see the "Hand tracking accuracy mitigation" section in [Adding Hand Tracking To First Steps](https://developers.meta.com/horizon/blog/adding-hand-tracking-to-first-steps/). ================================================ FILE: Plugins/OculusHandTools/README_Interactable.md ================================================ # The OculusInteractable Module The *HandPoseShowcase* brings a proven hand gameplay mechanic to UE4. One of our internal Meta Quest teams created a demo for Unity called Tiny Castles. We chose their *force grab/throw* mechanic to showcase in this project. To implement this, we created our own far field selector, [AInteractableSelector](./Source/OculusInteractable/Public/InteractableSelector.h), designed to attach to the VR character's motion controller. It can also attach to the HMD or other components. The interactable selector selects game actors subclassed from [AInteractable](./Source/OculusInteractable/Public/Interactable.h). ## Interactable Actors AInteractable actors have two main events triggered by the selector: *BeginSelection* and *EndSelection*. The selector selects only one object at a time. This object receives notifications when selection starts and ends. Typically, selection highlights the chosen actor. The interactable interface also defines three user events for game-specific use. The plugin does not use these events or define their meaning. In *HandPoseShowcase*, *Interaction1*, *Interaction2*, and *Interaction3* correspond to *BeginGrab* (user starts grabbing), *EndGrab* (object reaches the user's hand), and *Drop* (user releases the object), respectively. To be selectable, an AInteractable actor must have at least one mesh that generates overlap events with the first game trace channel. In our implementation (see [AInteractableSelector](./Source/OculusInteractable/Private/InteractableSelector.cpp)), the following is defined at the top of the file: ```cpp ECollisionChannel InteractableTraceChannel = ECollisionChannel::ECC_GameTraceChannel1; ``` This trace channel is added in the project settings under the engine collision section, as shown below. Interactable trace channel. If your project uses this channel, assign `InteractableTraceChannel` to a different collision channel. ## Interactable Selector The far field selector works out of the box but is meant to be subclassed and customized. It has many configuration parameters. Default far field selector. The selector usually starts deactivated. In *HandPoseShowcase*, it activates when the user makes an open palm hand pose. The selection ray begins at the specified *Raycast Offset* relative to the selector actor and traces forward for the specified *Raycast Distance*. Two angles control selector behavior: *Raycast Angle* and *Raycast Stickiness Angle*. The *Raycast Angle* defines the selection cone—only objects inside this cone can be selected. The *Raycast Stickiness Angle* allows the current selection to remain active even if it moves outside the stricter cone. The selector’s visuals include an *Aiming Actor* and a particle beam effect. If no *Aiming Actor* exists, the *Aiming Actor Class* spawns one. You can align the aiming actor with the surface hit normal and control its rotation speed using *Aiming Actor Rotation Rate*. The *Dampening Factor* affects aiming stability. Without dampening, aiming jitters with the user's hand movements. ================================================ FILE: Plugins/OculusHandTools/README_OculusUtils.md ================================================ # The OculusUtils Module ## Continuous Overlap The *RightFingerTip* in *HandsCharacterBase* is a *ContinuousOverlapSphereComponent*, unlike the fist spheres, which use a standard *SphereComponent*. This component prevents the fingertip from passing through buttons without triggering them. The standard *SphereComponent* only supports continuous **collision** detection (enabled with "Use CCD"), not continuous **overlap** detection. ## Tick Until The *Tick Until* blueprint node runs once per tick from the *Completed* pin until it hits *Break*. ================================================ FILE: Plugins/OculusHandTools/README_ThrowAssist.md ================================================ # The OculusThrowAssist Module This module implements the throwing system. It includes *ThrowingComponent*, an ActorComponent that tracks a hand and estimates throwing velocities. To use *ThrowingComponent* in your Character or Hand, add one instance per hand to your Actor. Call *Initialize* with the component controlling the hand’s transform (usually a *MotionControllerComponent*). Call *Update* every Tick to track the transform. Finally, call *GetThrowVector* to estimate the velocity of an object thrown from the tracked hand. ================================================ FILE: Plugins/OculusHandTools/Source/HandInput/CameraHandInput.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "CameraHandInput.h" #include "OculusXRInputFunctionLibrary.h" #include "Components/PoseableMeshComponent.h" #include "HandPose.h" #include "IXRTrackingSystem.h" #include "OculusXRHandComponent.h" #include "QuatUtil.h" #define ConvertBoneToFinger UOculusXRInputFunctionLibrary::ConvertBoneToFinger #define GetFingerTrackingConfidence UOculusXRInputFunctionLibrary::GetFingerTrackingConfidence namespace { bool IsOpenXRSystem() { const FName SystemName(TEXT("OpenXR")); return GEngine->XRSystem.IsValid() && (GEngine->XRSystem->GetSystemName() == SystemName); } } UCameraHandInput::UCameraHandInput(FObjectInitializer const& ObjectInitializer) : Super(ObjectInitializer) { PrimaryComponentTick.bCanEverTick = true; PrimaryComponentTick.TickGroup = TG_PostPhysics; bIsActive = false; for (auto& Quat : BoneRotations) { Quat = FQuat::Identity; } for (auto& Quat : BoneVelocities) { Quat = FQuat::Identity; } for (auto& Time : BoneLastFrozenTimes) { Time = -99999; } } void UCameraHandInput::BeginPlay() { Super::BeginPlay(); } void UCameraHandInput::SetHand(EControllerHand InHand) { Hand = InHand == EControllerHand::Left ? EOculusXRHandType::HandLeft : EOculusXRHandType::HandRight; } void UCameraHandInput::SetUpBoneMap(UPoseableMeshComponent* HandMesh, TArray& BoneMap) { if (HandMesh == nullptr) return; auto SkinnedAsset = HandMesh->GetSkinnedAsset(); if (SkinnedAsset == nullptr) return; auto& RefSkeleton = SkinnedAsset->GetRefSkeleton(); // set the bone ids for fast lookup for (auto& BoneMapping : BoneMap) { auto BoneName = BoneMapping.BoneName; auto const BoneIndex = RefSkeleton.FindBoneIndex(BoneName); BoneMapping.BoneId = BoneIndex; if (BoneIndex < 0) { UE_LOG(LogHandInput, Warning, TEXT("Hand Tracking: Unable to find bone named '%s' in hand skeleton."), *BoneMapping.BoneName.ToString()); } else { BoneMapping.ReferenceTransform = RefSkeleton.GetRefBonePose()[BoneIndex]; } } } void UCameraHandInput::SetPoseableMeshComponent(UPoseableMeshComponent* PoseableMeshComponent) { HandMesh = PoseableMeshComponent; UpdateMeshVisibility(); if (ensureMsgf(HandMesh, TEXT("SetPoseableMeshComponent failed")) && ensureMsgf(HandMesh->GetSkinnedAsset(), TEXT("SetPoseableMeshComponent failed"))) { SetUpBoneMap(HandMesh, BoneMap); GripBoneId = HandMesh->GetSkinnedAsset()->GetRefSkeleton().FindBoneIndex(GripBoneName); OnInitializeMesh.Broadcast(this); } } void UCameraHandInput::SetupInput(UInputComponent* Input) { if (Hand == EOculusXRHandType::HandLeft) { Input->BindAxisKey("OculusHand_Left_IndexPinchStrength", this, &UCameraHandInput::IndexFingerPinchUpdate); Input->BindAxisKey("OculusHand_Left_MiddlePinchStrength", this, &UCameraHandInput::MiddleFingerPinchUpdate); Input->BindAxisKey("OculusHand_Left_RingPinchStrength", this, &UCameraHandInput::RingFingerPinchUpdate); Input->BindAxisKey("OculusHand_Left_PinkPinchStrength", this, &UCameraHandInput::PinkyFingerPinchUpdate); } else { Input->BindAxisKey("OculusHand_Right_IndexPinchStrength", this, &UCameraHandInput::IndexFingerPinchUpdate); Input->BindAxisKey("OculusHand_Right_MiddlePinchStrength", this, &UCameraHandInput::MiddleFingerPinchUpdate); Input->BindAxisKey("OculusHand_Right_RingPinchStrength", this, &UCameraHandInput::RingFingerPinchUpdate); Input->BindAxisKey("OculusHand_Right_PinkPinchStrength", this, &UCameraHandInput::PinkyFingerPinchUpdate); } bInputIsInitialized = true; } bool UCameraHandInput::IsActive() { return UOculusXRInputFunctionLibrary::IsHandTrackingEnabled(); } void UCameraHandInput::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); // there must be a better way to do this that isn't polling if (!bInputIsInitialized) { if (auto const InputComponent = GetOwner()->InputComponent) { SetupInput(InputComponent); } } auto const bIsActiveThisFrame = IsActive(); if (bIsActive != bIsActiveThisFrame) { bIsActive = bIsActiveThisFrame; UpdateMeshVisibility(); } if (!bIsActiveThisFrame) { return; } UpdateTracking(); UpdateSkeleton(); if (bAlwaysUpdateGrab || IsTracked()) { UpdateGrabInput(); } UpdatePointingInput(); if (HandMesh) { for (auto&& BoneMapping : BoneMap) { BoneCache[BoneMapping.MappedBone] = HandMesh->GetSocketTransform(BoneMapping.BoneName); } } /* float Size = GetGrippingAxis() * 20.0f; FColor Color = IsPinching() ? FColor::Green : FColor::Red; DrawDebugSphere(GetWorld(), HandComponent->GetComponentLocation(), Size, 32, Color); */ // GEngine->AddOnScreenDebugMessage(-1, 0, FColor::Yellow, FString::Printf(TEXT("%i %f"), bInputIsInitialized ? 1 : 0, GetHighestPinchValue())); } void UCameraHandInput::UpdateTracking() { auto const IsTrackedThisFrame = IsTracked(); if (IsTrackedThisFrame && !WasTrackedLastFrame) { TimeWhenTrackingLastGained = GetWorld()->GetTimeSeconds(); } WasTrackedLastFrame = IsTrackedThisFrame; } bool UCameraHandInput::IsTracked() const { return UOculusXRInputFunctionLibrary::GetTrackingConfidence(Hand) == EOculusXRTrackingConfidence::High; } void UCameraHandInput::FilterBoneRotation(EOculusXRBone Bone, FQuat LastRotation, FQuat& Rotation) { auto const Now = GetWorld()->GetTimeSeconds(); auto& LastFrozenTime = BoneLastFrozenTimes[Bone]; auto& LastVelocity = BoneVelocities[Bone]; auto const Finger = ConvertBoneToFinger(Bone); auto const ActualAngularDistance = LastRotation.AngularDistance(Rotation); if (MaxBoneSmoothingAngularDistance > ActualAngularDistance) { auto const Alpha = FMath::Max(FMath::GetRangePct(MinBoneAngularDistance, MaxBoneSmoothingAngularDistance, ActualAngularDistance), 0.0f); Rotation = FQuat::Slerp(LastRotation, Rotation, Alpha); LastFrozenTime = Now; } else { auto const Alpha = FMath::Clamp((Now - LastFrozenTime) / BoneUnfreezeTime, 0.0f, 1.0f); Rotation = FQuat::Slerp(LastRotation, Rotation, Alpha); } if (bAlwaysClampBoneSpeed || GetFingerTrackingConfidence(Hand, Finger) != EOculusXRTrackingConfidence::High) { auto const DeltaSeconds = GetWorld()->GetDeltaSeconds(); auto const AngularDistance = LastRotation.AngularDistance(Rotation); auto const MaxAngularDistance = MaxBoneAngularSpeed * DeltaSeconds; if (MaxAngularDistance < AngularDistance) { Rotation = Scale(LastVelocity, DeltaSeconds) * LastRotation; LastVelocity = Scale(LastVelocity, BoneVelocityDamping); } else { LastVelocity = Scale(Rotation * LastRotation.Inverse(), 1.0f / DeltaSeconds); } } } void UCameraHandInput::SetBoneRotation(UPoseableMeshComponent* HandMesh, FHandBoneMapping BoneMapping, FQuat BoneRotation, bool IsLeft) { if (BoneMapping.BoneId == -1 || HandMesh == nullptr || HandMesh->BoneSpaceTransforms.Num() <= BoneMapping.BoneId) { return; } if (IsOpenXRSystem() && BoneMapping.BoneId == 0) { BoneMapping.RotationOffset = IsLeft ? LeftHandRootFixupRotationOpenXR : RightHandRootFixupRotationOpenXR; } BoneRotation = BoneMapping.RotationOffset * BoneRotation; BoneRotation.Normalize(); HandMesh->BoneSpaceTransforms[BoneMapping.BoneId] = FTransform( BoneRotation, BoneMapping.ReferenceTransform.GetTranslation(), BoneMapping.ReferenceTransform.GetScale3D()); if (BoneMapping.MappedBone == EOculusXRBone::Wrist_Root) { HandMesh->BoneSpaceTransforms[BoneMapping.BoneId].SetLocation(FVector::ZeroVector); } } void UCameraHandInput::UpdateSkeleton() { if (!HandMesh) { return; } auto const bHasCustomGestureThisFrame = bHasCustomGesture; if (bHasCustomGesture) { // custom gestures have to be applied every frame, so reset the flag here bHasCustomGesture = false; bHadCustomGestureLastFrame = true; } else if (bHadCustomGestureLastFrame) { // reset the grip bone auto&& Pose = HandMesh->GetSkinnedAsset()->GetRefSkeleton().GetRefBonePose(); if (ensureMsgf(Pose.IsValidIndex(GripBoneId), TEXT("GripBoneId is %i"), GripBoneId)) { HandMesh->BoneSpaceTransforms[GripBoneId] = Pose[GripBoneId]; } bHadCustomGestureLastFrame = false; } if (bHasCustomGestureThisFrame && DigitsMaskedFromCustomGesture == 0) { // get the bone rotations anyway, since we need them for gesture detection (eg. dropping) for (auto Index = 0; Index != static_cast(EOculusXRBone::Bone_Max); Index += 1) { auto const Bone = static_cast(Index); auto const Rotation = UOculusXRInputFunctionLibrary::GetBoneRotation(Hand, Bone); RawLocalSpaceRotations[Bone] = Rotation; } return; } // Update finger rotations for (auto Index = 0; Index != static_cast(EOculusXRBone::Bone_Max); Index += 1) { auto const Bone = static_cast(Index); auto& LastRotation = BoneRotations[Bone]; auto Rotation = UOculusXRInputFunctionLibrary::GetBoneRotation(Hand, Bone); RawLocalSpaceRotations[Bone] = Rotation; if (bBoneRotationFilteringEnabled) { FilterBoneRotation(Bone, LastRotation, Rotation); } LastRotation = Rotation; } for (auto BoneMapping : BoneMap) { if (BoneMapping.BoneId >= 0) { // skip updating if we're in a custom gesture and the bone's digit isn't masked out if (bHasCustomGestureThisFrame) { if (BoneMapping.Digit == EHandDigit::None) { continue; } if ((static_cast(BoneMapping.Digit) & DigitsMaskedFromCustomGesture) != static_cast(BoneMapping.Digit)) { continue; } } auto const BoneRotation = BoneRotations[BoneMapping.MappedBone]; SetBoneRotation(HandMesh, BoneMapping, BoneRotation, GetHand() == EOculusXRHandType::HandLeft); } } HandMesh->MarkRefreshTransformDirty(); if (bDynamicScalingEnabled) { auto const Scale = UOculusXRInputFunctionLibrary::GetHandScale(Hand); HandMesh->SetRelativeScale3D(FVector(Scale)); } } void UCameraHandInput::UpdateGrabInput() { // Hand pose grab input auto const GrabAxisValue = GetGrippingAxis(); if (bIsInGrabPose) { if (GrabAxisValue < ReleaseThresholdForGrip) { bIsInGrabPose = false; } } else if (GrabAxisValue > GrabThresholdForGrip) { bIsInGrabPose = true; } // pinch axis input auto const HighestPinchValueThisFrame = GetHighestPinchValue(); if (bIsPinching) { if (HighestPinchValueLastFrame > ReleaseThreshold && HighestPinchValueThisFrame <= ReleaseThreshold) { bIsPinching = false; } } else { if (HighestPinchValueLastFrame < GrabThreshold && HighestPinchValueThisFrame >= GrabThreshold) { bIsPinching = true; } } HighestPinchValueLastFrame = HighestPinchValueThisFrame; } void UCameraHandInput::UpdatePointingInput() { bIsPointing = GetPointingAxis() > 0.5f; } void UCameraHandInput::IndexFingerPinchUpdate(float Value) { FingerPinchUpdate(0, Value); } void UCameraHandInput::MiddleFingerPinchUpdate(float Value) { FingerPinchUpdate(1, Value); } void UCameraHandInput::RingFingerPinchUpdate(float Value) { FingerPinchUpdate(2, Value); } void UCameraHandInput::PinkyFingerPinchUpdate(float Value) { FingerPinchUpdate(3, Value); } void UCameraHandInput::FingerPinchUpdate(int FingerIndex, float Value) { // don't update if the hand, finger, or thumb have low confidence tracking if (!IsTracked()) { return; } if (GetFingerTrackingConfidence(Hand, EOculusXRFinger::Thumb) == EOculusXRTrackingConfidence::Low) { return; } auto const OculusFinger = static_cast(FingerIndex + 1); if (GetFingerTrackingConfidence(Hand, OculusFinger) == EOculusXRTrackingConfidence::Low) { return; } FingerPinchValues[FingerIndex] = Value; } float UCameraHandInput::GetHighestPinchValue() { // only look at the index and middle fingers return FMath::Max(FingerPinchValues[0], FingerPinchValues[1]); } FTransform UCameraHandInput::GetGripBoneTransform(EBoneSpaces::Type BoneSpace) { auto const TransformSpace = BoneSpace == EBoneSpaces::ComponentSpace ? RTS_Component : RTS_World; if (HandMesh) { return HandMesh->GetSocketTransform(GripBoneName, TransformSpace); } return FTransform::Identity; } EOculusXRHandType UCameraHandInput::GetHand() const { return Hand; } UPoseableMeshComponent* UCameraHandInput::GetMesh() const { return HandMesh; } FName UCameraHandInput::GetGripBoneName() const { return GripBoneName; } void UCameraHandInput::ForceMeshHidden(bool Hidden) { if (Hidden != bForceMeshHidden) { bForceMeshHidden = Hidden; UpdateMeshVisibility(); } } void UCameraHandInput::UpdateMeshVisibility() const { auto const bVisible = bIsActive && !bForceMeshHidden; if (HandMesh) { HandMesh->SetHiddenInGame(!bVisible); } } // these are all a mess and we really need a generic (data-driven) system to handle this float UCameraHandInput::GetPointingAxis() { auto const SummedJoints = RawLocalSpaceRotations[EOculusXRBone::Index_1].GetNormalized().GetAngle() + RawLocalSpaceRotations[EOculusXRBone::Index_2].GetNormalized().GetAngle() + RawLocalSpaceRotations[EOculusXRBone::Index_3].GetNormalized().GetAngle(); return FMath::GetMappedRangeValueClamped(PointingAxisJointRotationRange, FVector2D(0.f, 1.f), SummedJoints); } float UCameraHandInput::GetGrippingAxis() { auto const SummedJoints = RawLocalSpaceRotations[EOculusXRBone::Middle_1].GetNormalized().GetAngle() + RawLocalSpaceRotations[EOculusXRBone::Middle_2].GetNormalized().GetAngle() + RawLocalSpaceRotations[EOculusXRBone::Middle_3].GetNormalized().GetAngle() + RawLocalSpaceRotations[EOculusXRBone::Ring_1].GetNormalized().GetAngle() + RawLocalSpaceRotations[EOculusXRBone::Ring_2].GetNormalized().GetAngle() + RawLocalSpaceRotations[EOculusXRBone::Ring_3].GetNormalized().GetAngle() + RawLocalSpaceRotations[EOculusXRBone::Pinky_1].GetNormalized().GetAngle() + RawLocalSpaceRotations[EOculusXRBone::Pinky_2].GetNormalized().GetAngle() + RawLocalSpaceRotations[EOculusXRBone::Pinky_3].GetNormalized().GetAngle(); return FMath::GetMappedRangeValueClamped(GrippingAxisJointRotationRange, FVector2D(0.f, 1.f), SummedJoints); } float UCameraHandInput::GetThumbUpAxis() { auto const SummedJoints = RawLocalSpaceRotations[EOculusXRBone::Thumb_0].GetNormalized().GetAngle() + RawLocalSpaceRotations[EOculusXRBone::Thumb_1].GetNormalized().GetAngle() + RawLocalSpaceRotations[EOculusXRBone::Thumb_2].GetNormalized().GetAngle() + RawLocalSpaceRotations[EOculusXRBone::Thumb_3].GetNormalized().GetAngle(); return FMath::GetMappedRangeValueClamped(ThumbUpAxisJointRotationRange, FVector2D(0.f, 1.f), SummedJoints); } bool UCameraHandInput::ApplyPoseToMesh( FString PoseString, UPoseableMeshComponent* HandMesh, TArray const& BoneMappings, bool IsLeft) { auto BoneToRecognizedBone = [](EOculusXRBone Bone) { switch (Bone) { case EOculusXRBone::Wrist_Root: return Wrist; case EOculusXRBone::Thumb_0: return Thumb_0; case EOculusXRBone::Thumb_1: return Thumb_1; case EOculusXRBone::Thumb_2: return Thumb_2; case EOculusXRBone::Thumb_3: return Thumb_3; case EOculusXRBone::Index_1: return Index_1; case EOculusXRBone::Index_2: return Index_2; case EOculusXRBone::Index_3: return Index_3; case EOculusXRBone::Middle_1: return Middle_1; case EOculusXRBone::Middle_2: return Middle_2; case EOculusXRBone::Middle_3: return Middle_3; case EOculusXRBone::Ring_1: return Ring_1; case EOculusXRBone::Ring_2: return Ring_2; case EOculusXRBone::Ring_3: return Ring_3; case EOculusXRBone::Pinky_0: return Pinky_0; case EOculusXRBone::Pinky_1: return Pinky_1; case EOculusXRBone::Pinky_2: return Pinky_2; case EOculusXRBone::Pinky_3: return Pinky_3; default: return static_cast(-1); } }; FHandPose Pose; Pose.CustomEncodedPose = PoseString; if (HandMesh == nullptr || Pose.Decode() == false) { return false; } for (auto Mapping : BoneMappings) { auto const Bone = BoneToRecognizedBone(Mapping.MappedBone); if (Bone != static_cast(-1)) { auto Rotator = Bone == Wrist ? FRotator::ZeroRotator : Pose.GetRotator(Bone); SetBoneRotation(HandMesh, Mapping, Rotator.Quaternion(), IsLeft); } } HandMesh->RefreshBoneTransforms(); return true; } bool UCameraHandInput::SetPose(FString PoseString) { if (ApplyPoseToMesh(PoseString, HandMesh, BoneMap, GetHand() == EOculusXRHandType::HandLeft)) { bHasCustomGesture = true; return true; } return false; } #undef ConvertBoneToFinger #undef GetFingerTrackingConfidence #ifdef EOculusXRFinger #undef EOculusXRFinger #endif ================================================ FILE: Plugins/OculusHandTools/Source/HandInput/CameraHandInput.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "HandInput.h" #include "OculusXRInputFunctionLibrary.h" #include "EnumMap.h" #include "CameraHandInput.generated.h" class UTransformBufferComponent; namespace OculusInput { enum class EOculusXRHandButton; } class UPoseableMeshComponent; class UThrowingComponent; class UHandComponentBase; UENUM(BlueprintType, meta = (Bitflags, UseEnumValuesAsMaskValuesInEditor = "true")) enum class EHandDigit : uint8 { None = 0, Thumb = 1 << 0, Index = 1 << 1, Middle = 1 << 2, Ring = 1 << 3, Pinky = 1 << 4, Count = 6 }; USTRUCT(BlueprintType) struct HANDINPUT_API FHandBoneMapping { GENERATED_BODY() UPROPERTY(BlueprintReadOnly, EditAnywhere) FName BoneName; UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Transient) int BoneId{}; UPROPERTY(BlueprintReadOnly, EditAnywhere) EOculusXRBone MappedBone{}; UPROPERTY(BlueprintReadOnly, EditAnywhere) FQuat RotationOffset = FQuat::Identity; UPROPERTY(BlueprintReadOnly, EditAnywhere) EHandDigit Digit{}; UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Transient) FTransform ReferenceTransform; }; UCLASS(meta = (BlueprintSpawnableComponent)) class HANDINPUT_API UCameraHandInput : public UActorComponent, public IHandInput { GENERATED_BODY() public: UCameraHandInput(FObjectInitializer const& ObjectInitializer); virtual void BeginPlay() override; UFUNCTION(BlueprintCallable) void SetPoseableMeshComponent(UPoseableMeshComponent* PoseableMeshComponent); void SetupInput(UInputComponent* Input); void ForceMeshHidden(bool Hidden); UFUNCTION(BlueprintCallable) static bool ApplyPoseToMesh( FString PoseString, UPoseableMeshComponent* HandMesh, TArray const& BoneMappings, bool IsLeft); UFUNCTION(BlueprintCallable) static void SetUpBoneMap(UPoseableMeshComponent* HandMesh, UPARAM(ref) TArray& BoneMap); // IHandInput UFUNCTION(BlueprintCallable) virtual void SetHand(EControllerHand InHand) override; virtual bool IsActive() override; virtual FTransform GetGripBoneTransform(EBoneSpaces::Type BoneSpace) override; virtual bool IsPointing() override { return bIsPointing; } virtual EHandTrackingMode GetTrackingMode() override { return EHandTrackingMode::Camera; } // ~IHandInput UFUNCTION(BlueprintPure) EOculusXRHandType GetHand() const; UFUNCTION(BlueprintPure) UPoseableMeshComponent* GetMesh() const; UFUNCTION(BlueprintPure) FName GetGripBoneName() const; UPROPERTY(EditAnywhere, Category = "Hand Input") FName GripBoneName; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Hand Input") bool bDynamicScalingEnabled = true; UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Hand Input") float GrabThreshold = 0.8f; UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Hand Input") float ReleaseThreshold = 0.3f; UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Hand Input") float GrabThresholdForGrip = 0.6f; UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Hand Input") float ReleaseThresholdForGrip = 0.3f; UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Hand Input") float ThrowGestureReleaseFactor = 1.0f; // How long to delay before releasing the grip when a previously gripped hand loses tracking and is // regained in a non-gripped pose UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Hand Input") float GripReleaseDelayOnTrackingResumed = .5f; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Hand Input") bool bDropDelayEnabled = true; UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Hand Input") TArray BoneMap; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Hand Input") bool bAlwaysUpdateGrab = false; UPROPERTY(BlueprintReadWrite, Category = "Hand Input", Transient) UPoseableMeshComponent* HandMesh = nullptr; // grip release delay with good tracking UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Hand Input") float GlobalDropDelay = 0.05f; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Hand Input") bool bGlobalDropDelayEnabled = true; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Hand Input") float GlobalDropDelayReductionFactorWhenThrowing = 2.f; /// minimum distance for a bone to not be frozen UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Bone Jitter Mitigation") float MinBoneAngularDistance = 0.01f; /// maximum distance for a bone to be smoothed UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Bone Jitter Mitigation") float MaxBoneSmoothingAngularDistance = 0.25f; /// amount of time to smooth back from frozen to unfrozen UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Bone Jitter Mitigation") float BoneUnfreezeTime = 0.1f; /// max speed for a bone to extrapolate UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Bone Data Filter") float MaxBoneAngularSpeed = 0.5f; /// when enabled, bone speed will be clamped even if data is high quality UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Bone Data Filter") bool bAlwaysClampBoneSpeed = false; /// per-frame damping of extrapolated bone speed UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Bone Data Filter") float BoneVelocityDamping = 0.95f; /// when enabled, bone filtering will be active UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Bone Data Filter") bool bBoneRotationFilteringEnabled = true; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Hand Input") FVector2D PointingAxisJointRotationRange = FVector2D(14.f, 18.f); UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Hand Input") FVector2D GrippingAxisJointRotationRange = FVector2D(55.f, 44.f); UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Hand Input") FVector2D ThumbUpAxisJointRotationRange = FVector2D(21.8f, 22.7f); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInitializeMesh, UCameraHandInput *, CameraHandInput); UPROPERTY(BlueprintAssignable, Category = "Hand Input") FOnInitializeMesh OnInitializeMesh; UFUNCTION(BlueprintPure) bool IsPinching() const { return bIsPinching; } UFUNCTION(BlueprintPure) bool IsInGrabPose() const { return bIsInGrabPose; } UFUNCTION(BlueprintPure) FTransform GetBoneTransformWorld(EOculusXRBone Bone) { return BoneCache[Bone]; } UFUNCTION(BlueprintPure) bool IsTracked() const; UFUNCTION(BlueprintPure) float GetPointingAxis(); UFUNCTION(BlueprintPure) float GetGrippingAxis(); UFUNCTION(BlueprintPure) float GetThumbUpAxis(); UFUNCTION(BlueprintCallable) bool SetPose(FString PoseString); protected: virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; private: EOculusXRHandType Hand = EOculusXRHandType::None; bool bIsActive = false; void UpdateTracking(); bool WasTrackedLastFrame = false; float TimeWhenTrackingLastGained = -1; void FilterBoneRotation(EOculusXRBone Bone, FQuat LastRotation, FQuat& Rotation); static void SetBoneRotation(UPoseableMeshComponent* HandMesh, FHandBoneMapping BoneMapping, FQuat BoneRotation, bool IsLeft); void UpdateSkeleton(); bool bInputIsInitialized = false; void UpdateGrabInput(); float GetHighestPinchValue(); void IndexFingerPinchUpdate(float Value); void MiddleFingerPinchUpdate(float Value); void RingFingerPinchUpdate(float Value); void PinkyFingerPinchUpdate(float Value); void FingerPinchUpdate(int FingerIndex, float Value); float FingerPinchValues[4] = {0}; bool bIsPinching = false; float HighestPinchValueLastFrame = 0; bool bIsInGrabPose = false; void UpdatePointingInput(); bool bIsPointing = false; int GripBoneId = -1; bool bHasCustomGesture = false; bool bHadCustomGestureLastFrame = false; uint8 DigitsMaskedFromCustomGesture = 0; bool bForceMeshHidden = false; void UpdateMeshVisibility() const; // cache bone transforms from hand tracking for use by gameplay code TEnumMap BoneCache; TEnumMap RawLocalSpaceRotations; // cache bone rotations from hand tracking for smoothing TEnumMap BoneRotations; TEnumMap BoneVelocities; TEnumMap BoneLastFrozenTimes; }; ================================================ FILE: Plugins/OculusHandTools/Source/HandInput/EnumMap.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "Containers/StaticArray.h" template class TEnumMap : public TStaticArray(EEnum::Invalid)> { using Super = TStaticArray(EEnum::Invalid)>; public: InElementType& operator[](EEnum Index) { return (*static_cast(this))[static_cast(Index)]; } InElementType const& operator[](EEnum Index) const { return (*static_cast(this))[static_cast(Index)]; } }; ================================================ FILE: Plugins/OculusHandTools/Source/HandInput/HandInput.Build.cs ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. using UnrealBuildTool; public class HandInput : ModuleRules { public HandInput(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; //CppStandard = CppStandardVersion.Latest; PublicDependencyModuleNames.AddRange( new string[] { "Core", "CoreUObject", "Engine", "InputCore", "OculusXRInput", "HeadMountedDisplay" } ); PrivateDependencyModuleNames.AddRange( new string[] { "OculusHandPoseRecognition", "OculusUtils" } ); PublicIncludePaths.AddRange(new string[] { // TODO(els): Relative to UE4\Source... not sure why this fixes the build since we do depend on Core. // "Runtime/Core/Public/Containers", // "Runtime/CoreUObject/Public/UObject", }); PrivateIncludePaths.AddRange(new string[] { }); IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_6; } } ================================================ FILE: Plugins/OculusHandTools/Source/HandInput/HandInput.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "HandInput.h" ================================================ FILE: Plugins/OculusHandTools/Source/HandInput/HandInput.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "HandInput.generated.h" UENUM(BlueprintType) enum class EHandTrackingMode : uint8 { Controller, Camera, Unknown }; UINTERFACE() class HANDINPUT_API UHandInput : public UInterface { GENERATED_BODY() }; class HANDINPUT_API IHandInput { GENERATED_BODY() public: virtual void SetHand(EControllerHand Hand) = 0; virtual bool IsActive() = 0; virtual FTransform GetGripBoneTransform(EBoneSpaces::Type BoneSpace) = 0; virtual bool IsPointing() = 0; virtual EHandTrackingMode GetTrackingMode() = 0; }; DECLARE_LOG_CATEGORY_EXTERN(LogHandInput, Log, All); ================================================ FILE: Plugins/OculusHandTools/Source/HandInput/HandInputModule.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "HandInputModule.h" #include "HandInput.h" #define LOCTEXT_NAMESPACE "FHandInputModule" #include "OculusDeveloperTelemetry.h" OCULUS_TELEMETRY_LOAD_MODULE("Unreal-HandInput"); bool FHandInputModule::IsGameModule() const { return true; } #undef LOCTEXT_NAMESPACE IMPLEMENT_MODULE(FHandInputModule, HandInput); DEFINE_LOG_CATEGORY(LogHandInput); ================================================ FILE: Plugins/OculusHandTools/Source/HandInput/HandInputModule.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "Modules/ModuleInterface.h" class HANDINPUT_API FHandInputModule : public IModuleInterface { public: virtual bool IsGameModule() const override; }; ================================================ FILE: Plugins/OculusHandTools/Source/HandInput/QuatUtil.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "Math/Quat.h" static FORCEINLINE FQuat Scale(FQuat Rotation, float S) { return FQuat::Slerp(FQuat::Identity, Rotation, S); } ================================================ FILE: Plugins/OculusHandTools/Source/HandTrackingFilter/HandTrackingFilter.Build.cs ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. using UnrealBuildTool; public class HandTrackingFilter : ModuleRules { public HandTrackingFilter(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; //CppStandard = CppStandardVersion.Latest; PublicDependencyModuleNames.AddRange( new string[] { "Core", "CoreUObject", "Engine", "InputCore", "OculusXRInput", "HeadMountedDisplay" } ); PrivateDependencyModuleNames.AddRange( new string[] { "OculusUtils", "XRBase", } ); PublicIncludePaths.AddRange(new string[] { // TODO(els): Relative to UE4\Source... not sure why this fixes the build since we do depend on Core. // "Runtime/Core/Public/Containers", // "Runtime/CoreUObject/Public/UObject", }); PrivateIncludePaths.AddRange(new string[] { }); IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_6; } } ================================================ FILE: Plugins/OculusHandTools/Source/HandTrackingFilter/HandTrackingFilter.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "HandTrackingFilter.h" #define LOCTEXT_NAMESPACE "FHandTrackingFilterModule" #include "OculusDeveloperTelemetry.h" OCULUS_TELEMETRY_LOAD_MODULE("Unreal-HandTrackingFilter"); bool FHandTrackingFilterModule::IsGameModule() const { return true; } #undef LOCTEXT_NAMESPACE IMPLEMENT_MODULE(FHandTrackingFilterModule, HandTrackingFilter); ================================================ FILE: Plugins/OculusHandTools/Source/HandTrackingFilter/HandTrackingFilter.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "Modules/ModuleInterface.h" class HANDTRACKINGFILTER_API FHandTrackingFilterModule : public IModuleInterface { public: virtual bool IsGameModule() const override; }; ================================================ FILE: Plugins/OculusHandTools/Source/HandTrackingFilter/HandTrackingFilterComponent.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "HandTrackingFilterComponent.h" #include "MotionControllerComponent.h" #include "OculusXRInputFunctionLibrary.h" #include "Camera/CameraComponent.h" #include "QuatUtil.h" #include "XRMotionControllerBase.h" DECLARE_LOG_CATEGORY_EXTERN(LogHandTrackingFilter, Log, All); DEFINE_LOG_CATEGORY(LogHandTrackingFilter); // use real time because this can get hit multiple times per frame #define NOW FPlatformTime::Seconds() UHandTrackingFilterComponent::UHandTrackingFilterComponent() { bAutoActivate = true; } void UHandTrackingFilterComponent::BeginPlay() { Super::BeginPlay(); LastFrameData = FHandTrackingFilterData{NOW, GetComponentTransform()}; if (auto Controller = Cast(GetAttachParent())) { UOculusXRInputFunctionLibrary::HandMovementFilter.AddWeakLambda(this, [this, ThisHand = Controller->GetTrackingSource()] ( EControllerHand Hand, FVector* Location, FRotator* Orientation, bool* Success ) { if (Hand == ThisHand && UOculusXRInputFunctionLibrary::IsHandTrackingEnabled()) { if (IsInGameThread() && PreFilterComponent) { PreFilterComponent->SetActive(*Success); if (*Success) { PreFilterComponent->SetRelativeRotation(*Orientation); PreFilterComponent->SetRelativeLocation(*Location); } } DoFiltering(*Location, *Orientation, !*Success); *Success = true; } }); } } void UHandTrackingFilterComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) { UOculusXRInputFunctionLibrary::HandMovementFilter.RemoveAll(this); Super::EndPlay(EndPlayReason); } EHandTrackingDataQuality UHandTrackingFilterComponent::GetDataQualityOverride() const { if (bIgnoreConfidence) return EHandTrackingDataQuality::None; if (auto const Controller = Cast(GetAttachParent())) { auto Hand = EControllerHand::Left; FXRMotionControllerBase::GetHandEnumForSourceName(Controller->MotionSource, Hand); auto const DeviceHand = Hand == EControllerHand::Left ? EOculusXRHandType::HandLeft : EOculusXRHandType::HandRight; return Controller->IsTracked() ? UOculusXRInputFunctionLibrary::GetTrackingConfidence(DeviceHand) == EOculusXRTrackingConfidence::High ? EHandTrackingDataQuality::Good : EHandTrackingDataQuality::None : EHandTrackingDataQuality::Bad; } return EHandTrackingDataQuality::None; } FVector UHandTrackingFilterComponent::SmoothPosition(FVector StartPos, FVector TargetPos) { if (SmoothPositionFactor > 0.99f) { // Updating disabled return StartPos; } auto const Diff = TargetPos - StartPos; auto const Dist = Diff.Size(); if (Dist < MinSmoothPositionDistance) { // Not enough of a change to update UE_LOG(LogHandTrackingFilter, Verbose, TEXT("%s - SmoothPos - Not enough of a change to update"), *GetName()); LastGoodVelocity = FVector::ZeroVector; return StartPos; } if (Dist >= MaxSmoothPositionDistance) { // Clamp max distance from target UE_LOG(LogHandTrackingFilter, Verbose, TEXT("%s - SmoothPos - Clamp max distance from target"), *GetName()); auto const Dir = Diff / Dist; auto const MoveDist = Dist - MaxSmoothPositionDistance; return StartPos + Dir * MoveDist; } UE_LOG(LogHandTrackingFilter, Verbose, TEXT("%s - SmoothPos - Smooth"), *GetName()); auto const SmoothFactor = 1.0f - SmoothPositionFactor; return FMath::Lerp(StartPos, TargetPos, SmoothFactor); } void UHandTrackingFilterComponent::SetPreFilterComponent(USceneComponent* Component) { PreFilterComponent = Component; } FQuat UHandTrackingFilterComponent::SmoothRotation(FQuat StartRot, FQuat TargetRot) { if (SmoothRotationFactorMin > 0.99f) { // Updating disabled return StartRot; } auto const CosAngle = StartRot | TargetRot; if (CosAngle > SmoothRotationDotMax) { // Not enough of a change to update UE_LOG(LogHandTrackingFilter, Verbose, TEXT("%s - SmoothRotation - Not enough of a change to update"), *GetName()); return StartRot; } float SmoothFactor; if (CosAngle <= SmoothRotationDotMin) { UE_LOG(LogHandTrackingFilter, Verbose, TEXT("%s - SmoothRotation - CosAngle %f <= SmoothRotationDotMin %f"), *GetName(), CosAngle, SmoothRotationDotMin); SmoothFactor = 1.0f - SmoothRotationFactorMin; } else { UE_LOG(LogHandTrackingFilter, Verbose, TEXT("%s - SmoothRotation - CosAngle %f > SmoothRotationDotMin %f"), *GetName(), CosAngle, SmoothRotationDotMin); auto const Weight = (CosAngle - SmoothRotationDotMin) / (SmoothRotationDotMax - SmoothRotationDotMin); SmoothFactor = 1.0f - FMath::Lerp(SmoothRotationFactorMin, SmoothRotationFactorMax, Weight); } UE_LOG(LogHandTrackingFilter, Verbose, TEXT("%s - SmoothRotation - %f"), *GetName(), SmoothFactor); return FQuat::Slerp(StartRot, TargetRot, SmoothFactor); } bool UHandTrackingFilterComponent::DoFirstPassFilter( FHandTrackingFilterData const& LastData, FHandTrackingFilterData const& ThisFrameInitData, FTransform& NewTransform) { auto const LastLocation = LastData.Transform.GetLocation(); auto const NewLocation = ThisFrameInitData.Transform.GetLocation(); auto const DeltaLocation = NewLocation - LastLocation; auto const DistanceSquared = DeltaLocation.SizeSquared(); // if there hasn't been a tracking update, extrapolate if (DistanceSquared < MinTrackingDistance * MinTrackingDistance) { UE_LOG(LogHandTrackingFilter, Verbose, TEXT("%s - DoFirstPassFilter - if the location hasn't changed (%f), there hasn't been a tracking update"), *GetName(), DistanceSquared); NewTransform = LastSetTransform; return true; } auto const NewRotation = ThisFrameInitData.Transform.GetRotation(); auto const SmoothedPosition = SmoothPosition(LastSetTransform.GetLocation(), NewLocation); NewTransform.SetLocation(SmoothedPosition); auto const SmoothedRotation = SmoothRotation(LastSetTransform.GetRotation(), NewRotation); NewTransform.SetRotation(SmoothedRotation); return false; } FTransform UHandTrackingFilterComponent::DoFilteringImpl(FTransform HandTransform, bool bForceBadData) { auto CalculatedData = FHandTrackingFilterCalculatedData(); auto const LastData = LastFrameData; auto ThisFrameInitData = FHandTrackingFilterData{NOW, HandTransform}; auto const DeltaTime = ThisFrameInitData.Time - LastData.Time; auto MitigatedTransform = ThisFrameInitData.Transform; auto const EarlyOut = DoFirstPassFilter(LastData, ThisFrameInitData, MitigatedTransform); if (!bForceBadData && EarlyOut) { LastSetTransform = MitigatedTransform; return MitigatedTransform; } UE_LOG(LogHandTrackingFilter, Verbose, TEXT("%s - DoFilteringImpl - dt %lf"), *GetName(), DeltaTime); auto& Data = LastFrameData; Data = ThisFrameInitData; auto const DeltaLocation = MitigatedTransform.GetLocation() - LastSetTransform.GetLocation(); auto const Distance = DeltaLocation.Size(); CalculatedData.Distance = Distance; Data.Velocity = DeltaLocation / DeltaTime; CalculatedData.Velocity = Data.Velocity; auto const DeltaVelocity = Data.Velocity - LastData.Velocity; auto const Acceleration = CalculatedData.Acceleration = DeltaVelocity / DeltaTime; CalculatedData.AccelerationScalar = Acceleration.Size(); auto DeltaRotation = Data.Transform.GetRotation() * LastSetTransform.GetRotation().Inverse(); Data.AngularVelocity = Scale(DeltaRotation, 1 / DeltaTime); CalculatedData.AngularVelocityScalar = Data.AngularVelocity.GetAngle(); auto BadData = CalculatedData.AccelerationScalar > MaxAcceleration || Distance > MaxDistancePerFrame || Data.Velocity.SizeSquared() > MaxSpeed * MaxSpeed || CalculatedData.AngularVelocityScalar > MaxAngularVelocity; auto const QualityOverride = GetDataQualityOverride(); if (QualityOverride == EHandTrackingDataQuality::Good) BadData = false; else if (QualityOverride == EHandTrackingDataQuality::Bad) BadData = true; auto const Camera = GetOwner()->FindComponentByClass(); if (Camera != nullptr) { CalculatedData.CameraDistance = FVector::Dist(Data.Transform.GetLocation(), Camera->GetComponentLocation()); if ((QualityOverride != EHandTrackingDataQuality::Good || bCameraRadiusIgnoreConfidence) && CalculatedData.CameraDistance < IgnoreCameraLocationRadius) BadData = true; } else { CalculatedData.CameraDistance = -1; } if (bForceBadData) { UE_LOG(LogHandTrackingFilter, Verbose, TEXT("%s - DoFilteringImpl - Bad Data Forced"), *GetName()); BadData = true; } auto const OriginalDistance = Distance; auto const LastSetTransformActual = LastSetTransform; auto NewTransform = IntegrateFilterData(MitigatedTransform, Data, DeltaTime, BadData); if (FVector::Dist(LastSetTransformActual.GetLocation(), NewTransform.GetLocation()) > OriginalDistance) { UE_LOG(LogHandTrackingFilter, Verbose, TEXT("%s - DoFilteringImpl - over 5 oh no - %s %f"), *GetName(), BadData ? TEXT("bad data") : TEXT("good data"), OriginalDistance); } CalculatedData.QualityOverride = QualityOverride; CalculatedData.BadData = BadData; OnCalculatedData.Broadcast(CalculatedData); if (UE_LOG_ACTIVE(LogHandTrackingFilter, VeryVerbose)) { auto CalculatedDataText = FString(TEXT("")); FHandTrackingFilterCalculatedData::StaticStruct()->ExportText(CalculatedDataText, &CalculatedData, nullptr, this, PPF_ExportsNotFullyQualified | PPF_Copy | PPF_Delimited | PPF_IncludeTransient | PPF_ExternalEditor, nullptr); UE_LOG(LogHandTrackingFilter, VeryVerbose, TEXT("%s - OnCalculatedData - (%i,%i,%i) %s"), *GetName(), (int)NewTransform.GetLocation().X, (int)NewTransform.GetLocation().Y, (int)NewTransform.GetLocation().Z, *CalculatedDataText); } return NewTransform; } void UHandTrackingFilterComponent::DoFiltering(FVector& Location, FRotator& Orientation, bool bForceBadData) { if (IsActive() == false) return; if (bForceZeroTransform) { Location = FVector::ZeroVector; Orientation = FRotator::ZeroRotator; return; } auto ParentTransform = GetAttachParent()->GetAttachParent()->GetComponentTransform(); auto RelativeTransform = FTransform(Orientation, Location); auto WorldTransform = DoFilteringImpl(RelativeTransform * ParentTransform, bForceBadData); auto NewRelativeTransform = WorldTransform * ParentTransform.Inverse(); Location = NewRelativeTransform.GetLocation(); Orientation = NewRelativeTransform.Rotator(); } void UHandTrackingFilterComponent::ExtrapolateTransform(float DeltaTime, FVector& FakeLocation, FQuat& FakeRotation) { UE_LOG(LogHandTrackingFilter, Verbose, TEXT("%s - ExtrapolateTransform - LastGoodVelocity = %f"), *GetName(), LastGoodVelocity.Size()); FakeLocation = LastSetTransform.GetLocation() + LastGoodVelocity * DeltaTime; FakeRotation = Scale(LastGoodAngularVelocity, DeltaTime) * LastSetTransform.GetRotation(); } FTransform UHandTrackingFilterComponent::IntegrateFilterData( FTransform MitigatedTransform, FHandTrackingFilterData const& Data, float DeltaTime, bool BadData) { if (BadData) { LastBadDataTime = Data.Time; LastGoodVelocity *= VelocityDamping; // damp the velocity so it doesn't fly off LastGoodAngularVelocity = Scale(LastGoodAngularVelocity, VelocityDamping); UE_LOG(LogHandTrackingFilter, Verbose, TEXT("%s - IntegrateFilterData - Bad Data"), *GetName()); } else { LastGoodVelocity = FMath::Lerp(LastGoodVelocity, Data.Velocity.GetClampedToMaxSize(MaxFakeVelocity), GoodVelocityBlendRate); LastGoodAngularVelocity = Scale(Data.AngularVelocity, FMath::Max(Data.AngularVelocity.GetAngle() / MaxAngularVelocity, 1.0f)); UE_LOG(LogHandTrackingFilter, Verbose, TEXT("%s - IntegrateFilterData - Good Data"), *GetName()); } FVector FakeLocation; FQuat FakeRotation; ExtrapolateTransform(DeltaTime, FakeLocation, FakeRotation); if (BadData) { // when the data is bad it's possible for the MitigatedTransform to be invalid (containing NaNs) // so we set LastSetTransform directly instead of using the lerps LastSetTransform.SetLocation(FakeLocation); LastSetTransform.SetRotation(FakeRotation); } else { auto const FadePercent = FMath::Clamp((Data.Time - LastBadDataTime) / BadTransformFadeTime, 0.0, 1.0); LastSetTransform.SetLocation(FMath::LerpStable(FakeLocation, MitigatedTransform.GetLocation(), FadePercent)); LastSetTransform.SetRotation(FQuat::Slerp(FakeRotation, MitigatedTransform.GetRotation(), FadePercent)); } return LastSetTransform; } #undef NOW ================================================ FILE: Plugins/OculusHandTools/Source/HandTrackingFilter/HandTrackingFilterComponent.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" #include "Components/SceneComponent.h" #include "HandTrackingFilterComponent.generated.h" struct HANDTRACKINGFILTER_API FHandTrackingFilterData { double Time; FTransform Transform; FVector Velocity; FQuat AngularVelocity; }; UENUM(BlueprintType) enum class EHandTrackingDataQuality : uint8 { None, Good, Bad }; USTRUCT(BlueprintType) struct HANDTRACKINGFILTER_API FHandTrackingFilterCalculatedData { GENERATED_BODY() UPROPERTY(BlueprintReadOnly) FVector Acceleration = FVector::ZeroVector; UPROPERTY(BlueprintReadOnly) float AccelerationScalar{}; UPROPERTY(BlueprintReadOnly) float AngularVelocityScalar{}; UPROPERTY(BlueprintReadOnly) float CameraDistance{}; UPROPERTY(BlueprintReadOnly) float Distance{}; UPROPERTY(BlueprintReadOnly) FVector Velocity = FVector::ZeroVector; /// hand confidence information UPROPERTY(BlueprintReadOnly) EHandTrackingDataQuality QualityOverride{}; /// if the data should be ignored UPROPERTY(BlueprintReadOnly) bool BadData{}; }; UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) class HANDTRACKINGFILTER_API UHandTrackingFilterComponent : public USceneComponent { GENERATED_BODY() public: UHandTrackingFilterComponent(); protected: virtual void BeginPlay() override; virtual void EndPlay(EEndPlayReason::Type const EndPlayReason) override; FHandTrackingFilterData LastFrameData; double LastBadDataTime = -99999; FVector LastGoodVelocity = FVector::ZeroVector; FQuat LastGoodAngularVelocity = FQuat::Identity; FTransform LastSetTransform = FTransform::Identity; bool DoFirstPassFilter(FHandTrackingFilterData const& LastData, FHandTrackingFilterData const& ThisFrameInitData, FTransform& NewTransform); FTransform DoFilteringImpl(FTransform HandTransform, bool bForceBadData); FTransform IntegrateFilterData(FTransform MitigatedTransform, FHandTrackingFilterData const& Data, float DeltaTime, bool BadData); void DoFiltering(FVector& Location, FRotator& Orientation, bool bForceBadData); void ExtrapolateTransform(float DeltaTime, FVector& FakeLocation, FQuat& FakeRotation); EHandTrackingDataQuality GetDataQualityOverride() const; FQuat SmoothRotation(FQuat StartRot, FQuat TargetRot); FVector SmoothPosition(FVector StartPos, FVector TargetPos); double LastFrozenMovementTime = -99999; double LastFrozenRotationTime = -99999; UPROPERTY(Transient) USceneComponent* PreFilterComponent = nullptr; public: /** Percentage to de-jitter the position by */ UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Jitter Mitigation") float SmoothPositionFactor = 0.875f; /** Max distance that should be considered "no movement" (cm) */ UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Jitter Mitigation") float MinSmoothPositionDistance = 0.2f; /** Max distance that should be considered "jittery movement" (cm) */ UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Jitter Mitigation") float MaxSmoothPositionDistance = 1.0f; /** Minimum percentage to de-jitter the position by */ UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Jitter Mitigation") float SmoothRotationFactorMin = 0.0f; /** Maximum percentage to de-jitter the position by */ UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Jitter Mitigation") float SmoothRotationFactorMax = 0.75f; /** Max angle that should be considered "no rotation" (cosine units) */ UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Jitter Mitigation") float SmoothRotationDotMin = FMath::Cos(FMath::DegreesToRadians(3.0f)); /** Max angle that should be considered "jittery rotation" (cosine units) */ UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Jitter Mitigation") float SmoothRotationDotMax = FMath::Cos(FMath::DegreesToRadians(0.5f)); /** Acceleration limit to trigger the filter (cm/s^2)*/ UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Filter Triggers") float MaxAcceleration = 100000.0f; /** Speed limit to trigger the filter (cm/s) */ UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Filter Triggers") float MaxSpeed = 2000.0f; /** Distance per frame limit to trigger the filter (cm) */ UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Filter Triggers") float MaxDistancePerFrame = 9999999.0f; /** Angular velocity limit to trigger the filter (rad/s) */ UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Filter Triggers") float MaxAngularVelocity = 3.0f; /** Time it will take to interpolate from filtered data to good data (s) */ UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Filter Effects") float BadTransformFadeTime = 0.1f; /** Radius from the HMD to trigger the filter (cm) */ UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Filter Triggers") float IgnoreCameraLocationRadius = 10.0f; /** Radius from the HMD to trigger the filter (cm) */ UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Filter Triggers") bool bCameraRadiusIgnoreConfidence = true; /** Speed to clamp the extrapolated velocity (cm/s) */ UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Filter Effects") float MaxFakeVelocity = 0.8f; /** Per-frame multiplier on the extrapolated velocity (%/frame) */ UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (UIMin = 0, UIMax = 1), Category = "Filter Effects") float VelocityDamping = 0.96f; /** DEBUG - Disables usage of confidence to determine filtering */ UPROPERTY(BlueprintReadWrite, EditAnywhere) bool bIgnoreConfidence = false; /** DEBUG - Enable for the filter to always return a zero transform */ UPROPERTY(BlueprintReadWrite, EditAnywhere) bool bForceZeroTransform = false; // Minimum distance the hand must move to be recognized as a new "tick" of tracking UPROPERTY(BlueprintReadWrite, EditAnywhere) float MinTrackingDistance = 0.001f; // How quickly to integrate presumed good velocity data into the extrapolation velocity UPROPERTY(BlueprintReadWrite, EditAnywhere) float GoodVelocityBlendRate = 0.5f; DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCalculatedDataEvent, FHandTrackingFilterCalculatedData const &, Data); /** * @brief Called every tick with the data calculated by the filter. Used for debugging and tuning. */ UPROPERTY(BlueprintAssignable) FOnCalculatedDataEvent OnCalculatedData; /** * @return The pre-filter component */ UFUNCTION(BlueprintCallable) USceneComponent* GetPreFilterComponent() const { return PreFilterComponent; } /** * @param Component A scene component that should be updated with the pre-filtered transform data. * Used for accessing pre-filtered hand tracking data. */ UFUNCTION(BlueprintCallable) void SetPreFilterComponent(USceneComponent* Component); }; ================================================ FILE: Plugins/OculusHandTools/Source/HandTrackingFilter/QuatUtil.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "Math/Quat.h" FORCEINLINE FQuat HANDTRACKINGFILTER_API Scale(FQuat Rotation, float S) { return FQuat::Slerp(FQuat::Identity, Rotation, S); } ================================================ FILE: Plugins/OculusHandTools/Source/OculusHandPoseRecognition/OculusHandPoseRecognition.Build.cs ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. using UnrealBuildTool; public class OculusHandPoseRecognition : ModuleRules { public OculusHandPoseRecognition (ReadOnlyTargetRules Target) : base(Target) { PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; PublicIncludePaths.AddRange( new string[] { // ... add public include paths required here ... } ); PrivateIncludePaths.AddRange( new string[] { // ... add other private include paths required here ... } ); PublicDependencyModuleNames.AddRange( new string[] { "Core", // ... add other public dependencies that you statically link with here ... } ); PrivateDependencyModuleNames.AddRange( new string[] { "CoreUObject", "Engine", "Slate", "SlateCore", "OculusXRInput", "OculusUtils" } ); DynamicallyLoadedModuleNames.AddRange( new string[] { // ... add any modules that your module loads dynamically here ... } ); IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_6; } } ================================================ FILE: Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/FRecordHandPoseAction.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" #include "UObject/WeakObjectPtr.h" #include "Engine/LatentActionManager.h" #include "LatentActions.h" #include "HandPoseRecognizer.h" #include "OculusHandPoseRecognitionModule.h" /** Latent action for a blueprint node that can record the range of motion of a pose. */ class FRecordHandPoseAction : public FPendingLatentAction { public: FName ExecutionFunction; int32 OutputLink; FWeakObjectPtr CallbackTarget; UHandPoseRecognizer* Recognizer; const ERecordHandPoseEntryType* InExecs; ERecordHandPoseExitType* OutExecs; FRecordHandPoseAction(const FLatentActionInfo& LatentInfo, UHandPoseRecognizer* Recognizer, const ERecordHandPoseEntryType* InExecs, ERecordHandPoseExitType* OutExecs) : ExecutionFunction(LatentInfo.ExecutionFunction) , OutputLink(LatentInfo.Linkage) , CallbackTarget(LatentInfo.CallbackTarget) , Recognizer(Recognizer) , InExecs(InExecs) , OutExecs(OutExecs) { bIsRecording = false; HandPoseRangeIndex = 0; } virtual void UpdateOperation(FLatentResponse& Response) override { if (*InExecs == ERecordHandPoseEntryType::StartRecording) { if (!bIsRecording) { // Let's reset min and max poses. MinPose.UpdatePose(Recognizer->Side, Recognizer->GetComponentRotation()); MaxPose.UpdatePose(Recognizer->Side, Recognizer->GetComponentRotation()); bIsRecording = true; *OutExecs = ERecordHandPoseExitType::RecordingStarted; Response.TriggerLink(ExecutionFunction, OutputLink, CallbackTarget); } else { FHandPose NewPose; NewPose.UpdatePose(Recognizer->Side, Recognizer->GetComponentRotation()); MinPose.Min(NewPose); MaxPose.Max(NewPose); } } else // if (*InExecs == ERecordHandPoseEntryType::StopRecording) { if (bIsRecording) { LogHandPoseRecordedRange(); bIsRecording = false; } *OutExecs = ERecordHandPoseExitType::RecordingStopped; Response.FinishAndTriggerIf(true, ExecutionFunction, OutputLink, CallbackTarget); } } #if WITH_EDITOR // Returns a human readable description of the latent operation's current state virtual FString GetDescription() const override { return FString::Format( TEXT("Hand Pose Recorder is %srecording the %s"), { bIsRecording ? TEXT("") : TEXT("not "), *UEnum::GetValueAsString(Recognizer->Side) }); } #endif protected: void LogHandPoseRecordedRange() { UE_LOG(LogHandPoseRecognition, Error, TEXT("Hand Pose Range Recorded #%d"), HandPoseRangeIndex++); // Error at 0.5 (half full range) is the maximum error. auto TotalSquare0500Error = 0.0f; auto TotalSquare0250Error = 0.0f; auto TotalSquare0125Error = 0.0f; for (auto Bone = 0; Bone < NUM; ++Bone) { auto MinRot = MinPose.GetRotator((ERecognizedBone)Bone); auto MaxRot = MaxPose.GetRotator((ERecognizedBone)Bone); // Range is as large as 180 degrees. auto PitchRange = FMath::Abs(FMath::FindDeltaAngleDegrees(MaxRot.Pitch, MinRot.Pitch)); auto YawRange = FMath::Abs(FMath::FindDeltaAngleDegrees(MaxRot.Yaw, MinRot.Yaw)); auto RollRange = FMath::Abs(FMath::FindDeltaAngleDegrees(MaxRot.Roll, MinRot.Roll)); // Max error is at most 180 degrees (0.5 * range). TotalSquare0500Error += FMath::Square(0.500 * PitchRange) + FMath::Square(0.500 * YawRange) + FMath::Square(0.500 * RollRange); TotalSquare0250Error += FMath::Square(0.250 * PitchRange) + FMath::Square(0.250 * YawRange) + FMath::Square(0.250 * RollRange); TotalSquare0125Error += FMath::Square(0.125 * PitchRange) + FMath::Square(0.125 * YawRange) + FMath::Square(0.125 * RollRange); UE_LOG(LogHandPoseRecognition, Warning, TEXT("%8s pitch %6.2f [%+7.2f .. %+7.2f] yaw %6.2f [%+7.2f .. %+7.2f] roll %6.2f [%+7.2f .. %+7.2f]"), *UEnum::GetValueAsString((ERecognizedBone)Bone), PitchRange, MinRot.Pitch, MaxRot.Pitch, YawRange, MinRot.Yaw, MaxRot.Yaw, RollRange, MinRot.Roll, MaxRot.Roll); } UE_LOG(LogHandPoseRecognition, Warning, TEXT("Hand pose range total square error - full=%0.2f half=%0.2f quarter=%0.2f"), TotalSquare0500Error, TotalSquare0250Error, TotalSquare0125Error); } private: bool bIsRecording; FHandPose MinPose{}, MaxPose{}; int HandPoseRangeIndex; }; ================================================ FILE: Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/FWaitForHandGestureAction.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" #include "UObject/WeakObjectPtr.h" #include "Engine/LatentActionManager.h" #include "LatentActions.h" #include "HandGestureRecognizer.h" /** Latent action for blueprint node that waits for a gesture to be recognized. */ class FWaitForHandGestureAction : public FPendingLatentAction { public: FName ExecutionFunction; int32 OutputLink; FWeakObjectPtr CallbackTarget; TWeakObjectPtr HandGestureRecognizer; float TimeToWait; EGestureConsumptionBehavior Behavior; int* GestureIndex; FString* GestureName; FVector* GestureDirection; float* GestureOuterDuration; float* GestureInnerDuration; EWaitForHandGestureExitType* OutExecs; FWaitForHandGestureAction(const FLatentActionInfo& LatentInfo, UHandGestureRecognizer* HandGestureRecognizer, float TimeToWait, EGestureConsumptionBehavior Behavior, int* GestureIndex, FString* GestureName, FVector* GestureDirection, float* GestureOuterDuration, float* GestureInnerDuration, EWaitForHandGestureExitType* OutExecs) : ExecutionFunction(LatentInfo.ExecutionFunction) , OutputLink(LatentInfo.Linkage) , CallbackTarget(LatentInfo.CallbackTarget) , HandGestureRecognizer(HandGestureRecognizer) , TimeToWait(TimeToWait) , Behavior(Behavior) , GestureIndex(GestureIndex) , GestureName(GestureName) , GestureDirection(GestureDirection) , GestureOuterDuration(GestureOuterDuration) , GestureInnerDuration(GestureInnerDuration) , OutExecs(OutExecs) { } virtual void UpdateOperation(FLatentResponse& Response) override { // Time out? if (TimeToWait > 0.0f) { TimeToWait -= Response.ElapsedTime(); if (TimeToWait <= 0.0f) { // We have timed out while waiting. *GestureIndex = -1; GestureName->Empty(); *OutExecs = EWaitForHandGestureExitType::TimeOut; Response.FinishAndTriggerIf(true, ExecutionFunction, OutputLink, CallbackTarget); } } // Did we recognize a gesture? int Index; FString Name; FVector Direction; float OuterDuration, InnerDuration; if (HandGestureRecognizer->GetRecognizedHandGesture(Behavior, Index, Name, Direction, OuterDuration, InnerDuration)) { *GestureIndex = Index; *GestureName = Name; *GestureDirection = Direction; *GestureOuterDuration = OuterDuration; *GestureInnerDuration = InnerDuration; *OutExecs = EWaitForHandGestureExitType::GestureSeen; Response.FinishAndTriggerIf(true, ExecutionFunction, OutputLink, CallbackTarget); } } #if WITH_EDITOR // Returns a human readable description of the latent operation's current state virtual FString GetDescription() const override { FString Msg = TEXT("Waiting for Hand Gesture"); return Msg; } #endif }; ================================================ FILE: Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/FWaitForHandPoseAction.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" #include "UObject/WeakObjectPtr.h" #include "Engine/LatentActionManager.h" #include "LatentActions.h" #include "HandPoseRecognizer.h" /** Latent action for blueprint node that waits for a pose to be recognized. */ class FWaitForHandPoseAction : public FPendingLatentAction { public: FName ExecutionFunction; int32 OutputLink; FWeakObjectPtr CallbackTarget; TWeakObjectPtr HandPoseRecognizer; float PoseMinDuration; float TimeToWait; int* PoseIndex; FString* PoseName; EWaitForHandPoseExitType* OutExecs; FWaitForHandPoseAction(const FLatentActionInfo& LatentInfo, UHandPoseRecognizer* HandPoseRecognizer, float PoseMinDuration, float TimeToWait, int* PoseIndex, FString* PoseName, EWaitForHandPoseExitType* OutExecs) : ExecutionFunction(LatentInfo.ExecutionFunction) , OutputLink(LatentInfo.Linkage) , CallbackTarget(LatentInfo.CallbackTarget) , HandPoseRecognizer(HandPoseRecognizer) , PoseMinDuration(PoseMinDuration) , TimeToWait(TimeToWait) , PoseIndex(PoseIndex) , PoseName(PoseName) , OutExecs(OutExecs) { } virtual void UpdateOperation(FLatentResponse& Response) override { // Time out? if (TimeToWait > 0.0f) { TimeToWait -= Response.ElapsedTime(); if (TimeToWait <= 0.0f) { // We have timed out while waiting. *PoseIndex = -1; PoseName->Empty(); *OutExecs = EWaitForHandPoseExitType::TimeOut; Response.FinishAndTriggerIf(true, ExecutionFunction, OutputLink, CallbackTarget); } } // Did we recognize a pose? int Index; FString Name; float Duration; float Error; float Confidence; if (HandPoseRecognizer->GetRecognizedHandPose(Index, Name, Duration, Error, Confidence) && Duration >= PoseMinDuration) { *PoseIndex = Index; *PoseName = Name; *OutExecs = EWaitForHandPoseExitType::PoseSeen; Response.FinishAndTriggerIf(true, ExecutionFunction, OutputLink, CallbackTarget); } } #if WITH_EDITOR // Returns a human readable description of the latent operation's current state virtual FString GetDescription() const override { FString Msg = TEXT("Waiting for Hand Pose"); return Msg; } #endif }; ================================================ FILE: Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/HandGesture.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "HandGesture.h" #include "OculusHandPoseRecognitionModule.h" #include "HandPoseRecognizer.h" #include "Misc/Char.h" bool FHandGesture::ProcessEncodedGestureString(UHandPoseRecognizer* HandPoseRecognizer) { TCHAR const* Buffer = CustomEncodedGesture.GetCharArray().GetData(); while (Buffer && *Buffer) { int PoseIndex; float PoseMinDuration; if (!ReadTimedPose(HandPoseRecognizer, &Buffer, &PoseIndex, &PoseMinDuration)) { UE_LOG(LogHandPoseRecognition, Error, TEXT("Hand gesture error near position %d of %s"), CustomEncodedGesture.GetCharArray().GetData() - Buffer, *CustomEncodedGesture); return false; } TimedPoses.Add({PoseIndex, PoseMinDuration, 0.0f, 0.0f}); } // Reset state Reset(true); return true; } bool FHandGesture::Step(int PoseIndex, float PoseDuration, float DeltaTime, float CurrentTime, FVector const& Location) { // Nothing to do if there are no poses if (TimedPoses.Num() == 0) { return false; } if (GestureState == EGestureState::GestureNotStarted) { // We only go into "gesture in progress" state when we have held the initial pose a minimum amount of time. if (TimedPoses.Num() > 0 && TimedPoses[0].PoseIndex == PoseIndex && TimedPoses[0].PoseMinDuration < PoseDuration) { GestureState = EGestureState::GestureInProgress; CurrentStep = 0; GestureStartLocation = GestureEndLocation = Location; DurationInCurrentStep = PoseDuration; TimedPoses[0].StepFirstTime = TimedPoses[0].StepLastTime = CurrentTime; // Debugging if (bGestureDebugLog) { UE_LOG(LogHandPoseRecognition, Display, TEXT("Gesture %s step: index=%d duration=%0.2fs delta=%0.2fs time=%0.2fs"), *GestureName, PoseIndex, PoseDuration, DeltaTime, CurrentTime); UE_LOG(LogHandPoseRecognition, Display, TEXT(" Started with %0.2fs on first pose"), PoseDuration); } } } else // if (GestureState == GestureInProgress || (IsLooping && GestureState == GestureCompleted)) { // Debugging if (bGestureDebugLog) { UE_LOG(LogHandPoseRecognition, Display, TEXT("Gesture %s step: index=%d duration=%0.2fs delta=%0.2fs time=%0.2fs"), *GestureName, PoseIndex, PoseDuration, DeltaTime, CurrentTime); } if (TimedPoses[CurrentStep].PoseIndex == PoseIndex) { // We need a minimum of time in the current pose before we can move on to the next one. DurationInCurrentStep = PoseDuration; DurationInTransition = 0.0f; TimedPoses[CurrentStep].StepLastTime = CurrentTime; // For gesture start location, we are looking for a 'late' location in the first step, // and for the end location, we are looking for an 'early' location in the last step. if (CurrentStep == 0) { GestureStartLocation = GestureStartLocation * DirectionBufferingFactor + Location * (1.0f - DirectionBufferingFactor); } else if (CurrentStep == (TimedPoses.Num() - 1)) { GestureEndLocation = GestureEndLocation * (1.0f - DirectionBufferingFactor) + Location * DirectionBufferingFactor; } // Debugging if (bGestureDebugLog) { UE_LOG(LogHandPoseRecognition, Display, TEXT(" Pose held for %0.2fs"), DurationInCurrentStep); } } else { // We also cannot be in any other pose longer than the MaxTransitionTime. auto const NextStep = (CurrentStep + 1) % (TimedPoses.Num() + (bIsLooping ? 0 : 1)); if (NextStep < TimedPoses.Num() && DurationInCurrentStep >= TimedPoses[CurrentStep].PoseMinDuration && TimedPoses[NextStep].PoseIndex == PoseIndex) { // We meet all the conditions to move forward CurrentStep = NextStep; DurationInCurrentStep = PoseDuration; DurationInTransition = 0.0f; TimedPoses[CurrentStep].StepFirstTime = TimedPoses[CurrentStep].StepLastTime = CurrentTime; if (CurrentStep == (TimedPoses.Num() - 1)) { GestureEndLocation = Location; } // Debugging if (bGestureDebugLog) { UE_LOG(LogHandPoseRecognition, Display, TEXT(" Moved to next pose since current pose duration %0.2fs > minimum %0.2fs"), DurationInCurrentStep, TimedPoses[CurrentStep].PoseMinDuration); } } else { // This is not the current pose, and we are not ready to move to the next one, // so this counts as a transition. DurationInTransition += DeltaTime; // Non-looping gestures do not allow transitions on the last pose. // In all other situations, we test against the max transition time. if (DurationInTransition > MaxTransitionTime || (GestureState == EGestureState::GestureCompleted && !bIsLooping)) { if (bGestureDebugLog) { UE_LOG(LogHandPoseRecognition, Display, TEXT(" In transition for %0.2fs > max transition time %0.2fs => reset"), DurationInTransition, MaxTransitionTime); } Reset(); return false; } if (bGestureDebugLog) { UE_LOG(LogHandPoseRecognition, Display, TEXT(" In transition for %0.2fs"), DurationInTransition); } } } } // Checking for completion. if (GestureState != EGestureState::GestureNotStarted && CurrentStep == (TimedPoses.Num() - 1) && DurationInCurrentStep >= TimedPoses[CurrentStep].PoseMinDuration) { if (bGestureDebugLog && GestureState != EGestureState::GestureCompleted) { UE_LOG(LogHandPoseRecognition, Display, TEXT(" Gesture completed!")); } GestureState = EGestureState::GestureCompleted; } return GestureState == EGestureState::GestureCompleted; } float FHandGesture::ComputeTransitionTime( int FirstPoseIndex /* = 0 */, EPoseTimeType FirstPoseType /* = PoseLastTime */, int SecondPoseIndex /* = -1 */, EPoseTimeType SecondPoseType /* = PoseFirstTime */) const { auto const NumPoses = TimedPoses.Num(); if (SecondPoseIndex == -1) { SecondPoseIndex = TimedPoses.Num() - 1; } if (FirstPoseIndex < 0 || FirstPoseIndex >= NumPoses || SecondPoseIndex < 0 || SecondPoseIndex >= NumPoses || FirstPoseIndex >= SecondPoseIndex) { return 0.0f; } auto const FirstTime = FirstPoseType == PoseFirstTime ? TimedPoses[FirstPoseIndex].StepFirstTime : TimedPoses[FirstPoseIndex].StepLastTime; auto const SecondTime = SecondPoseType == PoseFirstTime ? TimedPoses[SecondPoseIndex].StepFirstTime : TimedPoses[SecondPoseIndex].StepLastTime; auto const Duration = SecondTime - FirstTime; return Duration < 0.0f ? 0.0f : Duration; } void FHandGesture::Reset(bool Force /* = false */) { if (Force || GestureState != EGestureState::GestureNotStarted) { GestureState = EGestureState::GestureNotStarted; CurrentStep = -1; DurationInCurrentStep = 0.0f; DurationInTransition = 0.0f; GestureStartLocation.Set(0, 0, 0); GestureEndLocation.Set(0, 0, 0); DirectionBufferingFactor = 0.75f; for (auto& TimedPose : TimedPoses) { TimedPose.StepFirstTime = 0.0f; TimedPose.StepLastTime = 0.0f; } } } void FHandGesture::DumpGestureState(int GestureIndex, UHandPoseRecognizer const* HandPoseRecognizer) const { FString StateString; switch (GestureState) { case EGestureState::GestureNotStarted: StateString = TEXT("NotStarted"); break; case EGestureState::GestureInProgress: StateString = TEXT("InProgress"); break; case EGestureState::GestureCompleted: StateString = TEXT("Completed"); break; } UE_LOG(LogHandPoseRecognition, Display, TEXT("Gesture %s[%d] %s"), *GestureName, GestureIndex, *StateString); UE_LOG(LogHandPoseRecognition, Display, TEXT("Duration %05.3f Transition %05.3f"), DurationInCurrentStep, DurationInTransition); auto Step = 0; for (auto const& TimedPose : TimedPoses) { UE_LOG(LogHandPoseRecognition, Display, TEXT(" %c %d %8s[%d] %05.3f - %05.3f"), CurrentStep==Step ? TEXT('>') : TEXT(' '), Step, *(HandPoseRecognizer->Poses[TimedPose.PoseIndex].PoseName), TimedPose.PoseIndex, TimedPose.StepFirstTime, TimedPose.StepLastTime); Step++; } } int FHandGesture::FindPoseIndex(UHandPoseRecognizer* Recognizer, FString const& PoseName) { for (auto PoseIndex = 0; PoseIndex < Recognizer->Poses.Num(); ++PoseIndex) { if (Recognizer->Poses[PoseIndex].PoseName == PoseName) { return PoseIndex; } } return -1; } bool FHandGesture::ReadTimedPose(UHandPoseRecognizer* Recognizer, TCHAR const** Buffer, int* PoseIndex, float* PoseMinDuration) { // Pose name FString PoseNameRead; while (FChar::IsWhitespace(**Buffer)) { ++(*Buffer); } while (**Buffer && **Buffer != '/' && **Buffer != ',' && !FChar::IsWhitespace(**Buffer)) { PoseNameRead += **Buffer; ++(*Buffer); } // Pose index auto const PoseIndexFound = FindPoseIndex(Recognizer, PoseNameRead); if (PoseIndexFound == -1) { UE_LOG(LogHandPoseRecognition, Error, TEXT("Unrecognized pose called %s in %s"), *PoseNameRead, *Recognizer->GetName()); return false; } // Pose min duration auto PoseMinDurationReadMillis = 0; while (FChar::IsWhitespace(**Buffer)) { ++(*Buffer); } if (**Buffer == '/') { ++(*Buffer); // We have a min duration in milliseconds while (FChar::IsWhitespace(**Buffer)) { ++(*Buffer); } while (FChar::IsDigit(**Buffer)) { PoseMinDurationReadMillis *= 10; PoseMinDurationReadMillis += (**Buffer) - '0'; ++(*Buffer); } } // Consume end of timed pose while (FChar::IsWhitespace(**Buffer)) { ++(*Buffer); } if (**Buffer == ',') { ++(*Buffer); } else if (**Buffer) { UE_LOG(LogHandPoseRecognition, Error, TEXT("End of timed pose expected near '%s'"), *Buffer); return false; } // Return results *PoseIndex = PoseIndexFound; *PoseMinDuration = PoseMinDurationReadMillis * 0.001f; return true; } ================================================ FILE: Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/HandGestureRecognizer.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "HandGestureRecognizer.h" #include "OculusHandPoseRecognitionModule.h" #include "Kismet/GameplayStatics.h" UHandGestureRecognizer::UHandGestureRecognizer(const FObjectInitializer& ObjectInitializer /* = FObjectInitializer::Get() */): Super(ObjectInitializer) { PrimaryComponentTick.bCanEverTick = true; RecognitionInterval = 0.0f; RecognitionSkippedFrames = 1; bHasRecognizedGesture = false; TimeSinceLastRecognition = 0.0f; SkippedFramesSinceLastRecognition = 0; } void UHandGestureRecognizer::BeginPlay() { Super::BeginPlay(); // We must be attached to a HandPoseRecognizer component TArray Parents; GetParentComponents(Parents); FString ImmediateParentClassName; if (Parents.Num() >= 1) { ImmediateParentClassName = Parents[0]->GetClass()->GetName(); HandPoseRecognizer = Cast(Parents[0]); } auto TurnOffGestureRecognition = false; if (!HandPoseRecognizer) { UE_LOG(LogHandPoseRecognition, Error, TEXT("UHandGestureRecognizer called %s MUST be attached to a UHandPoseRecognizer not a %s."), *GetName(), *ImmediateParentClassName); TurnOffGestureRecognition = true; } if (HandPoseRecognizer->Side == EOculusXRHandType::None) { UE_LOG(LogHandPoseRecognition, Warning, TEXT("UHandGestureRecognizer called %s is attached to a disabled UHandPoseRecognizer."), *GetName()); TurnOffGestureRecognition = true; } if (TurnOffGestureRecognition) { SetComponentTickEnabled(false); return; } // We decode the hand gestures for (auto GestureIndex = 0; GestureIndex < Gestures.Num(); ++GestureIndex) { if (!Gestures[GestureIndex].ProcessEncodedGestureString(HandPoseRecognizer)) { UE_LOG(LogHandPoseRecognition, Error, TEXT("UHandGestureRecognizer gesture at index %d is invalid."), GestureIndex); } } } void UHandGestureRecognizer::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); if (!HandPoseRecognizer) return; // Recognition is throttled TimeSinceLastRecognition += DeltaTime; if (TimeSinceLastRecognition < RecognitionInterval) { SkippedFramesSinceLastRecognition++; return; } if (SkippedFramesSinceLastRecognition < RecognitionSkippedFrames) { SkippedFramesSinceLastRecognition++; // For better recognition of gesture strength (speed of transition from first to last pose), // it is necessary to skip at least one cycle. // UE_LOG(LogOculusHandPoseRecognition, Error, TEXT("*** Skipping Frame (skipped %d)"), SkippedFramesSinceLastRecognition); return; } TimeSinceLastRecognition = 0.0f; SkippedFramesSinceLastRecognition = 0; // We need the current game time for recognizing the transition time between the first and last poses. auto const World = GetWorld(); check(World != nullptr); auto const CurrentTime = UGameplayStatics::GetTimeSeconds(World); // Currently recognized hand pose int PoseIndex; FString PoseName; float PoseDuration; float PoseError; float PoseConfidence; HandPoseRecognizer->GetRecognizedHandPose(PoseIndex, PoseName, PoseDuration, PoseError, PoseConfidence); // By getting the relative location this way we have: // X+ is forward, Y+ is right, Z+ is up auto const ActorRelativeLocation = GetComponentTransform().GetRelativeTransform(GetOwner()->GetTransform()).GetLocation(); // We process all hand gestures CompletedGestures.Reset(); for (auto GestureIndex = 0; GestureIndex < Gestures.Num(); ++GestureIndex) { if (Gestures[GestureIndex].Step(PoseIndex, PoseDuration, DeltaTime, CurrentTime, ActorRelativeLocation)) { CompletedGestures.Push(GestureIndex); } } // Updating property that indicates that at least one gesture is ready. bHasRecognizedGesture = CompletedGestures.Num() > 0; } bool UHandGestureRecognizer::GetRecognizedHandGesture( EGestureConsumptionBehavior Behavior, int& Index, FString& Name, FVector& GestureDirection, float& GestureOuterDuration, float& GestureInnerDuration) { if (CompletedGestures.Num() == 0) { // There are no completed gestures at this time Index = -1; Name = "None"; GestureDirection = FVector::ZeroVector; GestureOuterDuration = 0.0f; GestureInnerDuration = 0.0f; return false; } Index = CompletedGestures.Pop(EAllowShrinking::No); Name = Gestures[Index].GestureName; GestureDirection = Gestures[Index].GetGestureDirection(); GestureOuterDuration = Gestures[Index].ComputeOuterDuration(); GestureInnerDuration = Gestures[Index].ComputeInnerDuration(); // UE_LOG(LogOculusHandPoseRecognition, Warning, TEXT("Recognized gesture %d:%s."), Index, *Name); // Reset gesture(s) according to preference if (Behavior == EGestureConsumptionBehavior::Reset) { ResetHandGesture(Index); } else if (Behavior == EGestureConsumptionBehavior::ResetAll) { ResetAllHandGestures(); CompletedGestures.Reset(); } // Updating property that indicates that at least one other gesture is ready. bHasRecognizedGesture = CompletedGestures.Num() > 0; return true; } EGestureState UHandGestureRecognizer::GetGestureRecognitionState(int Index) { if (Index >= 0 && Index < Gestures.Num()) { return Gestures[Index].GetGestureState(); } return EGestureState::GestureNotStarted; } void UHandGestureRecognizer::ResetHandGesture(int& Index) { if (Index >= 0 && Index < Gestures.Num()) { Gestures[Index].Reset(); } } void UHandGestureRecognizer::ResetAllHandGestures() { for (auto& Gesture : Gestures) { Gesture.Reset(); } } void UHandGestureRecognizer::DumpAllGestureStates() const { UE_LOG(LogHandPoseRecognition, Warning, TEXT("Gesture states for %s"), *GetName()); for (auto GestureIndex = 0; GestureIndex < Gestures.Num(); ++GestureIndex) { Gestures[GestureIndex].DumpGestureState(GestureIndex, HandPoseRecognizer); } } ================================================ FILE: Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/HandPose.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "HandPose.h" #include "OculusXRInputFunctionLibrary.h" #include void FHandPose::UpdatePose(EOculusXRHandType Side, FRotator Wrist) { Hand = Side; Rotations[Thumb_0] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Thumb_0).Rotator(); Rotations[Thumb_1] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Thumb_1).Rotator(); Rotations[Thumb_2] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Thumb_2).Rotator(); Rotations[Thumb_3] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Thumb_3).Rotator(); Rotations[Index_1] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Index_1).Rotator(); Rotations[Index_2] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Index_2).Rotator(); Rotations[Index_3] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Index_3).Rotator(); Rotations[Middle_1] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Middle_1).Rotator(); Rotations[Middle_2] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Middle_2).Rotator(); Rotations[Middle_3] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Middle_3).Rotator(); Rotations[Ring_1] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Ring_1).Rotator(); Rotations[Ring_2] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Ring_2).Rotator(); Rotations[Ring_3] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Ring_3).Rotator(); Rotations[Pinky_0] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Pinky_0).Rotator(); Rotations[Pinky_1] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Pinky_1).Rotator(); Rotations[Pinky_2] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Pinky_2).Rotator(); Rotations[Pinky_3] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Pinky_3).Rotator(); Rotations[ERecognizedBone::Wrist] = Wrist; } void FHandPose::Encode() { CustomEncodedPose.Empty(1024); CustomEncodedPose .Append(Hand == EOculusXRHandType::HandLeft ? "L" : "R") .Append(*FmtRot(TEXT(" T0"), Rotations[Thumb_0])) .Append(*FmtRot(TEXT(" T1"), Rotations[Thumb_1])) .Append(*FmtRot(TEXT(" T2"), Rotations[Thumb_2])) .Append(*FmtRot(TEXT(" T3"), Rotations[Thumb_3])) .Append(*FmtRot(TEXT(" I1"), Rotations[Index_1])) .Append(*FmtRot(TEXT(" I2"), Rotations[Index_2])) .Append(*FmtRot(TEXT(" I3"), Rotations[Index_3])) .Append(*FmtRot(TEXT(" M1"), Rotations[Middle_1])) .Append(*FmtRot(TEXT(" M2"), Rotations[Middle_2])) .Append(*FmtRot(TEXT(" M3"), Rotations[Middle_3])) .Append(*FmtRot(TEXT(" R1"), Rotations[Ring_1])) .Append(*FmtRot(TEXT(" R2"), Rotations[Ring_2])) .Append(*FmtRot(TEXT(" R3"), Rotations[Ring_3])) .Append(*FmtRot(TEXT(" P0"), Rotations[Pinky_0])) .Append(*FmtRot(TEXT(" P1"), Rotations[Pinky_1])) .Append(*FmtRot(TEXT(" P2"), Rotations[Pinky_2])) .Append(*FmtRot(TEXT(" P3"), Rotations[Pinky_3])) .Append(*FmtRot(TEXT(" W"), Rotations[Wrist])); } bool FHandPose::Decode() { const TCHAR* Buffer = CustomEncodedPose.GetCharArray().GetData(); if (!Buffer) { Hand = EOculusXRHandType::None; return false; } SkipWhitespace(&Buffer); // Hand if (Buffer && *Buffer == 'L') { Hand = EOculusXRHandType::HandLeft; } else if (Buffer && *Buffer == 'R') { Hand = EOculusXRHandType::HandRight; } else { Hand = EOculusXRHandType::None; return false; } ++Buffer; // Rotators auto const Successful = ReadRot(&Buffer, TEXT("T0"), Rotations[Thumb_0], Weights[Thumb_0]) && ReadRot(&Buffer, TEXT("T1"), Rotations[Thumb_1], Weights[Thumb_1]) && ReadRot(&Buffer, TEXT("T2"), Rotations[Thumb_2], Weights[Thumb_2]) && ReadRot(&Buffer, TEXT("T3"), Rotations[Thumb_3], Weights[Thumb_3]) && ReadRot(&Buffer, TEXT("I1"), Rotations[Index_1], Weights[Index_1]) && ReadRot(&Buffer, TEXT("I2"), Rotations[Index_2], Weights[Index_2]) && ReadRot(&Buffer, TEXT("I3"), Rotations[Index_3], Weights[Index_3]) && ReadRot(&Buffer, TEXT("M1"), Rotations[Middle_1], Weights[Middle_1]) && ReadRot(&Buffer, TEXT("M2"), Rotations[Middle_2], Weights[Middle_2]) && ReadRot(&Buffer, TEXT("M3"), Rotations[Middle_3], Weights[Middle_3]) && ReadRot(&Buffer, TEXT("R1"), Rotations[Ring_1], Weights[Ring_1]) && ReadRot(&Buffer, TEXT("R2"), Rotations[Ring_2], Weights[Ring_2]) && ReadRot(&Buffer, TEXT("R3"), Rotations[Ring_3], Weights[Ring_3]) && ReadRot(&Buffer, TEXT("P0"), Rotations[Pinky_0], Weights[Pinky_0]) && ReadRot(&Buffer, TEXT("P1"), Rotations[Pinky_1], Weights[Pinky_1]) && ReadRot(&Buffer, TEXT("P2"), Rotations[Pinky_2], Weights[Pinky_2]) && ReadRot(&Buffer, TEXT("P3"), Rotations[Pinky_3], Weights[Pinky_3]) && ReadRot(&Buffer, TEXT("W"), Rotations[Wrist], Weights[Wrist]); if (!Successful) { Hand = EOculusXRHandType::None; } return Successful; } float FHandPose::ComputeConfidence(const FHandPose& Other, float* RawError /* = nullptr */) const { auto const Err = RotError(Thumb_0, Rotations, Weights, Other.Rotations) + RotError(Thumb_1, Rotations, Weights, Other.Rotations) + RotError(Thumb_2, Rotations, Weights, Other.Rotations) + RotError(Thumb_3, Rotations, Weights, Other.Rotations) + RotError(Index_1, Rotations, Weights, Other.Rotations) + RotError(Index_2, Rotations, Weights, Other.Rotations) + RotError(Index_3, Rotations, Weights, Other.Rotations) + RotError(Index_1, Rotations, Weights, Other.Rotations) + RotError(Middle_1, Rotations, Weights, Other.Rotations) + RotError(Middle_2, Rotations, Weights, Other.Rotations) + RotError(Middle_3, Rotations, Weights, Other.Rotations) + RotError(Ring_1, Rotations, Weights, Other.Rotations) + RotError(Ring_2, Rotations, Weights, Other.Rotations) + RotError(Ring_3, Rotations, Weights, Other.Rotations) + RotError(Pinky_0, Rotations, Weights, Other.Rotations) + RotError(Pinky_1, Rotations, Weights, Other.Rotations) + RotError(Pinky_2, Rotations, Weights, Other.Rotations) + RotError(Pinky_3, Rotations, Weights, Other.Rotations) + RotError(Wrist, Rotations, Weights, Other.Rotations); auto const MinErr = FMath::Max(ErrorAtMaxConfidence, 100.0f); auto const Confidence = MinErr / FMath::Max(Err, MinErr); if (RawError) { *RawError = Err; } return Confidence; } void FHandPose::AddWeighted(const FHandPose& Other, float OtherRatio) { OtherRatio = FMath::Clamp(OtherRatio, 0.0f, 1.0f); for (auto Bone = 0; Bone < NUM; ++Bone) { Rotations[Bone] *= 1.0f - OtherRatio; Rotations[Bone] += Other.Rotations[Bone] * OtherRatio; } } void FHandPose::Min(const FHandPose& Other) { for (auto Bone = 0; Bone < NUM; ++Bone) { Rotations[Bone].Pitch = FMath::Min(Rotations[Bone].Pitch, Other.Rotations[Bone].Pitch); Rotations[Bone].Yaw = FMath::Min(Rotations[Bone].Yaw, Other.Rotations[Bone].Yaw); Rotations[Bone].Roll = FMath::Min(Rotations[Bone].Roll, Other.Rotations[Bone].Roll); } } void FHandPose::Max(const FHandPose& Other) { for (auto Bone = 0; Bone < NUM; ++Bone) { Rotations[Bone].Pitch = FMath::Max(Rotations[Bone].Pitch, Other.Rotations[Bone].Pitch); Rotations[Bone].Yaw = FMath::Max(Rotations[Bone].Yaw, Other.Rotations[Bone].Yaw); Rotations[Bone].Roll = FMath::Max(Rotations[Bone].Roll, Other.Rotations[Bone].Roll); } } int FHandPose::NormalizedOutputAngle(float Angle) { // We never return 0, as it is used in reference poses to ignore angles. auto const IntAngle = static_cast(roundf(Angle)); return IntAngle ? IntAngle : 1; } FString FHandPose::FmtRot(FString Prefix, FRotator R) { // Never output 0 degree angles, as they are used to disable comparisons. auto const Pitch = NormalizedOutputAngle(R.Pitch); auto const Yaw = NormalizedOutputAngle(R.Yaw); auto const Roll = NormalizedOutputAngle(R.Roll); return FString::Printf(TEXT("%s%+0d%+0d%+0d"), *Prefix, Pitch, Yaw, Roll); } void FHandPose::SkipWhitespace(const TCHAR** Buffer) { while (**Buffer == ' ' || **Buffer == '\t') ++*Buffer; } bool FHandPose::ReadRotComp(const TCHAR** Buffer, double* RotComp) { SkipWhitespace(Buffer); auto Negative = false; if (**Buffer == '+') { ++*Buffer; } else if (**Buffer == '-') { Negative = true; ++*Buffer; } else { return false; } *RotComp = 0.0; while (**Buffer >= '0' && **Buffer <= '9') { *RotComp *= 10; *RotComp += **Buffer - '0'; ++*Buffer; } if (Negative) { *RotComp *= -1.0; } return true; } bool FHandPose::ReadWeight(const TCHAR** Buffer, float* Weight) { SkipWhitespace(Buffer); // Check for weight marker if (**Buffer != '*') { *Weight = 1.0f; // Defaults to 1 return true; } ++*Buffer; SkipWhitespace(Buffer); // Read weight factor auto Denominator = 0.0f; auto Value = 0.0f; while (**Buffer >= '0' && **Buffer <= '9' || **Buffer == '.') { if (**Buffer == '.') { Denominator = 1.0f; } else { Value *= 10.0f; Value += **Buffer - '0'; Denominator *= 10.0f; // Stays 0.0 as long as we have not seen the decimal point } ++*Buffer; } if (Denominator > 0.0f) { Value /= Denominator; } *Weight = Value; return true; } bool FHandPose::ReadRot(const TCHAR** Buffer, const TCHAR* Prefix, FRotator& R, float& Weight) { SkipWhitespace(Buffer); // Check if prefix matches auto PrefixPtr = Prefix; auto BufferPtr = *Buffer; while (*PrefixPtr && *BufferPtr && *PrefixPtr == *BufferPtr) { PrefixPtr++; BufferPtr++; } if (*PrefixPtr) { // Prefix mismatch, this may be a missing bone. R.Pitch = R.Yaw = R.Roll = 0.0f; Weight = 0.0f; return true; } // Looks good, let's get the rotations and optional weight *Buffer = BufferPtr; return ReadWeight(Buffer, &Weight) && ReadRotComp(Buffer, &R.Pitch) && ReadRotComp(Buffer, &R.Yaw) && ReadRotComp(Buffer, &R.Roll); } float FHandPose::ComputeAngleError(float Ref, float Angle) { // A reference angle of 0.0 is ignored. if (Ref == 0.0f) return 0.0f; // We find the minimum angle and square it auto const DeltaAngleDegrees = FMath::FindDeltaAngleDegrees(Ref, Angle); return DeltaAngleDegrees * DeltaAngleDegrees; } float FHandPose::RotError(ERecognizedBone Bone, const FRotator* RefRot, const float* RefWeight, const FRotator* OtherRot) { return RefWeight[Bone] * (ComputeAngleError(RefRot[Bone].Pitch, OtherRot[Bone].Pitch) + ComputeAngleError(RefRot[Bone].Yaw, OtherRot[Bone].Yaw) + ComputeAngleError(RefRot[Bone].Roll, OtherRot[Bone].Roll)); } ================================================ FILE: Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/HandPoseRecognizer.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "HandPoseRecognizer.h" #include "OculusHandPoseRecognitionModule.h" #include UHandPoseRecognizer::UHandPoseRecognizer(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer) { PrimaryComponentTick.bCanEverTick = true; // Recognition default parameters Side = EOculusXRHandType::None; RecognitionInterval = 0.0f; DefaultConfidenceFloor = 0.5; DampingFactor = 0.0f; // Current hand pose being recognized TimeSinceLastRecognition = 0.0f; CurrentHandPose = -1; CurrentHandPoseDuration = 0.0f; CurrentHandPoseConfidence = 0.0f; CurrentHandPoseError = std::numeric_limits::max(); // Encoded hand pose logged index LoggedIndex = 0; } void UHandPoseRecognizer::BeginPlay() { Super::BeginPlay(); // We decode the hand poses for (auto PatternIndex = 0; PatternIndex < Poses.Num(); ++PatternIndex) { if (!Poses[PatternIndex].Decode()) { UE_LOG(LogHandPoseRecognition, Error, TEXT("UHandPoseRecognizer(%s) encoded pose at index %d is invalid."), *GetName(), PatternIndex); } } } FRotator UHandPoseRecognizer::GetWristRotator(FQuat ComponentQuat) const { auto ComponentRotator = ComponentQuat.Rotator(); auto const World = GetWorld(); if (!World) return ComponentRotator; auto const PlayerController = World->GetFirstPlayerController(); if (!PlayerController) return ComponentRotator; auto const ComponentRotationToCamera = PlayerController->PlayerCameraManager->GetTransform().InverseTransformRotation(ComponentQuat).Rotator(); ComponentRotator.Yaw = ComponentRotationToCamera.Yaw; return ComponentRotator; } void UHandPoseRecognizer::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); if (Side == EOculusXRHandType::None) { // Recognizer is disabled return; } // Recognition is throttled TimeSinceLastRecognition += DeltaTime; if (TimeSinceLastRecognition < RecognitionInterval) { return; } // Ignore low confidence cases if (UOculusXRInputFunctionLibrary::GetTrackingConfidence(Side) == EOculusXRTrackingConfidence::Low) { return; } // Updating tracked hand. // Note that the wrist rotation pitch and roll are world relative, and the yaw is hmd relative. Pose.UpdatePose(Side, GetWristRotator(GetComponentQuat())); // Finding closest pattern auto ClosestHandPose = -1; auto ClosestHandPoseConfidence = DefaultConfidenceFloor; auto ClosestHandPoseError = std::numeric_limits::max(); auto HighestConfidence = 0.0f; for (auto PatternIndex = 0; PatternIndex < Poses.Num(); ++PatternIndex) { // Skip patterns that are not for this side if (Poses[PatternIndex].GetHandType() != Side) continue; // Computing confidence (we ignore the wrist yaw by default) auto RawError = 0.0f; auto const Confidence = Poses[PatternIndex].ComputeConfidence(Pose, &RawError); // UE_LOG(LogHandPoseRecognition, Error, TEXT("%s confidence %0.0f raw error %0.0f"), *Poses[PatternIndex].PoseName, Confidence, RawError); // We always record the smallest error, in case no pattern matches if (HighestConfidence < Confidence) HighestConfidence = Confidence; // We update the best pattern match (first: best match so far has lower confidence than the current pose) if (ClosestHandPoseConfidence < Confidence) { // Second: checking for custom pose error ceiling if (Poses[PatternIndex].CustomConfidenceFloor > 0.0f && Confidence < Poses[PatternIndex].CustomConfidenceFloor) continue; ClosestHandPoseConfidence = Confidence; ClosestHandPoseError = RawError; ClosestHandPose = PatternIndex; } } // If we have no match, we report the highest confidence seen. if (ClosestHandPose == -1) { ClosestHandPoseConfidence = HighestConfidence; } if (CurrentHandPose == ClosestHandPose) { // Same pose as before is being held CurrentHandPoseDuration += TimeSinceLastRecognition; TimeSinceLastRecognition = 0.0; CurrentHandPoseConfidence = DampingFactor * CurrentHandPoseConfidence + (1.0f - DampingFactor) * ClosestHandPoseConfidence; CurrentHandPoseError = DampingFactor * CurrentHandPoseError + (1.0f - DampingFactor) * ClosestHandPoseError; } else { // Change of pose CurrentHandPose = ClosestHandPose; CurrentHandPoseDuration = 0.0f; CurrentHandPoseConfidence = ClosestHandPoseConfidence; CurrentHandPoseError = ClosestHandPoseError; } } bool UHandPoseRecognizer::GetRecognizedHandPose(int& Index, FString& Name, float& Duration, float& Error, float& Confidence) { Index = CurrentHandPose; Duration = CurrentHandPoseDuration; Error = CurrentHandPoseError; Confidence = CurrentHandPoseConfidence; if (Index >= 0 && Index < Poses.Num()) { Name = Poses[Index].PoseName; return true; } Name = TEXT("None"); return false; } void UHandPoseRecognizer::LogEncodedHandPose() { Pose.Encode(); UE_LOG(LogHandPoseRecognition, Warning, TEXT("HAND POSE %d: %s"), LoggedIndex++, *Pose.CustomEncodedPose); } ================================================ FILE: Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/HandRecognitionFunctionLibrary.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "HandRecognitionFunctionLibrary.h" #include "Engine.h" #include "FWaitForHandPoseAction.h" #include "FWaitForHandGestureAction.h" #include "FRecordHandPoseAction.h" void UHandRecognitionFunctionLibrary::WaitForHandPose( UObject* WorldContextObject, UHandPoseRecognizer* HandPoseRecognizer, float PoseMinDuration, float TimeToWait, EWaitForHandPoseExitType& OutExecs, int& PoseIndex, FString& PoseName, FLatentActionInfo LatentInfo) { if (auto World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull)) { auto& LatentActionManager = World->GetLatentActionManager(); if (LatentActionManager.FindExistingAction(LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr) { LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, new FWaitForHandPoseAction(LatentInfo, HandPoseRecognizer, PoseMinDuration, TimeToWait, &PoseIndex, &PoseName, &OutExecs)); } } } void UHandRecognitionFunctionLibrary::WaitForHandGesture( UObject* WorldContextObject, UHandGestureRecognizer* HandGestureRecognizer, float TimeToWait, EGestureConsumptionBehavior Behavior, EWaitForHandGestureExitType& OutExecs, int& GestureIndex, FString& GestureName, FVector& GestureDirection, float& GestureOuterDuration, float& GestureInnerDuration, FLatentActionInfo LatentInfo) { if (auto World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull)) { auto& LatentActionManager = World->GetLatentActionManager(); if (LatentActionManager.FindExistingAction(LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr) { LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, new FWaitForHandGestureAction(LatentInfo, HandGestureRecognizer, TimeToWait, Behavior, &GestureIndex, &GestureName, &GestureDirection, &GestureOuterDuration, &GestureInnerDuration, &OutExecs)); } } } void UHandRecognitionFunctionLibrary::RecordHandPose(UObject* WorldContextObject, UHandPoseRecognizer* Recognizer, const ERecordHandPoseEntryType& InExecs, ERecordHandPoseExitType& OutExecs, FLatentActionInfo LatentInfo) { if (auto World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull)) { auto& LatentActionManager = World->GetLatentActionManager(); if (LatentActionManager.FindExistingAction(LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr) { LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, new FRecordHandPoseAction(LatentInfo, Recognizer, &InExecs, &OutExecs)); } } } ================================================ FILE: Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/HandRecognitionGameMode.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "HandRecognitionGameMode.h" #include "OculusHandPoseRecognitionModule.h" #include "OculusXRInputFunctionLibrary.h" #include "OculusXRHandComponent.h" #include "HandPose.h" int AHandRecognitionGameMode::LoggedIndex = 0; void AHandRecognitionGameMode::LogHandPose(FString Side) { auto const World = GetWorld(); if (!World) { UE_LOG(LogHandPoseRecognition, Error, TEXT("LogHandPose failed to find a valid world.")); return; } auto const Controller = World->GetFirstPlayerController(); if (!Controller) { UE_LOG(LogHandPoseRecognition, Error, TEXT("LogHandPose failed to find a player controller.")); return; } auto const Pawn = Controller->GetPawn(); if (!Pawn) { UE_LOG(LogHandPoseRecognition, Error, TEXT("LogHandPose failed to find a pawn.")); return; } // We use the first matching OculusHandComponent. auto HandType = EOculusXRHandType::None; if (Side.Equals(TEXT("left"), ESearchCase::IgnoreCase)) HandType = EOculusXRHandType::HandLeft; else if (Side.Equals(TEXT("right"), ESearchCase::IgnoreCase)) HandType = EOculusXRHandType::HandRight; if (HandType == EOculusXRHandType::None) { UE_LOG(LogHandPoseRecognition, Error, TEXT("LogHandPose requires \"left\" or \"right\" parameter, but received \"%s\""), *Side); return; } TArray OculusHandComponents; Pawn->GetComponents(OculusHandComponents); for (auto HandComponent : OculusHandComponents) { if (HandComponent->SkeletonType == HandType) { FHandPose Pose; Pose.UpdatePose(HandType, HandComponent->GetComponentRotation()); Pose.Encode(); UE_LOG(LogHandPoseRecognition, Warning, TEXT("HAND POSE %d: %s"), LoggedIndex++, *Pose.CustomEncodedPose); return; } } UE_LOG(LogHandPoseRecognition, Error, TEXT("LogHandPose did not find a valid OculusHandComponent on the %s side"), *Side); } ================================================ FILE: Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/OculusHandPoseRecognitionModule.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "OculusHandPoseRecognitionModule.h" #define LOCTEXT_NAMESPACE "FOculusHandPoseRecognitionModule" DEFINE_LOG_CATEGORY(LogHandPoseRecognition); #include "OculusDeveloperTelemetry.h" OCULUS_TELEMETRY_LOAD_MODULE("Unreal-OculusHandPoseRecognition"); void FOculusHandPoseRecognitionModule::StartupModule() { // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module } void FOculusHandPoseRecognitionModule::ShutdownModule() { // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, // we call this function before unloading the module. } #undef LOCTEXT_NAMESPACE IMPLEMENT_MODULE(FOculusHandPoseRecognitionModule, OculusHandPoseRecognition) ================================================ FILE: Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/PoseableHandComponent.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "PoseableHandComponent.h" #include "OculusHandPoseRecognitionModule.h" void UPoseableHandComponent::BeginPlay() { if (GetSkinnedAsset() != nullptr) { bCustomPoseableHandMesh = true; } Super::BeginPlay(); } void UPoseableHandComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); if (CustomPoseWeightCurrent != CustomPoseWeightTarget) { CustomPoseWeightCurrent = FMath::FInterpConstantTo(CustomPoseWeightCurrent, CustomPoseWeightTarget, DeltaTime, CustomPoseWeightLerpSpeed); } if (CustomPoseWeightCurrent > 0.f) { UpdateBonePose(EOculusXRBone::Thumb_0, Thumb_0); UpdateBonePose(EOculusXRBone::Thumb_1, Thumb_1); UpdateBonePose(EOculusXRBone::Thumb_2, Thumb_2); UpdateBonePose(EOculusXRBone::Thumb_3, Thumb_3); UpdateBonePose(EOculusXRBone::Index_1, Index_1); UpdateBonePose(EOculusXRBone::Index_2, Index_2); UpdateBonePose(EOculusXRBone::Index_3, Index_3); UpdateBonePose(EOculusXRBone::Middle_1, Middle_1); UpdateBonePose(EOculusXRBone::Middle_2, Middle_2); UpdateBonePose(EOculusXRBone::Middle_3, Middle_3); UpdateBonePose(EOculusXRBone::Ring_1, Ring_1); UpdateBonePose(EOculusXRBone::Ring_2, Ring_2); UpdateBonePose(EOculusXRBone::Ring_3, Ring_3); UpdateBonePose(EOculusXRBone::Pinky_0, Pinky_0); UpdateBonePose(EOculusXRBone::Pinky_1, Pinky_1); UpdateBonePose(EOculusXRBone::Pinky_2, Pinky_2); UpdateBonePose(EOculusXRBone::Pinky_3, Pinky_3); } } void UPoseableHandComponent::UpdateBonePose(EOculusXRBone Bone, ERecognizedBone PosedBone) { auto BoneIndex = (int)Bone; if (bCustomPoseableHandMesh && BoneNameMappings.Contains(Bone)) { auto const MappedBoneIndex = GetSkinnedAsset()->GetRefSkeleton().FindBoneIndex(BoneNameMappings[Bone]); if (MappedBoneIndex != INDEX_NONE) { BoneIndex = MappedBoneIndex; } } if (BoneSpaceTransforms.Num() > BoneIndex) { auto const TrackedRotation = BoneSpaceTransforms[BoneIndex].GetRotation(); auto const PosedRotation = CustomPose.GetRotator(PosedBone).Quaternion(); auto const PosedWeight = CustomPose.GetWeight(PosedBone) * CustomPoseWeightCurrent; if (PosedWeight > 0) { auto const NewRotation = FQuat::Slerp(TrackedRotation, PosedRotation, PosedWeight); BoneSpaceTransforms[BoneIndex].SetRotation(NewRotation); } } } void UPoseableHandComponent::SetPose(FString PoseString, float LerpSpeedOverride) { CustomPose.CustomEncodedPose = PoseString; if (CustomPose.Decode()) { CustomPoseWeightTarget = 1.f; CustomPoseWeightCurrent = 0.f; CustomPoseWeightLerpSpeed = LerpSpeedOverride > 0 ? LerpSpeedOverride : DefaultLerpSpeed; } else { UE_LOG(LogHandPoseRecognition, Warning, TEXT("Pose string '%s' can not be decoded."), *PoseString); } } void UPoseableHandComponent::ClearPose(float LerpSpeedOverride) { CustomPoseWeightTarget = 0.f; CustomPoseWeightLerpSpeed = LerpSpeedOverride > 0 ? LerpSpeedOverride : DefaultLerpSpeed; } ================================================ FILE: Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Public/HandGesture.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" #include "HandGesture.generated.h" class UHandPoseRecognizer; /** Gesture progress. */ UENUM(BlueprintType) enum class EGestureState : uint8 { GestureNotStarted, GestureInProgress, GestureCompleted }; /** A single pose with minimum duration. */ struct FHandGestureStep { // Values provided by the gesture description. int PoseIndex; float PoseMinDuration; // Timings acquired during recognition of this step. float StepFirstTime, StepLastTime; }; /** A struct that represents a series of poses over time. */ USTRUCT(BlueprintType) struct OCULUSHANDPOSERECOGNITION_API FHandGesture { GENERATED_BODY() /** Name for this gesture. */ UPROPERTY(Category = "Hand Gesture", EditAnywhere, BlueprintReadWrite) FString GestureName; /** Tolerance (in seconds) for intermediate poses that do not match the sequence. */ UPROPERTY(Category = "Hand Gesture", EditAnywhere, BlueprintReadWrite) float MaxTransitionTime{}; /** Is this gesture representing a looping series of poses? (e.g. waving) */ UPROPERTY(Category = "Hand Gesture", EditAnywhere, BlueprintReadWrite) bool bIsLooping{}; /** Series of poses as a string. */ UPROPERTY(Category = "Hand Gesture", EditAnywhere, BlueprintReadWrite) FString CustomEncodedGesture; /** If true, will log gesture updates and transitions. */ UPROPERTY(Category = "Hand Gesture", EditAnywhere, BlueprintReadWrite) bool bGestureDebugLog = false; /** * Decodes the gesture string. * @param HandPoseRecognizer - Needed to identify poses by name. */ bool ProcessEncodedGestureString(UHandPoseRecognizer* HandPoseRecognizer); /** * Called regularly with current pose information to recognize the gesture. * @param PoseIndex - HandPoseRecognizer current recognized pose index. * @param PoseDuration - How long this pose has been held. * @param DeltaTime - Time elapsed since last call. * @param CurrentTime - Current time. * @param Location - Current controller location. * @return A boolean that indicates if the gesture is recognized. */ bool Step(int PoseIndex, float PoseDuration, float DeltaTime, float CurrentTime, const FVector& Location); /** Each pose has a first and last game time registered. */ enum EPoseTimeType { PoseFirstTime, PoseLastTime }; /** * Computes the time between two gestures steps. * By default, it computes the time taken from the end of the first pose and the last. * @param FirstPoseIndex - Index of the first pose * @param FirstPoseType - Time type of the first pose * @param SecondPoseIndex - Index of the second pose * @param SecondPoseType - Time type of the second pose * @return Duration to transition from the first and second poses. */ float ComputeTransitionTime(int FirstPoseIndex = 0, EPoseTimeType FirstPoseType = PoseLastTime, int SecondPoseIndex = -1, EPoseTimeType SecondPoseType = PoseFirstTime) const; /** Returns the outer gesture duration. */ float ComputeOuterDuration() const { return ComputeTransitionTime(0, PoseFirstTime, -1, PoseLastTime); } /** Returns the outer gesture duration. */ float ComputeInnerDuration() const { return ComputeTransitionTime(0, PoseLastTime, -1, PoseFirstTime); } /** * Called to reset the gesture recognition state. * @param Force - Normally only resets when in progress or completed. */ void Reset(bool Force = false); /** * Debug gesture with full log dump. * @param GestureIndex - Supplied by the gesture recognition system. * @param HandPoseRecognizer - Used to retrieve the pose name. */ void DumpGestureState(int GestureIndex, const UHandPoseRecognizer* HandPoseRecognizer) const; /** Returns the gesture direction. */ FVector GetGestureDirection() const { return GestureEndLocation - GestureStartLocation; } /** Returns the gesture recognition state. */ EGestureState GetGestureState() const { return GestureState; } protected: /** Array of decoded poses that represent the gesture. */ TArray TimedPoses; /** Gesture progress. */ EGestureState GestureState = EGestureState::GestureNotStarted; int CurrentStep = -1; FVector GestureStartLocation, GestureEndLocation; float DirectionBufferingFactor = 0.0f; float DurationInCurrentStep = 0.0f, DurationInTransition = 0.0f; private: static int FindPoseIndex(UHandPoseRecognizer* Recognizer, const FString& PoseName); static bool ReadTimedPose(UHandPoseRecognizer* Recognizer, const TCHAR** Buffer, int* PoseIndex, float* PoseMinDuration); }; ================================================ FILE: Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Public/HandGestureRecognizer.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" #include "Components/ActorComponent.h" #include "HandGesture.h" #include "HandPoseRecognizer.h" #include "HandGestureRecognizer.generated.h" /** * What to do with a gesture after it has been returned by GetRecognizedHandGesture(). */ UENUM(BlueprintType) enum class EGestureConsumptionBehavior : uint8 { Reset, ResetAll, NoReset }; /** * Bones for gesture strength settings. */ UENUM(BlueprintType) enum class EGestureStrengthBone : uint8 { Thumb0 = 0, Thumb1, Thumb2, Thumb3, Index1, Index2, Index3, Middle1, Middle2, Middle3, Ring1, Ring2, Ring3, Pinky0, Pinky1, Pinky2, Pinky3, Wrist }; /** * Angles for gesture strength settings. */ UENUM(BlueprintType) enum class EGestureStrengthBoneAngle : uint8 { Pitch = 0, Yaw, Roll }; /** * Actor component that recognizes gestures (i.e. poses over time). * * @warning Must be attached to a UHandPoseRecognizer. */ UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent)) class OCULUSHANDPOSERECOGNITION_API UHandGestureRecognizer : public USceneComponent { GENERATED_BODY() public: /** Sets default values for this component's properties. */ UHandGestureRecognizer(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); protected: /** Called when the game starts. */ virtual void BeginPlay() override; /** Parent hand pose recognizer. */ UHandPoseRecognizer* HandPoseRecognizer = nullptr; public: /** Called every frame. */ virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; /** Minimum time interval between hand recognitions (throttling). */ UPROPERTY(Category = "Hand Gesture Recognition", EditAnywhere, BlueprintReadWrite) float RecognitionInterval; /** Minimum number of frames to skip between hand recognitions (throttling). */ UPROPERTY(Category = "Hand Gesture Recognition", EditAnywhere, BlueprintReadWrite) int RecognitionSkippedFrames; /** Collection of hand gestures to recognize. */ UPROPERTY(Category = "Hand Gesture Recognition", EditAnywhere, BlueprintReadWrite) TArray Gestures; /** * Call to check if there is a recognized gesture pending. * @return A boolean that indicates if there's at least one pending gesture recognized. */ UPROPERTY(Category = "Hand Gesture Recognition", BlueprintReadOnly) bool bHasRecognizedGesture; /** * Call to get the currently recognized gesture. * @param Behavior - Auto-reset of the behavior returned. * @param Index - Index of the recognized gesture. * @param Name - Name of the recognized gesture. * @param GestureDirection - Gesture vector, relative to owning actor. * @param GestureOuterDuration - Overall gesture duration. * @param GestureInnerDuration - Inner gesture duration. * @return A boolean that indicates if a gesture is currently recognized. */ UFUNCTION(BlueprintCallable) UPARAM(DisplayName = "Gesture Recognized") bool GetRecognizedHandGesture( EGestureConsumptionBehavior Behavior, int& Index, FString& Name, FVector& GestureDirection, float& GestureOuterDuration, float& GestureInnerDuration); /** * Call to get the state of recognition of a specific gesture. * @param Index - Index of the gesture to query. * @return Current state of gesture recognition. */ UFUNCTION(BlueprintCallable) UPARAM(DisplayName = "Gesture State") EGestureState GetGestureRecognitionState(int Index); /** * Resets the specified hand gesture by index. * @param Index - Where to store the index of the recognized gesture. */ UFUNCTION(BlueprintCallable) void ResetHandGesture(int& Index); /** Resets all hand gestures. */ UFUNCTION(BlueprintCallable) void ResetAllHandGestures(); /** For debugging purposes: a state dump of all hand gestures. */ UFUNCTION(BlueprintCallable) void DumpAllGestureStates() const; private: /** Recognition state. */ int SkippedFramesSinceLastRecognition; float TimeSinceLastRecognition; /** Gestures that were completed in the last tick. */ TArray CompletedGestures; }; ================================================ FILE: Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Public/HandPose.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" #include "OculusXRInputFunctionLibrary.h" #include "HandPose.generated.h" /** Bones that we care about. */ UENUM() enum ERecognizedBone { Thumb_0 = 0, Thumb_1, Thumb_2, Thumb_3, Index_1, Index_2, Index_3, Middle_1, Middle_2, Middle_3, Ring_1, Ring_2, Ring_3, Pinky_0, Pinky_1, Pinky_2, Pinky_3, Wrist, NUM }; /** A struct that represents a hand pose. */ USTRUCT(BlueprintType) struct OCULUSHANDPOSERECOGNITION_API FHandPose { GENERATED_BODY() public: /** Name for this pose. */ UPROPERTY(Category = "Hand Pose", EditAnywhere, BlueprintReadWrite) FString PoseName; /** Hand pose encoded in a string. */ UPROPERTY(Category = "Hand Pose", EditAnywhere, BlueprintReadWrite) FString CustomEncodedPose; /** Hand pose custom confidence floor, when greater than 0. */ UPROPERTY(Category = "Hand Pose", EditAnywhere, BlueprintReadWrite) float CustomConfidenceFloor = 0.0f; /** * Hand pose error at max confidence of 1.0. * Confidence of 0.50 corresponds to twice this error. * Confidence of 0.25 corresponds to four times this error. * Confidence of 0.10 corresponds to ten times this error. */ UPROPERTY(Category = "Hand Pose", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "100.0", UIMin = "100.0")) float ErrorAtMaxConfidence = 2000.0f; /** Returns the side that this */ EOculusXRHandType GetHandType() const { return Hand; } /** * Rotator access. * @param Bone * @return A const reference to the rotator of that bone. */ const FRotator& GetRotator(ERecognizedBone Bone) const { return Rotations[Bone]; } /** * Weight access. * @param Bone * @return The weight of the given bone. */ float GetWeight(ERecognizedBone Bone) const { return Weights[Bone]; } /** * Updates the structure with the current bone rotations for the side specified. * Wrist information is received from the HandPoseRecognizer. * * @param Side - EOculusXRHandType to track * @param Wrist - FRotator from the controller. */ void UpdatePose(EOculusXRHandType Hand, FRotator Wrist); /** Encodes rotators to string form, without weights. */ void Encode(); /** * Decodes the encoded pose string into rotators and weights. * This is always called to configure references poses. * @return True if there were no issues during decoding. */ bool Decode(); /** * Computes the confidence of the other pose being similar to this reference pose. * @param Other - The hand pose to evaluate. * @param RawError - The raw error (optional). * @return The confidence level. * @warning You should call this method on a reference pose. */ float ComputeConfidence(const FHandPose& Other, float* RawError = nullptr) const; /** * Used for average bone rotations: weighted transition to another pose. * @param Other - Hand pose. * @param OtherRatio - How much of the other pose we take. */ void AddWeighted(const FHandPose& Other, float OtherRatio); /** * Used to record the minimum bone angles. * Copies all pitch/yaw/roll angles from Other that are smaller that our current ones. * @param Other - Hand pose. */ void Min(const FHandPose& Other); /** * Used to record the maximum bone angles. * Copies all pitch/yaw/roll angles from Other that are greater that our current ones. * @param Other - Hand pose. */ void Max(const FHandPose& Other); protected: /** Hand side that will be set during parsing. */ EOculusXRHandType Hand = EOculusXRHandType::None; /** Hand bone rotators. */ FRotator Rotations[NUM]; /** Hand bone weights. */ float Weights[NUM] = {}; private: static int NormalizedOutputAngle(float Angle); static FString FmtRot(FString Prefix, FRotator R); static void SkipWhitespace(const TCHAR** Buffer); static bool ReadRotComp(const TCHAR** Buffer, double* RotComp); static bool ReadWeight(const TCHAR** Buffer, float* Weight); static bool ReadRot(const TCHAR** Buffer, const TCHAR* Prefix, FRotator& R, float& Weight); static float ComputeAngleError(float Ref, float Angle); static float RotError(ERecognizedBone Bone, const FRotator* RefRot, const float* RefWeight, const FRotator* OtherRot); }; ================================================ FILE: Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Public/HandPoseRecognizer.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" #include "Components/ActorComponent.h" #include "HandPose.h" #include "OculusXRHandComponent.h" #include "OculusXRInputFunctionLibrary.h" #include "HandPoseRecognizer.generated.h" /** * Actor component that recognizes hand poses. * * @warning Must be attached to a component that moves with hands. */ UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) class OCULUSHANDPOSERECOGNITION_API UHandPoseRecognizer : public USceneComponent { GENERATED_BODY() public: /** Sets default values for this component's properties. */ UHandPoseRecognizer(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); protected: /** Called when the game starts. */ virtual void BeginPlay() override; public: /** Called every frame. */ virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; /** Hand side recognized (set to None to disable the component). */ UPROPERTY(Category = "Hand Pose Recognition", EditAnywhere, BlueprintReadWrite) EOculusXRHandType Side; /** Minimum time interval between hand recognitions (throttling). */ UPROPERTY(Category = "Hand Pose Recognition", EditAnywhere, BlueprintReadWrite) float RecognitionInterval; /** Minimum confidence level needed to recognize a pose. Can be overridden for each individual pose. */ UPROPERTY(Category = "Hand Pose Recognition", EditAnywhere, BlueprintReadWrite) float DefaultConfidenceFloor; /** Fraction of the current confidence level that we keep per unit of time. */ UPROPERTY(Category = "Hand Pose Recognition", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0.0", ClampMax = "1.0", UIMin = "0.0", UIMax = "1.0")) float DampingFactor; /** Recognized hand pose patterns. */ UPROPERTY(Category = "Hand Pose Recognition", EditAnywhere, BlueprintReadWrite) TArray Poses; /** * Call to get the currently recognized hand pose. * @param Index - Index of the recognized pose. * @param Name - Name of the recognized pose. * @param Duration - How long this pose has been held. * @param Error - Raw recognition error. * @param Confidence - Confidence level from 0.0 to 1.0. * @return A boolean that indicates if a pose is currently recognized. */ UFUNCTION(BlueprintCallable) UPARAM(DisplayName = "Pose Recognized") bool GetRecognizedHandPose(int& Index, FString& Name, float& Duration, float& Error, float& Confidence); /** Access to the last hand pose information. */ const FHandPose& GetCurrentPose() const { return Pose; } /** * Call to log the current hand pose. * This is used to create reference poses that can then be tweaked. */ UFUNCTION(BlueprintCallable) void LogEncodedHandPose(); protected: /** Structure storing the current bone rotators. */ FHandPose Pose; /** * Returns the wrist rotation with yaw adjusted relative to the player's gaze. * @param ComponentQuat - Quaternion representing the controller rotation. * @return The adjusted rotator. */ FRotator GetWristRotator(FQuat ComponentQuat) const; private: /** Recognition state. */ float TimeSinceLastRecognition; int CurrentHandPose; float CurrentHandPoseDuration; float CurrentHandPoseConfidence; float CurrentHandPoseError; /** Index incremented every time LogEncodedHandPose() is called, to help identify reference poses in the logs. */ int LoggedIndex; }; ================================================ FILE: Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Public/HandRecognitionFunctionLibrary.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" #include "HandRecognitionFunctionLibrary.generated.h" class UHandPoseRecognizer; class UHandGestureRecognizer; /** Output exec pins for the WaitForHandPose blueprint node. */ UENUM(BlueprintType) enum class EWaitForHandPoseExitType : uint8 { PoseSeen, TimeOut, }; /** Output exec pins for the WaitForHandGesture blueprint node. */ UENUM(BlueprintType) enum class EWaitForHandGestureExitType : uint8 { GestureSeen, TimeOut, }; /** Input/output exec pins for the RecordHandPose blueprint node. */ UENUM(BlueprintType) enum class ERecordHandPoseEntryType : uint8 { StartRecording, StopRecording, }; UENUM(BlueprintType) enum class ERecordHandPoseExitType : uint8 { RecordingStarted, RecordingStopped, }; /** Library for all hand pose recognition nodes. */ UCLASS() class OCULUSHANDPOSERECOGNITION_API UHandRecognitionFunctionLibrary : public UBlueprintFunctionLibrary { GENERATED_BODY() public: /** * Waits for a hand pose to be recognized. * @param WorldContextObject - World context * @param HandPoseRecognizer - The hand pose recognizer component to wait on. * @param PoseMinDuration - The pose must be maintained for this amount of time to be recognized. * @param TimeToWait - The node will exit with TimeOut if no pose is recognized in this amount of time. Use negative seconds to never time out. * @param PoseIndex - Recognized pose index. * @param PoseName - Recognized pose name. * @param LatentInfo - For latent node * @param OutExecs - Exit pins. */ UFUNCTION(BlueprintCallable, meta = (Latent, LatentInfo = "LatentInfo", WorldContext = "WorldContextObject", ExpandEnumAsExecs = "OutExecs", PoseMinDuration = "0", TimeToWait = "-1"), Category = "Hand Recognition") static void WaitForHandPose( UObject* WorldContextObject, UHandPoseRecognizer* HandPoseRecognizer, float PoseMinDuration, float TimeToWait, EWaitForHandPoseExitType& OutExecs, int& PoseIndex, FString& PoseName, FLatentActionInfo LatentInfo); /** * Waits for a hand gesture to be recognized. * @param WorldContextObject - world context * @param HandGestureRecognizer - The hand gesture recognizer component to wait on. * @param TimeToWait - The node will exit with TimeOut if no gesture is recognized in this amount of time. Use negative seconds to never time out. * @param Behavior - Auto-reset of the behavior returned. * @param GestureIndex - Recognized gesture index. * @param GestureName - Recognized gesture name. * @param GestureDirection - Recognized gesture direction. * @param GestureOuterDuration - Recognized outer gesture duration * @param GestureInnerDuration - Recognized inner gesture duration * @param LatentInfo - for latent node * @param OutExecs - Exit pins. */ UFUNCTION(BlueprintCallable, meta = (Latent, LatentInfo = "LatentInfo", WorldContext = "WorldContextObject", ExpandEnumAsExecs = "OutExecs", PoseMinDuration = "0", TimeToWait = "-1"), Category = "Hand Recognition") static void WaitForHandGesture( UObject* WorldContextObject, UHandGestureRecognizer* HandGestureRecognizer, float TimeToWait, EGestureConsumptionBehavior Behavior, EWaitForHandGestureExitType& OutExecs, int& GestureIndex, FString& GestureName, FVector& GestureDirection, float& GestureOuterDuration, float& GestureInnerDuration, FLatentActionInfo LatentInfo); /** * Records the range of hand pose to be recognized. * @param WorldContextObject - World context * @param Recognizer - HandPoseRecognizer used for hand side and wrist information. * @param InExecs - Entry pins. * @param OutExecs - Exit pins. * @param LatentInfo - For latent node */ UFUNCTION(BlueprintCallable, meta = (Latent, LatentInfo = "LatentInfo", WorldContext = "WorldContextObject", ExpandEnumAsExecs = "InExecs,OutExecs"), Category = "Hand Recognition") static void RecordHandPose(UObject* WorldContextObject, UHandPoseRecognizer* Recognizer, const ERecordHandPoseEntryType& InExecs, ERecordHandPoseExitType& OutExecs, FLatentActionInfo LatentInfo); }; ================================================ FILE: Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Public/HandRecognitionGameMode.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" #include "GameFramework/GameModeBase.h" #include "Misc/CoreMisc.h" #include "HandRecognitionGameMode.generated.h" /** * A game mode that offers some console commands to write poses to the log. */ UCLASS() class OCULUSHANDPOSERECOGNITION_API AHandRecognitionGameMode : public AGameModeBase { GENERATED_BODY() public: /** * Prints a hand pose string in the output log. * @param Side - Either "left" or "right", case insensitive. */ UFUNCTION(Exec) void LogHandPose(FString Side); private: /** Helps keep track of poses logged. */ static int LoggedIndex; }; ================================================ FILE: Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Public/OculusHandPoseRecognitionModule.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" #include "Modules/ModuleManager.h" DECLARE_LOG_CATEGORY_EXTERN(LogHandPoseRecognition, Log, All); class FOculusHandPoseRecognitionModule : public IModuleInterface { public: /** IModuleInterface implementation */ virtual void StartupModule() override; virtual void ShutdownModule() override; }; ================================================ FILE: Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Public/PoseableHandComponent.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" #include "OculusXRHandComponent.h" #include "HandPose.h" #include "PoseableHandComponent.generated.h" UCLASS(Blueprintable, meta = (BlueprintSpawnableComponent), ClassGroup = OculusHand) class OCULUSHANDPOSERECOGNITION_API UPoseableHandComponent : public UOculusXRHandComponent { GENERATED_BODY() public: virtual void BeginPlay() override; virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; UFUNCTION(BlueprintCallable) void SetPose(FString PoseString, float LerpSpeedOverride = -1.f); UFUNCTION(BlueprintCallable) void ClearPose(float LerpSpeedOverride = -1.f); UPROPERTY(EditAnywhere, BlueprintReadWrite) float DefaultLerpSpeed = 10.f; protected: bool bCustomPoseableHandMesh = false; private: void UpdateBonePose(EOculusXRBone Bone, ERecognizedBone PosedBone); float CustomPoseWeightCurrent; float CustomPoseWeightTarget; float CustomPoseWeightLerpSpeed; FHandPose CustomPose; }; ================================================ FILE: Plugins/OculusHandTools/Source/OculusInteractable/OculusInteractable.Build.cs ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. using UnrealBuildTool; public class OculusInteractable : ModuleRules { public OculusInteractable (ReadOnlyTargetRules Target) : base(Target) { PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; PublicIncludePaths.AddRange( new string[] { // ... add public include paths required here ... } ); PrivateIncludePaths.AddRange( new string[] { // ... add other private include paths required here ... } ); PublicDependencyModuleNames.AddRange( new string[] { "Core", // ... add other public dependencies that you statically link with here ... } ); PrivateDependencyModuleNames.AddRange( new string[] { "CoreUObject", "Engine", "Slate", "SlateCore", "OculusXRInput", "OculusUtils", } ); DynamicallyLoadedModuleNames.AddRange( new string[] { // ... add any modules that your module loads dynamically here ... } ); IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_6; } } ================================================ FILE: Plugins/OculusHandTools/Source/OculusInteractable/Private/AimingActor.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "AimingActor.h" // Sets default values AAimingActor::AAimingActor() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; } // Called when the game starts or when spawned void AAimingActor::BeginPlay() { Super::BeginPlay(); } // Called every frame void AAimingActor::Tick(float DeltaTime) { Super::Tick(DeltaTime); } // Called when the actor is activated void AAimingActor::Activate_Implementation() { SetActorHiddenInGame(false); } // Called when the actor is deactivated void AAimingActor::Deactivate_Implementation(AInteractable* Selected) { SetActorHiddenInGame(true); } ================================================ FILE: Plugins/OculusHandTools/Source/OculusInteractable/Private/HandGrabbingComponent.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "HandGrabbingComponent.h" #include "Interactable.h" #include "Engine/OverlapResult.h" AInteractable* UHandGrabbingComponent::TryGrab(FTransform GrabTransform) { auto HitResults = TArray{}; auto QueryParams = FCollisionQueryParams::DefaultQueryParam; auto ResponseParams = FCollisionResponseParams(ECR_Overlap); GetWorld()->OverlapMultiByChannel(HitResults, GrabTransform.GetLocation(), GrabTransform.GetRotation(), ECC_WorldDynamic, FCollisionShape::MakeCapsule(GrabCapsuleRadius, GrabCapsuleHeight / 2.f), FCollisionQueryParams::DefaultQueryParam, ResponseParams); if (HitResults.Num() > 0) { auto ClosestInteractable = (AInteractable*)nullptr; auto DistanceToClosestInteractable = 0.0f; for (auto&& HitResult : HitResults) { if (auto HitActor = HitResult.GetActor()) { auto Interactable = Cast(HitActor); if (Interactable && Interactable->IsMovable()) { auto Distance = FVector::Dist(GrabTransform.GetLocation(), Interactable->GetActorLocation()); if (ClosestInteractable == nullptr || Distance < DistanceToClosestInteractable) { ClosestInteractable = Interactable; DistanceToClosestInteractable = Distance; } } } } if (ClosestInteractable) { auto InteractableRoot = ClosestInteractable->GetRootComponent(); if (auto OtherHand = Cast(InteractableRoot->GetAttachParent())) { OtherHand->TryRelease(); } if (InteractableRoot != nullptr) { auto GrabbedPrimitive = Cast(ClosestInteractable->GetRootComponent()); if(GrabbedPrimitive != nullptr && GrabbedPrimitive->IsSimulatingPhysics()) { bGrabbedActorHasPhysics = true; ClosestInteractable->SetInteractablePhysicsSimulation(false); } ClosestInteractable->Interaction1(); GrabbedActor = ClosestInteractable; GrabbedActor->OnDestroyed.AddDynamic(this, &UHandGrabbingComponent::HandleHeldActorDestroyed); } } } return GrabbedActor; } AInteractable* UHandGrabbingComponent::TryRelease(bool bReenablePhysics) { auto const ReleasedActor = GrabbedActor; if (GrabbedActor != nullptr) { GrabbedActor->OnDestroyed.RemoveDynamic(this, &UHandGrabbingComponent::HandleHeldActorDestroyed); if(bReenablePhysics && bGrabbedActorHasPhysics) { GrabbedActor->SetInteractablePhysicsSimulation(true); } GrabbedActor = nullptr; bGrabbedActorHasPhysics = false; } return ReleasedActor; } void UHandGrabbingComponent::HandleHeldActorDestroyed(AActor* DestroyedActor) { if (DestroyedActor == GrabbedActor) { TryRelease(); } } ================================================ FILE: Plugins/OculusHandTools/Source/OculusInteractable/Private/Interactable.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "Interactable.h" #include "InteractableSelector.h" #include "TransformString.h" #include "OculusInteractableModule.h" AInteractable::AInteractable() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; // Defaults. FarFieldSelectionDelayMs = 100.0f; } void AInteractable::BeginPlay() { Super::BeginPlay(); } void AInteractable::EndPlay(const EEndPlayReason::Type EndPlayReason) { while (Selectors.Num() > 0) { auto Selector = Selectors.Pop(EAllowShrinking::No); Selector->SetSelectedInteractable(nullptr, false); // Remove with no notification. } Super::EndPlay(EndPlayReason); } void AInteractable::Tick(float DeltaTime) { Super::Tick(DeltaTime); } void AInteractable::BeginSelection_Implementation(AInteractableSelector* Selector) { // Meant to be subclassed, but do not forget to call Super::BeginSelection_Implementation. if (!Selectors.Contains(Selector)) { Selectors.Add(Selector); } } void AInteractable::EndSelection_Implementation(AInteractableSelector* Selector) { // Meant to be subclassed, but do not forget to call Super::EndSelection_Implementation. if (Selectors.Contains(Selector)) { Selectors.Remove(Selector); } } bool AInteractable::IsSelected() const { return Selectors.Num() > 0; } const TArray& AInteractable::GetSelectors() const { return Selectors; } void AInteractable::SetInteractablePhysicsSimulation_Implementation(bool SimulatePhysics) { auto RootPrimitive = Cast(GetRootComponent()); if (RootPrimitive) { RootPrimitive->SetSimulatePhysics(SimulatePhysics); } } bool AInteractable::IsMovable_Implementation() { auto const RootPrimitive = Cast(GetRootComponent()); if (RootPrimitive) { return RootPrimitive->Mobility == EComponentMobility::Movable; } return false; } void AInteractable::SelectGrabPose(EHandSide Side, bool& GrabPoseFound, FString& GrabPoseName, FTransform& GrabTransform, FString& GrabHandPose) { auto& GrabPoses = Side == EHandSide::HandLeft ? GrabPosesLeftHand : GrabPosesRightHand; GrabPoseFound = GrabPoses.Num() > 0; if (GrabPoseFound) { // Random selection for this first pass. auto const RandomGrabIndex = FMath::RandRange(0, GrabPoses.Num() - 1); auto const EncodedTransform = GrabPoses[RandomGrabIndex].RelativeHandTransform; if (FTransformString::StringToTransform(EncodedTransform, GrabTransform)) { GrabPoseName = GrabPoses[RandomGrabIndex].PoseName; GrabHandPose = GrabPoses[RandomGrabIndex].HandPose; } else { UE_LOG(LogInteractable, Warning, TEXT("Invalid grab transform on %s, %s side at index %d: \"%s\""), *GetHumanReadableName(), Side == EHandSide::HandLeft ? TEXT("left") : TEXT("right"), RandomGrabIndex, *EncodedTransform); GrabPoseFound = false; } } } ================================================ FILE: Plugins/OculusHandTools/Source/OculusInteractable/Private/InteractableFunctionLibrary.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "InteractableFunctionLibrary.h" #include "OculusInteractableModule.h" #include "TransformString.h" ECollisionChannel UInteractableFunctionLibrary::InteractableTraceChannel = ECC_GameTraceChannel1; void UInteractableFunctionLibrary::LogNearbyInteractables(AActor* ReferenceActor, float RadiusAroundActor) { auto World = ReferenceActor ? ReferenceActor->GetWorld() : nullptr; if (!World) return; auto CollisionSphere = FCollisionShape::MakeSphere(RadiusAroundActor); TArray Hits; auto ReferenceTransform = ReferenceActor->GetTransform(); auto Loc = ReferenceTransform.GetLocation(); World->SweepMultiByChannel(Hits, Loc, Loc, FQuat::Identity, InteractableTraceChannel, CollisionSphere); if (Hits.Num() > 0) { UE_LOG(LogInteractable, Error, TEXT("Interactables near %s radius %0.2f"), *ReferenceActor->GetHumanReadableName(), RadiusAroundActor); for (const auto& Hit : Hits) { auto ActorTransform = Hit.GetActor()->GetTransform(); FTransform RelativeActorTransform; RelativeActorTransform.SetLocation(ReferenceTransform.InverseTransformPosition(ActorTransform.GetLocation())); RelativeActorTransform.SetRotation(ReferenceTransform.InverseTransformRotation(ActorTransform.GetRotation())); RelativeActorTransform.SetScale3D(ActorTransform.GetScale3D()); FString RelativeActorTransformString; FTransformString::TransformToString(RelativeActorTransform, RelativeActorTransformString); UE_LOG(LogInteractable, Warning, TEXT("Interactable %s %s"), *Hit.GetActor()->GetHumanReadableName(), *RelativeActorTransformString); } } } ================================================ FILE: Plugins/OculusHandTools/Source/OculusInteractable/Private/InteractableSelector.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "InteractableSelector.h" #include "DrawDebugHelpers.h" #include "Kismet/GameplayStatics.h" ECollisionChannel AInteractableSelector::InteractableTraceChannel = ECC_GameTraceChannel1; FName AInteractableSelector::BeamSource("Source"); FName AInteractableSelector::BeamTarget("Target"); AInteractableSelector::AInteractableSelector() { // Property defaults bSelectorStartsActivated = false; NearFieldRadius = 10.0f; RaycastOffset = 0.0f; RaycastDistance = 1000.0f; RaycastAngleDegrees = 15.0f; RaycastStickinessAngleDegrees = 20.0f; DampeningFactor = 0.95f; AimingActorRotationRate = 0.1f; bRaycastDebugTrace = false; // State bAlignAimingActorWithHitNormal = false; bSelectorActivated = false; bAimingActorOwned = false; CandidatePreSelection = nullptr; CandidatePreSelectionTimeMs = 0.0f; SelectedInteractable = nullptr; PrimaryActorTick.bCanEverTick = true; // Root this->RootComponent = CreateDefaultSubobject(TEXT("Root")); #if WITH_EDITOR ArrowComponent = CreateEditorOnlyDefaultSubobject(TEXT("Arrow")); if (!IsRunningCommandlet()) { if (ArrowComponent) { ArrowComponent->SetupAttachment(RootComponent); ArrowComponent->ArrowColor = FColor(128, 92, 207); ArrowComponent->ArrowSize = 1.0f; ArrowComponent->bIsScreenSizeScaled = true; } } #endif //WITH_EDITOR } void AInteractableSelector::BeginPlay() { Super::BeginPlay(); BuildAimingActor(); BuildBeam(); if (bSelectorStartsActivated) { ActivateSelector(true); } } void AInteractableSelector::EndPlay(EEndPlayReason::Type const EndPlayReason) { ActivateSelector(false); DestroyBeam(); DestroyAimingActor(); SetSelectedInteractable(nullptr); // Notify selected interactable that we are going away. Super::EndPlay(EndPlayReason); } void AInteractableSelector::Tick(float DeltaTime) { Super::Tick(DeltaTime); auto TestShouldSelect = [this](TWeakObjectPtr Actor) { return ShouldSelect(Cast(Actor.Get())); }; if (bSelectorActivated) { auto const World = GetWorld(); if (!World) { return; } AInteractable* Candidate = nullptr; auto CandidateInNearField = false; FVector StartCast, EndCast; ComputeNearFieldRaycastEndpoints(StartCast, EndCast); TArray Hits; auto UpdateAimingActorTransform = [&] { World->LineTraceMultiByChannel(Hits, StartCast, EndCast, ECC_Visibility); auto const AimingQuat = AimingActor->GetActorQuat(); // Default target aiming when we have not hit normal. auto const RightVector = FVector::CrossProduct(DampenedForwardVector, FVector::UpVector); auto ForwardVector = FVector::CrossProduct(StartCast - EndCast, RightVector); auto TargetAimingQuat = FQuat(FRotationMatrix::MakeFromXZ(ForwardVector, StartCast - EndCast).Rotator()); if (Hits.Num() > 0) { NonInteractableActorHit = Hits[0].GetActor(); if (bAlignAimingActorWithHitNormal) { // Align with hit normal. ForwardVector = FVector::CrossProduct(Hits[0].ImpactNormal, RightVector); TargetAimingQuat = FQuat(FRotationMatrix::MakeFromXZ(ForwardVector, Hits[0].ImpactNormal).Rotator()); } AimingActor->SetActorLocation(Hits[0].ImpactPoint); } else { // Place the actor at the end of the cast of nothing is hit. AimingActor->SetActorLocation(EndCast); } auto const LerpedTargetAimingQuat = FQuat::FastLerp(AimingQuat, TargetAimingQuat, AimingActorRotationRate); AimingActor->SetActorRotation(LerpedTargetAimingQuat); }; // Near-field selection has priority. if (NearFieldRadius > 0.0f) { auto const NearFieldCollisionSphere = FCollisionShape::MakeSphere(NearFieldRadius); auto CandidateDistance = NearFieldRadius * 100.0f; World->SweepMultiByChannel(Hits, StartCast, StartCast, FQuat::Identity, InteractableTraceChannel, NearFieldCollisionSphere); if (Hits.Num() > 0) { for (auto const& Hit : Hits) { if (Hit.GetActor() && Hit.GetActor()->GetClass()->IsChildOf(AInteractable::StaticClass()) && TestShouldSelect(Hit.GetActor())) { auto const Distance = FVector::Distance(Hit.GetActor()->GetActorLocation(), StartCast); if (CandidateDistance > Distance) { CandidateDistance = Distance; Candidate = Cast(Hit.GetActor()); CandidateInNearField = true; } } // UE_LOG(LogTemp, Error, TEXT("Hit near field %s at %0.0f"), *Hit.GetActor()->GetName(), CandidateDistance); } } } // If no candidates found in the near-field, we perform far-field selection. if (!Candidate) { UpdateDampenedForwardVector(DampeningFactor); ComputeFarFieldRaycastEndpoints(StartCast, EndCast); if (bRaycastDebugTrace) { DrawDebugLine(World, StartCast, EndCast, FColor::Green, false, -1.0f, 0, 0.1f); } // Trace in a cone against interactable. auto const SphereRadius = ComputeSphereRadiusForCast(); auto const CollisionSphere = FCollisionShape::MakeSphere(SphereRadius); World->SweepMultiByChannel(Hits, StartCast, EndCast, FQuat::Identity, InteractableTraceChannel, CollisionSphere); // Looking for the closest candidate by angle and distance. auto CandidateAngle = RaycastAngleDegrees; auto CandidateDistance = RaycastDistance; if (Hits.Num() > 0) { for (auto const& Hit : Hits) { // GEngine->AddOnScreenDebugMessage(-1, 0, FColor::Blue, FString::Printf(TEXT("Hit %s at %f"), *Hit.GetActor()->GetName(), Hit.Distance)); if (Hit.GetActor() && Hit.GetActor()->GetClass()->IsChildOf(AInteractable::StaticClass()) && TestShouldSelect(Hit.GetActor())) { auto const Angle = ComputeAngularDistance(Hit.GetActor()); // Since CandidateAngle starts at RaycastAngleDegrees, we cannot select outside of the cone. if (Angle > CandidateAngle || Angle == CandidateAngle && Hit.Distance >= CandidateDistance) { continue; } CandidateAngle = Angle; CandidateDistance = Hit.Distance; Candidate = Cast(Hit.GetActor()); } } } // Do not change the selection if it is within some angle. if (SelectedInteractable != nullptr && Candidate != nullptr) { auto const AngleToCurrentSelection = ComputeAngularDistance(SelectedInteractable); if (AngleToCurrentSelection <= RaycastStickinessAngleDegrees && AngleToCurrentSelection < ComputeAngularDistance(Candidate)) { // Re-orient the beam. OrientBeam(); if (bAlwaysShowAimingActor) { UpdateAimingActorTransform(); } return; } } } if (!Candidate || bAlwaysShowAimingActor) { if (AimingActor) { // If there are no hits, we help by displaying the aiming actor. ActivateAimingActor(true); UpdateAimingActorTransform(); } } else { // Do no show the aiming actor when we hit an interactable. ActivateAimingActor(false); // When we have a candidate, this first non-interactable actor hit is cleared. NonInteractableActorHit.Reset(); } // Near-field selection is immediate. , otherwise we update selection time on candidate in pre-selection. if (CandidateInNearField) { CandidatePreSelection = Candidate; CandidatePreSelectionTimeMs = Candidate->FarFieldSelectionDelayMs; } else if (CandidatePreSelection != Candidate) { CandidatePreSelection = Candidate; CandidatePreSelectionTimeMs = 0.0f; } else { CandidatePreSelectionTimeMs += DeltaTime * 1000.0f; } SetSelectedInteractable(CandidatePreSelection, CandidatePreSelectionTimeMs, true, CandidateInNearField); } } void AInteractableSelector::SetSelectedInteractable(AInteractable* Candidate, float SelectionDurationMs /* = 0.0f */, bool Notify /* = true */, bool CandidateInNearField /* = false */) { auto const SameCandidate = SelectedInteractable == Candidate; auto const SameField = SelectedInteractableInNearField == CandidateInNearField; // Old interactable. if (SelectedInteractable) { if (SameCandidate) { // Same non-null target in the same field: just need to update beam orientation. if (SameField) { if (!CandidateInNearField) { OrientBeam(); } return; } } else { if (Notify) { SelectedInteractable->EndSelection(this); } } } // Activating beam on the candidate. TargetBeam(CandidateInNearField ? nullptr : Candidate); // Swapping to candidate. if (Candidate && Candidate->FarFieldSelectionDelayMs <= SelectionDurationMs) { SelectedInteractable = Candidate; SelectedInteractableInNearField = CandidateInNearField; } else { SelectedInteractable = nullptr; } // New interactable (potentially nullptr). if (SelectedInteractable && !SameCandidate && Notify) { SelectedInteractable->BeginSelection(this); } } AInteractable* AInteractableSelector::GetSelectedInteractable(bool& SelectedInNearField) const { SelectedInNearField = SelectedInteractableInNearField; return SelectedInteractable; } AActor* AInteractableSelector::GetNonInteractableHit() const { // Can return null. return NonInteractableActorHit.Get(); } bool AInteractableSelector::ShouldSelect_Implementation(AInteractable* Interactable) const { return true; } void AInteractableSelector::ActivateSelector(bool Activate) { if (bSelectorActivated != Activate) { if (Activate) { UpdateDampenedForwardVector(0.0f); } else { SetSelectedInteractable(nullptr); ActivateAimingActor(false); } bSelectorActivated = Activate; } } void AInteractableSelector::UpdateDampenedForwardVector(float Dampening) { DampenedForwardVector *= Dampening; DampenedForwardVector += GetActorForwardVector() * (1.0f - Dampening); DampenedForwardVector.Normalize(); } void AInteractableSelector::ComputeNearFieldRaycastEndpoints(FVector& Start, FVector& End) const { auto const ActorPos = GetActorLocation(); auto const ActorFwd = GetActorForwardVector(); Start = ActorPos - ActorFwd * NearFieldRadius; End = ActorPos + ActorFwd * NearFieldRadius; } void AInteractableSelector::ComputeFarFieldRaycastEndpoints(FVector& Start, FVector& End) const { auto const ActorPos = GetActorLocation(); auto const ActorFwd = DampenedForwardVector; Start = ActorPos + ActorFwd * RaycastOffset; End = ActorPos + ActorFwd * (RaycastOffset + RaycastDistance); } float AInteractableSelector::ComputeSphereRadiusForCast() const { return RaycastDistance * FMath::Tan(FMath::DegreesToRadians(RaycastAngleDegrees * 0.5f)); } float AInteractableSelector::ComputeAngularDistance(AActor* Target) const { auto const LineStart = GetActorLocation(); auto const LineEnd = GetActorLocation() + 100.0f * DampenedForwardVector; // Any point will do, but projecting forward will reduce errors. auto const TargetLocation = Target->GetActorLocation(); auto const NearestLocation = FMath::ClosestPointOnInfiniteLine(LineStart, LineEnd, TargetLocation); auto const DistNearestToUs = FVector::Distance(NearestLocation, LineStart); auto const DistNearestToTargetCenter = FVector::Distance(NearestLocation, TargetLocation); auto const DistNearestToTarget = FMath::Max(DistNearestToTargetCenter - Target->GetSimpleCollisionRadius() * 0.5f, 0.0f); auto const AngleToTargetRadians = FMath::FastAsin(DistNearestToTarget / DistNearestToUs); return FMath::RadiansToDegrees(AngleToTargetRadians); } void AInteractableSelector::ActivateAimingActor(bool Activate) const { if (!AimingActor) { return; } if (Activate) { // This is one of AAimingActor's events. AimingActor->Activate(); } else { // This is one of AAimingActor's events. AimingActor->Deactivate(SelectedInteractable); } } void AInteractableSelector::BuildAimingActor() { // Minimum handling of multiplayer: don't spawn aiming actor if we are not on the server. if (!HasAuthority()) { return; } auto World = GetWorld(); if (!World) { return; } if (!AimingActor && AimingActorClass) { FActorSpawnParameters Params; Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; Params.bAllowDuringConstructionScript = true; Params.Owner = this; // Spawn actor of desired class. auto Location = GetActorLocation(); auto Rotation = GetActorRotation(); AimingActor = Cast(World->SpawnActor(AimingActorClass, &Location, &Rotation, Params)); if (!AimingActor) { UE_LOG(LogTemp, Error, TEXT("Could not spawn aiming actor of class %s for interactable selector %s"), *AimingActorClass->GetName(), *GetName()); } else { bAimingActorOwned = true; AimingActor->SetActorTickEnabled(true); AimingActor->SetActorEnableCollision(false); } } } void AInteractableSelector::DestroyAimingActor() { auto World = GetWorld(); if (!World) { return; } if (bAimingActorOwned && AimingActor->HasAuthority() && IsValid(AimingActor) && !AimingActor->IsUnreachable()) { World->DestroyActor(AimingActor); AimingActor = nullptr; bAimingActorOwned = false; } } void AInteractableSelector::TargetBeam(AActor* Target) { if (!Beam) { return; } if (Target) { Beam->SetActorParameter(BeamSource, this); Beam->SetActorParameter(BeamTarget, Target); Beam->SetComponentTickEnabled(true); Beam->SetHiddenInGame(false); OrientBeam(); } else { Beam->SetHiddenInGame(true); Beam->SetComponentTickEnabled(false); } } void AInteractableSelector::OrientBeam() const { if (Beam) { auto const BeamTangent = DampenedForwardVector; // UE_LOG(LogTemp, Error, TEXT("*** BEAM UPDATE %0.2f %0.2f %0.2f"), DampenedForwardVector.X, DampenedForwardVector.Y, DampenedForwardVector.Z); Beam->SetBeamSourceTangent(0, BeamTangent, 0); } } void AInteractableSelector::BuildBeam() { if (BeamTemplate) { Beam = UGameplayStatics::SpawnEmitterAttached(BeamTemplate, this->GetRootComponent()); TargetBeam(nullptr); } } void AInteractableSelector::DestroyBeam() { if (Beam) { Beam->DestroyComponent(); Beam = nullptr; } } ================================================ FILE: Plugins/OculusHandTools/Source/OculusInteractable/Private/OculusInteractableModule.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "OculusInteractableModule.h" #define LOCTEXT_NAMESPACE "FOculusInteractableModule" #include "OculusDeveloperTelemetry.h" OCULUS_TELEMETRY_LOAD_MODULE("Unreal-OculusInteractable"); DEFINE_LOG_CATEGORY(LogInteractable); void FOculusInteractableModule::StartupModule() { // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module } void FOculusInteractableModule::ShutdownModule() { // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, // we call this function before unloading the module. } #undef LOCTEXT_NAMESPACE IMPLEMENT_MODULE(FOculusInteractableModule, OculusInteractable) ================================================ FILE: Plugins/OculusHandTools/Source/OculusInteractable/Private/TransformString.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "TransformString.h" void FTransformString::TransformToString(FTransform Transform, FString& String) { auto const Location = Transform.GetLocation(); auto const Rotation = Transform.GetRotation(); auto const Scale = Transform.GetScale3D(); String = FString::Printf( TEXT("LOC X%0.3f Y%0.3f Z%0.3f ROT W%0.3f X%0.3f Y%0.3f Z%0.3f"), Location.X, Location.Y, Location.Z, Rotation.W, Rotation.X, Rotation.Y, Rotation.Z); float const Tolerance = 0.01; if (FMath::Abs(Scale.X - 1.0f) > Tolerance || FMath::Abs(Scale.Y - 1.0f) > Tolerance || FMath::Abs(Scale.Z - 1.0f) > Tolerance) { String.Append(FString::Printf( TEXT(" SCA X%0.3f Y%0.3f Z%0.3f"), Scale.X, Scale.Y, Scale.Z)); } } bool FTransformString::StringToTransform(FString String, FTransform& Transform) { FVector Location; FQuat Rotation; FVector Scale(1.0f, 1.0f, 1.0f); TArray Elems; auto const NumElems = String.ParseIntoArray(Elems, TEXT(" "), true); if (NumElems != 9 && NumElems != 13) return false; if (Elems[0] != TEXT("LOC")) return false; if (!ReadFloat(Elems[1], TEXT("X"), Location.X)) return false; if (!ReadFloat(Elems[2], TEXT("Y"), Location.Y)) return false; if (!ReadFloat(Elems[3], TEXT("Z"), Location.Z)) return false; if (Elems[4] != TEXT("ROT")) return false; if (!ReadFloat(Elems[5], TEXT("W"), Rotation.W)) return false; if (!ReadFloat(Elems[6], TEXT("X"), Rotation.X)) return false; if (!ReadFloat(Elems[7], TEXT("Y"), Rotation.Y)) return false; if (!ReadFloat(Elems[8], TEXT("Z"), Rotation.Z)) return false; if (NumElems == 13) { if (Elems[9] != TEXT("SCA")) return false; if (!ReadFloat(Elems[10], TEXT("X"), Scale.X)) return false; if (!ReadFloat(Elems[11], TEXT("Y"), Scale.Y)) return false; if (!ReadFloat(Elems[12], TEXT("Z"), Scale.Z)) return false; } Transform.SetLocation(Location); Transform.SetRotation(Rotation); Transform.SetScale3D(Scale); return true; } ================================================ FILE: Plugins/OculusHandTools/Source/OculusInteractable/Private/TransformString.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" /** * Class for transform encoding. */ class FTransformString { public: /** * Transform to String. * @param Transform - Transform to convert to string. * @param String - Output string. */ static void TransformToString(FTransform Transform, FString& String); /** * String to Transform. * @param String - String encoded transform to convert to Transform. * @param Transform - Decoded Transform. * @return bool - Whether the encoded string is proper. */ static bool StringToTransform(FString String, FTransform& Transform); private: /** * Parsing float from string, with expected prefix. * @param Source - Transform sub-string of the form . * @param ExpectedPrefix - The expected one-letter prefix for this sub-field. * @param Destination - Output float. * @return bool - True if prefix matches. */ static bool ReadFloat(FString& Source, const TCHAR* ExpectedPrefix, double& Destination) { if (**Source != *ExpectedPrefix) return false; Destination = FCString::Atof(*Source + 1); return true; } }; ================================================ FILE: Plugins/OculusHandTools/Source/OculusInteractable/Public/AimingActor.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "AimingActor.generated.h" UCLASS() class OCULUSINTERACTABLE_API AAimingActor : public AActor { GENERATED_BODY() public: /** Sets default values for this actor's properties. */ AAimingActor(); protected: /** Called when the game starts or when spawned. */ virtual void BeginPlay() override; public: /** Called every frame. */ virtual void Tick(float DeltaTime) override; /** * Called by the selector when the aiming actor is activated. * By default, it makes the actor visible. */ UFUNCTION(BlueprintNativeEvent) void Activate(); /** * Called by the selector when the aiming actor is deactivated. * By default, it makes the actor invisible. * @param Selected - Interactable selected, if any. */ UFUNCTION(BlueprintNativeEvent) void Deactivate(AInteractable* Selected); }; ================================================ FILE: Plugins/OculusHandTools/Source/OculusInteractable/Public/HandGrabbingComponent.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" #include "Interactable.h" #include "OculusXRInputFunctionLibrary.h" #include "HandGrabbingComponent.generated.h" class OCULUSINTERACTABLE_API AInteractable; UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) class OCULUSINTERACTABLE_API UHandGrabbingComponent : public USceneComponent { GENERATED_BODY() public: UFUNCTION(BlueprintCallable, Category = "Grabbing") AInteractable* TryGrab(FTransform GrabTransform); UFUNCTION(BlueprintCallable, Category = "Grabbing") AInteractable* TryRelease(bool bReenablePhysics = true); UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Grabbing") EOculusXRHandType Hand = EOculusXRHandType::HandLeft; UPROPERTY(BlueprintReadOnly, Transient, Category = "Grabbing") AInteractable* GrabbedActor = nullptr; UPROPERTY(BlueprintReadOnly, Transient, Category = "Grabbing") bool bGrabbedActorHasPhysics = false; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Grabbing") float GrabCapsuleHeight = 30.f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Grabbing") float GrabCapsuleRadius = 20.f; private: UFUNCTION() void HandleHeldActorDestroyed(AActor* DestroyedActor); }; ================================================ FILE: Plugins/OculusHandTools/Source/OculusInteractable/Public/Interactable.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "InteractablePose.h" #include "Interactable.generated.h" class AInteractableSelector; UENUM(BlueprintType) enum class EHandSide : uint8 { HandLeft, HandRight }; /** Base actor class of interactable objects. */ UCLASS() class OCULUSINTERACTABLE_API AInteractable : public AActor { GENERATED_BODY() public: /** Sets default values for this actor's properties. */ AInteractable(); protected: /** Called when the game starts or when spawned. */ virtual void BeginPlay() override; /** Called when this actor is being removed from the level. */ virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; public: /** Called every frame. */ virtual void Tick(float DeltaTime) override; /** Minimum time required for the selector to stay on this object before it can be selected in the far-field. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interactable Grab Pose") float FarFieldSelectionDelayMs; /** * Event fired when selection starts. * Can only be called from C++. * @param Selector - The Interactable Selector that just selected us. */ UFUNCTION(BlueprintNativeEvent) void BeginSelection(AInteractableSelector* Selector); /** * Event fired when selection ends. * Can only be called from C++. * @param Selector - The Interactable Selector that stopped selecting us. */ UFUNCTION(BlueprintNativeEvent) void EndSelection(AInteractableSelector* Selector); /** * Call to check if you are currently selected. * @return boolean */ UFUNCTION(BlueprintCallable) bool IsSelected() const; /** * Optional generic user event. * The meaning, implementation and invocation is left to the user. */ UFUNCTION(BlueprintCallable, BlueprintImplementableEvent) void Interaction1(); /** * Optional generic user event. * The meaning, implementation and invocation is left to the user. */ UFUNCTION(BlueprintCallable, BlueprintImplementableEvent) void Interaction2(); /** * Optional generic user event. * The meaning, implementation and invocation is left to the user. */ UFUNCTION(BlueprintCallable, BlueprintImplementableEvent) void Interaction3(); /** * Call to get all selectors currently selecting us. * @return An array of Interactable Selectors. */ const TArray& GetSelectors() const; /** * Method called to turn on/off physics on interactable. * By default it checks if the root component is a primitive and applies the change there. * You can override this method in blueprint for special cases. * @param SimulatePhysics - boolean. */ UFUNCTION(BlueprintCallable, BlueprintNativeEvent) void SetInteractablePhysicsSimulation(bool SimulatePhysics); /** * Method to check if object is movable. * By default it checks if the root component is movable. * You can override this method in blueprint for special cases. */ UFUNCTION(BlueprintCallable, BlueprintNativeEvent) bool IsMovable(); /** Hand poses when grabbed. */ UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Interactable Grab Pose") TArray GrabPosesLeftHand; /** Hand poses when grabbed. */ UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Interactable Grab Pose") TArray GrabPosesRightHand; /** * Selects the best grab pose for the interactable, if any. * @param Side - EOculusXRHandType to select left or right hand. * @param GrabPoseFound - returns a boolean indicating if a grab pose was found. * @param GrabPoseName - if GrabPoseFound, returns the hand pose while grabbing. * @param GrabTransform - if GrabPoseFound, returns the Interactable's transform relative to the hand. * @param GrabHandPose - returns the grab pose string */ UFUNCTION(BlueprintCallable, Category = "Interactable Grab Pose") void SelectGrabPose(EHandSide Side, bool& GrabPoseFound, FString& GrabPoseName, FTransform& GrabTransform, FString& GrabHandPose); protected: /** List of selectors currently selecting us. */ TArray Selectors; }; ================================================ FILE: Plugins/OculusHandTools/Source/OculusInteractable/Public/InteractableFunctionLibrary.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" #include "InteractableFunctionLibrary.generated.h" UCLASS() class OCULUSINTERACTABLE_API UInteractableFunctionLibrary : public UBlueprintFunctionLibrary { GENERATED_BODY() public: /** * Locate nearby Interactables and give their relative position and orientation to * some reference object. * @param ReferenceActor * @param RadiusAroundActor */ UFUNCTION(BlueprintCallable, Category = "Interactable") static void LogNearbyInteractables(AActor* ReferenceActor, float RadiusAroundActor); private: static ECollisionChannel InteractableTraceChannel; }; ================================================ FILE: Plugins/OculusHandTools/Source/OculusInteractable/Public/InteractablePose.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" #include "InteractablePose.generated.h" /** A struct that represents a hand pose. */ USTRUCT(BlueprintType) struct OCULUSINTERACTABLE_API FInteractablePose { GENERATED_BODY() /** Name for this pose. */ UPROPERTY(Category = "Interactable Grab Pose", EditAnywhere, BlueprintReadWrite) FString PoseName; /** Hand pose. */ UPROPERTY(Category = "Interactable Grab Pose", EditAnywhere, BlueprintReadWrite) FString HandPose; /** Relative hand transform. */ UPROPERTY(Category = "Interactable Grab Pose", EditAnywhere, BlueprintReadWrite) FString RelativeHandTransform; }; ================================================ FILE: Plugins/OculusHandTools/Source/OculusInteractable/Public/InteractableSelector.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "Components/ArrowComponent.h" #include "Particles/ParticleSystemComponent.h" #include "Interactable.h" #include "AimingActor.h" #include "InteractableSelector.generated.h" /** * Base actor class for interactable selectors. */ UCLASS() class OCULUSINTERACTABLE_API AInteractableSelector : public AActor { friend class AInteractable; GENERATED_BODY() public: /** Sets default values for this actor's properties. */ AInteractableSelector(); /** Arrow component to indicate forward direction of selection. */ #if WITH_EDITORONLY_DATA private: UPROPERTY() class UArrowComponent* ArrowComponent; public: #endif /** Initial activation state of the selector. */ UPROPERTY(Category = "Selector", EditAnywhere, BlueprintReadWrite) bool bSelectorStartsActivated; /** Radius of near-field selection around the selector. Disabled if <= 0.0. */ UPROPERTY(Category = "Selector", EditAnywhere, BlueprintReadWrite) float NearFieldRadius; /** Distance from selector at which we start raycasting. */ UPROPERTY(Category = "Selector", EditAnywhere, BlueprintReadWrite) float RaycastOffset; /** Raycast range. */ UPROPERTY(Category = "Selector", EditAnywhere, BlueprintReadWrite) float RaycastDistance; /** Raycast angle: we select within a cone. */ UPROPERTY(Category = "Selector", EditAnywhere, BlueprintReadWrite) float RaycastAngleDegrees; /** Raycast stickiness angle: we stick to a selection while we do not exceed this angle. */ UPROPERTY(Category = "Selector", EditAnywhere, BlueprintReadWrite) float RaycastStickinessAngleDegrees; /** Align aiming actor with hit normal. */ UPROPERTY(Category = "Selector", EditAnywhere, BlueprintReadWrite) bool bAlignAimingActorWithHitNormal; /** Aiming actor placed at targetting location. */ UPROPERTY(Category = "Selector", EditAnywhere, BlueprintReadWrite) AAimingActor* AimingActor; /** Aiming actor class to spawn if no AimingActor is specified. */ UPROPERTY(Category = "Selector", EditAnywhere, BlueprintReadWrite) TSubclassOf AimingActorClass; /** Particle system for beam. */ UPROPERTY(Category = "Selector", EditAnywhere, BlueprintReadOnly) UParticleSystem* BeamTemplate; /** Aiming actor rotation rate. */ UPROPERTY(Category = "Selector", EditAnywhere, BlueprintReadWrite) bool bAlwaysShowAimingActor = false; /** Aiming actor rotation rate. */ UPROPERTY(Category = "Selector", EditAnywhere, BlueprintReadWrite) float AimingActorRotationRate; /** Aiming using a number of samples for stabilization. */ UPROPERTY(Category = "Selector", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "0.0", ClampMax = "1.0", UIMin = "0.0", UIMax = "1.0")) float DampeningFactor; /** Raycast debug. */ UPROPERTY(Category = "Selector", EditAnywhere, BlueprintReadWrite) bool bRaycastDebugTrace; UFUNCTION(Category = "Selector", BlueprintNativeEvent) bool ShouldSelect(AInteractable* Interactable) const; /** * Call to activate / deactivate the selector. * @param Activate - A boolean. */ UFUNCTION(BlueprintCallable) void ActivateSelector(bool Activate); /** * Access to the currently selected interactable actor. * @param SelectedInNearField - When the return value is not null, indicates if selected in near field. * @return AInteractable or nullptr if none is selected. */ UFUNCTION(BlueprintCallable) AInteractable* GetSelectedInteractable(bool& SelectedInNearField) const; /** * Access any non-interactable actor hit by the selector. * @return AActor or nullptr if none is selected. */ UFUNCTION(BlueprintCallable) AActor* GetNonInteractableHit() const; /** Called every frame. */ virtual void Tick(float DeltaTime) override; protected: /** Called when the game starts or when spawned. */ virtual void BeginPlay() override; /** Called when this actor is being removed from the level. */ virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; /** Whether the selector is currently activated or not. */ bool bSelectorActivated; /** Dampened forward vector. */ FVector DampenedForwardVector; /** * Updates DampenedForwardVector. * @param Dampening - Fraction of the current vector to keep. */ void UpdateDampenedForwardVector(float Dampening); /** * Computes the current start and end locations for the near-field selector. * @param Start - Where to store the start location. * @param End - Where to store the end location. */ void ComputeNearFieldRaycastEndpoints(FVector& Start, FVector& End) const; /** * Computes the current start and end locations for the far-field selector. * @param Start - Where to store the start location. * @param End - Where to store the end location. */ void ComputeFarFieldRaycastEndpoints(FVector& Start, FVector& End) const; /** * Computes the radius of the sphere required to hit at RaycastDistance and RaycastAngle. * @return Radius of sphere. */ float ComputeSphereRadiusForCast() const; /** * Computes our approximative angular distance to an Actor. * It is approximative because we take the actor's bounding volume into consideration. * @param Target - The actor. * @return An approximative angular distance in degrees. */ float ComputeAngularDistance(AActor* Target) const; /** Candidate in pre-selection, when object has a FarFieldSelectionDelay > 0.0. */ UPROPERTY() AInteractable* CandidatePreSelection; float CandidatePreSelectionTimeMs; /** Interactable selected. */ UPROPERTY() AInteractable* SelectedInteractable; /** Interactable is in near-field. */ bool SelectedInteractableInNearField; /** If we hit a non-interactable object, we keep a reference to it. */ TWeakObjectPtr NonInteractableActorHit; /** * Implements changes of target. * @param Candidate - An interactable actor. * @param SelectionDurationMs - How long the candidate has been under selection in milliseconds. * @param Notify - Whether we should notify the old and/or new selections. * @param CandidateInNearField - Whether the selected interactable */ void SetSelectedInteractable(AInteractable* Candidate, float SelectionDurationMs = 0.0f, bool Notify = true, bool CandidateInNearField = false); /** * Call to change the activation of the aiming actor. * @param Activate - New activation state. */ void ActivateAimingActor(bool Activate) const; /** Do we own the aiming actor? */ bool bAimingActorOwned; /** Constructs the aiming actor, if necessary. */ void BuildAimingActor(); /** Destroys the aiming actor, if we own it. */ void DestroyAimingActor(); /** * Call to target the beam at an actor. * @param Target - Actor to target the beam to, or nullptr to deactivate. */ void TargetBeam(AActor* Target); /** Updates the beam start tangent. */ void OrientBeam() const; /** Constructs the selection beam, if a template was provided. */ void BuildBeam(); /** Destroys the selection beam, if one has been created. */ void DestroyBeam(); /** The beam particle system component created based on the BeatTemplate particle system. */ UPROPERTY() UParticleSystemComponent* Beam; private: static ECollisionChannel InteractableTraceChannel; static FName BeamSource; static FName BeamTarget; }; ================================================ FILE: Plugins/OculusHandTools/Source/OculusInteractable/Public/OculusInteractableModule.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" DECLARE_LOG_CATEGORY_EXTERN(LogInteractable, Log, All); class FOculusInteractableModule : public IModuleInterface { public: /** IModuleInterface implementation */ virtual void StartupModule() override; virtual void ShutdownModule() override; }; ================================================ FILE: Plugins/OculusHandTools/Source/OculusThrowAssist/OculusThrowAssist.Build.cs ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. using UnrealBuildTool; public class OculusThrowAssist : ModuleRules { public OculusThrowAssist (ReadOnlyTargetRules Target) : base(Target) { PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; PublicIncludePaths.AddRange( new string[] { // ... add public include paths required here ... } ); PrivateIncludePaths.AddRange( new string[] { // ... add other private include paths required here ... } ); PublicDependencyModuleNames.AddRange( new string[] { "Core", // ... add other public dependencies that you statically link with here ... } ); PrivateDependencyModuleNames.AddRange( new string[] { "CoreUObject", "Engine", "Slate", "SlateCore", "OculusUtils" } ); DynamicallyLoadedModuleNames.AddRange( new string[] { // ... add any modules that your module loads dynamically here ... } ); IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_6; } } ================================================ FILE: Plugins/OculusHandTools/Source/OculusThrowAssist/Private/OculusThrowAssistModule.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "OculusThrowAssistModule.h" #include "OculusDeveloperTelemetry.h" OCULUS_TELEMETRY_LOAD_MODULE("Unreal-OculusThrowAssist"); #define LOCTEXT_NAMESPACE "FOculusThrowAssistModule" DEFINE_LOG_CATEGORY(LogOculusThrowAssist); void FOculusThrowAssistModule::StartupModule() { // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module } void FOculusThrowAssistModule::ShutdownModule() { // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, // we call this function before unloading the module. } #undef LOCTEXT_NAMESPACE IMPLEMENT_MODULE(FOculusThrowAssistModule, OculusThrowAssist) ================================================ FILE: Plugins/OculusHandTools/Source/OculusThrowAssist/Private/ThrowingComponent.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "ThrowingComponent.h" #include "UObject/UObjectGlobals.h" #include "Engine/World.h" #include "DrawDebugHelpers.h" #include "TransformBufferComponent.h" static TAutoConsoleVariable CVarDebugDrawThrowingVector( TEXT("throw.DebugDrawThrowingVector"), 0, TEXT("Shows throwing vector for camera-tracked hands. 1- draw when throwing. 2- draw always"), ECVF_Cheat); UThrowingComponent::UThrowingComponent() { PrimaryComponentTick.bCanEverTick = true; } void UThrowingComponent::Initialize(USceneComponent* AttachParent) { auto const UniqueName = [&](auto&& Base) { return MakeUniqueObjectName(GetOwner(), UTransformBufferComponent::StaticClass(), Base); }; HighConfidenceTransformBuffer = NewObject(GetOwner(), UniqueName(TEXT("HandHighConfidenceTransformBuffer"))); HighConfidenceTransformBuffer->RegisterComponent(); HighConfidenceTransformBuffer->AttachToComponent(AttachParent, FAttachmentTransformRules::KeepRelativeTransform); HighConfidenceTransformBuffer->UpdateMode = ETransformBufferUpdateMode::Manual; HighConfidenceTransformBuffer->MaxBufferTimeSeconds = 1.f; GetOwner()->AddInstanceComponent(HighConfidenceTransformBuffer); AllTransformsTransformBuffer = NewObject(GetOwner(), UniqueName(TEXT("HandAllTransformsTransformBuffer"))); AllTransformsTransformBuffer->RegisterComponent(); AllTransformsTransformBuffer->AttachToComponent(AttachParent, FAttachmentTransformRules::KeepRelativeTransform); AllTransformsTransformBuffer->UpdateMode = ETransformBufferUpdateMode::Manual; AllTransformsTransformBuffer->MaxBufferTimeSeconds = 1.f; GetOwner()->AddInstanceComponent(AllTransformsTransformBuffer); } void UThrowingComponent::Update(bool IsTracked) { if (!HighConfidenceTransformBuffer || !AllTransformsTransformBuffer) { return; } if (IsTracked) { HighConfidenceTransformBuffer->BufferCurrentData(); } AllTransformsTransformBuffer->BufferCurrentData(); FTransformBufferData TransformBufferDataNow; AllTransformsTransformBuffer->GetBufferData(0, TransformBufferDataNow); if (IsTracked != WasTrackedLastFrame) { auto const TimeNow = GetWorld()->GetTimeSeconds(); if (IsTracked) { MostRecentTrackingGainTime = TimeNow; MostRecentTrackingGainTransform = TransformBufferDataNow.Transform; MostRecentTrackingGainVelocity = TransformBufferDataNow.Velocity; } else { MostRecentTrackingLossTime = TimeNow; MostRecentTrackingLossTransform = TransformBufferDataNow.Transform; MostRecentTrackingLossVelocity = TransformBufferDataNow.Velocity; } WasTrackedLastFrame = IsTracked; } } FVector UThrowingComponent::GetThrowVector(FVector LookDirection) const { if (bSelectBestThrowVectorFromPast) { auto BestThrowVector = FVector::ZeroVector; auto BestThrowVectorScore = FLT_MIN; check(NumThrowVectorSamples > 0); auto const TimeStep = OldestPossibleThrowVectorSeconds / NumThrowVectorSamples; for (auto i = 0; i < NumThrowVectorSamples; ++i) { auto const TimeInPast = i * TimeStep; auto const ThrowVector = GetThrowVectorInPast(TimeInPast, LookDirection); auto const Score = GetScoreForThrowVector(ThrowVector, LookDirection, i / (float)NumThrowVectorSamples); if (Score > BestThrowVectorScore) { BestThrowVector = ThrowVector; BestThrowVectorScore = Score; } } return BestThrowVector; } return GetThrowVectorInPast(ThrowLatencyAdjustmentTimeSeconds, LookDirection); } FVector UThrowingComponent::GetThrowVectorInPast(float SecondsAgo, FVector LookDirection) const { #if !UE_BUILD_SHIPPING auto ArrowColor = FColor::Green; #endif FVector ThrowVector; FTransformBufferData TransformBufferData; auto const TimeWithGoodTracking = GetTimeWithGoodTracking(); // High confidence throwing if (TimeWithGoodTracking >= HighConfidenceThrowMinTrackingTime) { HighConfidenceTransformBuffer->GetBufferData(SecondsAgo, TransformBufferData); ThrowVector = TransformBufferData.Velocity; } else { // check whether we can get a decent vector from the all transforms buffer AllTransformsTransformBuffer->GetBufferData(SecondsAgo, TransformBufferData); // Low confidence throwing if (TransformBufferData.Velocity.Size() > LowConfidenceThrowMinSpeed && FVector::DotProduct(TransformBufferData.Velocity, LookDirection) > 0) { auto const ThrowSpeed = TransformBufferData.Velocity.Size(); ThrowVector = TransformBufferData.Velocity.GetSafeNormal() * (1 - LowConfidenceHeadForwardFactor) + LookDirection * LowConfidenceHeadForwardFactor; ThrowVector *= ThrowSpeed; #if !UE_BUILD_SHIPPING ArrowColor = FColor::Yellow; #endif } // Very low confidence throwing else { auto const TrackingLossVector = TransformBufferData.Transform.GetLocation() - MostRecentTrackingLossTransform.GetLocation(); ThrowVector = TrackingLossVector.GetSafeNormal() * VeryLowConfidenceVectorFactor + LookDirection * VeryLowConfidenceHeadForwardFactor; ThrowVector *= TrackingLossVector.Size() * VeryLowConfidenceSpeedFactor; #if !UE_BUILD_SHIPPING ArrowColor = FColor::Red; #endif } } #if !UE_BUILD_SHIPPING if (CVarDebugDrawThrowingVector.GetValueOnAnyThread() > 0) { DrawDebugDirectionalArrow( GetWorld(), TransformBufferData.Transform.GetLocation(), TransformBufferData.Transform.GetLocation() + ThrowVector * 0.2f, 1.0f, ArrowColor, false, 4.0f, 0, 1.f); } #endif return ThrowVector; } float UThrowingComponent::GetTimeWithGoodTracking() const { if (!WasTrackedLastFrame) { return 0.f; } auto const TimeNow = GetWorld()->GetTimeSeconds(); return TimeNow - MostRecentTrackingGainTime; } float UThrowingComponent::GetScoreForThrowVector(FVector ThrowVector, FVector LookDirection, float TimeInPastNormalized) const { // recency auto const RecencyScore = (1 - TimeInPastNormalized) * ThrowVectorSelectionRecencyScoring; // direction auto DirectionScore = FVector::DotProduct(LookDirection, ThrowVector.GetSafeNormal()) / 2.f + 0.5f; DirectionScore *= ThrowVectorSelectionDirectionScoring; // speed auto SpeedScore = FMath::GetMappedRangeValueClamped(FVector2D(0, 200), FVector2D(0, 1), ThrowVector.Size()); SpeedScore *= ThrowVectorSelectionSpeedScoring; return RecencyScore + DirectionScore + SpeedScore; } ================================================ FILE: Plugins/OculusHandTools/Source/OculusThrowAssist/Private/TransformBufferComponent.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "TransformBufferComponent.h" #include "DrawDebugHelpers.h" #include "Kismet/KismetMathLibrary.h" #include "OculusThrowAssistModule.h" static TAutoConsoleVariable CVarDebugDrawTransformBuffer( TEXT("mnux.DebugDrawTransformBuffer"), 0, TEXT("Draws all transforms in the transform buffer"), ECVF_Cheat); UTransformBufferComponent::UTransformBufferComponent(FObjectInitializer const& ObjectInitializer) : Super(ObjectInitializer) { PrimaryComponentTick.bCanEverTick = true; } void UTransformBufferComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); if (UpdateMode == ETransformBufferUpdateMode::EveryFrame) { BufferCurrentData(); } #if !UE_BUILD_SHIPPING if (CVarDebugDrawTransformBuffer.GetValueOnAnyThread() > 0) { DebugDrawBuffer(); } #endif } void UTransformBufferComponent::BufferCurrentData() { auto Timestamp = GetWorld()->GetTimeSeconds(); auto const Transform = GetComponentTransform(); auto const PrevIndex = BufferPosition == 0 ? (int)Buffer.Capacity() - 1 : BufferPosition - 1; auto const PrevTransform = Buffer[PrevIndex].Value.Transform; auto Velocity = FVector::ZeroVector; if (!PrevTransform.Equals(FTransform::Identity)) { auto const Delta = Transform.GetLocation() - PrevTransform.GetLocation(); auto const Time = Timestamp - Buffer[PrevIndex].Key; if (Time > 0) { Velocity = Delta / Time; Buffer[BufferPosition] = TTuple( Timestamp, FTransformBufferData(GetComponentTransform(), Velocity) ); ++BufferPosition; } } else { Buffer[BufferPosition] = TTuple( Timestamp, FTransformBufferData(GetComponentTransform(), Velocity) ); ++BufferPosition; } } bool UTransformBufferComponent::GetBufferData(float SecondsAgo, FTransformBufferData& OutBufferData) { if (SecondsAgo <= 0) { int Index = BufferPosition == 0 ? Buffer.Capacity() - 1 : BufferPosition - 1; OutBufferData = Buffer[Index].Value; return true; } if (SecondsAgo > MaxBufferTimeSeconds) { UE_LOG(LogOculusThrowAssist, Warning, TEXT("UTransformBufferComponent::GetTransform: SecondsAgo can not be greater than MaxBufferTimeSeconds." )); SecondsAgo = MaxBufferTimeSeconds; } // find the pair of buffer values to interpolate between auto LookupTime = GetWorld()->GetTimeSeconds() - SecondsAgo; auto Index = BufferPosition - 1; // check for the case that the most recent buffered value is older than the lookup time if (Buffer[Index].Key <= LookupTime) { UE_LOG(LogOculusThrowAssist, Warning, TEXT("UTransformBufferComponent::GetTransform: No data recent enough for an accurate result.")); OutBufferData = Buffer[Index].Value; return false; } int const BufferCapacity = Buffer.Capacity(); for (auto i = 0; i < BufferCapacity; ++i) { --Index; if (Index < 0) { Index = BufferCapacity + Index; } auto BufferTime = Buffer[Index].Key; if (BufferTime <= LookupTime) { auto NextElementTime = Buffer[Index + 1].Key; auto t = (LookupTime - BufferTime) / (NextElementTime - BufferTime); auto PrevData = Buffer[Index].Value; auto NextData = Buffer[Index + 1].Value; auto Transform = UKismetMathLibrary::TLerp(PrevData.Transform, NextData.Transform, t); auto Velocity = FMath::Lerp(PrevData.Velocity, NextData.Velocity, t); OutBufferData = FTransformBufferData(Transform, Velocity); auto const MaxPeriodForReliableData = 0.1f; return NextElementTime - BufferTime < MaxPeriodForReliableData; } } UE_LOG(LogOculusThrowAssist, Warning, TEXT("UTransformBufferComponent::GetTransform: No data old enough for an accurate result.")); OutBufferData = Buffer[BufferPosition].Value; // return the oldest (could be default value) return false; } void UTransformBufferComponent::DebugDrawBuffer() const { auto const TimeNow = GetWorld()->GetTimeSeconds(); auto const OldestTime = TimeNow - MaxBufferTimeSeconds; auto const MaxScale = 5.0f; for (uint32 i = 0; i < Buffer.Capacity(); ++i) { auto BufferedTransform = Buffer[i].Value.Transform; auto const BufferedTimestamp = Buffer[i].Key; auto const NormalizedTime = FMath::GetMappedRangeValueClamped( FVector2D(OldestTime, TimeNow), FVector2D(0, 1), BufferedTimestamp); auto const Scale = NormalizedTime * MaxScale; DrawDebugCoordinateSystem(GetWorld(), BufferedTransform.GetLocation(), BufferedTransform.Rotator(), Scale); auto BufferedVelocity = Buffer[i].Value.Velocity; DrawDebugDirectionalArrow( GetWorld(), BufferedTransform.GetLocation(), BufferedTransform.GetLocation() + BufferedVelocity * 0.2f, 1.f, FColor::Orange, false, -1, 0, .3f); } } ================================================ FILE: Plugins/OculusHandTools/Source/OculusThrowAssist/Public/OculusThrowAssistModule.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" DECLARE_LOG_CATEGORY_EXTERN(LogOculusThrowAssist, Log, All); class FOculusThrowAssistModule : public IModuleInterface { public: /** IModuleInterface implementation */ virtual void StartupModule() override; virtual void ShutdownModule() override; }; ================================================ FILE: Plugins/OculusHandTools/Source/OculusThrowAssist/Public/ThrowingComponent.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" #include "Components/ActorComponent.h" #include "ThrowingComponent.generated.h" class UTransformBufferComponent; UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) class OCULUSTHROWASSIST_API UThrowingComponent : public UActorComponent { GENERATED_BODY() public: UThrowingComponent(); /** * @brief Called to initialize the throw calculator. * @param AttachParent Usually the hand to be tracked for throwing objects. */ UFUNCTION(BlueprintCallable) void Initialize(USceneComponent* AttachParent); /** * @param LookDirection The world-space direction the player is looking in (to assist with aiming). * @return The world-space velocity of the calculated throw. */ UFUNCTION(BlueprintPure) FVector GetThrowVector(FVector LookDirection) const; /** * @brief Tick the throw calculator with new info from its parent transform. * @param IsTracked Whether or not the current data is considered high quality. */ UFUNCTION(BlueprintCallable) void Update(bool IsTracked); /// How much time to look back in the past when making a throw to account for input and render latency UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Throwing") float ThrowLatencyAdjustmentTimeSeconds = 0.04f; /// minimum amount of time with uninterrupted tracking needed in order to make a high-confidence throw UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Throwing: High Confidence") float HighConfidenceThrowMinTrackingTime = 0.08f; /// minimum throw vector speed for a low-confidence throw UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Throwing: Low Confidence") float LowConfidenceThrowMinSpeed = 50.f; /// factor that determines how much to weigh the head forward vector with low confidence throwing UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Throwing: Low Confidence") float LowConfidenceHeadForwardFactor = 0.4f; /// speed factor for very low confidence throws UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Throwing: Very Low Confidence") float VeryLowConfidenceSpeedFactor = 5.f; /// factor that determines how much to weigh the very low confidence vector /// (the point that tracking was lost to the current point) UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Throwing: Very Low Confidence") float VeryLowConfidenceVectorFactor = 0.3f; /// factor that determines how much to weigh the head forward vector with very low confidence throwing UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Throwing: Very Low Confidence") float VeryLowConfidenceHeadForwardFactor = 0.7f; /// High-confidence transform buffer for the hands UPROPERTY(Transient) UTransformBufferComponent* HighConfidenceTransformBuffer; /// Transform buffer for the hands UPROPERTY(Transient) UTransformBufferComponent* AllTransformsTransformBuffer; /// Whether to choose the "best" throw vector or just a simple look back. UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Throwing: Throw Vector Selection") bool bSelectBestThrowVectorFromPast = true; /// How far back to track throw vector samples. UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Throwing: Throw Vector Selection") float OldestPossibleThrowVectorSeconds = 1.0f; /// How many throw vector samples to track. UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Throwing: Throw Vector Selection") int NumThrowVectorSamples = 10; /// Score weight for recency. UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Throwing: Throw Vector Selection") float ThrowVectorSelectionRecencyScoring = 1.0f; /// Score weight for direction relative to look direction. UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Throwing: Throw Vector Selection") float ThrowVectorSelectionDirectionScoring = 1.0f; /// Score weight for speed. UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Throwing: Throw Vector Selection") float ThrowVectorSelectionSpeedScoring = 1.0f; private: FVector GetThrowVectorInPast(float SecondsAgo, FVector LookDirection) const; float GetScoreForThrowVector(FVector ThrowVector, FVector LookDirection, float TimeInPastNormalized) const; float GetTimeWithGoodTracking() const; bool WasTrackedLastFrame = false; float MostRecentTrackingLossTime; FTransform MostRecentTrackingLossTransform; FVector MostRecentTrackingLossVelocity; float MostRecentTrackingGainTime; FTransform MostRecentTrackingGainTransform; FVector MostRecentTrackingGainVelocity; }; ================================================ FILE: Plugins/OculusHandTools/Source/OculusThrowAssist/Public/TransformBufferComponent.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" #include "Components/SceneComponent.h" #include "Containers/CircularBuffer.h" #include "TransformBufferComponent.generated.h" UENUM() enum class ETransformBufferUpdateMode : uint8 { Manual, EveryFrame }; USTRUCT(BlueprintType) struct FTransformBufferData { GENERATED_BODY() FTransformBufferData() : Transform(FTransform::Identity), Velocity(FVector::ZeroVector) { } FTransformBufferData(FTransform InTransform, FVector InVelocity) : Transform(InTransform), Velocity(InVelocity) { } FTransform Transform; FVector Velocity; }; UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) class OCULUSTHROWASSIST_API UTransformBufferComponent : public USceneComponent { GENERATED_BODY() public: explicit UTransformBufferComponent(FObjectInitializer const& ObjectInitializer); /// How the buffer will be updated with new data. UPROPERTY(EditAnywhere, Category = "Transform Buffer") ETransformBufferUpdateMode UpdateMode = ETransformBufferUpdateMode::Manual; /// How long to buffer data. UPROPERTY(EditAnywhere, Category = "Transform Buffer") float MaxBufferTimeSeconds = 1.0f; /// Force an update to the buffer with new data. UFUNCTION(BlueprintCallable) void BufferCurrentData(); virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; /// Get transform data from the buffer. Returns true if the data can be considered reliable, otherwise false. UFUNCTION(BlueprintCallable) bool GetBufferData(float SecondsAgo, FTransformBufferData& OutBufferData); private: // buffer size assumes max FPS at 90 TCircularBuffer> Buffer{(uint32)FMath::CeilToInt(MaxBufferTimeSeconds * 90)}; int BufferPosition = 0; void DebugDrawBuffer() const; }; ================================================ FILE: Plugins/OculusUtils/OculusUtils.uplugin ================================================ { "FileVersion": 3, "Version": 1, "VersionName": "1.0", "FriendlyName": "Oculus Utils", "Description": "Utilities used by other Oculus plugins.", "Category": "Utilities", "CreatedBy": "Oculus", "CreatedByURL": "https://www.oculus.com/", "DocsURL": "", "MarketplaceURL": "", "SupportURL": "", "CanContainContent": true, "IsBetaVersion": false, "IsExperimentalVersion": false, "Installed": false, "Modules": [ { "Name": "OculusUtils", "Type": "Runtime", "LoadingPhase": "Default", "WhitelistPlatforms": [ "Win64", "Android" ] } ], "Plugins": [ { "Name": "OculusXR", "Enabled": true, "MarketplaceURL": "com.epicgames.launcher://ue/marketplace/product/8313d8d7e7cf4e03a33e79eb757bccba", "SupportedTargetPlatforms": [ "Win64", "Android" ] } ] } ================================================ FILE: Plugins/OculusUtils/Source/OculusUtils/OculusUtils.Build.cs ================================================ // Copyright (c) Facebook, Inc. and its affiliates. using System.IO; using UnrealBuildTool; public class OculusUtils : ModuleRules { public OculusUtils(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; PrivateIncludePaths.Add(Path.Combine(GetModuleDirectory("OculusXRHMD"), "Private")); PublicDependencyModuleNames.AddRange( new string[] { "Core", // ... add other public dependencies that you statically link with here ... } ); PrivateDependencyModuleNames.AddRange( new string[] { "CoreUObject", "Engine", "Slate", "SlateCore", "OculusXRHMD", "DeveloperSettings", "OVRPluginXR" } ); if (Target.bBuildEditor) { PrivateDependencyModuleNames.Add("UnrealEd"); PrivateDependencyModuleNames.Add("DetailCustomizations"); PrivateDependencyModuleNames.Add("ToolWidgets"); } bLegacyParentIncludePaths = true; try { string telemetryPath = GetModuleDirectory("OculusXRTelemetry"); if (telemetryPath != "") { PrivateDependencyModuleNames.AddRange(new string[] { "OculusXRTelemetry" }); PrivateDefinitions.Add("OCULUS_XR_TELEMETRY=1"); } } catch { // do nothing, the module doesn't exist } } } ================================================ FILE: Plugins/OculusUtils/Source/OculusUtils/Private/ContinuousOverlapSphereComponent.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "ContinuousOverlapSphereComponent.h" #include "OculusUtilsModule.h" void UContinuousOverlapSphereComponent::SetLocation_Direct(FVector Location) { if (GetAttachParent() != nullptr && !IsUsingAbsoluteLocation()) { auto const ParentToWorld = GetAttachParent()->GetSocketTransform(GetAttachSocketName()); Location = ParentToWorld.InverseTransformPosition(Location); } SetRelativeLocation_Direct(Location); UpdateComponentToWorld(); } void UContinuousOverlapSphereComponent::OnUpdateTransform(EUpdateTransformFlags UpdateTransformFlags, ETeleportType Teleport) { // Prevent infinite loop if (bIsInOnUpdateTransform) return; bIsInOnUpdateTransform = true; auto const NewTransform = GetComponentTransform(); auto const Delta = NewTransform.GetLocation() - LastLocation; if (bIsInitialized && Teleport == ETeleportType::None && !Delta.IsNearlyZero()) { SetLocation_Direct(LastLocation); MoveComponent(Delta, NewTransform.GetRotation(), true); } bIsInitialized = true; LastLocation = NewTransform.GetLocation(); Super::OnUpdateTransform(UpdateTransformFlags, Teleport); bIsInOnUpdateTransform = false; } ================================================ FILE: Plugins/OculusUtils/Source/OculusUtils/Private/OculusDeveloperTelemetry.cpp ================================================ // Copyright (c) Facebook, Inc. and its affiliates. #include "OculusDeveloperTelemetry.h" #include "OculusXRHMD/Private/OculusXRHMDModule.h" #include "Widgets/Input/SHyperlink.h" #include "Widgets/Text/SRichTextBlock.h" #ifdef OCULUS_XR_TELEMETRY #include "OculusXRTelemetry.h" #endif #define PRIVACY_POLICY_URL "https://www.meta.com/legal/quest/privacy-policy/" #define LOCTEXT_NAMESPACE "FOculusUtilsModule" #if WITH_EDITOR #include "Dialog/SCustomDialog.h" #include "DetailLayoutBuilder.h" #include "DetailCategoryBuilder.h" #include "DetailWidgetRow.h" void UOculusDeveloperTelemetry::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) { Super::PostEditChangeProperty(PropertyChangedEvent); Flush(); } auto MakeTelemetryTextBlock(float Width) { auto const TelemetryMessage = LOCTEXT("EnableTelemetryMessage", "Enabling telemetry will transmit data to Meta about your usage of its samples and tools. " "This information is used by Meta to improve our products and better serve our developers. For more information, go to this url: " "" PRIVACY_POLICY_URL ""); auto const OnPrivacyPolicyClicked = FSlateHyperlinkRun::FOnClick::CreateLambda( [](auto) { FPlatformProcess::LaunchURL(TEXT(PRIVACY_POLICY_URL), nullptr, nullptr); }); return SNew(SRichTextBlock). Text(TelemetryMessage). WrapTextAt(Width). AutoWrapText(Width == 0) + SRichTextBlock::HyperlinkDecorator(TEXT("link"), OnPrivacyPolicyClicked); } static auto const EnableText = LOCTEXT("EnableTelemetryEnableButton", "Enable"); static auto const OptOutText = LOCTEXT("EnableTelemetryOptOut", "Opt out"); #endif void UOculusDeveloperTelemetry::SendEvent(const char* eventName, const char* param, const char* source) { OnFlush.AddLambda([eventName, param, source] { #ifdef OCULUS_XR_TELEMETRY OculusXRTelemetry::SendEvent(eventName, param, source); #else FOculusXRHMDModule::GetPluginWrapper().SendEvent2(eventName, param, source); #endif }); Flush(); } void UOculusDeveloperTelemetry::Flush() { #if WITH_EDITOR if (bHasPrompted == false) { auto const Title = LOCTEXT("EnableTelemetryTitle", "Enable Meta Telemetry"); TSharedRef CustomDialog = SNew(SCustomDialog). Title(Title). Content()[ MakeTelemetryTextBlock(400) ]. Buttons({ SCustomDialog::FButton(EnableText), SCustomDialog::FButton(OptOutText), }); auto const Return = CustomDialog->ShowModal(); bHasPrompted = true; bIsEnabled = Return == 0; Modify(); SaveConfig(); } if (bIsEnabled) { #ifdef OCULUS_XR_TELEMETRY OculusXRTelemetry::SetDeveloperTelemetryConsent(true); #else if (FOculusXRHMDModule::GetPluginWrapper().SetDeveloperMode) { FOculusXRHMDModule::GetPluginWrapper().SetDeveloperMode(true); } #endif OnFlush.Broadcast(); OnFlush.Clear(); } #endif } #if WITH_EDITOR void FOculusTelemetrySettingsCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) { TArray> Objects; DetailBuilder.GetObjectsBeingCustomized(Objects); for (auto Object : Objects) { auto Telemetry = MakeWeakObjectPtr(Cast(Object)); if (Telemetry != nullptr) { auto&& Category = DetailBuilder.EditCategory(TEXT("Privacy")); auto IsEnabledProperty = DetailBuilder.GetProperty(TEXT("bIsEnabled")); auto IsEnabled = [Telemetry]{ return Telemetry.IsValid() && Telemetry->bIsEnabled ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }; auto IsOptedOut = [Telemetry]{ return Telemetry.IsValid() && Telemetry->bIsEnabled ? ECheckBoxState::Unchecked : ECheckBoxState::Checked; }; auto SetEnabled = [Telemetry](ECheckBoxState state) { if (Telemetry.IsValid()) { Telemetry->bIsEnabled = state == ECheckBoxState::Checked; Telemetry->bHasPrompted = true; Telemetry->Modify(); Telemetry->SaveConfig(); Telemetry->Flush(); } }; auto SetOptOut = [SetEnabled](ECheckBoxState state){ SetEnabled(state == ECheckBoxState::Unchecked ? ECheckBoxState::Checked : ECheckBoxState::Unchecked); }; Category.InitiallyCollapsed(false) .AddProperty(IsEnabledProperty) .ShouldAutoExpand(true) .CustomWidget(true) [ SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight() [ MakeTelemetryTextBlock(0) ] + SVerticalBox::Slot().AutoHeight() [ SNew(SCheckBox). IsChecked_Lambda(IsEnabled). OnCheckStateChanged_Lambda(SetEnabled). Content() [ SNew(STextBlock).Text(EnableText) ] ] + SVerticalBox::Slot().AutoHeight() [ SNew(SCheckBox). IsChecked_Lambda(IsOptedOut). OnCheckStateChanged_Lambda(SetOptOut). Content() [ SNew(STextBlock).Text(OptOutText) ] ] ]; } } } #endif #undef LOCTEXT_NAMESPACE #undef PRIVACY_POLICY_URL ================================================ FILE: Plugins/OculusUtils/Source/OculusUtils/Private/OculusUtilsLibrary.cpp ================================================ // Copyright (c) Facebook, Inc. and its affiliates. #include "OculusUtilsLibrary.h" #include "DelayAction.h" #include "OculusUtilsModule.h" #include "Misc/DefaultValueHelper.h" #include "Misc/DateTime.h" bool UOculusUtilsLibrary::GetOculusBuildInfo(FString& SourceControlChangelist, FString& BuildDateTimeString) { auto BuildInfo = GetDefault(); if (!BuildInfo) { SourceControlChangelist = "Unknown Changelist"; BuildDateTimeString = "Unknown Build Time"; return false; } // Oculus Store builds have a changelist number, otherwise it's a development one. SourceControlChangelist = BuildInfo->PackageChangelist; UE_LOG(LogOculusUtils, Display, TEXT("Build Info - source control changelist: \"%s\""), *SourceControlChangelist); // Extracting date and time values. FString DateString, TimeString; auto DateNumber = 0, TimeNumber = 0; if (BuildInfo->PackageDateAndTime.Split(TEXT(" "), &DateString, &TimeString) && FDefaultValueHelper::ParseInt(DateString, DateNumber) && FDefaultValueHelper::ParseInt(TimeString, TimeNumber)) { UE_LOG(LogOculusUtils, Display, TEXT("Build Info - parsed package date %d and time %d"), DateNumber, TimeNumber); // Separating date/time components. auto const Day = DateNumber % 100; DateNumber /= 100; auto const Month = DateNumber % 100; auto const Year = DateNumber / 100; auto const Second = TimeNumber % 100; TimeNumber /= 100; auto const Minute = TimeNumber % 100; auto const Hour = TimeNumber / 100; // We use a generic PT for both PDT and PST, where Oculus build servers are located. FDateTime BuildDate(Year, Month, Day, Hour, Minute, Second); BuildDateTimeString = BuildDate.ToString(TEXT("%Y-%m-%d %H:%M:%S PT")); } else { // If unparsable, we keep it as is. UE_LOG(LogOculusUtils, Display, TEXT("Build Info - cannot parse package date and time: \"%s\""), *(BuildInfo->PackageDateAndTime)); BuildDateTimeString = BuildInfo->PackageDateAndTime; } return !(SourceControlChangelist.IsEmpty() && BuildDateTimeString.IsEmpty()); } TArray UOculusUtilsLibrary::SortComponentsByName(TArray const& Components) { auto SortedComponents = Components; SortedComponents.Sort( [](UActorComponent const& A, UActorComponent const& B) -> bool { return A.GetName().Compare(B.GetName()) < 0; }); return SortedComponents; } class FTickUntilAction : public FPendingLatentAction { public: FName ExecutionFunction; int32 OutputLink; FWeakObjectPtr CallbackTarget; bool bIsComplete = false; FTickUntilAction(FLatentActionInfo const& LatentInfo) : ExecutionFunction(LatentInfo.ExecutionFunction), OutputLink(LatentInfo.Linkage), CallbackTarget(LatentInfo.CallbackTarget) { } virtual void UpdateOperation(FLatentResponse& Response) override { if (bIsComplete) { Response.DoneIf(true); } else { Response.TriggerLink(ExecutionFunction, OutputLink, CallbackTarget); } } }; void UOculusUtilsLibrary::TickUntil(UObject const* WorldContextObject, ETickUntilInputPin InputPin, FLatentActionInfo LatentInfo) { if (auto World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull)) { auto& LatentActionManager = World->GetLatentActionManager(); auto Action = LatentActionManager.FindExistingAction(LatentInfo.CallbackTarget, LatentInfo.UUID); if (InputPin == ETickUntilInputPin::Start) { if (Action == nullptr) { LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, new FTickUntilAction(LatentInfo)); } } else if (InputPin == ETickUntilInputPin::Break) { if (Action != nullptr) { Action->bIsComplete = true; } } } } ================================================ FILE: Plugins/OculusUtils/Source/OculusUtils/Private/OculusUtilsModule.cpp ================================================ // Copyright (c) Facebook, Inc. and its affiliates. #include "OculusUtilsModule.h" #if WITH_EDITOR #include "ISettingsModule.h" #endif #include "OculusDeveloperTelemetry.h" OCULUS_TELEMETRY_LOAD_MODULE("Unreal-OculusUtils"); #define LOCTEXT_NAMESPACE "FOculusUtilsModule" DEFINE_LOG_CATEGORY(LogOculusUtils); void FOculusUtilsModule::StartupModule() { #if WITH_EDITOR auto&& PropertyModule = FModuleManager::GetModuleChecked("PropertyEditor"); PropertyModule.RegisterCustomClassLayout( "OculusDeveloperTelemetry", FOnGetDetailCustomizationInstance::CreateLambda([] { return MakeShared(); })); auto SettingsModule = FModuleManager::GetModulePtr("Settings"); SettingsModule->RegisterSettings( "Editor", "Privacy", "Oculus Developer Telemetry", LOCTEXT("PrivacyAnalyticsSettingsName", "Oculus Developer Telemetry"), LOCTEXT("PrivacyAnalyticsSettingsDescription", "Configure the way your Editor usage information is handled."), GetMutableDefault()); #endif } void FOculusUtilsModule::ShutdownModule() { // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, // we call this function before unloading the module. } #undef LOCTEXT_NAMESPACE IMPLEMENT_MODULE(FOculusUtilsModule, OculusUtils) ================================================ FILE: Plugins/OculusUtils/Source/OculusUtils/Public/ContinuousOverlapSphereComponent.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "Components/SphereComponent.h" #include "ContinuousOverlapSphereComponent.generated.h" /** * Used similarly to SphereComponent, but modified to use continuous "collision" for triggering overlaps. * Useful for fast-moving spheres with non-overlap collision disabled. */ UCLASS(BlueprintType, meta=(BlueprintSpawnableComponent)) class OCULUSUTILS_API UContinuousOverlapSphereComponent : public USphereComponent { GENERATED_BODY() FVector LastLocation; bool bIsInitialized = false; bool bIsInOnUpdateTransform = false; void SetLocation_Direct(FVector Location); public: virtual void OnUpdateTransform(EUpdateTransformFlags UpdateTransformFlags, ETeleportType Teleport) override; }; ================================================ FILE: Plugins/OculusUtils/Source/OculusUtils/Public/OculusDeveloperTelemetry.h ================================================ // Copyright (c) Facebook, Inc. and its affiliates. #pragma once #include "CoreMinimal.h" #if WITH_EDITOR #include "IDetailCustomization.h" #endif #include "OculusDeveloperTelemetry.generated.h" UCLASS(Config=EditorPerProjectUserSettings, meta=(DisplayName="Oculus Developer Telemetry")) class OCULUSUTILS_API UOculusDeveloperTelemetry : public UObject { GENERATED_BODY() DECLARE_MULTICAST_DELEGATE(FOnFlushEvent); FOnFlushEvent OnFlush; public: #if WITH_EDITOR virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; #endif UPROPERTY(EditAnywhere, Config, meta=(Tooltip="Enabling telemetry will transmit data to Oculus about your usage of its samples and tools.")) bool bIsEnabled = false; UPROPERTY(Config) bool bHasPrompted = false; void SendEvent(const char* eventName, const char* param, const char* source); void Flush(); }; #if WITH_EDITOR class FOculusTelemetrySettingsCustomization : public IDetailCustomization { public: virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; }; #endif #if WITH_EDITOR #define OCULUS_TELEMETRY_LOAD_MODULE(ModuleName) \ static FDelayedAutoRegisterHelper ANONYMOUS_VARIABLE(GMetricsHelper) (EDelayedRegisterRunPhase::EndOfEngineInit, [] \ { \ GetMutableDefault()->SendEvent("module_loaded", ModuleName, "integration"); \ }); #else #define OCULUS_TELEMETRY_LOAD_MODULE(...) #endif ================================================ FILE: Plugins/OculusUtils/Source/OculusUtils/Public/OculusUtilsLibrary.h ================================================ // Copyright (c) Facebook, Inc. and its affiliates. #pragma once #include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" #include "Components/ActorComponent.h" #include "OculusUtilsLibrary.generated.h" UENUM(BlueprintType) enum class ETickUntilInputPin : uint8 { Start, Break }; /** * Build configuration from the project's game configuration. */ UCLASS(Config=Game) class OCULUSUTILS_API UBuildInfo : public UObject { GENERATED_BODY() public: /** Source control changelist */ UPROPERTY(Config) FString PackageChangelist; /** Build date and time (Pacific time) "YYYYMMDD HHMMSS" */ UPROPERTY(Config) FString PackageDateAndTime; }; /** * Oculus Utils Blueprint Library. */ UCLASS() class OCULUSUTILS_API UOculusUtilsLibrary : public UBlueprintFunctionLibrary { GENERATED_BODY() public: /** Returns the Oculus store version. */ UFUNCTION(BlueprintCallable, Category = "Oculus Utils") static bool GetOculusBuildInfo(FString& SourceControlChangelist, FString& BuildDateTimeString); /** Returns the array of components sorted by name. */ UFUNCTION(BlueprintCallable, Category = "Oculus Utils", meta = (ComponentClass = "ActorComponent", DeterminesOutputType = "Components")) static TArray SortComponentsByName(const TArray& Components); /** * Executes the "Completed" pin every tick until Break is hit. Calling Start again while it is still ticking will be ignored. * * @param WorldContextObject World context. * @param InputPin Which pin is being called. * @param LatentInfo The latent action. */ UFUNCTION(BlueprintCallable, Category="Utilities|FlowControl", meta=(Latent, WorldContext="WorldContextObject", LatentInfo="LatentInfo", Keywords="sleep", ExpandEnumAsExecs="InputPin")) static void TickUntil(const UObject* WorldContextObject, ETickUntilInputPin InputPin, struct FLatentActionInfo LatentInfo); }; ================================================ FILE: Plugins/OculusUtils/Source/OculusUtils/Public/OculusUtilsModule.h ================================================ // Copyright (c) Facebook, Inc. and its affiliates. #pragma once #include "CoreMinimal.h" DECLARE_LOG_CATEGORY_EXTERN(LogOculusUtils, Log, All); class FOculusUtilsModule : public IModuleInterface { public: /** IModuleInterface implementation */ virtual void StartupModule() override; virtual void ShutdownModule() override; }; ================================================ FILE: README.md ================================================ # Hand Gameplay Showcase [](https://www.meta.com/experiences/oculus-hand-gameplay-showcase-for-unreal/4232440213539049/) This project offers reusable components built on the robust hand tracking mechanics from [First Steps with Hand Tracking](https://www.meta.com/experiences/first-steps-with-hand-tracking/3974885535895823/) and [Tiny Castles](https://www.meta.com/experiences/tiny-castles/3647163948685453/). Try the showcase yourself on the [Horizon Store](https://www.meta.com/experiences/4232440213539049/). ## Build Instructions ### Download the Project First, install Git LFS by running: ```sh git lfs install ``` Next, clone this repository using the "Code" button above or run: ```sh git clone https://github.com/oculus-samples/Unreal-HandGameplay.git ``` Finally, open the project in Unreal Editor using one of the following methods. #### Epic Games Launcher The easiest way to start is with the prebuilt Unreal Engine from the Epic Games Launcher. Note that the [Hand Movement Filtering](./Plugins/OculusHandTools/README_HandTrackingFilter.md) will not work without the Oculus fork described below. 1. Install the [Epic Games Launcher](https://store.epicgames.com/en-US/download). 2. Install Unreal Engine 5.3 or later via the launcher. 3. Launch Unreal Editor. 4. Click "More"
5. Click "Browse" and select `HandGameplay.uproject`. #### Oculus Unreal Fork The Oculus Unreal fork provides the latest Oculus feature integration but requires building the editor from source. 1. [Get access to the Unreal source code](https://developers.meta.com/horizon/documentation/unreal/unreal-building-ue4-from-source/#prerequisites). 2. [Clone the `oculus-5.6` branch of the Oculus fork](https://github.com/Oculus-VR/UnrealEngine/tree/oculus-5.6). 3. [Install Visual Studio](https://developers.meta.com/horizon/documentation/unreal/unreal-building-ue4-from-source/#to-download-and-build-unreal-engine). 4. Open a command prompt in the Unreal root directory and run: ```sh .\GenerateProjectFiles.bat -Game HandGameplay -Engine \HandGameplay.uproject ``` 5. Open the generated `HandGameplay.sln` file in the `Unreal-HandGameplay` directory. 6. Set `HandGameplay` as the start-up project and `Development Editor` as the configuration. 7. Press `F5` to build and debug the project and engine. ### Use as Plugin To add these features to your project, install the OculusHandTools plugin. Download the latest release of `OculusHandTools.zip` and extract it into your project's `Plugins` folder. For a detailed explanation of the mechanics, see [here](./Plugins/OculusHandTools/README.md#mechanics-implementations). The OculusHandTools plugin also includes several useful C++ modules: - [HandInput](./Plugins/OculusHandTools/README_HandInput.md) - [HandPoseRecognition](./Plugins/OculusHandTools/README_HandPoseRecognition.md) - [OculusHandTrackingFilter](./Plugins/OculusHandTools/README_HandTrackingFilter.md) - [OculusInteractable](./Plugins/OculusHandTools/README_Interactable.md) - [OculusThrowAssist](./Plugins/OculusHandTools/README_ThrowAssist.md) - [OculusUtils](./Plugins/OculusHandTools/README_OculusUtils.md) ## Features | Feature | Image | Description | |---------|-------|-------------| | **[Teleportation](./Plugins/OculusHandTools/README.md#teleportation)** | | Simple movement using pose recognition from the Hand Pose Showcase. | | **[Grabbing](./Plugins/OculusHandTools/README_HandInput.md)** | | Recognizes natural grab gestures, attaches objects to your hand, and overrides hand pose for visual feedback. | | **[Throwing](./Plugins/OculusHandTools/README.md#throwing)** |   | Uses hand history data to calculate the velocity of thrown objects. | | **[Button Pushing](./Plugins/OculusHandTools/README.md#pushing-buttons)** | | Reliable digital interaction (on/off). (Bonus: your pointer finger is a digit!) | | **[Punching](./Plugins/OculusHandTools/README.md#punching)** | | A satisfying hand interaction, despite fast movement sometimes causing tracking loss. | | **[Hand Movement Filtering](./Plugins/OculusHandTools/README_HandTrackingFilter.md)** |   | Stabilizes hand and finger movement during low-quality or lost tracking, improving feel especially during punching. More details [here](https://developers.meta.com/horizon/blog/adding-hand-tracking-to-first-steps/). | | **[Two-handed Aiming](./Plugins/OculusHandTools/README.md#two-handed-aiming)** | | Reliable and fulfilling hand interaction using both hands. | | **[Example Hands for Tutorials](./Plugins/OculusHandTools/README.md#example-hands-for-tutorials)** |   | Illustrates the poses your app expects from users. | ## License This codebase serves as a reference and template for multiplayer VR games. All code and assets follow the license found [here](./LICENSE), unless otherwise noted. ## Contribution See the [CONTRIBUTING](./CONTRIBUTING.md) file for contribution guidelines. # Updates ## 20 December 2023 Update We updated the project to UE5.3. ## March 2025 Update We updated the project to use OpenXR from Epic with meta vendor extensions. Please note, you can still use the OVRPlugin, but you'll need to update the Grab Poses on: * Content/HandGameplay/Probs/Blocks/InteractableBrick * Content/HandGameplay/Probs/RingWeapon/InteractableArtifactHandle Example for InteractableBrick: * Change relative Hand Transform left to: ``LOC X1.623 Y12.178 Z8.067 ROT W0.160 X0.189 Y-0.832 Z-0.497`` * Change relative Hand Transform right to: ``LOC X5.690 Y-10.448 Z-8.640 ROT W0.840 X0.534 Y0.064 Z0.068`` Example for InteractableArtifactHandle: * Change relative Hand Transform left to: ``LOC X1.094 Y6.294 Z11.635 ROT W0.101 X-0.548 Y-0.830 Z-0.009`` * Change relative Hand Transform right to: ``LOC X-0.519 Y-6.451 Z-12.047 ROT W0.859 X0.053 Y0.058 Z-0.506`` Best way to get new HandTransforms: * Open HansCharacterHandsState from OculusHandTools/Content/Hands/ * Reconnect the Blueprint-flow-nodes * This will output the location of your hand when grabbing an object in the correct format for copy and paste. ## 04 December 2025 Update We fixed an issue related to objects jittering when grabbed. We updated the project to UE5.6. + Meta SDK v81 ================================================ FILE: Source/HandGameplay/HandGameplay.Build.cs ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. using UnrealBuildTool; public class HandGameplay : ModuleRules { public HandGameplay(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "OculusHandPoseRecognition" }); PrivateDependencyModuleNames.AddRange(new string[] { "OculusUtils" }); // Uncomment if you are using Slate UI // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" }); // Uncomment if you are using online features // PrivateDependencyModuleNames.Add("OnlineSubsystem"); // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true if (Target.Platform == UnrealTargetPlatform.Android) { var manifestFile = System.IO.Path.Combine(ModuleDirectory, "UpdatePermissions.xml"); AdditionalPropertiesForReceipt.Add("AndroidPlugin", manifestFile); } } } ================================================ FILE: Source/HandGameplay/HandGameplay.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "HandGameplay.h" #include "Modules/ModuleManager.h" #include "OculusDeveloperTelemetry.h" OCULUS_TELEMETRY_LOAD_MODULE("Unreal-HandGameplayShowcase"); IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, HandGameplay, "HandGameplay" ); DEFINE_LOG_CATEGORY(LogHandGameplayShowcase); ================================================ FILE: Source/HandGameplay/HandGameplay.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" DECLARE_LOG_CATEGORY_EXTERN(LogHandGameplayShowcase, Log, All); ================================================ FILE: Source/HandGameplay/HandGameplayGameModeBase.cpp ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #include "HandGameplayGameModeBase.h" ================================================ FILE: Source/HandGameplay/HandGameplayGameModeBase.h ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "CoreMinimal.h" #include "GameFramework/GameModeBase.h" #include "HandGameplayGameModeBase.generated.h" /** * */ UCLASS() class HANDGAMEPLAY_API AHandGameplayGameModeBase : public AGameModeBase { GENERATED_BODY() }; ================================================ FILE: Source/HandGameplay/UpdatePermissions.xml ================================================ ================================================ FILE: Source/HandGameplay.Target.cs ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. using UnrealBuildTool; using System.Collections.Generic; public class HandGameplayTarget : TargetRules { public HandGameplayTarget( TargetInfo Target) : base(Target) { Type = TargetType.Game; DefaultBuildSettings = BuildSettingsVersion.V4; IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_6; ExtraModuleNames.AddRange( new string[] { "HandGameplay" } ); } } ================================================ FILE: Source/HandGameplayEditor.Target.cs ================================================ // Copyright (c) Meta Platforms, Inc. and affiliates. using UnrealBuildTool; using System.Collections.Generic; public class HandGameplayEditorTarget : TargetRules { public HandGameplayEditorTarget( TargetInfo Target) : base(Target) { Type = TargetType.Editor; DefaultBuildSettings = BuildSettingsVersion.V4; IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_6; ExtraModuleNames.AddRange( new string[] { "HandGameplay" } ); } }