diff --git a/Content/Sample/Common/Audio/Boop.uasset b/Content/Sample/Common/Audio/Boop.uasset new file mode 100644 index 0000000..9c496d6 Binary files /dev/null and b/Content/Sample/Common/Audio/Boop.uasset differ diff --git a/Content/Sample/Common/Audio/ButtonPress.uasset b/Content/Sample/Common/Audio/ButtonPress.uasset new file mode 100644 index 0000000..428df44 Binary files /dev/null and b/Content/Sample/Common/Audio/ButtonPress.uasset differ diff --git a/Content/Sample/Common/Audio/Explosion.uasset b/Content/Sample/Common/Audio/Explosion.uasset new file mode 100644 index 0000000..44d9345 Binary files /dev/null and b/Content/Sample/Common/Audio/Explosion.uasset differ diff --git a/Content/Sample/Common/Audio/Grab.uasset b/Content/Sample/Common/Audio/Grab.uasset new file mode 100644 index 0000000..6b6ed21 Binary files /dev/null and b/Content/Sample/Common/Audio/Grab.uasset differ diff --git a/Content/Sample/Common/Audio/Grab1.uasset b/Content/Sample/Common/Audio/Grab1.uasset new file mode 100644 index 0000000..9ffb866 Binary files /dev/null and b/Content/Sample/Common/Audio/Grab1.uasset differ diff --git a/Content/Sample/Common/Audio/Grab2.uasset b/Content/Sample/Common/Audio/Grab2.uasset new file mode 100644 index 0000000..97dd27f Binary files /dev/null and b/Content/Sample/Common/Audio/Grab2.uasset differ diff --git a/Content/Sample/Common/Audio/Grab3.uasset b/Content/Sample/Common/Audio/Grab3.uasset new file mode 100644 index 0000000..8b1bf3b Binary files /dev/null and b/Content/Sample/Common/Audio/Grab3.uasset differ diff --git a/Content/Sample/Common/Audio/KnifeSlash.uasset b/Content/Sample/Common/Audio/KnifeSlash.uasset new file mode 100644 index 0000000..a16743a Binary files /dev/null and b/Content/Sample/Common/Audio/KnifeSlash.uasset differ diff --git a/Content/Sample/Common/Audio/SC_IceJauntCharging.uasset b/Content/Sample/Common/Audio/SC_IceJauntCharging.uasset new file mode 100644 index 0000000..4150a35 Binary files /dev/null and b/Content/Sample/Common/Audio/SC_IceJauntCharging.uasset differ diff --git a/Content/Sample/Common/Audio/SC_IceJauntExplosion.uasset b/Content/Sample/Common/Audio/SC_IceJauntExplosion.uasset new file mode 100644 index 0000000..598eb6a Binary files /dev/null and b/Content/Sample/Common/Audio/SC_IceJauntExplosion.uasset differ diff --git a/Content/Sample/Common/Audio/StoneDrop1.uasset b/Content/Sample/Common/Audio/StoneDrop1.uasset new file mode 100644 index 0000000..d3065f7 Binary files /dev/null and b/Content/Sample/Common/Audio/StoneDrop1.uasset differ diff --git a/Content/Sample/Common/Audio/StoneDrop2.uasset b/Content/Sample/Common/Audio/StoneDrop2.uasset new file mode 100644 index 0000000..47a1462 Binary files /dev/null and b/Content/Sample/Common/Audio/StoneDrop2.uasset differ diff --git a/Content/Sample/Common/Audio/StoneDrop3.uasset b/Content/Sample/Common/Audio/StoneDrop3.uasset new file mode 100644 index 0000000..2137e94 Binary files /dev/null and b/Content/Sample/Common/Audio/StoneDrop3.uasset differ diff --git a/Content/Sample/Common/Audio/StoneDrop4.uasset b/Content/Sample/Common/Audio/StoneDrop4.uasset new file mode 100644 index 0000000..59a3f82 Binary files /dev/null and b/Content/Sample/Common/Audio/StoneDrop4.uasset differ diff --git a/Content/Sample/Common/Audio/StoneOnBush.uasset b/Content/Sample/Common/Audio/StoneOnBush.uasset new file mode 100644 index 0000000..17aeee6 Binary files /dev/null and b/Content/Sample/Common/Audio/StoneOnBush.uasset differ diff --git a/Content/Sample/Common/Audio/StoneOnGlass.uasset b/Content/Sample/Common/Audio/StoneOnGlass.uasset new file mode 100644 index 0000000..2c24a70 Binary files /dev/null and b/Content/Sample/Common/Audio/StoneOnGlass.uasset differ diff --git a/Content/Sample/Common/Audio/StoneOnStone.uasset b/Content/Sample/Common/Audio/StoneOnStone.uasset new file mode 100644 index 0000000..e19e5c6 Binary files /dev/null and b/Content/Sample/Common/Audio/StoneOnStone.uasset differ diff --git a/Content/Sample/Common/Audio/StoneOnWater.uasset b/Content/Sample/Common/Audio/StoneOnWater.uasset new file mode 100644 index 0000000..0c7432b Binary files /dev/null and b/Content/Sample/Common/Audio/StoneOnWater.uasset differ diff --git a/Content/Sample/Common/Audio/StoneOnWood.uasset b/Content/Sample/Common/Audio/StoneOnWood.uasset new file mode 100644 index 0000000..6ed1379 Binary files /dev/null and b/Content/Sample/Common/Audio/StoneOnWood.uasset differ diff --git a/Content/Sample/Common/Audio/Stylized_Music.uasset b/Content/Sample/Common/Audio/Stylized_Music.uasset new file mode 100644 index 0000000..55d2f65 Binary files /dev/null and b/Content/Sample/Common/Audio/Stylized_Music.uasset differ diff --git a/Content/Sample/Common/Audio/Stylized_Water01.uasset b/Content/Sample/Common/Audio/Stylized_Water01.uasset new file mode 100644 index 0000000..47f9e31 Binary files /dev/null and b/Content/Sample/Common/Audio/Stylized_Water01.uasset differ diff --git a/Content/Sample/Common/Audio/Stylized_Water02.uasset b/Content/Sample/Common/Audio/Stylized_Water02.uasset new file mode 100644 index 0000000..935f05f Binary files /dev/null and b/Content/Sample/Common/Audio/Stylized_Water02.uasset differ diff --git a/Content/Sample/Common/Audio/Stylized_WindMain01.uasset b/Content/Sample/Common/Audio/Stylized_WindMain01.uasset new file mode 100644 index 0000000..9567b93 Binary files /dev/null and b/Content/Sample/Common/Audio/Stylized_WindMain01.uasset differ diff --git a/Content/Sample/Common/Audio/Stylized_WindTrees01.uasset b/Content/Sample/Common/Audio/Stylized_WindTrees01.uasset new file mode 100644 index 0000000..3bd2a42 Binary files /dev/null and b/Content/Sample/Common/Audio/Stylized_WindTrees01.uasset differ diff --git a/Content/Sample/Common/Audio/Throw.uasset b/Content/Sample/Common/Audio/Throw.uasset new file mode 100644 index 0000000..7adde65 Binary files /dev/null and b/Content/Sample/Common/Audio/Throw.uasset differ diff --git a/Content/Sample/Common/Audio/Throw1.uasset b/Content/Sample/Common/Audio/Throw1.uasset new file mode 100644 index 0000000..8b39ae9 Binary files /dev/null and b/Content/Sample/Common/Audio/Throw1.uasset differ diff --git a/Content/Sample/Common/Audio/Throw2.uasset b/Content/Sample/Common/Audio/Throw2.uasset new file mode 100644 index 0000000..7c6b933 Binary files /dev/null and b/Content/Sample/Common/Audio/Throw2.uasset differ diff --git a/Content/Sample/Common/Audio/Throw3.uasset b/Content/Sample/Common/Audio/Throw3.uasset new file mode 100644 index 0000000..5bd3ba7 Binary files /dev/null and b/Content/Sample/Common/Audio/Throw3.uasset differ diff --git a/Content/Sample/Common/Audio/Throw4.uasset b/Content/Sample/Common/Audio/Throw4.uasset new file mode 100644 index 0000000..e5e7fa1 Binary files /dev/null and b/Content/Sample/Common/Audio/Throw4.uasset differ diff --git a/Content/Sample/Common/Audio/WeaponFire02.uasset b/Content/Sample/Common/Audio/WeaponFire02.uasset new file mode 100644 index 0000000..c9ecce2 Binary files /dev/null and b/Content/Sample/Common/Audio/WeaponFire02.uasset differ diff --git a/Content/Sample/Common/Audio/Wind.uasset b/Content/Sample/Common/Audio/Wind.uasset new file mode 100644 index 0000000..d1625ab Binary files /dev/null and b/Content/Sample/Common/Audio/Wind.uasset differ diff --git a/Content/Sample/Common/Blueprints/BPI_Attackable.uasset b/Content/Sample/Common/Blueprints/BPI_Attackable.uasset new file mode 100644 index 0000000..1fae23b Binary files /dev/null and b/Content/Sample/Common/Blueprints/BPI_Attackable.uasset differ diff --git a/Content/Sample/Common/Blueprints/BPI_Control.uasset b/Content/Sample/Common/Blueprints/BPI_Control.uasset new file mode 100644 index 0000000..fad2125 Binary files /dev/null and b/Content/Sample/Common/Blueprints/BPI_Control.uasset differ diff --git a/Content/Sample/Common/Blueprints/BPI_CustomHighlight.uasset b/Content/Sample/Common/Blueprints/BPI_CustomHighlight.uasset new file mode 100644 index 0000000..c923de9 Binary files /dev/null and b/Content/Sample/Common/Blueprints/BPI_CustomHighlight.uasset differ diff --git a/Content/Sample/Common/Blueprints/BPI_FocusInteractable.uasset b/Content/Sample/Common/Blueprints/BPI_FocusInteractable.uasset new file mode 100644 index 0000000..238434c Binary files /dev/null and b/Content/Sample/Common/Blueprints/BPI_FocusInteractable.uasset differ diff --git a/Content/Sample/Common/Blueprints/BPI_SceneSelector.uasset b/Content/Sample/Common/Blueprints/BPI_SceneSelector.uasset new file mode 100644 index 0000000..fd2bdc5 Binary files /dev/null and b/Content/Sample/Common/Blueprints/BPI_SceneSelector.uasset differ diff --git a/Content/Sample/Common/Blueprints/BPL_TobiiSamples.uasset b/Content/Sample/Common/Blueprints/BPL_TobiiSamples.uasset new file mode 100644 index 0000000..d8bf291 Binary files /dev/null and b/Content/Sample/Common/Blueprints/BPL_TobiiSamples.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_ArrowTrail.uasset b/Content/Sample/Common/Blueprints/BP_ArrowTrail.uasset new file mode 100644 index 0000000..0d15709 Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_ArrowTrail.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_BarrelWithBottles.uasset b/Content/Sample/Common/Blueprints/BP_BarrelWithBottles.uasset new file mode 100644 index 0000000..bf21b6a Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_BarrelWithBottles.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_BaseExamplePlayerController.uasset b/Content/Sample/Common/Blueprints/BP_BaseExamplePlayerController.uasset new file mode 100644 index 0000000..f1f8c4f Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_BaseExamplePlayerController.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_Bird.uasset b/Content/Sample/Common/Blueprints/BP_Bird.uasset new file mode 100644 index 0000000..fb12cc4 Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_Bird.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_BirdPatch.uasset b/Content/Sample/Common/Blueprints/BP_BirdPatch.uasset new file mode 100644 index 0000000..6ef32ab Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_BirdPatch.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_BirdSign.uasset b/Content/Sample/Common/Blueprints/BP_BirdSign.uasset new file mode 100644 index 0000000..75516d9 Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_BirdSign.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_BirdSpawner.uasset b/Content/Sample/Common/Blueprints/BP_BirdSpawner.uasset new file mode 100644 index 0000000..7af618e Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_BirdSpawner.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_Bottle.uasset b/Content/Sample/Common/Blueprints/BP_Bottle.uasset new file mode 100644 index 0000000..c80860f Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_Bottle.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_ButtonPillar.uasset b/Content/Sample/Common/Blueprints/BP_ButtonPillar.uasset new file mode 100644 index 0000000..93ccdda Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_ButtonPillar.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_EyetrackingToggle.uasset b/Content/Sample/Common/Blueprints/BP_EyetrackingToggle.uasset new file mode 100644 index 0000000..bcb96f5 Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_EyetrackingToggle.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_FocusableBox.uasset b/Content/Sample/Common/Blueprints/BP_FocusableBox.uasset new file mode 100644 index 0000000..45e814f Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_FocusableBox.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_GazeVisualizer.uasset b/Content/Sample/Common/Blueprints/BP_GazeVisualizer.uasset new file mode 100644 index 0000000..54a32cf Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_GazeVisualizer.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_InfoSign.uasset b/Content/Sample/Common/Blueprints/BP_InfoSign.uasset new file mode 100644 index 0000000..e9e279e Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_InfoSign.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_InfoSignPage.uasset b/Content/Sample/Common/Blueprints/BP_InfoSignPage.uasset new file mode 100644 index 0000000..3e47636 Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_InfoSignPage.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_InfoSignPage_DebugTool.uasset b/Content/Sample/Common/Blueprints/BP_InfoSignPage_DebugTool.uasset new file mode 100644 index 0000000..a0273d6 Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_InfoSignPage_DebugTool.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_InfoSignPage_SceneSelector.uasset b/Content/Sample/Common/Blueprints/BP_InfoSignPage_SceneSelector.uasset new file mode 100644 index 0000000..2ef272a Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_InfoSignPage_SceneSelector.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_InteractableItem.uasset b/Content/Sample/Common/Blueprints/BP_InteractableItem.uasset new file mode 100644 index 0000000..c65b9d0 Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_InteractableItem.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_MirrorRobot.uasset b/Content/Sample/Common/Blueprints/BP_MirrorRobot.uasset new file mode 100644 index 0000000..0b14816 Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_MirrorRobot.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_MovingFocusableBox.uasset b/Content/Sample/Common/Blueprints/BP_MovingFocusableBox.uasset new file mode 100644 index 0000000..8678c53 Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_MovingFocusableBox.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_SceneSelector.uasset b/Content/Sample/Common/Blueprints/BP_SceneSelector.uasset new file mode 100644 index 0000000..def145d Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_SceneSelector.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_Tree.uasset b/Content/Sample/Common/Blueprints/BP_Tree.uasset new file mode 100644 index 0000000..d267591 Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_Tree.uasset differ diff --git a/Content/Sample/Common/Blueprints/BP_Window.uasset b/Content/Sample/Common/Blueprints/BP_Window.uasset new file mode 100644 index 0000000..4097e59 Binary files /dev/null and b/Content/Sample/Common/Blueprints/BP_Window.uasset differ diff --git a/Content/Sample/Common/Blueprints/E_InfoPage.uasset b/Content/Sample/Common/Blueprints/E_InfoPage.uasset new file mode 100644 index 0000000..89d8b3f Binary files /dev/null and b/Content/Sample/Common/Blueprints/E_InfoPage.uasset differ diff --git a/Content/Sample/Common/Blueprints/W_BirdGrenadeLove.uasset b/Content/Sample/Common/Blueprints/W_BirdGrenadeLove.uasset new file mode 100644 index 0000000..e433362 Binary files /dev/null and b/Content/Sample/Common/Blueprints/W_BirdGrenadeLove.uasset differ diff --git a/Content/Sample/Common/Blueprints/W_CalibrationSceneInfo_DebugToolGuide.uasset b/Content/Sample/Common/Blueprints/W_CalibrationSceneInfo_DebugToolGuide.uasset new file mode 100644 index 0000000..7576ed9 Binary files /dev/null and b/Content/Sample/Common/Blueprints/W_CalibrationSceneInfo_DebugToolGuide.uasset differ diff --git a/Content/Sample/Common/Blueprints/W_CalibrationSceneInfo_SceneSelectorGuide.uasset b/Content/Sample/Common/Blueprints/W_CalibrationSceneInfo_SceneSelectorGuide.uasset new file mode 100644 index 0000000..3b2622a Binary files /dev/null and b/Content/Sample/Common/Blueprints/W_CalibrationSceneInfo_SceneSelectorGuide.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Materials/Fire/M_Fire_01_Add_6X6_INST.uasset b/Content/Sample/Common/Effects/FX_Materials/Fire/M_Fire_01_Add_6X6_INST.uasset new file mode 100644 index 0000000..0756f39 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Materials/Fire/M_Fire_01_Add_6X6_INST.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Materials/Fire/M_Fire_Sheet_01_INST.uasset b/Content/Sample/Common/Effects/FX_Materials/Fire/M_Fire_Sheet_01_INST.uasset new file mode 100644 index 0000000..5be8417 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Materials/Fire/M_Fire_Sheet_01_INST.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Materials/Gradient/M_Flare_02_INST.uasset b/Content/Sample/Common/Effects/FX_Materials/Gradient/M_Flare_02_INST.uasset new file mode 100644 index 0000000..93333db Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Materials/Gradient/M_Flare_02_INST.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Materials/Gradient/M_Flare_03_INST.uasset b/Content/Sample/Common/Effects/FX_Materials/Gradient/M_Flare_03_INST.uasset new file mode 100644 index 0000000..a747e7e Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Materials/Gradient/M_Flare_03_INST.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Materials/Gradient/M_Flare_04_INST.uasset b/Content/Sample/Common/Effects/FX_Materials/Gradient/M_Flare_04_INST.uasset new file mode 100644 index 0000000..25b1cb6 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Materials/Gradient/M_Flare_04_INST.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Materials/Gradient/M_Flare_07_INST.uasset b/Content/Sample/Common/Effects/FX_Materials/Gradient/M_Flare_07_INST.uasset new file mode 100644 index 0000000..0b5fde5 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Materials/Gradient/M_Flare_07_INST.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Materials/Gradient/M_Flare_Streak_01_INST.uasset b/Content/Sample/Common/Effects/FX_Materials/Gradient/M_Flare_Streak_01_INST.uasset new file mode 100644 index 0000000..ae84042 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Materials/Gradient/M_Flare_Streak_01_INST.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Materials/Gradient/M_Flare_Vertical_02_Inst.uasset b/Content/Sample/Common/Effects/FX_Materials/Gradient/M_Flare_Vertical_02_Inst.uasset new file mode 100644 index 0000000..74ae8ed Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Materials/Gradient/M_Flare_Vertical_02_Inst.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Materials/ICE/M_IceChunksBase.uasset b/Content/Sample/Common/Effects/FX_Materials/ICE/M_IceChunksBase.uasset new file mode 100644 index 0000000..dbb026c Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Materials/ICE/M_IceChunksBase.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Materials/ICE/M_IceSkills_Base_Lit.uasset b/Content/Sample/Common/Effects/FX_Materials/ICE/M_IceSkills_Base_Lit.uasset new file mode 100644 index 0000000..c5dbbd2 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Materials/ICE/M_IceSkills_Base_Lit.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Materials/ICE/M_Ice_Solid_01_INST.uasset b/Content/Sample/Common/Effects/FX_Materials/ICE/M_Ice_Solid_01_INST.uasset new file mode 100644 index 0000000..62f1f6e Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Materials/ICE/M_Ice_Solid_01_INST.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Materials/ICE/M_JaggedRockIce_MeshEmit.uasset b/Content/Sample/Common/Effects/FX_Materials/ICE/M_JaggedRockIce_MeshEmit.uasset new file mode 100644 index 0000000..a9c804b Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Materials/ICE/M_JaggedRockIce_MeshEmit.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Materials/Liquid/M_Liquid_Spray_01_4X6_INST.uasset b/Content/Sample/Common/Effects/FX_Materials/Liquid/M_Liquid_Spray_01_4X6_INST.uasset new file mode 100644 index 0000000..9a1c1ab Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Materials/Liquid/M_Liquid_Spray_01_4X6_INST.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Materials/M_Shockwave_01_ADD_INST.uasset b/Content/Sample/Common/Effects/FX_Materials/M_Shockwave_01_ADD_INST.uasset new file mode 100644 index 0000000..18ff9f0 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Materials/M_Shockwave_01_ADD_INST.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Materials/Smoke/M_Smoke_01_8X8_INST.uasset b/Content/Sample/Common/Effects/FX_Materials/Smoke/M_Smoke_01_8X8_INST.uasset new file mode 100644 index 0000000..737626b Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Materials/Smoke/M_Smoke_01_8X8_INST.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Materials/Spark/M_Spark_03_1_4_INST.uasset b/Content/Sample/Common/Effects/FX_Materials/Spark/M_Spark_03_1_4_INST.uasset new file mode 100644 index 0000000..eda9dad Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Materials/Spark/M_Spark_03_1_4_INST.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Materials/Trails/M_Swipe_Mesh_Add_2Sided_01_INST.uasset b/Content/Sample/Common/Effects/FX_Materials/Trails/M_Swipe_Mesh_Add_2Sided_01_INST.uasset new file mode 100644 index 0000000..8040263 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Materials/Trails/M_Swipe_Mesh_Add_2Sided_01_INST.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Meshes/ICE/SM_IcePiece01.uasset b/Content/Sample/Common/Effects/FX_Meshes/ICE/SM_IcePiece01.uasset new file mode 100644 index 0000000..4122da6 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Meshes/ICE/SM_IcePiece01.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Meshes/ICE/SM_IceSkill_AOE.uasset b/Content/Sample/Common/Effects/FX_Meshes/ICE/SM_IceSkill_AOE.uasset new file mode 100644 index 0000000..8991a33 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Meshes/ICE/SM_IceSkill_AOE.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Meshes/ICE/SM_IceSkill_AOE_outer.uasset b/Content/Sample/Common/Effects/FX_Meshes/ICE/SM_IceSkill_AOE_outer.uasset new file mode 100644 index 0000000..287563f Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Meshes/ICE/SM_IceSkill_AOE_outer.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Meshes/Skills/SM_FireBlastMesh.uasset b/Content/Sample/Common/Effects/FX_Meshes/Skills/SM_FireBlastMesh.uasset new file mode 100644 index 0000000..549c64c Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Meshes/Skills/SM_FireBlastMesh.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Meshes/Skills/SM_SpinSpiral.uasset b/Content/Sample/Common/Effects/FX_Meshes/Skills/SM_SpinSpiral.uasset new file mode 100644 index 0000000..9dc3c41 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Meshes/Skills/SM_SpinSpiral.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Mobile/ICE/P_levelUp_Ice.uasset b/Content/Sample/Common/Effects/FX_Mobile/ICE/P_levelUp_Ice.uasset new file mode 100644 index 0000000..31fc850 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Mobile/ICE/P_levelUp_Ice.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Mobile/ICE/combat/P_AOE_Ice_CircleAttack.uasset b/Content/Sample/Common/Effects/FX_Mobile/ICE/combat/P_AOE_Ice_CircleAttack.uasset new file mode 100644 index 0000000..bc78894 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Mobile/ICE/combat/P_AOE_Ice_CircleAttack.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Monsters/FX_Monster_FrostGiant/ICE/P_ProjectileLob_Explo_Ice.uasset b/Content/Sample/Common/Effects/FX_Monsters/FX_Monster_FrostGiant/ICE/P_ProjectileLob_Explo_Ice.uasset new file mode 100644 index 0000000..bd785e3 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Monsters/FX_Monster_FrostGiant/ICE/P_ProjectileLob_Explo_Ice.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Textures/Fire/T_FireBlastTile.uasset b/Content/Sample/Common/Effects/FX_Textures/Fire/T_FireBlastTile.uasset new file mode 100644 index 0000000..41db917 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Textures/Fire/T_FireBlastTile.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Textures/Fire/T_Fire_subUV_6X6.uasset b/Content/Sample/Common/Effects/FX_Textures/Fire/T_Fire_subUV_6X6.uasset new file mode 100644 index 0000000..a2b1854 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Textures/Fire/T_Fire_subUV_6X6.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Textures/Gradient/T_BlastWave.uasset b/Content/Sample/Common/Effects/FX_Textures/Gradient/T_BlastWave.uasset new file mode 100644 index 0000000..5f4b5be Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Textures/Gradient/T_BlastWave.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Textures/Gradient/T_Flare_Streaks_02.uasset b/Content/Sample/Common/Effects/FX_Textures/Gradient/T_Flare_Streaks_02.uasset new file mode 100644 index 0000000..a822160 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Textures/Gradient/T_Flare_Streaks_02.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Textures/Gradient/T_GlowFlare_Circular_02.uasset b/Content/Sample/Common/Effects/FX_Textures/Gradient/T_GlowFlare_Circular_02.uasset new file mode 100644 index 0000000..82edac2 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Textures/Gradient/T_GlowFlare_Circular_02.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Textures/Gradient/T_LightCone.uasset b/Content/Sample/Common/Effects/FX_Textures/Gradient/T_LightCone.uasset new file mode 100644 index 0000000..b20a0a1 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Textures/Gradient/T_LightCone.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Textures/Gradient/T_SoftFallOff.uasset b/Content/Sample/Common/Effects/FX_Textures/Gradient/T_SoftFallOff.uasset new file mode 100644 index 0000000..8501e23 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Textures/Gradient/T_SoftFallOff.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Textures/Gradient/T_SwipeTrail.uasset b/Content/Sample/Common/Effects/FX_Textures/Gradient/T_SwipeTrail.uasset new file mode 100644 index 0000000..593d896 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Textures/Gradient/T_SwipeTrail.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Textures/Gradient/T_starFlare_02.uasset b/Content/Sample/Common/Effects/FX_Textures/Gradient/T_starFlare_02.uasset new file mode 100644 index 0000000..107eb3c Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Textures/Gradient/T_starFlare_02.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Textures/Gradient/T_starFlare_03.uasset b/Content/Sample/Common/Effects/FX_Textures/Gradient/T_starFlare_03.uasset new file mode 100644 index 0000000..88f742d Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Textures/Gradient/T_starFlare_03.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Textures/Gradient/T_starFlare_4.uasset b/Content/Sample/Common/Effects/FX_Textures/Gradient/T_starFlare_4.uasset new file mode 100644 index 0000000..8c12a95 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Textures/Gradient/T_starFlare_4.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Textures/Ice/T_IceChunks.uasset b/Content/Sample/Common/Effects/FX_Textures/Ice/T_IceChunks.uasset new file mode 100644 index 0000000..ecebb51 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Textures/Ice/T_IceChunks.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Textures/Ice/T_IceShards_E.uasset b/Content/Sample/Common/Effects/FX_Textures/Ice/T_IceShards_E.uasset new file mode 100644 index 0000000..a8e3188 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Textures/Ice/T_IceShards_E.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Textures/Ice/T_IceStrips.uasset b/Content/Sample/Common/Effects/FX_Textures/Ice/T_IceStrips.uasset new file mode 100644 index 0000000..400fe40 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Textures/Ice/T_IceStrips.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Textures/Ice/T_JaggedRockSnowy_01_D.uasset b/Content/Sample/Common/Effects/FX_Textures/Ice/T_JaggedRockSnowy_01_D.uasset new file mode 100644 index 0000000..ec902fa Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Textures/Ice/T_JaggedRockSnowy_01_D.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Textures/Ice/T_JaggedRockSnowy_01_N.uasset b/Content/Sample/Common/Effects/FX_Textures/Ice/T_JaggedRockSnowy_01_N.uasset new file mode 100644 index 0000000..7610966 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Textures/Ice/T_JaggedRockSnowy_01_N.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Textures/Liquid/T_bloodTest_4X6.uasset b/Content/Sample/Common/Effects/FX_Textures/Liquid/T_bloodTest_4X6.uasset new file mode 100644 index 0000000..43d5f73 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Textures/Liquid/T_bloodTest_4X6.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Textures/Smoke/T_SmokeBall_01_8_8.uasset b/Content/Sample/Common/Effects/FX_Textures/Smoke/T_SmokeBall_01_8_8.uasset new file mode 100644 index 0000000..d55bf4c Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Textures/Smoke/T_SmokeBall_01_8_8.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Textures/Spark/T_Sparks_03_SubUV_1_4.uasset b/Content/Sample/Common/Effects/FX_Textures/Spark/T_Sparks_03_SubUV_1_4.uasset new file mode 100644 index 0000000..f862c15 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Textures/Spark/T_Sparks_03_SubUV_1_4.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Textures/Texture/T_Ice_Mesh02b.uasset b/Content/Sample/Common/Effects/FX_Textures/Texture/T_Ice_Mesh02b.uasset new file mode 100644 index 0000000..e52af2c Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Textures/Texture/T_Ice_Mesh02b.uasset differ diff --git a/Content/Sample/Common/Effects/FX_Textures/Tile/T_Black_32.uasset b/Content/Sample/Common/Effects/FX_Textures/Tile/T_Black_32.uasset new file mode 100644 index 0000000..703afb9 Binary files /dev/null and b/Content/Sample/Common/Effects/FX_Textures/Tile/T_Black_32.uasset differ diff --git a/Content/Sample/Common/Effects/Masters/M_Add_Sprite_2Sided_Master.uasset b/Content/Sample/Common/Effects/Masters/M_Add_Sprite_2Sided_Master.uasset new file mode 100644 index 0000000..2f051b9 Binary files /dev/null and b/Content/Sample/Common/Effects/Masters/M_Add_Sprite_2Sided_Master.uasset differ diff --git a/Content/Sample/Common/Effects/Masters/M_Add_Sprite_Master.uasset b/Content/Sample/Common/Effects/Masters/M_Add_Sprite_Master.uasset new file mode 100644 index 0000000..c9ed191 Binary files /dev/null and b/Content/Sample/Common/Effects/Masters/M_Add_Sprite_Master.uasset differ diff --git a/Content/Sample/Common/Effects/Masters/M_Add_Sprite_Master_DepthFade.uasset b/Content/Sample/Common/Effects/Masters/M_Add_Sprite_Master_DepthFade.uasset new file mode 100644 index 0000000..a57ad5d Binary files /dev/null and b/Content/Sample/Common/Effects/Masters/M_Add_Sprite_Master_DepthFade.uasset differ diff --git a/Content/Sample/Common/Effects/Masters/M_Add_Sprite_SubUV_Master.uasset b/Content/Sample/Common/Effects/Masters/M_Add_Sprite_SubUV_Master.uasset new file mode 100644 index 0000000..c1a9ba9 Binary files /dev/null and b/Content/Sample/Common/Effects/Masters/M_Add_Sprite_SubUV_Master.uasset differ diff --git a/Content/Sample/Common/Effects/Masters/M_Add_Trail_2Sided_Pan_Master.uasset b/Content/Sample/Common/Effects/Masters/M_Add_Trail_2Sided_Pan_Master.uasset new file mode 100644 index 0000000..09443ec Binary files /dev/null and b/Content/Sample/Common/Effects/Masters/M_Add_Trail_2Sided_Pan_Master.uasset differ diff --git a/Content/Sample/Common/Effects/Masters/M_Opaque_MeshEmit_Emis_Master.uasset b/Content/Sample/Common/Effects/Masters/M_Opaque_MeshEmit_Emis_Master.uasset new file mode 100644 index 0000000..c436201 Binary files /dev/null and b/Content/Sample/Common/Effects/Masters/M_Opaque_MeshEmit_Emis_Master.uasset differ diff --git a/Content/Sample/Common/Effects/Masters/M_Trans_Sprite_Master_DepthFade.uasset b/Content/Sample/Common/Effects/Masters/M_Trans_Sprite_Master_DepthFade.uasset new file mode 100644 index 0000000..9c2e12e Binary files /dev/null and b/Content/Sample/Common/Effects/Masters/M_Trans_Sprite_Master_DepthFade.uasset differ diff --git a/Content/Sample/Common/Effects/PS_Beam.uasset b/Content/Sample/Common/Effects/PS_Beam.uasset new file mode 100644 index 0000000..9f17f52 Binary files /dev/null and b/Content/Sample/Common/Effects/PS_Beam.uasset differ diff --git a/Content/Sample/Common/Effects/P_FireBombExplosion.uasset b/Content/Sample/Common/Effects/P_FireBombExplosion.uasset new file mode 100644 index 0000000..614008d Binary files /dev/null and b/Content/Sample/Common/Effects/P_FireBombExplosion.uasset differ diff --git a/Content/Sample/Common/Environment/Levels/ExampleEnvironmentLevel.umap b/Content/Sample/Common/Environment/Levels/ExampleEnvironmentLevel.umap new file mode 100644 index 0000000..00917b3 Binary files /dev/null and b/Content/Sample/Common/Environment/Levels/ExampleEnvironmentLevel.umap differ diff --git a/Content/Sample/Common/Environment/Levels/LayerInfoObject_Beach.uasset b/Content/Sample/Common/Environment/Levels/LayerInfoObject_Beach.uasset new file mode 100644 index 0000000..f04df1e Binary files /dev/null and b/Content/Sample/Common/Environment/Levels/LayerInfoObject_Beach.uasset differ diff --git a/Content/Sample/Common/Environment/Levels/LayerInfoObject_Grass.uasset b/Content/Sample/Common/Environment/Levels/LayerInfoObject_Grass.uasset new file mode 100644 index 0000000..12de8ba Binary files /dev/null and b/Content/Sample/Common/Environment/Levels/LayerInfoObject_Grass.uasset differ diff --git a/Content/Sample/Common/Environment/Levels/LayerInfoObject_Path.uasset b/Content/Sample/Common/Environment/Levels/LayerInfoObject_Path.uasset new file mode 100644 index 0000000..b6050dd Binary files /dev/null and b/Content/Sample/Common/Environment/Levels/LayerInfoObject_Path.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/Fake_Lighting_MPC.uasset b/Content/Sample/Common/Environment/Materials/Fake_Lighting_MPC.uasset new file mode 100644 index 0000000..781e364 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/Fake_Lighting_MPC.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_BackgroundHouses_A.uasset b/Content/Sample/Common/Environment/Materials/M_BackgroundHouses_A.uasset new file mode 100644 index 0000000..f13d987 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_BackgroundHouses_A.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Bush_Clumps.uasset b/Content/Sample/Common/Environment/Materials/M_Bush_Clumps.uasset new file mode 100644 index 0000000..e2f6234 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Bush_Clumps.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Bush_Lines.uasset b/Content/Sample/Common/Environment/Materials/M_Bush_Lines.uasset new file mode 100644 index 0000000..141b9c2 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Bush_Lines.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Cloud_Solid_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_Cloud_Solid_Inst.uasset new file mode 100644 index 0000000..7e62b35 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Cloud_Solid_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Flag_Waving.uasset b/Content/Sample/Common/Environment/Materials/M_Flag_Waving.uasset new file mode 100644 index 0000000..48420e9 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Flag_Waving.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Flag_Waving_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_Flag_Waving_Inst.uasset new file mode 100644 index 0000000..e081690 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Flag_Waving_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Grass_Masked.uasset b/Content/Sample/Common/Environment/Materials/M_Grass_Masked.uasset new file mode 100644 index 0000000..1029d26 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Grass_Masked.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Grass_Masked_B_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_Grass_Masked_B_Inst.uasset new file mode 100644 index 0000000..3890aa8 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Grass_Masked_B_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Grass_Masked_C_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_Grass_Masked_C_Inst.uasset new file mode 100644 index 0000000..00940ec Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Grass_Masked_C_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Grass_Masked_D_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_Grass_Masked_D_Inst.uasset new file mode 100644 index 0000000..73593e1 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Grass_Masked_D_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Grass_Masked_E_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_Grass_Masked_E_Inst.uasset new file mode 100644 index 0000000..61622b3 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Grass_Masked_E_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Grass_Masked_Water_A_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_Grass_Masked_Water_A_Inst.uasset new file mode 100644 index 0000000..81bf376 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Grass_Masked_Water_A_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Mushroom_A.uasset b/Content/Sample/Common/Environment/Materials/M_Mushroom_A.uasset new file mode 100644 index 0000000..617aab0 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Mushroom_A.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Mushroom_A_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_Mushroom_A_Inst.uasset new file mode 100644 index 0000000..78383eb Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Mushroom_A_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_PostProcess_Base.uasset b/Content/Sample/Common/Environment/Materials/M_PostProcess_Base.uasset new file mode 100644 index 0000000..2ee0be6 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_PostProcess_Base.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_PostProcess_Base_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_PostProcess_Base_Inst.uasset new file mode 100644 index 0000000..8a7c585 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_PostProcess_Base_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_PostProcess_Paper.uasset b/Content/Sample/Common/Environment/Materials/M_PostProcess_Paper.uasset new file mode 100644 index 0000000..881f902 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_PostProcess_Paper.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_PostProcess_Paper_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_PostProcess_Paper_Inst.uasset new file mode 100644 index 0000000..3e6a469 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_PostProcess_Paper_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Skybox_A.uasset b/Content/Sample/Common/Environment/Materials/M_Skybox_A.uasset new file mode 100644 index 0000000..61f231e Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Skybox_A.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Skybox_A_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_Skybox_A_Inst.uasset new file mode 100644 index 0000000..364f532 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Skybox_A_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_StoneWall_A_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_StoneWall_A_Inst.uasset new file mode 100644 index 0000000..8005142 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_StoneWall_A_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_StoneWall_FlatColor.uasset b/Content/Sample/Common/Environment/Materials/M_StoneWall_FlatColor.uasset new file mode 100644 index 0000000..779b69b Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_StoneWall_FlatColor.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_StoneWall_FlatColor_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_StoneWall_FlatColor_Inst.uasset new file mode 100644 index 0000000..691d098 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_StoneWall_FlatColor_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_StoneWall_FlatColor_Inst2.uasset b/Content/Sample/Common/Environment/Materials/M_StoneWall_FlatColor_Inst2.uasset new file mode 100644 index 0000000..f8801dc Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_StoneWall_FlatColor_Inst2.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_StoneWall_FlatColor_Inst3.uasset b/Content/Sample/Common/Environment/Materials/M_StoneWall_FlatColor_Inst3.uasset new file mode 100644 index 0000000..335f7c4 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_StoneWall_FlatColor_Inst3.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_StoneWall_Master.uasset b/Content/Sample/Common/Environment/Materials/M_StoneWall_Master.uasset new file mode 100644 index 0000000..fb60084 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_StoneWall_Master.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Stone_Path.uasset b/Content/Sample/Common/Environment/Materials/M_Stone_Path.uasset new file mode 100644 index 0000000..c3e69c1 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Stone_Path.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Stone_Path_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_Stone_Path_Inst.uasset new file mode 100644 index 0000000..822cf75 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Stone_Path_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Sun_Sphere.uasset b/Content/Sample/Common/Environment/Materials/M_Sun_Sphere.uasset new file mode 100644 index 0000000..9e9c8c6 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Sun_Sphere.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Terrain_Ground_A.uasset b/Content/Sample/Common/Environment/Materials/M_Terrain_Ground_A.uasset new file mode 100644 index 0000000..9222110 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Terrain_Ground_A.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Terrain_Ground_A_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_Terrain_Ground_A_Inst.uasset new file mode 100644 index 0000000..d66846f Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Terrain_Ground_A_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Tree_Background.uasset b/Content/Sample/Common/Environment/Materials/M_Tree_Background.uasset new file mode 100644 index 0000000..c28d5f0 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Tree_Background.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Tree_Background_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_Tree_Background_Inst.uasset new file mode 100644 index 0000000..6b01f3e Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Tree_Background_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Tree_Bark_A.uasset b/Content/Sample/Common/Environment/Materials/M_Tree_Bark_A.uasset new file mode 100644 index 0000000..26a42fd Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Tree_Bark_A.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Tree_Bark_A_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_Tree_Bark_A_Inst.uasset new file mode 100644 index 0000000..d91e6af Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Tree_Bark_A_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Tree_Leaf_Base.uasset b/Content/Sample/Common/Environment/Materials/M_Tree_Leaf_Base.uasset new file mode 100644 index 0000000..21daa5f Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Tree_Leaf_Base.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Tree_Leaf_Base_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_Tree_Leaf_Base_Inst.uasset new file mode 100644 index 0000000..4db8a00 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Tree_Leaf_Base_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Tree_Leaf_Base_Solid.uasset b/Content/Sample/Common/Environment/Materials/M_Tree_Leaf_Base_Solid.uasset new file mode 100644 index 0000000..9b5bd1e Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Tree_Leaf_Base_Solid.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Tree_Leaf_Base_Solid_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_Tree_Leaf_Base_Solid_Inst.uasset new file mode 100644 index 0000000..60db989 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Tree_Leaf_Base_Solid_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Tree_Type_B_Bark.uasset b/Content/Sample/Common/Environment/Materials/M_Tree_Type_B_Bark.uasset new file mode 100644 index 0000000..43053a9 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Tree_Type_B_Bark.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Tree_Type_B_Bark_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_Tree_Type_B_Bark_Inst.uasset new file mode 100644 index 0000000..9ebe602 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Tree_Type_B_Bark_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Water_Surface.uasset b/Content/Sample/Common/Environment/Materials/M_Water_Surface.uasset new file mode 100644 index 0000000..aaaf134 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Water_Surface.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Water_Waves.uasset b/Content/Sample/Common/Environment/Materials/M_Water_Waves.uasset new file mode 100644 index 0000000..c863874 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Water_Waves.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Water_Waves_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_Water_Waves_Inst.uasset new file mode 100644 index 0000000..dd84a57 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Water_Waves_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Wood_Sheet_Fence_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_Wood_Sheet_Fence_Inst.uasset new file mode 100644 index 0000000..f0acccf Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Wood_Sheet_Fence_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Wood_Sheet_Master.uasset b/Content/Sample/Common/Environment/Materials/M_Wood_Sheet_Master.uasset new file mode 100644 index 0000000..c95353d Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Wood_Sheet_Master.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Wood_Sheet_Pillar_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_Wood_Sheet_Pillar_Inst.uasset new file mode 100644 index 0000000..1d160a8 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Wood_Sheet_Pillar_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_Wood_Sheet_Rings_Inst.uasset b/Content/Sample/Common/Environment/Materials/M_Wood_Sheet_Rings_Inst.uasset new file mode 100644 index 0000000..bbd3b1e Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_Wood_Sheet_Rings_Inst.uasset differ diff --git a/Content/Sample/Common/Environment/Materials/M_cloud_Solid.uasset b/Content/Sample/Common/Environment/Materials/M_cloud_Solid.uasset new file mode 100644 index 0000000..2d378a1 Binary files /dev/null and b/Content/Sample/Common/Environment/Materials/M_cloud_Solid.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_BackgroundHouses_A.uasset b/Content/Sample/Common/Environment/Meshes/SM_BackgroundHouses_A.uasset new file mode 100644 index 0000000..aff69ae Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_BackgroundHouses_A.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Bushes_Cluster_A.uasset b/Content/Sample/Common/Environment/Meshes/SM_Bushes_Cluster_A.uasset new file mode 100644 index 0000000..218c638 Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Bushes_Cluster_A.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Castle_BlockRoof_A.uasset b/Content/Sample/Common/Environment/Meshes/SM_Castle_BlockRoof_A.uasset new file mode 100644 index 0000000..04b685a Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Castle_BlockRoof_A.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Castle_Tower_A.uasset b/Content/Sample/Common/Environment/Meshes/SM_Castle_Tower_A.uasset new file mode 100644 index 0000000..913141d Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Castle_Tower_A.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Castle_Tower_B.uasset b/Content/Sample/Common/Environment/Meshes/SM_Castle_Tower_B.uasset new file mode 100644 index 0000000..2286fc9 Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Castle_Tower_B.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Castle_Wall_A.uasset b/Content/Sample/Common/Environment/Meshes/SM_Castle_Wall_A.uasset new file mode 100644 index 0000000..c141e20 Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Castle_Wall_A.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Castle_Wall_Top.uasset b/Content/Sample/Common/Environment/Meshes/SM_Castle_Wall_Top.uasset new file mode 100644 index 0000000..c39dfc3 Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Castle_Wall_Top.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Castle_Window_A.uasset b/Content/Sample/Common/Environment/Meshes/SM_Castle_Window_A.uasset new file mode 100644 index 0000000..8ea36aa Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Castle_Window_A.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Clouds_A.uasset b/Content/Sample/Common/Environment/Meshes/SM_Clouds_A.uasset new file mode 100644 index 0000000..37e4035 Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Clouds_A.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Dock_Floor.uasset b/Content/Sample/Common/Environment/Meshes/SM_Dock_Floor.uasset new file mode 100644 index 0000000..923d92c Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Dock_Floor.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Dock_Pillar.uasset b/Content/Sample/Common/Environment/Meshes/SM_Dock_Pillar.uasset new file mode 100644 index 0000000..2f9fdd7 Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Dock_Pillar.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Flag.uasset b/Content/Sample/Common/Environment/Meshes/SM_Flag.uasset new file mode 100644 index 0000000..af85eda Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Flag.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Grass_02.uasset b/Content/Sample/Common/Environment/Meshes/SM_Grass_02.uasset new file mode 100644 index 0000000..dc7c4f2 Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Grass_02.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Grass_03.uasset b/Content/Sample/Common/Environment/Meshes/SM_Grass_03.uasset new file mode 100644 index 0000000..006b4c6 Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Grass_03.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Grass_03_B.uasset b/Content/Sample/Common/Environment/Meshes/SM_Grass_03_B.uasset new file mode 100644 index 0000000..eaee232 Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Grass_03_B.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Grass_04.uasset b/Content/Sample/Common/Environment/Meshes/SM_Grass_04.uasset new file mode 100644 index 0000000..0812103 Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Grass_04.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Grass_05.uasset b/Content/Sample/Common/Environment/Meshes/SM_Grass_05.uasset new file mode 100644 index 0000000..aa1cabd Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Grass_05.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Mushroom_A.uasset b/Content/Sample/Common/Environment/Meshes/SM_Mushroom_A.uasset new file mode 100644 index 0000000..6950dde Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Mushroom_A.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_SkyDome.uasset b/Content/Sample/Common/Environment/Meshes/SM_SkyDome.uasset new file mode 100644 index 0000000..7aad3c9 Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_SkyDome.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Stone_Path_B.uasset b/Content/Sample/Common/Environment/Meshes/SM_Stone_Path_B.uasset new file mode 100644 index 0000000..51c9d37 Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Stone_Path_B.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Tree_Cluster_A.uasset b/Content/Sample/Common/Environment/Meshes/SM_Tree_Cluster_A.uasset new file mode 100644 index 0000000..ec70fc1 Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Tree_Cluster_A.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Tree_Type_B.uasset b/Content/Sample/Common/Environment/Meshes/SM_Tree_Type_B.uasset new file mode 100644 index 0000000..ce4627b Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Tree_Type_B.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Tree_Type_B_Leaves.uasset b/Content/Sample/Common/Environment/Meshes/SM_Tree_Type_B_Leaves.uasset new file mode 100644 index 0000000..18e38d9 Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Tree_Type_B_Leaves.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Water_Waves.uasset b/Content/Sample/Common/Environment/Meshes/SM_Water_Waves.uasset new file mode 100644 index 0000000..c19ff8d Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Water_Waves.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Water_Waves_A.uasset b/Content/Sample/Common/Environment/Meshes/SM_Water_Waves_A.uasset new file mode 100644 index 0000000..29d550e Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Water_Waves_A.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Water_Waves_B.uasset b/Content/Sample/Common/Environment/Meshes/SM_Water_Waves_B.uasset new file mode 100644 index 0000000..0dd22fb Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Water_Waves_B.uasset differ diff --git a/Content/Sample/Common/Environment/Meshes/SM_Wood_Fence.uasset b/Content/Sample/Common/Environment/Meshes/SM_Wood_Fence.uasset new file mode 100644 index 0000000..96e0240 Binary files /dev/null and b/Content/Sample/Common/Environment/Meshes/SM_Wood_Fence.uasset differ diff --git a/Content/Sample/Common/Environment/Textures/T_BackgroundHouses_A.uasset b/Content/Sample/Common/Environment/Textures/T_BackgroundHouses_A.uasset new file mode 100644 index 0000000..b10efe1 Binary files /dev/null and b/Content/Sample/Common/Environment/Textures/T_BackgroundHouses_A.uasset differ diff --git a/Content/Sample/Common/Environment/Textures/T_BrushStrokes_A.uasset b/Content/Sample/Common/Environment/Textures/T_BrushStrokes_A.uasset new file mode 100644 index 0000000..f170d86 Binary files /dev/null and b/Content/Sample/Common/Environment/Textures/T_BrushStrokes_A.uasset differ diff --git a/Content/Sample/Common/Environment/Textures/T_BrushStrokes_B.uasset b/Content/Sample/Common/Environment/Textures/T_BrushStrokes_B.uasset new file mode 100644 index 0000000..11c86e0 Binary files /dev/null and b/Content/Sample/Common/Environment/Textures/T_BrushStrokes_B.uasset differ diff --git a/Content/Sample/Common/Environment/Textures/T_Bumpy_N.uasset b/Content/Sample/Common/Environment/Textures/T_Bumpy_N.uasset new file mode 100644 index 0000000..4ec3780 Binary files /dev/null and b/Content/Sample/Common/Environment/Textures/T_Bumpy_N.uasset differ diff --git a/Content/Sample/Common/Environment/Textures/T_Bush_Squiggly.uasset b/Content/Sample/Common/Environment/Textures/T_Bush_Squiggly.uasset new file mode 100644 index 0000000..b4c16db Binary files /dev/null and b/Content/Sample/Common/Environment/Textures/T_Bush_Squiggly.uasset differ diff --git a/Content/Sample/Common/Environment/Textures/T_Grass_A.uasset b/Content/Sample/Common/Environment/Textures/T_Grass_A.uasset new file mode 100644 index 0000000..a58aae4 Binary files /dev/null and b/Content/Sample/Common/Environment/Textures/T_Grass_A.uasset differ diff --git a/Content/Sample/Common/Environment/Textures/T_MaskedPaper_Screen.uasset b/Content/Sample/Common/Environment/Textures/T_MaskedPaper_Screen.uasset new file mode 100644 index 0000000..a07dcf0 Binary files /dev/null and b/Content/Sample/Common/Environment/Textures/T_MaskedPaper_Screen.uasset differ diff --git a/Content/Sample/Common/Environment/Textures/T_Mushroom_A.uasset b/Content/Sample/Common/Environment/Textures/T_Mushroom_A.uasset new file mode 100644 index 0000000..cb88692 Binary files /dev/null and b/Content/Sample/Common/Environment/Textures/T_Mushroom_A.uasset differ diff --git a/Content/Sample/Common/Environment/Textures/T_Rock_Cliff_D.uasset b/Content/Sample/Common/Environment/Textures/T_Rock_Cliff_D.uasset new file mode 100644 index 0000000..a741b4d Binary files /dev/null and b/Content/Sample/Common/Environment/Textures/T_Rock_Cliff_D.uasset differ diff --git a/Content/Sample/Common/Environment/Textures/T_ShapeNormal.uasset b/Content/Sample/Common/Environment/Textures/T_ShapeNormal.uasset new file mode 100644 index 0000000..a03fa63 Binary files /dev/null and b/Content/Sample/Common/Environment/Textures/T_ShapeNormal.uasset differ diff --git a/Content/Sample/Common/Environment/Textures/T_Splotches_A.uasset b/Content/Sample/Common/Environment/Textures/T_Splotches_A.uasset new file mode 100644 index 0000000..1258a0a Binary files /dev/null and b/Content/Sample/Common/Environment/Textures/T_Splotches_A.uasset differ diff --git a/Content/Sample/Common/Environment/Textures/T_StoneWall_A.uasset b/Content/Sample/Common/Environment/Textures/T_StoneWall_A.uasset new file mode 100644 index 0000000..cbef51c Binary files /dev/null and b/Content/Sample/Common/Environment/Textures/T_StoneWall_A.uasset differ diff --git a/Content/Sample/Common/Environment/Textures/T_Sun_Sheet.uasset b/Content/Sample/Common/Environment/Textures/T_Sun_Sheet.uasset new file mode 100644 index 0000000..030d0bc Binary files /dev/null and b/Content/Sample/Common/Environment/Textures/T_Sun_Sheet.uasset differ diff --git a/Content/Sample/Common/Environment/Textures/T_TexturedPaper.uasset b/Content/Sample/Common/Environment/Textures/T_TexturedPaper.uasset new file mode 100644 index 0000000..1066b90 Binary files /dev/null and b/Content/Sample/Common/Environment/Textures/T_TexturedPaper.uasset differ diff --git a/Content/Sample/Common/Environment/Textures/T_Water_Waves_M.uasset b/Content/Sample/Common/Environment/Textures/T_Water_Waves_M.uasset new file mode 100644 index 0000000..eecf4d1 Binary files /dev/null and b/Content/Sample/Common/Environment/Textures/T_Water_Waves_M.uasset differ diff --git a/Content/Sample/Common/Environment/Textures/T_Wood_Sheet.uasset b/Content/Sample/Common/Environment/Textures/T_Wood_Sheet.uasset new file mode 100644 index 0000000..3f64556 Binary files /dev/null and b/Content/Sample/Common/Environment/Textures/T_Wood_Sheet.uasset differ diff --git a/Content/Sample/Common/Physics/PA_Paper.uasset b/Content/Sample/Common/Physics/PA_Paper.uasset new file mode 100644 index 0000000..806b175 Binary files /dev/null and b/Content/Sample/Common/Physics/PA_Paper.uasset differ diff --git a/Content/Sample/Common/Physics/PM_Foliage.uasset b/Content/Sample/Common/Physics/PM_Foliage.uasset new file mode 100644 index 0000000..2f345e6 Binary files /dev/null and b/Content/Sample/Common/Physics/PM_Foliage.uasset differ diff --git a/Content/Sample/Common/Physics/PM_Glass.uasset b/Content/Sample/Common/Physics/PM_Glass.uasset new file mode 100644 index 0000000..95206ca Binary files /dev/null and b/Content/Sample/Common/Physics/PM_Glass.uasset differ diff --git a/Content/Sample/Common/Physics/PM_Grenade.uasset b/Content/Sample/Common/Physics/PM_Grenade.uasset new file mode 100644 index 0000000..69944d7 Binary files /dev/null and b/Content/Sample/Common/Physics/PM_Grenade.uasset differ diff --git a/Content/Sample/Common/Physics/PM_Stone.uasset b/Content/Sample/Common/Physics/PM_Stone.uasset new file mode 100644 index 0000000..03f6348 Binary files /dev/null and b/Content/Sample/Common/Physics/PM_Stone.uasset differ diff --git a/Content/Sample/Common/Physics/PM_Water.uasset b/Content/Sample/Common/Physics/PM_Water.uasset new file mode 100644 index 0000000..3cd927c Binary files /dev/null and b/Content/Sample/Common/Physics/PM_Water.uasset differ diff --git a/Content/Sample/Common/Physics/PM_Wood.uasset b/Content/Sample/Common/Physics/PM_Wood.uasset new file mode 100644 index 0000000..095da45 Binary files /dev/null and b/Content/Sample/Common/Physics/PM_Wood.uasset differ diff --git a/Content/Sample/Common/Visuals/1M_Cube.uasset b/Content/Sample/Common/Visuals/1M_Cube.uasset new file mode 100644 index 0000000..d43d0be Binary files /dev/null and b/Content/Sample/Common/Visuals/1M_Cube.uasset differ diff --git a/Content/Sample/Common/Visuals/1M_Cube_Chamfer.uasset b/Content/Sample/Common/Visuals/1M_Cube_Chamfer.uasset new file mode 100644 index 0000000..61766c7 Binary files /dev/null and b/Content/Sample/Common/Visuals/1M_Cube_Chamfer.uasset differ diff --git a/Content/Sample/Common/Visuals/Cartoonic_Massive_Regular.uasset b/Content/Sample/Common/Visuals/Cartoonic_Massive_Regular.uasset new file mode 100644 index 0000000..bbc309a Binary files /dev/null and b/Content/Sample/Common/Visuals/Cartoonic_Massive_Regular.uasset differ diff --git a/Content/Sample/Common/Visuals/Cartoonic_Massive_Regular_Font.uasset b/Content/Sample/Common/Visuals/Cartoonic_Massive_Regular_Font.uasset new file mode 100644 index 0000000..e6b7a13 Binary files /dev/null and b/Content/Sample/Common/Visuals/Cartoonic_Massive_Regular_Font.uasset differ diff --git a/Content/Sample/Common/Visuals/CubeMaterialOverride.uasset b/Content/Sample/Common/Visuals/CubeMaterialOverride.uasset new file mode 100644 index 0000000..a3cd9ff Binary files /dev/null and b/Content/Sample/Common/Visuals/CubeMaterialOverride.uasset differ diff --git a/Content/Sample/Common/Visuals/FirstPersonCrosshair.uasset b/Content/Sample/Common/Visuals/FirstPersonCrosshair.uasset new file mode 100644 index 0000000..7df52ab Binary files /dev/null and b/Content/Sample/Common/Visuals/FirstPersonCrosshair.uasset differ diff --git a/Content/Sample/Common/Visuals/FirstPersonProjectileMaterial.uasset b/Content/Sample/Common/Visuals/FirstPersonProjectileMaterial.uasset new file mode 100644 index 0000000..d6543fe Binary files /dev/null and b/Content/Sample/Common/Visuals/FirstPersonProjectileMaterial.uasset differ diff --git a/Content/Sample/Common/Visuals/FirstPersonProjectileMesh.uasset b/Content/Sample/Common/Visuals/FirstPersonProjectileMesh.uasset new file mode 100644 index 0000000..4c0efb6 Binary files /dev/null and b/Content/Sample/Common/Visuals/FirstPersonProjectileMesh.uasset differ diff --git a/Content/Sample/Common/Visuals/Health.uasset b/Content/Sample/Common/Visuals/Health.uasset new file mode 100644 index 0000000..c358509 Binary files /dev/null and b/Content/Sample/Common/Visuals/Health.uasset differ diff --git a/Content/Sample/Common/Visuals/MF_SampleVisibleCustomStencil.uasset b/Content/Sample/Common/Visuals/MF_SampleVisibleCustomStencil.uasset new file mode 100644 index 0000000..772b9f9 Binary files /dev/null and b/Content/Sample/Common/Visuals/MF_SampleVisibleCustomStencil.uasset differ diff --git a/Content/Sample/Common/Visuals/MI_GlassBottle.uasset b/Content/Sample/Common/Visuals/MI_GlassBottle.uasset new file mode 100644 index 0000000..8dace19 Binary files /dev/null and b/Content/Sample/Common/Visuals/MI_GlassBottle.uasset differ diff --git a/Content/Sample/Common/Visuals/ML_GlossyBlack_Latex_UE4.uasset b/Content/Sample/Common/Visuals/ML_GlossyBlack_Latex_UE4.uasset new file mode 100644 index 0000000..c927067 Binary files /dev/null and b/Content/Sample/Common/Visuals/ML_GlossyBlack_Latex_UE4.uasset differ diff --git a/Content/Sample/Common/Visuals/ML_Plastic_Shiny_Beige.uasset b/Content/Sample/Common/Visuals/ML_Plastic_Shiny_Beige.uasset new file mode 100644 index 0000000..052c4a6 Binary files /dev/null and b/Content/Sample/Common/Visuals/ML_Plastic_Shiny_Beige.uasset differ diff --git a/Content/Sample/Common/Visuals/ML_Plastic_Shiny_Beige_LOGO.uasset b/Content/Sample/Common/Visuals/ML_Plastic_Shiny_Beige_LOGO.uasset new file mode 100644 index 0000000..708aa18 Binary files /dev/null and b/Content/Sample/Common/Visuals/ML_Plastic_Shiny_Beige_LOGO.uasset differ diff --git a/Content/Sample/Common/Visuals/ML_Screen.uasset b/Content/Sample/Common/Visuals/ML_Screen.uasset new file mode 100644 index 0000000..ff226b2 Binary files /dev/null and b/Content/Sample/Common/Visuals/ML_Screen.uasset differ diff --git a/Content/Sample/Common/Visuals/ML_SoftMetal_UE4.uasset b/Content/Sample/Common/Visuals/ML_SoftMetal_UE4.uasset new file mode 100644 index 0000000..70ff029 Binary files /dev/null and b/Content/Sample/Common/Visuals/ML_SoftMetal_UE4.uasset differ diff --git a/Content/Sample/Common/Visuals/M_Arrow.uasset b/Content/Sample/Common/Visuals/M_Arrow.uasset new file mode 100644 index 0000000..4525a98 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_Arrow.uasset differ diff --git a/Content/Sample/Common/Visuals/M_Barrel.uasset b/Content/Sample/Common/Visuals/M_Barrel.uasset new file mode 100644 index 0000000..04808e9 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_Barrel.uasset differ diff --git a/Content/Sample/Common/Visuals/M_BaseMaterial.uasset b/Content/Sample/Common/Visuals/M_BaseMaterial.uasset new file mode 100644 index 0000000..accb8c8 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_BaseMaterial.uasset differ diff --git a/Content/Sample/Common/Visuals/M_Beam.uasset b/Content/Sample/Common/Visuals/M_Beam.uasset new file mode 100644 index 0000000..bcb9634 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_Beam.uasset differ diff --git a/Content/Sample/Common/Visuals/M_Bubbles.uasset b/Content/Sample/Common/Visuals/M_Bubbles.uasset new file mode 100644 index 0000000..5cf68a5 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_Bubbles.uasset differ diff --git a/Content/Sample/Common/Visuals/M_Button_ChangeColor.uasset b/Content/Sample/Common/Visuals/M_Button_ChangeColor.uasset new file mode 100644 index 0000000..4602eb0 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_Button_ChangeColor.uasset differ diff --git a/Content/Sample/Common/Visuals/M_Button_Off.uasset b/Content/Sample/Common/Visuals/M_Button_Off.uasset new file mode 100644 index 0000000..f9926e2 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_Button_Off.uasset differ diff --git a/Content/Sample/Common/Visuals/M_Button_On.uasset b/Content/Sample/Common/Visuals/M_Button_On.uasset new file mode 100644 index 0000000..66273a7 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_Button_On.uasset differ diff --git a/Content/Sample/Common/Visuals/M_Confetti.uasset b/Content/Sample/Common/Visuals/M_Confetti.uasset new file mode 100644 index 0000000..6db5098 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_Confetti.uasset differ diff --git a/Content/Sample/Common/Visuals/M_CubeMaterial.uasset b/Content/Sample/Common/Visuals/M_CubeMaterial.uasset new file mode 100644 index 0000000..d6b4a26 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_CubeMaterial.uasset differ diff --git a/Content/Sample/Common/Visuals/M_FPGun.uasset b/Content/Sample/Common/Visuals/M_FPGun.uasset new file mode 100644 index 0000000..b156a34 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_FPGun.uasset differ diff --git a/Content/Sample/Common/Visuals/M_GazeVizCone.uasset b/Content/Sample/Common/Visuals/M_GazeVizCone.uasset new file mode 100644 index 0000000..0b43f27 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_GazeVizCone.uasset differ diff --git a/Content/Sample/Common/Visuals/M_GazeVizDecal.uasset b/Content/Sample/Common/Visuals/M_GazeVizDecal.uasset new file mode 100644 index 0000000..9175022 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_GazeVizDecal.uasset differ diff --git a/Content/Sample/Common/Visuals/M_Glass.uasset b/Content/Sample/Common/Visuals/M_Glass.uasset new file mode 100644 index 0000000..c40edf6 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_Glass.uasset differ diff --git a/Content/Sample/Common/Visuals/M_GlassBottle.uasset b/Content/Sample/Common/Visuals/M_GlassBottle.uasset new file mode 100644 index 0000000..ce34eaf Binary files /dev/null and b/Content/Sample/Common/Visuals/M_GlassBottle.uasset differ diff --git a/Content/Sample/Common/Visuals/M_Green.uasset b/Content/Sample/Common/Visuals/M_Green.uasset new file mode 100644 index 0000000..f4dc15c Binary files /dev/null and b/Content/Sample/Common/Visuals/M_Green.uasset differ diff --git a/Content/Sample/Common/Visuals/M_GrenadeMaterial.uasset b/Content/Sample/Common/Visuals/M_GrenadeMaterial.uasset new file mode 100644 index 0000000..f1d43f4 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_GrenadeMaterial.uasset differ diff --git a/Content/Sample/Common/Visuals/M_InteractableItem.uasset b/Content/Sample/Common/Visuals/M_InteractableItem.uasset new file mode 100644 index 0000000..8282eac Binary files /dev/null and b/Content/Sample/Common/Visuals/M_InteractableItem.uasset differ diff --git a/Content/Sample/Common/Visuals/M_MK2.uasset b/Content/Sample/Common/Visuals/M_MK2.uasset new file mode 100644 index 0000000..29d0163 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_MK2.uasset differ diff --git a/Content/Sample/Common/Visuals/M_MK2MeTal.uasset b/Content/Sample/Common/Visuals/M_MK2MeTal.uasset new file mode 100644 index 0000000..298a8ce Binary files /dev/null and b/Content/Sample/Common/Visuals/M_MK2MeTal.uasset differ diff --git a/Content/Sample/Common/Visuals/M_MK2Y.uasset b/Content/Sample/Common/Visuals/M_MK2Y.uasset new file mode 100644 index 0000000..5d68c67 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_MK2Y.uasset differ diff --git a/Content/Sample/Common/Visuals/M_Magenta.uasset b/Content/Sample/Common/Visuals/M_Magenta.uasset new file mode 100644 index 0000000..1f60ac3 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_Magenta.uasset differ diff --git a/Content/Sample/Common/Visuals/M_MagicRock.uasset b/Content/Sample/Common/Visuals/M_MagicRock.uasset new file mode 100644 index 0000000..8f2948b Binary files /dev/null and b/Content/Sample/Common/Visuals/M_MagicRock.uasset differ diff --git a/Content/Sample/Common/Visuals/M_MagicRockMotes.uasset b/Content/Sample/Common/Visuals/M_MagicRockMotes.uasset new file mode 100644 index 0000000..829db8f Binary files /dev/null and b/Content/Sample/Common/Visuals/M_MagicRockMotes.uasset differ diff --git a/Content/Sample/Common/Visuals/M_MagicThrowingRockSimple.uasset b/Content/Sample/Common/Visuals/M_MagicThrowingRockSimple.uasset new file mode 100644 index 0000000..be08c87 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_MagicThrowingRockSimple.uasset differ diff --git a/Content/Sample/Common/Visuals/M_MirrorRobot.uasset b/Content/Sample/Common/Visuals/M_MirrorRobot.uasset new file mode 100644 index 0000000..30728cf Binary files /dev/null and b/Content/Sample/Common/Visuals/M_MirrorRobot.uasset differ diff --git a/Content/Sample/Common/Visuals/M_MirrorRobot_Black.uasset b/Content/Sample/Common/Visuals/M_MirrorRobot_Black.uasset new file mode 100644 index 0000000..271da32 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_MirrorRobot_Black.uasset differ diff --git a/Content/Sample/Common/Visuals/M_MirrorRobot_Blue.uasset b/Content/Sample/Common/Visuals/M_MirrorRobot_Blue.uasset new file mode 100644 index 0000000..0508e0d Binary files /dev/null and b/Content/Sample/Common/Visuals/M_MirrorRobot_Blue.uasset differ diff --git a/Content/Sample/Common/Visuals/M_MirrorRobot_White.uasset b/Content/Sample/Common/Visuals/M_MirrorRobot_White.uasset new file mode 100644 index 0000000..9cb61c8 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_MirrorRobot_White.uasset differ diff --git a/Content/Sample/Common/Visuals/M_Paper.uasset b/Content/Sample/Common/Visuals/M_Paper.uasset new file mode 100644 index 0000000..495221a Binary files /dev/null and b/Content/Sample/Common/Visuals/M_Paper.uasset differ diff --git a/Content/Sample/Common/Visuals/M_Potion.uasset b/Content/Sample/Common/Visuals/M_Potion.uasset new file mode 100644 index 0000000..033d980 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_Potion.uasset differ diff --git a/Content/Sample/Common/Visuals/M_RoughMetal.uasset b/Content/Sample/Common/Visuals/M_RoughMetal.uasset new file mode 100644 index 0000000..9c5105a Binary files /dev/null and b/Content/Sample/Common/Visuals/M_RoughMetal.uasset differ diff --git a/Content/Sample/Common/Visuals/M_ThrowingRock.uasset b/Content/Sample/Common/Visuals/M_ThrowingRock.uasset new file mode 100644 index 0000000..e193555 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_ThrowingRock.uasset differ diff --git a/Content/Sample/Common/Visuals/M_TobiiController.uasset b/Content/Sample/Common/Visuals/M_TobiiController.uasset new file mode 100644 index 0000000..df25f63 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_TobiiController.uasset differ diff --git a/Content/Sample/Common/Visuals/M_TobiiControllerButtons.uasset b/Content/Sample/Common/Visuals/M_TobiiControllerButtons.uasset new file mode 100644 index 0000000..2ed40a2 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_TobiiControllerButtons.uasset differ diff --git a/Content/Sample/Common/Visuals/M_TobiiEye.uasset b/Content/Sample/Common/Visuals/M_TobiiEye.uasset new file mode 100644 index 0000000..a3a6d76 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_TobiiEye.uasset differ diff --git a/Content/Sample/Common/Visuals/M_TobiiOutline.uasset b/Content/Sample/Common/Visuals/M_TobiiOutline.uasset new file mode 100644 index 0000000..2e09610 Binary files /dev/null and b/Content/Sample/Common/Visuals/M_TobiiOutline.uasset differ diff --git a/Content/Sample/Common/Visuals/M_ToonRocket.uasset b/Content/Sample/Common/Visuals/M_ToonRocket.uasset new file mode 100644 index 0000000..7268a4a Binary files /dev/null and b/Content/Sample/Common/Visuals/M_ToonRocket.uasset differ diff --git a/Content/Sample/Common/Visuals/M_UE4Man_Body.uasset b/Content/Sample/Common/Visuals/M_UE4Man_Body.uasset new file mode 100644 index 0000000..d8a900c Binary files /dev/null and b/Content/Sample/Common/Visuals/M_UE4Man_Body.uasset differ diff --git a/Content/Sample/Common/Visuals/M_YouCalibratedRight.uasset b/Content/Sample/Common/Visuals/M_YouCalibratedRight.uasset new file mode 100644 index 0000000..892dc8e Binary files /dev/null and b/Content/Sample/Common/Visuals/M_YouCalibratedRight.uasset differ diff --git a/Content/Sample/Common/Visuals/PS_Confetti.uasset b/Content/Sample/Common/Visuals/PS_Confetti.uasset new file mode 100644 index 0000000..cc42c85 Binary files /dev/null and b/Content/Sample/Common/Visuals/PS_Confetti.uasset differ diff --git a/Content/Sample/Common/Visuals/PS_MagicRock.uasset b/Content/Sample/Common/Visuals/PS_MagicRock.uasset new file mode 100644 index 0000000..1d4bdc4 Binary files /dev/null and b/Content/Sample/Common/Visuals/PS_MagicRock.uasset differ diff --git a/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Cube.uasset b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Cube.uasset new file mode 100644 index 0000000..be65ebe Binary files /dev/null and b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Cube.uasset differ diff --git a/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Cube__1_.uasset b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Cube__1_.uasset new file mode 100644 index 0000000..1e9184d Binary files /dev/null and b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Cube__1_.uasset differ diff --git a/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Cube__3_.uasset b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Cube__3_.uasset new file mode 100644 index 0000000..6017078 Binary files /dev/null and b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Cube__3_.uasset differ diff --git a/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Cube__4_.uasset b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Cube__4_.uasset new file mode 100644 index 0000000..498f086 Binary files /dev/null and b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Cube__4_.uasset differ diff --git a/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Cube__5_.uasset b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Cube__5_.uasset new file mode 100644 index 0000000..71a6a05 Binary files /dev/null and b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Cube__5_.uasset differ diff --git a/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Cylinder.uasset b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Cylinder.uasset new file mode 100644 index 0000000..e1d71ad Binary files /dev/null and b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Cylinder.uasset differ diff --git a/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Jacket.uasset b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Jacket.uasset new file mode 100644 index 0000000..fb736b2 Binary files /dev/null and b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Jacket.uasset differ diff --git a/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_LeftHand.uasset b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_LeftHand.uasset new file mode 100644 index 0000000..88615e4 Binary files /dev/null and b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_LeftHand.uasset differ diff --git a/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_RightHand.uasset b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_RightHand.uasset new file mode 100644 index 0000000..3830009 Binary files /dev/null and b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_RightHand.uasset differ diff --git a/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Shirt.uasset b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Shirt.uasset new file mode 100644 index 0000000..27888c7 Binary files /dev/null and b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Shirt.uasset differ diff --git a/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Tie.uasset b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Tie.uasset new file mode 100644 index 0000000..5305f45 Binary files /dev/null and b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_Tie.uasset differ diff --git a/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_eye_left.uasset b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_eye_left.uasset new file mode 100644 index 0000000..710b8f5 Binary files /dev/null and b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_eye_left.uasset differ diff --git a/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_eye_left_1.uasset b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_eye_left_1.uasset new file mode 100644 index 0000000..2e4807a Binary files /dev/null and b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_eye_left_1.uasset differ diff --git a/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_eye_right.uasset b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_eye_right.uasset new file mode 100644 index 0000000..96d8553 Binary files /dev/null and b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_eye_right.uasset differ diff --git a/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_eye_right_1.uasset b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_eye_right_1.uasset new file mode 100644 index 0000000..48d0060 Binary files /dev/null and b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_eye_right_1.uasset differ diff --git a/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_pupil.uasset b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_pupil.uasset new file mode 100644 index 0000000..d51cccd Binary files /dev/null and b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_pupil.uasset differ diff --git a/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_pupil_1.uasset b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_pupil_1.uasset new file mode 100644 index 0000000..7f964a6 Binary files /dev/null and b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_pupil_1.uasset differ diff --git a/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_pupil_left.uasset b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_pupil_left.uasset new file mode 100644 index 0000000..176ffb8 Binary files /dev/null and b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_pupil_left.uasset differ diff --git a/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_pupil_right.uasset b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_pupil_right.uasset new file mode 100644 index 0000000..b3e33ee Binary files /dev/null and b/Content/Sample/Common/Visuals/RobotMeshes/Robot_player_pupil_right.uasset differ diff --git a/Content/Sample/Common/Visuals/SM_ArrowMesh.uasset b/Content/Sample/Common/Visuals/SM_ArrowMesh.uasset new file mode 100644 index 0000000..9ef328b Binary files /dev/null and b/Content/Sample/Common/Visuals/SM_ArrowMesh.uasset differ diff --git a/Content/Sample/Common/Visuals/SM_Birdie.uasset b/Content/Sample/Common/Visuals/SM_Birdie.uasset new file mode 100644 index 0000000..75a5130 Binary files /dev/null and b/Content/Sample/Common/Visuals/SM_Birdie.uasset differ diff --git a/Content/Sample/Common/Visuals/SM_Confetti.uasset b/Content/Sample/Common/Visuals/SM_Confetti.uasset new file mode 100644 index 0000000..1315e49 Binary files /dev/null and b/Content/Sample/Common/Visuals/SM_Confetti.uasset differ diff --git a/Content/Sample/Common/Visuals/SM_HandGrenade.uasset b/Content/Sample/Common/Visuals/SM_HandGrenade.uasset new file mode 100644 index 0000000..1e7f8e5 Binary files /dev/null and b/Content/Sample/Common/Visuals/SM_HandGrenade.uasset differ diff --git a/Content/Sample/Common/Visuals/SM_MagicRock.uasset b/Content/Sample/Common/Visuals/SM_MagicRock.uasset new file mode 100644 index 0000000..ad49b83 Binary files /dev/null and b/Content/Sample/Common/Visuals/SM_MagicRock.uasset differ diff --git a/Content/Sample/Common/Visuals/SM_Paper.uasset b/Content/Sample/Common/Visuals/SM_Paper.uasset new file mode 100644 index 0000000..4e9d5c2 Binary files /dev/null and b/Content/Sample/Common/Visuals/SM_Paper.uasset differ diff --git a/Content/Sample/Common/Visuals/SM_Paper_PhysicsAsset.uasset b/Content/Sample/Common/Visuals/SM_Paper_PhysicsAsset.uasset new file mode 100644 index 0000000..a5a3c8b Binary files /dev/null and b/Content/Sample/Common/Visuals/SM_Paper_PhysicsAsset.uasset differ diff --git a/Content/Sample/Common/Visuals/SM_Paper_Skeleton.uasset b/Content/Sample/Common/Visuals/SM_Paper_Skeleton.uasset new file mode 100644 index 0000000..9692994 Binary files /dev/null and b/Content/Sample/Common/Visuals/SM_Paper_Skeleton.uasset differ diff --git a/Content/Sample/Common/Visuals/SM_TobiiBarrel.uasset b/Content/Sample/Common/Visuals/SM_TobiiBarrel.uasset new file mode 100644 index 0000000..192f278 Binary files /dev/null and b/Content/Sample/Common/Visuals/SM_TobiiBarrel.uasset differ diff --git a/Content/Sample/Common/Visuals/SM_TobiiBottle.uasset b/Content/Sample/Common/Visuals/SM_TobiiBottle.uasset new file mode 100644 index 0000000..cfec1ee Binary files /dev/null and b/Content/Sample/Common/Visuals/SM_TobiiBottle.uasset differ diff --git a/Content/Sample/Common/Visuals/SM_TobiiController.uasset b/Content/Sample/Common/Visuals/SM_TobiiController.uasset new file mode 100644 index 0000000..2c391c9 Binary files /dev/null and b/Content/Sample/Common/Visuals/SM_TobiiController.uasset differ diff --git a/Content/Sample/Common/Visuals/SM_TobiiRock.uasset b/Content/Sample/Common/Visuals/SM_TobiiRock.uasset new file mode 100644 index 0000000..af3761e Binary files /dev/null and b/Content/Sample/Common/Visuals/SM_TobiiRock.uasset differ diff --git a/Content/Sample/Common/Visuals/SM_TobiiSceneSelectPillar_Board.uasset b/Content/Sample/Common/Visuals/SM_TobiiSceneSelectPillar_Board.uasset new file mode 100644 index 0000000..17714be Binary files /dev/null and b/Content/Sample/Common/Visuals/SM_TobiiSceneSelectPillar_Board.uasset differ diff --git a/Content/Sample/Common/Visuals/SM_TobiiSceneSelectPillar_Box.uasset b/Content/Sample/Common/Visuals/SM_TobiiSceneSelectPillar_Box.uasset new file mode 100644 index 0000000..81efa71 Binary files /dev/null and b/Content/Sample/Common/Visuals/SM_TobiiSceneSelectPillar_Box.uasset differ diff --git a/Content/Sample/Common/Visuals/SM_TobiiToonBottle_Cork.uasset b/Content/Sample/Common/Visuals/SM_TobiiToonBottle_Cork.uasset new file mode 100644 index 0000000..1d5f7b2 Binary files /dev/null and b/Content/Sample/Common/Visuals/SM_TobiiToonBottle_Cork.uasset differ diff --git a/Content/Sample/Common/Visuals/SM_ToonRocket.uasset b/Content/Sample/Common/Visuals/SM_ToonRocket.uasset new file mode 100644 index 0000000..28c7afb Binary files /dev/null and b/Content/Sample/Common/Visuals/SM_ToonRocket.uasset differ diff --git a/Content/Sample/Common/Visuals/Shiny.uasset b/Content/Sample/Common/Visuals/Shiny.uasset new file mode 100644 index 0000000..d79b301 Binary files /dev/null and b/Content/Sample/Common/Visuals/Shiny.uasset differ diff --git a/Content/Sample/Common/Visuals/SliderThumbFocus.uasset b/Content/Sample/Common/Visuals/SliderThumbFocus.uasset new file mode 100644 index 0000000..bb3270b Binary files /dev/null and b/Content/Sample/Common/Visuals/SliderThumbFocus.uasset differ diff --git a/Content/Sample/Common/Visuals/SliderThumbNormal.uasset b/Content/Sample/Common/Visuals/SliderThumbNormal.uasset new file mode 100644 index 0000000..662c4db Binary files /dev/null and b/Content/Sample/Common/Visuals/SliderThumbNormal.uasset differ diff --git a/Content/Sample/Common/Visuals/SteamVR_HUD_Transparent.uasset b/Content/Sample/Common/Visuals/SteamVR_HUD_Transparent.uasset new file mode 100644 index 0000000..287d859 Binary files /dev/null and b/Content/Sample/Common/Visuals/SteamVR_HUD_Transparent.uasset differ diff --git a/Content/Sample/Common/Visuals/T_Arrow.uasset b/Content/Sample/Common/Visuals/T_Arrow.uasset new file mode 100644 index 0000000..40e16c8 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_Arrow.uasset differ diff --git a/Content/Sample/Common/Visuals/T_Arrows.uasset b/Content/Sample/Common/Visuals/T_Arrows.uasset new file mode 100644 index 0000000..be74da3 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_Arrows.uasset differ diff --git a/Content/Sample/Common/Visuals/T_BirdGrenadeLove.uasset b/Content/Sample/Common/Visuals/T_BirdGrenadeLove.uasset new file mode 100644 index 0000000..e3e7726 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_BirdGrenadeLove.uasset differ diff --git a/Content/Sample/Common/Visuals/T_Bubbles.uasset b/Content/Sample/Common/Visuals/T_Bubbles.uasset new file mode 100644 index 0000000..4a389fa Binary files /dev/null and b/Content/Sample/Common/Visuals/T_Bubbles.uasset differ diff --git a/Content/Sample/Common/Visuals/T_Button.uasset b/Content/Sample/Common/Visuals/T_Button.uasset new file mode 100644 index 0000000..267c766 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_Button.uasset differ diff --git a/Content/Sample/Common/Visuals/T_ButtonHovered.uasset b/Content/Sample/Common/Visuals/T_ButtonHovered.uasset new file mode 100644 index 0000000..7919f4a Binary files /dev/null and b/Content/Sample/Common/Visuals/T_ButtonHovered.uasset differ diff --git a/Content/Sample/Common/Visuals/T_CalibrationButton.uasset b/Content/Sample/Common/Visuals/T_CalibrationButton.uasset new file mode 100644 index 0000000..dc2cc13 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_CalibrationButton.uasset differ diff --git a/Content/Sample/Common/Visuals/T_DebugTabletItem.uasset b/Content/Sample/Common/Visuals/T_DebugTabletItem.uasset new file mode 100644 index 0000000..709f21b Binary files /dev/null and b/Content/Sample/Common/Visuals/T_DebugTabletItem.uasset differ diff --git a/Content/Sample/Common/Visuals/T_DesktopCalibration2.uasset b/Content/Sample/Common/Visuals/T_DesktopCalibration2.uasset new file mode 100644 index 0000000..0afe651 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_DesktopCalibration2.uasset differ diff --git a/Content/Sample/Common/Visuals/T_DesktopCalibration3.uasset b/Content/Sample/Common/Visuals/T_DesktopCalibration3.uasset new file mode 100644 index 0000000..ae3d461 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_DesktopCalibration3.uasset differ diff --git a/Content/Sample/Common/Visuals/T_DesktopCalibrationScene.uasset b/Content/Sample/Common/Visuals/T_DesktopCalibrationScene.uasset new file mode 100644 index 0000000..919e126 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_DesktopCalibrationScene.uasset differ diff --git a/Content/Sample/Common/Visuals/T_DesktopShooterScene.uasset b/Content/Sample/Common/Visuals/T_DesktopShooterScene.uasset new file mode 100644 index 0000000..296562e Binary files /dev/null and b/Content/Sample/Common/Visuals/T_DesktopShooterScene.uasset differ diff --git a/Content/Sample/Common/Visuals/T_DesktopSimulationScene.uasset b/Content/Sample/Common/Visuals/T_DesktopSimulationScene.uasset new file mode 100644 index 0000000..18889c9 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_DesktopSimulationScene.uasset differ diff --git a/Content/Sample/Common/Visuals/T_DesktopSocialScene.uasset b/Content/Sample/Common/Visuals/T_DesktopSocialScene.uasset new file mode 100644 index 0000000..bb5dee0 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_DesktopSocialScene.uasset differ diff --git a/Content/Sample/Common/Visuals/T_Desktopcalibration1.uasset b/Content/Sample/Common/Visuals/T_Desktopcalibration1.uasset new file mode 100644 index 0000000..72c7450 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_Desktopcalibration1.uasset differ diff --git a/Content/Sample/Common/Visuals/T_EmptyCheckbox.uasset b/Content/Sample/Common/Visuals/T_EmptyCheckbox.uasset new file mode 100644 index 0000000..fb9f158 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_EmptyCheckbox.uasset differ diff --git a/Content/Sample/Common/Visuals/T_FPGun_M.uasset b/Content/Sample/Common/Visuals/T_FPGun_M.uasset new file mode 100644 index 0000000..c159290 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_FPGun_M.uasset differ diff --git a/Content/Sample/Common/Visuals/T_FPGun_N.uasset b/Content/Sample/Common/Visuals/T_FPGun_N.uasset new file mode 100644 index 0000000..c61c36a Binary files /dev/null and b/Content/Sample/Common/Visuals/T_FPGun_N.uasset differ diff --git a/Content/Sample/Common/Visuals/T_FilledCheckbox.uasset b/Content/Sample/Common/Visuals/T_FilledCheckbox.uasset new file mode 100644 index 0000000..eb6cdb4 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_FilledCheckbox.uasset differ diff --git a/Content/Sample/Common/Visuals/T_GatlingButton.uasset b/Content/Sample/Common/Visuals/T_GatlingButton.uasset new file mode 100644 index 0000000..53d2d17 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_GatlingButton.uasset differ diff --git a/Content/Sample/Common/Visuals/T_GatlingButtonPressed.uasset b/Content/Sample/Common/Visuals/T_GatlingButtonPressed.uasset new file mode 100644 index 0000000..8849d71 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_GatlingButtonPressed.uasset differ diff --git a/Content/Sample/Common/Visuals/T_HandGrenadeMK2Green.uasset b/Content/Sample/Common/Visuals/T_HandGrenadeMK2Green.uasset new file mode 100644 index 0000000..094c36c Binary files /dev/null and b/Content/Sample/Common/Visuals/T_HandGrenadeMK2Green.uasset differ diff --git a/Content/Sample/Common/Visuals/T_HandGrenadeMetal.uasset b/Content/Sample/Common/Visuals/T_HandGrenadeMetal.uasset new file mode 100644 index 0000000..ae83b8d Binary files /dev/null and b/Content/Sample/Common/Visuals/T_HandGrenadeMetal.uasset differ diff --git a/Content/Sample/Common/Visuals/T_HandGrenademk2Yellow.uasset b/Content/Sample/Common/Visuals/T_HandGrenademk2Yellow.uasset new file mode 100644 index 0000000..339e603 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_HandGrenademk2Yellow.uasset differ diff --git a/Content/Sample/Common/Visuals/T_Heart.uasset b/Content/Sample/Common/Visuals/T_Heart.uasset new file mode 100644 index 0000000..2933134 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_Heart.uasset differ diff --git a/Content/Sample/Common/Visuals/T_ML_Aluminum01.uasset b/Content/Sample/Common/Visuals/T_ML_Aluminum01.uasset new file mode 100644 index 0000000..584f01e Binary files /dev/null and b/Content/Sample/Common/Visuals/T_ML_Aluminum01.uasset differ diff --git a/Content/Sample/Common/Visuals/T_ML_Aluminum01_N.uasset b/Content/Sample/Common/Visuals/T_ML_Aluminum01_N.uasset new file mode 100644 index 0000000..a547049 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_ML_Aluminum01_N.uasset differ diff --git a/Content/Sample/Common/Visuals/T_ML_FineRubber.uasset b/Content/Sample/Common/Visuals/T_ML_FineRubber.uasset new file mode 100644 index 0000000..ee628d4 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_ML_FineRubber.uasset differ diff --git a/Content/Sample/Common/Visuals/T_ML_Rubber_Blue_01_D.uasset b/Content/Sample/Common/Visuals/T_ML_Rubber_Blue_01_D.uasset new file mode 100644 index 0000000..5dabcf0 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_ML_Rubber_Blue_01_D.uasset differ diff --git a/Content/Sample/Common/Visuals/T_ML_Rubber_Blue_01_N.uasset b/Content/Sample/Common/Visuals/T_ML_Rubber_Blue_01_N.uasset new file mode 100644 index 0000000..5254a22 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_ML_Rubber_Blue_01_N.uasset differ diff --git a/Content/Sample/Common/Visuals/T_Paper.uasset b/Content/Sample/Common/Visuals/T_Paper.uasset new file mode 100644 index 0000000..72c695e Binary files /dev/null and b/Content/Sample/Common/Visuals/T_Paper.uasset differ diff --git a/Content/Sample/Common/Visuals/T_RocketButton.uasset b/Content/Sample/Common/Visuals/T_RocketButton.uasset new file mode 100644 index 0000000..879c312 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_RocketButton.uasset differ diff --git a/Content/Sample/Common/Visuals/T_RocketButtonPressed.uasset b/Content/Sample/Common/Visuals/T_RocketButtonPressed.uasset new file mode 100644 index 0000000..21d3733 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_RocketButtonPressed.uasset differ diff --git a/Content/Sample/Common/Visuals/T_SelectButton.uasset b/Content/Sample/Common/Visuals/T_SelectButton.uasset new file mode 100644 index 0000000..13db103 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_SelectButton.uasset differ diff --git a/Content/Sample/Common/Visuals/T_SelectionBorder.uasset b/Content/Sample/Common/Visuals/T_SelectionBorder.uasset new file mode 100644 index 0000000..0820fad Binary files /dev/null and b/Content/Sample/Common/Visuals/T_SelectionBorder.uasset differ diff --git a/Content/Sample/Common/Visuals/T_SideHUD.uasset b/Content/Sample/Common/Visuals/T_SideHUD.uasset new file mode 100644 index 0000000..03c9c0c Binary files /dev/null and b/Content/Sample/Common/Visuals/T_SideHUD.uasset differ diff --git a/Content/Sample/Common/Visuals/T_SocialScene.uasset b/Content/Sample/Common/Visuals/T_SocialScene.uasset new file mode 100644 index 0000000..c6201c4 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_SocialScene.uasset differ diff --git a/Content/Sample/Common/Visuals/T_SteamVR_HUD_Position_Guide_Transparent.uasset b/Content/Sample/Common/Visuals/T_SteamVR_HUD_Position_Guide_Transparent.uasset new file mode 100644 index 0000000..75ebda4 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_SteamVR_HUD_Position_Guide_Transparent.uasset differ diff --git a/Content/Sample/Common/Visuals/T_ThrowingScene.uasset b/Content/Sample/Common/Visuals/T_ThrowingScene.uasset new file mode 100644 index 0000000..10c7ffe Binary files /dev/null and b/Content/Sample/Common/Visuals/T_ThrowingScene.uasset differ diff --git a/Content/Sample/Common/Visuals/T_TobiiControllerDiff.uasset b/Content/Sample/Common/Visuals/T_TobiiControllerDiff.uasset new file mode 100644 index 0000000..0858c19 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_TobiiControllerDiff.uasset differ diff --git a/Content/Sample/Common/Visuals/T_TobiiEye.uasset b/Content/Sample/Common/Visuals/T_TobiiEye.uasset new file mode 100644 index 0000000..a6a723b Binary files /dev/null and b/Content/Sample/Common/Visuals/T_TobiiEye.uasset differ diff --git a/Content/Sample/Common/Visuals/T_TobiiTutorial.uasset b/Content/Sample/Common/Visuals/T_TobiiTutorial.uasset new file mode 100644 index 0000000..c4f5eb0 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_TobiiTutorial.uasset differ diff --git a/Content/Sample/Common/Visuals/T_ToolButton.uasset b/Content/Sample/Common/Visuals/T_ToolButton.uasset new file mode 100644 index 0000000..fb2c046 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_ToolButton.uasset differ diff --git a/Content/Sample/Common/Visuals/T_ToonRocket_Diffuse.uasset b/Content/Sample/Common/Visuals/T_ToonRocket_Diffuse.uasset new file mode 100644 index 0000000..377a3b5 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_ToonRocket_Diffuse.uasset differ diff --git a/Content/Sample/Common/Visuals/T_ToonRocket_Metallic.uasset b/Content/Sample/Common/Visuals/T_ToonRocket_Metallic.uasset new file mode 100644 index 0000000..3f1554c Binary files /dev/null and b/Content/Sample/Common/Visuals/T_ToonRocket_Metallic.uasset differ diff --git a/Content/Sample/Common/Visuals/T_ToonRocket_Normal.uasset b/Content/Sample/Common/Visuals/T_ToonRocket_Normal.uasset new file mode 100644 index 0000000..f7ce0b6 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_ToonRocket_Normal.uasset differ diff --git a/Content/Sample/Common/Visuals/T_TutorialTabletItem.uasset b/Content/Sample/Common/Visuals/T_TutorialTabletItem.uasset new file mode 100644 index 0000000..3952a23 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_TutorialTabletItem.uasset differ diff --git a/Content/Sample/Common/Visuals/T_UIScene.uasset b/Content/Sample/Common/Visuals/T_UIScene.uasset new file mode 100644 index 0000000..7c3949d Binary files /dev/null and b/Content/Sample/Common/Visuals/T_UIScene.uasset differ diff --git a/Content/Sample/Common/Visuals/T_XRCalibrationScene.uasset b/Content/Sample/Common/Visuals/T_XRCalibrationScene.uasset new file mode 100644 index 0000000..e0f9631 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_XRCalibrationScene.uasset differ diff --git a/Content/Sample/Common/Visuals/T_YouCalibratedRight.uasset b/Content/Sample/Common/Visuals/T_YouCalibratedRight.uasset new file mode 100644 index 0000000..8165e86 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_YouCalibratedRight.uasset differ diff --git a/Content/Sample/Common/Visuals/T_heli_gamepad_legend.uasset b/Content/Sample/Common/Visuals/T_heli_gamepad_legend.uasset new file mode 100644 index 0000000..061f4a9 Binary files /dev/null and b/Content/Sample/Common/Visuals/T_heli_gamepad_legend.uasset differ diff --git a/Content/Sample/Common/Visuals/UE4_LOGO_CARD.uasset b/Content/Sample/Common/Visuals/UE4_LOGO_CARD.uasset new file mode 100644 index 0000000..0b6422f Binary files /dev/null and b/Content/Sample/Common/Visuals/UE4_LOGO_CARD.uasset differ diff --git a/Content/Sample/Common/Visuals/UE4_Mannequin_MAT_MASKA.uasset b/Content/Sample/Common/Visuals/UE4_Mannequin_MAT_MASKA.uasset new file mode 100644 index 0000000..0e351e9 Binary files /dev/null and b/Content/Sample/Common/Visuals/UE4_Mannequin_MAT_MASKA.uasset differ diff --git a/Content/Sample/Common/Visuals/UE4_Mannequin__normals.uasset b/Content/Sample/Common/Visuals/UE4_Mannequin__normals.uasset new file mode 100644 index 0000000..7dac838 Binary files /dev/null and b/Content/Sample/Common/Visuals/UE4_Mannequin__normals.uasset differ diff --git a/Content/Sample/Common/Visuals/Widget3DPassThrough_Translucent_NoDepth.uasset b/Content/Sample/Common/Visuals/Widget3DPassThrough_Translucent_NoDepth.uasset new file mode 100644 index 0000000..86657a7 Binary files /dev/null and b/Content/Sample/Common/Visuals/Widget3DPassThrough_Translucent_NoDepth.uasset differ diff --git a/Content/Sample/Common/Visuals/WorldGridMaterial.uasset b/Content/Sample/Common/Visuals/WorldGridMaterial.uasset new file mode 100644 index 0000000..daf5deb Binary files /dev/null and b/Content/Sample/Common/Visuals/WorldGridMaterial.uasset differ diff --git a/Content/Sample/Common/Visuals/gamepad_legend.uasset b/Content/Sample/Common/Visuals/gamepad_legend.uasset new file mode 100644 index 0000000..9b156cf Binary files /dev/null and b/Content/Sample/Common/Visuals/gamepad_legend.uasset differ diff --git a/Content/Sample/Desktop/BP_DesktopExampleCharacter.uasset b/Content/Sample/Desktop/BP_DesktopExampleCharacter.uasset new file mode 100644 index 0000000..af00cf0 Binary files /dev/null and b/Content/Sample/Desktop/BP_DesktopExampleCharacter.uasset differ diff --git a/Content/Sample/Desktop/BP_DesktopExampleGameMode.uasset b/Content/Sample/Desktop/BP_DesktopExampleGameMode.uasset new file mode 100644 index 0000000..c9937ef Binary files /dev/null and b/Content/Sample/Desktop/BP_DesktopExampleGameMode.uasset differ diff --git a/Content/Sample/Desktop/BP_DesktopExamplePlayerController.uasset b/Content/Sample/Desktop/BP_DesktopExamplePlayerController.uasset new file mode 100644 index 0000000..0e862a5 Binary files /dev/null and b/Content/Sample/Desktop/BP_DesktopExamplePlayerController.uasset differ diff --git a/Content/Sample/Desktop/BP_InfiniteScreenCameraModifier.uasset b/Content/Sample/Desktop/BP_InfiniteScreenCameraModifier.uasset new file mode 100644 index 0000000..185565f Binary files /dev/null and b/Content/Sample/Desktop/BP_InfiniteScreenCameraModifier.uasset differ diff --git a/Content/Sample/Desktop/CalibrationScene/BP_InfoSignPage_DesktopCalibrationInfo.uasset b/Content/Sample/Desktop/CalibrationScene/BP_InfoSignPage_DesktopCalibrationInfo.uasset new file mode 100644 index 0000000..5caab92 Binary files /dev/null and b/Content/Sample/Desktop/CalibrationScene/BP_InfoSignPage_DesktopCalibrationInfo.uasset differ diff --git a/Content/Sample/Desktop/CalibrationScene/DesktopCalibrationLevel.umap b/Content/Sample/Desktop/CalibrationScene/DesktopCalibrationLevel.umap new file mode 100644 index 0000000..b2e70c5 Binary files /dev/null and b/Content/Sample/Desktop/CalibrationScene/DesktopCalibrationLevel.umap differ diff --git a/Content/Sample/Desktop/CalibrationScene/W_DesktopCalibrationInfo.uasset b/Content/Sample/Desktop/CalibrationScene/W_DesktopCalibrationInfo.uasset new file mode 100644 index 0000000..ed8761f Binary files /dev/null and b/Content/Sample/Desktop/CalibrationScene/W_DesktopCalibrationInfo.uasset differ diff --git a/Content/Sample/Desktop/DesktopExampleMap.umap b/Content/Sample/Desktop/DesktopExampleMap.umap new file mode 100644 index 0000000..aaa2e3a Binary files /dev/null and b/Content/Sample/Desktop/DesktopExampleMap.umap differ diff --git a/Content/Sample/Desktop/E_CharacterMode.uasset b/Content/Sample/Desktop/E_CharacterMode.uasset new file mode 100644 index 0000000..3d04de0 Binary files /dev/null and b/Content/Sample/Desktop/E_CharacterMode.uasset differ diff --git a/Content/Sample/Desktop/M_Wireframe.uasset b/Content/Sample/Desktop/M_Wireframe.uasset new file mode 100644 index 0000000..ea2336d Binary files /dev/null and b/Content/Sample/Desktop/M_Wireframe.uasset differ diff --git a/Content/Sample/Desktop/PP_IDBufferVisualization.uasset b/Content/Sample/Desktop/PP_IDBufferVisualization.uasset new file mode 100644 index 0000000..13b7e42 Binary files /dev/null and b/Content/Sample/Desktop/PP_IDBufferVisualization.uasset differ diff --git a/Content/Sample/Desktop/SM_HeadPoseDebug.uasset b/Content/Sample/Desktop/SM_HeadPoseDebug.uasset new file mode 100644 index 0000000..f4d6a30 Binary files /dev/null and b/Content/Sample/Desktop/SM_HeadPoseDebug.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/BP_BalloonWithBottles.uasset b/Content/Sample/Desktop/ShooterScene/BP_BalloonWithBottles.uasset new file mode 100644 index 0000000..9ea9d69 Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/BP_BalloonWithBottles.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/BP_Grenade.uasset b/Content/Sample/Desktop/ShooterScene/BP_Grenade.uasset new file mode 100644 index 0000000..20ab5ce Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/BP_Grenade.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/BP_IceJauntCastingAnimation.uasset b/Content/Sample/Desktop/ShooterScene/BP_IceJauntCastingAnimation.uasset new file mode 100644 index 0000000..10b199b Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/BP_IceJauntCastingAnimation.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/BP_IceJauntSpell.uasset b/Content/Sample/Desktop/ShooterScene/BP_IceJauntSpell.uasset new file mode 100644 index 0000000..cbddc23 Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/BP_IceJauntSpell.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/BP_InfoSignPage_DesktopShooterInfo.uasset b/Content/Sample/Desktop/ShooterScene/BP_InfoSignPage_DesktopShooterInfo.uasset new file mode 100644 index 0000000..c9eccef Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/BP_InfoSignPage_DesktopShooterInfo.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/BP_TobiiHUD.uasset b/Content/Sample/Desktop/ShooterScene/BP_TobiiHUD.uasset new file mode 100644 index 0000000..a4a7dd3 Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/BP_TobiiHUD.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/DesktopShooterLevel.umap b/Content/Sample/Desktop/ShooterScene/DesktopShooterLevel.umap new file mode 100644 index 0000000..bec05f9 Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/DesktopShooterLevel.umap differ diff --git a/Content/Sample/Desktop/ShooterScene/FirstPersonFire_Montage.uasset b/Content/Sample/Desktop/ShooterScene/FirstPersonFire_Montage.uasset new file mode 100644 index 0000000..8dc3131 Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/FirstPersonFire_Montage.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/FirstPerson_AnimBP.uasset b/Content/Sample/Desktop/ShooterScene/FirstPerson_AnimBP.uasset new file mode 100644 index 0000000..3d4ac6b Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/FirstPerson_AnimBP.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/FirstPerson_Fire.uasset b/Content/Sample/Desktop/ShooterScene/FirstPerson_Fire.uasset new file mode 100644 index 0000000..e1a2edf Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/FirstPerson_Fire.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/FirstPerson_Idle.uasset b/Content/Sample/Desktop/ShooterScene/FirstPerson_Idle.uasset new file mode 100644 index 0000000..b25eb4b Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/FirstPerson_Idle.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/FirstPerson_JumpEnd.uasset b/Content/Sample/Desktop/ShooterScene/FirstPerson_JumpEnd.uasset new file mode 100644 index 0000000..cd2ad02 Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/FirstPerson_JumpEnd.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/FirstPerson_JumpLoop.uasset b/Content/Sample/Desktop/ShooterScene/FirstPerson_JumpLoop.uasset new file mode 100644 index 0000000..bc23821 Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/FirstPerson_JumpLoop.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/FirstPerson_JumpStart.uasset b/Content/Sample/Desktop/ShooterScene/FirstPerson_JumpStart.uasset new file mode 100644 index 0000000..a54aef4 Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/FirstPerson_JumpStart.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/FirstPerson_Run.uasset b/Content/Sample/Desktop/ShooterScene/FirstPerson_Run.uasset new file mode 100644 index 0000000..6357204 Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/FirstPerson_Run.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/SK_FPGun.uasset b/Content/Sample/Desktop/ShooterScene/SK_FPGun.uasset new file mode 100644 index 0000000..cc5ec4a Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/SK_FPGun.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/SK_FPGun_PhysicsAsset.uasset b/Content/Sample/Desktop/ShooterScene/SK_FPGun_PhysicsAsset.uasset new file mode 100644 index 0000000..78ee706 Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/SK_FPGun_PhysicsAsset.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/SK_FPGun_Skeleton.uasset b/Content/Sample/Desktop/ShooterScene/SK_FPGun_Skeleton.uasset new file mode 100644 index 0000000..7abe794 Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/SK_FPGun_Skeleton.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/SK_Mannequin_Arms.uasset b/Content/Sample/Desktop/ShooterScene/SK_Mannequin_Arms.uasset new file mode 100644 index 0000000..9141ad6 Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/SK_Mannequin_Arms.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/SK_Mannequin_Arms_PhysicsAsset.uasset b/Content/Sample/Desktop/ShooterScene/SK_Mannequin_Arms_PhysicsAsset.uasset new file mode 100644 index 0000000..aa1382a Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/SK_Mannequin_Arms_PhysicsAsset.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/SK_Mannequin_Arms_Skeleton.uasset b/Content/Sample/Desktop/ShooterScene/SK_Mannequin_Arms_Skeleton.uasset new file mode 100644 index 0000000..bd4f49f Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/SK_Mannequin_Arms_Skeleton.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/W_DesktopExampleHUD.uasset b/Content/Sample/Desktop/ShooterScene/W_DesktopExampleHUD.uasset new file mode 100644 index 0000000..82b23aa Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/W_DesktopExampleHUD.uasset differ diff --git a/Content/Sample/Desktop/ShooterScene/W_DesktopShooterInfo.uasset b/Content/Sample/Desktop/ShooterScene/W_DesktopShooterInfo.uasset new file mode 100644 index 0000000..b4d9559 Binary files /dev/null and b/Content/Sample/Desktop/ShooterScene/W_DesktopShooterInfo.uasset differ diff --git a/Content/Sample/Desktop/SimulationScene/BP_GatlingBullet.uasset b/Content/Sample/Desktop/SimulationScene/BP_GatlingBullet.uasset new file mode 100644 index 0000000..d3db997 Binary files /dev/null and b/Content/Sample/Desktop/SimulationScene/BP_GatlingBullet.uasset differ diff --git a/Content/Sample/Desktop/SimulationScene/BP_HelicopterPawn.uasset b/Content/Sample/Desktop/SimulationScene/BP_HelicopterPawn.uasset new file mode 100644 index 0000000..ad25ca8 Binary files /dev/null and b/Content/Sample/Desktop/SimulationScene/BP_HelicopterPawn.uasset differ diff --git a/Content/Sample/Desktop/SimulationScene/BP_HelicopterUI.uasset b/Content/Sample/Desktop/SimulationScene/BP_HelicopterUI.uasset new file mode 100644 index 0000000..8cbe612 Binary files /dev/null and b/Content/Sample/Desktop/SimulationScene/BP_HelicopterUI.uasset differ diff --git a/Content/Sample/Desktop/SimulationScene/BP_HomingMissile.uasset b/Content/Sample/Desktop/SimulationScene/BP_HomingMissile.uasset new file mode 100644 index 0000000..bba9c72 Binary files /dev/null and b/Content/Sample/Desktop/SimulationScene/BP_HomingMissile.uasset differ diff --git a/Content/Sample/Desktop/SimulationScene/BP_InfoSignPage_DesktopSimulationInfo.uasset b/Content/Sample/Desktop/SimulationScene/BP_InfoSignPage_DesktopSimulationInfo.uasset new file mode 100644 index 0000000..5c9a3ed Binary files /dev/null and b/Content/Sample/Desktop/SimulationScene/BP_InfoSignPage_DesktopSimulationInfo.uasset differ diff --git a/Content/Sample/Desktop/SimulationScene/DesktopSimulationLevel.umap b/Content/Sample/Desktop/SimulationScene/DesktopSimulationLevel.umap new file mode 100644 index 0000000..c1a279d Binary files /dev/null and b/Content/Sample/Desktop/SimulationScene/DesktopSimulationLevel.umap differ diff --git a/Content/Sample/Desktop/SimulationScene/PS_ToonRocketThruster.uasset b/Content/Sample/Desktop/SimulationScene/PS_ToonRocketThruster.uasset new file mode 100644 index 0000000..fda685b Binary files /dev/null and b/Content/Sample/Desktop/SimulationScene/PS_ToonRocketThruster.uasset differ diff --git a/Content/Sample/Desktop/SimulationScene/ShipBodyMesh.uasset b/Content/Sample/Desktop/SimulationScene/ShipBodyMesh.uasset new file mode 100644 index 0000000..22789b5 Binary files /dev/null and b/Content/Sample/Desktop/SimulationScene/ShipBodyMesh.uasset differ diff --git a/Content/Sample/Desktop/SimulationScene/W_DesktopSimulationInfo.uasset b/Content/Sample/Desktop/SimulationScene/W_DesktopSimulationInfo.uasset new file mode 100644 index 0000000..6370554 Binary files /dev/null and b/Content/Sample/Desktop/SimulationScene/W_DesktopSimulationInfo.uasset differ diff --git a/Content/Sample/Desktop/SimulationScene/W_SimulationUI_Bottom.uasset b/Content/Sample/Desktop/SimulationScene/W_SimulationUI_Bottom.uasset new file mode 100644 index 0000000..5e9c9fa Binary files /dev/null and b/Content/Sample/Desktop/SimulationScene/W_SimulationUI_Bottom.uasset differ diff --git a/Content/Sample/Desktop/SimulationScene/W_SimulationUI_Left.uasset b/Content/Sample/Desktop/SimulationScene/W_SimulationUI_Left.uasset new file mode 100644 index 0000000..f1a4d04 Binary files /dev/null and b/Content/Sample/Desktop/SimulationScene/W_SimulationUI_Left.uasset differ diff --git a/Content/Sample/Desktop/SimulationScene/W_SimulationUI_Right.uasset b/Content/Sample/Desktop/SimulationScene/W_SimulationUI_Right.uasset new file mode 100644 index 0000000..1aee89a Binary files /dev/null and b/Content/Sample/Desktop/SimulationScene/W_SimulationUI_Right.uasset differ diff --git a/Content/Sample/Desktop/SocialScene/DesktopSocialLevel.umap b/Content/Sample/Desktop/SocialScene/DesktopSocialLevel.umap new file mode 100644 index 0000000..d56f71e Binary files /dev/null and b/Content/Sample/Desktop/SocialScene/DesktopSocialLevel.umap differ diff --git a/Content/Sample/Desktop/W_DesktopSceneSelector.uasset b/Content/Sample/Desktop/W_DesktopSceneSelector.uasset new file mode 100644 index 0000000..9e1d32b Binary files /dev/null and b/Content/Sample/Desktop/W_DesktopSceneSelector.uasset differ diff --git a/Content/Sample/XR/BPI_Grabbable.uasset b/Content/Sample/XR/BPI_Grabbable.uasset new file mode 100644 index 0000000..d7f639c Binary files /dev/null and b/Content/Sample/XR/BPI_Grabbable.uasset differ diff --git a/Content/Sample/XR/BP_DebugInfoTablet.uasset b/Content/Sample/XR/BP_DebugInfoTablet.uasset new file mode 100644 index 0000000..0b065f8 Binary files /dev/null and b/Content/Sample/XR/BP_DebugInfoTablet.uasset differ diff --git a/Content/Sample/XR/BP_ToolSelectMenu.uasset b/Content/Sample/XR/BP_ToolSelectMenu.uasset new file mode 100644 index 0000000..f2b74fc Binary files /dev/null and b/Content/Sample/XR/BP_ToolSelectMenu.uasset differ diff --git a/Content/Sample/XR/BP_TutorialTablet.uasset b/Content/Sample/XR/BP_TutorialTablet.uasset new file mode 100644 index 0000000..b7f663d Binary files /dev/null and b/Content/Sample/XR/BP_TutorialTablet.uasset differ diff --git a/Content/Sample/XR/BP_XRExampleGameMode.uasset b/Content/Sample/XR/BP_XRExampleGameMode.uasset new file mode 100644 index 0000000..965f866 Binary files /dev/null and b/Content/Sample/XR/BP_XRExampleGameMode.uasset differ diff --git a/Content/Sample/XR/BP_XRExamplePawn.uasset b/Content/Sample/XR/BP_XRExamplePawn.uasset new file mode 100644 index 0000000..f82647d Binary files /dev/null and b/Content/Sample/XR/BP_XRExamplePawn.uasset differ diff --git a/Content/Sample/XR/BP_XRExamplePlayerController.uasset b/Content/Sample/XR/BP_XRExamplePlayerController.uasset new file mode 100644 index 0000000..17ef5c0 Binary files /dev/null and b/Content/Sample/XR/BP_XRExamplePlayerController.uasset differ diff --git a/Content/Sample/XR/CalibrationScene/BP_InfoSignPage_XRCalibrationInfo.uasset b/Content/Sample/XR/CalibrationScene/BP_InfoSignPage_XRCalibrationInfo.uasset new file mode 100644 index 0000000..80dee97 Binary files /dev/null and b/Content/Sample/XR/CalibrationScene/BP_InfoSignPage_XRCalibrationInfo.uasset differ diff --git a/Content/Sample/XR/CalibrationScene/W_CalibrationSceneInfo_XRCalibrationInfo.uasset b/Content/Sample/XR/CalibrationScene/W_CalibrationSceneInfo_XRCalibrationInfo.uasset new file mode 100644 index 0000000..45edcfe Binary files /dev/null and b/Content/Sample/XR/CalibrationScene/W_CalibrationSceneInfo_XRCalibrationInfo.uasset differ diff --git a/Content/Sample/XR/CalibrationScene/XRCalibrationLevel.umap b/Content/Sample/XR/CalibrationScene/XRCalibrationLevel.umap new file mode 100644 index 0000000..68d6d7b Binary files /dev/null and b/Content/Sample/XR/CalibrationScene/XRCalibrationLevel.umap differ diff --git a/Content/Sample/XR/E_Tools.uasset b/Content/Sample/XR/E_Tools.uasset new file mode 100644 index 0000000..5d6efd1 Binary files /dev/null and b/Content/Sample/XR/E_Tools.uasset differ diff --git a/Content/Sample/XR/ShooterScene/XRShooterLevel.umap b/Content/Sample/XR/ShooterScene/XRShooterLevel.umap new file mode 100644 index 0000000..0ede85a Binary files /dev/null and b/Content/Sample/XR/ShooterScene/XRShooterLevel.umap differ diff --git a/Content/Sample/XR/SocialScene/MirrorTexture.uasset b/Content/Sample/XR/SocialScene/MirrorTexture.uasset new file mode 100644 index 0000000..6d495e3 Binary files /dev/null and b/Content/Sample/XR/SocialScene/MirrorTexture.uasset differ diff --git a/Content/Sample/XR/SocialScene/XRSocialLevel.umap b/Content/Sample/XR/SocialScene/XRSocialLevel.umap new file mode 100644 index 0000000..2cf9f8c Binary files /dev/null and b/Content/Sample/XR/SocialScene/XRSocialLevel.umap differ diff --git a/Content/Sample/XR/ThrowingScene/BP_BarrelWithMagicRocks.uasset b/Content/Sample/XR/ThrowingScene/BP_BarrelWithMagicRocks.uasset new file mode 100644 index 0000000..afcc9c9 Binary files /dev/null and b/Content/Sample/XR/ThrowingScene/BP_BarrelWithMagicRocks.uasset differ diff --git a/Content/Sample/XR/ThrowingScene/BP_BarrelWithRocks.uasset b/Content/Sample/XR/ThrowingScene/BP_BarrelWithRocks.uasset new file mode 100644 index 0000000..4c20273 Binary files /dev/null and b/Content/Sample/XR/ThrowingScene/BP_BarrelWithRocks.uasset differ diff --git a/Content/Sample/XR/ThrowingScene/BP_MagicThrowingRock.uasset b/Content/Sample/XR/ThrowingScene/BP_MagicThrowingRock.uasset new file mode 100644 index 0000000..7b49c89 Binary files /dev/null and b/Content/Sample/XR/ThrowingScene/BP_MagicThrowingRock.uasset differ diff --git a/Content/Sample/XR/ThrowingScene/BP_ThrowingRock.uasset b/Content/Sample/XR/ThrowingScene/BP_ThrowingRock.uasset new file mode 100644 index 0000000..985f974 Binary files /dev/null and b/Content/Sample/XR/ThrowingScene/BP_ThrowingRock.uasset differ diff --git a/Content/Sample/XR/ThrowingScene/XRThrowingLevel.umap b/Content/Sample/XR/ThrowingScene/XRThrowingLevel.umap new file mode 100644 index 0000000..2af1081 Binary files /dev/null and b/Content/Sample/XR/ThrowingScene/XRThrowingLevel.umap differ diff --git a/Content/Sample/XR/UserInterfaceScene/BP_UIDemonstration.uasset b/Content/Sample/XR/UserInterfaceScene/BP_UIDemonstration.uasset new file mode 100644 index 0000000..1eae908 Binary files /dev/null and b/Content/Sample/XR/UserInterfaceScene/BP_UIDemonstration.uasset differ diff --git a/Content/Sample/XR/UserInterfaceScene/W_UIDemonstration.uasset b/Content/Sample/XR/UserInterfaceScene/W_UIDemonstration.uasset new file mode 100644 index 0000000..c1cda80 Binary files /dev/null and b/Content/Sample/XR/UserInterfaceScene/W_UIDemonstration.uasset differ diff --git a/Content/Sample/XR/UserInterfaceScene/XRUserInterfaceLevel.umap b/Content/Sample/XR/UserInterfaceScene/XRUserInterfaceLevel.umap new file mode 100644 index 0000000..5cb1222 Binary files /dev/null and b/Content/Sample/XR/UserInterfaceScene/XRUserInterfaceLevel.umap differ diff --git a/Content/Sample/XR/W_DebugInfoWidget.uasset b/Content/Sample/XR/W_DebugInfoWidget.uasset new file mode 100644 index 0000000..2444e59 Binary files /dev/null and b/Content/Sample/XR/W_DebugInfoWidget.uasset differ diff --git a/Content/Sample/XR/W_ToolSelectMenu.uasset b/Content/Sample/XR/W_ToolSelectMenu.uasset new file mode 100644 index 0000000..a4c1af0 Binary files /dev/null and b/Content/Sample/XR/W_ToolSelectMenu.uasset differ diff --git a/Content/Sample/XR/W_TutorialWidget.uasset b/Content/Sample/XR/W_TutorialWidget.uasset new file mode 100644 index 0000000..7971c29 Binary files /dev/null and b/Content/Sample/XR/W_TutorialWidget.uasset differ diff --git a/Content/Sample/XR/W_XRSceneSelector.uasset b/Content/Sample/XR/W_XRSceneSelector.uasset new file mode 100644 index 0000000..0ef75df Binary files /dev/null and b/Content/Sample/XR/W_XRSceneSelector.uasset differ diff --git a/Content/Sample/XR/XRExampleMap.umap b/Content/Sample/XR/XRExampleMap.umap new file mode 100644 index 0000000..d69cf07 Binary files /dev/null and b/Content/Sample/XR/XRExampleMap.umap differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..f97a79a --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Thank You for downloading the Tobii Unreal Engine SDK! + +## To get started: + +### Prerequisites: +Tobii Unreal Engine SDK requires the following setup on your computer: + +* Unreal Engine 4.21 or later +* Windows 10, 8.1, or 7 +* Target build platform Windows32/64 +* Tobii Eye Tracking Core Software (either downloaded install bundle for peripheral eye tracker, or pre-installed on integrated systems) +* Tobii consumer eye tracker peripheral or built-in device. + +### Add the Plugin to your Project: +Unzip this package into the Plugins folder in your project. The uplugin file should be located here: + + MyProject/Plugins/TobiiEyetracking/TobiiEyetracking.uplugin + +Launch your project again and follow the prompts from there to get to the samples and try out some +eyetracked interactions! + +If you are developing for desktop, a good place to start is the Tobii +Unreal Engine SDK section on for desktop [Tobii Developer Zone] (https://developer.tobii.com/pc-gaming/unreal-engine-sdk/api-reference/). + +If you are focusing on VR have a look at the VR section of [Tobii Developer Zone] (https://developer.tobii.com/vr/develop/ue4/api/introduction/). + +**Have Fun!** \ No newline at end of file diff --git a/Resources/CleanUI16x.png b/Resources/CleanUI16x.png new file mode 100644 index 0000000..0132a23 Binary files /dev/null and b/Resources/CleanUI16x.png differ diff --git a/Resources/Icon128.png b/Resources/Icon128.png new file mode 100644 index 0000000..e09a584 Binary files /dev/null and b/Resources/Icon128.png differ diff --git a/Resources/License.txt b/Resources/License.txt new file mode 100644 index 0000000..1769213 --- /dev/null +++ b/Resources/License.txt @@ -0,0 +1,122 @@ +TOBII TECH GETTING STARTED SOFTWARE DEVELOPMENT LICENSE AGREEMENT + +The Software Development Kit (“SDK”) and other software and related files that you have downloaded, or otherwise obtained, includes Tobii software which is governed by this license agreement (the “Licensed Software”). This agreement grants you limited rights to use the Licensed Software to develop your own software. No support or maintenance for the Licensed Software is provided to you under this agreement. +- You may only develop software for your personal and non-commercial use under this agreement. If you wish to develop or distribute software to the public or any third-party for commercial use, you must obtain a Commercial License. In order to obtain such license, please contact Tobii at https://www.tobii.com/tech/contact/sales-form/ +- Under this limited license, you may not develop software that stores eye tracking data or transfers eye tracking data to another device or network. If you are developing software where eye tracking data is stored, or where it is transferred to another device or network, you must obtain another license. Please see https://transparency.tobii.com/ for more information and contact Tobii at https://www.tobii.com/tech/contact/sales-form/. +- Under this limited license, you may not develop software for medical solutions or medical equipment. If you wish to develop such software, you need to obtain a Medical Use License. In order to obtain such license, please contact Tobii at https://www.tobii.com/tech/contact/sales-form/ + +Please note that the Licensed Software is only for use with Tobii related hardware and software, and you may not use it to develop or evaluate competing eye tracking technologies. + +If you are interested in additional eye tracking capabilities or functionality, or if you would like to receive support and maintenance from Tobii, please contact Tobii at https://www.tobii.com/tech/contact/sales-form/ + + + +Tobii Tech AB (reg. No. 559104-1081), having its registered office at Karlsrovägen 2D, SE-182 53, Danderyd, Sweden ("Tobii"), grants you (“You”) a license to use the Licensed Software according to the following terms and conditions (the “Agreement”). Both Tobii and You are a “Party” to this Agreement and referred to together as “Parties”. You enter into this Agreement by installing or using the Licensed Software. + +WHEREAS, Tobii is a supplier of hardware and software solutions, either standalone or as part of third-party products, for eye tracking which contain intellectual property rights owned by Tobii (“Tobii Eye Tracking”), and You desire to license from Tobii to use certain Licensed Software as defined by this Agreement. Any other use of the Licensed Software requires a separate agreement between the Parties. + +BEFORE USING THE LICENSED SOFTWARE PLEASE READ THIS AGREEMENT CAREFULLY. BY DOWNLOADING, INSTALLING, USING, ACCESSING OR DISTRIBUTING THE LICENSED SOFTWARE YOU AGREE THAT: +i) YOU HAVE READ AND UNDERSTOOD THE TERMS AND CONDITIONS IN THIS AGREEMENT; +ii) YOU AGREE TO BE BOUND BY THE TERMS AND CONDITIONS OF THIS AGREEMENT; AND +iii) UNLESS SPECIFICALLY EXCLUDED BY AN EXISTING AGREEMENT BETWEEN YOU AND TOBII, THIS IS THE COMPLETE AND EXCLUSIVE STATEMENT OF THE RIGHTS AND LIABILITY BETWEEN YOU AND TOBII IN RELATION TO YOUR USE AND DISTRIBUTION OF THE LICENSED SOFTWARE, AND THIS AGREEMENT SUPERSEDES ALL PRIOR COMMUNICATIONS AND AGREEMENTS BETWEEN YOU AND TOBII REGARDING THE LICENSED SOFTWARE. + +You may only enter into this agreement and use the Licensed Software if you are legally permitted to enter into a binding contract with Tobii. + +The terms of this Agreement will apply to any updates that Tobii makes available to you. You agree that updates may require you to change or update your application and may affect your ability to use, access or interact with Licensed Software. + +Tobii may amend this Agreement at any time, without notice to You, by posting the revised terms on its website www.tobii.com. + + +DEFINITIONS + +The terms below are defined as follows under this Agreement: + +“Eye Tracking Data” means data about a user’s eye gaze, eye images, pupil size, facial expression, presence, behaviour, head pose or position from Tobii Eye Tracking, in raw or processed form, on its own or in combination with other information. + +“Intellectual Property Rights” means patents, rights to apply for patents, trademarks, trade names, service marks, domain names, copyrights and all applications and registrations of such worldwide, schematics, industrial models, inventions, know-how, trade secrets, computer software programs and other intangible proprietary information. + +“Licensed Software” means the Software provided to You by Tobii, including all included files, directories, documentation, and associated API function calls. + +“Your Software” means software developed by You, including all object code, source code, and any documentation thereof. + + +1. PERMITTED USE + +1.1 Tobii provides You limited rights to use the Licensed Software subject to the terms and restrictions contained in this Agreement. + +1.2 Your use of the Licensed Software is strictly permitted solely to develop Software for your own personal and non-commercial use (“Your Use”). + + +2. LICENSE + +2.1 Subject to Sections 2.2, 3 and 7, Tobii grants You a limited, non-exclusive, non-transferable, non-sublicensable, royalty-free, revocable, license: +a) to use the Licensed Software solely to develop Your Software for Your Use; and +b) to any Intellectual Property Rights owned by Tobii covering binary files, or example source code, in the Licensed Software, necessary for Your Use. + +2.2 The license rights in this Section 2 are conditional upon compliance with all terms in this Agreement and, specifically, the following: +a) You may not commercialise, sell, lease or profit from Your Software, including by providing ancillary services in relation to it. +b) You may not use the Licensed Software on other eye tracking platforms than those produced by Tobii, or platforms that include Tobii hardware. +c) Your Software may not store or transfer the Eye Tracking Data on or to any other application, computing device or network. +d) Your Software must not allow other software to access Eye Tracking Data that originates from the Licensed Software. + + +3. USE RESTRICTIONS + +3.1 Except as expressly permitted by this Agreement, You will not, nor permit or authorise anyone to: +a) distribute, convey, lend, lease, share, sell, transfer, sublicense, rent, or time share the Licensed Software, or any of its components or license files, or permit third parties to download or install the Licensed Software. +b) copy, decompile, disassemble or reverse engineer or otherwise attempt to extract or derive the source code or any methods, algorithms or procedures from the Licensed Software, or modify, adapt, translate or create derivative works based upon the Tobii Products except as otherwise expressly permitted by applicable law. +c) use the Licensed Software if You are a competitor, or use the Licensed Software in any manner that competes with Tobii, including but not limited to, benchmarking, collecting and publishing data or analysis relating to the performance of the Licensed Software, or developing or marketing a product that is competitive with any Tobii product or service. +d) remove any copyright, trademark or other proprietary notice from the Licensed Software. +e) alter or circumvent any product, key or license restrictions or limitations on Licensed Software to exceed purchased quantities or to defeat any restrictions on access or use. + + +4. OWNERSHIP OF LICENSED SOFTWARE + +Except for the licenses explicitly granted to You, Tobii retains all right, title, and interest in and to the Licensed Software, including all updates and modifications and derivative works created, whether or not specifically recognised, registered, or perfected under the laws of the country in which the Licensed Software is located. You do not own any rights, including the copyright or any other Intellectual Property Rights, in the Licensed Software. Your rights to use the Licensed Software are strictly as specified in this Agreement. + + +5. USAGE DATA TO ENHANCE USER EXPERIENCE + +The Licensed Software may report anonymous usage statistics and/or error and bug reports to Tobii's servers in order to identify problems that may affect the technical stability and/or overall quality of the application, as well as inform Tobii which of the program components have been in use. For the avoidance of doubt, this data does not include Eye Tracking Data. + + +6. SUPPORT & MAINTENANCE + +Tobii will not provide You with any support for, or maintenance of, the Licensed Software, including no obligation to provide updates to the Licensed Software. + + +7. MEDICAL OR HIGH-RISK USE + +7.1 Medical Use: You may not develop Your Software for Medical Use under this Agreement. “Medical Use” is any use in medically classified devices or environments, for instance hospitals, surgeries, or other healthcare environments. Examples of Medical Use include systems for Assistive and Alternative Communication (AAC), medical condition assessment solutions, vision or eye condition solutions and solutions or systems intended to be used in hospitals, surgeries and doctor’s consulting rooms. If you wish to use the Licensed Software for Medical Use, then you must contact Tobii Tech at https://www.tobii.com/tech/contact/sales-form/. + +7.2 High-Risk Use: You have no right to develop Your Software for High-Risk Use. “High Risk Use” is use in any environment where failure or fault of any kind could lead to death or serious bodily injury of any person, or to severe physical or environmental damage. High Risk Use includes, but is not limited to, aircraft navigation, control of other modes of human mass transportation, military applications and control of nuclear or chemical facilities. + + +8. LIMITATION OF LIABILITY AND INDEMNIFICATION + +8.1 In no event will Tobii be liable for the following, whether arising out of the use of, or inability to use, the Licensed Software, or for any other reason, even if it has been advised of the possibility of such damages: (a) indirect, incidental, special or consequential damages; (b) loss or corruption of data or interrupted or loss of business; or (c) loss of revenue, profits, goodwill or anticipated sales or savings. Tobii and its affiliates, officers, directors, employees, agents and suppliers’ maximum liability arising out of, or in connection with, the Licensed Software regardless of the cause of action, whether in tort (including negligence), breach, warranty, contract or otherwise will not exceed USD $100. Some jurisdictions do not allow the limitation or exclusion of liability. Accordingly, some of these limitations may not apply to You. To the extent that Tobii may not, as a matter of applicable law, disclaim any implied warranty or limit its liabilities, the scope of duration of such warranty and the extent of Tobii’s liability shall be the minimum permitted under such applicable law. + +8.2 You agree to indemnify, defend and hold harmless Tobii and its licensors, affiliates, contractors, officers, directors, employees or agents from any and all third-party claims, liabilities, damages, costs and +expenses, including reasonable attorney fees, arising from your violation of any provision of Sections 2 and 3 of this Agreement. + + +9. TERM AND TERMINATION + +9.1 This Agreement remains in force until termination (the “Term”). + +9.2 Tobii may terminate this Agreement at any time for convenience. + +9.3 Upon termination, all obligations relating to the ownership of the Licensed Technology and any indemnification or liability obligations survive. + + +10. GENERAL + +10.1 Amendment: Tobii may amend this Agreement at any time, without notice to You, by posting the revised terms on its website www.tobii.com. + +10.2 Assignment: You may not assign this Agreement or any of its rights, interests, or obligations hereunder. + +10.3 NO WARRANTY: EXCEPT AS SPECIFICALLY PROVIDED HEREIN, TOBII MAKES NO WARRANTY, EITHER EXPRESS OR IMPLIED, RELATING TO THE LICENSED SOFTWARE AND TOBII FURTHER EXPRESSLY DISCLAIMS, TO THE EXTENT PERMITTED BY APPLICABLE LAW, ALL WARRANTIES AND CONDITIONS OF ANY KIND RELATED THERETO, WHETHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, ANY IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, ACCURACY, OR FITNESS FOR A PARTICULAR PURPOSE. + +10.4 Governing Law: This Agreement will be governed by and construed according to the laws of Sweden (without regard to conflict of laws principles), all rights and remedies being governed by said laws. Any dispute or conflict under this agreement shall be finally settled by arbitration administered by the Arbitration Institute of the Stockholm Chamber of Commerce, in accordance with its rules. The seat of the arbitration will be Stockholm and the tribunal shall comprise of one (1) arbitrator. The language of the arbitration will be English. + +10.5 Disclaimer and Injunctive Relief: The Parties specifically disclaim the application of the UN Convention on Contracts for the International Sale of Goods. Regardless of the above governing law, Tobii may seek interim injunctive relief respect to any alleged breach of its Intellectual Property Rights, or any other proprietary rights. diff --git a/Source/TobiiCore/Private/TobiiBlueprintLibrary.cpp b/Source/TobiiCore/Private/TobiiBlueprintLibrary.cpp new file mode 100644 index 0000000..435c723 --- /dev/null +++ b/Source/TobiiCore/Private/TobiiBlueprintLibrary.cpp @@ -0,0 +1,322 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#include "TobiiBlueprintLibrary.h" +#include "TobiiCoreModule.h" +#include "TobiiPlatformSpecific.h" +#include "ITobiiCore.h" + +#include "IXRTrackingSystem.h" +#include "Engine/LocalPlayer.h" +#include "Engine/Engine.h" +#include "GameFramework/PlayerController.h" +#include "Misc/ConfigCacheIni.h" +#include "Misc/Paths.h" +#include "Slate/SceneViewport.h" + +UTobiiBlueprintLibrary::UTobiiBlueprintLibrary(const class FObjectInitializer& ObjectInitializer) +: Super(ObjectInitializer) +{ +} + +void UTobiiBlueprintLibrary::SetTobiiEyetrackingEnabled(bool bEyetrackingEnabled) +{ + SetTobiiInt("tobii.EnableEyetracking", bEyetrackingEnabled ? 1 : 0); +} + +bool UTobiiBlueprintLibrary::GetTobiiEyetrackingEnabled() +{ + return GetTobiiInt("tobii.EnableEyetracking") != 0; +} + +void UTobiiBlueprintLibrary::SetTobiiEyetrackingFrozen(bool bEyetrackingFrozen) +{ + SetTobiiInt("tobii.FreezeGazeData", bEyetrackingFrozen ? 1 : 0); +} + +bool UTobiiBlueprintLibrary::GetTobiiEyetrackingFrozen() +{ + return GetTobiiInt("tobii.FreezeGazeData") != 0; +} + +FTobiiGazeData UTobiiBlueprintLibrary::GetTobiiCombinedGazeData() +{ + static FTobiiGazeData DummyData; + return FTobiiCoreModule::IsAvailable() ? FTobiiCoreModule::GetEyeTracker()->GetCombinedGazeData() : DummyData; +} + +FTobiiGazeData UTobiiBlueprintLibrary::GetTobiiLeftGazeData() +{ + static FTobiiGazeData DummyData; + return FTobiiCoreModule::IsAvailable() ? FTobiiCoreModule::GetEyeTracker()->GetLeftGazeData() : DummyData; +} + +FTobiiGazeData UTobiiBlueprintLibrary::GetTobiiRightGazeData() +{ + static FTobiiGazeData DummyData; + return FTobiiCoreModule::IsAvailable() ? FTobiiCoreModule::GetEyeTracker()->GetRightGazeData() : DummyData; +} + +FHitResult UTobiiBlueprintLibrary::GetTobiiCombinedWorldGazeHitData() +{ + static FHitResult DummyData; + DummyData.Actor = nullptr; + return FTobiiCoreModule::IsAvailable() ? FTobiiCoreModule::GetEyeTracker()->GetCombinedWorldGazeHitData() : DummyData; +} + +ETobiiGazeTrackerStatus UTobiiBlueprintLibrary::GetTobiiGazeTrackerStatus() +{ + return FTobiiCoreModule::IsAvailable() ? FTobiiCoreModule::GetEyeTracker()->GetGazeTrackerStatus() : ETobiiGazeTrackerStatus::NotConnected; +} + +FTobiiDisplayInfo UTobiiBlueprintLibrary::GetTobiiDisplayInformation() +{ + return FTobiiCoreModule::IsAvailable() ? FTobiiCoreModule::GetEyeTracker()->GetDisplayInformation() : FTobiiDisplayInfo(); +} + +const FTobiiHeadPoseData& UTobiiBlueprintLibrary::GetTobiiHeadPoseData() +{ + static FTobiiHeadPoseData DummyData; + return FTobiiCoreModule::IsAvailable() ? FTobiiCoreModule::GetEyeTracker()->GetHeadPoseData() : DummyData; +} + +const FTobiiDesktopTrackBox& UTobiiBlueprintLibrary::GetTobiiDesktopTrackBox() +{ + static FTobiiDesktopTrackBox DummyData; + return FTobiiCoreModule::IsAvailable() ? FTobiiCoreModule::GetEyeTracker()->GetDesktopTrackBox() : DummyData; +} + +FRotator UTobiiBlueprintLibrary::GetTobiiInfiniteScreenAngles() +{ + if (FTobiiCoreModule::IsAvailable()) + { + return ITobiiCore::GetEyeTracker()->GetInfiniteScreenAngles(); + } + else + { + return FRotator::ZeroRotator; + } +} + +/************************************************************************/ +/* Utils */ +/************************************************************************/ +bool UTobiiBlueprintLibrary::VirtualDesktopPixelToViewportCoordinateUNorm(const FVector2D& VirtualDesktopPixel, FVector2D& OutViewportCoordinateUNorm) +{ + if (!FTobiiCoreModule::IsAvailable() || GEngine == nullptr || GEngine->GameViewport == nullptr || GEngine->GameViewport->GetGameViewport() == nullptr) + { + return false; + } + + OutViewportCoordinateUNorm = GEngine->GameViewport->GetGameViewport()->VirtualDesktopPixelToViewport(FIntPoint(VirtualDesktopPixel.X, VirtualDesktopPixel.Y)); + return true; +} + +bool UTobiiBlueprintLibrary::ViewportCoordinateUNormToVirtualDesktopPixel(const FVector2D& ViewportCoordinateUNorm, FVector2D& OutVirtualDesktopPixel) +{ + if (!FTobiiCoreModule::IsAvailable() || GEngine == nullptr || GEngine->GameViewport == nullptr || GEngine->GameViewport->GetGameViewport() == nullptr) + { + return false; + } + + const FTobiiDisplayInfo& DisplayInfo = FTobiiCoreModule::GetEyeTracker()->GetDisplayInformation(); + FVector2D ScaledInPoint(ViewportCoordinateUNorm.X / DisplayInfo.DpiScale, ViewportCoordinateUNorm.Y / DisplayInfo.DpiScale); + OutVirtualDesktopPixel = GEngine->GameViewport->GetGameViewport()->ViewportToVirtualDesktopPixel(ScaledInPoint); + + return true; +} + +bool UTobiiBlueprintLibrary::ViewportPixelCoordToCmCoord(const FVector2D& InCoordinatePx, FVector2D& OutCoordinateCm) +{ + const FTobiiDisplayInfo& DisplayInfo = FTobiiCoreModule::GetEyeTracker()->GetDisplayInformation(); + + if (DisplayInfo.MainViewportWidthPx > 0.0f && DisplayInfo.MainViewportWidthCm > 0.0f && DisplayInfo.MainViewportHeightPx > 0.0f && DisplayInfo.MainViewportHeightCm > 0.0f) + { + OutCoordinateCm.Set(InCoordinatePx.X / DisplayInfo.MainViewportWidthPx * DisplayInfo.MainViewportWidthCm, InCoordinatePx.Y / DisplayInfo.MainViewportHeightPx * DisplayInfo.MainViewportHeightCm); + return true; + } + + OutCoordinateCm = FVector2D::ZeroVector; + return false; +} + +bool UTobiiBlueprintLibrary::ViewportCmCoordToPixelCoord(const FVector2D& InCoordinateCm, FVector2D& OutCoordinatePx) +{ + const FTobiiDisplayInfo& DisplayInfo = FTobiiCoreModule::GetEyeTracker()->GetDisplayInformation(); + + if (DisplayInfo.MainViewportWidthPx > 0.0f && DisplayInfo.MainViewportWidthCm > 0.0f && DisplayInfo.MainViewportHeightPx > 0.0f && DisplayInfo.MainViewportHeightCm > 0.0f) + { + OutCoordinatePx.Set(InCoordinateCm.X / DisplayInfo.MainViewportWidthCm * DisplayInfo.MainViewportWidthPx, InCoordinateCm.Y / DisplayInfo.MainViewportHeightCm * DisplayInfo.MainViewportHeightPx); + return true; + } + + OutCoordinatePx = FVector2D::ZeroVector; + return false; +} + +bool UTobiiBlueprintLibrary::ViewportPixelCoordToUNormCoord(const FVector2D& InCoordinatePx, FVector2D& OutCoordinateUNorm) +{ + const FTobiiDisplayInfo& DisplayInfo = FTobiiCoreModule::GetEyeTracker()->GetDisplayInformation(); + + if (DisplayInfo.MainViewportWidthPx > 0.0f && DisplayInfo.MainViewportHeightPx > 0.0f) + { + OutCoordinateUNorm.Set(InCoordinatePx.X / DisplayInfo.MainViewportWidthPx, InCoordinatePx.Y / DisplayInfo.MainViewportHeightPx); + return true; + } + + OutCoordinateUNorm = FVector2D::ZeroVector; + return false; +} + +bool UTobiiBlueprintLibrary::ViewportUNormCoordToPixelCoord(const FVector2D& InCoordinateUNorm, FVector2D& OutCoordinatePx) +{ + const FTobiiDisplayInfo& DisplayInfo = FTobiiCoreModule::GetEyeTracker()->GetDisplayInformation(); + + if (DisplayInfo.MainViewportWidthPx > 0.0f && DisplayInfo.MainViewportHeightPx > 0.0f) + { + OutCoordinatePx.Set(InCoordinateUNorm.X * DisplayInfo.MainViewportWidthPx, InCoordinateUNorm.Y * DisplayInfo.MainViewportHeightPx); + return true; + } + + OutCoordinatePx = FVector2D::ZeroVector; + return false; +} + +// internal helper +FQuat GetHMDToWorldRotation(APlayerController* PlayerController) +{ + FQuat ToWorld = FQuat::Identity; + FVector HMDPosition; + bool valid = GEngine->XRSystem->GetCurrentPose(IXRTrackingSystem::HMDDeviceId, ToWorld, HMDPosition); + + const bool IsEmulated = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.emulation.EnableEyetrackingEmulation"))->GetInt() != 0; + const bool IsApplyControlRotation = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.xr.ApplyControlRotation"))->GetInt() != 0; + const bool IsApplyActorRotation = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.xr.ApplyActorRotation"))->GetInt() != 0; + + if (!IsEmulated) + { + if (IsApplyControlRotation) + { + FVector RealWorldUp = ToWorld.Inverse().RotateVector(FVector::UpVector); + FQuat YawRotation = FQuat(RealWorldUp, FMath::DegreesToRadians(PlayerController->GetControlRotation().Yaw)); + ToWorld *= YawRotation; + } + } + + if (IsApplyActorRotation && PlayerController != nullptr && PlayerController->GetPawn() != nullptr) + { + ToWorld *= PlayerController->GetPawn()->GetActorRotation().Quaternion(); + } + + return ToWorld; +} + +int32 UTobiiBlueprintLibrary::GetTobiiInt(FString CVarName) +{ + const auto CVar = IConsoleManager::Get().FindConsoleVariable(*CVarName); + return (CVar != nullptr) ? CVar->GetInt() : 0; +} +void UTobiiBlueprintLibrary::SetTobiiInt(FString CVarName, const int32 NewValue) +{ + const auto CVar = IConsoleManager::Get().FindConsoleVariable(*CVarName); + if (CVar != nullptr) + { + CVar->Set(NewValue); + } +} + +float UTobiiBlueprintLibrary::GetTobiiFloat(FString CVarName) +{ + const auto CVar = IConsoleManager::Get().FindConsoleVariable(*CVarName); + return (CVar != nullptr) ? CVar->GetFloat() : 0.0f; +} +void UTobiiBlueprintLibrary::SetTobiiFloat(FString CVarName, const float NewValue) +{ + const auto CVar = IConsoleManager::Get().FindConsoleVariable(*CVarName); + if (CVar != nullptr) + { + CVar->Set(NewValue); + } +} + +FString GetTobiiSettingsFilePath() +{ + return FPaths::Combine(FPaths::GeneratedConfigDir() + ANSI_TO_TCHAR(FPlatformProperties::PlatformName()), "TobiiEyetracking.ini"); +} +void UTobiiBlueprintLibrary::LoadTobiiSettings() +{ + ApplyCVarSettingsFromIni(TEXT("TobiiEyetracking"), *GetTobiiSettingsFilePath(), ECVF_SetByConstructor); +} +void UTobiiBlueprintLibrary::SaveTobiiSetting(FString CVarSettingToSave) +{ + auto CVar = IConsoleManager::Get().FindConsoleVariable(*CVarSettingToSave); + if (CVar != nullptr) + { + GConfig->EmptySection(TEXT("TobiiEyetracking"), *GetTobiiSettingsFilePath()); + GConfig->SetString(TEXT("TobiiEyetracking"), *CVarSettingToSave, *CVar->GetString(), *GetTobiiSettingsFilePath()); + GConfig->Flush(false, *GetTobiiSettingsFilePath()); + } +} + +bool UTobiiBlueprintLibrary::IsXRPlayerController(const APlayerController* PlayerController) +{ + if (GEngine != nullptr) + { + if (PlayerController != nullptr) + { + ULocalPlayer* LocalPlayer = Cast(PlayerController->Player); + if (LocalPlayer != nullptr && LocalPlayer->ViewportClient != nullptr) + { + //@TODO: Bugged in Shipping builds 4.18? Returns false even though running through VIVE. + return GEngine->IsStereoscopic3D(LocalPlayer->ViewportClient->Viewport); + } + } + + return GEngine->IsStereoscopic3D(); + } + + return false; +} + +FVector UTobiiBlueprintLibrary::ConvertGazeToUserSpace(APlayerController* PlayerController, const FVector& WorldSpaceGazeDirection) +{ + FVector LocalSpaceDirection = WorldSpaceGazeDirection; + if (GEngine == nullptr || PlayerController == nullptr) + { + return LocalSpaceDirection; + } + + if (UTobiiBlueprintLibrary::IsXRPlayerController(PlayerController)) + { + static const auto HMDOrientationCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.xr.ApplyHMDOrientation")); + static const auto ActorRotationCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.xr.ApplyActorRotation")); + + if (ActorRotationCVar->GetInt() != 0 + && PlayerController->GetPawn() != nullptr) + { + FQuat ActorRotation = PlayerController->GetPawn()->GetActorRotation().Quaternion(); + LocalSpaceDirection = ActorRotation.UnrotateVector(LocalSpaceDirection); + } + + if (HMDOrientationCVar->GetInt() != 0) + { + FQuat HMDOrientation; + FVector HMDPosition; + GEngine->XRSystem->GetCurrentPose(IXRTrackingSystem::HMDDeviceId, HMDOrientation, HMDPosition); + LocalSpaceDirection = HMDOrientation.UnrotateVector(LocalSpaceDirection); + } + } + else + { + if (PlayerController->PlayerCameraManager != nullptr) + { + FRotator CameraRotation = PlayerController->PlayerCameraManager->GetCameraRotation(); + LocalSpaceDirection = CameraRotation.UnrotateVector(LocalSpaceDirection); + } + } + + return LocalSpaceDirection; +} diff --git a/Source/TobiiCore/Private/TobiiCoreModule.cpp b/Source/TobiiCore/Private/TobiiCoreModule.cpp new file mode 100644 index 0000000..a4c7ec6 --- /dev/null +++ b/Source/TobiiCore/Private/TobiiCoreModule.cpp @@ -0,0 +1,84 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#include "TobiiCoreModule.h" + +#include "UserGuide/TobiiEditorExtension.h" + +#include "IEyeTrackerModule.h" +#include "GameFramework/HUD.h" +#include "Misc/Paths.h" + +IMPLEMENT_MODULE(FTobiiCoreModule, TobiiCore) + +/************************************************************************/ +/* FTobiiCoreModule */ +/************************************************************************/ +void FTobiiCoreModule::StartupModule() +{ + EyeTracker.Reset(); + +#if TOBII_EYETRACKING_ACTIVE + FString RelativeGICDllPath = FString(TEXT(TOBII_GIC_RELATIVE_DLL_PATH)); + +#if TOBII_COMPILE_AS_ENGINE_PLUGIN + FString FullGICDllPath = FPaths::Combine(FPaths::EngineDir(), RelativeGICDllPath); +#else + FString FullGICDllPath = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::ProjectPluginsDir(), RelativeGICDllPath)); +#endif //TOBII_COMPILE_AS_ENGINE_PLUGIN + + GICDllHandle = FPlatformProcess::GetDllHandle(*FullGICDllPath); + + if (GICDllHandle != nullptr) + { + FTobiiEyeTracker* NewEyeTracker = new FTobiiEyeTracker(); + if (NewEyeTracker != nullptr && NewEyeTracker->IsConnectedToEyeTracker()) + { + IModularFeatures::Get().RegisterModularFeature(GetModularFeatureName(), this); + EyeTracker = TSharedPtr(NewEyeTracker); + } + +#if WITH_EDITOR + EditorExtensions = new FTobiiEditorExtension(this); +#endif //WITH_EDITOR + } +#endif //TOBII_EYETRACKING_ACTIVE +} + +void FTobiiCoreModule::ShutdownModule() +{ +#if WITH_EDITOR + if (EditorExtensions != nullptr) + { + delete EditorExtensions; + } +#endif + + if (EyeTracker.IsValid()) + { + IModularFeatures::Get().UnregisterModularFeature(GetModularFeatureName(), this); + + FTobiiEyeTracker* EyetrackerPtr = (FTobiiEyeTracker*)EyeTracker.Get(); + EyetrackerPtr->Shutdown(); + EyeTracker.Reset(); + } +} + +TSharedPtr FTobiiCoreModule::CreateEyeTracker() +{ + return StaticCastSharedPtr(EyeTracker); +} + +bool FTobiiCoreModule::IsEyeTrackerConnected() const +{ + bool bSRanipalRunning = FModuleManager::Get().IsModuleLoaded("SRanipalCore") && FModuleManager::Get().GetModuleChecked("SRanipalCore").IsEyeTrackerConnected(); + return EyeTracker.IsValid() && !bSRanipalRunning; +} + +TSharedPtr FTobiiCoreModule::GetEyeTrackerInternal() +{ + return EyeTracker; +} diff --git a/Source/TobiiCore/Private/TobiiCoreModule.h b/Source/TobiiCore/Private/TobiiCoreModule.h new file mode 100644 index 0000000..7e32c65 --- /dev/null +++ b/Source/TobiiCore/Private/TobiiCoreModule.h @@ -0,0 +1,41 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "IInputDevice.h" +#include "ITobiiCore.h" +#include "TobiiEyetracker.h" + +#include "CoreMinimal.h" +#include "DisplayDebugHelpers.h" + +class AHUD; +class UCanvas; + +class FTobiiCoreModule : public ITobiiCore +{ + /************************************************************************/ + /* ITobiiCore */ + /************************************************************************/ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; + + virtual TSharedPtr CreateEyeTracker() override; + virtual bool IsEyeTrackerConnected() const override; + +protected: + virtual TSharedPtr GetEyeTrackerInternal() override; + +private: + TSharedPtr EyeTracker; + void* GICDllHandle; + +#if WITH_EDITOR + class FTobiiEditorExtension* EditorExtensions; +#endif +}; diff --git a/Source/TobiiCore/Private/TobiiEyetracker.cpp b/Source/TobiiCore/Private/TobiiEyetracker.cpp new file mode 100644 index 0000000..87789d7 --- /dev/null +++ b/Source/TobiiCore/Private/TobiiEyetracker.cpp @@ -0,0 +1,929 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#include "TobiiEyetracker.h" +#include "TobiiBlueprintLibrary.h" + +#include "DrawDebugHelpers.h" +#include "IXRTrackingSystem.h" +#include "Engine/Engine.h" +#include "Engine/Canvas.h" +#include "Engine/LocalPlayer.h" +#include "GameFramework/HUD.h" +#include "GameFramework/PlayerController.h" +#include "Slate/SceneViewport.h" +#include "Widgets/SWindow.h" +#include "Misc/App.h" + +#include "tobii_gameintegration.h" + +/************************************************************************/ +/* Configuration CVars */ +/************************************************************************/ +static TAutoConsoleVariable CVarTobiiEnableEyetracking(TEXT("tobii.EnableEyetracking"), 1, TEXT("0 - Eyetracking is disabled. 1 - Eyetracking is enabled.")); +static TAutoConsoleVariable CVarTobiiFreezingGazeData(TEXT("tobii.FreezeGazeData"), 0, TEXT("0 - Eyetracking data is not frozen. 1 - Eyetracking data is frozen. This is really useful for visualizing gaze and debugging.")); +static TAutoConsoleVariable CVarTobiiFocusTraceChannel(TEXT("tobii.FocusTraceChannel"), (int32)ECC_Visibility, TEXT("This is the trace channel that will be used in all eye tracking queries.")); +static TAutoConsoleVariable CVarTobiiFovealConeAngleDegrees(TEXT("tobii.FovealConeAngleDegrees"), 5.0f, TEXT("A larger value here will lead to the GTOM system considering a larger area around the gaze point. Refer to this link to see what values are reasonable: https://en.wikipedia.org/wiki/Fovea_centralis#/media/File:Macula.svg. Eventually, we will hopefully select good values for you so you don't have to care about this.")); +static TAutoConsoleVariable CVarTobiiMaximumTraceDistance(TEXT("tobii.MaximumTraceDistance"), 5000.0f, TEXT("This is how far in front of the player we can detect objects. This could impact game play so be careful. Shorter range can also positively impact performance.")); +static TAutoConsoleVariable CVarTobiiMaximumAcceptableStableGazeAverageAngularSpeedDegPerMicroSecs(TEXT("tobii.MaximumAcceptableStableGazeAverageAngularSpeedDegPerMicroSecs"), 0.00001f, TEXT("Raising this value will make the system more generous when determining if a gaze point is currently stable or not.")); +static TAutoConsoleVariable CVarTobiiStablePointInterpolationBias(TEXT("tobii.StablePointInterpolationBias"), 0.6f, TEXT("Gaze point stability is based on a linear interpolation filter. This scalar determines the bias given to the new point.")); + +static TAutoConsoleVariable CVarTobiiWatchingToNotWatchingInertiaSecs(TEXT("tobii.desktop.WatchingToNotWatchingInertiaSecs"), 0.6f, TEXT("To make the experience more stable we introduce inertia for transitioning from the UserPresentAndWatchingWindow eyetracking status state. This is the length of that inertia period in seconds. Please note that the eyetracker also has innate inertia, this is just a way to further control it.")); +static TAutoConsoleVariable CVarTobiiAdditionalWindowMarginCm(TEXT("tobii.desktop.AdditionalWindowMarginCm"), 1.3f, TEXT("We add an artificial margin around the game window to avoid precision and accuracy problems when the gaze point is close to the edge of the screen. The value is the size of this extra border in centimeters (same as unreal units).")); +static TAutoConsoleVariable CVarTobiiHeadPosePositionInterpolationBias(TEXT("tobii.desktop.HeadPosePositionInterpolationBias"), 0.6f, TEXT("Head pose position filtration is done using a linear interpolation filter. This scalar determines the bias given to the new position data.")); +static TAutoConsoleVariable CVarTobiiHeadPoseRotationInterpolationBias(TEXT("tobii.desktop.HeadPoseRotationInterpolationBias"), 0.1f, TEXT("Head pose rotation filtration is done using a spherical linear interpolation filter. This scalar is used as the interpolation constant.")); +static TAutoConsoleVariable CVarInfiniteScreenEnabled(TEXT("tobii.desktop.InfiniteScreenEnabled"), 1, TEXT("0 - Extended View is off. 1 - Extended View is on.")); +static TAutoConsoleVariable CVarExtendedViewMaxGazeAngleDeg(TEXT("tobii.desktop.ExtendedViewMaxGazeAngleDeg"), 30, TEXT("Extended View angles will not rotate the camera more than this amount of degrees due to the user's gaze.")); +static TAutoConsoleVariable CVarExtendedViewHeadSensitivity(TEXT("tobii.desktop.ExtendedViewHeadSensitivity"), 0.5f, TEXT("This is how sensitive the head component of extended view will be.")); +static TAutoConsoleVariable CVarExtendedViewGazeSensitivity(TEXT("tobii.desktop.ExtendedViewGazeSensitivity"), 0.5f, TEXT("This is how sensitive the gaze component of extended view will be.")); +static TAutoConsoleVariable CVarExtendedViewGazeOnlyScalar(TEXT("tobii.desktop.ExtendedViewGazeOnlyScalar"), 1.3f, TEXT("This is how we will scale the gaze extended view if only gaze is available.")); +static TAutoConsoleVariable CVarExtendedViewHeadOnlyScalar(TEXT("tobii.desktop.ExtendedViewHeadOnlyScalar"), 1.3f, TEXT("This is how we will scale the head extended view if only head is available.")); + +static TAutoConsoleVariable CVarHMDScreenDistanceToEyeCm(TEXT("tobii.xr.HMDScreenDistanceToEyeCm"), 3.0f, TEXT("Since the foveal cone on the screen depends on the distance of the eye from the screen, we need this. Since UE4 doesn't provide APIs to poll it dynamically, you have to set it manually here instead.")); +static TAutoConsoleVariable CVarTobiiApplyHMDOrientation(TEXT("tobii.xr.ApplyHMDOrientation"), 1, TEXT("Apply the HMD orientation to your gaze direction")); +static TAutoConsoleVariable CVarTobiiApplyActorRotation(TEXT("tobii.xr.ApplyActorRotation"), 1, TEXT("Apply your actor rotation to your gaze direction")); + +static TAutoConsoleVariable CVarEnableEyetrackingEmulation(TEXT("tobii.emulation.EnableEyetrackingEmulation"), 0, TEXT("0 - Don't emulate eye tracking. 1 - Emulate eye tracking.")); +static TAutoConsoleVariable CVarEyetrackingEmulationGazeSpeed(TEXT("tobii.emulation.DesktopEyetrackingEmulationGazeSpeed"), 0.4f, TEXT("When emulating gaze on desktop, this is the speed scalar used for the gaze point.")); + +static TAutoConsoleVariable CVarEnableEyetrackingDebug(TEXT("tobii.debug"), 0, TEXT("0 - Eyetracking debug visualizations are disabled. 1 - Eyetracking debug visualizations are enabled.")); +static TAutoConsoleVariable CVarEnableGazePointDebug(TEXT("tobii.debug.EnableGazePointDebug"), 1, TEXT("0 - Gaze point debug visualizations are disabled. 1 - Gaze point debug visualizations are enabled.")); +static TAutoConsoleVariable CVarEnableHeadPoseDebug(TEXT("tobii.debug.EnableHeadPoseDebug"), 0, TEXT("0 - Head pose debug visualizations are disabled. 1 - Head pose debug visualizations are enabled.")); + +#if TOBII_EYETRACKING_ACTIVE + +using namespace TobiiGameIntegration; + +FTobiiEyeTracker::FTobiiEyeTracker() + : TgiApi(nullptr) + , ActivePlayerController(nullptr) + , bIsXR(false) +{ + TgiApi = GetApi(TCHAR_TO_ANSI(FApp::GetProjectName())); + StartTime = FDateTime::UtcNow(); + ResetData(); +} + +FTobiiEyeTracker::~FTobiiEyeTracker() +{ + Shutdown(); +} + +void FTobiiEyeTracker::Shutdown() +{ + if (TgiApi != nullptr) + { + TgiApi->Shutdown(); + TgiApi = nullptr; + } + + GazeTrackerStatus = ETobiiGazeTrackerStatus::NotConnected; +} + +bool FTobiiEyeTracker::IsConnectedToEyeTracker() +{ + if (TgiApi != nullptr) + { + IStreamsProvider* StreamsProvider = TgiApi->GetStreamsProvider(); + ITrackerController* TrackerController = TgiApi->GetTrackerController(); + if (TrackerController != nullptr && StreamsProvider != nullptr) + { + return true; + } + } + + return false; +} + +void FTobiiEyeTracker::ResetData() +{ + ActivePlayerController.Reset(); + CombinedWorldGazeHitData = FHitResult(); + LeftWorldGazeHitData= FHitResult(); + RightWorldGazeHitData = FHitResult(); + LeftGazeData = FTobiiGazeData(); + RightGazeData = FTobiiGazeData(); + CombinedGazeData = FTobiiGazeData(); + GazeTrackerStatus = ETobiiGazeTrackerStatus::NotConnected; + HeadPoseData = FTobiiHeadPoseData(); + DisplayInfo = FTobiiDisplayInfo(); + + GazePointDeltaTimeMicroSecs = 0; + CurrentAverageGazeAngularSpeedDegPerMicroSecs = 0.0; +} + +bool FTobiiEyeTracker::Tick(float DeltaTime) +{ + QUICK_SCOPE_CYCLE_COUNTER(STAT_TobiiEyetracking_Tick); + + if (GEngine->EyeTrackingDevice.Get() != this) + { + Shutdown(); + return true; + } + + ////////////////////////////////////////////////////////////////////////// + // Update status + ////////////////////////////////////////////////////////////////////////// + if (!CVarTobiiEnableEyetracking.GetValueOnGameThread()) + { + if (GazeTrackerStatus != ETobiiGazeTrackerStatus::Disabled) + { + ResetData(); + GazeTrackerStatus = ETobiiGazeTrackerStatus::Disabled; + } + + return true; + } + + if (GEngine == nullptr + || GEngine->GameViewport == nullptr + || GEngine->GameViewport->GetGameViewport() == nullptr + || TgiApi == nullptr) + { + ResetData(); + return true; + } + + if (!UpdateLowLevelResources()) + { + ResetData(); + return true; + } + + //If we have no active PC, set default + if (!ActivePlayerController.IsValid()) + { + SetEyeTrackedPlayer(nullptr); + } + + if (!CVarTobiiFreezingGazeData.GetValueOnGameThread()) + { + if (bIsXR) + { + TickXR(DeltaTime); + } + else + { + TickDesktop(DeltaTime); + } + } + + UpdateWorldSpaceData(DeltaTime); + UpdateStabilityData(DeltaTime); + + if (ActivePlayerController.IsValid() + && ActivePlayerController->GetWorld() != nullptr + && CVarEnableEyetrackingDebug.GetValueOnGameThread()) + { + if (CVarEnableGazePointDebug.GetValueOnGameThread()) + { + UE_LOG(LogTemp, Warning, TEXT("Gaze Direction: (%.2f, %.2f, %.2f)"), CombinedGazeData.WorldGazeDirection.X, CombinedGazeData.WorldGazeDirection.Y, CombinedGazeData.WorldGazeDirection.Z); + const float DrawSize = FVector::Dist(CombinedGazeData.WorldGazeOrigin, CombinedWorldGazeHitData.Location) * FMath::Tan(FMath::DegreesToRadians(CombinedGazeData.WorldGazeConeAngleDegrees)); + DrawDebugSphere(ActivePlayerController->GetWorld(), CombinedWorldGazeHitData.Location, DrawSize, 16, CombinedGazeData.bIsStable ? FColor::Green : FColor::Red, false, 0.0f); + } + + if (CVarEnableHeadPoseDebug.GetValueOnGameThread()) + { + UE_LOG(LogTemp, Warning, TEXT("Head location: (%.2f, %.2f, %.2f) ~~ Head orientation: (%.2f, %.2f, %.2f)") + , HeadPoseData.HeadLocation.X, HeadPoseData.HeadLocation.Y, HeadPoseData.HeadLocation.Z + , HeadPoseData.HeadOrientation.Yaw, HeadPoseData.HeadOrientation.Pitch, HeadPoseData.HeadOrientation.Roll); + } + } + + return true; +} + +bool FTobiiEyeTracker::UpdateLowLevelResources() +{ + //Get game window and monitor handles + if (DisplayInfo.GameMonitorHandle == nullptr || PlatformNotifications.bShouldUpdateGameMonitorHandle) + { + PlatformNotifications.bShouldUpdateGameMonitorHandle = false; + + if (!GEngine->GameViewport->GetWindow().IsValid()) + { + return false; + } + SWindow* GameWindow = GEngine->GameViewport->GetWindow().Get(); + if (!GameWindow->GetNativeWindow().IsValid()) + { + return false; + } + + DisplayInfo.DpiScale = GameWindow->GetCachedGeometry().Scale; + DisplayInfo.GameWindowHandle = GameWindow->GetNativeWindow().Get()->GetOSWindowHandle(); + if (DisplayInfo.GameWindowHandle == nullptr) + { + return false; + } + + DisplayInfo.GameMonitorHandle = FTobiiPlatformSpecific::GetMonitorInformation(DisplayInfo.GameWindowHandle, DisplayInfo.MonitorWidthPx, DisplayInfo.MonitorHeightPx); + } + + //Since the viewport size can change every frame, we cannot treat this data as constant. + IStreamsProvider* StreamsProvider = TgiApi->GetStreamsProvider(); + if (StreamsProvider != nullptr) + { + GazePoint MaxGazeUNorm, MaxGazeMm; + MaxGazeUNorm.X = MaxGazeUNorm.Y = 1.0f; + StreamsProvider->ConvertGazePoint(MaxGazeUNorm, MaxGazeMm, Normalized, Mm); + + DisplayInfo.MonitorWidthCm = MaxGazeMm.X / 10.0f; + DisplayInfo.MonitorHeightCm = MaxGazeMm.Y / 10.0f; + + FVector2D ViewportSize; + GEngine->GameViewport->GetViewportSize(ViewportSize); + DisplayInfo.MainViewportWidthPx = ViewportSize.X; + DisplayInfo.MainViewportHeightPx = ViewportSize.Y; + DisplayInfo.MainViewportWidthCm = (((float)DisplayInfo.MainViewportWidthPx / (float)DisplayInfo.MonitorWidthPx) * DisplayInfo.MonitorWidthCm); + DisplayInfo.MainViewportHeightCm = (((float)DisplayInfo.MainViewportHeightPx / (float)DisplayInfo.MonitorHeightPx) * DisplayInfo.MonitorHeightCm); + } + + return true; +} + +FVector2D& FTobiiEyeTracker::TickEmulatedGazePointUNorm(float DeltaTime) +{ + static FVector2D EmulatedGazeNorm(0.5f, 0.5f); + + if (ActivePlayerController.IsValid()) + { + const float EmulatedGazeSpeed = CVarEyetrackingEmulationGazeSpeed.GetValueOnGameThread(); + if (ActivePlayerController->IsInputKeyDown(FKey("Up"))) + { + EmulatedGazeNorm.Y -= DeltaTime * EmulatedGazeSpeed; + } + else if (ActivePlayerController->IsInputKeyDown(FKey("Down"))) + { + EmulatedGazeNorm.Y += DeltaTime * EmulatedGazeSpeed; + } + + if (ActivePlayerController->IsInputKeyDown(FKey("Left"))) + { + EmulatedGazeNorm.X -= DeltaTime * EmulatedGazeSpeed; + } + else if (ActivePlayerController->IsInputKeyDown(FKey("Right"))) + { + EmulatedGazeNorm.X += DeltaTime * EmulatedGazeSpeed; + } + } + + EmulatedGazeNorm.X = FMath::Clamp(EmulatedGazeNorm.X, 0.0f, 1.0f); + EmulatedGazeNorm.Y = FMath::Clamp(EmulatedGazeNorm.Y, 0.0f, 1.0f); + + return EmulatedGazeNorm; +} + +void FTobiiEyeTracker::TickDesktop(float DeltaTime) +{ + //Pump API + ITrackerController* TrackerController = TgiApi->GetTrackerController(); + if (TrackerController != nullptr) + { + TrackerController->TrackWindow(DisplayInfo.GameWindowHandle); + } + TgiApi->Update(); + + FDateTime Now = FDateTime::UtcNow(); + IStreamsProvider* StreamsProvider = nullptr; + bool bIsEmulating = false; + if (CVarEnableEyetrackingEmulation.GetValueOnGameThread()) + { + bIsEmulating = true; + GazeTrackerStatus = ETobiiGazeTrackerStatus::UserPresent; + } + else + { + //Test for status + StreamsProvider = TgiApi->GetStreamsProvider(); + if (StreamsProvider != nullptr && TrackerController != nullptr && TrackerController->IsConnected()) + { + if (GazeTrackerStatus < ETobiiGazeTrackerStatus::UserNotPresent) + { + GazeTrackerStatus = ETobiiGazeTrackerStatus::UserNotPresent; + } + } + else + { + GazeTrackerStatus = ETobiiGazeTrackerStatus::NotConnected; + } + } + + if (GazeTrackerStatus >= ETobiiGazeTrackerStatus::UserNotPresent) + { + if (bIsEmulating) + { + RawGazePoint.TimeStamp = Now; + RawGazePoint.GazePointNormalized = TickEmulatedGazePointUNorm(DeltaTime); + HeadPoseData.HeadLocation = FVector::ZeroVector; + HeadPoseData.HeadOrientation = FRotator::ZeroRotator; + InfiniteScreenAngles = FRotator::ZeroRotator; + } + else + { + GazeTrackerStatus = StreamsProvider->IsPresent() ? ETobiiGazeTrackerStatus::UserPresent : ETobiiGazeTrackerStatus::UserNotPresent; + + //Get new gaze data + const GazePoint* GazePointsSinceLastUpdateSNorm; + int NumGazePointsSinceLastUpdate = StreamsProvider->GetGazePoints(GazePointsSinceLastUpdateSNorm); + if (NumGazePointsSinceLastUpdate > 0) + { + RawGazePoint.GazePointNormalized.Set(0.0f, 0.0f); + for (int32 GazeIdx = 0; GazeIdx < NumGazePointsSinceLastUpdate; GazeIdx++) + { + RawGazePoint.GazePointNormalized += FVector2D(GazePointsSinceLastUpdateSNorm[GazeIdx].X, GazePointsSinceLastUpdateSNorm[GazeIdx].Y); + } + + RawGazePoint.TimeStamp = Now; + RawGazePoint.GazePointNormalized /= (float)NumGazePointsSinceLastUpdate; + + //Convert to UNorm + RawGazePoint.GazePointNormalized.Set((RawGazePoint.GazePointNormalized.X + 1.0f) / 2.0f + , (-RawGazePoint.GazePointNormalized.Y + 1.0f) / 2.0f); + } + + //Get new head pose data + const HeadPose* HeadPosesSinceLastUpdate; + int NumHeadPosesSinceLastUpdate = StreamsProvider->GetHeadPoses(HeadPosesSinceLastUpdate); + if (NumHeadPosesSinceLastUpdate > 0) + { + RawHeadPose.HeadPositionCm.Set(0.0f, 0.0f, 0.0f); + float RawPitchRad = 0.0f; + float RawYawRad = 0.0f; + float RawRollRad = 0.0f; + + for (int32 HeadPoseIdx = 0; HeadPoseIdx < NumHeadPosesSinceLastUpdate; HeadPoseIdx++) + { + RawHeadPose.HeadPositionCm += FVector(HeadPosesSinceLastUpdate[HeadPoseIdx].Position.X, HeadPosesSinceLastUpdate[HeadPoseIdx].Position.Y, HeadPosesSinceLastUpdate[HeadPoseIdx].Position.Z); + RawPitchRad += HeadPosesSinceLastUpdate[HeadPoseIdx].Rotation.Pitch; + RawYawRad += HeadPosesSinceLastUpdate[HeadPoseIdx].Rotation.Yaw; + RawRollRad += HeadPosesSinceLastUpdate[HeadPoseIdx].Rotation.Roll; + } + + RawHeadPose.HeadPositionCm /= (float)NumHeadPosesSinceLastUpdate; + RawPitchRad /= (float)NumHeadPosesSinceLastUpdate; + RawYawRad /= (float)NumHeadPosesSinceLastUpdate; + RawRollRad /= (float)NumHeadPosesSinceLastUpdate; + + RawHeadPose.TimeStamp = Now; + RawHeadPose.HeadPositionCm /= 10.0f; //Unit conversion + RawHeadPose.HeadOrientation = FRotator(FMath::RadiansToDegrees(RawPitchRad), FMath::RadiansToDegrees(RawYawRad), FMath::RadiansToDegrees(RawRollRad)); + } + + //Calculate derived data + const float HeadPosePositionInterpolationBias = FMath::Clamp(CVarTobiiHeadPosePositionInterpolationBias.GetValueOnGameThread(), 0.3f, 1.0f); + const float HeadPoseRotationInterpolationBias = FMath::Clamp(CVarTobiiHeadPoseRotationInterpolationBias.GetValueOnGameThread(), 0.01f, 1.0f); + + HeadPoseData.HeadLocation = FMath::LerpStable(HeadPoseData.HeadLocation, RawHeadPose.HeadPositionCm, HeadPosePositionInterpolationBias); + HeadPoseData.HeadOrientation = FQuat::Slerp(HeadPoseData.HeadOrientation.Quaternion(), RawHeadPose.HeadOrientation.Quaternion(), HeadPoseRotationInterpolationBias).Rotator(); + + //Infinite screen + bool bExtendedViewUpdateSuccessful = false; + IFeatures* Features = TgiApi->GetFeatures(); + if (CVarInfiniteScreenEnabled.GetValueOnGameThread() && !bIsXR && Features != nullptr) + { + IExtendedView* ExtendedView = Features->GetExtendedView(); + if (ExtendedView != nullptr) + { + const float BaseHeadViewResponsiveness = FMath::Clamp(CVarExtendedViewHeadSensitivity.GetValueOnGameThread(), 0.0f, 1.0f); + const float BaseGazeViewResponsiveness = FMath::Clamp(CVarExtendedViewGazeSensitivity.GetValueOnGameThread(), 0.0f, 1.0f); + + ExtendedViewSettings Settings; + Settings.NormalizedGazeViewMinimumExtensionAngle = Settings.NormalizedGazeViewExtensionAngle = CVarExtendedViewMaxGazeAngleDeg.GetValueOnGameThread() / 360.0f; + Settings.HeadViewResponsiveness = BaseHeadViewResponsiveness; + Settings.GazeViewResponsiveness = BaseGazeViewResponsiveness; + Settings.HeadViewAutoCenter.IsEnabled = false; + + //Update default settings + ExtendedView->UpdateSettings(Settings); + + //Update specific settings + Settings.GazeViewResponsiveness = BaseGazeViewResponsiveness * CVarExtendedViewGazeOnlyScalar.GetValueOnGameThread(); + ExtendedView->UpdateGazeOnlySettings(Settings); + Settings.HeadViewResponsiveness = BaseHeadViewResponsiveness * CVarExtendedViewHeadOnlyScalar.GetValueOnGameThread(); + ExtendedView->UpdateHeadOnlySettings(Settings); + + Transformation ExtendedViewData = ExtendedView->GetTransformation(); + InfiniteScreenAngles.Roll = 0.0f; + InfiniteScreenAngles.Yaw = ExtendedViewData.Rotation.Yaw; + InfiniteScreenAngles.Pitch = ExtendedViewData.Rotation.Pitch; + bExtendedViewUpdateSuccessful = true; + } + } + if (!bExtendedViewUpdateSuccessful) + { + InfiniteScreenAngles = FRotator::ZeroRotator; + } + } + + //Since these are not time based, only run when necessary. + static FDateTime PreviousRawGazePointTime; + if (PreviousRawGazePointTime != RawGazePoint.TimeStamp) + { + GazePointDeltaTimeMicroSecs = (uint64)FMath::Max((RawGazePoint.TimeStamp - PreviousRawGazePointTime).GetTotalMicroseconds(), 0.0); + CombinedGazeData.TimeStamp = RawGazePoint.TimeStamp; + FVector2D ScreenSpaceGazePointUNorm = ConvertRawGazePointUNormToGameViewportCoordinateUNorm(GEngine->GameViewport->GetGameViewport(), RawGazePoint.GazePointNormalized); + CombinedGazeData.ScreenGazePointPx.Set(ScreenSpaceGazePointUNorm.X * DisplayInfo.MainViewportWidthPx, ScreenSpaceGazePointUNorm.Y * DisplayInfo.MainViewportHeightPx); + + PreviousRawGazePointTime = RawGazePoint.TimeStamp; + } + GazeDataTimeStampMicroSecs = (PreviousRawGazePointTime - StartTime).GetTotalMicroseconds(); + + const float AspectRatio = DisplayInfo.MainViewportWidthPx / (float)DisplayInfo.MainViewportHeightPx; + const float FovealRegionSizeDeg = FMath::Max(CVarTobiiFovealConeAngleDegrees.GetValueOnGameThread(), 0.0f); + CombinedGazeData.ScreenGazeCircleRadiiPx.Y = FEyetrackingUtils::CalculateFovealRegionHeightPx(DisplayInfo.MainViewportHeightCm, DisplayInfo.MainViewportHeightPx, HeadPoseData.HeadLocation.Z, FovealRegionSizeDeg); + CombinedGazeData.ScreenGazeCircleRadiiPx.X = CombinedGazeData.ScreenGazeCircleRadiiPx.Y * AspectRatio; + CombinedGazeData.WorldGazeConeAngleDegrees = FovealRegionSizeDeg; + CombinedGazeData.EyeOpenness = 1.0f; + } +} + +void FTobiiEyeTracker::TickXR(float DeltaTime) +{ + //Pump API + ITrackerController* TrackerController = TgiApi->GetTrackerController(); + if (TrackerController != nullptr) + { + TrackerController->TrackHMD(); + } + TgiApi->Update(); + + //Test for status + FDateTime Now = FDateTime::UtcNow(); + IStreamsProvider* StreamsProvider = nullptr; + bool bIsEmulating = false; + if (CVarEnableEyetrackingEmulation.GetValueOnGameThread()) + { + bIsEmulating = true; + GazeTrackerStatus = ETobiiGazeTrackerStatus::UserPresent; + } + else + { + StreamsProvider = TgiApi->GetStreamsProvider(); + GazeTrackerStatus = StreamsProvider != nullptr && TrackerController != nullptr && TrackerController->IsConnected() + ? ETobiiGazeTrackerStatus::UserPresent + : ETobiiGazeTrackerStatus::NotConnected; + } + + if (GazeTrackerStatus == ETobiiGazeTrackerStatus::UserPresent && ActivePlayerController.IsValid()) + { + bool bNewGazeData = false; + + if (bIsEmulating) + { + FQuat HMDOrientation; + FVector HMDPosition; + GEngine->XRSystem->GetCurrentPose(IXRTrackingSystem::HMDDeviceId, HMDOrientation, HMDPosition); + + FVector2D& GazeModifierUNorm = TickEmulatedGazePointUNorm(DeltaTime); + FVector2D GazeModifierSNorm((GazeModifierUNorm.X - 0.5f) * 2.0f, (GazeModifierUNorm.Y - 0.5f) * 2.0f); + HMDOrientation *= FQuat::MakeFromEuler(FVector(0.0f, -GazeModifierSNorm.Y * 90.0f, GazeModifierSNorm.X * 90.0f)); + LeftGazeData.WorldGazeDirection = RightGazeData.WorldGazeDirection = HMDOrientation.GetForwardVector(); + + LeftGazeData.bIsGazeDataValid = RightGazeData.bIsGazeDataValid = true; + LeftGazeData.EyeOpenness = RightGazeData.EyeOpenness = 1.0f; + bNewGazeData = true; + } + else + { + //Find direction and openness + //const HMDGaze* RawTobiiGazeData; + HMDGaze RawTobiiGazeData; + //int NumDataSinceLastUpdate = StreamsProvider->GetHMDGaze(RawTobiiGazeData); + int NumDataSinceLastUpdate = StreamsProvider->GetLatestHMDGaze(RawTobiiGazeData) ? 1 : 0; + if (NumDataSinceLastUpdate > 0) + { + const HMDGaze& LatestGazeData = RawTobiiGazeData;//[NumDataSinceLastUpdate - 1]; + LeftGazeData.bIsGazeDataValid = (LatestGazeData.Validity & HMDValidityFlags::LeftEyeIsValid) == HMDValidityFlags::LeftEyeIsValid; + RightGazeData.bIsGazeDataValid = (LatestGazeData.Validity & HMDValidityFlags::RightEyeIsValid) == HMDValidityFlags::RightEyeIsValid; + + LeftGazeData.WorldGazeDirection.Set(0.0f, 0.0f, 0.0f); + RightGazeData.WorldGazeDirection.Set(0.0f, 0.0f, 0.0f); + for (int32 GazeIdx = 0; GazeIdx < NumDataSinceLastUpdate; GazeIdx++) + { + const HMDGaze& GazeData = RawTobiiGazeData;// [GazeIdx]; + LeftGazeData.WorldGazeDirection += FVector(GazeData.LeftEyeInfo.GazeDirection.Z, -GazeData.LeftEyeInfo.GazeDirection.X, GazeData.LeftEyeInfo.GazeDirection.Y); + RightGazeData.WorldGazeDirection += FVector(GazeData.RightEyeInfo.GazeDirection.Z, -GazeData.RightEyeInfo.GazeDirection.X, GazeData.RightEyeInfo.GazeDirection.Y); + } + LeftGazeData.bIsGazeDataValid = LeftGazeData.bIsGazeDataValid && LeftGazeData.WorldGazeDirection.Normalize(); + RightGazeData.bIsGazeDataValid = RightGazeData.bIsGazeDataValid && RightGazeData.WorldGazeDirection.Normalize(); + + LeftGazeData.EyeOpenness = LatestGazeData.LeftEyeInfo.EyeOpenness; + RightGazeData.EyeOpenness = LatestGazeData.RightEyeInfo.EyeOpenness; + GazePointDeltaTimeMicroSecs = (int64)LatestGazeData.Timestamp - GazeDataTimeStampMicroSecs; + GazeDataTimeStampMicroSecs = (int64)LatestGazeData.Timestamp; + bNewGazeData = true; + } + + //Optionally correct orientation depending on project settings + { + if (CVarTobiiApplyHMDOrientation.GetValueOnGameThread()) + { + FQuat HMDOrientation; + FVector HMDPosition; + GEngine->XRSystem->GetCurrentPose(IXRTrackingSystem::HMDDeviceId, HMDOrientation, HMDPosition); + LeftGazeData.WorldGazeDirection = HMDOrientation.RotateVector(LeftGazeData.WorldGazeDirection); + RightGazeData.WorldGazeDirection = HMDOrientation.RotateVector(RightGazeData.WorldGazeDirection); + } + } + } + + if (CVarTobiiApplyActorRotation.GetValueOnGameThread() + && ActivePlayerController.IsValid() + && ActivePlayerController->GetPawn() != nullptr) + { + FQuat ActorRotation = ActivePlayerController->GetPawn()->GetActorRotation().Quaternion(); + + LeftGazeData.WorldGazeDirection = ActorRotation.RotateVector(LeftGazeData.WorldGazeDirection); + RightGazeData.WorldGazeDirection = ActorRotation.RotateVector(RightGazeData.WorldGazeDirection); + } + + //Calculate combined data + CombinedGazeData.bIsGazeDataValid = LeftGazeData.bIsGazeDataValid || RightGazeData.bIsGazeDataValid; + if (LeftGazeData.bIsGazeDataValid && RightGazeData.bIsGazeDataValid) + { + CombinedGazeData.WorldGazeDirection = (LeftGazeData.WorldGazeDirection + RightGazeData.WorldGazeDirection); + CombinedGazeData.bIsGazeDataValid = CombinedGazeData.bIsGazeDataValid && CombinedGazeData.WorldGazeDirection.Normalize(); + } + else if (LeftGazeData.bIsGazeDataValid) + { + CombinedGazeData.WorldGazeDirection = LeftGazeData.WorldGazeDirection; + } + else if (RightGazeData.bIsGazeDataValid) + { + CombinedGazeData.WorldGazeDirection = RightGazeData.WorldGazeDirection; + } + + if (bNewGazeData) + { + CombinedGazeData.TimeStamp = Now; + } + + const float AspectRatio = DisplayInfo.MainViewportWidthPx / (float)DisplayInfo.MainViewportHeightPx; + const float FovealRegionSizeDeg = FMath::Max(CVarTobiiFovealConeAngleDegrees.GetValueOnGameThread(), 0.0f); + CombinedGazeData.ScreenGazeCircleRadiiPx.Y = FEyetrackingUtils::CalculateFovealRegionHeightPx(DisplayInfo.MainViewportHeightCm, DisplayInfo.MainViewportHeightPx, CVarHMDScreenDistanceToEyeCm.GetValueOnGameThread(), FovealRegionSizeDeg); + CombinedGazeData.ScreenGazeCircleRadiiPx.X = CombinedGazeData.ScreenGazeCircleRadiiPx.Y * AspectRatio; + LeftGazeData.WorldGazeConeAngleDegrees = RightGazeData.WorldGazeConeAngleDegrees = CombinedGazeData.WorldGazeConeAngleDegrees = FovealRegionSizeDeg; + LeftGazeData.ScreenGazeCircleRadiiPx = RightGazeData.ScreenGazeCircleRadiiPx = CombinedGazeData.ScreenGazeCircleRadiiPx; + } +} + + +void FTobiiEyeTracker::UpdateWorldSpaceData(float DeltaTime) +{ + if (ActivePlayerController.IsValid() && ActivePlayerController->GetWorld() != nullptr) + { + if (bIsXR) + { + ULocalPlayer* const LocalPlayer = ActivePlayerController->GetLocalPlayer(); + if (LocalPlayer != nullptr) + { + //Find origin and screen point. This also calculates screen data for VR as it is expressed in world space data. + //@TODO: We need to make it obvious that if you want to line trace here, you must move the origin forward to the near plane. + const float ProjectionDistanceCm = 1000.0f; + FSceneViewProjectionData LeftProjectionData, RightProjectionData; + if (LocalPlayer->GetProjectionData(LocalPlayer->ViewportClient->Viewport, eSSP_LEFT_EYE, /*out*/ LeftProjectionData)) + { + LeftGazeData.WorldGazeOrigin = LeftProjectionData.ViewOrigin; + const FVector ProjectionPoint = LeftGazeData.WorldGazeOrigin + (LeftGazeData.WorldGazeDirection * ProjectionDistanceCm); + ActivePlayerController->ProjectWorldLocationToScreen(ProjectionPoint, LeftGazeData.ScreenGazePointPx); + } + else if (ActivePlayerController->PlayerCameraManager != nullptr) + { + LeftGazeData.WorldGazeOrigin = ActivePlayerController->PlayerCameraManager->GetCameraLocation(); + } + if (LocalPlayer->GetProjectionData(LocalPlayer->ViewportClient->Viewport, eSSP_RIGHT_EYE, /*out*/ RightProjectionData)) + { + RightGazeData.WorldGazeOrigin = RightProjectionData.ViewOrigin; + const FVector ProjectionPoint = RightGazeData.WorldGazeOrigin + (RightGazeData.WorldGazeDirection * ProjectionDistanceCm); + ActivePlayerController->ProjectWorldLocationToScreen(ProjectionPoint, RightGazeData.ScreenGazePointPx); + } + else if (ActivePlayerController->PlayerCameraManager != nullptr) + { + RightGazeData.WorldGazeOrigin = ActivePlayerController->PlayerCameraManager->GetCameraLocation(); + } + + CombinedGazeData.WorldGazeOrigin = (LeftGazeData.WorldGazeOrigin + RightGazeData.WorldGazeOrigin) / 2.0f; + CombinedGazeData.ScreenGazePointPx = (LeftGazeData.ScreenGazePointPx + RightGazeData.ScreenGazePointPx) / 2.0f; + } + } + else + { + CombinedGazeData.bIsGazeDataValid = ActivePlayerController->DeprojectScreenPositionToWorld(CombinedGazeData.ScreenGazePointPx.X, CombinedGazeData.ScreenGazePointPx.Y, CombinedGazeData.WorldGazeOrigin, CombinedGazeData.WorldGazeDirection); + CombinedGazeData.WorldGazeOrigin = ActivePlayerController->PlayerCameraManager->GetCameraLocation(); + LeftGazeData = RightGazeData = CombinedGazeData; + } + + //Verify direction validity + if (!CombinedGazeData.WorldGazeDirection.IsNormalized()) + { + CombinedGazeData.bIsGazeDataValid = false; + } + if (!LeftGazeData.WorldGazeDirection.IsNormalized()) + { + LeftGazeData.bIsGazeDataValid = false; + } + if (!RightGazeData.WorldGazeDirection.IsNormalized()) + { + RightGazeData.bIsGazeDataValid = false; + } + + FCollisionQueryParams CollisionQueryParams; + CollisionQueryParams.AddIgnoredActor(ActivePlayerController.Get()); + CollisionQueryParams.AddIgnoredActor(ActivePlayerController->GetPawn()); + const float MaximumTraceDistance = FMath::Max(CVarTobiiMaximumTraceDistance.GetValueOnGameThread(), 0.0f); + const FVector CombinedGazeFarLocation = CombinedGazeData.WorldGazeOrigin + (CombinedGazeData.WorldGazeDirection * MaximumTraceDistance); + if (!CombinedGazeData.bIsGazeDataValid || + !ActivePlayerController->GetWorld()->LineTraceSingleByChannel(CombinedWorldGazeHitData, CombinedGazeData.WorldGazeOrigin + , CombinedGazeFarLocation, (ECollisionChannel)CVarTobiiFocusTraceChannel.GetValueOnGameThread(), CollisionQueryParams)) + { + CombinedWorldGazeHitData.Actor = nullptr; + CombinedWorldGazeHitData.Component = nullptr; + CombinedWorldGazeHitData.Distance = MaximumTraceDistance; + CombinedWorldGazeHitData.Location = CombinedGazeFarLocation; + CombinedWorldGazeHitData.bBlockingHit = false; + } + + const FVector LeftGazeFarLocation = LeftGazeData.WorldGazeOrigin + (LeftGazeData.WorldGazeDirection * MaximumTraceDistance); + if (!LeftGazeData.bIsGazeDataValid || + !ActivePlayerController->GetWorld()->LineTraceSingleByChannel(LeftWorldGazeHitData, LeftGazeData.WorldGazeOrigin + , LeftGazeFarLocation, (ECollisionChannel)CVarTobiiFocusTraceChannel.GetValueOnGameThread(), CollisionQueryParams)) + { + LeftWorldGazeHitData.Actor = nullptr; + LeftWorldGazeHitData.Component = nullptr; + LeftWorldGazeHitData.Distance = MaximumTraceDistance; + LeftWorldGazeHitData.Location = CombinedGazeFarLocation; + LeftWorldGazeHitData.bBlockingHit = false; + } + + const FVector RightGazeFarLocation = RightGazeData.WorldGazeOrigin + (RightGazeData.WorldGazeDirection * MaximumTraceDistance); + if (!RightGazeData.bIsGazeDataValid || + !ActivePlayerController->GetWorld()->LineTraceSingleByChannel(RightWorldGazeHitData, RightGazeData.WorldGazeOrigin + , RightGazeFarLocation, (ECollisionChannel)CVarTobiiFocusTraceChannel.GetValueOnGameThread(), CollisionQueryParams)) + { + RightWorldGazeHitData.Actor = nullptr; + RightWorldGazeHitData.Component = nullptr; + RightWorldGazeHitData.Distance = MaximumTraceDistance; + RightWorldGazeHitData.Location = CombinedGazeFarLocation; + RightWorldGazeHitData.bBlockingHit = false; + } + } +} + +void FTobiiEyeTracker::UpdateStabilityData(float DeltaTime) +{ + if (CVarEnableEyetrackingEmulation.GetValueOnGameThread() && bIsXR) + { + CombinedGazeData.bIsStable = true; + } + else + { + //We will calculate the average gaze point velocity using a very simple moving cumulative average. + //If the average velocity of these points is under some predetermined threshold, we will consider it stable. + if (GazePointDeltaTimeMicroSecs > 0) + { + const float GazeAngleDiffDeg = FMath::RadiansToDegrees(FMath::Acos(FVector::DotProduct(CombinedGazeData.WorldGazeDirection, PrevCombinedGazeDirection))); + const double AngularSpeedDegPerMicroSecs = GazeAngleDiffDeg / (double)GazePointDeltaTimeMicroSecs; + const float StablePointInterpolationBias = FMath::Clamp(CVarTobiiStablePointInterpolationBias.GetValueOnGameThread(), 0.3f, 1.0f); + const float NewAvgSpeed = FMath::LerpStable(CurrentAverageGazeAngularSpeedDegPerMicroSecs, AngularSpeedDegPerMicroSecs, StablePointInterpolationBias); + CurrentAverageGazeAngularSpeedDegPerMicroSecs = FMath::IsFinite(NewAvgSpeed) ? NewAvgSpeed : 0.0f; //Protect from overflow + + PrevCombinedGazeDirection = CombinedGazeData.WorldGazeDirection; + + const float MaxAcceptableStablePointAverageSpeed = FMath::Max(CVarTobiiMaximumAcceptableStableGazeAverageAngularSpeedDegPerMicroSecs.GetValueOnGameThread(), 0.0f); + CombinedGazeData.bIsStable = CurrentAverageGazeAngularSpeedDegPerMicroSecs <= MaxAcceptableStablePointAverageSpeed; + } + } + + LeftGazeData.bIsStable = RightGazeData.bIsStable = CombinedGazeData.bIsStable; +} + +void FTobiiEyeTracker::SetEyeTrackedPlayer(APlayerController* PlayerController) +{ + if (PlayerController != nullptr) + { + ActivePlayerController = PlayerController; + } + else if (GEngine != nullptr && GEngine->GameViewport != nullptr) + { + ActivePlayerController = GEngine->GetFirstLocalPlayerController(GEngine->GameViewport->GetWorld()); + } + + bIsXR = UTobiiBlueprintLibrary::IsXRPlayerController(ActivePlayerController.Get()); +} + +/************************************************************************/ +/* IGazeTracker */ +/************************************************************************/ +const FTobiiGazeData& FTobiiEyeTracker::GetCombinedGazeData() const +{ + return CombinedGazeData; +} + +const FTobiiGazeData& FTobiiEyeTracker::GetLeftGazeData() const +{ + return LeftGazeData; +} + +const FTobiiGazeData& FTobiiEyeTracker::GetRightGazeData() const +{ + return RightGazeData; +} + +ETobiiGazeTrackerStatus FTobiiEyeTracker::GetGazeTrackerStatus() const +{ + return GazeTrackerStatus; +} + +bool FTobiiEyeTracker::GetGazeTrackerCapability(ETobiiGazeTrackerCapability Capability) const +{ + //Desktop trackers only support CombinedGazeData + return bIsXR || Capability == ETobiiGazeTrackerCapability::CombinedGazeData; +} + +/************************************************************************/ +/* IHeadTracker */ +/************************************************************************/ +const FTobiiHeadPoseData& FTobiiEyeTracker::GetHeadPoseData() const +{ + return HeadPoseData; +} + +/************************************************************************/ +/* ITobiiEyetracker */ +/************************************************************************/ +const FHitResult& FTobiiEyeTracker::GetCombinedWorldGazeHitData() const +{ + return CombinedWorldGazeHitData; +} + +const FHitResult& FTobiiEyeTracker::GetLeftWorldGazeHitData() const +{ + return LeftWorldGazeHitData; +} + +const FHitResult& FTobiiEyeTracker::GetRightWorldGazeHitData() const +{ + return RightWorldGazeHitData; +} + +const FTobiiDisplayInfo& FTobiiEyeTracker::GetDisplayInformation() const +{ + return DisplayInfo; +} + +const FTobiiDesktopTrackBox& FTobiiEyeTracker::GetDesktopTrackBox() const +{ + return DesktopTrackBox; +} + +const FRotator& FTobiiEyeTracker::GetInfiniteScreenAngles() const +{ + return InfiniteScreenAngles; +} + +/************************************************************************/ +/* IEyetracker */ +/************************************************************************/ +bool FTobiiEyeTracker::GetEyeTrackerGazeData(FEyeTrackerGazeData& OutGazeData) const +{ + OutGazeData.ConfidenceValue = CombinedGazeData.bIsGazeDataValid ? 1.0f : 0.0f; + OutGazeData.FixationPoint = FVector::ZeroVector; //This data is not useful for a game designer as it is not reliable enough, so we don't supply it. + OutGazeData.GazeOrigin = CombinedGazeData.WorldGazeOrigin; + OutGazeData.GazeDirection = CombinedGazeData.WorldGazeDirection; + + return true; +} + +bool FTobiiEyeTracker::GetEyeTrackerStereoGazeData(FEyeTrackerStereoGazeData& OutGazeData) const +{ + OutGazeData.ConfidenceValue = CombinedGazeData.bIsGazeDataValid ? 1.0f : 0.0f; + OutGazeData.FixationPoint = FVector::ZeroVector; //This data is not useful for a game designer as it is not reliable enough, so we don't supply it. + OutGazeData.LeftEyeOrigin = LeftGazeData.WorldGazeOrigin; + OutGazeData.LeftEyeDirection = LeftGazeData.WorldGazeDirection; + OutGazeData.RightEyeOrigin = RightGazeData.WorldGazeOrigin; + OutGazeData.RightEyeOrigin = RightGazeData.WorldGazeDirection; + + return true; +} + +EEyeTrackerStatus FTobiiEyeTracker::GetEyeTrackerStatus() const +{ + switch (GazeTrackerStatus) + { + case ETobiiGazeTrackerStatus::NotConnected: return EEyeTrackerStatus::NotConnected; + case ETobiiGazeTrackerStatus::NotConfigured: return EEyeTrackerStatus::NotConnected; + case ETobiiGazeTrackerStatus::Disabled: return EEyeTrackerStatus::NotConnected; + case ETobiiGazeTrackerStatus::UserNotPresent: return EEyeTrackerStatus::NotTracking; + case ETobiiGazeTrackerStatus::UserPresent: return EEyeTrackerStatus::Tracking; + default: return EEyeTrackerStatus::NotConnected; + } +} + +FVector2D FTobiiEyeTracker::ConvertRawGazePointUNormToGameViewportCoordinateUNorm(FSceneViewport* GameViewport, const FVector2D& InNormalizedPoint) +{ +#if WITH_EDITOR + //We need this to make sure we get a correct gaze point if the editor has tool windows attached to the window itself. + FIntPoint VirtualDesktopPixelCoord; + if (FTobiiPlatformSpecific::ConvertGazeCoordinateToVirtualDesktopPixel(DisplayInfo.GameWindowHandle, InNormalizedPoint, VirtualDesktopPixelCoord)) + { + FVector2D ViewportPixelCoordUNorm; + if (UTobiiBlueprintLibrary::VirtualDesktopPixelToViewportCoordinateUNorm(VirtualDesktopPixelCoord, ViewportPixelCoordUNorm)) + { + return ViewportPixelCoordUNorm; + } + } +#endif + + return InNormalizedPoint; +} + +// void FTobiiEyeTracker::DrawDebug(AHUD* HUD, UCanvas* Canvas, const FDebugDisplayInfo& DebugDisplayInfo, float& YL, float& YPos) +// { +// if (bIsXR) +// { +// return; +// } +// +// UWorld* World = Canvas->GetWorld(); +// UFont* RenderFont = GEngine->GetSmallFont(); +// EGazeTrackerStatus Status = GetGazeTrackerStatus(); +// +// //First draw our status string +// FString StatusText; +// switch (Status) +// { +// case EGazeTrackerStatus::Disabled: +// StatusText = "Eyetracking disabled"; +// Canvas->DrawColor = FColor::Magenta; +// break; +// case EGazeTrackerStatus::UserNotPresent: +// StatusText = "User not present"; +// Canvas->DrawColor = FColor::Purple; +// break; +// case EGazeTrackerStatus::UserPresent: +// StatusText = "User is present"; +// Canvas->DrawColor = FColor::Blue; +// break; +// default: +// StatusText = "Eyetracking not available"; +// Canvas->DrawColor = FColor::Red; +// break; +// } +// +// TArray Args; +// Args.Emplace(StatusText); +// +// //Can't use SizeX/Y since it wont work on other DPI settings +// float CanvasDrawWidth = Canvas->ClipX - Canvas->OrgX; +// float CanvasDrawHeight = Canvas->ClipY - Canvas->OrgY; +// +// Canvas->DrawText(RenderFont, FString::Format(TEXT("Status: {0}"), Args), 0.5f * CanvasDrawWidth - 100, 0.05f * CanvasDrawHeight); +// +// if (CVarEnableHeadPoseDebug.GetValueOnGameThread()) +// { +// const float PoseDebugScale = 1.0f; +// +// FVector WorldPosition, WorldDirection; +// HUD->Deproject(CanvasDrawWidth * 0.5f, CanvasDrawHeight * 0.8f, WorldPosition, WorldDirection); +// +// //First we figure our origin data +// const FVector PoseDebugLocation = WorldPosition + WorldDirection * 200.0f; +// const FQuat PoseDebugRotation = HUD->GetOwner()->GetActorRotation().Quaternion(); +// +// FCanvasLineItem LineItem; +// const FHeadPoseData& HeadPose = GetHeadPoseData(); +// const float BasisScale = PoseDebugScale * 7.0f; +// LineItem.LineThickness = 3.0f; +// +// FQuat OriginRotation = PoseDebugRotation * HeadPose.HeadOrientation.Quaternion(); +// FVector ForwardBasis = OriginRotation.RotateVector(FVector::ForwardVector) * BasisScale; +// FVector RightBasis = OriginRotation.RotateVector(FVector::RightVector) * BasisScale * 0.5f; +// FVector UpBasis = OriginRotation.RotateVector(FVector::UpVector) * BasisScale * 0.5f; +// +// FVector OriginWorldPosition = PoseDebugLocation;// + PoseDebugRotation.RotateVector((HeadPose.HeadPositionCm - TrackBoxCenter) * PoseDebugScale); +// FVector ProjectedOrigin = HUD->Project(OriginWorldPosition); +// FVector ProjectedForward = HUD->Project(OriginWorldPosition + ForwardBasis); +// FVector ProjectedRight = HUD->Project(OriginWorldPosition + RightBasis); +// FVector ProjectedUp = HUD->Project(OriginWorldPosition + UpBasis); +// +// LineItem.SetColor(FLinearColor::Red); +// LineItem.Origin = ProjectedOrigin; +// LineItem.EndPos = ProjectedForward; +// Canvas->DrawItem(LineItem); +// LineItem.SetColor(FLinearColor::Green); +// LineItem.Origin = ProjectedOrigin; +// LineItem.EndPos = ProjectedRight; +// Canvas->DrawItem(LineItem); +// LineItem.SetColor(FLinearColor::Blue); +// LineItem.Origin = ProjectedOrigin; +// LineItem.EndPos = ProjectedUp; +// Canvas->DrawItem(LineItem); +// } +// } + +#endif //TOBII_EYETRACKING_ACTIVE diff --git a/Source/TobiiCore/Private/TobiiEyetracker.h b/Source/TobiiCore/Private/TobiiEyetracker.h new file mode 100644 index 0000000..0be05e0 --- /dev/null +++ b/Source/TobiiCore/Private/TobiiEyetracker.h @@ -0,0 +1,104 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#if TOBII_EYETRACKING_ACTIVE + +#include "ITobiiEyeTracker.h" +#include "TobiiPlatformSpecific.h" +#include "TobiiInternalTypes.h" +#include "tobii_gameintegration.h" + +#include "CoreMinimal.h" +#include "Containers/Ticker.h" +#include "GameFramework/PlayerController.h" +#include "Slate/SceneViewport.h" + +/* + * FTickerObjectBase here is ticked almost last in a frame. + * The order seems to go 'IInputDevice', then shortly after that 'Actor/Components' and very late after that the 'TickerCore' and thereby also FTickerObjectBase. + * No matter if we choose IInputDevice or FTickerObjectBase we will be working with complete physics data from the last frame. + * Of course we would rather be working with current frame physics data, but doing that would make our data more TickGroup dependent. + * Maybe that's fine, but seeing as our traces are not really precise anyways, I think the simplicity of design of using FTickerObjectBase wins out. + * + * The big alternative would be to move all GTOM related objects to an actor that belongs to the PostPhysics tick group together with the focus managers and introduce a tick dependency. + * See FEngineLoop::Tick() for more detailed info. + */ +class FTobiiEyeTracker : public ITobiiEyeTracker, public FTickerObjectBase +{ +public: + FTobiiEyeTracker(); + virtual ~FTobiiEyeTracker(); + virtual bool Tick(float DeltaTime) override; + void Shutdown(); + bool IsConnectedToEyeTracker(); + + /************************************************************************/ + /* ITobiiEyetracker */ + /************************************************************************/ +public: + virtual const FTobiiGazeData& GetCombinedGazeData() const override; + virtual const FTobiiGazeData& GetLeftGazeData() const override; + virtual const FTobiiGazeData& GetRightGazeData() const override; + virtual ETobiiGazeTrackerStatus GetGazeTrackerStatus() const override; + virtual bool GetGazeTrackerCapability(ETobiiGazeTrackerCapability Capability) const override; + + virtual const FHitResult& GetCombinedWorldGazeHitData() const override; + virtual const FHitResult& GetLeftWorldGazeHitData() const override; + virtual const FHitResult& GetRightWorldGazeHitData() const override; + virtual const FTobiiDisplayInfo& GetDisplayInformation() const override; + virtual const FTobiiHeadPoseData& GetHeadPoseData() const override; + virtual const FTobiiDesktopTrackBox& GetDesktopTrackBox() const override; + virtual const FRotator& GetInfiniteScreenAngles() const override; + +public: + virtual void SetEyeTrackedPlayer(APlayerController* PlayerController) override; + virtual bool IsStereoGazeDataAvailable() const override { return bIsXR; } + virtual bool GetEyeTrackerGazeData(FEyeTrackerGazeData& OutGazeData) const override; + virtual bool GetEyeTrackerStereoGazeData(FEyeTrackerStereoGazeData& OutGazeData) const override; + virtual EEyeTrackerStatus GetEyeTrackerStatus() const override; + +private: + TobiiGameIntegration::ITobiiGameIntegrationApi* TgiApi; + FTobiiPlatformNotifications PlatformNotifications; + TWeakObjectPtr ActivePlayerController; + + FHitResult CombinedWorldGazeHitData; + FHitResult LeftWorldGazeHitData; + FHitResult RightWorldGazeHitData; + FTobiiGazeData LeftGazeData; + FTobiiGazeData RightGazeData; + FTobiiGazeData CombinedGazeData; + ETobiiGazeTrackerStatus GazeTrackerStatus; + FTobiiHeadPoseData HeadPoseData; + FTobiiDisplayInfo DisplayInfo; + FRotator InfiniteScreenAngles; + FTobiiDesktopTrackBox DesktopTrackBox; + + FTobiiRawGazePoint RawGazePoint; + FTobiiRawHeadPose RawHeadPose; + FVector PrevCombinedGazeDirection; + + bool bIsXR; + int64 GazeDataTimeStampMicroSecs; + uint64 GazePointDeltaTimeMicroSecs; + double CurrentAverageGazeAngularSpeedDegPerMicroSecs; + FDateTime StartTime; + + void ResetData(); + bool UpdateLowLevelResources(); + void TickDesktop(float DeltaTime); + void TickXR(float DeltaTime); + void UpdateWorldSpaceData(float DeltaTime); + void UpdateStabilityData(float DeltaTime); + + FVector2D ConvertRawGazePointUNormToGameViewportCoordinateUNorm(FSceneViewport* GameViewport, const FVector2D& InNormalizedPoint); + + FVector2D& TickEmulatedGazePointUNorm(float DeltaTime); +}; + +#endif //TOBII_EYETRACKING_ACTIVE diff --git a/Source/TobiiCore/Private/TobiiInternalTypes.h b/Source/TobiiCore/Private/TobiiInternalTypes.h new file mode 100644 index 0000000..e1a7334 --- /dev/null +++ b/Source/TobiiCore/Private/TobiiInternalTypes.h @@ -0,0 +1,76 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "TobiiTypes.h" + +#include "CoreMinimal.h" + +class FEyetrackingUtils +{ +public: + static float CalculateFovealRegionHeightPx(const float MainViewportHeightCm, const float MainViewportHeightPx, const float HeadDistanceCm, const float FovealRegionAngleDeg) + { + if (MainViewportHeightCm > FLT_EPSILON && MainViewportHeightPx > FLT_EPSILON && HeadDistanceCm > FLT_EPSILON) + { + const float FovealRegionSizeCm = HeadDistanceCm * FMath::Tan(FMath::DegreesToRadians(FovealRegionAngleDeg)); + return FovealRegionSizeCm / MainViewportHeightCm * MainViewportHeightPx; + } + + return 100.0f; + } +}; + +struct FTobiiRawGazePoint +{ +public: + FVector2D GazePointNormalized; + FDateTime TimeStamp; + + FTobiiRawGazePoint() + : GazePointNormalized(0.0f, 0.0f) + , TimeStamp() + { } +}; + +struct FTobiiRawHeadPose +{ +public: + FVector HeadPositionCm; + FRotator HeadOrientation; + FDateTime TimeStamp; + +public: + FTobiiRawHeadPose() + : HeadPositionCm(0.0f, 0.0f, 0.0f) + , HeadOrientation(0.0f, 0.0f, 0.0f) + , TimeStamp() + {} +}; + +struct FTobiiRawXREyetrackingData +{ +public: + FVector RawLeftEyeDirection; + FVector RawRightEyeDirection; + FDateTime EngineTimeStamp; + int64 EyetrackerTimeStamp; + bool bLeftEyeValid; + bool bRightEyeValid; + + FTobiiRawXREyetrackingData() + : RawLeftEyeDirection() + , RawRightEyeDirection() + , EngineTimeStamp() + , EyetrackerTimeStamp(-1) + , bLeftEyeValid(false) + , bRightEyeValid(false) + { + } +}; + +DEFINE_LOG_CATEGORY_STATIC(LogTobiiEyetracking, All, All); diff --git a/Source/TobiiCore/Private/TobiiPlatformSpecific.cpp b/Source/TobiiCore/Private/TobiiPlatformSpecific.cpp new file mode 100644 index 0000000..3618645 --- /dev/null +++ b/Source/TobiiCore/Private/TobiiPlatformSpecific.cpp @@ -0,0 +1,135 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#include "TobiiPlatformSpecific.h" + +#include "Framework/Application/SlateApplication.h" + +#if PLATFORM_WINDOWS +#include "Windows/AllowWindowsPlatformTypes.h" +#include "dbt.h" + +struct MonitorHandleContext +{ + FString DeviceName; + HMONITOR hMonitorHandle; +}; + +BOOL CALLBACK EnumDisplayMonitorsProc(HMONITOR hMonitorHandle, HDC, LPRECT, LPARAM Data) +{ + MonitorHandleContext* Context = (MonitorHandleContext*)Data; + MONITORINFOEXA MonitorInfo; + MonitorInfo.cbSize = sizeof(MonitorInfo); + int res = GetMonitorInfoA(hMonitorHandle, &MonitorInfo); + if (res && _stricmp(MonitorInfo.szDevice, TCHAR_TO_ANSI(*Context->DeviceName)) == 0) + { + Context->hMonitorHandle = hMonitorHandle; + return FALSE; + } + return TRUE; +} + +void* FTobiiPlatformSpecific::GetMonitorInformation(const void* GameWindowHandle, int& MonitorWidthPx, int& MonitorHeightPx) +{ + HWND hGameWindowHandle = (HWND)GameWindowHandle; + HMONITOR hGameMonitor = MonitorFromWindow(hGameWindowHandle, MONITOR_DEFAULTTONEAREST); + MONITORINFO MonitorInfo; + MonitorInfo.cbSize = sizeof(MONITORINFO); + if (GetMonitorInfo(hGameMonitor, &MonitorInfo)) + { + MonitorWidthPx = MonitorInfo.rcMonitor.right - MonitorInfo.rcMonitor.left; + MonitorHeightPx = MonitorInfo.rcMonitor.bottom - MonitorInfo.rcMonitor.top; + return (void*)hGameMonitor; + } + + MonitorWidthPx = -1; + MonitorHeightPx = -1; + return nullptr; +} + +void* FTobiiPlatformSpecific::MonitorHandleFromDeviceName(FString DeviceName) +{ + MonitorHandleContext Context; + Context.DeviceName = DeviceName; + Context.hMonitorHandle = nullptr; + EnumDisplayMonitors(nullptr, nullptr, EnumDisplayMonitorsProc, (LPARAM)&Context); + return (void*)Context.hMonitorHandle; +} + +bool FTobiiPlatformSpecific::ConvertGazeCoordinateToVirtualDesktopPixel(void* GameWindowHandle, const FVector2D& ClientCoordsUNorm, FIntPoint& OutVirtualDesktopPixel) +{ + //Now we can try to convert our point + RECT ClientRect; + if (GetClientRect((HWND)GameWindowHandle, &ClientRect)) + { + POINT ClientPoint; + ClientPoint.x = (int32)(ClientCoordsUNorm.X * (ClientRect.right - ClientRect.left)); + ClientPoint.y = (int32)(ClientCoordsUNorm.Y * (ClientRect.bottom - ClientRect.top)); + if (ClientToScreen((HWND)GameWindowHandle, &ClientPoint)) + { + OutVirtualDesktopPixel.X = ClientPoint.x; + OutVirtualDesktopPixel.Y = ClientPoint.y; + return true; + } + } + + return false; +} + +FTobiiPlatformNotifications::FTobiiPlatformNotifications() + : bShouldForceEyetrackerReconnect(true) + , bShouldUpdateGameMonitorHandle(true) +{ + if (FSlateApplication::IsInitialized()) + { + const TSharedPtr Application = FSlateApplication::Get().GetPlatformApplication(); + if (Application.IsValid()) + { + FWindowsApplication* WindowsApplication = (FWindowsApplication*)Application.Get(); + WindowsApplication->AddMessageHandler(*this); + } + } +} + +FTobiiPlatformNotifications::~FTobiiPlatformNotifications() +{ + if (FSlateApplication::IsInitialized()) + { + const TSharedPtr Application = FSlateApplication::Get().GetPlatformApplication(); + if (Application.IsValid()) + { + FWindowsApplication* WindowsApplication = (FWindowsApplication*)Application.Get(); + WindowsApplication->RemoveMessageHandler(*this); + } + } +} + +bool FTobiiPlatformNotifications::ProcessMessage(HWND hwnd, uint32 msg, WPARAM wParam, LPARAM lParam, int32& OutResult) +{ + switch (msg) + { + case WM_DEVICECHANGE: + { + if (wParam == DBT_DEVICEARRIVAL) + { + //This is narrow enough for us. We just want to short-circuit the reconnection timer if someone plugged in an eyetracker so they don't have to wait the full reconnection time. + bShouldForceEyetrackerReconnect = true; + } + break; + } + + case WM_DISPLAYCHANGE: + { + bShouldUpdateGameMonitorHandle = true; + break; + } + } + + return false; +} + +#include "Runtime/Core/Public/Windows/HideWindowsPlatformTypes.h" +#endif diff --git a/Source/TobiiCore/Private/TobiiPlatformSpecific.h b/Source/TobiiCore/Private/TobiiPlatformSpecific.h new file mode 100644 index 0000000..d4500e1 --- /dev/null +++ b/Source/TobiiCore/Private/TobiiPlatformSpecific.h @@ -0,0 +1,39 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "CoreMinimal.h" + +class FTobiiPlatformSpecific +{ +public: + static void* GetMonitorInformation(const void* GameWindowHandle, int& MonitorWidthPx, int& MonitorHeightPx); + static void* MonitorHandleFromDeviceName(FString DeviceName); + + static bool ConvertGazeCoordinateToVirtualDesktopPixel(void* GameWindowHandle, const FVector2D& ClientCoordsUNorm, FIntPoint& OutVirtualDesktopPixel); +}; + +#if PLATFORM_WINDOWS +#include "Windows/WindowsApplication.h" +#include "HAL/ThreadSafeBool.h" + +class FTobiiPlatformNotifications : public IWindowsMessageHandler +{ +public: + FTobiiPlatformNotifications(); + virtual ~FTobiiPlatformNotifications(); + + virtual bool ProcessMessage(HWND hwnd, uint32 msg, WPARAM wParam, LPARAM lParam, int32& OutResult) override; + +public: + FThreadSafeBool bShouldForceEyetrackerReconnect; + FThreadSafeBool bShouldUpdateGameMonitorHandle; + +}; +#elif PLATFORM_MAC +#else +#endif \ No newline at end of file diff --git a/Source/TobiiCore/Private/UserGuide/STobiiLicenseWindow.cpp b/Source/TobiiCore/Private/UserGuide/STobiiLicenseWindow.cpp new file mode 100644 index 0000000..63b3549 --- /dev/null +++ b/Source/TobiiCore/Private/UserGuide/STobiiLicenseWindow.cpp @@ -0,0 +1,154 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#if WITH_EDITOR + +#include "STobiiLicenseWindow.h" +#include "STobiiWelcomeWindow.h" + +#include "CoreGlobals.h" +#include "Editor.h" +#include "UnrealClient.h" +#include "EditorStyleSet.h" +#include "Framework/Application/SlateApplication.h" +#include "Misc/ConfigCacheIni.h" +#include "Misc/FileHelper.h" +#include "Widgets/Layout/SGridPanel.h" +#include "Widgets/Layout/SScrollBox.h" +#include "Widgets/Images/SImage.h" +#include "Widgets/Text/STextBlock.h" +#include "Widgets/Input/SButton.h" + +#define LOCTEXT_NAMESPACE "TobiiLicenseWindow" + +void STobiiLicenseWindow::Construct(const FArguments& InArgs, FTobiiCoreModule* InCoreModule) +{ + if (GEditor == nullptr) + { + return; + } + + CoreModule = InCoreModule; + +#if TOBII_COMPILE_AS_ENGINE_PLUGIN + FString LicenseFilePath = FPaths::EnginePluginsDir() / TEXT("Runtime/TobiiEyetracking/Resources/License.txt"); +#else + FString LicenseFilePath = FPaths::ProjectPluginsDir() / TEXT("TobiiEyetracking/Resources/License.txt"); +#endif + + FString LicenseText; + bool bCanAccept = true; + if (!FFileHelper::LoadFileToString(LicenseText, *LicenseFilePath)) + { + LicenseText = "License file not found in Resources folder!\n\nAccept button disabled.\nIf you cannot repair/replace the Tobii SDK, please remove it from your project."; + bCanAccept = false; + } + + SWindow::Construct(SWindow::FArguments() + .SupportsMaximize(false) + .SupportsMinimize(false) + .IsPopupWindow(true) + .CreateTitleBar(false) + .SizingRule(ESizingRule::FixedSize) + .SupportsTransparency(EWindowTransparency::None) + .InitialOpacity(1.0f) + .FocusWhenFirstShown(true) + .bDragAnywhere(false) + .ActivationPolicy(EWindowActivationPolicy::FirstShown) + .ClientSize(FVector2D(1024, 768)) + .ScreenPosition(FVector2D((float)(GEditor->GetActiveViewport()->GetSizeXY().X) / 2.0, + (float)(GEditor->GetActiveViewport()->GetSizeXY().Y) / 2.0)) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .FillHeight(0.9f) + .Padding(5) + [ + SNew(SScrollBox) + +SScrollBox::Slot() + [ + SNew(SGridPanel) + .FillColumn(0, 1.0f) + .FillRow(0, 1.0f) + + + SGridPanel::Slot(0, 0) + [ + SNew(SImage) + .ColorAndOpacity(FSlateColor(FLinearColor(FColor::Black))) + ] + + + SGridPanel::Slot(0, 0) + [ + SNew(STextBlock) + .AutoWrapText(true) + .Margin(10.0f) + .Text(FText::FromString(LicenseText)) + ] + ] + ] + + + SVerticalBox::Slot() + .FillHeight(0.05f) + [ + SNew(SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Fill) + .IsEnabled(bCanAccept) + .OnClicked(this, &STobiiLicenseWindow::AcceptLicense) + [ + SNew(STextBlock) + .Text(LOCTEXT("OkButton", "I Accept the terms of this agreement")) + .Justification(ETextJustify::Center) + ] + ] + + SVerticalBox::Slot() + .FillHeight(0.05f) + [ + SNew(SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Fill) + .OnClicked(this, &STobiiLicenseWindow::ShutdownEditor) + [ + SNew(STextBlock) + .Text(LOCTEXT("ExitButton", "I do not accept the terms. Shut down editor so I can remove the plugin.")) + .Justification(ETextJustify::Center) + ] + ] + ]); + + bIsTopmostWindow = true; + FlashWindow(); +} + +FReply STobiiLicenseWindow::AcceptLicense() +{ + RequestDestroyWindow(); + + if (GConfig != nullptr) + { + GConfig->SetBool(TEXT("Tobii"), TEXT("TobiiLicenseAccepted"), true, GGameIni); + } + + bool bTutorialShown = false; + GConfig->GetBool(TEXT("Tobii"), TEXT("TobiiTutorialShown"), bTutorialShown, GGameIni); + if (!bTutorialShown && GEditor != nullptr) + { + FSlateApplication::Get().AddWindow(SNew(STobiiWelcomeWindow)); + } + + return FReply::Handled(); +} + +FReply STobiiLicenseWindow::ShutdownEditor() +{ + RequestDestroyWindow(); + FGenericPlatformMisc::RequestExit(false); + return FReply::Handled(); +} + +#undef LOCTEXT_NAMESPACE + +#endif //WITH_EDITOR diff --git a/Source/TobiiCore/Private/UserGuide/STobiiLicenseWindow.h b/Source/TobiiCore/Private/UserGuide/STobiiLicenseWindow.h new file mode 100644 index 0000000..0efb416 --- /dev/null +++ b/Source/TobiiCore/Private/UserGuide/STobiiLicenseWindow.h @@ -0,0 +1,36 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#if WITH_EDITOR + +#include "../TobiiCoreModule.h" + +#include "CoreMinimal.h" +#include "SWebBrowser.h" +#include "Widgets/SWindow.h" + +class STobiiLicenseWindow : public SWindow +{ +public: + SLATE_BEGIN_ARGS(STobiiLicenseWindow) { } + SLATE_END_ARGS() + + STobiiLicenseWindow() {} + + /** Widget constructor */ + void Construct(const FArguments& Args, FTobiiCoreModule* InCoreModule); + +private: + TSharedPtr MainBrowser; + FTobiiCoreModule* CoreModule; + + FReply AcceptLicense(); + FReply ShutdownEditor(); +}; + +#endif //WITH_EDITOR diff --git a/Source/TobiiCore/Private/UserGuide/STobiiWelcomeWindow.cpp b/Source/TobiiCore/Private/UserGuide/STobiiWelcomeWindow.cpp new file mode 100644 index 0000000..f4cd896 --- /dev/null +++ b/Source/TobiiCore/Private/UserGuide/STobiiWelcomeWindow.cpp @@ -0,0 +1,214 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#if WITH_EDITOR + +#include "STobiiWelcomeWindow.h" + +#include "Editor.h" +#include "EditorStyleSet.h" +#include "ContentBrowserModule.h" +#include "IContentBrowserSingleton.h" +#include "CoreGlobals.h" +#include "UnrealClient.h" +#include "Widgets/Layout/SGridPanel.h" +#include "Widgets/Layout/SScrollBox.h" +#include "Widgets/Images/SImage.h" +#include "Widgets/Text/STextBlock.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Input/SCheckBox.h" +#include "Misc/ConfigCacheIni.h" +#include "Misc/FileHelper.h" +#include "Modules/ModuleManager.h" + +#define LOCTEXT_NAMESPACE "STobiiWelcomeWindow" + +void STobiiWelcomeWindow::Construct(const FArguments& InArgs) +{ + if (GEditor == nullptr) + { + return; + } + + bDontShowAgain = false; + + SWindow::Construct(SWindow::FArguments() + .SupportsMaximize(false) + .SupportsMinimize(false) + .IsPopupWindow(true) + .CreateTitleBar(false) + .SizingRule(ESizingRule::FixedSize) + .SupportsTransparency(EWindowTransparency::None) + .InitialOpacity(1.0f) + .FocusWhenFirstShown(true) + .bDragAnywhere(false) + .ActivationPolicy(EWindowActivationPolicy::FirstShown) + .ClientSize(FVector2D(600, 400)) + .ScreenPosition(FVector2D((float)(GEditor->GetActiveViewport()->GetSizeXY().X) / 2.0, + (float)(GEditor->GetActiveViewport()->GetSizeXY().Y) / 2.0)) + [ + SNew(SGridPanel) + .FillColumn(0, 1.0f) + .FillRow(0, 1.0f) + + + SGridPanel::Slot(0, 0) + [ + SNew(SImage) + .ColorAndOpacity(FSlateColor(FLinearColor(FColor::Black))) + ] + + + SGridPanel::Slot(0, 0) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .Padding(10.0f) + [ + SNew(STextBlock) + .Justification(ETextJustify::Center) + .Margin(5.0f) + .Font(FEditorStyle::GetFontStyle(FName("ToggleButton.LabelFont"))) + .Text(FText::FromString("Tobii Sample Content")) + ] + + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(STextBlock) + .AutoWrapText(true) + .Margin(5.0f) + .Text(FText::FromString("Thank you again for downloading the UE4 Tobii eyetracking plugin!.\nThis plugin comes loaded with sample content as well as the core functionality to enable your development environment to leverage the eyetracker hardware.\nThe sample content is packaged can be accessed via the content browser under the 'TobiiEyetracking' plugin section. As plugin content is not visible by default however, you need to first click the small eye button at the bottom right of your content browser and check the 'show plugin content' checkbox. After that you should be able to scroll your content browser down to see the category.")) + ] + + + SVerticalBox::Slot() + .AutoHeight() + .Padding(15.0f) + [ + SNew(SHorizontalBox) + +SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SCheckBox) + .IsChecked(bDontShowAgain ? ECheckBoxState::Checked : ECheckBoxState::Unchecked) + .OnCheckStateChanged_Lambda([this](ECheckBoxState State) { bDontShowAgain = State == ECheckBoxState::Checked; }) + ] + +SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(STextBlock) + .Text(FText::FromString("Don't show this window again.")) + ] + ] + + + SVerticalBox::Slot() + .Padding(0.0f, 50.0f, 0.0f, 0.0f) + [ + SNew(STextBlock) + .Text(FText::FromString("Would you like to navigate to the samples now?")) + ] + + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Fill) + .OnClicked(this, &STobiiWelcomeWindow::ShowDesktopSamples) + [ + SNew(STextBlock) + .Text(LOCTEXT("OkButton", "Yes, please show me the desktop samples")) + .Justification(ETextJustify::Center) + ] + ] + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Fill) + .OnClicked(this, &STobiiWelcomeWindow::ShowXRSamples) + [ + SNew(STextBlock) + .Text(LOCTEXT("OkButtonXR", "Yes, please show me the XR samples")) + .Justification(ETextJustify::Center) + ] + ] + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SButton) + .VAlign(VAlign_Center) + .HAlign(HAlign_Fill) + .OnClicked(this, &STobiiWelcomeWindow::AbortTutorial) + [ + SNew(STextBlock) + .Text(LOCTEXT("ExitButton", "No thank you.")) + .Justification(ETextJustify::Center) + ] + ] + ] + ]); + + bIsTopmostWindow = true; + FlashWindow(); +} + +void STobiiWelcomeWindow::UpdateFlag() +{ + if (GConfig != nullptr && bDontShowAgain) + { + GConfig->SetBool(TEXT("Tobii"), TEXT("TobiiTutorialShown"), true, GGameIni); + } +} + +FReply STobiiWelcomeWindow::ShowDesktopSamples() +{ + RequestDestroyWindow(); + UpdateFlag(); + + //Actually turn on plugin content viewing and navigate the content browser to the sample + IContentBrowserSingleton& ContentBrowserInterface = FModuleManager::LoadModuleChecked(TEXT("ContentBrowser")).Get(); + ContentBrowserInterface.ForceShowPluginContent(false); + + TArray MapAsset; + MapAsset.Add(FAssetData(FName("/TobiiEyetracking/Sample/Desktop/DesktopExampleMap") + , FName("/TobiiEyetracking/Sample/Desktop") + , FName("DesktopExampleMap") + , FName("World"))); + ContentBrowserInterface.SyncBrowserToAssets(MapAsset); + + return FReply::Handled(); +} + +FReply STobiiWelcomeWindow::ShowXRSamples() +{ + RequestDestroyWindow(); + UpdateFlag(); + + //Actually turn on plugin content viewing and navigate the content browser to the sample + IContentBrowserSingleton& ContentBrowserInterface = FModuleManager::LoadModuleChecked(TEXT("ContentBrowser")).Get(); + ContentBrowserInterface.ForceShowPluginContent(false); + + TArray MapAsset; + MapAsset.Add(FAssetData(FName("/TobiiEyetracking/Sample/XR/XRExampleMap") + , FName("/TobiiEyetracking/Sample/XR") + , FName("XRExampleMap") + , FName("World"))); + ContentBrowserInterface.SyncBrowserToAssets(MapAsset); + + return FReply::Handled(); +} + +FReply STobiiWelcomeWindow::AbortTutorial() +{ + RequestDestroyWindow(); + UpdateFlag(); + + return FReply::Handled(); +} + +#undef LOCTEXT_NAMESPACE + +#endif //WITH_EDITOR diff --git a/Source/TobiiCore/Private/UserGuide/STobiiWelcomeWindow.h b/Source/TobiiCore/Private/UserGuide/STobiiWelcomeWindow.h new file mode 100644 index 0000000..be669af --- /dev/null +++ b/Source/TobiiCore/Private/UserGuide/STobiiWelcomeWindow.h @@ -0,0 +1,38 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#if WITH_EDITOR + +#include "CoreMinimal.h" +#include "SWebBrowser.h" +#include "Widgets/SWindow.h" + +class STobiiWelcomeWindow : public SWindow +{ +public: + SLATE_BEGIN_ARGS(STobiiWelcomeWindow) + { + } + SLATE_END_ARGS() + + STobiiWelcomeWindow() {} + + /** Widget constructor */ + void Construct(const FArguments& Args); + +private: + TSharedPtr MainBrowser; + bool bDontShowAgain; + + void UpdateFlag(); + FReply ShowDesktopSamples(); + FReply ShowXRSamples(); + FReply AbortTutorial(); +}; + +#endif //WITH_EDITOR diff --git a/Source/TobiiCore/Private/UserGuide/TobiiEditorExtension.cpp b/Source/TobiiCore/Private/UserGuide/TobiiEditorExtension.cpp new file mode 100644 index 0000000..05073e5 --- /dev/null +++ b/Source/TobiiCore/Private/UserGuide/TobiiEditorExtension.cpp @@ -0,0 +1,88 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#if WITH_EDITOR + +#include "TobiiEditorExtension.h" +#include "STobiiLicenseWindow.h" + +#include "RHI.h" +#include "Editor.h" +#include "Modules/ModuleManager.h" +#include "Framework/Application/SlateApplication.h" + +FTobiiEditorExtension::FTobiiEditorExtension(FTobiiCoreModule* InCoreModule) + : CoreModule(InCoreModule) + , bIsEditorInitialized(false) +{ + // Defer Level Editor UI extensions until Level Editor has been loaded: + if (FModuleManager::Get().IsModuleLoaded("LevelEditor")) + { + Initialize(); + } + else + { + FModuleManager::Get().OnModulesChanged().AddLambda([this](FName name, EModuleChangeReason reason) + { + if ((name == "LevelEditor") && (reason == EModuleChangeReason::ModuleLoaded)) + { + Initialize(); + } + }); + } +} + +FTobiiEditorExtension::~FTobiiEditorExtension() +{ +} + +void FTobiiEditorExtension::Initialize() +{ + if (GUsingNullRHI) + { + return; + } + + FSlateRenderer* SlateRenderer = FSlateApplication::Get().GetRenderer(); + if (SlateRenderer != nullptr) + { + LoadedDelegateHandle = SlateRenderer->OnSlateWindowRendered().AddRaw(this, &FTobiiEditorExtension::OnEditorLoaded); + } +} + +void FTobiiEditorExtension::OnEditorLoaded(SWindow& SlateWindow, void* ViewportRHIPtr) +{ + // would be nice to use the preprocessor definition WITH_EDITOR instead, + // but the user may launch a standalone game through the editor... + if (GEditor == nullptr || CoreModule == nullptr) + { + return; + } + + if (IsInGameThread()) + { + FSlateRenderer* SlateRenderer = FSlateApplication::Get().GetRenderer(); + SlateRenderer->OnSlateWindowRendered().Remove(LoadedDelegateHandle); + } + + if (bIsEditorInitialized) + { + return; + } + bIsEditorInitialized = true; + + if (GConfig != nullptr) + { + bool bTobiiLicenseAccepted(false); + GConfig->GetBool(TEXT("Tobii"), TEXT("TobiiLicenseAccepted"), bTobiiLicenseAccepted, GGameIni); + if (!bTobiiLicenseAccepted && GEditor != nullptr) + { + FSlateApplication::Get().AddWindow(SNew(STobiiLicenseWindow, CoreModule)); + } + } +} + +#endif //WITH_EDITOR diff --git a/Source/TobiiCore/Private/UserGuide/TobiiEditorExtension.h b/Source/TobiiCore/Private/UserGuide/TobiiEditorExtension.h new file mode 100644 index 0000000..55c3872 --- /dev/null +++ b/Source/TobiiCore/Private/UserGuide/TobiiEditorExtension.h @@ -0,0 +1,30 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#if WITH_EDITOR + +#include "../TobiiCoreModule.h" + +#include "CoreMinimal.h" + +class FTobiiEditorExtension +{ +public: + FTobiiEditorExtension(FTobiiCoreModule* InCoreModule); + ~FTobiiEditorExtension(); + void Initialize(); + +private: + FTobiiCoreModule* CoreModule; + FDelegateHandle LoadedDelegateHandle; + bool bIsEditorInitialized; + + void OnEditorLoaded(SWindow& SlateWindow, void* ViewportRHIPtr); +}; + +#endif //WITH_EDITOR diff --git a/Source/TobiiCore/Public/ITobiiCore.h b/Source/TobiiCore/Public/ITobiiCore.h new file mode 100644 index 0000000..3c79cea --- /dev/null +++ b/Source/TobiiCore/Public/ITobiiCore.h @@ -0,0 +1,59 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "ITobiiEyetracker.h" + +#include "CoreMinimal.h" +#include "IEyeTrackerModule.h" +#include "Modules/ModuleInterface.h" +#include "Modules/ModuleManager.h" + +DECLARE_DELEGATE(FPostLoadedTobiiEyetrackingDelegate); + +/** + * The public interface of the TobiiEyeX Module. + */ +class ITobiiCore : public IEyeTrackerModule +{ +public: + /** + * Singleton-like access to this module's interface. This is just for convenience! + * Beware of calling this during the shutdown phase, though. Your module might have been + * unloaded already. + * + * @return Returns singleton instance, loading the module on demand if needed + */ + static inline ITobiiCore& Get() + { + return FModuleManager::LoadModuleChecked("TobiiCore"); + } + + /** + * Checks to see if this module is loaded and ready. It is only valid to call Get() if + * IsAvailable() returns true. + * + * @return True if the module is loaded and ready to use + */ + static inline bool IsAvailable() + { + return FModuleManager::Get().IsModuleLoaded("TobiiCore") && Get().GetEyeTrackerInternal().IsValid(); + } + + virtual FString GetModuleKeyName() const override + { + return FString("TobiiCore"); + } + + static TSharedPtr GetEyeTracker() + { + return IsAvailable() ? Get().GetEyeTrackerInternal() : TSharedPtr(nullptr); + } + +protected: + virtual TSharedPtr GetEyeTrackerInternal() = 0; +}; diff --git a/Source/TobiiCore/Public/ITobiiEyetracker.h b/Source/TobiiCore/Public/ITobiiEyetracker.h new file mode 100644 index 0000000..6eb8b13 --- /dev/null +++ b/Source/TobiiCore/Public/ITobiiEyetracker.h @@ -0,0 +1,118 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "TobiiTypes.h" + +#include "CoreMinimal.h" +#include "IEyeTracker.h" + +class ITobiiEyeTracker : public IEyeTracker +{ + /************************************************************************/ + /* Common Data */ + /************************************************************************/ +public: + /** + * Will return gaze data for a combination of both eyes. + * For mono trackers this might mean sending the data from the active eye. + * For a more advanced system, it might be some combination of both eyes. + * It contains all kinds of data you might need for low level interaction. + * + * @returns Combined gaze data. + */ + virtual const FTobiiGazeData& GetCombinedGazeData() const = 0; + + /** + * Will return the gaze data for the left eye. + * It contains all kinds of data you might need for low level interaction. + * + * @returns Gaze data for the right eye. + */ + virtual const FTobiiGazeData& GetLeftGazeData() const = 0; + + /** + * Will return the gaze data for the right eye. + * It contains all kinds of data you might need for low level interaction. + * + * @returns Gaze data for the left eye. + */ + virtual const FTobiiGazeData& GetRightGazeData() const = 0; + + /** + * Returns information about the status of the current device. + * + * @return The status of the device. + */ + virtual ETobiiGazeTrackerStatus GetGazeTrackerStatus() const = 0; + + /** + * Some gaze trackers might only be able to deliver data for one eye, and others might be able to serve data for both. + * If you are planning to use other functions than GetCombinedGazeData, checking that the gaze tracker you are working with supports them is recommended. + * + * @param Capability The capability to poll. + * @returns Whether the gaze tracker supports the capability polled. + */ + virtual bool GetGazeTrackerCapability(ETobiiGazeTrackerCapability Capability) const = 0; + + /** + * This is information about where the combined gaze ray hits the world. As this is needed by many different gaze related features, we do this raycast for the developer. + * + * @returns Relevant hit result. + */ + virtual const FHitResult& GetCombinedWorldGazeHitData() const = 0; + + /** + * This is information about where the combined gaze ray hits the world. As this is needed by many different gaze related features, we do this raycast for the developer. + * + * @returns Relevant hit result. + */ + virtual const FHitResult& GetLeftWorldGazeHitData() const = 0; + + /** + * This is information about where the combined gaze ray hits the world. As this is needed by many different gaze related features, we do this raycast for the developer. + * + * @returns Relevant hit result. + */ + virtual const FHitResult& GetRightWorldGazeHitData() const = 0; + + /** + * Knowledge about the monitor size when designing gaze interactions on desktop can be very useful since working in SI units can make code screen size invariant. Display information is also useful for foveation techniques both in desktop and XR. + * + * @returns Information regarding the display associated with the currently active tracker. + */ + virtual const FTobiiDisplayInfo& GetDisplayInformation() const = 0; + + /************************************************************************/ + /* Head Tracker */ + /************************************************************************/ +public: + /** + * This gives you all the relevant data for head tracking + * + * @returns Information regarding head position and orientation + */ + virtual const FTobiiHeadPoseData& GetHeadPoseData() const = 0; + + /************************************************************************/ + /* Desktop */ + /************************************************************************/ +public: + /** + * On desktop, the tracker can only track the user within a limited frustum. + * + * @returns The vertices defining the tracking frustum in the Tobii User Coordinate Space. + */ + virtual const FTobiiDesktopTrackBox& GetDesktopTrackBox() const = 0; + + /** + * This is output from the Tobii Infinite screen algorithm. You ideally shouldn't use these angles by yourself, but instead use the camera modifier found in the interaction module. + * + * @returns Infinite screen angles. + */ + virtual const FRotator& GetInfiniteScreenAngles() const = 0; +}; diff --git a/Source/TobiiCore/Public/TobiiBlueprintLibrary.h b/Source/TobiiCore/Public/TobiiBlueprintLibrary.h new file mode 100644 index 0000000..63652ed --- /dev/null +++ b/Source/TobiiCore/Public/TobiiBlueprintLibrary.h @@ -0,0 +1,153 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "CoreMinimal.h" +#include "TobiiTypes.h" + +#include "Kismet/BlueprintFunctionLibrary.h" +#include "Components/WidgetComponent.h" + +#include "TobiiBlueprintLibrary.generated.h" + +/** + * Simplified interface for blueprint use. Only exposes the features most likely to be consumed from BP. + */ +UCLASS() +class TOBIICORE_API UTobiiBlueprintLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_UCLASS_BODY() + + /************************************************************************/ + /* Common functions */ + /************************************************************************/ +public: + + /** + * This is just a helper function to set the eyetracking enabled CVar for blueprint convenience. + * Set eyetracking disabled if you don't need tracking and don't want to pay the CPU cost in some parts of your program. + */ + UFUNCTION(BlueprintCallable, Category = "Tobii Eyetracking") + static void SetTobiiEyetrackingEnabled(bool bEyetrackingEnabled); + + UFUNCTION(BlueprintPure, Category = "Tobii Eyetracking") + static bool GetTobiiEyetrackingEnabled(); + + /** + * This is just a helper function to set the eyetracking frozen CVar for blueprint convenience. + * Freezing the eyetracking can be very useful for debugging as well as when trying to show someone what effect eyetracking has. + */ + UFUNCTION(BlueprintCallable, Category = "Tobii Eyetracking") + static void SetTobiiEyetrackingFrozen(bool bEyetrackingFrozen); + + UFUNCTION(BlueprintPure, Category = "Tobii Eyetracking") + static bool GetTobiiEyetrackingFrozen(); + + /** + * This is the main method to get basic eye tracking data. You should not be using this though for most things. Use the focus system instead if you can. + */ + UFUNCTION(BlueprintPure, Category = "Tobii Eyetracking") + static FTobiiGazeData GetTobiiCombinedGazeData(); + UFUNCTION(BlueprintPure, Category = "Tobii Eyetracking") + static FTobiiGazeData GetTobiiLeftGazeData(); + UFUNCTION(BlueprintPure, Category = "Tobii Eyetracking") + static FTobiiGazeData GetTobiiRightGazeData(); + UFUNCTION(BlueprintPure, Category = "Tobii Eyetracking") + static FHitResult GetTobiiCombinedWorldGazeHitData(); + + /** + * This will indicate the readiness of the gaze tracking subsystem. For most applications this will also indicate head tracking readiness. + */ + UFUNCTION(BlueprintPure, Category = "Tobii Eyetracking") + static ETobiiGazeTrackerStatus GetTobiiGazeTrackerStatus(); + + /** + * Gets information about the display device and window used for the application. + */ + UFUNCTION(BlueprintPure, Category = "Tobii Eyetracking") + static FTobiiDisplayInfo GetTobiiDisplayInformation(); + + UFUNCTION(BlueprintPure, Category = "Tobii Eyetracking") + static FRotator GetTobiiInfiniteScreenAngles(); + + /************************************************************************/ + /* Desktop functions */ + /************************************************************************/ +public: + /** + * This is the main method to get basic head pose data. + */ + UFUNCTION(BlueprintPure, Category = "Tobii Desktop Eyetracking") + static const FTobiiHeadPoseData& GetTobiiHeadPoseData(); + /** + * Track box information. + */ + UFUNCTION(BlueprintPure, Category = "Tobii Desktop Eyetracking") + static const FTobiiDesktopTrackBox& GetTobiiDesktopTrackBox(); + + /************************************************************************/ + /* Settings functions */ + /************************************************************************/ +public: + //Use these functions to get and set CVars in runtime. + UFUNCTION(BlueprintPure, Category = "Tobii Settings") + static int32 GetTobiiInt(FString CVarName); + UFUNCTION(BlueprintCallable, Category = "Tobii Settings") + static void SetTobiiInt(FString CVarName, const int32 NewValue); + UFUNCTION(BlueprintPure, Category = "Tobii Settings") + static float GetTobiiFloat(FString CVarName); + UFUNCTION(BlueprintCallable, Category = "Tobii Settings") + static void SetTobiiFloat(FString CVarName, const float NewValue); + + //If you are persisting your custom CVar values, you should call this function when you start your application. + UFUNCTION(BlueprintCallable, Category = "Tobii Settings") + static void LoadTobiiSettings(); + //If you want to persist a CVar to .ini, use this function. + UFUNCTION(BlueprintCallable, Category = "Tobii Settings") + static void SaveTobiiSetting(FString CVarSettingToSave); + + /************************************************************************/ + /* Math Util functions */ + /************************************************************************/ +public: + /** + * Use this function if you want to project slate information to viewport space. Includes DPI adjustments + */ + UFUNCTION(BlueprintPure, Category = "Tobii Math Utils") + static bool VirtualDesktopPixelToViewportCoordinateUNorm(const FVector2D& VirtualDesktopPixel, FVector2D& OutViewportCoordinateUNorm); + + UFUNCTION(BlueprintPure, Category = "Tobii Math Utils") + static bool ViewportCoordinateUNormToVirtualDesktopPixel(const FVector2D& ViewportCoordinateUNorm, FVector2D& OutVirtualDesktopPixel); + + UFUNCTION(BlueprintPure, Category = "Tobii Math Utils") + static bool ViewportPixelCoordToCmCoord(const FVector2D& InCoordinatePx, FVector2D& OutCoordinateCm); + + UFUNCTION(BlueprintPure, Category = "Tobii Math Utils") + static bool ViewportCmCoordToPixelCoord(const FVector2D& InCoordinateCm, FVector2D& OutCoordinatePx); + + UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils") + static bool ViewportPixelCoordToUNormCoord(const FVector2D& InCoordinatePx, FVector2D& OutCoordinateUNorm); + + UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils") + static bool ViewportUNormCoordToPixelCoord(const FVector2D& InCoordinateUNorm, FVector2D& OutCoordinatePx); + + /** + * This function lets you move from world space to the local space of the user. In XR this would be head space, and for desktop it would be relative to the scene camera. + */ + UFUNCTION(BlueprintPure, Category = "Tobii Math Utils") + static FVector ConvertGazeToUserSpace(APlayerController* PlayerController, const FVector& WorldSpaceGazeDirection); + + /************************************************************************/ + /* Util functions */ + /************************************************************************/ +public: + /** + * Tests if a given player controller is XR enabled + */ + UFUNCTION(BlueprintPure, Category = "Tobii XR Utils") + static bool IsXRPlayerController(const APlayerController* PlayerController); +}; diff --git a/Source/TobiiCore/Public/TobiiTypes.h b/Source/TobiiCore/Public/TobiiTypes.h new file mode 100644 index 0000000..e2fd7d9 --- /dev/null +++ b/Source/TobiiCore/Public/TobiiTypes.h @@ -0,0 +1,225 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/EngineTypes.h" + +#include "TobiiTypes.generated.h" + +/** + * Structure that contains gaze information from one eye in both screen and world space. + */ +USTRUCT(BlueprintType) +struct FTobiiGazeData +{ + GENERATED_USTRUCT_BODY() + +public: + //Origin of the eye's gaze ray in Unreal world space. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Space Gaze Data") + FVector WorldGazeOrigin; + //Forward direction of the eye's gaze ray this frame in Unreal world space. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Space Gaze Data") + FVector WorldGazeDirection; + //Due to how the eye works and imperfections in eye tracking technology, it makes more sense to express the world gaze field as a cone rather than a ray. This angle is the angle between the Gaze Direction and the side of the cone expressed in degrees. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Space Data") + float WorldGazeConeAngleDegrees; + + //The gaze point in screen space in pixels this frame. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Screen Space Gaze Data") + FVector2D ScreenGazePointPx; + //Due to how the eye works and imperfections in eye tracking technology, it makes more sense to express the screen space gaze field as an ellipse rather than a point. This is the extent of that ellipse expressed in pixels for the eye. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Screen Space Data") + FVector2D ScreenGazeCircleRadiiPx; + + //Time when the gaze point was created. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data") + FDateTime TimeStamp; + //This is how open the eye is. 0 means closed, 1 means open. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data") + float EyeOpenness; + //If the gaze point is moving slowly enough, it is considered stable. This is useful when trying to prevent false positives when the gaze point is moving past objects. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data") + bool bIsStable; + //If this is true, all other properties related to the eye should be safe to use. This might be false because the tracking system is a mono tracker, or if the data is bad for this frame. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data") + bool bIsGazeDataValid; + + FTobiiGazeData() + : bIsGazeDataValid(false) + { + } +}; + +/** + * Location and orientation of the head. + */ +USTRUCT(BlueprintType) +struct FTobiiHeadPoseData +{ + GENERATED_USTRUCT_BODY() + +public: + FTobiiHeadPoseData() + : HeadLocation(0.0f, 0.0f, 0.0f) + , HeadOrientation(0.0f, 0.0f, 0.0f) + {} + + //This is the local space location of the head relative to the device specified origin in centimeters. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Pose Data") + FVector HeadLocation; + //This is the local space orientation of the head. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Pose Data") + FRotator HeadOrientation; +}; + +/** + * Information regarding the display associated with the currently active eye tracker. + */ +USTRUCT(BlueprintType) +struct FTobiiDisplayInfo +{ + GENERATED_USTRUCT_BODY() + +public: + //This is the width in pixels of the monitor the game is running on as reported by windows. + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Display Info") + int32 MonitorWidthPx; + //This is the height in pixels of the monitor the game is running on as reported by windows. + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Display Info") + int32 MonitorHeightPx; + //This is the width in centimeters of the monitor the active eyetracker is attached to as reported by the eyetracker. Centimeters is also the measurement used for unreal units. + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Display Info") + float MonitorWidthCm; + //This is the height in centimeters of the monitor the active eyetracker is attached to as reported by the eyetracker. Centimeters is also the measurement used for unreal units. + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Display Info") + float MonitorHeightCm; + + //This is the width in pixels of the main game viewport as reported by the game. + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Display Info") + int32 MainViewportWidthPx; + //This is the height in pixels of the main game viewport as reported by the game. + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Display Info") + int32 MainViewportHeightPx; + //This is the width of the main game viewport in centimeters as calculated using data from the game and the eyetracker. Centimeters is also the measurement used for unreal units. + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Display Info") + float MainViewportWidthCm; + //This is the height of the main game viewport in centimeters as calculated using data from the game and the eyetracker. Centimeters is also the measurement used for unreal units. + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Display Info") + float MainViewportHeightCm; + + //This is the dpi scale of the monitor the game is running on + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Display Info") + float DpiScale; + +public: + void* GameWindowHandle; + void* GameMonitorHandle; + +public: + FTobiiDisplayInfo() + : MonitorWidthPx(0) + , MonitorHeightPx(0) + , MonitorWidthCm(0.0f) + , MonitorHeightCm(0.0f) + + , MainViewportWidthPx(0) + , MainViewportHeightPx(0) + , MainViewportWidthCm(0.0f) + , MainViewportHeightCm(0.0f) + + , DpiScale(1.0f) + + , GameWindowHandle(nullptr) + , GameMonitorHandle(nullptr) + {} +}; + +/** + * This is the vertices for the desktop only track box expressed in Tobii User Coordinate System coordinates. + * This coordinate system has its origin in the middle of the active display. X+ is towards the user's right. Y+ is up. Z+ is away from the screen towards the user. + * See general API documentation for details. + */ +USTRUCT(BlueprintType) +struct FTobiiDesktopTrackBox +{ + GENERATED_USTRUCT_BODY() + +public: + //These are the extreme points of the eye tracker tracking frustum where we get optimal tracking quality in centimeters. These are in the Tobii User Coordinate System. + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Track Box") + FVector FrontUpperRightPointCm; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Track Box") + FVector FrontUpperLeftPointCm; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Track Box") + FVector FrontLowerLeftPointCm; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Track Box") + FVector FrontLowerRightPointCm; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Track Box") + FVector BackUpperRightPointCm; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Track Box") + FVector BackUpperLeftPointCm; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Track Box") + FVector BackLowerLeftPointCm; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Track Box") + FVector BackLowerRightPointCm; + +public: + FTobiiDesktopTrackBox() + : FrontUpperRightPointCm(0.0f, 0.0f, 0.0f) + , FrontUpperLeftPointCm(0.0f, 0.0f, 0.0f) + , FrontLowerLeftPointCm(0.0f, 0.0f, 0.0f) + , FrontLowerRightPointCm(0.0f, 0.0f, 0.0f) + , BackUpperRightPointCm(0.0f, 0.0f, 0.0f) + , BackUpperLeftPointCm(0.0f, 0.0f, 0.0f) + , BackLowerLeftPointCm(0.0f, 0.0f, 0.0f) + , BackLowerRightPointCm(0.0f, 0.0f, 0.0f) + {} +}; + +/** + * Describes the current state of the gaze tracker. Is designed in such a way as later values indicates a higher level of readiness to make it easy to use in interactions. + */ +UENUM(BlueprintType) +enum class ETobiiGazeTrackerStatus : uint8 +{ + /** The gaze tracker is not connected to UE4 for some reason. The tracker might not be plugged in, the game window is currently running on a screen without a gaze tracker or is otherwise not available. */ + NotConnected, + + /** The gaze tracker is connected, but cannot track as it is missing some important configuration step. */ + NotConfigured, + + /** Gaze Tracking has been disabled by the user or developer or some other eventuality, like for example the game being minimized which might suspend the tracking. */ + Disabled, + + /** The gaze tracker is running but has not yet detected a user. Not relevant in XR. */ + UserNotPresent, + + /** The gaze tracker has detected a user and is actively tracking them. They appear not to be focusing on the game window at the moment however, so be careful about how you are using the data. */ + UserPresent +}; + +/** + * Some gaze trackers might only be able to deliver data for one eye, and others might be able to serve data for both. + * If you are planning to use the left or right gaze data functions, checking that the gaze tracker you are working with supports them is recommended. + */ +UENUM(BlueprintType) +enum class ETobiiGazeTrackerCapability : uint8 +{ + /** This gaze tracker can deliver combined gaze data. */ + None = 0 UMETA(DisplayName = "No Support"), + + /** This gaze tracker can deliver combined gaze data. */ + CombinedGazeData = 1 UMETA(DisplayName = "Supports Combined Gaze Data"), + + /** This gaze tracker can deliver individual gaze data for the left eye. */ + LeftGazeData = 2 UMETA(DisplayName = "Supports Left Eye Gaze Data"), + + /** This gaze tracker can deliver individual gaze data for the right eye. */ + RightGazeData = 4 UMETA(DisplayName = "Supports Right Eye Gaze Data") +}; diff --git a/Source/TobiiCore/TobiiCore.Build.cs b/Source/TobiiCore/TobiiCore.Build.cs new file mode 100644 index 0000000..9acf8eb --- /dev/null +++ b/Source/TobiiCore/TobiiCore.Build.cs @@ -0,0 +1,112 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +using System; +using System.IO; +using System.Diagnostics; +using Tools.DotNETCommon; + +namespace UnrealBuildTool.Rules +{ + public class TobiiCore : ModuleRules + { + //If you want to remove eyetracking functionality from your builds without modifying your game code / project you can do so easily by setting this to false. + private bool IsEyetrackingActive { get { return true; } } + + public TobiiCore(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + PrivateDependencyModuleNames.AddRange(new string[] + { + "Core" + , "CoreUObject" + , "Engine" + , "UMG" + , "InputCore" + , "InputDevice" + , "HeadMountedDisplay" + }); + + PublicDependencyModuleNames.AddRange(new string[] + { + "EyeTracker" + }); + + if (Target.bBuildEditor) + { + DynamicallyLoadedModuleNames.AddRange(new string[] { "LevelEditor" }); + PublicDependencyModuleNames.AddRange(new string[] + { + "Slate" + , "SlateCore" + , "EditorStyle" + , "UnrealEd" + , "MainFrame" + , "GameProjectGeneration" + , "WebBrowser" + , "RHI" + }); + } + + string AssemblyLocation = Path.GetDirectoryName(new Uri(System.Reflection.Assembly.GetExecutingAssembly().CodeBase).LocalPath); + DirectoryReference RootDirectory = new DirectoryReference(Path.Combine(AssemblyLocation, "..", "..", "..")); + bool IsEnginePlugin = RootDirectory.GetDirectoryName() == "Engine"; + PublicDefinitions.Add("TOBII_COMPILE_AS_ENGINE_PLUGIN=" + (IsEnginePlugin ? 1 : 0)); + + //Platform specific + if (IsEyetrackingActive && (Target.Platform == UnrealTargetPlatform.Win64 || Target.Platform == UnrealTargetPlatform.Win32)) + { + string PlatformString = (Target.Platform == UnrealTargetPlatform.Win64) ? "Win64" : "Win32"; + string PluginsPath = Path.Combine(ModuleDirectory, "../../../"); + string TobiiRelativeAPIPath = "TobiiEyetracking/ThirdParty/GameIntegration"; + string TobiiRelativeIncludePath = Path.Combine(TobiiRelativeAPIPath, "include"); + string TobiiRelativeLibraryBasePath = Path.Combine(TobiiRelativeAPIPath, "lib"); + + //Includes + PrivateIncludePaths.Add(Path.Combine(PluginsPath, TobiiRelativeIncludePath)); + + //Add libraries + AddLibrary(Path.Combine(PluginsPath, TobiiRelativeLibraryBasePath, PlatformString, (Target.Platform == UnrealTargetPlatform.Win32) ? "tobii_gameintegration_x86.lib" : "tobii_gameintegration_x64.lib")); + + //Add DLL + string RelativeGICDllPath = ""; + string GICDllName = (Target.Platform == UnrealTargetPlatform.Win32) ? "tobii_gameintegration_x86.dll" : "tobii_gameintegration_x64.dll"; + + if (IsEnginePlugin) + { + RelativeGICDllPath = Path.Combine("Binaries/ThirdParty/TobiiEyetracking", PlatformString, GICDllName); + RuntimeDependencies.Add("$(EngineDir)/" + RelativeGICDllPath); + } + else + { + RelativeGICDllPath = Path.Combine(TobiiRelativeLibraryBasePath, PlatformString, GICDllName); + RuntimeDependencies.Add(Path.Combine(PluginsPath, RelativeGICDllPath)); + } + + PublicDefinitions.Add("TOBII_EYETRACKING_ACTIVE=1"); + PublicDefinitions.Add("TOBII_GIC_RELATIVE_DLL_PATH=R\"(" + RelativeGICDllPath + ")\""); + PublicDelayLoadDLLs.Add(GICDllName); + } + else + { + PublicDefinitions.Add("TOBII_EYETRACKING_ACTIVE=0"); + } + } + + private void AddLibrary(string libraryPath) + { + if (File.Exists(libraryPath)) + { + PublicAdditionalLibraries.Add(libraryPath); + } + else + { + Debug.WriteLine("Cannot find Tobii API Lib file. Path does not exist! " + libraryPath); + } + } + } +} diff --git a/Source/TobiiGTOM/Private/GTOMAwareUI/STobiiGazeFocusableWidget.cpp b/Source/TobiiGTOM/Private/GTOMAwareUI/STobiiGazeFocusableWidget.cpp new file mode 100644 index 0000000..4a61523 --- /dev/null +++ b/Source/TobiiGTOM/Private/GTOMAwareUI/STobiiGazeFocusableWidget.cpp @@ -0,0 +1,161 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#include "STobiiGazeFocusableWidget.h" +#include "TobiiGTOMBlueprintLibrary.h" + +#include "Engine/Canvas.h" +#include "Engine/Engine.h" +#include "Components/WidgetComponent.h" +#include "HAL/IConsoleManager.h" +#include "Slate/SceneViewport.h" + + +static TAutoConsoleVariable CVarEnableCleanUI(TEXT("tobii.EnableCleanUI"), 1, TEXT("0 - CleanUI is disabled. 1 - CleanUI is enabled.")); +static TAutoConsoleVariable CVarCleanUIFadeInTimeSecs(TEXT("tobii.CleanUIFadeInTimeSecs"), 0.0f, TEXT("We want the fade in time to be fairly fast so that the information is instantly available to the user.")); +static TAutoConsoleVariable CVarCleanUIFadeOutTimeSecs(TEXT("tobii.CleanUIFadeOutTimeSecs"), 1.8f, TEXT("The fade out time should be fairly slow however to make sure that it doesn't draw peripheral vision attention.")); +static TAutoConsoleVariable CVarCleanUIMinAlpha(TEXT("tobii.CleanUIMinAlpha"), 0.3f, TEXT("The clean UI won't fade something out beyond this minimum alpha.")); +static TAutoConsoleVariable CVarCleanUIMaxAlpha(TEXT("tobii.CleanUIMaxAlpha"), 1.0f, TEXT("Change this if you don't want the clean UI to increase alpha beyond this point")); + +STobiiGazeFocusableWidget::STobiiGazeFocusableWidget() + : CleanUIAlpha(1.0f) + , CleanUIContainersToPollHitsFrom() +{ + bCanSupportFocus = false; + + SetCanTick(true); +} + +void STobiiGazeFocusableWidget::Construct(const FArguments& InArgs) +{ + SBox::FArguments ParentArgs; + ParentArgs._HAlign = InArgs._HAlign; + ParentArgs._VAlign = InArgs._VAlign; + ParentArgs._Padding = InArgs._Padding; + ParentArgs._Content = InArgs._Content; + SBox::Construct(ParentArgs); + + OnHovered = InArgs._OnHovered; + OnUnhovered = InArgs._OnUnhovered; +} + +bool STobiiGazeFocusableWidget::HitByGaze() +{ + return UMGWidget.IsValid() && (UMGWidget->CleanUIMode == ETobiiCleanUIMode::FocusExclusive ? + UMGWidget->HasFocus() : UMGWidget->IsInFocusCollection()); +} + +void STobiiGazeFocusableWidget::AddExtraCleanUIContainerToPollHitsFrom(STobiiGazeFocusableWidget* CleanUIContainerToPoll, bool PollGazeHits /*= true*/, bool PollMouseHits /*= true*/) +{ + if (CleanUIContainerToPoll != nullptr) + { + CleanUIPollingInfo PollingInfo; + PollingInfo.CleanUIToPollFrom = TWeakPtr(CleanUIContainerToPoll->AsShared()); + PollingInfo.PollGaze = PollGazeHits; + PollingInfo.PollPointer = PollMouseHits; + CleanUIContainersToPollHitsFrom.Add(PollingInfo); + } +} + +void STobiiGazeFocusableWidget::RemoveExtraCleanUIContainerToPollHitsFrom(STobiiGazeFocusableWidget* CleanUIContainerToStopPolling) +{ + if (CleanUIContainerToStopPolling != nullptr) + { + CleanUIContainersToPollHitsFrom.RemoveAll([=](CleanUIPollingInfo& CurCleanUIPollingInfo) + { + return !CurCleanUIPollingInfo.CleanUIToPollFrom.IsValid() || CurCleanUIPollingInfo.CleanUIToPollFrom.HasSameObject(CleanUIContainerToStopPolling); + }); + } +} + +void STobiiGazeFocusableWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) +{ + SWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); + + if (!UMGWidget.IsValid() + || UMGWidget->CleanUIMode == ETobiiCleanUIMode::Disabled + || GEngine == nullptr + || GEngine->GameViewport == nullptr + || GEngine->GameViewport->GetGameViewport() == nullptr + || GetChildren()->Num() == 0) + { + CleanUIAlpha = 1.0f; + return; + } + + bool bTriggerCleanUIOnMouseOver = UMGWidget.IsValid() && UMGWidget->bTriggerCleanUIOnMouseOver; + bool bHitByGaze = HitByGaze(); + bool bShouldFadeIn = bHitByGaze || (bTriggerCleanUIOnMouseOver && bIsHovered); + + // Check if we have any hits on our CleanUIContainersToPollHitsFrom to see if we should still fade in even though we still think we should fade out + if (!bShouldFadeIn) + { + for (CleanUIPollingInfo& PollingInfo : CleanUIContainersToPollHitsFrom) + { + if (PollingInfo.CleanUIToPollFrom.IsValid()) + { + TSharedPtr CleanUIWidget = PollingInfo.CleanUIToPollFrom.Pin(); + STobiiGazeFocusableWidget* CleanUIToPollFrom = (STobiiGazeFocusableWidget*)CleanUIWidget.Get(); + + if ((PollingInfo.PollGaze && CleanUIToPollFrom->HitByGaze()) + || PollingInfo.PollPointer && bTriggerCleanUIOnMouseOver && CleanUIToPollFrom->bIsHovered) + { + bShouldFadeIn = true; + break; + } + } + } + } + + UMGWidget->TimeCleanUIIsSuppressedForSecs = FMath::Max(UMGWidget->TimeCleanUIIsSuppressedForSecs -= InDeltaTime, 0.0f); + bool bCleanUISuppressed = UMGWidget->TimeCleanUIIsSuppressedForSecs > FLT_EPSILON; + bShouldFadeIn = bShouldFadeIn || bCleanUISuppressed; + + float MinAlpha = UMGWidget->CleanUIMinAlphaOverride >= 0.0f ? UMGWidget->CleanUIMinAlphaOverride : CVarCleanUIMinAlpha.GetValueOnAnyThread(); + float MaxAlpha = UMGWidget->CleanUIMaxAlphaOverride >= 0.0f ? UMGWidget->CleanUIMaxAlphaOverride : CVarCleanUIMaxAlpha.GetValueOnAnyThread(); + if (bShouldFadeIn) + { + float FadeInTime = UMGWidget->CleanUIFadeInTimeSecsOverride >= 0.0f ? UMGWidget->CleanUIFadeInTimeSecsOverride : CVarCleanUIFadeInTimeSecs.GetValueOnAnyThread(); + CleanUIAlpha = FadeInTime > 0.0f ? FMath::Clamp(CleanUIAlpha + InDeltaTime / FadeInTime, MinAlpha, MaxAlpha) : MaxAlpha; + } + else + { + float FadeOutTime = UMGWidget->CleanUIFadeOutTimeSecsOverride >= 0.0f ? UMGWidget->CleanUIFadeOutTimeSecsOverride : CVarCleanUIFadeOutTimeSecs.GetValueOnAnyThread(); + CleanUIAlpha = FadeOutTime > 0.0f ? FMath::Clamp(CleanUIAlpha - InDeltaTime / FadeOutTime, MinAlpha, MaxAlpha) : MinAlpha; + } +} + +int32 STobiiGazeFocusableWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const +{ + const float Alpha = UMGWidget.IsValid() ? (UMGWidget->CleanUIMode == ETobiiCleanUIMode::Silent ? 1.0f : CleanUIAlpha) : 1.0f; + FWidgetStyle CompoundedWidgetStyle = FWidgetStyle(InWidgetStyle).BlendColorAndOpacityTint(FLinearColor(1.0f, 1.0f, 1.0f, Alpha)); + return SBox::OnPaint(Args, AllottedGeometry, MyClippingRect, OutDrawElements, LayerId, CompoundedWidgetStyle, bParentEnabled); +} + +void STobiiGazeFocusableWidget::OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + SWidget::OnMouseEnter(MyGeometry, MouseEvent); + OnHovered.ExecuteIfBound(); + Invalidate(EInvalidateWidget::Layout); +} + +void STobiiGazeFocusableWidget::OnMouseLeave(const FPointerEvent& MouseEvent) +{ + // Call parent implementation + SWidget::OnMouseLeave(MouseEvent); + OnUnhovered.ExecuteIfBound(); + Invalidate(EInvalidateWidget::Layout); +} + +STobiiGazeFocusableWidget* STobiiGazeFocusableWidget::TryCastToTobiiGazeFocusable(SWidget* Widget) +{ + if (Widget != nullptr && Widget->GetType() == FName("STobiiGazeFocusableWidget")) + { + return (STobiiGazeFocusableWidget*)Widget; + } + + return nullptr; +} \ No newline at end of file diff --git a/Source/TobiiGTOM/Private/GTOMAwareUI/STobiiRadialMenuWidget.cpp b/Source/TobiiGTOM/Private/GTOMAwareUI/STobiiRadialMenuWidget.cpp new file mode 100644 index 0000000..8ccc5dd --- /dev/null +++ b/Source/TobiiGTOM/Private/GTOMAwareUI/STobiiRadialMenuWidget.cpp @@ -0,0 +1,318 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#include "STobiiRadialMenuWidget.h" +#include "TobiiGTOMBlueprintLibrary.h" + +#include "Engine/Canvas.h" +#include "Engine/Engine.h" +#include "Components/WidgetComponent.h" +#include "Framework/Application/SlateApplication.h" +#include "Slate/SceneViewport.h" + +#define TWO_PI (6.28318530718) +#define PI_OVER_TWO (1.57079632679) + +STobiiRadialMenuWidget::STobiiRadialMenuWidget() + : Children(this) +{ + SetCanTick(false); + bCanSupportFocus = false; +} + +void STobiiRadialMenuWidget::Construct(const STobiiRadialMenuWidget::FArguments& InArgs) +{ + const int32 NumSlots = InArgs.Slots.Num(); + for (int32 SlotIndex = 0; SlotIndex < NumSlots; ++SlotIndex) + { + Children.Add(InArgs.Slots[SlotIndex]); + } + + bUseHardBorder = InArgs._UseHardBorder; + BorderColor = InArgs._BorderColor; + PanelColor = InArgs._PanelColor; + BorderThicknessPx = InArgs._BorderThicknessPx; + SegmentSeparationPx = InArgs._SegmentSeparationPx; + AngularDisplacementDeg = InArgs._AngularDisplacementDeg; + RadiusPx = InArgs._RadiusPx; + VertexCount = InArgs._VertexCount; + + BrushResourceHandle = FSlateApplication::Get().GetRenderer()->GetResourceHandle(*FCoreStyle::Get().GetDefaultBrush()); +} + +void STobiiRadialMenuWidget::ClearChildren() +{ + if (Children.Num()) + { + Invalidate(EInvalidateWidget::Layout); + Children.Empty(); + } +} + +int32 STobiiRadialMenuWidget::RemoveSlot(const TSharedRef& SlotWidget) +{ + Invalidate(EInvalidateWidget::Layout); + + for (int32 SlotIdx = 0; SlotIdx < Children.Num(); ++SlotIdx) + { + if (SlotWidget == Children[SlotIdx].GetWidget()) + { + Children.RemoveAt(SlotIdx); + return SlotIdx; + } + } + + return -1; +} + +void STobiiRadialMenuWidget::GeneratePanelRenderData(const FGeometry& AllottedGeometry, int32 NrChildren, TArray& OutChildRenderData) const +{ + OutChildRenderData.Empty(NrChildren); + + const FVector2D WidgetCenter = AllottedGeometry.GetLocalSize() / 2.0f; + const int32 VerticesPerSegment = FMath::Max(VertexCount / Children.Num(), 3); + const float ActualRadius = FMath::Min(WidgetCenter.X, WidgetCenter.Y); + const float AngleStep = TWO_PI / Children.Num(); + const float AngleHalfStep = AngleStep / 2.0f; + const float AngleMinorStep = AngleStep / VerticesPerSegment; + const float HalfSeparation = SegmentSeparationPx / 2.0f; + const float BorderCenterOffsetDist = HalfSeparation / FMath::Sin(AngleHalfStep); + const float BorderSeparationAngle = FMath::Asin(HalfSeparation / ActualRadius); + + const float NoBorderDistOffset = HalfSeparation + BorderThicknessPx; + const float NoBorderRadius = ActualRadius - BorderThicknessPx; + const float NoBorderCenterOffsetDist = NoBorderDistOffset / FMath::Sin(AngleHalfStep); + const float NoBorderSeparationAngle = FMath::Asin(NoBorderDistOffset / NoBorderRadius); + + float CurrentChildAngle = PI_OVER_TWO + FMath::DegreesToRadians(AngularDisplacementDeg); //Start at the top + + for (int32 ChildIndex = 0; ChildIndex < NrChildren; ++ChildIndex) + { + FTobiiRadialMenuPanelRenderData NewRenderData; + + const STobiiRadialMenuWidget::FSlot& CurChild = Children[ChildIndex]; + const float ChildAlpha = CurChild.AlphaAttr.Get(); + + FColor AdjustedBorderColor = BorderColor; + AdjustedBorderColor.A *= ChildAlpha; + FColor AdjustedPanelColor = PanelColor; + AdjustedPanelColor.A *= ChildAlpha; + + //Panel polygon + { + float ChildSine, ChildCosine; + FMath::SinCos(&ChildSine, &ChildCosine, -CurrentChildAngle); + FVector2D WidgetCenterToChildCenterDir(ChildCosine, ChildSine); + + //First generate the nexus point + const FVector2D CenterVertexPosition = WidgetCenter + WidgetCenterToChildCenterDir * NoBorderCenterOffsetDist; + FSlateVertex CenterVertex = FSlateVertex::Make(AllottedGeometry.GetAccumulatedRenderTransform(), CenterVertexPosition, FVector2D::ZeroVector, AdjustedPanelColor); + NewRenderData.PanelVertexData.Add(CenterVertex); + + //Next we need to find our segment extreme points + const float SegmentStartVertexAngle = CurrentChildAngle - AngleHalfStep + NoBorderSeparationAngle; + const float SegmentEndVertexAngle = CurrentChildAngle + AngleHalfStep - NoBorderSeparationAngle; + + //Generate all edge vertices + for (int32 VertexIdx = 0; VertexIdx < VerticesPerSegment; VertexIdx++) + { + const float AngleAlpha = (float)VertexIdx / (VerticesPerSegment - 1); + const float CurrentAngle = FMath::Lerp(SegmentStartVertexAngle, SegmentEndVertexAngle, AngleAlpha); + float VertexSine, VertexCosine; + FMath::SinCos(&VertexSine, &VertexCosine, -CurrentAngle); + + const FVector2D SegmentVertexPosition = WidgetCenter + FVector2D(NoBorderRadius * VertexCosine, NoBorderRadius * VertexSine); + FSlateVertex SegmentVertex = FSlateVertex::Make(AllottedGeometry.GetAccumulatedRenderTransform(), SegmentVertexPosition, FVector2D::ZeroVector, AdjustedPanelColor); + NewRenderData.PanelVertexData.Add(SegmentVertex); + } + + //Build indices + for (int32 VertexIdx = 1; VertexIdx < VerticesPerSegment; VertexIdx++) + { + NewRenderData.PanelIndexData.Add(0); + NewRenderData.PanelIndexData.Add(VertexIdx); + NewRenderData.PanelIndexData.Add(VertexIdx + 1); + } + } + + //Border polygon + { + float ChildSine, ChildCosine; + FMath::SinCos(&ChildSine, &ChildCosine, -CurrentChildAngle); + FVector2D WidgetCenterToChildCenterDir(ChildCosine, ChildSine); + + //First generate the nexus point + const FVector2D CenterVertexPosition = WidgetCenter + WidgetCenterToChildCenterDir * BorderCenterOffsetDist; + FSlateVertex CenterVertex = FSlateVertex::Make(AllottedGeometry.GetAccumulatedRenderTransform(), CenterVertexPosition, FVector2D::ZeroVector, AdjustedBorderColor); + NewRenderData.BorderVertexData.Add(CenterVertex); + + //Next we need to find our segment extreme points + const float SegmentStartVertexAngle = CurrentChildAngle - AngleHalfStep + BorderSeparationAngle; + const float SegmentEndVertexAngle = CurrentChildAngle + AngleHalfStep - BorderSeparationAngle; + + //Generate all edge vertices + for (int32 VertexIdx = 0; VertexIdx < VerticesPerSegment; VertexIdx++) + { + const float AngleAlpha = (float)VertexIdx / (VerticesPerSegment - 1); + const float CurrentAngle = FMath::Lerp(SegmentStartVertexAngle, SegmentEndVertexAngle, AngleAlpha); + float VertexSine, VertexCosine; + FMath::SinCos(&VertexSine, &VertexCosine, -CurrentAngle); + + const FVector2D SegmentVertexPosition = WidgetCenter + FVector2D(ActualRadius * VertexCosine, ActualRadius * VertexSine); + + FSlateVertex SegmentVertex = FSlateVertex::Make(AllottedGeometry.GetAccumulatedRenderTransform(), SegmentVertexPosition, FVector2D::ZeroVector, AdjustedBorderColor); + NewRenderData.BorderVertexData.Add(SegmentVertex); + } + + //Add the panel vertices at the end of the array + const int32 FirstPanelVertexIdx = NewRenderData.BorderVertexData.Num(); + for (FSlateVertex SlateVertex : NewRenderData.PanelVertexData) + { + if (bUseHardBorder) + { + SlateVertex.Color = AdjustedBorderColor; + } + + NewRenderData.BorderVertexData.Add(SlateVertex); + } + + //Build indices, building quads from the inner panel outwards + for (int32 VertexIdx = 0; VertexIdx < VerticesPerSegment; VertexIdx++) + { + //First tri + NewRenderData.BorderIndexData.Add(FirstPanelVertexIdx + VertexIdx); + NewRenderData.BorderIndexData.Add(VertexIdx); + NewRenderData.BorderIndexData.Add(VertexIdx + 1); + + //Second tri + NewRenderData.BorderIndexData.Add(FirstPanelVertexIdx + VertexIdx); + NewRenderData.BorderIndexData.Add(VertexIdx + 1); + NewRenderData.BorderIndexData.Add(FirstPanelVertexIdx + VertexIdx + 1); + } + + //Final two tris + NewRenderData.BorderIndexData.Add(NewRenderData.BorderVertexData.Num() - 1); + NewRenderData.BorderIndexData.Add(FirstPanelVertexIdx - 1); + NewRenderData.BorderIndexData.Add(0); + NewRenderData.BorderIndexData.Add(NewRenderData.BorderVertexData.Num() - 1); + NewRenderData.BorderIndexData.Add(0); + NewRenderData.BorderIndexData.Add(FirstPanelVertexIdx); + } + + OutChildRenderData.Add(MoveTemp(NewRenderData)); + CurrentChildAngle += AngleStep; + } +} + +void STobiiRadialMenuWidget::OnArrangeChildren(const FGeometry& AllottedGeometry, FArrangedChildren& ArrangedChildren) const +{ + if (Children.Num() > 0) + { + const FVector2D WidgetCenter = AllottedGeometry.GetLocalSize() / 2.0f; + const float ActualRadius = FMath::Min(WidgetCenter.X, WidgetCenter.Y); + const float AngleStep = TWO_PI / Children.Num(); + + FVector2D LargestInscribedRect; + float DistanceToRectCenter; + UTobiiGTOMBlueprintLibrary::FindLargestInscribedAlignedRect(AngleStep, ActualRadius, LargestInscribedRect, DistanceToRectCenter); + const float ChildElementSideLength = FMath::Min(LargestInscribedRect.X, LargestInscribedRect.Y); + const float SlightlyScaledDownSize = ChildElementSideLength * 0.8f; + + float CurrentAngle = PI_OVER_TWO + FMath::DegreesToRadians(AngularDisplacementDeg); //Start at the top + for (int32 ChildIndex = 0; ChildIndex < Children.Num(); ++ChildIndex) + { + const STobiiRadialMenuWidget::FSlot& CurChild = Children[ChildIndex]; + const TSharedRef& CurWidget = CurChild.GetWidget(); + + const EVisibility ChildVisibility = CurWidget->GetVisibility(); + if (ArrangedChildren.Accepts(ChildVisibility)) + { + const FVector2D Offset = CurChild.OffsetAttr.Get(); + float ChildSine, ChildCosine; + FMath::SinCos(&ChildSine, &ChildCosine, -CurrentAngle); + const FVector2D ChildCenter(DistanceToRectCenter * ChildCosine, DistanceToRectCenter * ChildSine); + + const float ChildScale = CurChild.ScaleAttr.Get(); + const FVector2D ChildSize(SlightlyScaledDownSize * ChildScale, SlightlyScaledDownSize * ChildScale); + const FVector2D BaseChildOffset = -ChildSize / 2.0f; + const FVector2D LocalPosition = WidgetCenter + ChildCenter + BaseChildOffset + Offset; + CurrentAngle += AngleStep; + + ArrangedChildren.AddWidget(ChildVisibility, AllottedGeometry.MakeChild(CurWidget, LocalPosition, ChildSize)); + } + } + } +} + +int32 STobiiRadialMenuWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const +{ + SCOPED_NAMED_EVENT_TEXT("SConstraintCanvas", FColor::Orange); + + if (Children.Num() < 2) + { + return LayerId; + } + + FArrangedChildren ArrangedChildren(EVisibility::Visible); + OnArrangeChildren(AllottedGeometry, ArrangedChildren); + + TArray ChildRenderData; + GeneratePanelRenderData(AllottedGeometry, Children.Num(), ChildRenderData); + + const bool bForwardedEnabled = ShouldBeEnabled(bParentEnabled); + const float AngleStep = TWO_PI / Children.Num(); + + float CurrentChildAngle = PI_OVER_TWO + FMath::DegreesToRadians(AngularDisplacementDeg); //Start at the top + + // Because we paint multiple children, we must track the maximum layer id that they produced in case one of our parents + // wants to an overlay for all of its contents. + int32 MaxLayerId = LayerId; + const FPaintArgs NewArgs = Args.WithNewParent(this); + for (int32 ChildIndex = 0; ChildIndex < ArrangedChildren.Num(); ++ChildIndex) + { + FArrangedWidget& CurWidget = ArrangedChildren[ChildIndex]; + if (!IsChildWidgetCulled(MyCullingRect, CurWidget)) + { + MaxLayerId++; + + //Draw underlying panel if our render data is valid + if (ChildIndex < ChildRenderData.Num()) + { + const FTobiiRadialMenuPanelRenderData& CurrentRenderData = ChildRenderData[ChildIndex]; + + FSlateDrawElement::MakeCustomVerts(OutDrawElements, LayerId + , BrushResourceHandle + , CurrentRenderData.PanelVertexData + , CurrentRenderData.PanelIndexData + , nullptr, 0, 0, ESlateDrawEffect::NoPixelSnapping); + + FSlateDrawElement::MakeCustomVerts(OutDrawElements, LayerId + , BrushResourceHandle + , CurrentRenderData.BorderVertexData + , CurrentRenderData.BorderIndexData + , nullptr, 0, 0, ESlateDrawEffect::NoPixelSnapping); + } + + const STobiiRadialMenuWidget::FSlot& CurChild = Children[ChildIndex]; + FWidgetStyle CompoundedWidgetStyle = FWidgetStyle(InWidgetStyle).BlendColorAndOpacityTint(FLinearColor(1.0f, 1.0f, 1.0f, CurChild.AlphaAttr.Get())); + CurWidget.Widget->Paint(NewArgs, CurWidget.Geometry, MyCullingRect, OutDrawElements, MaxLayerId, CompoundedWidgetStyle, bForwardedEnabled); + CurrentChildAngle += AngleStep; + } + } + + return MaxLayerId; +} + +FVector2D STobiiRadialMenuWidget::ComputeDesiredSize(float LayoutScaleMultiplier) const +{ + const float DiameterPx = RadiusPx * 2.0f; + return FVector2D(DiameterPx, DiameterPx); +} + +FChildren* STobiiRadialMenuWidget::GetChildren() +{ + return &Children; +} diff --git a/Source/TobiiGTOM/Private/GTOMAwareUI/TobiiGazeFocusableWidget.cpp b/Source/TobiiGTOM/Private/GTOMAwareUI/TobiiGazeFocusableWidget.cpp new file mode 100644 index 0000000..abc4b9a --- /dev/null +++ b/Source/TobiiGTOM/Private/GTOMAwareUI/TobiiGazeFocusableWidget.cpp @@ -0,0 +1,223 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#include "TobiiGazeFocusableWidget.h" +#include "TobiiGTOMBlueprintLibrary.h" + +#include "Components/SizeBoxSlot.h" + +#define LOCTEXT_NAMESPACE "UMG" + +static TMap> GRegisteredTobiiScreenSpaceFocusableWidgets; + +UTobiiGazeFocusableWidget::UTobiiGazeFocusableWidget(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + + , OnHovered() + , OnUnhovered() + + , bHasGazeFocus(false) + , bIsGazeFocusable(true) + , GazeFocusPriority(0.0f) + , MaxFocusDistance(-1.0f) + , FocusLayer("") + , ReceivedGazeFocus() + , LostGazeFocus() + + , CleanUIMode(ETobiiCleanUIMode::Disabled) + , TimeCleanUIIsSuppressedForSecs(0.0f) + , CleanUIFadeInTimeSecsOverride(-1.0f) + , CleanUIFadeOutTimeSecsOverride(-1.0f) + , CleanUIMinAlphaOverride(-1.0f) + , CleanUIMaxAlphaOverride(-1.0f) + , bTriggerCleanUIOnMouseOver(false) + + , WorldSpaceHostWidgetComponent() + , MySlateWidget() +{ + Visibility = ESlateVisibility::Visible; + bIsVariable = true; +} + +void UTobiiGazeFocusableWidget::ReleaseSlateResources(bool bReleaseChildren) +{ + Super::ReleaseSlateResources(bReleaseChildren); + + MySlateWidget.Reset(); +} + +TSharedRef UTobiiGazeFocusableWidget::RebuildWidget() +{ + MySizeBox = MySlateWidget = SNew(STobiiGazeFocusableWidget) + .OnHovered_UObject(this, &ThisClass::SlateHandleHovered) + .OnUnhovered_UObject(this, &ThisClass::SlateHandleUnhovered); + + MySlateWidget->UMGWidget = this; + + if (GetChildrenCount() > 0) + { + Cast(GetContentSlot())->BuildSlot(MySizeBox.ToSharedRef()); + } + + return MySizeBox.ToSharedRef(); +} + +bool UTobiiGazeFocusableWidget::IsHoveredByPointer() +{ + if (MySlateWidget.IsValid()) + { + return MySlateWidget->IsHoveredByPointer(); + } + + return false; +} + +void UTobiiGazeFocusableWidget::RegisterWidgetToGTOM(UWidgetComponent* HostWidget) +{ + if (HostWidget != nullptr) + { + WorldSpaceHostWidgetComponent = HostWidget; + } + else + { + uint32 Uid = GetUniqueID(); + if (GRegisteredTobiiScreenSpaceFocusableWidgets.Contains(Uid)) + { + //Overwrite + GRegisteredTobiiScreenSpaceFocusableWidgets[Uid] = this; + } + else + { + GRegisteredTobiiScreenSpaceFocusableWidgets.Add(Uid, this); + } + } +} + +bool UTobiiGazeFocusableWidget::IsWorldSpaceWidget() +{ + return WorldSpaceHostWidgetComponent.IsValid(); +} + +UWidgetComponent* UTobiiGazeFocusableWidget::GetWorldSpaceHostWidgetComponent() +{ + return WorldSpaceHostWidgetComponent.IsValid() ? WorldSpaceHostWidgetComponent.Get() : nullptr; +} + +bool UTobiiGazeFocusableWidget::HasFocus() +{ + FTobiiGazeFocusData FocusData; + return UTobiiGTOMBlueprintLibrary::GetGazeFocusData(FocusData) && FocusData.FocusedWidget == this; +} + +bool UTobiiGazeFocusableWidget::IsInFocusCollection() +{ + TArray AllFocusData; + if (UTobiiGTOMBlueprintLibrary::GetAllGazeFocusData(AllFocusData)) + { + for (const FTobiiGazeFocusData& FocusData : AllFocusData) + { + if (FocusData.FocusedWidget == this) + { + return true; + } + } + } + + return false; +} + +float UTobiiGazeFocusableWidget::GetCleanUIAlpha() +{ + if (MySlateWidget.IsValid()) + { + return MySlateWidget->GetCleanUIAlpha(); + } + + return 1.0f; +} + +void UTobiiGazeFocusableWidget::AddGazeFocusableWidgetToPollHitsFrom(UTobiiGazeFocusableWidget* GazeFocusableWidgetToPoll, bool PollGazeHits /*= true*/, bool PollMouseHits /*= true*/) +{ + if (GazeFocusableWidgetToPoll != nullptr) + { + STobiiGazeFocusableWidget* SlateCleanUIContainerToPoll = GazeFocusableWidgetToPoll->GetSlateGazeFocusableWidget(); + if (SlateCleanUIContainerToPoll != nullptr && MySlateWidget.IsValid()) + { + MySlateWidget->AddExtraCleanUIContainerToPollHitsFrom(SlateCleanUIContainerToPoll, PollGazeHits, PollMouseHits); + } + } +} + +void UTobiiGazeFocusableWidget::RemoveGazeFocusableWidgetToPollHitsFrom(UTobiiGazeFocusableWidget* GazeFocusableWidgetToStopPolling) +{ + if (GazeFocusableWidgetToStopPolling != nullptr) + { + STobiiGazeFocusableWidget* SlateCleanUIContainerToStopPolling = GazeFocusableWidgetToStopPolling->GetSlateGazeFocusableWidget(); + if (SlateCleanUIContainerToStopPolling != nullptr && MySlateWidget.IsValid()) + { + MySlateWidget->RemoveExtraCleanUIContainerToPollHitsFrom(SlateCleanUIContainerToStopPolling); + } + } +} + +void UTobiiGazeFocusableWidget::ReceiveFocus() +{ + ReceivedGazeFocus.Broadcast(this); +} + +void UTobiiGazeFocusableWidget::LoseFocus() +{ + LostGazeFocus.Broadcast(this); +} + +void UTobiiGazeFocusableWidget::SlateHandleHovered() +{ + OnHovered.Broadcast(this); +} + +void UTobiiGazeFocusableWidget::SlateHandleUnhovered() +{ + OnUnhovered.Broadcast(this); +} + +STobiiGazeFocusableWidget* UTobiiGazeFocusableWidget::GetSlateGazeFocusableWidget() +{ + if (MySlateWidget.IsValid()) + { + return MySlateWidget.Get(); + } + + return nullptr; +} + +TMap>& UTobiiGazeFocusableWidget::GetTobiiScreenSpaceFocusableWidgets() +{ + TArray IdsToPrune; + for (auto& WidgetPair : GRegisteredTobiiScreenSpaceFocusableWidgets) + { + if (!WidgetPair.Value.IsValid()) + { + IdsToPrune.Add(WidgetPair.Key); + } + } + for (FTobiiFocusableUID IdToPrune : IdsToPrune) + { + GRegisteredTobiiScreenSpaceFocusableWidgets.Remove(IdToPrune); + } + + return GRegisteredTobiiScreenSpaceFocusableWidgets; +} + +#if WITH_EDITOR +const FText UTobiiGazeFocusableWidget::GetPaletteCategory() +{ + return LOCTEXT("Tobii", "Tobii"); +} +#endif + +/////////////////////////////////////////////////// + +#undef LOCTEXT_NAMESPACE diff --git a/Source/TobiiGTOM/Private/GTOMAwareUI/TobiiRadialMenuWidget.cpp b/Source/TobiiGTOM/Private/GTOMAwareUI/TobiiRadialMenuWidget.cpp new file mode 100644 index 0000000..8616529 --- /dev/null +++ b/Source/TobiiGTOM/Private/GTOMAwareUI/TobiiRadialMenuWidget.cpp @@ -0,0 +1,334 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#include "TobiiRadialMenuWidget.h" +#include "TobiiGazeFocusableWidget.h" +#include "STobiiGazeFocusableWidget.h" + +#define LOCTEXT_NAMESPACE "UMG" + +UTobiiRadialMenuSlot::UTobiiRadialMenuSlot(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , Offset(0.0f, 0.0f) + , Scale(1.0f) + , Alpha(1.0f) + + , Slot(nullptr) +{ +} + +void UTobiiRadialMenuSlot::ReleaseSlateResources(bool bReleaseChildren) +{ + Super::ReleaseSlateResources(bReleaseChildren); + Slot = nullptr; +} + +void UTobiiRadialMenuSlot::BuildSlot(TSharedRef RadialMenu) +{ + Slot = &RadialMenu->AddSlot() + [ + Content == nullptr ? SNullWidget::NullWidget : Content->TakeWidget() + ]; + + SynchronizeProperties(); +} + +#if WITH_EDITOR + +bool UTobiiRadialMenuSlot::NudgeByDesigner(const FVector2D& NudgeDirection, const TOptional& GridSnapSize) +{ + return false; +} + +bool UTobiiRadialMenuSlot::DragDropPreviewByDesigner(const FVector2D& LocalCursorPosition, const TOptional& XGridSnapSize, const TOptional& YGridSnapSize) +{ + return false; +} + +void UTobiiRadialMenuSlot::SynchronizeFromTemplate(const UPanelSlot* const TemplateSlot) +{ +} + +#endif //WITH_EDITOR + +void UTobiiRadialMenuSlot::SetOffset(FVector2D InOffset) +{ + Offset = InOffset; + if (Slot) + { + Slot->Offset(InOffset); + } +} +FVector2D UTobiiRadialMenuSlot::GetOffset() const +{ + if (Slot) + { + return Slot->OffsetAttr.Get(); + } + + return FVector2D::ZeroVector; +} + +void UTobiiRadialMenuSlot::SetScale(float InScale) +{ + Scale = InScale; + if (Slot) + { + Slot->Scale(InScale); + } +} +float UTobiiRadialMenuSlot::GetScale() const +{ + if (Slot) + { + return Slot->ScaleAttr.Get(); + } + + return 1.0f; +} + +void UTobiiRadialMenuSlot::SetAlpha(float InAlpha) +{ + Alpha = InAlpha; + if (Slot) + { + Slot->Alpha(InAlpha); + } +} +float UTobiiRadialMenuSlot::GetAlpha() const +{ + if (Slot) + { + return Slot->AlphaAttr.Get(); + } + + return 1.0f; +} + +void UTobiiRadialMenuSlot::SynchronizeProperties() +{ + SetOffset(Offset); + SetScale(Scale); + SetAlpha(Alpha); +} + +#if WITH_EDITOR + +void UTobiiRadialMenuSlot::PreEditChange(class FEditPropertyChain& PropertyAboutToChange) +{ + Super::PreEditChange(PropertyAboutToChange); +} +void UTobiiRadialMenuSlot::PostEditChangeChainProperty(struct FPropertyChangedChainEvent& PropertyChangedEvent) +{ + SynchronizeProperties(); + Super::PostEditChangeProperty(PropertyChangedEvent); +} + +#endif + +/////////////////////////////////////////////////////////////////////////////////////////////////////////// + +UTobiiRadialMenuWidget::UTobiiRadialMenuWidget(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , bUseHardBorder(true) + , BorderColor(FColor(230, 230, 230)) + , PanelColor(FColor(70, 70, 70)) + , BorderThicknessPx(5.0f) + , SegmentSeparationPx(10.0f) + , AngularDisplacementDeg(0.0f) + , RadiusPx(500.0f) + , VertexCount(360) +{ + bIsVariable = false; + + STobiiRadialMenuWidget::FArguments Defaults; + Visibility = UWidget::ConvertRuntimeToSerializedVisibility(Defaults._Visibility.Get()); +} + +void UTobiiRadialMenuWidget::ReleaseSlateResources(bool bReleaseChildren) +{ + Super::ReleaseSlateResources(bReleaseChildren); + MyRadialMenu.Reset(); +} + +TSharedRef UTobiiRadialMenuWidget::RebuildWidget() +{ + MyRadialMenu = SNew(STobiiRadialMenuWidget) + .UseHardBorder(bUseHardBorder) + .BorderColor(BorderColor) + .PanelColor(PanelColor) + .BorderThicknessPx(BorderThicknessPx) + .SegmentSeparationPx(SegmentSeparationPx) + .AngularDisplacementDeg(AngularDisplacementDeg) + .RadiusPx(RadiusPx) + .VertexCount(VertexCount); + + for (UPanelSlot* PanelSlot : Slots) + { + if (UTobiiRadialMenuSlot* TypedSlot = Cast(PanelSlot)) + { + TypedSlot->Parent = this; + TypedSlot->BuildSlot(MyRadialMenu.ToSharedRef()); + + UTobiiGazeFocusableWidget* GazeFocusableWidget = Cast(TypedSlot->Content); + if (GazeFocusableWidget != nullptr) + { + STobiiGazeFocusableWidget* SlateWidget = GazeFocusableWidget->GetSlateGazeFocusableWidget(); + //SlateWidget->ParentRadialMenuSlot = TypedSlot->GetSlateSlot(); + } + } + } + + return MyRadialMenu.ToSharedRef(); +} + +UClass* UTobiiRadialMenuWidget::GetSlotClass() const +{ + return UTobiiRadialMenuSlot::StaticClass(); +} + +void UTobiiRadialMenuWidget::SetUseHardBorder(bool bNewUseHardBorder) +{ + bUseHardBorder = bNewUseHardBorder; + if (MyRadialMenu.IsValid()) + { + MyRadialMenu->bUseHardBorder = bUseHardBorder; + } +} + +void UTobiiRadialMenuWidget::SetBorderColor(FColor NewBorderColor) +{ + BorderColor = NewBorderColor; + if (MyRadialMenu.IsValid()) + { + MyRadialMenu->BorderColor = BorderColor; + } +} + +void UTobiiRadialMenuWidget::SetPanelColor(FColor NewPanelColor) +{ + PanelColor = NewPanelColor; + if (MyRadialMenu.IsValid()) + { + MyRadialMenu->PanelColor = PanelColor; + } +} + +void UTobiiRadialMenuWidget::SetBorderThicknessPx(float NewBorderThicknessPx) +{ + BorderThicknessPx = NewBorderThicknessPx; + if (MyRadialMenu.IsValid()) + { + MyRadialMenu->BorderThicknessPx = BorderThicknessPx; + } +} + +void UTobiiRadialMenuWidget::SetSegmentSeparationPx(float NewSegmentSeparationPx) +{ + SegmentSeparationPx = NewSegmentSeparationPx; + if (MyRadialMenu.IsValid()) + { + MyRadialMenu->SegmentSeparationPx = SegmentSeparationPx; + } +} + +void UTobiiRadialMenuWidget::SetAngularDisplacementDeg(float NewAngularDisplacementDeg) +{ + AngularDisplacementDeg = NewAngularDisplacementDeg; + if (MyRadialMenu.IsValid()) + { + MyRadialMenu->AngularDisplacementDeg = AngularDisplacementDeg; + } +} + +void UTobiiRadialMenuWidget::SetRadiusPx(float NewRadiusPx) +{ + RadiusPx = NewRadiusPx; + if (MyRadialMenu.IsValid()) + { + MyRadialMenu->RadiusPx = RadiusPx; + } +} + +void UTobiiRadialMenuWidget::SetVertexCount(int32 NewVertexCount) +{ + VertexCount = NewVertexCount; + if (MyRadialMenu.IsValid()) + { + MyRadialMenu->VertexCount = VertexCount; + } +} + +void UTobiiRadialMenuWidget::OnSlotAdded(UPanelSlot* InSlot) +{ + // Add the child to the live canvas if it already exists + if (MyRadialMenu.IsValid()) + { + CastChecked(InSlot)->BuildSlot(MyRadialMenu.ToSharedRef()); + } +} + +void UTobiiRadialMenuWidget::OnSlotRemoved(UPanelSlot* InSlot) +{ + // Remove the widget from the live slot if it exists. + if (MyRadialMenu.IsValid()) + { + TSharedPtr Widget = InSlot->Content->GetCachedWidget(); + if (Widget.IsValid()) + { + MyRadialMenu->RemoveSlot(Widget.ToSharedRef()); + } + } +} + +UTobiiRadialMenuSlot* UTobiiRadialMenuWidget::AddRadialMenuChild(UWidget* Content) +{ + return Cast(Super::AddChild(Content)); +} + +bool UTobiiRadialMenuWidget::GetGeometryForSlot(int32 SlotIndex, FGeometry& ArrangedGeometry) const +{ + UTobiiRadialMenuSlot* PanelSlot = CastChecked(Slots[SlotIndex]); + return GetGeometryForSlot(PanelSlot, ArrangedGeometry); +} + +bool UTobiiRadialMenuWidget::GetGeometryForSlot(UTobiiRadialMenuSlot* InSlot, FGeometry& ArrangedGeometry) const +{ + if (InSlot->Content == nullptr) + { + return false; + } + + if (MyRadialMenu.IsValid()) + { + FArrangedChildren ArrangedChildren(EVisibility::All); + MyRadialMenu->ArrangeChildren(MyRadialMenu->GetCachedGeometry(), ArrangedChildren); + + for (int32 ChildIndex = 0; ChildIndex < ArrangedChildren.Num(); ChildIndex++) + { + if (ArrangedChildren[ChildIndex].Widget == InSlot->Content->GetCachedWidget()) + { + ArrangedGeometry = ArrangedChildren[ChildIndex].Geometry; + return true; + } + } + } + + return false; +} + +#if WITH_EDITOR + +const FText UTobiiRadialMenuWidget::GetPaletteCategory() +{ + return LOCTEXT("Tobii", "Tobii"); +} + +#endif + +///////////////////////////////////////////////////// + +#undef LOCTEXT_NAMESPACE diff --git a/Source/TobiiGTOM/Private/TobiiGTOMBlueprintLibrary.cpp b/Source/TobiiGTOM/Private/TobiiGTOMBlueprintLibrary.cpp new file mode 100644 index 0000000..97c659d --- /dev/null +++ b/Source/TobiiGTOM/Private/TobiiGTOMBlueprintLibrary.cpp @@ -0,0 +1,508 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#include "TobiiGTOMBlueprintLibrary.h" +#include "TobiiGTOMModule.h" +#include "TobiiGazeFocusableComponent.h" +#include "TobiiGTOMInternalTypes.h" +#include "TobiiGTOMEngine.h" + +#include "IEyeTracker.h" +#include "Engine/Engine.h" +#include "Engine/LocalPlayer.h" +#include "GameFramework/PlayerController.h" +#include "Misc/ConfigCacheIni.h" +#include "Misc/Paths.h" +#include "Slate/SceneViewport.h" + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#define TWO_PI (6.28318530718) + +UTobiiGTOMBlueprintLibrary::UTobiiGTOMBlueprintLibrary(const class FObjectInitializer& ObjectInitializer) +: Super(ObjectInitializer) +{ +} + +void UTobiiGTOMBlueprintLibrary::SetGTOMPlayerController(APlayerController* NewGTOMPlayerController) +{ + if (FTobiiGTOMModule::IsAvailable()) + { + if (FTobiiGTOMModule::Get().GTOMInputDevice.IsValid()) + { + FTobiiGTOMModule::Get().GTOMInputDevice->GTOMPlayerController = NewGTOMPlayerController; + } + if (GEngine && GEngine->EyeTrackingDevice.IsValid()) + { + GEngine->EyeTrackingDevice->SetEyeTrackedPlayer(NewGTOMPlayerController); + } + } +} + +bool UTobiiGTOMBlueprintLibrary::GetGazeFocusData(FTobiiGazeFocusData& OutFocusData) +{ + if (FTobiiGTOMModule::IsAvailable() && FTobiiGTOMModule::Get().GTOMInputDevice.IsValid()) + { + const TArray& FocusData = FTobiiGTOMModule::Get().GTOMInputDevice->GetFocusData(); + if (FocusData.Num() > 0) + { + OutFocusData = FocusData[0]; + return true; + } + } + + OutFocusData.FocusedActor = nullptr; + OutFocusData.FocusedPrimitiveComponent = nullptr; + OutFocusData.FocusedWidget = nullptr; + return false; +} + +bool UTobiiGTOMBlueprintLibrary::GetAllGazeFocusData(TArray& OutFocusData) +{ + OutFocusData.Empty(); + + if (FTobiiGTOMModule::IsAvailable() && FTobiiGTOMModule::Get().GTOMInputDevice.IsValid()) + { + OutFocusData = FTobiiGTOMModule::Get().GTOMInputDevice->GetFocusData(); + return OutFocusData.Num() > 0; + } + + return false; +} + +bool UTobiiGTOMBlueprintLibrary::GetFilteredGazeFocusData(const TArray& FocusLayerFilterList, const bool bIsWhiteList, const bool bWantPrimitives, const bool bWantWidgets, FTobiiGazeFocusData& OutFocusData) +{ + if (FTobiiGTOMModule::IsAvailable() && FTobiiGTOMModule::Get().GTOMInputDevice.IsValid()) + { + const TArray& AllFocusData = FTobiiGTOMModule::Get().GTOMInputDevice->GetFocusData(); + for (const FTobiiGazeFocusData& FocusData : AllFocusData) + { + if (bWantWidgets && FocusData.FocusedWidget.IsValid()) + { + if (FTobiiGTOMUtils::ValidateFocusLayers(FocusLayerFilterList, bIsWhiteList, FocusData.FocusedWidget.Get())) + { + OutFocusData = FocusData; + return true; + } + } + else if(bWantPrimitives && FocusData.FocusedPrimitiveComponent.IsValid() && !FocusData.FocusedWidget.IsValid()) + { + if (FTobiiGTOMUtils::ValidateFocusLayers(FocusLayerFilterList, bIsWhiteList, FocusData.FocusedPrimitiveComponent.Get())) + { + OutFocusData = FocusData; + return true; + } + } + } + } + + OutFocusData.FocusedActor = nullptr; + OutFocusData.FocusedPrimitiveComponent = nullptr; + OutFocusData.FocusedWidget = nullptr; + return false; +} + +bool UTobiiGTOMBlueprintLibrary::GetAllFilteredGazeFocusData(const TArray& FocusLayerFilterList, const bool bIsWhiteList, const bool bWantPrimitives, const bool bWantWidgets, TArray& OutFocusData) +{ + OutFocusData.Empty(); + + if (FTobiiGTOMModule::IsAvailable() && FTobiiGTOMModule::Get().GTOMInputDevice.IsValid()) + { + const TArray& AllFocusData = FTobiiGTOMModule::Get().GTOMInputDevice->GetFocusData(); + for (const FTobiiGazeFocusData& FocusData : AllFocusData) + { + if (bWantWidgets && FocusData.FocusedWidget.IsValid()) + { + if (FTobiiGTOMUtils::ValidateFocusLayers(FocusLayerFilterList, bIsWhiteList, FocusData.FocusedWidget.Get())) + { + OutFocusData.Add(FocusData); + } + } + else if (bWantPrimitives && FocusData.FocusedPrimitiveComponent.IsValid() && !FocusData.FocusedWidget.IsValid()) + { + if (FTobiiGTOMUtils::ValidateFocusLayers(FocusLayerFilterList, bIsWhiteList, FocusData.FocusedPrimitiveComponent.Get())) + { + OutFocusData.Add(FocusData); + } + } + } + } + + return OutFocusData.Num() > 0; +} + +void UTobiiGTOMBlueprintLibrary::RegisterScreenSpaceGazeFocusableWidgets(UWidget* Root) +{ + UUserWidget* UserWidget = Cast(Root); + if (UserWidget != nullptr) + { + RegisterScreenSpaceGazeFocusableWidgets(UserWidget->GetRootWidget()); + } + else + { + UTobiiGazeFocusableWidget* GazeFocusableWidget = Cast(Root); + if (GazeFocusableWidget != nullptr) + { + GazeFocusableWidget->RegisterWidgetToGTOM(nullptr); + } + + UPanelWidget* Panel = Cast(Root); + if (Panel != nullptr) + { + for (int32 ChildIdx = 0; ChildIdx < Panel->GetChildrenCount(); ChildIdx++) + { + RegisterScreenSpaceGazeFocusableWidgets(Panel->GetChildAt(ChildIdx)); + } + } + } +} + +const FHitResult& UTobiiGTOMBlueprintLibrary::GetNaiveGazeHit() +{ + static FHitResult Dummy; + if (FTobiiGTOMModule::IsAvailable() && FTobiiGTOMModule::Get().GTOMInputDevice.IsValid()) + { + return FTobiiGTOMModule::Get().GTOMInputDevice->CombinedWorldGazeHitData; + } + else + { + return Dummy; + } +} + +bool UTobiiGTOMBlueprintLibrary::IsPrimitiveComponentGazeFocusable(UPrimitiveComponent* PrimitiveComponent) +{ + return UTobiiGazeFocusableComponent::IsPrimitiveFocusable(PrimitiveComponent); +} + +bool UTobiiGTOMBlueprintLibrary::IsWidgetGazeFocusable(UWidget* Widget) +{ + return UTobiiGazeFocusableComponent::IsWidgetFocusable(Cast(Widget)); +} + +bool UTobiiGTOMBlueprintLibrary::GetPrimitiveComponentFocusOffset(USceneComponent* Component, FVector& OutFocusOffset) +{ + OutFocusOffset = FVector::ZeroVector; + if (Component == nullptr) + { + return false; + } + + for (const FName& CurrentTag : Component->ComponentTags) + { + FString CurrentTagString = CurrentTag.ToString(); + if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetXTag)) + { + FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetXTag.Len()); + OutFocusOffset.X += FCString::Atof(*Argument); + } + if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetYTag)) + { + FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetYTag.Len()); + OutFocusOffset.Y += FCString::Atof(*Argument); + } + if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetZTag)) + { + FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetZTag.Len()); + OutFocusOffset.Z += FCString::Atof(*Argument); + } + } + + return true; +} + +bool UTobiiGTOMBlueprintLibrary::GetPrimitiveComponentFocusLocation(USceneComponent* Component, FVector& OutFocusLocation) +{ + OutFocusLocation = FVector::ZeroVector; + if (Component == nullptr) + { + return false; + } + + OutFocusLocation = Component->GetComponentLocation(); + + FVector Offset = FVector::ZeroVector; + for (const FName& CurrentTag : Component->ComponentTags) + { + FString CurrentTagString = CurrentTag.ToString(); + if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetXTag)) + { + FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetXTag.Len()); + Offset.X += FCString::Atof(*Argument); + } + if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetYTag)) + { + FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetYTag.Len()); + Offset.Y += FCString::Atof(*Argument); + } + if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetZTag)) + { + FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetZTag.Len()); + Offset.Z += FCString::Atof(*Argument); + } + } + + OutFocusLocation += Component->GetComponentTransform().TransformVector(Offset); + + return true; +} + +bool UTobiiGTOMBlueprintLibrary::MakeGazeFocusDataForWidgetInteractionComponent(UWidgetInteractionComponent* WidgetInteraction, ECollisionChannel CollisionChannel, FTobiiGazeFocusData& OutGazeFocusData) +{ + OutGazeFocusData = FTobiiGazeFocusData(); + if (WidgetInteraction == nullptr || !WidgetInteraction->bEnableHitTesting) + { + return false; + } + + UWorld* CurrentWorld = WidgetInteraction->GetWorld(); + if (CurrentWorld == nullptr) + { + return false; + } + + FHitResult HitResult; + FVector StartPoint = WidgetInteraction->GetComponentLocation(); + FVector EndPoint = StartPoint + WidgetInteraction->GetForwardVector() * WidgetInteraction->InteractionDistance; + FCollisionQueryParams CollisionParams; + CollisionParams.AddIgnoredActor(WidgetInteraction->GetOwner()); + if (CurrentWorld->LineTraceSingleByChannel(HitResult, StartPoint, EndPoint, CollisionChannel, CollisionParams)) + { + UPrimitiveComponent* HitComponent = HitResult.GetComponent(); + if (UTobiiGTOMBlueprintLibrary::IsPrimitiveComponentGazeFocusable(HitComponent)) + { + OutGazeFocusData.FocusedActor = HitResult.GetActor(); + OutGazeFocusData.FocusedPrimitiveComponent = HitComponent; + OutGazeFocusData.LastVisibleWorldLocation = HitResult.Location; + OutGazeFocusData.FocusConfidence = 1.0f; + + for (const TWeakPtr& WidgetMaybe : WidgetInteraction->GetHoveredWidgetPath().Widgets) + { + if (WidgetMaybe.IsValid()) + { + SWidget* HoveredWidgetPtr = WidgetMaybe.Pin().Get(); + STobiiGazeFocusableWidget* TobiiSlateWidget = STobiiGazeFocusableWidget::TryCastToTobiiGazeFocusable(HoveredWidgetPtr); + if (TobiiSlateWidget != nullptr && TobiiSlateWidget->UMGWidget.IsValid()) + { + OutGazeFocusData.FocusedWidget = TobiiSlateWidget->UMGWidget; + break; + } + } + } + } + } + + return OutGazeFocusData.FocusedActor != nullptr; +} + +void UTobiiGTOMBlueprintLibrary::EmulateGazeFocusUsingWidgetInteractionComponent(UWidgetInteractionComponent* WidgetInteraction, ECollisionChannel CollisionChannel) +{ + if (FTobiiGTOMModule::IsAvailable()) + { + FTobiiGazeFocusData GazeFocusData; + MakeGazeFocusDataForWidgetInteractionComponent(WidgetInteraction, CollisionChannel, GazeFocusData); + TArray SingleFocusData; + SingleFocusData.Add(GazeFocusData); + FTobiiGTOMModule::Get().GTOMInputDevice->EmulateGazeFocus(SingleFocusData); + } +} + +bool UTobiiGTOMBlueprintLibrary::FindLargestInscribedAlignedRect(float CircleSegmentAngleRad, float CircleRadius, FVector2D& LargestInscribedRectSize, float& DistanceToCenter) +{ + //https://math.stackexchange.com/questions/2829710/largest-inscribed-rectangle-in-sector + + if (CircleSegmentAngleRad > FLT_EPSILON && CircleRadius > FLT_EPSILON) + { + if (CircleSegmentAngleRad >= PI - FLT_EPSILON) + { + const float SqrtTwo = FMath::Sqrt(2.0f); + const float SqrtTwoOverTwo = SqrtTwo / 2.0f; + LargestInscribedRectSize.Set(SqrtTwo * CircleRadius, SqrtTwoOverTwo * CircleRadius); + DistanceToCenter = CircleRadius / 2.0f; + } + else + { + const float Theta = CircleSegmentAngleRad / 4.0f; + float ThetaSine, ThetaCosine; + FMath::SinCos(&ThetaSine, &ThetaCosine, Theta); + const float Height = CircleRadius * ThetaSine * 2.0f; + const float AlongX = CircleRadius * ThetaCosine; + + const float HalfAlpha = CircleSegmentAngleRad / 2.0f; + float HalfAlphaSine, HalfAlphaCosine; + FMath::SinCos(&HalfAlphaSine, &HalfAlphaCosine, HalfAlpha); + const float B = CircleRadius * ThetaSine * HalfAlphaCosine / HalfAlphaSine; + + LargestInscribedRectSize.Set(AlongX - B, Height); + DistanceToCenter = B + FMath::Min(LargestInscribedRectSize.X, LargestInscribedRectSize.Y) / 2.0f; + } + + return true; + } + + LargestInscribedRectSize.Set(0.0f, 0.0f); + DistanceToCenter = 0.0f; + return false; +} + +bool UTobiiGTOMBlueprintLibrary::TransformWidgetLocalPointToWorld(UWidgetComponent* Component, const FVector2D& LocalWidgetLocation, FVector& OutWorldLocation) +{ + if (Component == nullptr) + { + return false; + } + + EWidgetGeometryMode GeometryMode = Component->GetGeometryMode(); + switch (GeometryMode) + { + case EWidgetGeometryMode::Plane: + { + //First let's calculate some measurements we will need + const FVector2D DrawSize = Component->GetDrawSize(); + const FVector2D Pivot = Component->GetPivot(); + + //Undo the pivot and transform to world space + OutWorldLocation = Component->GetComponentTransform().TransformPosition(FVector(0.0f, DrawSize.X * Pivot.X - LocalWidgetLocation.X, DrawSize.Y * Pivot.Y - LocalWidgetLocation.Y)); + + return true; + } + + case EWidgetGeometryMode::Cylinder: + { + //First let's calculate some measurements we will need + const FVector2D DrawSize = Component->GetDrawSize(); + const FVector2D Pivot = Component->GetPivot(); + const float NormalizedLocationX = LocalWidgetLocation.X / DrawSize.X; + const float ArcAngleRadians = FMath::DegreesToRadians(Component->GetCylinderArcAngle()); + const float Radius = DrawSize.X / ArcAngleRadians; + const float Apothem = Radius * FMath::Cos(0.5f * ArcAngleRadians); + const float ChordLength = 2.0f * Radius * FMath::Sin(0.5f * ArcAngleRadians); + const float PivotOffsetX = ChordLength * (0.5 - Pivot.X); + + //Determine the endpoints of the UI surface in UI space radians. The projection is "reversed" here so the UI surface segment is actually centered around X-. + const float Endpoint1 = FMath::Fmod(FMath::Atan2(-0.5f * ChordLength, -Apothem) + 2 * PI, 2 * PI); + const float Endpoint2 = FMath::Fmod(FMath::Atan2(+0.5f * ChordLength, -Apothem) + 2 * PI, 2 * PI); + + //Figure out where on the circle segment our X is using our normalized coordinate + const float InterpolatedAngle = FMath::Lerp(Endpoint1, Endpoint2, NormalizedLocationX); + + //We can now determine our coordinate in circle space (the space where the origin is the center of the cylinder circle Y-segment the X-coordinate we are seeking is on) as well as the distance to the cylinder surface. + const float CircleSpaceWidgetX = Radius * FMath::Cos(InterpolatedAngle); + const float CircleSpaceWidgetDistanceY = Radius * FMath::Sin(InterpolatedAngle); + + //Figure out our Z coordinate in widget space. This is easy since it's a cylinder. + const float WidgetSpaceZ = DrawSize.Y * Pivot.Y - LocalWidgetLocation.Y; + + //We now convert the circle space coordinates to widget space (The origin is located at the center of the corda, in the middle of the widget component plane) and insert the widget space Z. + const FVector WidgetSpaceCoord(Apothem + CircleSpaceWidgetX, -CircleSpaceWidgetDistanceY + PivotOffsetX, WidgetSpaceZ); + + //Finally we can simply transform this location to world space and we're done. + OutWorldLocation = Component->GetComponentTransform().TransformPosition(WidgetSpaceCoord); + + return true; + } + + default: + break; + } + + return false; +} + +bool UTobiiGTOMBlueprintLibrary::TransformWorldPointToWidgetLocal(UWidgetComponent* Component, const FVector& WorldLocation, const FVector& WorldDirection, FVector2D& OutLocalWidgetLocation) +{ + if (Component == nullptr) + { + return false; + } + + EWidgetGeometryMode GeometryMode = Component->GetGeometryMode(); + switch (GeometryMode) + { + case EWidgetGeometryMode::Plane: + { + Component->GetLocalHitLocation(WorldLocation, OutLocalWidgetLocation); + return true; + } + + case EWidgetGeometryMode::Cylinder: + { + TTuple Output = Component->GetCylinderHitLocation(WorldLocation, WorldDirection); + OutLocalWidgetLocation = Output.Value; + return true; + } + + default: + break; + } + + return false; +} + +bool UTobiiGTOMBlueprintLibrary::TestConeSphereIntersection(const FVector& ConeApex, const FVector& ConeDirection, const float ConeAngleDeg, const FVector& SphereCenter, const float SphereRadius) +{ + //http://blog.julien.cayzac.name/2009/12/frustum-culling-sphere-cone-test-with.html + + float ConeAngleSine, ConeAngleCosine; + FMath::SinCos(&ConeAngleSine, &ConeAngleCosine, FMath::DegreesToRadians(ConeAngleDeg)); + + const FVector DirectionTowardsSphere = SphereCenter - ConeApex; + const float SelfDot = FVector::DotProduct(DirectionTowardsSphere, DirectionTowardsSphere); + const float a = FVector::DotProduct(DirectionTowardsSphere, ConeDirection); + const float p(a*ConeAngleSine); + const float q(ConeAngleCosine * ConeAngleCosine * SelfDot - a * a); + const bool tmp(q < 0); + + float lhs[2]; + lhs[0] = (SphereRadius*SphereRadius - q); + lhs[1] = -lhs[0]; + int result = (lhs[(p < SphereRadius) || !tmp] < 2.0f * SphereRadius * p) ? -1 : tmp; + return result != 0; // -1 means partially included. 1 means fully included +} + +bool UTobiiGTOMBlueprintLibrary::TestRectEllipseIntersection(const FVector2D& RectangleCenter, const FVector2D& RectangleRightAxis, const FVector2D& RectangleUpAxis, const FVector2D& RectangleExtents, const FVector2D& EllipseCenter, const FVector2D& EllipseRadii, const float& EllipseRotationDeg) +{ + //https://www.geometrictools.com/Documentation/IntersectionRectangleEllipse.pdf :: Minkowski sum + + //Compute the increase in extents for R'. + float EllipseSine, EllipseCosine; + FMath::SinCos(&EllipseSine, &EllipseCosine, FMath::DegreesToRadians(EllipseRotationDeg)); + FMatrix2x2 V(EllipseCosine, -EllipseSine, EllipseSine, EllipseCosine); + FMatrix2x2 VT(EllipseCosine, EllipseSine, -EllipseSine, EllipseCosine); + FMatrix2x2 D(1.0f / EllipseRadii.X, 0.0f, 0.0f, 1.0f / EllipseRadii.Y); + FMatrix2x2 M = V.Concatenate(D).Concatenate(D).Concatenate(VT); + FMatrix2x2 MInv = M.Inverse(); + + const float LRight = FMath::Sqrt(FVector2D::DotProduct(RectangleRightAxis, MInv.TransformVector(RectangleRightAxis))); + const float LUp = FMath::Sqrt(FVector2D::DotProduct(RectangleUpAxis, MInv.TransformVector(RectangleUpAxis))); + + //Transform the ellipse center to the rectangle coordinate system. + FVector2D KmC = EllipseCenter - RectangleCenter; + float xi[2] = { FVector2D::DotProduct(RectangleRightAxis, KmC), FVector2D::DotProduct(RectangleUpAxis, KmC) }; + if (FMath::Abs(xi[0]) <= RectangleExtents.X + LRight && FMath::Abs(xi[1]) <= RectangleExtents.Y + LUp) + { + float s[2] = { (xi[0] >= 0.0f ? 1.0f : -1.0f), (xi[1] >= 0.0f ? 1.0f : -1.0f) }; + FVector2D PmC = s[0] * RectangleExtents.X * RectangleRightAxis + s[1] * RectangleExtents.X * RectangleUpAxis; + FVector2D MDelta = M.TransformVector(KmC - PmC); + + if (s[0] * FVector2D::DotProduct(RectangleRightAxis, MDelta) <= 0.0f + || s[1] * FVector2D::DotProduct(RectangleUpAxis, MDelta) <= 0.0f) + { + return true; + } + + return FVector2D::DotProduct(MDelta, M.TransformVector(MDelta)) <= 1.0f; + } + + return false; +} + +FVector2D UTobiiGTOMBlueprintLibrary::GetUniformlyDistributedRandomCirclePoint(float CircleRadius) +{ + float Angle = FMath::RandRange(0.0f, TWO_PI); + float Radius = CircleRadius * FMath::Sqrt(FMath::RandRange(0.0f, 1.0f)); + return FVector2D(Radius * FMath::Cos(Angle), Radius * FMath::Sin(Angle)); +} \ No newline at end of file diff --git a/Source/TobiiGTOM/Private/TobiiGTOMEngine.cpp b/Source/TobiiGTOM/Private/TobiiGTOMEngine.cpp new file mode 100644 index 0000000..4c1b81d --- /dev/null +++ b/Source/TobiiGTOM/Private/TobiiGTOMEngine.cpp @@ -0,0 +1,631 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#include "TobiiGTOMEngine.h" +#include "TobiiGazeFocusableComponent.h" +#include "TobiiGazeFocusableWidget.h" +#include "TobiiGTOMBlueprintLibrary.h" +#include "TobiiGTOMInternalTypes.h" + +#include "Engine/Engine.h" +#include "IEyeTracker.h" +#include "DrawDebugHelpers.h" +#include "GameFramework/PlayerController.h" +#include "Runtime/Engine/Public/Slate/SceneViewport.h" +#include "Misc/Paths.h" + +static TAutoConsoleVariable CVarDebugDisplayGTOMVisibility(TEXT("tobii.debug.DisplayGTOMVisibility"), 0, TEXT("1 means we visualize which objects are visible to G2OM")); +static TAutoConsoleVariable CVarDebugDisplayG2OMCandidateSet(TEXT("tobii.debug.DisplayG2OMCandidateSet"), 1, TEXT("1 will visualize all bounds calculated in G2OM. This is useful to test for math errors.")); + +FTobiiGTOMEngine::FTobiiGTOMEngine() +{ + g2om_context_create(&G2OMContext); +} + +FTobiiGTOMEngine::~FTobiiGTOMEngine() +{ + if (G2OMContext != nullptr) + { + g2om_context_destroy(&G2OMContext); + G2OMContext = nullptr; + } +} + +void FTobiiGTOMEngine::Tick(float DeltaTimeSecs) +{ + static const auto DrawDebugCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.debug")); + static const auto MaximumTraceDistanceCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.MaximumTraceDistance")); + static const auto FocusTraceChannelCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.FocusTraceChannel")); + + if (GEngine == nullptr + || GEngine->GameViewport == nullptr + || GEngine->GameViewport->GetWorld() == nullptr + || GEngine->GameViewport->GetGameViewport() == nullptr + || !GEngine->EyeTrackingDevice.IsValid()) + { + return; + } + + //Gaze data + FEyeTrackerGazeData CombinedGazeData; + GEngine->EyeTrackingDevice->GetEyeTrackerGazeData(CombinedGazeData); + + FVector2D CombinedGazePtUNorm = FVector2D::ZeroVector; + FVector2D ScreenGazePointPx; + FVector2D ViewportSize; + GEngine->GameViewport->GetViewportSize(ViewportSize); + GTOMPlayerController->ProjectWorldLocationToScreen(CombinedGazeData.GazeOrigin + CombinedGazeData.GazeDirection * 10.0f, ScreenGazePointPx); + if (ViewportSize.X > 0.0f && ViewportSize.Y > 0.0f) + { + CombinedGazePtUNorm = FVector2D(ScreenGazePointPx.X / ViewportSize.X, ScreenGazePointPx.Y / ViewportSize.Y); + } + + if (!GTOMPlayerController.IsValid()) + { + GTOMPlayerController = GEngine->GameViewport->GetWorld()->GetFirstPlayerController(); + GEngine->EyeTrackingDevice->SetEyeTrackedPlayer(GTOMPlayerController.Get()); + } + + //Main G2OM processing + if (G2OMContext == nullptr + || !GTOMPlayerController.IsValid() + || GTOMPlayerController->PlayerCameraManager == nullptr) + { + return; + } + + QUICK_SCOPE_CYCLE_COUNTER(STAT_TobiiEyetracking_GTOM); + + FRotator CameraRotation = GTOMPlayerController->PlayerCameraManager->GetCameraRotation(); + G2OMGazeData.timestamp_in_s = GTOMPlayerController->GetWorld()->GetTimeSeconds(); + G2OMGazeData.camera_up_direction_world_space = FTobiiGTOMUtils::UE4VectorToG2OMVector(CameraRotation.RotateVector(FVector::UpVector)); + G2OMGazeData.camera_right_direction_world_space = FTobiiGTOMUtils::UE4VectorToG2OMVector(CameraRotation.RotateVector(FVector::RightVector)); + G2OMGazeData.gaze_ray_world_space.is_valid = CombinedGazeData.ConfidenceValue > 0.5f; // Sigh. Why is this still a thing? + G2OMGazeData.gaze_ray_world_space.ray.origin.x = CombinedGazeData.GazeOrigin.X; + G2OMGazeData.gaze_ray_world_space.ray.origin.y = CombinedGazeData.GazeOrigin.Y; + G2OMGazeData.gaze_ray_world_space.ray.origin.z = CombinedGazeData.GazeOrigin.Z; + G2OMGazeData.gaze_ray_world_space.ray.direction.x = CombinedGazeData.GazeDirection.X; + G2OMGazeData.gaze_ray_world_space.ray.direction.y = CombinedGazeData.GazeDirection.Y; + G2OMGazeData.gaze_ray_world_space.ray.direction.z = CombinedGazeData.GazeDirection.Z; + + //Raycasts + FCollisionQueryParams CollisionQueryParams; + CollisionQueryParams.AddIgnoredActor(GTOMPlayerController.Get()); + CollisionQueryParams.AddIgnoredActor(GTOMPlayerController->GetPawn()); + const float MaximumTraceDistance = FMath::Max(MaximumTraceDistanceCVar->GetFloat(), 0.0f); + const FVector CombinedGazeFarLocation = CombinedGazeData.GazeOrigin + (CombinedGazeData.GazeDirection * MaximumTraceDistance); + if (CombinedGazeData.ConfidenceValue < 0.5f || + !GTOMPlayerController->GetWorld()->LineTraceSingleByChannel(CombinedWorldGazeHitData, CombinedGazeData.GazeOrigin + , CombinedGazeFarLocation, (ECollisionChannel)FocusTraceChannelCVar->GetInt(), CollisionQueryParams)) + { + CombinedWorldGazeHitData.Actor = nullptr; + CombinedWorldGazeHitData.Component = nullptr; + CombinedWorldGazeHitData.Distance = MaximumTraceDistance; + CombinedWorldGazeHitData.Location = CombinedGazeFarLocation; + CombinedWorldGazeHitData.bBlockingHit = false; + } + FillG2OMRaycast(CombinedWorldGazeHitData, CombinedGazePtUNorm, CombinedGazeData.GazeDirection, G2OMRaycastResults.raycast); + + //Candidate generation + TArray Candidates; + TArray CandidateResults; + TMap> FocusableComponentsWithWidgets; + const TMap& VisibleSet = OcclusionTester.Tick(DeltaTimeSecs, GTOMPlayerController.Get(), CombinedGazeData, G2OMGazeData, G2OMContext); + TMap>& ScreenSpaceWidgets = UTobiiGazeFocusableWidget::GetTobiiScreenSpaceFocusableWidgets(); + + { + QUICK_SCOPE_CYCLE_COUNTER(STAT_TobiiEyetracking_GTOM_BuildCandidates); + + //We must do this every frame since any of these properties might have changed since the last tick. + for (auto OcclusionDataIterator = VisibleSet.CreateConstIterator(); OcclusionDataIterator; ++OcclusionDataIterator) + { + const FTobiiGTOMOcclusionData& OcclusionData = OcclusionDataIterator.Value(); + const FEngineFocusableUID FocusableID = OcclusionDataIterator.Key(); + + if (OcclusionData.PrimitiveComponent.IsValid() && OcclusionData.PrimitiveComponent->IsVisible()) + { + bool bShouldAddPrimitive = true; + if (OcclusionData.FocusableComponent.IsValid()) + { + const TArray>& FocusableWidgets = OcclusionData.FocusableComponent->GetFocusableWidgetsForPrimitiveComponent(OcclusionData.PrimitiveComponent.Get()); + if (FocusableWidgets.Num() > 0) + { + //If our primitive contains widgets, then only add the widgets + bShouldAddPrimitive = false; + + g2om_candidate NewCandidate; + FTransform LocalToWorldTranform = OcclusionData.PrimitiveComponent->GetComponentTransform(); + FMatrix LocalToWorldMatrix = LocalToWorldTranform.ToMatrixWithScale(); + FMatrix WorldToLocalMatrix = LocalToWorldMatrix.InverseFast(); + FMemory::Memcpy(NewCandidate.local_to_world_matrix.data, LocalToWorldMatrix.M, 16 * sizeof(float)); + FMemory::Memcpy(NewCandidate.world_to_local_matrix.data, WorldToLocalMatrix.M, 16 * sizeof(float)); + + for (const auto& Widget : FocusableWidgets) + { + if (Widget.IsValid() && !FocusableComponentsWithWidgets.Contains(Widget->GetUniqueID()) + && UTobiiGazeFocusableComponent::IsWidgetFocusable(Widget.Get()) + && Widget->GetWorldSpaceHostWidgetComponent() != nullptr) + { + STobiiGazeFocusableWidget* SlateContainer = (STobiiGazeFocusableWidget*)&Widget->TakeWidget().Get(); + FSlateRect WidgetRenderBounds = SlateContainer->GetCachedGeometry().GetRenderBoundingRect(); + FVector WidgetTopLeft, WidgetBottomRight; + UTobiiGTOMBlueprintLibrary::TransformWidgetLocalPointToWorld(Widget->GetWorldSpaceHostWidgetComponent(), WidgetRenderBounds.GetTopLeft(), WidgetTopLeft); + UTobiiGTOMBlueprintLibrary::TransformWidgetLocalPointToWorld(Widget->GetWorldSpaceHostWidgetComponent(), WidgetRenderBounds.GetBottomRight(), WidgetBottomRight); + + //Range test + bool bIsInRange = true; + float MaxDistance; + const float DistanceToWidget = FVector::Distance(CombinedGazeData.GazeOrigin, (WidgetTopLeft + WidgetBottomRight) / 2.0f); + if (UTobiiGazeFocusableComponent::GetMaxFocusDistanceForWidget(Widget.Get(), MaxDistance)) + { + bIsInRange = DistanceToWidget <= MaxDistance; + } + + if (bIsInRange) + { + WidgetTopLeft = WorldToLocalMatrix.TransformPosition(WidgetTopLeft); + WidgetBottomRight = WorldToLocalMatrix.TransformPosition(WidgetBottomRight); + + NewCandidate.id = Widget->GetUniqueID(); + NewCandidate.min_local_space.x = WidgetTopLeft.X; + NewCandidate.min_local_space.y = WidgetTopLeft.Y; + NewCandidate.min_local_space.z = WidgetTopLeft.Z; + NewCandidate.max_local_space.x = WidgetBottomRight.X; + NewCandidate.max_local_space.y = WidgetBottomRight.Y; + NewCandidate.max_local_space.z = WidgetBottomRight.Z; + + Candidates.Add(NewCandidate); + CandidateResults.Add(g2om_candidate_result()); + FocusableComponentsWithWidgets.Add(Widget->GetUniqueID(), OcclusionData.FocusableComponent); + } + } + } + } + } + + if(bShouldAddPrimitive) + { + //If there were no widgets, just add the primitive. + g2om_candidate NewCandidate; + NewCandidate.id = FocusableID; + + FTransform LocalToWorldTranform = OcclusionData.PrimitiveComponent->GetComponentTransform(); + FMatrix LocalToWorldMatrix = LocalToWorldTranform.ToMatrixWithScale(); + FMatrix WorldToLocalMatrix = LocalToWorldMatrix.InverseFast(); + FMemory::Memcpy(NewCandidate.local_to_world_matrix.data, LocalToWorldMatrix.M, 16 * sizeof(float)); + FMemory::Memcpy(NewCandidate.world_to_local_matrix.data, WorldToLocalMatrix.M, 16 * sizeof(float)); + + FBox BoxBounds = OcclusionData.PrimitiveComponent->CalcBounds(FTransform::Identity).GetBox(); + NewCandidate.min_local_space.x = BoxBounds.Min.X; + NewCandidate.min_local_space.y = BoxBounds.Min.Y; + NewCandidate.min_local_space.z = BoxBounds.Min.Z; + NewCandidate.max_local_space.x = BoxBounds.Max.X; + NewCandidate.max_local_space.y = BoxBounds.Max.Y; + NewCandidate.max_local_space.z = BoxBounds.Max.Z; + + Candidates.Add(MoveTemp(NewCandidate)); + CandidateResults.Add(g2om_candidate_result()); + } + } + } + + //Finally add registered screen space widgets as candidates. + TArray StaleWidgetIds; + for (auto& WidgetPair : ScreenSpaceWidgets) + { + TWeakObjectPtr Widget = WidgetPair.Value; + if (Widget.IsValid()) + { + if (!FocusableComponentsWithWidgets.Contains(Widget->GetUniqueID()) && UTobiiGazeFocusableComponent::IsWidgetFocusable(Widget.Get())) + { + STobiiGazeFocusableWidget* SlateContainer = (STobiiGazeFocusableWidget*)&Widget->TakeWidget().Get(); + FSlateRect WidgetRenderBounds = SlateContainer->GetCachedGeometry().GetRenderBoundingRect(); + FVector2D ScreenTopLeft = GEngine->GameViewport->GetGameViewport()->VirtualDesktopPixelToViewport(FIntPoint(WidgetRenderBounds.GetTopLeft().X, WidgetRenderBounds.GetTopLeft().Y)); + FVector2D ScreenBottomRight = GEngine->GameViewport->GetGameViewport()->VirtualDesktopPixelToViewport(FIntPoint(WidgetRenderBounds.GetBottomRight().X, WidgetRenderBounds.GetBottomRight().Y)); + ScreenTopLeft.X *= ViewportSize.X; + ScreenTopLeft.Y *= ViewportSize.Y; + ScreenBottomRight.X *= ViewportSize.X; + ScreenBottomRight.Y *= ViewportSize.Y; + FVector2D ScreenBottomLeft(ScreenTopLeft.X, ScreenBottomRight.Y); + + FVector WorldTopLeftLocation, WorldBottomRightLocation, WorldBottomLeftLocation; + FVector WorldTopLeftDir, WorldBottomRightDir, WorldBottomLeftDir; + + GTOMPlayerController->DeprojectScreenPositionToWorld(ScreenTopLeft.X, ScreenTopLeft.Y, WorldTopLeftLocation, WorldTopLeftDir); + GTOMPlayerController->DeprojectScreenPositionToWorld(ScreenBottomRight.X, ScreenBottomRight.Y, WorldBottomRightLocation, WorldBottomRightDir); + GTOMPlayerController->DeprojectScreenPositionToWorld(ScreenBottomLeft.X, ScreenBottomLeft.Y, WorldBottomLeftLocation, WorldBottomLeftDir); + WorldTopLeftLocation += WorldTopLeftDir; + WorldBottomRightLocation += WorldBottomRightDir; + WorldBottomLeftLocation += WorldBottomLeftDir; + + FVector WorldTranslation = (WorldTopLeftLocation + WorldBottomRightLocation) / 2.0f; + FVector WorldRightY = WorldBottomRightLocation - WorldBottomLeftLocation; + FVector WorldUpZ = WorldTopLeftLocation - WorldBottomLeftLocation; + FVector WorldForwardX = FVector::CrossProduct(WorldRightY, WorldUpZ); + bool bCouldBuildOrthoNormalBasis = WorldForwardX.Normalize(); + bCouldBuildOrthoNormalBasis = bCouldBuildOrthoNormalBasis && WorldRightY.Normalize(); + bCouldBuildOrthoNormalBasis = bCouldBuildOrthoNormalBasis && WorldUpZ.Normalize(); + + if (bCouldBuildOrthoNormalBasis) + { + FMatrix LocalToWorldMatrix = FMatrix(WorldForwardX, WorldRightY, WorldUpZ, WorldTranslation); + FMatrix WorldToLocalMatrix = LocalToWorldMatrix.InverseFast(); + WorldTopLeftLocation = WorldToLocalMatrix.TransformPosition(WorldTopLeftLocation + WorldTopLeftDir); + WorldBottomRightLocation = WorldToLocalMatrix.TransformPosition(WorldBottomRightLocation + WorldBottomRightDir); + + //Screen space widgets are always in range, so we can skip the distance test + g2om_candidate NewCandidate; + NewCandidate.id = Widget->GetUniqueID(); + FMemory::Memcpy(NewCandidate.local_to_world_matrix.data, LocalToWorldMatrix.M, 16 * sizeof(float)); + FMemory::Memcpy(NewCandidate.world_to_local_matrix.data, WorldToLocalMatrix.M, 16 * sizeof(float)); + NewCandidate.min_local_space.x = WorldTopLeftLocation.X; + NewCandidate.min_local_space.y = WorldTopLeftLocation.Y; + NewCandidate.min_local_space.z = WorldTopLeftLocation.Z; + NewCandidate.max_local_space.x = WorldBottomRightLocation.X; + NewCandidate.max_local_space.y = WorldBottomRightLocation.Y; + NewCandidate.max_local_space.z = WorldBottomRightLocation.Z; + + Candidates.Add(NewCandidate); + CandidateResults.Add(g2om_candidate_result()); + } + } + } + else + { + StaleWidgetIds.Add(WidgetPair.Key); + } + + for (FTobiiFocusableUID Id : StaleWidgetIds) + { + ScreenSpaceWidgets.Remove(Id); + } + } + } + + UPrimitiveComponent* NewTopFocusPrimitive = nullptr; + UTobiiGazeFocusableWidget* NewTopFocusWidget = nullptr; + + { + QUICK_SCOPE_CYCLE_COUNTER(STAT_TobiiEyetracking_GTOM_G2OMProcess); + + g2om_process(G2OMContext, &G2OMGazeData, &G2OMRaycastResults, Candidates.Num(), Candidates.GetData(), CandidateResults.GetData()); + + G2OMFocusResults.Empty(); + for (const g2om_candidate_result& ResultCandidate : CandidateResults) + { + if (ResultCandidate.score > FLT_EPSILON) + { + if(ScreenSpaceWidgets.Contains(ResultCandidate.id)) + { + //Screen space widget + TWeakObjectPtr Widget = ScreenSpaceWidgets[ResultCandidate.id]; + if (Widget.IsValid()) + { + FTobiiGazeFocusData NewFocusData; + NewFocusData.FocusedWidget = Widget; + NewFocusData.FocusConfidence = ResultCandidate.score; + NewFocusData.FocusedActor = nullptr; + NewFocusData.FocusedPrimitiveComponent = nullptr; + NewFocusData.LastVisibleWorldLocation = FVector::ZeroVector; + + G2OMFocusResults.Add(MoveTemp(NewFocusData)); + + if (NewTopFocusWidget == nullptr) + { + NewTopFocusWidget = Widget.Get(); + } + } + } + else if (FocusableComponentsWithWidgets.Contains(ResultCandidate.id)) + { + //World space widget + TWeakObjectPtr Focusable = FocusableComponentsWithWidgets[ResultCandidate.id]; + if (Focusable.IsValid()) + { + const TMap>& FocusableWidgets = Focusable->GetAllFocusableWidgets(); + if (FocusableWidgets.Contains(ResultCandidate.id)) + { + TWeakObjectPtr Widget = FocusableWidgets[ResultCandidate.id]; + if (Widget.IsValid()) + { + FTobiiGazeFocusData NewFocusData; + NewFocusData.FocusedWidget = Widget; + NewFocusData.FocusConfidence = ResultCandidate.score; + + if (Widget->GetWorldSpaceHostWidgetComponent() != nullptr) + { + NewFocusData.FocusedActor = Widget->GetWorldSpaceHostWidgetComponent()->GetOwner(); + NewFocusData.FocusedPrimitiveComponent = Widget->GetWorldSpaceHostWidgetComponent(); + NewFocusData.LastVisibleWorldLocation = Widget->GetWorldSpaceHostWidgetComponent()->GetComponentLocation(); + } + + G2OMFocusResults.Add(MoveTemp(NewFocusData)); + + if (NewTopFocusWidget == nullptr) + { + NewTopFocusWidget = Widget.Get(); + } + } + } + } + } + else if(VisibleSet.Contains(ResultCandidate.id)) + { + //Primitive Component + TWeakObjectPtr FocusedPrimitivePtr = VisibleSet[ResultCandidate.id].PrimitiveComponent; + + if (FocusedPrimitivePtr.IsValid()) + { + FTobiiGazeFocusData NewFocusData; + NewFocusData.FocusedActor = FocusedPrimitivePtr->GetOwner(); + NewFocusData.FocusedPrimitiveComponent = FocusedPrimitivePtr; + NewFocusData.FocusedWidget = nullptr; + UTobiiGTOMBlueprintLibrary::GetPrimitiveComponentFocusLocation(FocusedPrimitivePtr.Get(), NewFocusData.LastVisibleWorldLocation); // We want this to be taken care of by GXOM, but the current system does not support it. + NewFocusData.FocusConfidence = ResultCandidate.score; + + G2OMFocusResults.Add(MoveTemp(NewFocusData)); + + if (NewTopFocusPrimitive == nullptr) + { + NewTopFocusPrimitive = FocusedPrimitivePtr.Get(); + } + } + } + } + } + } + + { + QUICK_SCOPE_CYCLE_COUNTER(STAT_TobiiEyetracking_GTOM_G2OMNotify); + UpdateWinners(NewTopFocusPrimitive, NewTopFocusWidget); + } + + if (DrawDebugCVar->GetInt() && CVarDebugDisplayG2OMCandidateSet.GetValueOnGameThread() != 0) + { + for (g2om_candidate& Candidate : Candidates) + { + bool bIsFocused = false; + float FocusAlpha = 1.0f; + for (const g2om_candidate_result& ResultCandidate : CandidateResults) + { + if (ResultCandidate.id == Candidate.id && ResultCandidate.score > FLT_EPSILON) + { + bIsFocused = true; + FocusAlpha = ResultCandidate.score; + break; + } + } + + g2om_vector Corners[G2OM_CORNERS_COUNT]; + g2om_get_worldspace_corner_of_candidate(&Candidate, G2OM_CORNERS_COUNT, Corners); + + TArray UCorners; + for (int32 Idx = 0; Idx < G2OM_CORNERS_COUNT; Idx++) + { + UCorners.Add(FTobiiGTOMUtils::G2OMVectorToUE4Vector(Corners[Idx])); + } + + UWorld* World = GTOMPlayerController->GetWorld(); + const float DistanceToFirstCorner = FVector::Distance(UCorners[G2OM_CORNERS_FLL], GTOMPlayerController->GetPawn()->GetActorLocation()); + const float Thickness = (bIsFocused && DistanceToFirstCorner > 100.0f) ? 3.0f : 0.0f; + const FColor BoxColor = bIsFocused ? FColor(0, 255, 0, FocusAlpha * 255) + : (FocusableComponentsWithWidgets.Contains(Candidate.id) ? FColor::Cyan : FColor::Orange); + + //FRONT + DrawDebugLine(World, UCorners[G2OM_CORNERS_FLL], UCorners[G2OM_CORNERS_FLR], BoxColor, false, 0.0f, 0, Thickness); + DrawDebugLine(World, UCorners[G2OM_CORNERS_FLR], UCorners[G2OM_CORNERS_FUR], BoxColor, false, 0.0f, 0, Thickness); + DrawDebugLine(World, UCorners[G2OM_CORNERS_FUR], UCorners[G2OM_CORNERS_FUL], BoxColor, false, 0.0f, 0, Thickness); + DrawDebugLine(World, UCorners[G2OM_CORNERS_FUL], UCorners[G2OM_CORNERS_FLL], BoxColor, false, 0.0f, 0, Thickness); + + //BACK + DrawDebugLine(World, UCorners[G2OM_CORNERS_BLL], UCorners[G2OM_CORNERS_BLR], BoxColor, false, 0.0f, 0, Thickness); + DrawDebugLine(World, UCorners[G2OM_CORNERS_BLR], UCorners[G2OM_CORNERS_BUR], BoxColor, false, 0.0f, 0, Thickness); + DrawDebugLine(World, UCorners[G2OM_CORNERS_BUR], UCorners[G2OM_CORNERS_BUL], BoxColor, false, 0.0f, 0, Thickness); + DrawDebugLine(World, UCorners[G2OM_CORNERS_BUL], UCorners[G2OM_CORNERS_BLL], BoxColor, false, 0.0f, 0, Thickness); + + //SPOKES + DrawDebugLine(World, UCorners[G2OM_CORNERS_FLL], UCorners[G2OM_CORNERS_BLL], BoxColor, false, 0.0f, 0, Thickness); + DrawDebugLine(World, UCorners[G2OM_CORNERS_FLR], UCorners[G2OM_CORNERS_BLR], BoxColor, false, 0.0f, 0, Thickness); + DrawDebugLine(World, UCorners[G2OM_CORNERS_FUR], UCorners[G2OM_CORNERS_BUR], BoxColor, false, 0.0f, 0, Thickness); + DrawDebugLine(World, UCorners[G2OM_CORNERS_FUL], UCorners[G2OM_CORNERS_BUL], BoxColor, false, 0.0f, 0, Thickness); + } + } +} + +void FTobiiGTOMEngine::UpdateWinners(UPrimitiveComponent* NewTopFocusPrimitive, UTobiiGazeFocusableWidget* NewTopFocusWidget) +{ + //Primitive components + { + if (NewTopFocusPrimitive == nullptr && PreviouslyFocusedPrimitiveComponent.IsValid()) + { + //We lost focus completely + NotifyPrimitiveComponentGazeFocusLost(*PreviouslyFocusedPrimitiveComponent.Get()); + } + else if (NewTopFocusPrimitive != nullptr && !PreviouslyFocusedPrimitiveComponent.IsValid()) + { + //We gained focus from no focus + NotifyPrimitiveComponentGazeFocusReceived(*NewTopFocusPrimitive); + } + else if (NewTopFocusPrimitive != nullptr && PreviouslyFocusedPrimitiveComponent.IsValid() && NewTopFocusPrimitive != PreviouslyFocusedPrimitiveComponent.Get()) + { + //Focus shifted from one object to another + NotifyPrimitiveComponentGazeFocusLost(*PreviouslyFocusedPrimitiveComponent.Get()); + NotifyPrimitiveComponentGazeFocusReceived(*NewTopFocusPrimitive); + } + + PreviouslyFocusedPrimitiveComponent = TWeakObjectPtr(NewTopFocusPrimitive); + } + + //Widgets + { + if (NewTopFocusWidget == nullptr && PreviouslyFocusedWidget.IsValid()) + { + //We lost focus completely + NotifyWidgetGazeFocusLost(*PreviouslyFocusedWidget.Get()); + } + else if (NewTopFocusWidget != nullptr && !PreviouslyFocusedWidget.IsValid()) + { + //We gained focus from no focus + NotifyWidgetGazeFocusReceived(*NewTopFocusWidget); + } + else if (NewTopFocusWidget != nullptr && PreviouslyFocusedWidget.IsValid() && NewTopFocusWidget != PreviouslyFocusedWidget.Get()) + { + //Focus shifted from one object to another + NotifyWidgetGazeFocusLost(*PreviouslyFocusedWidget.Get()); + NotifyWidgetGazeFocusReceived(*NewTopFocusWidget); + } + + PreviouslyFocusedWidget = TWeakObjectPtr(NewTopFocusWidget); + } +} + +void FTobiiGTOMEngine::FillG2OMRaycast(const FHitResult& HitResult, const FVector2D& ScreenGazePointUNorm, const FVector& GazeDirection, g2om_raycast& RaycastToModify) +{ + RaycastToModify.is_valid = false; + RaycastToModify.id = -1; + + if (GEngine == nullptr + || GEngine->GameViewport == nullptr + || GEngine->GameViewport->GetWorld() == nullptr + || GEngine->GameViewport->GetGameViewport() == nullptr + || !GEngine->EyeTrackingDevice.IsValid()) + { + return; + } + + //First test screen space widgets + FVector2D VirtualDesktopGazePoint = GEngine->GameViewport->GetGameViewport()->ViewportToVirtualDesktopPixel(ScreenGazePointUNorm); + TMap>& ScreenSpaceWidgets = UTobiiGazeFocusableWidget::GetTobiiScreenSpaceFocusableWidgets(); + for (auto& WidgetPair : ScreenSpaceWidgets) + { + TWeakObjectPtr Widget = WidgetPair.Value; + if (Widget.IsValid()) + { + if (UTobiiGazeFocusableComponent::IsWidgetFocusable(Widget.Get())) + { + STobiiGazeFocusableWidget* SlateContainer = (STobiiGazeFocusableWidget*)&Widget->TakeWidget().Get(); + FSlateRect WidgetRenderBounds = SlateContainer->GetCachedGeometry().GetRenderBoundingRect(); + if (WidgetRenderBounds.ContainsPoint(VirtualDesktopGazePoint)) + { + RaycastToModify.is_valid = true; + RaycastToModify.id = Widget->GetUniqueID(); + return; + } + } + } + } + + UPrimitiveComponent* Primitive = HitResult.GetComponent(); + if (Primitive != nullptr) + { + RaycastToModify.is_valid = UTobiiGazeFocusableComponent::IsPrimitiveFocusable(Primitive); + + if (Primitive->GetOwner()) + { + UTobiiGazeFocusableComponent* FocusableComponent = (UTobiiGazeFocusableComponent*)Primitive->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass()); + if (FocusableComponent != nullptr) + { + const TArray>& FocusableWidgets = FocusableComponent->GetFocusableWidgetsForPrimitiveComponent(Primitive); + + for (const auto& Widget : FocusableWidgets) + { + if (Widget.IsValid() && UTobiiGazeFocusableComponent::IsWidgetFocusable(Widget.Get())) + { + FVector2D WidgetLocation; + if (UTobiiGTOMBlueprintLibrary::TransformWorldPointToWidgetLocal(Widget->GetWorldSpaceHostWidgetComponent(), HitResult.Location, GazeDirection, WidgetLocation)) + { + STobiiGazeFocusableWidget* SlateContainer = (STobiiGazeFocusableWidget*)&Widget->TakeWidget().Get(); + FSlateRect WidgetRenderBounds = SlateContainer->GetCachedGeometry().GetRenderBoundingRect(); + + if (WidgetRenderBounds.ContainsPoint(WidgetLocation)) + { + //We found a hit! + RaycastToModify.id = Widget->GetUniqueID(); + return; + } + } + } + } + } + } + + //Default + RaycastToModify.id = Primitive->GetUniqueID(); + } +} + +void FTobiiGTOMEngine::NotifyPrimitiveComponentGazeFocusReceived(UPrimitiveComponent& PrimitiveComponentToReceiveFocus) +{ + PrimitiveComponentToReceiveFocus.ComponentTags.AddUnique(FTobiiPrimitiveComponentGazeFocusTags::HasGazeFocusTag); + + if (PrimitiveComponentToReceiveFocus.GetOwner() != nullptr) + { + UTobiiGazeFocusableComponent* GazeFocusableComponent = (UTobiiGazeFocusableComponent*)PrimitiveComponentToReceiveFocus.GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass()); + if (GazeFocusableComponent != nullptr) + { + GazeFocusableComponent->PrimitiveReceivedGazeFocus(&PrimitiveComponentToReceiveFocus); + } + } +} + +void FTobiiGTOMEngine::NotifyPrimitiveComponentGazeFocusLost(UPrimitiveComponent& PrimitiveComponentToLoseFocus) +{ + PrimitiveComponentToLoseFocus.ComponentTags.Remove(FTobiiPrimitiveComponentGazeFocusTags::HasGazeFocusTag); + + if (PrimitiveComponentToLoseFocus.GetOwner() != nullptr) + { + UTobiiGazeFocusableComponent* GazeFocusableComponent = (UTobiiGazeFocusableComponent*)PrimitiveComponentToLoseFocus.GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass()); + if (GazeFocusableComponent != nullptr) + { + GazeFocusableComponent->PrimitiveLostGazeFocus(&PrimitiveComponentToLoseFocus); + } + } +} + +void FTobiiGTOMEngine::NotifyWidgetGazeFocusReceived(UTobiiGazeFocusableWidget& WidgetToReceiveFocus) +{ + WidgetToReceiveFocus.bHasGazeFocus = true; + WidgetToReceiveFocus.ReceiveFocus(); + + if (WidgetToReceiveFocus.GetWorldSpaceHostWidgetComponent() != nullptr && WidgetToReceiveFocus.GetWorldSpaceHostWidgetComponent()->GetOwner() != nullptr) + { + UTobiiGazeFocusableComponent* GazeFocusableComponent = (UTobiiGazeFocusableComponent*)WidgetToReceiveFocus.GetWorldSpaceHostWidgetComponent()->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass()); + if (GazeFocusableComponent != nullptr) + { + GazeFocusableComponent->WidgetReceivedGazeFocus(&WidgetToReceiveFocus); + } + } +} + +void FTobiiGTOMEngine::NotifyWidgetGazeFocusLost(UTobiiGazeFocusableWidget& WidgetToLoseFocus) +{ + WidgetToLoseFocus.bHasGazeFocus = false; + WidgetToLoseFocus.LoseFocus(); + + if (WidgetToLoseFocus.GetWorldSpaceHostWidgetComponent() != nullptr && WidgetToLoseFocus.GetWorldSpaceHostWidgetComponent()->GetOwner() != nullptr) + { + UTobiiGazeFocusableComponent* GazeFocusableComponent = (UTobiiGazeFocusableComponent*)WidgetToLoseFocus.GetWorldSpaceHostWidgetComponent()->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass()); + if (GazeFocusableComponent != nullptr) + { + GazeFocusableComponent->WidgetLostGazeFocus(&WidgetToLoseFocus); + } + } +} + +void FTobiiGTOMEngine::EmulateGazeFocus(TArray& EmulatedFocusData) +{ + G2OMFocusResults.Empty(); + G2OMFocusResults = EmulatedFocusData; + UPrimitiveComponent* TopPrimitive = nullptr; + UTobiiGazeFocusableWidget* TopWidget = nullptr; + if (EmulatedFocusData.Num() > 0) + { + TopPrimitive = EmulatedFocusData[0].FocusedPrimitiveComponent.IsValid() ? EmulatedFocusData[0].FocusedPrimitiveComponent.Get() : nullptr; + TopWidget = EmulatedFocusData[0].FocusedWidget.IsValid() ? EmulatedFocusData[0].FocusedWidget.Get() : nullptr; + } + + UpdateWinners(TopPrimitive, TopWidget); +} diff --git a/Source/TobiiGTOM/Private/TobiiGTOMEngine.h b/Source/TobiiGTOM/Private/TobiiGTOMEngine.h new file mode 100644 index 0000000..4a21a87 --- /dev/null +++ b/Source/TobiiGTOM/Private/TobiiGTOMEngine.h @@ -0,0 +1,55 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "TobiiGTOMOcclusionTester.h" +#include "TobiiGazeFocusableWidget.h" +#include "TobiiGTOMTypes.h" +#include "tobii_g2om.h" + +#include "CoreMinimal.h" +#include "Runtime/InputDevice/Public/IInputDevice.h" + +class TOBIIGTOM_API FTobiiGTOMEngine : public IInputDevice +{ +public: + FTobiiGTOMEngine(); + virtual ~FTobiiGTOMEngine(); + + FHitResult CombinedWorldGazeHitData; + TWeakObjectPtr GTOMPlayerController; + + const TArray& GetFocusData() { return G2OMFocusResults; } + void EmulateGazeFocus(TArray& EmulatedFocusData); + +// IInputDevice +public: + virtual void Tick(float DeltaTime) override; + virtual void SendControllerEvents() override { } + virtual void SetMessageHandler(const TSharedRef& InMessageHandler) override { } + virtual bool Exec(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar) override { return true; } + virtual void SetChannelValue(int32 ControllerId, FForceFeedbackChannelType ChannelType, float Value) override { } + virtual void SetChannelValues(int32 ControllerId, const FForceFeedbackValues &values) override { } + +private: + g2om_context* G2OMContext; + g2om_raycast_result G2OMRaycastResults; + g2om_gaze_data G2OMGazeData; + FTobiiGTOMOcclusionTester OcclusionTester; + TArray G2OMFocusResults; + + TWeakObjectPtr PreviouslyFocusedPrimitiveComponent; + TWeakObjectPtr PreviouslyFocusedWidget; + + void UpdateWinners(UPrimitiveComponent* NewTopFocusPrimitive, UTobiiGazeFocusableWidget* NewTopFocusWidget); + + void NotifyPrimitiveComponentGazeFocusReceived(UPrimitiveComponent& PrimitiveComponentToReceiveFocus); + void NotifyPrimitiveComponentGazeFocusLost(UPrimitiveComponent& PrimitiveComponentToLoseFocus); + void NotifyWidgetGazeFocusReceived(UTobiiGazeFocusableWidget& WidgetToReceiveFocus); + void NotifyWidgetGazeFocusLost(UTobiiGazeFocusableWidget& WidgetToLoseFocus); + void FillG2OMRaycast(const FHitResult& HitResult, const FVector2D& ScreenGazePointUNorm, const FVector& GazeDirection, g2om_raycast& RaycastToModify); +}; diff --git a/Source/TobiiGTOM/Private/TobiiGTOMInternalTypes.h b/Source/TobiiGTOM/Private/TobiiGTOMInternalTypes.h new file mode 100644 index 0000000..60047b2 --- /dev/null +++ b/Source/TobiiGTOM/Private/TobiiGTOMInternalTypes.h @@ -0,0 +1,107 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "tobii_g2om.h" + +#include "CoreMinimal.h" + +static class FTobiiGTOMUtils +{ +public: + static g2om_vector UE4VectorToG2OMVector(const FVector& Input) + { + return g2om_vector { Input.X, Input.Y, Input.Z }; + } + + static FVector G2OMVectorToUE4Vector(const g2om_vector& Input) + { + return FVector(Input.x, Input.y, Input.z); + } + + static void UE4SpaceToUnitySpace(g2om_vector& Arg) + { + //Unity forward is Z, right is X, up is Y + //UE4 forward is X, right is Y, up is Z + + const float Temp = Arg.z; + Arg.z = Arg.x; + Arg.x = Arg.y; + Arg.y = Temp; + } + + static void UnitySpaceToUE4Space(g2om_vector& Arg) + { + //Unity forward is Z, right is X, up is Y + //UE4 forward is X, right is Y, up is Z + + const float Temp = Arg.x; + Arg.x = Arg.z; + Arg.z = Arg.y; + Arg.y = Temp; + } + + static bool QueryFocusableLayer(UPrimitiveComponent* Candidate, const FName& LayerToQuery) + { + FName FocusLayer = UTobiiGazeFocusableComponent::GetFocusLayerForPrimitive(Candidate); + if (FocusLayer == FName("Default")) + { + return true; + } + + return FocusLayer == LayerToQuery; + } + static bool QueryFocusableLayer(UTobiiGazeFocusableWidget* Candidate, const FName& LayerToQuery) + { + FName FocusLayer = UTobiiGazeFocusableComponent::GetFocusLayerForWidget(Candidate); + if (FocusLayer == FName("Default")) + { + return true; + } + + return FocusLayer == LayerToQuery; + } + + template + static bool ValidateFocusLayers(const TArray& FocusLayerFilterList, const bool bIsWhiteList, T* Candidate) + { + if (Candidate == nullptr) + { + return false; + } + + if (bIsWhiteList) + { + //A white list only allows focusables that have a layer that is in the list. + for (auto& Layer : FocusLayerFilterList) + { + if (QueryFocusableLayer(Candidate, Layer)) + { + return true; + } + } + } + else + { + //A black list will block any focusables whos layer is in the list. + for (auto& Layer : FocusLayerFilterList) + { + if (QueryFocusableLayer(Candidate, Layer)) + { + return false; + } + } + + //No filters hit, this object is ok. + return true; + } + + return false; + } +}; + +DEFINE_LOG_CATEGORY_STATIC(LogTobiiGTOM, All, All); diff --git a/Source/TobiiGTOM/Private/TobiiGTOMModule.cpp b/Source/TobiiGTOM/Private/TobiiGTOMModule.cpp new file mode 100644 index 0000000..de0e1af --- /dev/null +++ b/Source/TobiiGTOM/Private/TobiiGTOMModule.cpp @@ -0,0 +1,59 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#include "TobiiGTOMModule.h" +#include "TobiiGazeFocusableComponent.h" +#include "TobiiGazeFocusableWidget.h" +#include "TobiiGTOMBlueprintLibrary.h" +#include "TobiiGTOMInternalTypes.h" +#include "TobiiGTOMEngine.h" + +#include "IEyeTracker.h" +#include "DrawDebugHelpers.h" +#include "GameFramework/PlayerController.h" +#include "Misc/Paths.h" +#include "Runtime/Engine/Public/Slate/SceneViewport.h" + +IMPLEMENT_MODULE(FTobiiGTOMModule, TobiiGTOM) + +void FTobiiGTOMModule::StartupModule() +{ + G2OMDllHandle = nullptr; + +#if TOBII_EYETRACKING_ACTIVE + FString RelativeG2OMDllPath = FString(TEXT(TOBII_G2OM_RELATIVE_DLL_PATH)); + +#if TOBII_COMPILE_AS_ENGINE_PLUGIN + FString FullG2OMDllPath = FPaths::Combine(FPaths::EngineDir(), RelativeG2OMDllPath); +#else + FString FullG2OMDllPath = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::ProjectPluginsDir(), RelativeG2OMDllPath)); +#endif //TOBII_COMPILE_AS_ENGINE_PLUGIN + + G2OMDllHandle = FPlatformProcess::GetDllHandle(*FullG2OMDllPath); + + if (G2OMDllHandle != nullptr) + { + IModularFeatures::Get().RegisterModularFeature(IInputDeviceModule::GetModularFeatureName(), this); + } +#endif //TOBII_EYETRACKING_ACTIVE +} + +void FTobiiGTOMModule::ShutdownModule() +{ +#if TOBII_EYETRACKING_ACTIVE + if (G2OMDllHandle != nullptr) + { + IModularFeatures::Get().UnregisterModularFeature(IInputDeviceModule::GetModularFeatureName(), this); + } + G2OMDllHandle = nullptr; +#endif //TOBII_EYETRACKING_ACTIVE +} + +TSharedPtr FTobiiGTOMModule::CreateInputDevice(const TSharedRef& InMessageHandler) +{ + GTOMInputDevice = MakeShareable(new FTobiiGTOMEngine()); + return TSharedPtr(GTOMInputDevice); +} diff --git a/Source/TobiiGTOM/Private/TobiiGTOMModule.h b/Source/TobiiGTOM/Private/TobiiGTOMModule.h new file mode 100644 index 0000000..7e275a0 --- /dev/null +++ b/Source/TobiiGTOM/Private/TobiiGTOMModule.h @@ -0,0 +1,35 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "CoreMinimal.h" +#include "Runtime/InputDevice/Public/IInputDeviceModule.h" +#include "Modules/ModuleManager.h" + +class TOBIIGTOM_API FTobiiGTOMModule : public IInputDeviceModule +{ +public: + static inline FTobiiGTOMModule& Get() + { + return FModuleManager::LoadModuleChecked("TobiiGTOM"); + } + + static inline bool IsAvailable() + { + return FModuleManager::Get().IsModuleLoaded("TobiiGTOM"); + } + +public: + TSharedPtr GTOMInputDevice; + + virtual void StartupModule() override; + virtual void ShutdownModule() override; + virtual TSharedPtr< class IInputDevice > CreateInputDevice(const TSharedRef< FGenericApplicationMessageHandler >& InMessageHandler); + +private: + void* G2OMDllHandle; +}; diff --git a/Source/TobiiGTOM/Private/TobiiGTOMOcclusionTester.cpp b/Source/TobiiGTOM/Private/TobiiGTOMOcclusionTester.cpp new file mode 100644 index 0000000..bde22eb --- /dev/null +++ b/Source/TobiiGTOM/Private/TobiiGTOMOcclusionTester.cpp @@ -0,0 +1,203 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#if TOBII_EYETRACKING_ACTIVE + +#include "TobiiGTOMOcclusionTester.h" +#include "TobiiGazeFocusableComponent.h" +#include "TobiiGTOMInternalTypes.h" +#include "tobii_g2om.h" + +#include "DrawDebugHelpers.h" +#include "Components/PrimitiveComponent.h" +#include "Engine/Engine.h" +#include "Engine/LocalPlayer.h" + +#define TOBII_MAX_RAYS_PER_FRAME (15) + +static TAutoConsoleVariable CVarOcclusionTesterTrackingBlastMode(TEXT("tobii.gtom.OcclusionTesterTrackingBlastMode"), (int32)ETobiiTrackingBlastMode::OnlyTrackMissedObjects, TEXT("0 - Do not use tracking blasts. This will be very fast, but quite unstable. 1 - Only do tracking blasts when an object was not hit by a shotgun pellet to reduce hysteres somewhat. Prefer this mode if you have a lot of small objects constantly in the active set and mode two is working too slowly. 2 - Do tracking blasts for every focus component in the active group. This greatly helps reduce hysteres and leads to a LOT more stable last visible locations by trading off performance since it will require an additional extra line cast per active focus object and is such O(n).")); +static TAutoConsoleVariable CVarOcclusionTesterTracesPerSecond(TEXT("tobii.gtom.OcclusionTesterTracesPerSecond"), 700.0f, TEXT("This is the approximate number of additional line casts (in addition to the center one) that will be performed per second. Setting this to zero will make the shotgun behave like a normal line scorer. A higher number will improve reliability at the cost of performance.")); +static TAutoConsoleVariable CVarOcclusionTesterTimeToLive(TEXT("tobii.gtom.OcclusionTesterTimeToLive"), 0.5f, TEXT("This is how long an object will be kept in the visibility set given it hasn't been detected. Having this too high can make the occlusion testing system output objects that are no longer visible. Having it too low may lead to hysteres.")); + +static TAutoConsoleVariable CVarDebugDisplayGTOMVisibilityRays(TEXT("tobii.debug.DisplayGTOMVisibilityRays"), 1, TEXT("1 means we visualize the rays from GTOM")); + +g2om_gaze_ray RaysThisFrame[TOBII_MAX_RAYS_PER_FRAME]; + +FTobiiGTOMOcclusionTester::FTobiiGTOMOcclusionTester() +{ +} + +const TMap& FTobiiGTOMOcclusionTester::Tick(float DeltaTimeSecs, APlayerController* PlayerController, const FEyeTrackerGazeData& GazeData, g2om_gaze_data& G2OMGazeData, g2om_context* G2OMContext) +{ + QUICK_SCOPE_CYCLE_COUNTER(STAT_TobiiEyetracking_GTOM_OcclusionTesting); + + if (GEngine == nullptr + || GEngine->GameViewport == nullptr + || PlayerController == nullptr + || PlayerController->GetWorld() == nullptr) + { + VisibleSet.Empty(); + return VisibleSet; + } + + static const auto FovealAngleDegCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.FovealConeAngleDegrees")); + const float FovealAngleDeg = FovealAngleDegCVar->GetFloat(); + + const ETobiiTrackingBlastMode TrackingBlastMode = (ETobiiTrackingBlastMode)CVarOcclusionTesterTrackingBlastMode.GetValueOnGameThread(); + const int32 NrCastsThisFrame = FMath::Clamp(FMath::CeilToInt(CVarOcclusionTesterTracesPerSecond.GetValueOnGameThread() * DeltaTimeSecs), 3, TOBII_MAX_RAYS_PER_FRAME); + const FDateTime UtcNow = FDateTime::UtcNow(); + + FMemory::Memzero(RaysThisFrame, TOBII_MAX_RAYS_PER_FRAME * sizeof(g2om_gaze_ray)); + g2om_get_candidate_search_pattern(G2OMContext, &G2OMGazeData, NrCastsThisFrame, RaysThisFrame); + + FCollisionQueryParams CollisionQueryParams; + CollisionQueryParams.AddIgnoredActor(PlayerController); + CollisionQueryParams.AddIgnoredActor(PlayerController->GetPawn()); + + //Do shotgun blast + { + QUICK_SCOPE_CYCLE_COUNTER(STAT_TobiiEyetracking_GTOM_OcclusionTesting_Shotgun); + + for (int32 RayIdx = 0; RayIdx < NrCastsThisFrame; RayIdx++) + { + g2om_gaze_ray& G2OMRay = RaysThisFrame[RayIdx]; + if (!G2OMRay.is_valid) + { + continue; + } + + FVector CurrentDirection = FTobiiGTOMUtils::G2OMVectorToUE4Vector(G2OMRay.ray.direction); + if (CurrentDirection.Normalize()) + { + TestRay(UtcNow, PlayerController, GazeData.GazeOrigin, CurrentDirection, CollisionQueryParams); + } + } + } + + //Do tracking rays + if (TrackingBlastMode != ETobiiTrackingBlastMode::NoTrackingBlasts) + { + QUICK_SCOPE_CYCLE_COUNTER(STAT_TobiiEyetracking_GTOM_OcclusionTesting_TrackingBlasts); + + //First we should check if our previous objects are still visible using so called "tracking blasts". + //This is both to provide more stable last known locations as well as to avoid hysteresis. + for (auto RecordIterator = VisibleSet.CreateIterator(); RecordIterator; ++RecordIterator) + { + const FTobiiGTOMOcclusionData& Record = RecordIterator.Value(); + + if (TrackingBlastMode == ETobiiTrackingBlastMode::OnlyTrackMissedObjects && Record.LastHitTime == UtcNow) + { + continue; + } + + FVector TrackingBlastDirection = Record.LastKnownVisibleWorldSpaceLocation - GazeData.GazeOrigin; + if (TrackingBlastDirection.Normalize()) + { + const float DotBetweenBlastDirAndGaze = FVector::DotProduct(GazeData.GazeDirection, TrackingBlastDirection); + const float AngleBetweenObjectAndGazeDeg = FMath::RadiansToDegrees(FMath::Acos(DotBetweenBlastDirAndGaze)); + + if (AngleBetweenObjectAndGazeDeg > FovealAngleDeg) + { + //We need to clamp the rotation amount. + FVector RotationAxis = FVector::CrossProduct(GazeData.GazeDirection, TrackingBlastDirection); + RotationAxis.Normalize(); + TrackingBlastDirection = GazeData.GazeDirection.RotateAngleAxis(FovealAngleDeg, RotationAxis); + } + + TestRay(UtcNow, PlayerController, GazeData.GazeOrigin, TrackingBlastDirection, CollisionQueryParams); + } + } + } + + //Decay old objects + { + QUICK_SCOPE_CYCLE_COUNTER(STAT_TobiiEyetracking_GTOM_OcclusionTesting_Decay); + + TArray DecayedIds; + for (auto RecordIterator = VisibleSet.CreateIterator(); RecordIterator; ++RecordIterator) + { + const FTobiiGTOMOcclusionData& Record = RecordIterator.Value(); + if ((UtcNow - Record.LastHitTime).GetTotalSeconds() > CVarOcclusionTesterTimeToLive.GetValueOnGameThread()) + { + DecayedIds.Add(RecordIterator.Key()); + } + } + + for (FEngineFocusableUID DecayID : DecayedIds) + { + VisibleSet.Remove(DecayID); + } + } + + return VisibleSet; +} + + + +void FTobiiGTOMOcclusionTester::TestRay(const FDateTime& UtcNow, APlayerController* PlayerController, const FVector& Origin, const FVector& Direction, const FCollisionQueryParams& Params) +{ + static const auto DrawDebugCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.debug")); + const float DebugBoxSize = 2.0f; + + static const auto FocusTraceChannelCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.FocusTraceChannel")); + static const auto MaximumTraceDistanceCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.MaximumTraceDistance")); + const float MaxTraceDistance = MaximumTraceDistanceCVar->GetFloat(); + const ECollisionChannel TraceChannel = (ECollisionChannel)FocusTraceChannelCVar->GetInt(); + + const FVector RayEndPoint = Origin + (Direction * MaxTraceDistance); + + FHitResult RayHitResult; + if (PlayerController->GetWorld()->LineTraceSingleByChannel(RayHitResult, Origin, RayEndPoint, TraceChannel, Params)) + { + if (DrawDebugCVar->GetInt() != 0 && CVarDebugDisplayGTOMVisibilityRays.GetValueOnGameThread()) + { + DrawDebugBox(PlayerController->GetWorld(), RayHitResult.Location, FVector(DebugBoxSize, DebugBoxSize, DebugBoxSize), FColor::Green, false, 0.0f, 0, 2.0f); + } + + if (UTobiiGazeFocusableComponent::IsPrimitiveFocusable(RayHitResult.GetComponent())) + { + //Range test + bool bIsInRange = true; + float MaxDistance; + const float DistanceToPrimitive = FVector::Distance(Origin, RayHitResult.Location); + if (UTobiiGazeFocusableComponent::GetMaxFocusDistanceForPrimitive(RayHitResult.GetComponent(), MaxDistance)) + { + bIsInRange = DistanceToPrimitive <= MaxDistance; + } + + if (bIsInRange) + { + FEngineFocusableUID Id = RayHitResult.GetComponent()->GetUniqueID(); + if (VisibleSet.Contains(Id)) + { + VisibleSet[Id].LastKnownVisibleWorldSpaceLocation = RayHitResult.Location; + VisibleSet[Id].LastHitTime = UtcNow; + } + else + { + FTobiiGTOMOcclusionData NewRecord; + NewRecord.ParentActor = RayHitResult.GetActor(); + NewRecord.PrimitiveComponent = RayHitResult.GetComponent(); + NewRecord.LastKnownVisibleWorldSpaceLocation = RayHitResult.Location; + NewRecord.LastHitTime = UtcNow; + + if (NewRecord.ParentActor.IsValid()) + { + NewRecord.FocusableComponent = (UTobiiGazeFocusableComponent*)NewRecord.ParentActor->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass()); + } + + VisibleSet.Add(Id, MoveTemp(NewRecord)); + } + } + } + } + else if (DrawDebugCVar->GetInt() != 0 && CVarDebugDisplayGTOMVisibilityRays.GetValueOnGameThread()) + { + DrawDebugBox(PlayerController->GetWorld(), RayEndPoint, FVector(DebugBoxSize, DebugBoxSize, DebugBoxSize), FColor::Red, false, 0.0f, 0, 2.0f); + } +} + +#endif //TOBII_EYETRACKING_ACTIVE diff --git a/Source/TobiiGTOM/Private/TobiiGTOMOcclusionTester.h b/Source/TobiiGTOM/Private/TobiiGTOMOcclusionTester.h new file mode 100644 index 0000000..a23dffd --- /dev/null +++ b/Source/TobiiGTOM/Private/TobiiGTOMOcclusionTester.h @@ -0,0 +1,50 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#if TOBII_EYETRACKING_ACTIVE + +#include "TobiiGazeFocusableComponent.h" +#include "tobii_g2om.h" + +#include "CoreMinimal.h" +#include "IEyeTracker.h" +#include "GameFramework/PlayerController.h" + +enum class ETobiiTrackingBlastMode : uint8 +{ + NoTrackingBlasts = 0 + , OnlyTrackMissedObjects = 1 + , TrackAllObjects = 2 +}; + +struct FTobiiGTOMOcclusionData +{ +public: + TWeakObjectPtr ParentActor; + TWeakObjectPtr PrimitiveComponent; + TWeakObjectPtr FocusableComponent; + FVector LastKnownVisibleWorldSpaceLocation; + FDateTime LastHitTime; +}; + +class FTobiiGTOMOcclusionTester +{ +public: + FTobiiGTOMOcclusionTester(); + virtual ~FTobiiGTOMOcclusionTester() {} + + //Returns the visible set + const TMap& Tick(float DeltaTimeSecs, APlayerController* PlayerController, const FEyeTrackerGazeData& GazeData, g2om_gaze_data& G2OMGazeData, g2om_context* G2OMContext); + +private: + TMap VisibleSet; + + void TestRay(const FDateTime& UtcNow, APlayerController* PlayerController, const FVector& Origin, const FVector& Direction, const FCollisionQueryParams& Params); +}; + +#endif //TOBII_EYETRACKING_ACTIVE diff --git a/Source/TobiiGTOM/Private/TobiiGTOMTypes.cpp b/Source/TobiiGTOM/Private/TobiiGTOMTypes.cpp new file mode 100644 index 0000000..e93a18c --- /dev/null +++ b/Source/TobiiGTOM/Private/TobiiGTOMTypes.cpp @@ -0,0 +1,20 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "TobiiGTOMTypes.h" + +FName FTobiiPrimitiveComponentGazeFocusTags::HasGazeFocusTag("HasGazeFocus"); +FName FTobiiPrimitiveComponentGazeFocusTags::GazeFocusableTag("GazeFocusable"); +FName FTobiiPrimitiveComponentGazeFocusTags::NotGazeFocusableTag("NotGazeFocusable"); +FString FTobiiPrimitiveComponentGazeFocusTags::GazeFocusablePriorityTag("GazeFocusPriority"); +FString FTobiiPrimitiveComponentGazeFocusTags::GazeFocusableMaximumDistanceTag("GazeFocusMaxDistance"); +FString FTobiiPrimitiveComponentGazeFocusTags::GazeFocusableLayerTag("GazeFocusLayer"); + +FString FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetXTag("PrimitiveFocusOffsetX"); +FString FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetYTag("PrimitiveFocusOffsetY"); +FString FTobiiPrimitiveComponentGazeFocusTags::PrimitiveFocusOffsetZTag("PrimitiveFocusOffsetZ"); diff --git a/Source/TobiiGTOM/Private/TobiiGazeFocusManagerComponent.cpp b/Source/TobiiGTOM/Private/TobiiGazeFocusManagerComponent.cpp new file mode 100644 index 0000000..c3e4f42 --- /dev/null +++ b/Source/TobiiGTOM/Private/TobiiGazeFocusManagerComponent.cpp @@ -0,0 +1,137 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#include "TobiiGazeFocusManagerComponent.h" +#include "TobiiGazeFocusableComponent.h" +#include "TobiiGTOMModule.h" +#include "TobiiGTOMBlueprintLibrary.h" + +#include "DrawDebugHelpers.h" +#include "Components/PrimitiveComponent.h" +#include "Engine/Engine.h" +#include "HAL/IConsoleManager.h" + + +UTobiiGazeFocusManagerComponent::UTobiiGazeFocusManagerComponent() + : bWantPrimitives(true) + , bWantWidgets(true) + , bIsWhiteList(false) + , FocusLayerFilters() + , PreviouslyFocusedPrimitiveComponent() +{ + PrimaryComponentTick.bCanEverTick = true; + ResetFocusData(); +} + +void UTobiiGazeFocusManagerComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) +{ + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + + ResetFocusData(); + + UTobiiGTOMBlueprintLibrary::GetAllFilteredGazeFocusData(FocusLayerFilters, bIsWhiteList, bWantPrimitives, bWantWidgets, AllFocusData); + for (const FTobiiGazeFocusData& FocusData : AllFocusData) + { + if (bWantPrimitives + && !BestPrimitiveComponentFocusData.FocusedPrimitiveComponent.IsValid() + && (FocusData.FocusedPrimitiveComponent.IsValid() && !FocusData.FocusedWidget.IsValid())) + { + BestPrimitiveComponentFocusData = FocusData; + } + + if (bWantWidgets + && !BestWidgetFocusData.FocusedWidget.IsValid() + && FocusData.FocusedWidget.IsValid()) + { + BestWidgetFocusData = FocusData; + } + + if((!bWantPrimitives || (BestPrimitiveComponentFocusData.FocusedPrimitiveComponent.IsValid() && !BestPrimitiveComponentFocusData.FocusedWidget.IsValid())) + && (!bWantWidgets || BestWidgetFocusData.FocusedWidget.IsValid())) + { + //We found the best candidates we were after + break; + } + } + + //Primitive components + if(bWantPrimitives) + { + TWeakObjectPtr NewTopFocusPrimitive = BestPrimitiveComponentFocusData.FocusedPrimitiveComponent; + + if (!NewTopFocusPrimitive.IsValid() && PreviouslyFocusedPrimitiveComponent.IsValid()) + { + //We lost focus completely + OnPrimitiveLostGazeFocus.Broadcast(PreviouslyFocusedPrimitiveComponent.Get()); + } + else if (NewTopFocusPrimitive.IsValid() && !PreviouslyFocusedPrimitiveComponent.IsValid()) + { + //We gained focus from no focus + OnPrimitiveReceivedGazeFocus.Broadcast(NewTopFocusPrimitive.Get()); + } + else if (NewTopFocusPrimitive.IsValid() && PreviouslyFocusedPrimitiveComponent.IsValid() && NewTopFocusPrimitive.Get() != PreviouslyFocusedPrimitiveComponent.Get()) + { + //Focus shifted from one object to another + OnPrimitiveLostGazeFocus.Broadcast(PreviouslyFocusedPrimitiveComponent.Get()); + OnPrimitiveReceivedGazeFocus.Broadcast(NewTopFocusPrimitive.Get()); + } + + PreviouslyFocusedPrimitiveComponent = NewTopFocusPrimitive; + } + + //Widgets + if(bWantWidgets) + { + TWeakObjectPtr NewTopFocusWidget = BestPrimitiveComponentFocusData.FocusedWidget; + + if (!NewTopFocusWidget.IsValid() && PreviouslyFocusedWidget.IsValid()) + { + //We lost focus completely + OnWidgetLostGazeFocus.Broadcast(PreviouslyFocusedWidget.Get()); + } + else if (NewTopFocusWidget.IsValid() && !PreviouslyFocusedWidget.IsValid()) + { + //We gained focus from no focus + OnWidgetReceivedGazeFocus.Broadcast(NewTopFocusWidget.Get()); + } + else if (NewTopFocusWidget.IsValid() && PreviouslyFocusedWidget.IsValid() && NewTopFocusWidget.Get() != PreviouslyFocusedWidget.Get()) + { + //Focus shifted from one object to another + OnWidgetLostGazeFocus.Broadcast(PreviouslyFocusedWidget.Get()); + OnWidgetReceivedGazeFocus.Broadcast(NewTopFocusWidget.Get()); + } + + PreviouslyFocusedWidget = NewTopFocusWidget; + } +} + +void UTobiiGazeFocusManagerComponent::ResetFocusData() +{ + AllFocusData.Empty(); + + BestPrimitiveComponentFocusData.FocusedPrimitiveComponent.Reset(); + BestPrimitiveComponentFocusData.FocusedActor.Reset(); + BestPrimitiveComponentFocusData.FocusedWidget.Reset(); + + BestWidgetFocusData.FocusedPrimitiveComponent.Reset(); + BestWidgetFocusData.FocusedActor.Reset(); + BestWidgetFocusData.FocusedWidget.Reset(); +} + +void UTobiiGazeFocusManagerComponent::GetBestFilteredPrimitiveComponentFocusData(FTobiiGazeFocusData& OutFocusData) const +{ + OutFocusData = BestPrimitiveComponentFocusData; +} + +void UTobiiGazeFocusManagerComponent::GetBestFilteredWidgetComponentFocusData(FTobiiGazeFocusData& OutFocusData) const +{ + OutFocusData = BestWidgetFocusData; +} + +void UTobiiGazeFocusManagerComponent::GetAllFilteredFocusData(TArray& OutFocusData) const +{ + OutFocusData = AllFocusData; +} diff --git a/Source/TobiiGTOM/Private/TobiiGazeFocusableComponent.cpp b/Source/TobiiGTOM/Private/TobiiGazeFocusableComponent.cpp new file mode 100644 index 0000000..60de233 --- /dev/null +++ b/Source/TobiiGTOM/Private/TobiiGazeFocusableComponent.cpp @@ -0,0 +1,504 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#include "TobiiGazeFocusableComponent.h" +#include "TobiiGTOMBlueprintLibrary.h" +#include "TobiiGTOMModule.h" + +#include "DrawDebugHelpers.h" +#include "Engine/Engine.h" +#include "Components/PrimitiveComponent.h" +#include "Components/WidgetComponent.h" +#include "GameFramework/PlayerController.h" +#include "HAL/IConsoleManager.h" + +static TAutoConsoleVariable CVarDrawCleanUIDebug(TEXT("tobii.debug.CleanUI"), 1, TEXT("0 - CleanUI debug visualizations are not displayed. 1 - CleanUI debug visualizations are displayed.")); + +static TMap> GRegisteredTobiiFocusableComponents; +static TSet* GazeFocusPrioSetA = nullptr; +static TSet* GazeFocusPrioSetB = nullptr; +static bool bUseGazeFocusPrioSetA = true; + +void UTobiiGazeFocusableComponent::ClearFocusableComponents() +{ + GRegisteredTobiiFocusableComponents.Empty(); +} + +const TMap>& UTobiiGazeFocusableComponent::GetFocusableComponents() +{ + return GRegisteredTobiiFocusableComponents; +} + +void UTobiiGazeFocusableComponent::UpdateGazeFocusPrio(FVector POVActorLocation, int32 MaxNrFocusables) +{ + TMap PrioMap; + TSet NonPrioSet; + + //Determine which primitive components should be considered for gaze focus. + for (auto& FocusableElement : GRegisteredTobiiFocusableComponents) + { + if (FocusableElement.Value.IsValid()) + { + const UTobiiGazeFocusableComponent* FocusableComponent = FocusableElement.Value.Get(); + const AActor* FocusableActor = FocusableComponent->GetOwner(); + if (FocusableActor != nullptr) + { + TArray PrimitiveComponents = FocusableActor->GetComponentsByClass(UPrimitiveComponent::StaticClass()); + + + //TArray PrimativeComponents = FocusableActor->GetComponents + for (UActorComponent* Comp : PrimitiveComponents) + { + UPrimitiveComponent* PrimitiveComponent = (UPrimitiveComponent*)Comp; + if (PrimitiveComponent == nullptr) + { + continue; + } + + bool bSkipComponent = false; + float MaxDistance = 0.0f; + float Priority = 0.0f; + for (const FName& CurrentTag : PrimitiveComponent->ComponentTags) + { + if (CurrentTag == FTobiiPrimitiveComponentGazeFocusTags::NotGazeFocusableTag) + { + bSkipComponent = true; + break; + } + + FString CurrentTagString = CurrentTag.ToString(); + if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusableMaximumDistanceTag)) + { + FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusableMaximumDistanceTag.Len()); + MaxDistance = FCString::Atof(*Argument); + } + if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusablePriorityTag)) + { + FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusablePriorityTag.Len()); + Priority = FCString::Atof(*Argument); + } + } + + if (bSkipComponent) + { + continue; + } + + if (MaxDistance == 0.0f) + { + MaxDistance = FocusableComponent->DefaultMaxFocusDistance; + } + if (Priority == 0.0f) + { + Priority = FocusableComponent->DefaultFocusPriority; + } + + bool bAcceptComponent = true; + if (MaxDistance != 0.0f) + { + float Distance = FVector::Distance(POVActorLocation, PrimitiveComponent->GetComponentLocation()); + if (Distance > MaxDistance) + { + bAcceptComponent = false; + } + } + + if (bAcceptComponent) + { + if (Priority == 0.0f) + { + NonPrioSet.Add(PrimitiveComponent->ComponentId); + } + else + { + PrioMap.Add(PrimitiveComponent->ComponentId, Priority); + } + } + } + } + } + } + + PrioMap.ValueSort(TGreater()); + + TSet* NewGazeFocusPrioSet = new TSet(); + MaxNrFocusables -= 1; //Since we test before add, take that into account + for (auto& SortedElement : PrioMap) + { + if (NewGazeFocusPrioSet->Num() >= MaxNrFocusables) + { + break; + } + NewGazeFocusPrioSet->Add(SortedElement.Key); + } + for (auto NonPrioElement : NonPrioSet) + { + if (NewGazeFocusPrioSet->Num() >= MaxNrFocusables) + { + break; + } + NewGazeFocusPrioSet->Add(NonPrioElement); + } + + //Destroy old set, set our new one and flip our record pointer so the one we swapped into becomes the active one. + TSet*& ListPtr = bUseGazeFocusPrioSetA ? GazeFocusPrioSetB : GazeFocusPrioSetA; + if (ListPtr != nullptr) + { + delete ListPtr; + } + ListPtr = NewGazeFocusPrioSet; + bUseGazeFocusPrioSetA = !bUseGazeFocusPrioSetA; +} + +const TSet* UTobiiGazeFocusableComponent::GetGazeFocusPrioSet() +{ + return bUseGazeFocusPrioSetA ? GazeFocusPrioSetA : GazeFocusPrioSetB; +} + +bool UTobiiGazeFocusableComponent::IsPrimitiveFocusable(UPrimitiveComponent* Primitive) +{ + if (Primitive == nullptr || !Primitive->IsVisible() || Primitive->ComponentHasTag(FTobiiPrimitiveComponentGazeFocusTags::NotGazeFocusableTag)) + { + //Override disabled + return false; + } + else if (Primitive->ComponentHasTag(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusableTag)) + { + //Override enabled + return true; + } + else if (Primitive->GetOwner()) + { + UTobiiGazeFocusableComponent* GazeFocusable = (UTobiiGazeFocusableComponent*)Primitive->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass()); + if (GazeFocusable != nullptr && GazeFocusable->bDefaultIsFocusable) + { + return true; + } + } + + return false; +} + +bool UTobiiGazeFocusableComponent::GetMaxFocusDistanceForPrimitive(UPrimitiveComponent* Primitive, float& OutMaxDistance) +{ + if (Primitive == nullptr) + { + return false; + } + + bool bMaxDistanceFound = false; + + for (const FName& CurrentTag : Primitive->ComponentTags) + { + FString CurrentTagString = CurrentTag.ToString(); + if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusableMaximumDistanceTag)) + { + FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusableMaximumDistanceTag.Len()); + OutMaxDistance = FCString::Atof(*Argument); + bMaxDistanceFound = OutMaxDistance != 0.0f; //Atof returns 0.0 on failure + } + } + + if (!bMaxDistanceFound && Primitive->GetOwner()) + { + UTobiiGazeFocusableComponent* GazeFocusableComponent = (UTobiiGazeFocusableComponent*)Primitive->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass()); + if (GazeFocusableComponent != nullptr && GazeFocusableComponent->DefaultMaxFocusDistance > FLT_EPSILON) + { + OutMaxDistance = GazeFocusableComponent->DefaultMaxFocusDistance; + bMaxDistanceFound = true; + } + } + + return bMaxDistanceFound; +} + +float UTobiiGazeFocusableComponent::GetFocusPriorityForPrimitive(UPrimitiveComponent* Primitive) +{ + float Priority = 0.0f; + bool bPriorityFound = false; + if (Primitive == nullptr) + { + return Priority; + } + + for (const FName& CurrentTag : Primitive->ComponentTags) + { + FString CurrentTagString = CurrentTag.ToString(); + if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusablePriorityTag)) + { + FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusablePriorityTag.Len()); + Priority = FCString::Atof(*Argument); + bPriorityFound = Priority != 0.0f; //Atof returns 0.0 on failure + } + } + + if (!bPriorityFound && Primitive->GetOwner()) + { + UTobiiGazeFocusableComponent* GazeFocusableComponent = (UTobiiGazeFocusableComponent*)Primitive->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass()); + if (GazeFocusableComponent != nullptr) + { + Priority = GazeFocusableComponent->DefaultFocusPriority; + } + } + + return Priority; +} + +FName UTobiiGazeFocusableComponent::GetFocusLayerForPrimitive(UPrimitiveComponent* Primitive) +{ + FName FocusLayer = FName("Default"); + bool bFocusLayerFound = false; + if (Primitive == nullptr) + { + return FocusLayer; + } + + for (const FName& CurrentTag : Primitive->ComponentTags) + { + FString CurrentTagString = CurrentTag.ToString(); + if (CurrentTagString.StartsWith(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusableLayerTag)) + { + FString Argument = CurrentTagString.RightChop(FTobiiPrimitiveComponentGazeFocusTags::GazeFocusableLayerTag.Len()); + Argument.TrimStartAndEndInline(); + if (!Argument.IsEmpty()) + { + FocusLayer = FName(*Argument); + bFocusLayerFound = true; + } + } + } + + if (!bFocusLayerFound && Primitive->GetOwner()) + { + UTobiiGazeFocusableComponent* GazeFocusableComponent = (UTobiiGazeFocusableComponent*)Primitive->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass()); + if (GazeFocusableComponent != nullptr) + { + FocusLayer = GazeFocusableComponent->DefaultFocusLayer; + } + } + + return FocusLayer; +} + + +bool UTobiiGazeFocusableComponent::IsWidgetFocusable(UTobiiGazeFocusableWidget* GazeFocusableWidget) +{ + if (GazeFocusableWidget == nullptr || !GazeFocusableWidget->IsVisible() || !GazeFocusableWidget->bIsGazeFocusable) + { + return false; + } + else if (GazeFocusableWidget->GetWorldSpaceHostWidgetComponent() != nullptr && GazeFocusableWidget->GetWorldSpaceHostWidgetComponent()->GetOwner() != nullptr) + { + if (!GazeFocusableWidget->GetWorldSpaceHostWidgetComponent()->IsVisible()) + { + return false; + } + + UTobiiGazeFocusableComponent* GazeFocusable = (UTobiiGazeFocusableComponent*)GazeFocusableWidget->GetWorldSpaceHostWidgetComponent()->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass()); + if (GazeFocusable != nullptr) + { + return GazeFocusable->bDefaultIsFocusable; + } + } + + return true; +} + +bool UTobiiGazeFocusableComponent::GetMaxFocusDistanceForWidget(UTobiiGazeFocusableWidget* GazeFocusableWidget, float& OutMaxDistance) +{ + if (GazeFocusableWidget == nullptr) + { + return false; + } + + bool bMaxDistanceFound = false; + if (GazeFocusableWidget->MaxFocusDistance >= FLT_EPSILON) + { + OutMaxDistance = GazeFocusableWidget->MaxFocusDistance; + bMaxDistanceFound = true; + } + else if (GazeFocusableWidget->GetWorldSpaceHostWidgetComponent() != nullptr && GazeFocusableWidget->GetWorldSpaceHostWidgetComponent()->GetOwner() != nullptr) + { + UTobiiGazeFocusableComponent* GazeFocusableComponent = (UTobiiGazeFocusableComponent*)GazeFocusableWidget->GetWorldSpaceHostWidgetComponent()->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass()); + if (GazeFocusableComponent != nullptr && GazeFocusableComponent->DefaultMaxFocusDistance > FLT_EPSILON) + { + OutMaxDistance = GazeFocusableComponent->DefaultMaxFocusDistance; + bMaxDistanceFound = true; + } + } + + return bMaxDistanceFound; +} + +float UTobiiGazeFocusableComponent::GetFocusPriorityForWidget(UTobiiGazeFocusableWidget* GazeFocusableWidget) +{ + if (GazeFocusableWidget == nullptr) + { + return 0.0f; + } + + if (GazeFocusableWidget->GazeFocusPriority >= 0.0f) + { + return GazeFocusableWidget->GazeFocusPriority; + } + else if (GazeFocusableWidget->GetWorldSpaceHostWidgetComponent() != nullptr && GazeFocusableWidget->GetWorldSpaceHostWidgetComponent()->GetOwner() != nullptr) + { + UTobiiGazeFocusableComponent* GazeFocusableComponent = (UTobiiGazeFocusableComponent*)GazeFocusableWidget->GetWorldSpaceHostWidgetComponent()->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass()); + if (GazeFocusableComponent) + { + return GazeFocusableComponent->DefaultFocusPriority; + } + } + + return 0.0f; +} + +FName UTobiiGazeFocusableComponent::GetFocusLayerForWidget(UTobiiGazeFocusableWidget* GazeFocusableWidget) +{ + FName FocusLayer = FName("Default"); + if (GazeFocusableWidget == nullptr) + { + return FocusLayer; + } + + if (!GazeFocusableWidget->FocusLayer.IsEmpty()) + { + FocusLayer = FName(*GazeFocusableWidget->FocusLayer); + } + else if (GazeFocusableWidget->GetWorldSpaceHostWidgetComponent() != nullptr && GazeFocusableWidget->GetWorldSpaceHostWidgetComponent()->GetOwner() != nullptr) + { + UTobiiGazeFocusableComponent* GazeFocusableComponent = (UTobiiGazeFocusableComponent*)GazeFocusableWidget->GetWorldSpaceHostWidgetComponent()->GetOwner()->GetComponentByClass(UTobiiGazeFocusableComponent::StaticClass()); + if (GazeFocusableComponent != nullptr) + { + FocusLayer = GazeFocusableComponent->DefaultFocusLayer; + } + } + + return FocusLayer; +} + +////////////////////////////////////////////// + +UTobiiGazeFocusableComponent::UTobiiGazeFocusableComponent() + : bDefaultIsFocusable(true) + , DefaultFocusPriority(0.0f) + , DefaultMaxFocusDistance(0.0f) + , DefaultFocusLayer("Default") + + , bWidgetsRefreshedOnce(false) +{ + PrimaryComponentTick.bCanEverTick = true; +} + +void UTobiiGazeFocusableComponent::PrimitiveReceivedGazeFocus(UPrimitiveComponent* FocusedComponent) +{ + OnPrimitiveReceivedGazeFocus.Broadcast(FocusedComponent); +} + +void UTobiiGazeFocusableComponent::PrimitiveLostGazeFocus(UPrimitiveComponent* FocusedComponent) +{ + OnPrimitiveLostGazeFocus.Broadcast(FocusedComponent); +} + +void UTobiiGazeFocusableComponent::WidgetReceivedGazeFocus( UTobiiGazeFocusableWidget* FocusedWidget) +{ + OnWidgetReceivedGazeFocus.Broadcast(FocusedWidget); +} + +void UTobiiGazeFocusableComponent::WidgetLostGazeFocus(UTobiiGazeFocusableWidget* FocusedWidget) +{ + OnWidgetLostGazeFocus.Broadcast(FocusedWidget); +} + +const TArray>& UTobiiGazeFocusableComponent::GetFocusableWidgetsForPrimitiveComponent(UPrimitiveComponent* PrimitiveComponent) +{ + if (PrimitiveComponent == nullptr || !PrimitiveMap.Contains(PrimitiveComponent->GetUniqueID())) + { + static TArray> DefaultArray; + return DefaultArray; + } + + return PrimitiveMap[PrimitiveComponent->GetUniqueID()]; +} + +void UTobiiGazeFocusableComponent::BeginPlay() +{ + Super::BeginPlay(); + + bWidgetsRefreshedOnce = false; + GRegisteredTobiiFocusableComponents.Add(GetUniqueID(), this); +} + +void UTobiiGazeFocusableComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + GRegisteredTobiiFocusableComponents.Remove(GetUniqueID()); + + Super::EndPlay(EndPlayReason); +} + +void UTobiiGazeFocusableComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) +{ + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + + if (GEngine == nullptr + || GEngine->GameViewport == nullptr + || GEngine->GameViewport->GetGameViewport() == nullptr) + { + return; + } + + if (!bWidgetsRefreshedOnce) + { + bWidgetsRefreshedOnce = true; + RefreshOwnedWidgets(); + } +} + +void UTobiiGazeFocusableComponent::RefreshOwnedWidgets() +{ + AllFocusableWidgets.Empty(); + PrimitiveMap.Empty(); + + AActor* Owner = GetOwner(); + if (Owner != nullptr) + { + TArray WidgetComponents = Owner->GetComponentsByClass(UWidgetComponent::StaticClass()); + for (UActorComponent* Component : WidgetComponents) + { + UWidgetComponent* WidgetComponent = (UWidgetComponent*)Component; + TArray>& NewFocusableArray = PrimitiveMap.Add(WidgetComponent->GetUniqueID(), TArray>()); + GatherGazeFocusableWidgets(WidgetComponent->GetUserWidgetObject(), NewFocusableArray, WidgetComponent); + } + } +} + +void UTobiiGazeFocusableComponent::GatherGazeFocusableWidgets(UWidget* Parent, TArray>& WidgetArray, UWidgetComponent* OptionalHostWidgetComponent) +{ + UUserWidget* UserWidget = Cast(Parent); + if (UserWidget != nullptr) + { + GatherGazeFocusableWidgets(UserWidget->GetRootWidget(), WidgetArray, OptionalHostWidgetComponent); + } + else + { + UTobiiGazeFocusableWidget* CleanUIContainer = Cast(Parent); + if (CleanUIContainer != nullptr) + { + CleanUIContainer->RegisterWidgetToGTOM(OptionalHostWidgetComponent); + AllFocusableWidgets.Add(CleanUIContainer->GetUniqueID(), CleanUIContainer); + WidgetArray.Add(CleanUIContainer); + } + + UPanelWidget* Panel = Cast(Parent); + if (Panel != nullptr) + { + for (int32 ChildIdx = 0; ChildIdx < Panel->GetChildrenCount(); ChildIdx++) + { + GatherGazeFocusableWidgets(Panel->GetChildAt(ChildIdx), WidgetArray, OptionalHostWidgetComponent); + } + } + } +} diff --git a/Source/TobiiGTOM/Public/GTOMAwareUI/STobiiGazeFocusableWidget.h b/Source/TobiiGTOM/Public/GTOMAwareUI/STobiiGazeFocusableWidget.h new file mode 100644 index 0000000..e927dc4 --- /dev/null +++ b/Source/TobiiGTOM/Public/GTOMAwareUI/STobiiGazeFocusableWidget.h @@ -0,0 +1,90 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "Slate/SRetainerWidget.h" +#include "TobiiGTOMTypes.h" +#include "STobiiRadialMenuWidget.h" + +#include "CoreMinimal.h" +#include "DisplayDebugHelpers.h" +#include "Widgets/Layout/SBox.h" + +class AHUD; +class UCanvas; +class UTobiiGazeFocusableWidget; + +//This is the underlying slate widget. +//WARNING: Please note that we do not support using these directly yet. +class TOBIIGTOM_API STobiiGazeFocusableWidget : public SBox +{ +public: + SLATE_BEGIN_ARGS(STobiiGazeFocusableWidget) + { + _Visibility = EVisibility::SelfHitTestInvisible; + } + SLATE_EVENT(FSimpleDelegate, OnHovered) + SLATE_EVENT(FSimpleDelegate, OnUnhovered) + + /************************************************************************/ + /* From SBox widget */ + /************************************************************************/ + //Horizontal alignment of content in the area allotted to the SBox by its parent + SLATE_ARGUMENT(EHorizontalAlignment, HAlign) + + //Vertical alignment of content in the area allotted to the SBox by its parent + SLATE_ARGUMENT(EVerticalAlignment, VAlign) + + //Padding between the SBox and the content that it presents. Padding affects desired size. + SLATE_ATTRIBUTE(FMargin, Padding) + + //The widget content presented by the SBox + SLATE_DEFAULT_SLOT(FArguments, Content) + SLATE_END_ARGS() + + /************************************************************************/ + /* Tobii */ + /************************************************************************/ +public: + TWeakObjectPtr UMGWidget; + + STobiiGazeFocusableWidget(); + void Construct(const FArguments& InArgs); + + float GetCleanUIAlpha() { return CleanUIAlpha; } + bool IsHoveredByPointer() { return bIsHovered; } + bool HitByGaze(); + + void AddExtraCleanUIContainerToPollHitsFrom(STobiiGazeFocusableWidget* CleanUIContainerToPoll, bool PollGazeHits = true, bool PollMouseHits = true); + void RemoveExtraCleanUIContainerToPollHitsFrom(STobiiGazeFocusableWidget* CleanUIContainerToStopPolling); + + static STobiiGazeFocusableWidget* TryCastToTobiiGazeFocusable(SWidget* Widget); + + /************************************************************************/ + /* SBox */ + /************************************************************************/ +public: + FSimpleDelegate OnHovered; + FSimpleDelegate OnUnhovered; + + virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override; + virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override; + virtual void OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + virtual void OnMouseLeave(const FPointerEvent& MouseEvent) override; + +private: + struct CleanUIPollingInfo + { + TWeakPtr CleanUIToPollFrom; + bool PollGaze; + bool PollPointer; + }; + + float CleanUIAlpha; + + TArray CleanUIContainersToPollHitsFrom; +}; diff --git a/Source/TobiiGTOM/Public/GTOMAwareUI/STobiiRadialMenuWidget.h b/Source/TobiiGTOM/Public/GTOMAwareUI/STobiiRadialMenuWidget.h new file mode 100644 index 0000000..75e32b5 --- /dev/null +++ b/Source/TobiiGTOM/Public/GTOMAwareUI/STobiiRadialMenuWidget.h @@ -0,0 +1,159 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "CoreMinimal.h" +#include "DisplayDebugHelpers.h" +#include "Slate/SRetainerWidget.h" +#include "Widgets/Layout/SBox.h" + +class FArrangedChildren; +class FPaintArgs; +class FSlateWindowElementList; + +struct FTobiiRadialMenuPanelRenderData +{ + TArray PanelVertexData; + TArray PanelIndexData; + + TArray BorderVertexData; //This also contains the PanelVertexData in the end. + TArray BorderIndexData; +}; + +//This is the underlying slate widget. +class TOBIIGTOM_API STobiiRadialMenuWidget : public SPanel +{ +public: + class FSlot : public TSlotBase + { + public: + FSlot& Offset(const TAttribute& InOffset) + { + OffsetAttr = InOffset; + return *this; + } + + FSlot& Scale(const TAttribute& InScale) + { + ScaleAttr = InScale; + return *this; + } + + FSlot& Alpha(const TAttribute& InAlpha) + { + AlphaAttr = InAlpha; + return *this; + } + + /** Offset */ + TAttribute OffsetAttr; + + /** Scale */ + TAttribute ScaleAttr; + + /** Alpha */ + TAttribute AlphaAttr; + + /** Default values for a slot. */ + FSlot() + : TSlotBase() + , OffsetAttr(FVector2D(0.0f, 0.0f)) + , ScaleAttr(1.0f) + , AlphaAttr(1.0f) + { } + }; + + SLATE_BEGIN_ARGS(STobiiRadialMenuWidget) + : _UseHardBorder(true) + , _BorderColor(FColor(230, 230, 230)) + , _PanelColor(FColor(70, 70, 70)) + , _BorderThicknessPx(5.0f) + , _SegmentSeparationPx(10.0f) + , _AngularDisplacementDeg(0.0f) + , _RadiusPx(500.0f) + , _VertexCount(360) + { + _Visibility = EVisibility::SelfHitTestInvisible; + } + + SLATE_SUPPORTS_SLOT(STobiiRadialMenuWidget::FSlot) + SLATE_ARGUMENT(bool, UseHardBorder) + SLATE_ARGUMENT(FColor, BorderColor) + SLATE_ARGUMENT(FColor, PanelColor) + SLATE_ARGUMENT(float, BorderThicknessPx) + SLATE_ARGUMENT(float, SegmentSeparationPx) + SLATE_ARGUMENT(float, AngularDisplacementDeg) + SLATE_ARGUMENT(float, RadiusPx) + SLATE_ARGUMENT(int32, VertexCount) + + SLATE_END_ARGS() + + STobiiRadialMenuWidget(); + + void Construct(const FArguments& InArgs); + + static FSlot& Slot() + { + return *(new FSlot()); + } + + /** + * Adds a content slot. + * + * @return The added slot. + */ + FSlot& AddSlot() + { + Invalidate(EInvalidateWidget::Layout); + + STobiiRadialMenuWidget::FSlot& NewSlot = *(new FSlot()); + this->Children.Add(&NewSlot); + return NewSlot; + } + + /** + * Removes a particular content slot. + * + * @param SlotWidget The widget in the slot to remove. + */ + int32 RemoveSlot(const TSharedRef& SlotWidget); + + /** + * Removes all slots from the panel. + */ + void ClearChildren(); + +public: + // Begin SWidget overrides + virtual void OnArrangeChildren(const FGeometry& AllottedGeometry, FArrangedChildren& ArrangedChildren) const override; + virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override; + virtual FChildren* GetChildren() override; + // End SWidget overrides + +protected: + // Begin SWidget overrides. + virtual FVector2D ComputeDesiredSize(float LayoutScaleMultiplier) const override; + // End SWidget overrides. + +protected: + /** The ConstraintCanvas widget's children. */ + TPanelChildren Children; + +public: + FSlateResourceHandle BrushResourceHandle; + bool bUseHardBorder; + FColor BorderColor; + FColor PanelColor; + float BorderThicknessPx; + float SegmentSeparationPx; + float AngularDisplacementDeg; + float RadiusPx; + int32 VertexCount; + +private: + void GeneratePanelRenderData(const FGeometry& AllottedGeometry, int32 NrChildren, TArray& OutChildRenderData) const; +}; diff --git a/Source/TobiiGTOM/Public/GTOMAwareUI/TobiiGazeFocusableWidget.h b/Source/TobiiGTOM/Public/GTOMAwareUI/TobiiGazeFocusableWidget.h new file mode 100644 index 0000000..041cbb7 --- /dev/null +++ b/Source/TobiiGTOM/Public/GTOMAwareUI/TobiiGazeFocusableWidget.h @@ -0,0 +1,167 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "TobiiGTOMTypes.h" +#include "STobiiGazeFocusableWidget.h" + +#include "CoreMinimal.h" +#include "ObjectEditorUtils.h" +#include "Components/WidgetComponent.h" +#include "Components/SizeBox.h" + +#include "TobiiGazeFocusableWidget.generated.h" + +class UPrimitiveComponent; +class UTobiiGazeFocusManagerComponent; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FWidgetGazeFocusSignature, UTobiiGazeFocusableWidget*, FocusedWidget); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FWidgetHoverSignature, UTobiiGazeFocusableWidget*, FocusedWidget); + +/** + * Use this widget to wrap other widgets you want to mark as gaze focusable. + * In contrast to gaze focusable components, each widget you want to make focusable needs to be wrapped by its own UTobiiGazeFocusableWidget. There is more info below on why this is the case. + * Please note that while this wraps a raw slate widget, we currently do not officially support using the slate widgets directly. Please use the UMG wrapper. + * + * There are some major differences to how gaze focusable widgets versus components work: + * 1. Since widgets don't support tags, widgets can only participate in GTOM by being wrapped by this widget. + * 2. Actor component hierarchies can be very rigid since they can be partially constructed in cpp, both by the developer and the engine. This means that designating gaze focusability via component parenting wouldn't be reliable. + * 3. It is very common to want to make it so that every primitive component on an actor should contribute to focusing that actor. This is not true for widgets, here it is more common that each individual widget wants to know if it has been individually focused. + * + * All of these things should help to illustrate why using different models for gaze focus in slate/UMG and actors is to be preferred. + */ +UCLASS(BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent, DisplayName= "Tobii Gaze Focusable")) +class TOBIIGTOM_API UTobiiGazeFocusableWidget : public USizeBox +{ + GENERATED_UCLASS_BODY() + + /************************************************************************/ + /* USizeBox */ + /************************************************************************/ +public: + // UVisual interface + virtual void ReleaseSlateResources(bool bReleaseChildren) override; +protected: + // UWidget interface + virtual TSharedRef RebuildWidget() override; + +public: + //These are for pointer hovering + UPROPERTY(BlueprintAssignable, Category = "Event") + FWidgetHoverSignature OnHovered; + + UPROPERTY(BlueprintAssignable, Category = "Event") + FWidgetHoverSignature OnUnhovered; + + //This function must be called to make the widget participate in GTOM. + //WARNING: For screen space widgets, you must do this manually by sending in a nullptr HostWidget! For worldspace widgets, the UTobiiGazeFocusableComponent will do this for you though. + UFUNCTION(BlueprintCallable, Category = "Gaze Focus") + void RegisterWidgetToGTOM(UWidgetComponent* HostWidget); + + UFUNCTION(BlueprintPure, Category = "General") + UWidgetComponent* GetWorldSpaceHostWidgetComponent(); + + UFUNCTION(BlueprintPure, Category = "General") + bool IsHoveredByPointer(); + + UFUNCTION(BlueprintPure, Category = "General") + bool IsWorldSpaceWidget(); + + /************************************************************************/ + /* Tobii */ + /************************************************************************/ +public: + //This will be true if this is the most likely widget to currently have focus irregardless of layer filters. + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Gaze Focus") + bool bHasGazeFocus; + + //Set this to false if you want to stop this widget participating in GTOM. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus") + bool bIsGazeFocusable; + + //The gaze focus priority is optional input that you can provide to GTOM. Objects with higher focus priority are more "important" and are therefore more likely to be construed as being in focus. If this is negative, it will not be enforced. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus") + float GazeFocusPriority; + + //If this object is at least this far away from the player, it will not participate in GTOM. If this is less or equal to zero, it will not be enforced. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus") + float MaxFocusDistance; + + //A focus manager can opt to only query a subset of focusables by using a focus layer. If this is empty, the default layer will be used (Either from a gaze focusable component, or the global default). + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus") + FString FocusLayer; + + //This delegate will fire when this widget receives user focus + UPROPERTY(BlueprintAssignable, Category = "Gaze Focus|Event") + FWidgetGazeFocusSignature ReceivedGazeFocus; + + //This delegate will fire when this widget loses user focus + UPROPERTY(BlueprintAssignable, Category = "Gaze Focus|Event") + FWidgetGazeFocusSignature LostGazeFocus; + + //This will return true if this widget has user focus + UFUNCTION(BlueprintPure, Category = "Gaze Focus") + bool HasFocus(); + + //This will return true if this widget is close enough to the user's gaze to be considered in focus, even though it might not be the object the user is actively focusing on. + UFUNCTION(BlueprintPure, Category = "Gaze Focus") + bool IsInFocusCollection(); + +public: + //How should we apply CleanUI? + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "CleanUI") + ETobiiCleanUIMode CleanUIMode; + + //If this is larger than 0, CleanUI is currently being suppressed. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "CleanUI") + float TimeCleanUIIsSuppressedForSecs; + + //If you want this cleanUI panel to behave differently from the default governed by CVars you can override properties here. Any negative value will result in the CVar value being used. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "CleanUI") + float CleanUIFadeInTimeSecsOverride; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "CleanUI") + float CleanUIFadeOutTimeSecsOverride; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "CleanUI") + float CleanUIMinAlphaOverride; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "CleanUI") + float CleanUIMaxAlphaOverride; + + //If you have a game where you want this cleanUI widget to fade in on mouse over, set this to true. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "CleanUI") + bool bTriggerCleanUIOnMouseOver; + + UFUNCTION(BlueprintPure, Category = "CleanUI") + float GetCleanUIAlpha(); + +public: + //If this widget should fade out when another container is being looked at, use these functions to make that happen. This requires bUseFastHitTesting to be off. + UFUNCTION(BlueprintCallable, Category = "Dependent Widgets") + void AddGazeFocusableWidgetToPollHitsFrom(UTobiiGazeFocusableWidget* GazeFocusableWidgetToPoll, bool PollGazeHits = true, bool PollMouseHits = true); + UFUNCTION(BlueprintCallable, Category = "Dependent Widgets") + void RemoveGazeFocusableWidgetToPollHitsFrom(UTobiiGazeFocusableWidget* GazeFocusableWidgetToStopPolling); + +public: + STobiiGazeFocusableWidget* GetSlateGazeFocusableWidget(); + + virtual void ReceiveFocus(); + virtual void LoseFocus(); + + static TMap>& GetTobiiScreenSpaceFocusableWidgets(); + +public: +#if WITH_EDITOR + virtual const FText GetPaletteCategory() override; +#endif + +protected: + //This is set during run time by a UTobiiGazeFocusableComponent if this is a world space widget. + TWeakObjectPtr WorldSpaceHostWidgetComponent; + TSharedPtr MySlateWidget; + + void SlateHandleHovered(); + void SlateHandleUnhovered(); +}; \ No newline at end of file diff --git a/Source/TobiiGTOM/Public/GTOMAwareUI/TobiiRadialMenuWidget.h b/Source/TobiiGTOM/Public/GTOMAwareUI/TobiiRadialMenuWidget.h new file mode 100644 index 0000000..8aad92e --- /dev/null +++ b/Source/TobiiGTOM/Public/GTOMAwareUI/TobiiRadialMenuWidget.h @@ -0,0 +1,166 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "TobiiGTOMTypes.h" +#include "STobiiRadialMenuWidget.h" + +#include "CoreMinimal.h" +#include "ObjectEditorUtils.h" +#include "Components/SizeBox.h" +#include "Components/WidgetComponent.h" + +#include "TobiiRadialMenuWidget.generated.h" + +UCLASS() +class TOBIIGTOM_API UTobiiRadialMenuSlot : public UPanelSlot +{ + GENERATED_UCLASS_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Layout|Radial Menu Slot") + FVector2D Offset; + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Layout|Radial Menu Slot") + float Scale; + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Layout|Radial Menu Slot") + float Alpha; + +public: +#if WITH_EDITOR + virtual bool NudgeByDesigner(const FVector2D& NudgeDirection, const TOptional& GridSnapSize) override; + virtual bool DragDropPreviewByDesigner(const FVector2D& LocalCursorPosition, const TOptional& XGridSnapSize, const TOptional& YGridSnapSize) override; + virtual void SynchronizeFromTemplate(const UPanelSlot* const TemplateSlot) override; +#endif //WITH_EDITOR + + UFUNCTION(BlueprintCallable, Category = "Layout|Radial Menu Slot") + void SetOffset(FVector2D InOffset); + UFUNCTION(BlueprintCallable, Category = "Layout|Radial Menu Slot") + FVector2D GetOffset() const; + + UFUNCTION(BlueprintCallable, Category = "Layout|Radial Menu Slot") + void SetScale(float InScale); + UFUNCTION(BlueprintCallable, Category = "Layout|Radial Menu Slot") + float GetScale() const; + + UFUNCTION(BlueprintCallable, Category = "Layout|Radial Menu Slot") + void SetAlpha(float InAlpha); + UFUNCTION(BlueprintCallable, Category = "Layout|Radial Menu Slot") + float GetAlpha() const; + +public: + STobiiRadialMenuWidget::FSlot* GetSlateSlot() { return Slot; } + void BuildSlot(TSharedRef RadialMenu); + + // UPanelSlot interface + virtual void SynchronizeProperties() override; + // End of UPanelSlot interface + + virtual void ReleaseSlateResources(bool bReleaseChildren) override; + +#if WITH_EDITOR + // UObject interface + virtual void PreEditChange(class FEditPropertyChain& PropertyAboutToChange) override; + virtual void PostEditChangeChainProperty(struct FPropertyChangedChainEvent& PropertyChangedEvent) override; + // End of UObject interface +#endif + +private: + STobiiRadialMenuWidget::FSlot* Slot; +}; + +/** + */ +UCLASS(BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent, DisplayName= "Tobii Radial Menu")) +class TOBIIGTOM_API UTobiiRadialMenuWidget : public UPanelWidget +{ + GENERATED_UCLASS_BODY() + +public: + //A hard border will not be blended with the panel, while a soft border will be blended + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu") + bool bUseHardBorder; + //The color of the borders around each panel + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu") + FColor BorderColor; + //The color of the panels themselves + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu") + FColor PanelColor; + //The thickness of the border area + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu") + float BorderThicknessPx; + //The amount of pixels separating each segment + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu") + float SegmentSeparationPx; + //This is how rotated the panel is. Useful if you want another orientation. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu") + float AngularDisplacementDeg; + //This is the preferred radius. This will inform the desired size of the widget + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu") + float RadiusPx; + //The amount of vertices that will be used for the outside of the circle segments in total. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Radial Menu") + int32 VertexCount; + +public: + UFUNCTION(BlueprintCallable, Category = "Radial Menu") + void SetUseHardBorder(bool bNewUseHardBorder); + UFUNCTION(BlueprintCallable, Category = "Radial Menu") + void SetBorderColor(FColor NewBorderColor); + UFUNCTION(BlueprintCallable, Category = "Radial Menu") + void SetPanelColor(FColor NewPanelColor); + UFUNCTION(BlueprintCallable, Category = "Radial Menu") + void SetBorderThicknessPx(float NewBorderThicknessPx); + UFUNCTION(BlueprintCallable, Category = "Radial Menu") + void SetSegmentSeparationPx(float NewSegmentSeparationPx); + UFUNCTION(BlueprintCallable, Category = "Radial Menu") + void SetAngularDisplacementDeg(float NewAngularDisplacementDeg); + UFUNCTION(BlueprintCallable, Category = "Radial Menu") + void SetRadiusPx(float NewRadiusPx); + UFUNCTION(BlueprintCallable, Category = "Radial Menu") + void SetVertexCount(int32 NewVertexCount); + +public: + /** */ + UFUNCTION(BlueprintCallable, Category = "Radial Menu") + UTobiiRadialMenuSlot* AddRadialMenuChild(UWidget* Content); + + /** Computes the geometry for a particular slot based on the current geometry of the canvas. */ + bool GetGeometryForSlot(int32 SlotIndex, FGeometry& ArrangedGeometry) const; + bool GetGeometryForSlot(UTobiiRadialMenuSlot* InSlot, FGeometry& ArrangedGeometry) const; + + void ReleaseSlateResources(bool bReleaseChildren) override; + +#if WITH_EDITOR + // UWidget interface + virtual const FText GetPaletteCategory() override; + // End UWidget interface + + // UWidget interface + virtual bool LockToPanelOnDrag() const override + { + return true; + } + // End UWidget interface +#endif + +protected: + + // UPanelWidget + virtual UClass* GetSlotClass() const override; + virtual void OnSlotAdded(UPanelSlot* Slot) override; + virtual void OnSlotRemoved(UPanelSlot* Slot) override; + // End UPanelWidget + +protected: + + TSharedPtr MyRadialMenu; + +protected: + // UWidget interface + virtual TSharedRef RebuildWidget() override; + // End of UWidget interface +}; diff --git a/Source/TobiiGTOM/Public/TobiiGTOMBlueprintLibrary.h b/Source/TobiiGTOM/Public/TobiiGTOMBlueprintLibrary.h new file mode 100644 index 0000000..29b22ef --- /dev/null +++ b/Source/TobiiGTOM/Public/TobiiGTOMBlueprintLibrary.h @@ -0,0 +1,156 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "GTOMAwareUI/TobiiGazeFocusableWidget.h" + +#include "CoreMinimal.h" +#include "Components/WidgetComponent.h" +#include "Components/WidgetInteractionComponent.h" +#include "Kismet/BlueprintFunctionLibrary.h" + +#include "TobiiGTOMBlueprintLibrary.generated.h" + +/** + * Simplified interface for blueprint use. Only exposes the features most likely to be consumed from BP. + */ +UCLASS() +class TOBIIGTOM_API UTobiiGTOMBlueprintLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_UCLASS_BODY() + +public: + /** + * Set which player controller should be used for GTOM. + */ + UFUNCTION(BlueprintCallable, Category = "Tobii GTOM") + static void SetGTOMPlayerController(APlayerController* NewGTOMPlayerController); + + /** + * Get the component the focus system believes the user is looking at. + */ + UFUNCTION(BlueprintPure, Category = "Tobii GTOM") + static bool GetGazeFocusData(FTobiiGazeFocusData& OutFocusData); + + /** + * Get a sorted list of components that the focus system believes the user is looking at. + */ + UFUNCTION(BlueprintPure, Category = "Tobii GTOM") + static bool GetAllGazeFocusData(TArray& OutFocusData); + + /** + * Get the component the focus system believes the user is looking at. This variant allows you to filter out certain objects in accordance with a filter list. + * + * @param FocusLayerFilterList These are the filters that will determine what focus data you will receive. The behavior of the filters are controlled by bIsWhiteList. + * @param bIsWhiteList This controls how the filters in FocusFilterList are used. If this is true, only focusables with a layer that is in the FocusFilterList will be considered. If this is false, the FocusFilterList will act like a black list, excluding any focusables whos layer can be found in the array. + * @param bWantPrimitives If this is true, focus data with valid UPrimitiveComponents will be included in the output results. + * @param bWantWidgets If this is true, focus data with valid UWidgets will be included in the output results. + * @param OutFocusData Output focus data. + */ + UFUNCTION(BlueprintPure, Category = "Tobii GTOM") + static bool GetFilteredGazeFocusData(const TArray& FocusLayerFilterList, const bool bIsWhiteList, const bool bWantPrimitives, const bool bWantWidgets, FTobiiGazeFocusData& OutFocusData); + + /** + * Get a sorted list of components that the focus system believes the user is looking at. This variant allows you to filter out certain objects in accordance with a filter list. + * + * @param FocusLayerFilterList These are the filters that will determine what focus data you will receive. The behavior of the filters are controlled by bIsWhiteList. + * @param bIsWhiteList This controls how the filters in FocusFilterList are used. If this is true, only focusables with a layer that is in the FocusFilterList will be considered. If this is false, the FocusFilterList will act like a black list, excluding any focusables whos layer can be found in the array. + * @param bWantPrimitives If this is true, focus data with valid UPrimitiveComponents will be included in the output results. + * @param bWantWidgets If this is true, focus data with valid UWidgets will be included in the output results. + * @param OutFocusData Output focus data. + */ + UFUNCTION(BlueprintPure, Category = "Tobii GTOM") + static bool GetAllFilteredGazeFocusData(const TArray& FocusLayerFilterList, const bool bIsWhiteList, const bool bWantPrimitives, const bool bWantWidgets, TArray& OutFocusData); + + + /************************************************************************/ + /* Utils */ + /************************************************************************/ + /** + * Gets a simple ray hit along combined gaze + */ + UFUNCTION(BlueprintPure, Category = "Tobii GTOM Utils") + static const FHitResult& GetNaiveGazeHit(); + + /** + * Tests to see whether a primitive component is gaze focusable + */ + UFUNCTION(BlueprintPure, Category = "Tobii GTOM Utils") + static bool IsPrimitiveComponentGazeFocusable(UPrimitiveComponent* PrimitiveComponent); + + /** + * Tests to see whether a widget is gaze focusable + */ + UFUNCTION(BlueprintPure, Category = "Tobii GTOM Utils") + static bool IsWidgetGazeFocusable(UWidget* Widget); + + /** + * Gets the *untransformed* focus offset for a primitive component. + */ + UFUNCTION(BlueprintPure, Category = "Tobii GTOM Utils") + static bool GetPrimitiveComponentFocusOffset(USceneComponent* Component, FVector& OutFocusOffset); + + /** + * Gets the focus point on the primitive. + */ + UFUNCTION(BlueprintPure, Category = "Tobii GTOM Utils") + static bool GetPrimitiveComponentFocusLocation(USceneComponent* Component, FVector& OutFocusLocation); + + /* + * This function will traverse a widget tree given a root and add any UTobiiGazeFocusableWidget to the screen space widget collection. + */ + UFUNCTION(BlueprintCallable, Category = "Tobii GTOM Utils") + static void RegisterScreenSpaceGazeFocusableWidgets(UWidget* Root); + + /** + * Helper function to simulate gaze focus using a widget interaction component + */ + UFUNCTION(BlueprintCallable, Category = "Tobii GTOM Utils") + static bool MakeGazeFocusDataForWidgetInteractionComponent(UWidgetInteractionComponent* WidgetInteraction, ECollisionChannel CollisionChannel, FTobiiGazeFocusData& OutGazeFocusData); + + /** + * Helper function to simulate gaze focus using a widget interaction component + */ + UFUNCTION(BlueprintCallable, Category = "Tobii GTOM Utils") + static void EmulateGazeFocusUsingWidgetInteractionComponent(UWidgetInteractionComponent* WidgetInteraction, ECollisionChannel CollisionChannel); + + /** + * This function will find the largest possible rectangle that can be inscribed into a given circle. + */ + UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils Utils") + static bool FindLargestInscribedAlignedRect(float CircleSegmentAngleRad, float CircleRadius, FVector2D& LargestInscribedRectSize, float& DistanceToCenter); + + /** + * This function will project a local point in a widget hosted by a widget component into world space + */ + UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils") + static bool TransformWidgetLocalPointToWorld(UWidgetComponent* Component, const FVector2D& LocalWidgetLocation, FVector& OutWorldLocation); + + /** + * This function will project a world point to the widget's local space + */ + UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils") + static bool TransformWorldPointToWidgetLocal(UWidgetComponent* Component, const FVector& WorldLocation, const FVector& WorldDirection, FVector2D& OutLocalWidgetLocation); + + /** + * This function will test whether a right angle cone and a sphere intersects. Very useful for world space eye tracking. + */ + UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils") + static bool TestConeSphereIntersection(const FVector& ConeApex, const FVector& ConeDirection, const float ConeAngleDeg, const FVector& SphereCenter, const float SphereRadius); + + /** + * This function will test whether a rectangle and a rotated ellipse intersects. Very useful for screen space eye tracking. + */ + UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils") + static bool TestRectEllipseIntersection(const FVector2D& RectangleCenter, const FVector2D& RectangleRightAxis, const FVector2D& RectangleUpAxis, const FVector2D& RectangleExtents, const FVector2D& EllipseCenter, const FVector2D& EllipseRadii, const float& EllipseRotationDeg); + + /** + * Get a random point on a circle centered at the origin that is uniformly distributed. + */ + UFUNCTION(BlueprintPure, Category = "Tobii Math Utils") + static FVector2D GetUniformlyDistributedRandomCirclePoint(float CircleRadius); +}; diff --git a/Source/TobiiGTOM/Public/TobiiGTOMTypes.h b/Source/TobiiGTOM/Public/TobiiGTOMTypes.h new file mode 100644 index 0000000..49610dc --- /dev/null +++ b/Source/TobiiGTOM/Public/TobiiGTOMTypes.h @@ -0,0 +1,93 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/EngineTypes.h" + +#include "TobiiGTOMTypes.generated.h" + +class UTobiiGazeFocusableWidget; +typedef uint32 FTobiiFocusableUID; //Only use this ID type for our focus container types like UTobiiGazeFocusableComponents and UTobiiGazeFocusableWidgets +typedef uint32 FEngineFocusableUID; //Only use this for base engine things that can be focused, like UPrimitiveComponents and UWidgets + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FPrimitiveReceivedGazeFocusSignature, UPrimitiveComponent*, FocusedComponent); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FPrimitiveLostGazeFocusSignature, UPrimitiveComponent*, FocusedComponent); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FWidgetReceivedGazeFocusSignature, UTobiiGazeFocusableWidget*, FocusedWidget); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FWidgetLostGazeFocusSignature, UTobiiGazeFocusableWidget*, FocusedWidget); + +UENUM(BlueprintType) +enum class ETobiiCleanUIMode : uint8 +{ + /** CleanUI is disabled for this widget. */ + Disabled, + + /** CleanUI will only consider the gaze point for determining alpha, and will ignore nearby widgets. */ + Normal, + + /** This widget is aware of it's neighbors and only one widget can be faded in at a time. */ + FocusExclusive, + + /** This widget will still calcualte its CleanUI alpha value, but it will not apply it when calculating its own color. */ + Silent +}; + +/** + * Information about an object that is being considered when determining user focus. + * When utilizing gaze, this is almost always what you should be using as your input data. + */ +USTRUCT(BlueprintType) +struct FTobiiGazeFocusData +{ + GENERATED_USTRUCT_BODY() + +public: + FTobiiGazeFocusData() + : FocusedActor() + , FocusedPrimitiveComponent() + , FocusedWidget() + , LastVisibleWorldLocation(0.0f, 0.0f, 0.0f) + , FocusConfidence(0.0f) + {} + + //This is the actor that the Focused Primitive Component belongs to. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Focus Data") + TWeakObjectPtr FocusedActor; + + //This is the primitive component that is most likely to hold the user's focus. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Focus Data") + TWeakObjectPtr FocusedPrimitiveComponent; + + //This is the primitive component that is most likely to hold the user's focus. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Focus Data") + TWeakObjectPtr FocusedWidget; + + //This is the point on the Focused Primitive Component that was last confirmed visible to the user. This is very useful when aligning objects towards a focused object. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Focus Data") + FVector LastVisibleWorldLocation; + + //This is how confident the focus system is that this object is in focus. The object with the highest confidence is not necessarily the object with focus however. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Focus Data") + float FocusConfidence; +}; + +//This contains the tags that can optionally be used to inform the GTOM system about the primitive component they are attached to. These only exist since we cannot add UPROPERTY's to primitive components without engine modifications. +//If you want to override behavior or widgets, please change the relevant properties on the TobiiGazeFocusableWidget since we have full access to that type. +class TOBIIGTOM_API FTobiiPrimitiveComponentGazeFocusTags +{ +public: + static FName HasGazeFocusTag; //A primitive component with this tag is the primitive component that most likely has focus irregardless of layer filters. + static FName GazeFocusableTag; //A primitive component with this tag will participate in GTOM even if the actor owning the primitive doesn't have a GazeFocusableComponent, or if the default for the gaze focusable is off. + static FName NotGazeFocusableTag; //A primitive component with this tag will not participate in GTOM even if the actor owning the primitive component has a GazeFocusableComponent. + static FString GazeFocusablePriorityTag; //A primitive component with this tag will modify it's priority. Priority is used when only a certain number of focusables can participate in an operation. An example of this is ID buffer based GTOM. This is an argument tag, this means you must provide the value after the tag. Usage example: GazeFocusablePriorityTag 100 + static FString GazeFocusableMaximumDistanceTag; //A primitive component with this tag and has the GazeFocusableTag will only be considered if the distance between the GTOM source and the component is shorter than the argument part of the tag. Please note that this tag cannot be used to force the GTOM line traces to be longer than default, only exclude the primitive if the distance is longer than this argument. This is an argument tag, this means you must provide the value after the tag. Usage example: GazeFocusableMaximumDistanceTag 10000 + static FString GazeFocusableLayerTag; //A focus manager can opt to only query a subset of focusables by using a focus layer. A primitive will this tag will only be subject to the layer supplied as the argument. This is an argument tag, this means you must provide the value after the tag. Usage example: GazeFocusableLayerTag Enemies + + static FString PrimitiveFocusOffsetXTag; //If this tag is present, it tells the various systems interacting with gaze focus that the actual focus point is relative to the primitive origin by this amount. + static FString PrimitiveFocusOffsetYTag; //If this tag is present, it tells the various systems interacting with gaze focus that the actual focus point is relative to the primitive origin by this amount. + static FString PrimitiveFocusOffsetZTag; //If this tag is present, it tells the various systems interacting with gaze focus that the actual focus point is relative to the primitive origin by this amount. +}; diff --git a/Source/TobiiGTOM/Public/TobiiGazeFocusManagerComponent.h b/Source/TobiiGTOM/Public/TobiiGazeFocusManagerComponent.h new file mode 100644 index 0000000..130aacf --- /dev/null +++ b/Source/TobiiGTOM/Public/TobiiGazeFocusManagerComponent.h @@ -0,0 +1,88 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "TobiiGTOMTypes.h" + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "Delegates/DelegateCombinations.h" + +#include "TobiiGazeFocusManagerComponent.generated.h" + +/* + * Tobii focus managers are an easier way to set up focus layer filters and access the focusables associated with them. + * It also offers a way to get notified of changes in gaze focus via events. + */ +UCLASS(BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent)) +class TOBIIGTOM_API UTobiiGazeFocusManagerComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + UTobiiGazeFocusManagerComponent(); + +public: + //If this is true, focus data with valid UPrimitiveComponents will be included in the output results. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus Management") + bool bWantPrimitives; + + //If this is true, focus data with valid UWidgets will be included in the output results + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus Management") + bool bWantWidgets; + + //This controls how the filters in FocusFilterList are used. If this is true, only focusables with a layer that is in the FocusFilterList will be considered. If this is false, the FocusFilterList will act like a black list, excluding any focusables whos layer can be found in the array. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus Management") + bool bIsWhiteList; + + //These are the filters that will determine what focus data you will receive. The behavior of the filters are controlled by bIsWhiteList. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus Management") + TArray FocusLayerFilters; + +public: + //These functions will notify when gaze focus is lost for this focus manager's filtered set. + UPROPERTY(BlueprintAssignable, Category = "Gaze Focus Management") + FPrimitiveReceivedGazeFocusSignature OnPrimitiveReceivedGazeFocus; + + UPROPERTY(BlueprintAssignable, Category = "Gaze Focus Management") + FPrimitiveLostGazeFocusSignature OnPrimitiveLostGazeFocus; + + UPROPERTY(BlueprintAssignable, Category = "Gaze Focus Management") + FWidgetReceivedGazeFocusSignature OnWidgetReceivedGazeFocus; + + UPROPERTY(BlueprintAssignable, Category = "Gaze Focus Management") + FWidgetLostGazeFocusSignature OnWidgetLostGazeFocus; + +public: + //Get the component the focus system believes the user is looking at, given the filters the focus manager is configured with. + UFUNCTION(BlueprintPure, Category = "Gaze Focus Management") + void GetBestFilteredPrimitiveComponentFocusData(FTobiiGazeFocusData& OutFocusData) const; + + UFUNCTION(BlueprintPure, Category = "Gaze Focus Management") + void GetBestFilteredWidgetComponentFocusData(FTobiiGazeFocusData& OutFocusData) const; + + //Get a sorted list of components that the focus system believes the user is looking at, given the filters the focus manager is configured with. + UFUNCTION(BlueprintPure, Category = "Gaze Focus Management") + void GetAllFilteredFocusData(TArray& OutFocusData) const; + + /************************************************************************/ + /* UActorComponent */ + /************************************************************************/ +public: + virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override; + +private: + TWeakObjectPtr PreviouslyFocusedPrimitiveComponent; + TWeakObjectPtr PreviouslyFocusedWidget; + FTobiiGazeFocusData BestPrimitiveComponentFocusData; + FTobiiGazeFocusData BestWidgetFocusData; + TArray AllFocusData; + + void ResetFocusData(); + void NotifyPrimitiveComponentGazeFocusReceived(UPrimitiveComponent& PrimitiveComponentToReceiveFocus); + void NotifyPrimitiveComponentGazeFocusLost(UPrimitiveComponent& PrimitiveComponentToLoseFocus); +}; diff --git a/Source/TobiiGTOM/Public/TobiiGazeFocusableComponent.h b/Source/TobiiGTOM/Public/TobiiGazeFocusableComponent.h new file mode 100644 index 0000000..acf0f92 --- /dev/null +++ b/Source/TobiiGTOM/Public/TobiiGazeFocusableComponent.h @@ -0,0 +1,111 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "TobiiGazeFocusableWidget.h" +#include "TobiiGTOMTypes.h" + +#include "CoreMinimal.h" +#include "SceneTypes.h" +#include "Components/ActorComponent.h" + +#include "TobiiGazeFocusableComponent.generated.h" + +class UPrimitiveComponent; +class UTobiiGazeFocusManagerComponent; + +/** + * This component is a helper that can make it easier to setup GTOM, as well as provide an focus event interface for this object. + * If you don't use specific tags on your primitive components, the values on this component will be used instead. + * This component is REQUIRED for GPU based GTOM. + * This component is also REQUIRED for world space widget focus. + * This should be placed on the root actor for every component tree. This means that if you have an actor that has a child actor that has primitive components you would like to control, you need to add one of these components to that child actor as well. + * Having more than one of these components on one actor is not recommended, as it is unclear which focusable component's values will be used. + * If you want world space UI widgets to participate in GTOM, you must attach one of these components to the actor carrying the WidgetComponent(s) to affect + */ +UCLASS(BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent)) +class TOBIIGTOM_API UTobiiGazeFocusableComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + //The default value for focusability to be used by all primitives on the same actor as this component. It can be useful to set this to false if you only want a few primitives on the actor to be focusable. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Default Focus Settings") + bool bDefaultIsFocusable; + + //The focus priority is optional input that you can provide to GTOM. Objects with higher focus priority are more "important" and are therefore more likely to be construed as being in focus. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Default Focus Settings") + float DefaultFocusPriority; + + //If this object is at least this far away from the player, it will not participate in GTOM. This will only be in effect if it is greater than zero. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Default Focus Settings") + float DefaultMaxFocusDistance; + + //A developer can opt to only query a subset of focusables by using a focus layer. This is the default layer that will be used if none is set with tags on primitives or on a gazefocusablewidget. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Default Focus Settings") + FName DefaultFocusLayer; + +public: + //These functions will notify when gaze focus is lost or gained irregardless of focus layers. + UPROPERTY(BlueprintAssignable, Category = "Gaze Focus") + FPrimitiveReceivedGazeFocusSignature OnPrimitiveReceivedGazeFocus; + + UPROPERTY(BlueprintAssignable, Category = "Gaze Focus") + FPrimitiveLostGazeFocusSignature OnPrimitiveLostGazeFocus; + + UPROPERTY(BlueprintAssignable, Category = "Gaze Focus") + FWidgetReceivedGazeFocusSignature OnWidgetReceivedGazeFocus; + + UPROPERTY(BlueprintAssignable, Category = "Gaze Focus") + FWidgetLostGazeFocusSignature OnWidgetLostGazeFocus; + +public: + UFUNCTION(BlueprintCallable, Category = "CleanUI") + void RefreshOwnedWidgets(); + +public: + UTobiiGazeFocusableComponent(); + virtual void PrimitiveReceivedGazeFocus(UPrimitiveComponent* FocusedComponent); + virtual void PrimitiveLostGazeFocus(UPrimitiveComponent* FocusedComponent); + virtual void WidgetReceivedGazeFocus(UTobiiGazeFocusableWidget* FocusedWidget); + virtual void WidgetLostGazeFocus(UTobiiGazeFocusableWidget* FocusedWidget); + + const TMap>& GetAllFocusableWidgets() { return AllFocusableWidgets; } + const TArray>& GetFocusableWidgetsForPrimitiveComponent(UPrimitiveComponent* PrimitiveComponent); + + /************************************************************************/ + /* UActorComponent */ + /************************************************************************/ +public: + virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override; + +public: + static void ClearFocusableComponents(); + static const TMap>& GetFocusableComponents(); + + static void UpdateGazeFocusPrio(FVector POVActorLocation, int32 MaxNrFocusables); + static const TSet* GetGazeFocusPrioSet(); + + static bool IsPrimitiveFocusable(UPrimitiveComponent* Primitive); + static bool GetMaxFocusDistanceForPrimitive(UPrimitiveComponent* Primitive, float& OutMaxDistance); + static float GetFocusPriorityForPrimitive(UPrimitiveComponent* Primitive); + static FName GetFocusLayerForPrimitive(UPrimitiveComponent* Primitive); + + static bool IsWidgetFocusable(UTobiiGazeFocusableWidget* GazeFocusableWidget); + static bool GetMaxFocusDistanceForWidget(UTobiiGazeFocusableWidget* GazeFocusableWidget, float& OutMaxDistance); + static float GetFocusPriorityForWidget(UTobiiGazeFocusableWidget* GazeFocusableWidget); + static FName GetFocusLayerForWidget(UTobiiGazeFocusableWidget* GazeFocusableWidget); + +private: + TMap> AllFocusableWidgets; + TMap>> PrimitiveMap; + bool bWidgetsRefreshedOnce; + + void GatherGazeFocusableWidgets(UWidget* Parent, TArray>& WidgetArray, UWidgetComponent* OptionalHostWidgetComponent); +}; diff --git a/Source/TobiiGTOM/TobiiGTOM.Build.cs b/Source/TobiiGTOM/TobiiGTOM.Build.cs new file mode 100644 index 0000000..7bec594 --- /dev/null +++ b/Source/TobiiGTOM/TobiiGTOM.Build.cs @@ -0,0 +1,100 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +using System; +using System.IO; +using System.Diagnostics; +using Tools.DotNETCommon; + +namespace UnrealBuildTool.Rules +{ + public class TobiiGTOM : ModuleRules + { + public TobiiGTOM(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + PrivateDependencyModuleNames.AddRange(new string[] + { + "Core" + , "CoreUObject" + , "Engine" + , "Slate" + , "SlateCore" + , "UMG" + , "HeadMountedDisplay" + }); + + PublicDependencyModuleNames.AddRange(new string[] + { + "EyeTracker" + }); + + PrivateIncludePaths.AddRange(new string[] + { + "TobiiGTOM/Public" + , "TobiiGTOM/Public/GTOMAwareUI" + }); + + string AssemblyLocation = Path.GetDirectoryName(new Uri(System.Reflection.Assembly.GetExecutingAssembly().CodeBase).LocalPath); + DirectoryReference RootDirectory = new DirectoryReference(Path.Combine(AssemblyLocation, "..", "..", "..")); + bool IsEnginePlugin = RootDirectory.GetDirectoryName() == "Engine"; + PublicDefinitions.Add("TOBII_COMPILE_AS_ENGINE_PLUGIN=" + (IsEnginePlugin ? 1 : 0)); + + //Platform specific + if (Target.Platform == UnrealTargetPlatform.Win64 || Target.Platform == UnrealTargetPlatform.Win32) + { + string PlatformString = (Target.Platform == UnrealTargetPlatform.Win64) ? "Win64" : "Win32"; + string PluginsPath = Path.Combine(ModuleDirectory, "../../../"); + string TobiiRelativeAPIPath = "TobiiEyetracking/ThirdParty/GameIntegration"; + string TobiiRelativeIncludePath = Path.Combine(TobiiRelativeAPIPath, "include"); + string TobiiRelativeLibraryBasePath = Path.Combine(TobiiRelativeAPIPath, "lib"); + + //Includes + PrivateIncludePaths.Add(Path.Combine(PluginsPath, TobiiRelativeIncludePath)); + + //Add libraries + AddLibrary(Path.Combine(PluginsPath, TobiiRelativeLibraryBasePath, PlatformString, "tobii_g2om.lib")); + + //Add DLL + string RelativeG2OMDllPath = ""; + const string G2OMDllName = "tobii_g2om.dll"; + + if (IsEnginePlugin) + { + RelativeG2OMDllPath = Path.Combine("Binaries/ThirdParty/TobiiEyetracking", PlatformString, G2OMDllName); + RuntimeDependencies.Add("$(EngineDir)/" + RelativeG2OMDllPath); + } + else + { + RelativeG2OMDllPath = Path.Combine(TobiiRelativeLibraryBasePath, PlatformString, G2OMDllName); + RuntimeDependencies.Add(Path.Combine(PluginsPath, RelativeG2OMDllPath)); + } + + PublicDefinitions.Add("TOBII_G2OM_RELATIVE_DLL_PATH=R\"(" + RelativeG2OMDllPath + ")\""); + PublicDelayLoadDLLs.Add(G2OMDllName); + + PublicDefinitions.Add("TOBII_EYETRACKING_ACTIVE=1"); + } + else + { + PublicDefinitions.Add("TOBII_EYETRACKING_ACTIVE=0"); + } + } + + private void AddLibrary(string libraryPath) + { + if (File.Exists(libraryPath)) + { + PublicAdditionalLibraries.Add(libraryPath); + } + else + { + Debug.WriteLine("Cannot find Tobii API Lib file. Path does not exist! " + libraryPath); + } + } + } +} diff --git a/Source/TobiiInteractions/Private/Common/TobiiProjectileComponent.cpp b/Source/TobiiInteractions/Private/Common/TobiiProjectileComponent.cpp new file mode 100644 index 0000000..70bfcc6 --- /dev/null +++ b/Source/TobiiInteractions/Private/Common/TobiiProjectileComponent.cpp @@ -0,0 +1,261 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#include "TobiiProjectileComponent.h" +#include "TobiiGTOMBlueprintLibrary.h" +#include "TobiiInteractionsBlueprintLibrary.h" + +#include "Components/PrimitiveComponent.h" +#include "Engine/World.h" +#include "GameFramework/Actor.h" + +UTobiiProjectileComponent::UTobiiProjectileComponent() + : InitialVelocity(1.0f, 0.0f, 0.0f) + , bAffectedByGravity(true) + , WorldSpaceGravity(0.0f, 0.0f, 0.0f) + , InitialFuelSecs(0.0f) + , MaxLifeTimeSecs(0.0f) + + , HomingBehavior(ETobiiProjectileHomingBehavior::Acceleration) + , SteeringMaxTurnSpeedDegPerSec(10.0f) + + , GuidanceSystem(ETobiiProjectileGuidanceSystem::ComplexPrediction) + , GuidanceSystemUpdateFreq(0.0f) + , GuidanceSystemMaximumTargetAngleDeg(0.0f) + + , GuidanceSystemTarget() + , AccelerationVectorTowardsTarget() + , bTargetOffCourse(false) +{ + bCanThrust = true; + bUsesFuel = InitialFuelSecs > 0.0f; + CurrentFuelSecs = InitialFuelSecs; + CurrentLifeTime = 0.0f; + GuidanceSystemUpdateTimerSecsLeft = 0.0f; + LastTargetVelocity = FVector::ZeroVector; + TargetAcceleration = FVector::ZeroVector; + + HomingAccelerationMagnitude = 10000.0f; + PrimaryComponentTick.bCanEverTick = true; +} + +void UTobiiProjectileComponent::BeginPlay() +{ + Super::BeginPlay(); + + bCanThrust = true; + bUsesFuel = InitialFuelSecs > 0.0f; + CurrentFuelSecs = InitialFuelSecs; + CurrentLifeTime = 0.0f; + GuidanceSystemUpdateTimerSecsLeft = 0.0f; + LastTargetVelocity = FVector::ZeroVector; + TargetAcceleration = FVector::ZeroVector; + if (WorldSpaceGravity.IsZero()) + { + WorldSpaceGravity.Set(0.0f, 0.0f, GetGravityZ()); + } + + //This is a copy from the base class InitializeComponent function. This should really be done at beginplay to let any instigator configure initial speed etc. + if (InitialVelocity.SizeSquared() > 0.f) + { + FVector NewVelocity; + if (InitialSpeed > 0.f) + { + NewVelocity = InitialVelocity.GetSafeNormal() * InitialSpeed; + } + else + { + NewVelocity = InitialVelocity; + } + + if (bInitialVelocityInLocalSpace) + { + SetVelocityInLocalSpace(NewVelocity); + } + + if (bRotationFollowsVelocity) + { + if (UpdatedComponent) + { + UpdatedComponent->SetWorldRotation(NewVelocity.Rotation()); + } + } + + UpdateComponentVelocity(); + + if (UpdatedPrimitive && UpdatedPrimitive->IsSimulatingPhysics()) + { + UpdatedPrimitive->SetPhysicsLinearVelocity(NewVelocity); + } + } +} + +void UTobiiProjectileComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) +{ + CurrentLifeTime += DeltaTime; + + if (bUsesFuel && bCanThrust) + { + CurrentFuelSecs -= DeltaTime; + if (CurrentFuelSecs <= 0.0f) + { + CurrentFuelSecs = 0.0f; + bCanThrust = false; + } + } + + bTargetOffCourse = false; + if(GuidanceSystemMaximumTargetAngleDeg > FLT_EPSILON && HomingTargetComponent.IsValid()) + { + FVector TargetFocusPosition; + UTobiiGTOMBlueprintLibrary::GetPrimitiveComponentFocusLocation(HomingTargetComponent.Get(), TargetFocusPosition); + + //If our target is too far off course, disable thrust + FVector DeltaVector = TargetFocusPosition - GetOwner()->GetActorLocation(); + FVector MyDirection = Velocity; + + bool bHasValidVectors = DeltaVector.Normalize(); + bHasValidVectors = bHasValidVectors && MyDirection.Normalize(); + + if (bHasValidVectors) + { + float SeparationAngle = FMath::RadiansToDegrees(FMath::Acos(FVector::DotProduct(DeltaVector, MyDirection))); + if (SeparationAngle > GuidanceSystemMaximumTargetAngleDeg) + { + bTargetOffCourse = true; + } + } + } + + if (HomingTargetComponent.IsValid() && !bTargetOffCourse) + { + TargetAcceleration = HomingTargetComponent->GetComponentVelocity() - LastTargetVelocity; + LastTargetVelocity = HomingTargetComponent->GetComponentVelocity(); + + if (GuidanceSystemUpdateFreq == 0.0f) + { + SimulateGuidanceSystem(); + } + else + { + GuidanceSystemUpdateTimerSecsLeft -= DeltaTime; + if (GuidanceSystemUpdateTimerSecsLeft <= 0.0f) + { + GuidanceSystemUpdateTimerSecsLeft = 1.0f / GuidanceSystemUpdateFreq; + SimulateGuidanceSystem(); + } + } + + //Steering behavior + if (bIsHomingProjectile && bCanThrust && HomingBehavior == ETobiiProjectileHomingBehavior::Steering) + { + FVector ForwardDir = Velocity; + FVector TowardsTarget = GuidanceSystemTarget - GetOwner()->GetActorLocation(); + bool bHasValidVectors = ForwardDir.Normalize(); + bHasValidVectors = bHasValidVectors && TowardsTarget.Normalize(); + + if (bHasValidVectors) + { + float AngularDifferenceRad = FMath::Acos(FVector::DotProduct(ForwardDir, TowardsTarget)); + float AlphaStep = FMath::Clamp(FMath::DegreesToRadians(SteeringMaxTurnSpeedDegPerSec) * DeltaTime / AngularDifferenceRad, 0.0f, 1.0f); + FVector NewVelocityDirection = FQuat::Slerp(ForwardDir.ToOrientationQuat(), TowardsTarget.ToOrientationQuat(), AlphaStep).GetForwardVector(); + Velocity = NewVelocityDirection * Velocity.Size(); + UpdateComponentVelocity(); + } + } + } + + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + + if (MaxLifeTimeSecs > 0.0f && CurrentLifeTime > MaxLifeTimeSecs) + { + GetOwner()->Destroy(); + } +} + +FVector UTobiiProjectileComponent::ComputeAcceleration(const FVector& InVelocity, float DeltaTime) const +{ + FVector Acceleration(FVector::ZeroVector); + + if (bAffectedByGravity) + { + Acceleration += WorldSpaceGravity; + } + + if (bCanThrust && !bTargetOffCourse && bIsHomingProjectile && HomingTargetComponent.IsValid() && HomingBehavior == ETobiiProjectileHomingBehavior::Acceleration) + { + Acceleration += AccelerationVectorTowardsTarget; + } + + return Acceleration; +} + +void UTobiiProjectileComponent::SimulateGuidanceSystem() +{ + FVector TargetFocusPosition; + UTobiiGTOMBlueprintLibrary::GetPrimitiveComponentFocusLocation(HomingTargetComponent.Get(), TargetFocusPosition); + + switch (GuidanceSystem) + { + case ETobiiProjectileGuidanceSystem::Prediction: + case ETobiiProjectileGuidanceSystem::ComplexPrediction: + { + FTobiiAccelerationBasedHomingData InterceptData; + InterceptData.ProjectilePosition = GetOwner()->GetActorLocation(); + InterceptData.ProjectileVelocity = Velocity; + InterceptData.ProjectileAccelerationMagnitude = HomingAccelerationMagnitude; + InterceptData.TargetPosition = TargetFocusPosition; + InterceptData.TargetVelocity = HomingTargetComponent->GetComponentVelocity(); + InterceptData.TargetAcceleration = TargetAcceleration; + InterceptData.bAttemptClosestApproachSolution = GuidanceSystem == ETobiiProjectileGuidanceSystem::ComplexPrediction; + + FTobiiAccelerationBasedHomingResult HomingResult; + if (UTobiiInteractionsBlueprintLibrary::FindNeededAccelerationForAccelerationBasedHomingProjectile(InterceptData, HomingResult)) + { + GuidanceSystemTarget = HomingResult.ExpectedInterceptLocation; + AccelerationVectorTowardsTarget = HomingResult.SuggestedAcceleration; + } + else + { + GuidanceSystemTarget = TargetFocusPosition; + AccelerationVectorTowardsTarget = (GuidanceSystemTarget - UpdatedComponent->GetComponentLocation()).GetSafeNormal() * HomingAccelerationMagnitude; + } + break; + } + case ETobiiProjectileGuidanceSystem::Simple: + default: + { + GuidanceSystemTarget = TargetFocusPosition; + AccelerationVectorTowardsTarget = (GuidanceSystemTarget - UpdatedComponent->GetComponentLocation()).GetSafeNormal() * HomingAccelerationMagnitude; + } + } +} + +void UTobiiProjectileComponent::StartHoming(USceneComponent* NewHomingTargetComponent) +{ + HomingTargetComponent = NewHomingTargetComponent; + if (HomingTargetComponent != nullptr) + { + bIsHomingProjectile = true; + } + + bSimulationEnabled = true; + if (UpdatedComponent == nullptr) + { + UpdatedComponent = GetOwner()->GetRootComponent(); + } + if (Cast(UpdatedComponent) != nullptr) + { + Cast(UpdatedComponent)->SetSimulatePhysics(false); + } + PrimaryComponentTick.SetTickFunctionEnable(true); +} + +void UTobiiProjectileComponent::StopHoming() +{ + bIsHomingProjectile = false; + HomingTargetComponent = nullptr; +} diff --git a/Source/TobiiInteractions/Private/Common/TobiiThrowAtGazeComponent.cpp b/Source/TobiiInteractions/Private/Common/TobiiThrowAtGazeComponent.cpp new file mode 100644 index 0000000..b636854 --- /dev/null +++ b/Source/TobiiInteractions/Private/Common/TobiiThrowAtGazeComponent.cpp @@ -0,0 +1,466 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#include "TobiiThrowAtGazeComponent.h" +#include "TobiiInteractionsBlueprintLibrary.h" +#include "../TobiiInteractionsInternalTypes.h" + +#include "IEyeTracker.h" +#include "DrawDebugHelpers.h" +#include "Camera/CameraComponent.h" +#include "Engine/Engine.h" +#include "Engine/World.h" +#include "HAL/IConsoleManager.h" + +static TAutoConsoleVariable CVarThrowAtGazeEnabled(TEXT("tobii.interaction.ThrowAtGazeEnabled"), 1, TEXT("Throw where your are looking.")); +static TAutoConsoleVariable CVarThrowAtGazeDebug(TEXT("tobii.debug.ThrowAtGaze"), 1, TEXT("Draw throw at gaze debug data.")); + +UTobiiThrowAtGazeComponent::UTobiiThrowAtGazeComponent() + : ThrowTarget(nullptr) + , MaxThrowSpeedCmPerSecs(1000.0f) + + , MaxIterations(5) + , bAlwaysPreferLowApex(false) + , bPreferLowApexForTargetsBelowThrower(true) + , bShouldTraceResult(true) + , ThrowApexOffsetMinimumCm(10.0f) + , ThrowApexOffsetMaximumCm(500.0f) + , MinimumApexDeltaCm(10.0f) + , TraceRadiusCm(1.0f) + , MaxNrTraceSteps(50) + , TraceStepSizeSecs(0.1f) + , MaxTraceDistance(10000.0f) + , NoTargetAcceptanceThreshold(10.0f) + , TraceChannel(ECC_Visibility) + , TraceIgnoreActors() + , bShouldTraceIgnoreOwner(true) + + , bUseCustomGravity(false) + , CustomProjectileGravity(FVector::ZeroVector) + + , LastTargetVelocity(FVector::ZeroVector) + , CalculatedTargetAcceleration(FVector::ZeroVector) +{ +} + +FVector UTobiiThrowAtGazeComponent::CalculateProjectileGravityVector() +{ + return bUseCustomGravity ? CustomProjectileGravity : FVector(0.0f, 0.0f, GetWorld()->GetGravityZ()); +} + +bool UTobiiThrowAtGazeComponent::ThrowAtGazeAvailable() +{ + return UTobiiInteractionsBlueprintLibrary::IsThrowAtGazeEnabled(); +} + +bool UTobiiThrowAtGazeComponent::TraceBallisticProjectilePath(FVector ProjectileOrigin, FVector ProjectileInitialVelocity, TArray& OutTracedPath) +{ + TArray IgnoredActors = TraceIgnoreActors; + if (bShouldTraceIgnoreOwner) + { + IgnoredActors.Add(GetOwner()); + } + + FTobiiProjectileTraceData TraceData; + TraceData.TraceChannel = TraceChannel; + TraceData.IgnoredActors = IgnoredActors; + TraceData.MaxNrSteps = MaxNrTraceSteps; + TraceData.StepSizeSecs = TraceStepSizeSecs; + TraceData.TraceRadiusCm = TraceRadiusCm; + TraceData.ProjectileInitialPosition = ProjectileOrigin; + TraceData.ProjectileVelocity = ProjectileInitialVelocity; + TraceData.ProjectileAcceleration = CalculateProjectileGravityVector(); + + FHitResult HitResult; + return UTobiiInteractionsBlueprintLibrary::TraceBallisticProjectilePath(GetWorld(), TraceData, OutTracedPath, HitResult); +} + +void UTobiiThrowAtGazeComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) +{ + if (ThrowTarget.IsValid()) + { + const float AccelerationLerpSpeed = 0.7f; + FVector CurrentTargetVelocity = ThrowTarget->GetOwner()->GetVelocity(); + CalculatedTargetAcceleration = FMath::Lerp(CalculatedTargetAcceleration, CurrentTargetVelocity - LastTargetVelocity, AccelerationLerpSpeed); + LastTargetVelocity = CurrentTargetVelocity; + } +} + +FVector UTobiiThrowAtGazeComponent::CorrectThrow(const FVector& ThrowOrigin, const FVector& OriginalThrowVector, float ThrowAngleThresholdDeg, float ThrowSpeedThreshold) +{ + FTobiiBallisticResult BallisticResult; + TArray TracedPath; + ETobiiThrowAtGazeResult Result = CalculateThrowAtGazeVector(ThrowOrigin, BallisticResult, TracedPath); + + switch (Result) + { + case ETobiiThrowAtGazeResult::OutOfRange: + case ETobiiThrowAtGazeResult::DirectHit: + { + if (ThrowAngleThresholdDeg > FLT_EPSILON) + { + FVector SuggestedDirection = BallisticResult.SuggestedInitialVelocity; + FVector OriginalDirection = OriginalThrowVector; + bool bHasValidVectors = SuggestedDirection.Normalize(); + bHasValidVectors = bHasValidVectors && OriginalDirection.Normalize(); + + if (bHasValidVectors) + { + float SeparationDot = FVector::DotProduct(SuggestedDirection, OriginalDirection); + float SeparationAngleDeg = FMath::RadiansToDegrees(FMath::Acos(SeparationDot)); + if (SeparationAngleDeg > ThrowAngleThresholdDeg) + { + static const auto DrawDebugCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.debug")); + if (DrawDebugCVar->GetInt() && CVarThrowAtGazeDebug.GetValueOnGameThread()) + { + UE_LOG(LogTemp, Warning, TEXT("Throw diverged too much. Separation angle was: %f"), SeparationAngleDeg); + } + + return OriginalThrowVector; + } + } + } + + if (ThrowSpeedThreshold > FLT_EPSILON) + { + float SuggestedSpeedSq = BallisticResult.SuggestedInitialVelocity.SizeSquared(); + float OriginalSpeedSq = OriginalThrowVector.SizeSquared(); + float ThresholdSq = ThrowSpeedThreshold * ThrowSpeedThreshold; + + //Only invalidate too weak throws + if (OriginalSpeedSq + ThresholdSq < SuggestedSpeedSq) + { + static const auto DrawDebugCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.debug")); + if (DrawDebugCVar->GetInt() && CVarThrowAtGazeDebug.GetValueOnGameThread()) + { + UE_LOG(LogTemp, Warning, TEXT("Throw was too weak. Throw speed was: %f. Suggested speed was: %f"), OriginalThrowVector.Size(), BallisticResult.SuggestedInitialVelocity.Size()); + } + + return OriginalThrowVector; + } + } + + return BallisticResult.SuggestedInitialVelocity; + } + + default: + { + return OriginalThrowVector; + } + } +} + +ETobiiThrowAtGazeResult UTobiiThrowAtGazeComponent::CalculateThrowAtGazeVector(const FVector& ThrowOrigin, FTobiiBallisticResult& OutBallisticResult, TArray& OutTracedPath) +{ + UWorld* World = GetWorld(); + if (World == nullptr || GEngine == nullptr || !GEngine->EyeTrackingDevice.IsValid()) + { + return ETobiiThrowAtGazeResult::UnknownError; + } + + if (ThrowTarget.IsValid()) + { + return CalculateThrowArc(ThrowOrigin, ThrowTarget->GetCenterOfMass(), ThrowTarget->GetOwner()->GetVelocity(), CalculatedTargetAcceleration, OutBallisticResult, OutTracedPath); + } + else if (World->GetFirstPlayerController() != nullptr && World->GetFirstPlayerController()->PlayerCameraManager != nullptr) + { + FVector GazeTargetPosition; + FVector GazeRayOrigin = FVector::ZeroVector; + FVector GazeRayDirection = FVector::ZeroVector; + + FEyeTrackerGazeData CombinedGazeData; + GEngine->EyeTrackingDevice->GetEyeTrackerGazeData(CombinedGazeData); + if (CombinedGazeData.ConfidenceValue > 0.5f) + { + GazeRayOrigin = CombinedGazeData.GazeOrigin; + GazeRayDirection = CombinedGazeData.GazeDirection; + } + else + { + // If eyetracking data is not available this frame, just use the center of the screen + GazeRayOrigin = World->GetFirstPlayerController()->PlayerCameraManager->GetCameraLocation(); + GazeRayDirection = World->GetFirstPlayerController()->PlayerCameraManager->GetCameraRotation().Vector(); + } + + FVector EndPoint = GazeRayOrigin + GazeRayDirection * MaxTraceDistance; + FHitResult HitResult; + if (World->LineTraceSingleByChannel(HitResult, GazeRayOrigin, EndPoint, TraceChannel)) + { + GazeTargetPosition = HitResult.Location; + } + else + { + GazeTargetPosition = EndPoint; + } + + return CalculateThrowArc(ThrowOrigin, GazeTargetPosition, FVector::ZeroVector, FVector::ZeroVector, OutBallisticResult, OutTracedPath); + } + + return ETobiiThrowAtGazeResult::NoEyetrackingInput; +} + +ETobiiThrowAtGazeResult UTobiiThrowAtGazeComponent::CalculateThrowArc(const FVector& ThrowOrigin, const FVector& TargetLocation, const FVector& TargetVelocity, const FVector& TargetAcceleration, FTobiiBallisticResult& OutBallisticResult, TArray& OutTracedPath) +{ + OutBallisticResult = FTobiiBallisticResult(); + OutTracedPath = TArray(); + if (GetWorld() == nullptr) + { + return ETobiiThrowAtGazeResult::UnknownError; + } + + static const auto DrawDebugCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.debug")); + const FVector ProjectileAcceleration = CalculateProjectileGravityVector(); + const float BaseAcceptanceThreshold = TraceRadiusCm + NoTargetAcceptanceThreshold; + const float TraceAcceptanceThresholdSq = BaseAcceptanceThreshold * BaseAcceptanceThreshold; + + bool bShouldDrawDebug = DrawDebugCVar->GetInt() && CVarThrowAtGazeDebug.GetValueOnGameThread(); + if (bShouldDrawDebug) + { + DrawDebugSphere(GetWorld(), TargetLocation, 20.0f, 16, FColor::Yellow, false, 5.0f); + } + + FTobiiBallisticData BallisticData; + BallisticData.ProjectileInitialPosition = ThrowOrigin; + BallisticData.ProjectileAcceleration = ProjectileAcceleration; + BallisticData.TargetPosition = TargetLocation; + BallisticData.TargetVelocity = TargetVelocity; + BallisticData.TargetAcceleration = TargetAcceleration; + + TArray IgnoredActors = TraceIgnoreActors; + if (bShouldTraceIgnoreOwner) + { + IgnoredActors.Add(GetOwner()); + } + + FTobiiProjectileTraceData TraceData; + TraceData.TraceChannel = TraceChannel; + TraceData.IgnoredActors = IgnoredActors; + TraceData.MaxNrSteps = MaxNrTraceSteps; + TraceData.StepSizeSecs = TraceStepSizeSecs; + TraceData.TraceRadiusCm = TraceRadiusCm; + TraceData.ProjectileInitialPosition = ThrowOrigin; + TraceData.ProjectileAcceleration = ProjectileAcceleration; + + TArray ApexesToTest; + if (MaxIterations > 0) + { + //We want to first decide which apexes we should test, and then we can use logic to suss out which one ends up being the best. + int32 Iterations = FMath::Max(0, MaxIterations); + float ApexStep = (ThrowApexOffsetMaximumCm - ThrowApexOffsetMinimumCm) / Iterations; + for (int32 ApexIdx = 0; ApexIdx <= Iterations; ApexIdx++) + { + float CurrentApex = ThrowApexOffsetMinimumCm + ApexIdx * ApexStep; + ApexesToTest.Add(CurrentApex); + } + } + else + { + ApexesToTest.Add((ThrowApexOffsetMaximumCm + ThrowApexOffsetMinimumCm) / 2); + } + + // Loop variables + float BestResultDistanceToTargetSq = FLT_MAX; + ETobiiThrowAtGazeResult BestResultStatus = ETobiiThrowAtGazeResult::NoPath; + while (ApexesToTest.Num() > 0) + { + // Pick the next apex + int32 SelectedApexIndex; + if (bAlwaysPreferLowApex || (bPreferLowApexForTargetsBelowThrower && TargetLocation.Z < ThrowOrigin.Z)) + { + SelectedApexIndex = 0; + } + else + { + SelectedApexIndex = (int32)(ApexesToTest.Num() / 2.0f); + } + SelectedApexIndex = FMath::Clamp(SelectedApexIndex, 0, ApexesToTest.Num() - 1); + BallisticData.ProjectileApexOffsetCm = ApexesToTest[SelectedApexIndex]; + ApexesToTest.RemoveAt(SelectedApexIndex); + + // Find initial velocity for given apex + TArray Results; + if (UTobiiInteractionsBlueprintLibrary::FindNeededInitialVelocityForBallisticProjectile(BallisticData, Results)) + { + for (FTobiiBallisticResult& Result : Results) + { + float SuggestedInitialSpeed = Result.SuggestedInitialVelocity.Size(); + if (SuggestedInitialSpeed < FLT_EPSILON) + { + continue; + } + + bool bIsInRange = SuggestedInitialSpeed < MaxThrowSpeedCmPerSecs; + if (!bIsInRange) + { + Result.SuggestedInitialVelocity.Normalize(); + Result.SuggestedInitialVelocity *= MaxThrowSpeedCmPerSecs; + } + + TArray TracedPath; + bool bWayIsClear = true; + float DistanceToTargetSq = FLT_MAX; + if (bShouldTraceResult) + { + TraceData.ProjectileVelocity = Result.SuggestedInitialVelocity; + + FHitResult HitResult; + if (UTobiiInteractionsBlueprintLibrary::TraceBallisticProjectilePath(GetWorld(), TraceData, TracedPath, HitResult)) + { + FVector HitToTarget = TargetLocation - HitResult.Location; + float ToTargetDistSq = HitToTarget.SizeSquared(); + + if (ThrowTarget.IsValid()) + { + if (HitResult.Actor != ThrowTarget->GetOwner()) + { + bWayIsClear = false; // If we have a target, and hit something, that's okay as long as it's our target. + } + } + else + { + bWayIsClear = ToTargetDistSq < TraceAcceptanceThresholdSq; + } + + DistanceToTargetSq = ToTargetDistSq; + } + + if (bShouldDrawDebug) + { + if (TracedPath.Num() > 1) + { + FColor DrawColor = bWayIsClear ? FColor::Green : FColor::Red; + FVector PrevPoint = TracedPath[0]; + int32 MiddlePointIdx = (int32)(TracedPath.Num() / 2.0f); + if (MiddlePointIdx >= 0 && MiddlePointIdx < TracedPath.Num()) + { + DrawDebugString(GetWorld(), TracedPath[MiddlePointIdx] + FVector(0.0f, 0.0f, 10.0f), FString::Printf(TEXT("%f"), BallisticData.ProjectileApexOffsetCm), nullptr, DrawColor, 5.0f); + } + for (auto& TracePoint : TracedPath) + { + DrawDebugLine(GetWorld(), PrevPoint, TracePoint, DrawColor, false, 5.0f, 0, 1.0f); + PrevPoint = TracePoint; + } + } + } + } + + if (bWayIsClear) + { + if (bIsInRange) + { + //We're done! Best outcome. + OutBallisticResult = Result; + OutTracedPath = TracedPath; + BestResultDistanceToTargetSq = DistanceToTargetSq; + BestResultStatus = ETobiiThrowAtGazeResult::DirectHit; + break; + } + else if (BestResultStatus <= ETobiiThrowAtGazeResult::OutOfRange) + { + bool bIsFaster = Result.ExpectedInterceptTimeSecs < OutBallisticResult.ExpectedInterceptTimeSecs; + if (bIsFaster) + { + OutBallisticResult = Result; + OutTracedPath = TracedPath; + BestResultDistanceToTargetSq = DistanceToTargetSq; + BestResultStatus = ETobiiThrowAtGazeResult::OutOfRange; + } + } + } + else if (TracedPath.Num() > 0) + { + if (bIsInRange) + { + if (BestResultStatus <= ETobiiThrowAtGazeResult::BlockedByWorldInRange) + { + bool bIsCloser = DistanceToTargetSq < BestResultDistanceToTargetSq; + if (bIsCloser) + { + OutBallisticResult = Result; + OutTracedPath = TracedPath; + BestResultDistanceToTargetSq = DistanceToTargetSq; + BestResultStatus = ETobiiThrowAtGazeResult::BlockedByWorldInRange; + } + } + } + else + { + if (BestResultStatus <= ETobiiThrowAtGazeResult::BlockedByWorldAndOutOfRange) + { + bool bIsCloser = DistanceToTargetSq < BestResultDistanceToTargetSq; + if (bIsCloser) + { + OutBallisticResult = Result; + OutTracedPath = TracedPath; + BestResultDistanceToTargetSq = DistanceToTargetSq; + BestResultStatus = ETobiiThrowAtGazeResult::BlockedByWorldAndOutOfRange; + } + } + } + } + } + } + + if (BestResultStatus == ETobiiThrowAtGazeResult::DirectHit) + { + break; + } + + //Use what we have learned to prune apexes for future loops + if (Results.Num() == 0) + { + //If we didn't have any solutions or we are out of range, it means the apex is too low. Clear all apexes that are smaller than our current. + for (int32 CurrentApexIdx = 0; CurrentApexIdx < ApexesToTest.Num(); CurrentApexIdx++) + { + if (ApexesToTest[CurrentApexIdx] <= BallisticData.ProjectileApexOffsetCm) + { + ApexesToTest.RemoveAt(CurrentApexIdx); + CurrentApexIdx--; + } + } + } + else if (BestResultStatus == ETobiiThrowAtGazeResult::OutOfRange) + { + //If we are out of range, we need to select an apex closer to a 45 degree throw angle. + //@TODO: Should we maybe do a solve for throw force given 45 degree throw angle here to optimize? + FVector VelNorm = OutBallisticResult.SuggestedInitialVelocity; + FVector ProjectedToXY = FVector(VelNorm.X, VelNorm.Y, 0.0f); + bool bHasValidVectors = VelNorm.Normalize(); + bHasValidVectors = bHasValidVectors && ProjectedToXY.Normalize(); + if (bHasValidVectors) + { + float ThrowAngleCos = FVector::DotProduct(VelNorm, ProjectedToXY); + + //1 means it is along the ground, 0 is straight up. So if the value is between 0.5 and 1 we need to aim higher and if it's between 0 and 0.5 we need to go lower + if (ThrowAngleCos < 0.5f) + { + for (int32 CurrentApexIdx = 0; CurrentApexIdx < ApexesToTest.Num(); CurrentApexIdx++) + { + if (ApexesToTest[CurrentApexIdx] >= BallisticData.ProjectileApexOffsetCm) + { + ApexesToTest.RemoveAt(CurrentApexIdx); + CurrentApexIdx--; + } + } + } + else + { + for (int32 CurrentApexIdx = 0; CurrentApexIdx < ApexesToTest.Num(); CurrentApexIdx++) + { + if (ApexesToTest[CurrentApexIdx] <= BallisticData.ProjectileApexOffsetCm) + { + ApexesToTest.RemoveAt(CurrentApexIdx); + CurrentApexIdx--; + } + } + } + } + } + } + + // Output results + return BestResultStatus; +} diff --git a/Source/TobiiInteractions/Private/Desktop/TobiiAimAtGazeComponent.cpp b/Source/TobiiInteractions/Private/Desktop/TobiiAimAtGazeComponent.cpp new file mode 100644 index 0000000..e0e2bad --- /dev/null +++ b/Source/TobiiInteractions/Private/Desktop/TobiiAimAtGazeComponent.cpp @@ -0,0 +1,141 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#include "TobiiAimAtGazeComponent.h" +#include "TobiiInteractionsBlueprintLibrary.h" +#include "TobiiInteractionsInternalTypes.h" +#include "TobiiGTOMBlueprintLibrary.h" + +#include "DrawDebugHelpers.h" +#include "Engine/World.h" +#include "GameFramework/Pawn.h" +#include "GameFramework/PlayerController.h" +#include "HAL/IConsoleManager.h" + +static TAutoConsoleVariable CVarAimAtGazeEnabled(TEXT("tobii.interaction.AimAtGazeEnabled"), 1, TEXT("Aim@gaze will rotate the view to align with the gaze point when the user triggers it, usually by entering ADS (Aim Down Sights). 0 - Aim@gaze is off. 1 - Aim@gaze is on.")); +static TAutoConsoleVariable CVarAimAtGazeDoneThreshold(TEXT("tobii.interaction.AimAtGazeDoneThreshold"), 0.0001, TEXT("If the dot product between the current and target direction quats is smaller than this, we are done rotating.")); + +static TAutoConsoleVariable CVarAimAtGazeDebug(TEXT("tobii.debug.AimAtGaze"), 1, TEXT("0 - Will not visualize aim@gaze debug data. 1 - Will visualize aim@gaze debug data.")); + +UTobiiAimAtGazeComponent::UTobiiAimAtGazeComponent() + : bAllowRetarget(false) + , AimSpeed(0.2f) + + , CurrentFocusComponent(nullptr) + , CurrentAimTarget(0.0f, 0.0f, 0.0f) + , bIsGazeAiming(false) +{ + PrimaryComponentTick.bCanEverTick = true; +} + +bool UTobiiAimAtGazeComponent::AimAtGazeAvailable() +{ + return UTobiiInteractionsBlueprintLibrary::IsAimAtGazeEnabled(); +} + +void UTobiiAimAtGazeComponent::AimAtGaze() +{ + if (!AimAtGazeAvailable()) + { + return; + } + + CurrentFocusComponent.Reset(); + + FTobiiGazeFocusData FocusData; + UTobiiGTOMBlueprintLibrary::GetFilteredGazeFocusData(FocusLayerFilters, bIsWhiteList, true, false, FocusData); + if (FocusData.FocusedActor.IsValid()) + { + CurrentFocusComponent = FocusData.FocusedPrimitiveComponent; + CurrentAimTarget = FocusData.LastVisibleWorldLocation; + bIsGazeAiming = true; + } + else + { + const FHitResult& GazeHitData = UTobiiGTOMBlueprintLibrary::GetNaiveGazeHit(); + CurrentAimTarget = GazeHitData.Location; + bIsGazeAiming = true; + } +} + +void UTobiiAimAtGazeComponent::ContinuousAimAtGaze() +{ + if (!AimAtGazeAvailable()) + { + CurrentFocusComponent = nullptr; + return; + } + + if (bAllowRetarget) + { + FTobiiGazeFocusData FocusData; + UTobiiGTOMBlueprintLibrary::GetFilteredGazeFocusData(FocusLayerFilters, bIsWhiteList, true, false, FocusData); + if (FocusData.FocusedPrimitiveComponent.IsValid()) + { + CurrentFocusComponent = FocusData.FocusedPrimitiveComponent; + } + } + + if (CurrentFocusComponent.IsValid()) + { + FVector TargetFocusPosition; + UTobiiGTOMBlueprintLibrary::GetPrimitiveComponentFocusLocation(CurrentFocusComponent.Get(), TargetFocusPosition); + + CurrentAimTarget = TargetFocusPosition; + bIsGazeAiming = true; + } + else + { + CurrentFocusComponent.Reset(); + } +} + +void UTobiiAimAtGazeComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) +{ + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + + static const auto DrawDebugCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.debug")); + if (CurrentFocusComponent != nullptr && DrawDebugCVar->GetInt() && CVarAimAtGazeDebug.GetValueOnGameThread()) + { + DrawDebugBox(GetWorld(), CurrentAimTarget, FVector(50.0f), FColor::Cyan, false, 0.0f); + } + + APlayerController* PlayerController = nullptr; + APawn* OwnerPawn = Cast(GetOwner()); + + // We make it possible to attach AimAtGaze component to either Pawn or PlayerController + if (OwnerPawn != nullptr) + { + PlayerController = Cast(OwnerPawn->GetController()); + } + else + { + PlayerController = Cast(GetOwner()); + } + + if (PlayerController != nullptr) + { + if (AimAtGazeAvailable() && bIsGazeAiming && PlayerController->PlayerCameraManager != nullptr) + { + FVector CameraLocation = PlayerController->PlayerCameraManager->GetCameraLocation(); + FVector AimTargetDirection = CurrentAimTarget - CameraLocation; + + FQuat TargetQuat = AimTargetDirection.ToOrientationQuat(); + FQuat CurrentControlQuat = FQuat(PlayerController->GetControlRotation()); + + CurrentControlQuat = FQuat::Slerp(CurrentControlQuat, TargetQuat, AimSpeed); + float DotDistance = (CurrentControlQuat | TargetQuat); + + if ((1.0f - DotDistance) < CVarAimAtGazeDoneThreshold.GetValueOnGameThread()) + { + bIsGazeAiming = false; + CurrentControlQuat = TargetQuat; + } + + PlayerController->SetControlRotation(CurrentControlQuat.Rotator()); + } + } +} diff --git a/Source/TobiiInteractions/Private/Desktop/TobiiFireAtGazeComponent.cpp b/Source/TobiiInteractions/Private/Desktop/TobiiFireAtGazeComponent.cpp new file mode 100644 index 0000000..557fca7 --- /dev/null +++ b/Source/TobiiInteractions/Private/Desktop/TobiiFireAtGazeComponent.cpp @@ -0,0 +1,114 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#include "TobiiFireAtGazeComponent.h" +#include "TobiiInteractionsBlueprintLibrary.h" +#include "TobiiGTOMBlueprintLibrary.h" +#include "../TobiiInteractionsInternalTypes.h" + +#include "Engine/Engine.h" +#include "IEyeTracker.h" +#include "Camera/CameraComponent.h" +#include "Engine/World.h" +#include "HAL/IConsoleManager.h" + +static TAutoConsoleVariable CVarFireAtGazeEnabled(TEXT("tobii.interaction.FireAtGazeEnabled"), 1, TEXT("Fire at gaze lets you fire where you look when not in ADS (Aim Down Sights) mode. 0 - Fire at gaze is disabled. 1 - Fire at gaze is enabled.")); + +UTobiiFireAtGazeComponent::UTobiiFireAtGazeComponent() + : CameraComponent(nullptr) + + , FireAtGazeTargetActor(nullptr) + , FireAtGazeTargetComponent(nullptr) + , FireAtGazeTargetLocation(0.0f, 0.0f, 0.0f) + + , MaxDistance(10000.0f) + , NoTargetBehavior(ETobiiFireAtGazeNoTargetBehavior::PointGunToGaze) + , TraceChannel(ECC_Visibility) +{ + PrimaryComponentTick.bCanEverTick = true; +} + +bool UTobiiFireAtGazeComponent::FireAtGazeAvailable() +{ + return CameraComponent != nullptr + && UTobiiInteractionsBlueprintLibrary::IsFireAtGazeEnabled(); +} + +bool UTobiiFireAtGazeComponent::WantsCrosshair() +{ + if (!FireAtGazeAvailable()) + { + return false; + } + + switch (NoTargetBehavior) + { + case ETobiiFireAtGazeNoTargetBehavior::PointGunForward: + return !FireAtGazeTargetActor.IsValid(); + case ETobiiFireAtGazeNoTargetBehavior::PointGunToGaze: + return true; + default: + return false; + } +} + +void UTobiiFireAtGazeComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) +{ + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + + if (!FireAtGazeAvailable() || GEngine == nullptr || !GEngine->EyeTrackingDevice.IsValid()) + { + return; + } + + FTobiiGazeFocusData FocusData; + UTobiiGTOMBlueprintLibrary::GetFilteredGazeFocusData(FocusLayerFilters, bIsWhiteList, true, false, FocusData); + if (FocusData.FocusedPrimitiveComponent.IsValid()) + { + FireAtGazeTargetActor = FocusData.FocusedActor; + FireAtGazeTargetComponent = FocusData.FocusedPrimitiveComponent; + FireAtGazeTargetLocation = FocusData.LastVisibleWorldLocation; + } + else + { + FVector GazeRayOrigin, GazeRayDirection; + switch (NoTargetBehavior) + { + case ETobiiFireAtGazeNoTargetBehavior::PointGunForward: + { + GazeRayOrigin = CameraComponent->GetComponentLocation(); + GazeRayDirection = CameraComponent->GetForwardVector(); + break; + } + + case ETobiiFireAtGazeNoTargetBehavior::PointGunToGaze: + default: + { + FEyeTrackerGazeData CombinedGazeData; + GEngine->EyeTrackingDevice->GetEyeTrackerGazeData(CombinedGazeData); + GazeRayOrigin = CombinedGazeData.GazeOrigin; + GazeRayDirection = CombinedGazeData.GazeDirection; + break; + } + } + + FHitResult HitResult; + UWorld* World = GetWorld(); + if (World != nullptr && World->LineTraceSingleByChannel(HitResult, GazeRayOrigin, GazeRayOrigin + GazeRayDirection * MaxDistance, TraceChannel) + && UTobiiGTOMBlueprintLibrary::IsPrimitiveComponentGazeFocusable(HitResult.GetComponent())) + { + FireAtGazeTargetActor = HitResult.GetActor(); + FireAtGazeTargetComponent = HitResult.GetComponent(); + FireAtGazeTargetLocation = HitResult.Location; + } + else + { + FireAtGazeTargetActor = nullptr; + FireAtGazeTargetComponent = nullptr; + FireAtGazeTargetLocation = GazeRayOrigin + GazeRayDirection * MaxDistance; + } + } +} diff --git a/Source/TobiiInteractions/Private/TobiiInteractionsBlueprintLibrary.cpp b/Source/TobiiInteractions/Private/TobiiInteractionsBlueprintLibrary.cpp new file mode 100644 index 0000000..a3fe496 --- /dev/null +++ b/Source/TobiiInteractions/Private/TobiiInteractionsBlueprintLibrary.cpp @@ -0,0 +1,490 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +* Thanks to Jochen Schwarze (schwarze@isa.de) for functions to solve square, cubic and quartic functions as found here: +* https://github.com/erich666/GraphicsGems/blob/240a34f2ad3fa577ef57be74920db6c4b00605e4/gems/Roots3And4.c +******************************************************************************/ + +#include "TobiiInteractionsBlueprintLibrary.h" +#include "TobiiRootFinders.h" + +#include "Components/WidgetComponent.h" +#include "IEyeTracker.h" +#include "Engine/Engine.h" +#include "Engine/World.h" +#include "Engine/Texture2D.h" +#include "Engine/TextureRenderTarget2D.h" +#include "Framework/Application/SlateApplication.h" +#include "HAL/IConsoleManager.h" +#include "Slate/WidgetRenderer.h" + +UTobiiInteractionsBlueprintLibrary::UTobiiInteractionsBlueprintLibrary(const class FObjectInitializer& ObjectInitializer) +: Super(ObjectInitializer) +{ +} + +/** + * Renders a UMG Widget to a texture with the specified size. + * + * @param Widget The widget to be rendered. + * @param DrawSize The size to render the Widget to. Also will be the texture size. + * @return The texture containing the rendered widget. + */ +UTexture2D* UTobiiInteractionsBlueprintLibrary::TextureFromWidget(UUserWidget* const Widget, const FVector2D& DrawSize) +{ + if (FSlateApplication::IsInitialized() + && Widget != nullptr && Widget->IsValidLowLevel() + && DrawSize.X >= 1 && DrawSize.Y >= 1) + { + TSharedPtr SlateWidget(Widget->TakeWidget()); + if (!SlateWidget.IsValid()) + { + return nullptr; + } + + FWidgetRenderer WidgetRenderer = FWidgetRenderer(true); + + UTextureRenderTarget2D* TextureRenderTarget = WidgetRenderer.DrawWidget(SlateWidget.ToSharedRef(), DrawSize); + // Creates Texture2D to store RenderTexture content + UTexture2D *Texture = UTexture2D::CreateTransient(DrawSize.X, DrawSize.Y, PF_B8G8R8A8); +#if WITH_EDITORONLY_DATA + Texture->MipGenSettings = TMGS_NoMipmaps; +#endif + + // Lock and copies the data between the textures + TArray SurfData; + FRenderTarget* RenderTarget = TextureRenderTarget->GameThread_GetRenderTargetResource(); + RenderTarget->ReadPixels(SurfData); + + void* TextureData = Texture->PlatformData->Mips[0].BulkData.Lock(LOCK_READ_WRITE); + const int32 TextureDataSize = SurfData.Num() * 4; + FMemory::Memcpy(TextureData, SurfData.GetData(), TextureDataSize); + Texture->PlatformData->Mips[0].BulkData.Unlock(); + Texture->UpdateResource(); + + // Free resources + SurfData.Empty(); + TextureRenderTarget->ConditionalBeginDestroy(); + SlateWidget.Reset(); + + return Texture; + } + + return nullptr; +} + +bool UTobiiInteractionsBlueprintLibrary::IsInfiniteScreenEnabled() +{ + static const auto EyetrackingEnabledCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.EnableEyetracking")); + static const auto InfiniteScreenEnabledCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.desktop.InfiniteScreenEnabled")); + + if (EyetrackingEnabledCVar != nullptr && InfiniteScreenEnabledCVar != nullptr && GEngine != nullptr && GEngine->EyeTrackingDevice.IsValid()) + { + return GEngine->EyeTrackingDevice->GetEyeTrackerStatus() >= EEyeTrackerStatus::Tracking + && EyetrackingEnabledCVar->GetInt() + && InfiniteScreenEnabledCVar->GetInt(); + } + + return false; +} + +bool UTobiiInteractionsBlueprintLibrary::IsCleanUIEnabled() +{ + static const auto EyetrackingEnabledCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.EnableEyetracking")); + static const auto CleanUIEnabledCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.interaction.EnableCleanUI")); + + if (EyetrackingEnabledCVar != nullptr && CleanUIEnabledCVar != nullptr && GEngine != nullptr && GEngine->EyeTrackingDevice.IsValid()) + { + return GEngine->EyeTrackingDevice->GetEyeTrackerStatus() >= EEyeTrackerStatus::Tracking + && EyetrackingEnabledCVar->GetInt() + && CleanUIEnabledCVar->GetInt(); + } + + return false; +} + +bool UTobiiInteractionsBlueprintLibrary::IsAimAtGazeEnabled() +{ + static const auto EyetrackingEnabledCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.EnableEyetracking")); + static const auto AimAtGazeEnabledCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.interaction.AimAtGazeEnabled")); + + if (EyetrackingEnabledCVar != nullptr && AimAtGazeEnabledCVar != nullptr && GEngine != nullptr && GEngine->EyeTrackingDevice.IsValid()) + { + return GEngine->EyeTrackingDevice->GetEyeTrackerStatus() >= EEyeTrackerStatus::Tracking + && EyetrackingEnabledCVar->GetInt() + && AimAtGazeEnabledCVar->GetInt(); + } + + return false; +} + +bool UTobiiInteractionsBlueprintLibrary::IsFireAtGazeEnabled() +{ + static const auto EyetrackingEnabledCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.EnableEyetracking")); + static const auto FireAtGazeEnabledCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.interaction.FireAtGazeEnabled")); + + if (EyetrackingEnabledCVar != nullptr && FireAtGazeEnabledCVar != nullptr && GEngine != nullptr && GEngine->EyeTrackingDevice.IsValid()) + { + return GEngine->EyeTrackingDevice->GetEyeTrackerStatus() >= EEyeTrackerStatus::Tracking + && EyetrackingEnabledCVar->GetInt() + && FireAtGazeEnabledCVar->GetInt(); + } + + return false; +} + +bool UTobiiInteractionsBlueprintLibrary::IsThrowAtGazeEnabled() +{ + static const auto EyetrackingEnabledCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.EnableEyetracking")); + static const auto ThrowAtGazeEnabledCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("tobii.interaction.ThrowAtGazeEnabled")); + + if (EyetrackingEnabledCVar != nullptr && ThrowAtGazeEnabledCVar != nullptr && GEngine != nullptr && GEngine->EyeTrackingDevice.IsValid()) + { + return GEngine->EyeTrackingDevice->GetEyeTrackerStatus() >= EEyeTrackerStatus::Tracking + && EyetrackingEnabledCVar->GetInt() + && ThrowAtGazeEnabledCVar->GetInt(); + } + + return false; +} + +float UTobiiInteractionsBlueprintLibrary::CalculateSmoothPitchStep(float ViewPitch) +{ + //Limit infinite screen yaw depending on input device pitch + float NormalizedPitch = FRotator::NormalizeAxis(ViewPitch); + float PitchScale = 1.0f - (FMath::Abs(NormalizedPitch) / 90.0f); + return FMath::SmoothStep(0.0f, 1.0f, PitchScale); +} + +FRotator UTobiiInteractionsBlueprintLibrary::MakeInfiniteScreenCameraRotator(FRotator OriginalCameraRotation, FRotator InfiniteScreenAngles) +{ + FQuat WorldSpaceYawRotation = FQuat(FVector::UpVector, InfiniteScreenAngles.Yaw); + FQuat LocalSpacePitchRotation = FQuat(FVector::RightVector, -InfiniteScreenAngles.Pitch); + + FQuat InfiniteScreenViewRotation(OriginalCameraRotation); + InfiniteScreenViewRotation = InfiniteScreenViewRotation * LocalSpacePitchRotation; //Local space by multiplying on the right + InfiniteScreenViewRotation = WorldSpaceYawRotation * InfiniteScreenViewRotation; //World space by multiplying on the left + return InfiniteScreenViewRotation.Rotator(); +} + +//////////////////////////////////////////////////////////////////////////// + +void UTobiiInteractionsBlueprintLibrary::FindRealSquareRoots(float A, float B, float C, TArray& OutRealRoots) +{ + double Coefficients[3]{ C, B, A }; + double Solutions[2]{ 0.0, 0.0 }; + + int32 NrSolutions = SolveQuadric(Coefficients, Solutions); + OutRealRoots.Empty(NrSolutions); + for (int32 SolutionIdx = 0; SolutionIdx < NrSolutions; SolutionIdx++) + { + OutRealRoots.Add(Solutions[SolutionIdx]); + } +} + +void UTobiiInteractionsBlueprintLibrary::FindRealCubicRoots(float A, float B, float C, float D, TArray& OutRealRoots) +{ + double Coefficients[4]{ D, C, B, A }; + double Solutions[3]{ 0.0, 0.0, 0.0 }; + + int32 NrSolutions = SolveCubic(Coefficients, Solutions); + OutRealRoots.Empty(NrSolutions); + for (int32 SolutionIdx = 0; SolutionIdx < NrSolutions; SolutionIdx++) + { + OutRealRoots.Add(Solutions[SolutionIdx]); + } +} + +void UTobiiInteractionsBlueprintLibrary::FindRealQuarticRoots(float A, float B, float C, float D, float E, TArray& OutRealRoots) +{ + double Coefficients[5]{ E, D, C, B, A }; + double Solutions[4]{ 0.0, 0.0, 0.0, 0.0 }; + + int32 NrSolutions = SolveQuartic(Coefficients, Solutions); + OutRealRoots.Empty(NrSolutions); + for (int32 SolutionIdx = 0; SolutionIdx < NrSolutions; SolutionIdx++) + { + OutRealRoots.Add(Solutions[SolutionIdx]); + } +} + +/** + * Try to find the appropriate acceleration to hit a moving target. + * We do this by setting up an equation system with 4 equations in 4 unknowns. + * First we solve for time, and then we plug that into the other equations to find the wanted acceleration. + * + * VARIABLES: + * Time: t <--- Need to first solve for this + * Projectile Pos: PPX, PPY, PPZ + * Projectile Vel: PVX, PVY, PVZ + * Projectile Acc: pax, pay, paz <--- Solving for this is our goal + * Projectile AccMagnitude: PAM + * Target Pos: TPX, TPY, TPZ + * Target Vel: TVX, TVY, TVZ + * Target Acc: TAX, TAY, TAZ + * + * EQUATIONS: + * PPX + PVX * t + (1/2) * pax * t^2 = TPX + TVX * t + (1/2) * TAX * t^2 + * PPY + PVY * t + (1/2) * pay * t^2 = TPY + TVY * t + (1/2) * TAY * t^2 + * PPZ + PVZ * t + (1/2) * paz * t^2 = TPZ + TVZ * t + (1/2) * TAZ * t^2 + * PAM^2 = pax^2 + pay^2 + paz^2 + * + * SOLVE FOR ACCELERATION: + * pax = 2.0 * (TPX + TVX * t + (1/2) * TAX * t^2 - PPX - PVX * t) / t^2 + * pay = 2.0 * (TPY + TVY * t + (1/2) * TAY * t^2 - PPY - PVY * t) / t^2 + * paz = 2.0 * (TPZ + TVZ * t + (1/2) * TAZ * t^2 - PPZ - PVZ * t) / t^2 + * + * SIMPLIFY: + * DPX = TPX - PPX + * DPY = TPY - PPY + * DPZ = TPZ - PPZ + * DVX = TVX - PVX + * DVY = TVY - PVY + * DVZ = TVZ - PVZ + * pax = 2.0 * (DPX + DVX * t + (1/2) * TAX * t^2) / t^2 + * pay = 2.0 * (DPY + DVY * t + (1/2) * TAY * t^2) / t^2 + * paz = 2.0 * (DPZ + DVZ * t + (1/2) * TAZ * t^2) / t^2 + * + * SQUARE SO WE CAN SUBSTITUTE LATER: + * pax^2 = 4.0 * (DPX + DVX * t + (1/2) * TAX * t^2)^2 / t^4 + * pay^2 = 4.0 * (DPY + DVY * t + (1/2) * TAY * t^2)^2 / t^4 + * paz^2 = 4.0 * (DPZ + DVZ * t + (1/2) * TAZ * t^2)^2 / t^4 + * + * EXPAND THE SQUARES SO WE CAN EXTRACT COEFFICIENTS LATER: + * pax^2 = 4.0 * (DPX^2 + 2.0*DPX*DVX*t + DPX*TAX*t^2 + DVX^2*t^2 + DVX*TAX*t^3 + (1/4)*TAX^2*t^4) / t^4 + * pay^2 = 4.0 * (DPY^2 + 2.0*DPY*DVY*t + DPY*TAY*t^2 + DVY^2*t^2 + DVY*TAY*t^3 + (1/4)*TAY^2*t^4) / t^4 + * paz^2 = 4.0 * (DPZ^2 + 2.0*DPZ*DVZ*t + DPZ*TAZ*t^2 + DVZ^2*t^2 + DVZ*TAZ*t^3 + (1/4)*TAZ^2*t^4) / t^4 + * + * SUBSTITUTE INTO OUR PAM EQUATION: + * PAM^2 = 4.0 * (DPX^2 + 2.0*DPX*DVX*t + DPX*TAX*t^2 + DVX^2*t^2 + DVX*TAX*t^3 + (1/4)*TAX^2*t^4) / t^4 + * + 4.0 * (DPY^2 + 2.0*DPY*DVY*t + DPY*TAY*t^2 + DVY^2*t^2 + DVY*TAY*t^3 + (1/4)*TAY^2*t^4) / t^4 + * + 4.0 * (DPZ^2 + 2.0*DPZ*DVZ*t + DPZ*TAZ*t^2 + DVZ^2*t^2 + DVZ*TAZ*t^3 + (1/4)*TAZ^2*t^4) / t^4 + * + * MULTIPLY BY t^4: + * PAM^2 * t^4 = 4.0 * (DPX^2 + 2.0*DPX*DVX*t + DPX*TAX*t^2 + DVX^2*t^2 + DVX*TAX*t^3 + (1/4)*TAX^2*t^4) + * + 4.0 * (DPY^2 + 2.0*DPY*DVY*t + DPY*TAY*t^2 + DVY^2*t^2 + DVY*TAY*t^3 + (1/4)*TAY^2*t^4) + * + 4.0 * (DPZ^2 + 2.0*DPZ*DVZ*t + DPZ*TAZ*t^2 + DVZ^2*t^2 + DVZ*TAZ*t^3 + (1/4)*TAZ^2*t^4) + * + * MAKE 0 EQUATION AND FORM TERMS: + * 0 = 4.0*DPX^2 + 8.0*DPX*DVX*t + 4.0*DPX*TAX*t^2 + 4.0*DVX^2*t^2 + 4.0*DVX*TAX*t^3 + TAX^2*t^4 + * + 4.0*DPY^2 + 8.0*DPY*DVY*t + 4.0*DPY*TAY*t^2 + 4.0*DVY^2*t^2 + 4.0*DVY*TAY*t^3 + TAY^2*t^4 + * + 4.0*DPZ^2 + 8.0*DPZ*DVZ*t + 4.0*DPZ*TAZ*t^2 + 4.0*DVZ^2*t^2 + 4.0*DVZ*TAZ*t^3 + TAZ^2*t^4 + * - PAM^2 * t^4 + * + * ARRANGE IN COEFFICIENT FORM: + * 0 = (TAX^2 + TAY^2 + TAZ^2 - PAM^2) * t^4 + * 4.0 * (DVX*TAX + DVY*TAY + DVZ*TAZ) * t^3 + * 4.0 * (DVX^2 + DVY^2 + DVZ^2 + DPX*TAX + DPY*TAY + DPZ*TAZ) * t^2 + * 8.0 * (DPX*DVX + DPY*DVY + DPZ*DVZ) * t^1 + * 4.0 * (DPX^2 + DPY^2 + DPZ^2) * t^0 + * + * Solve the quartic! + * Then finally insert the smallest root (time) into the (pax, pay, paz) formulas to get the wanted acceleration. + */ +bool UTobiiInteractionsBlueprintLibrary::FindNeededAccelerationForAccelerationBasedHomingProjectile(const FTobiiAccelerationBasedHomingData& InputData, FTobiiAccelerationBasedHomingResult& BestResult) +{ + const FVector DeltaPosition = InputData.TargetPosition - InputData.ProjectilePosition; + if (DeltaPosition.SizeSquared() < FLT_EPSILON) + { + return false; + } + + const FVector DeltaVelocity = InputData.TargetVelocity - InputData.ProjectileVelocity; + + const double T4Coefficient = FVector::DotProduct(InputData.TargetAcceleration, InputData.TargetAcceleration) - InputData.ProjectileAccelerationMagnitude * InputData.ProjectileAccelerationMagnitude; + const double T3Coefficient = 4.0 * FVector::DotProduct(DeltaVelocity, InputData.TargetAcceleration); + const double T2Coefficient = 4.0 * FVector::DotProduct(DeltaVelocity, DeltaVelocity) + FVector::DotProduct(DeltaPosition, InputData.TargetAcceleration); + const double T1Coefficient = 8.0 * FVector::DotProduct(DeltaPosition, DeltaVelocity); + const double T0Coefficient = 4.0 * FVector::DotProduct(DeltaPosition, DeltaPosition); + + TArray Solutions; + double DirectHitCoefficients[5]{ T0Coefficient, T1Coefficient, T2Coefficient, T3Coefficient, T4Coefficient }; + double DirectHitSolutions[4]{ 0.0, 0.0, 0.0, 0.0 }; + + int32 NrDirectHitSolutions = SolveQuartic(DirectHitCoefficients, DirectHitSolutions); + for (int32 SolutionIdx = 0; SolutionIdx < NrDirectHitSolutions; SolutionIdx++) + { + const double Solution = DirectHitSolutions[SolutionIdx]; + if (FMath::IsFinite(Solution) && !FMath::IsNaN(Solution) && Solution > DBL_EPSILON) + { + Solutions.Add(Solution); + } + } + + if (Solutions.Num() == 0 && InputData.bAttemptClosestApproachSolution) + { + //Since we couldn't find a direct hit, attempt to find a closest approach instead as backup. + double ClosestApproachCoefficients[4]{ T1Coefficient, 2.0 * T2Coefficient, 3.0 * T3Coefficient, 4.0 * T4Coefficient }; + double ClosestApproachSolutions[3]{ 0.0, 0.0, 0.0 }; + + TMap ClosestApproachDistancesToInterceptTimes; + int32 NrClosestApproachSolutions = SolveCubic(ClosestApproachCoefficients, ClosestApproachSolutions); + for (int32 SolutionIdx = 0; SolutionIdx < NrClosestApproachSolutions; SolutionIdx++) + { + const double Solution = ClosestApproachSolutions[SolutionIdx]; + + if (FMath::IsFinite(Solution) && !FMath::IsNaN(Solution) && Solution > DBL_EPSILON + && Solution >= 0.0 && !ClosestApproachDistancesToInterceptTimes.Contains(Solution)) + { + const double Real = Solution; + const double RealSq = Real * Real; + const double RealCub = RealSq * Real; + const double RealQuart = RealCub * Real; + const double Distance = FMath::Abs(T4Coefficient * RealQuart + T3Coefficient * RealCub + T2Coefficient * RealSq + T1Coefficient * Real + T0Coefficient); + ClosestApproachDistancesToInterceptTimes.Add(Distance, Real); + } + } + + ClosestApproachDistancesToInterceptTimes.KeySort(TLess()); + for (auto& Pair : ClosestApproachDistancesToInterceptTimes) + { + Solutions.Add(Pair.Value); + break; + } + } + + if (Solutions.Num() == 0) + { + return false; + } + + Solutions.Sort(TLess()); + const double InterceptTime = Solutions[0]; + const double InterceptTimeSquare = InterceptTime * InterceptTime; + + BestResult.Type = ETobiiInterceptType::DirectHit; + BestResult.ExpectedInterceptTimeSecs = InterceptTime; + BestResult.ExpectedInterceptLocation = InputData.TargetPosition + InputData.TargetVelocity * InterceptTime + (1.0 / 2.0) * InputData.TargetAcceleration * InterceptTimeSquare; + BestResult.SuggestedAcceleration = 2.0 * (DeltaPosition + DeltaVelocity * InterceptTime + (1.0 / 2.0) * InputData.TargetAcceleration * InterceptTimeSquare) / InterceptTimeSquare; + + return true; +} + +/** + * Try to find the appropriate velocity to hit a moving target given a ballistic projectile. + * We do this by setting up an equation system with 4 equations in 4 unknowns. + * First we solve for time, and then we plug that into the other equations to find the wanted acceleration. + * + * VARIABLES: + * Time: t <--- Need to first solve for this + * Projectile Pos: PPX, PPY, PPZ + * Projectile Vel: pvx, pvy, pvz + * Projectile Gravity: PAX, PAY, PAZ <--- Most likely gravity + * Projectile Apex: PAPEX <--- This is at when (1/2)*t + * Target Pos: TPX, TPY, TPZ + * Target Vel: TVX, TVY, TVZ + * Target Acc: TAX, TAY, TAZ + * + * END EQUATIONS: + * PPX + pvx * t + (1/2) * PAX * t^2 = TPX + TVX * t + (1/2) * TAX * t^2 + * PPY + pvy * t + (1/2) * PAY * t^2 = TPY + TVY * t + (1/2) * TAY * t^2 + * PPZ + pvz * t + (1/2) * PAZ * t^2 = TPZ + TVZ * t + (1/2) * TAZ * t^2 + * + * KNOWN Z EQUATIONS: + * PAPEX = PPZ + (1/2) * pvz * t + (1/8) * PAZ * t^2 + * + * SOLVE FOR VELOCITY: + * pvx = (TPX + TVX * t + (1/2) * TAX * t^2 - PPX - (1/2) * PAX * t^2) / t + * pvy = (TPY + TVY * t + (1/2) * TAY * t^2 - PPY - (1/2) * PAY * t^2) / t + * pvz = (TPZ + TVZ * t + (1/2) * TAZ * t^2 - PPZ - (1/2) * PAZ * t^2) / t + * + * SIMPLIFY: + * DPX = TPX - PPX + * DPY = TPY - PPY + * DPZ = TPZ - PPZ + * DAX = (1/2) * (TAX - PAX) + * DAY = (1/2) * (TAY - PAY) + * DAZ = (1/2) * (TAZ - PAZ) + * pvx = (DPX + TVX * t + DAX * t^2) / t + * pvy = (DPY + TVY * t + DAY * t^2) / t + * pvz = (DPZ + TVZ * t + DAZ * t^2) / t + * + * SUBSTITUTE INTO OUR MIDPOINT EQUATIONS: + * PAPEX = PPZ + (1/2) * [(DPZ + TVZ * t + DAZ * t^2) / t] * t + (1/8) * PAZ * t^2 + * + * MAKE 0 EQUATION AND FORM TERMS: + * 0 = PPZ + (1/2) * DPZ - PAPEX + (1/2) * TVZ * t + (1/2) * DAZ * t^2 + (1/8) * PAZ * t^2 + * + * ARRANGE IN COEFFICIENT FORM: + * 0 = ((1/2) * DAZ + (1/8) * PAZ) * t^2 + * (1/2) * TVZ * t^1 + * PPZ + (1/2) * DPZ - PAPEX * t^0 + * + * Solve the square for time! + * Then finally insert the roots (time) into the (pvx, pvy, pvz) formulas to get the possible velocities. + */ +bool UTobiiInteractionsBlueprintLibrary::FindNeededInitialVelocityForBallisticProjectile(const FTobiiBallisticData& InputData, TArray& Results) +{ + const FVector DeltaPosition = InputData.TargetPosition - InputData.ProjectileInitialPosition; + if (DeltaPosition.SizeSquared() < FLT_EPSILON) + { + return false; + } + + const float ApexZ = FMath::Max(InputData.ProjectileInitialPosition.Z, InputData.TargetPosition.Z) + InputData.ProjectileApexOffsetCm; + const FVector DeltaAcceleration = 0.5f * (InputData.TargetAcceleration - InputData.ProjectileAcceleration); + + const double T2Coefficient = 0.5 * DeltaAcceleration.Z + 0.125 * InputData.ProjectileAcceleration.Z; + const double T1Coefficient = 0.5 * InputData.TargetVelocity.Z; + const double T0Coefficient = InputData.ProjectileInitialPosition.Z + 0.5 * DeltaPosition.Z - ApexZ; + double TimeCoefficients[3] { T0Coefficient, T1Coefficient, T2Coefficient }; + double TimeSolutions[2] { 0.0, 0.0 }; + + int32 NrTimeSolutions = SolveQuadric(TimeCoefficients, TimeSolutions); + for (int32 SolutionIdx = 0; SolutionIdx < NrTimeSolutions; SolutionIdx++) + { + const double Time = TimeSolutions[SolutionIdx]; + if (FMath::IsFinite(Time) && !FMath::IsNaN(Time) && Time > DBL_EPSILON) + { + const double HTime = Time / 2.0; + const double HTimeSq = HTime * HTime; + const double TimeSq = Time * Time; + + FTobiiBallisticResult NewResult; + NewResult.ExpectedInterceptTimeSecs = Time; + NewResult.SuggestedInitialVelocity = (DeltaPosition + InputData.TargetVelocity * Time + DeltaAcceleration * TimeSq) / Time; + NewResult.ExpectedInterceptLocation = InputData.ProjectileInitialPosition + NewResult.SuggestedInitialVelocity * Time + 0.5f * InputData.ProjectileAcceleration * TimeSq; + Results.Add(NewResult); + } + } + + return Results.Num() > 0; +} + +bool UTobiiInteractionsBlueprintLibrary::TraceBallisticProjectilePath(UObject* WorldContextObject, const FTobiiProjectileTraceData& InputData, TArray& OutTracedPath, FHitResult& OutHitResult) +{ + if (WorldContextObject == nullptr) + { + return false; + } + + float CurrentTime = InputData.StepSizeSecs; + FVector StartPoint = InputData.ProjectileInitialPosition; + FVector EndPoint = StartPoint + InputData.ProjectileVelocity * CurrentTime + 0.5f * InputData.ProjectileAcceleration * CurrentTime * CurrentTime; + + FCollisionQueryParams CollisionParams; + CollisionParams.AddIgnoredActors(InputData.IgnoredActors); + + OutTracedPath.Empty(); + OutTracedPath.Add(StartPoint); + + for (int32 StepCount = 0; StepCount < InputData.MaxNrSteps; StepCount++) + { + if (WorldContextObject->GetWorld()->SweepSingleByChannel(OutHitResult, StartPoint, EndPoint, FQuat::Identity, InputData.TraceChannel, FCollisionShape::MakeSphere(InputData.TraceRadiusCm), CollisionParams)) + { + OutTracedPath.Add(OutHitResult.Location); + return true; + } + else + { + OutTracedPath.Add(EndPoint); + } + + StartPoint = EndPoint; + CurrentTime += InputData.StepSizeSecs; + EndPoint = InputData.ProjectileInitialPosition + InputData.ProjectileVelocity * CurrentTime + 0.5f * InputData.ProjectileAcceleration * CurrentTime * CurrentTime; + } + + return false; +} diff --git a/Source/TobiiInteractions/Private/TobiiInteractionsInternalTypes.h b/Source/TobiiInteractions/Private/TobiiInteractionsInternalTypes.h new file mode 100644 index 0000000..b312314 --- /dev/null +++ b/Source/TobiiInteractions/Private/TobiiInteractionsInternalTypes.h @@ -0,0 +1,11 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "CoreMinimal.h" + +DEFINE_LOG_CATEGORY_STATIC(LogTobiiInteraction, All, All); diff --git a/Source/TobiiInteractions/Private/TobiiInteractionsModule.cpp b/Source/TobiiInteractions/Private/TobiiInteractionsModule.cpp new file mode 100644 index 0000000..52d915a --- /dev/null +++ b/Source/TobiiInteractions/Private/TobiiInteractionsModule.cpp @@ -0,0 +1,32 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#include "TobiiInteractionsModule.h" +#include "TobiiInteractionsStyle.h" + +#include "GameFramework/HUD.h" + +IMPLEMENT_MODULE(FTobiiInteractionsModule, TobiiInteractions); + +void FTobiiInteractionsModule::StartupModule() +{ +#if WITH_EDITOR + if (GIsEditor) + { + FTobiiInteractionsStyle::Initialize(); + } +#endif +} + +void FTobiiInteractionsModule::ShutdownModule() +{ +#if WITH_EDITOR + if (GIsEditor) + { + FTobiiInteractionsStyle::Shutdown(); + } +#endif +} diff --git a/Source/TobiiInteractions/Private/TobiiInteractionsModule.h b/Source/TobiiInteractions/Private/TobiiInteractionsModule.h new file mode 100644 index 0000000..0e9449d --- /dev/null +++ b/Source/TobiiInteractions/Private/TobiiInteractionsModule.h @@ -0,0 +1,19 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "CoreMinimal.h" + +#include "Modules/ModuleInterface.h" +#include "Modules/ModuleManager.h" + +class FTobiiInteractionsModule : public IModuleInterface +{ +private: + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/Source/TobiiInteractions/Private/TobiiInteractionsStyle.cpp b/Source/TobiiInteractions/Private/TobiiInteractionsStyle.cpp new file mode 100644 index 0000000..80cf03d --- /dev/null +++ b/Source/TobiiInteractions/Private/TobiiInteractionsStyle.cpp @@ -0,0 +1,77 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#include "TobiiInteractionsStyle.h" + +#include "Framework/Application/SlateApplication.h" +#include "Slate/SlateGameResources.h" +#include "Styling/SlateStyleRegistry.h" + +TSharedPtr FTobiiInteractionsStyle::TobiiEyetrackingInteractionsStyleInstance = nullptr; + +void FTobiiInteractionsStyle::Initialize() +{ + if (!TobiiEyetrackingInteractionsStyleInstance.IsValid()) + { + TobiiEyetrackingInteractionsStyleInstance = Create(); + FSlateStyleRegistry::RegisterSlateStyle(*TobiiEyetrackingInteractionsStyleInstance); + } +} + +void FTobiiInteractionsStyle::Shutdown() +{ + FSlateStyleRegistry::UnRegisterSlateStyle(*TobiiEyetrackingInteractionsStyleInstance); + ensure(TobiiEyetrackingInteractionsStyleInstance.IsUnique()); + TobiiEyetrackingInteractionsStyleInstance.Reset(); +} + +FName FTobiiInteractionsStyle::GetStyleSetName() +{ + static FName StyleSetName(TEXT("TobiiStyle")); + return StyleSetName; +} + +#define IMAGE_BRUSH( RelativePath, ... ) FSlateImageBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) +#define BOX_BRUSH( RelativePath, ... ) FSlateBoxBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) +#define BORDER_BRUSH( RelativePath, ... ) FSlateBorderBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) +#define TTF_FONT( RelativePath, ... ) FSlateFontInfo( Style->RootToContentDir( RelativePath, TEXT(".ttf") ), __VA_ARGS__ ) +#define OTF_FONT( RelativePath, ... ) FSlateFontInfo( Style->RootToContentDir( RelativePath, TEXT(".otf") ), __VA_ARGS__ ) + +const FVector2D Icon16x16(16.0f, 16.0f); +const FVector2D Icon20x20(20.0f, 20.0f); +const FVector2D Icon40x40(40.0f, 40.0f); + +TSharedRef FTobiiInteractionsStyle::Create() +{ + TSharedRef Style = MakeShareable(new FSlateStyleSet("TobiiStyle")); + +#if TOBII_COMPILE_AS_ENGINE_PLUGIN + Style->SetContentRoot(FPaths::EnginePluginsDir() / TEXT("Runtime/TobiiEyetracking/Resources")); +#else + Style->SetContentRoot(FPaths::ProjectPluginsDir() / TEXT("TobiiEyetracking/Resources")); +#endif + + //The names here after ClassIcon. must match the type names of the types we want to offer the icon for. + Style->Set("ClassIcon.TobiiCleanUIContainer", new IMAGE_BRUSH(TEXT("CleanUI16x"), Icon16x16)); + + return Style; +} + +#undef IMAGE_BRUSH +#undef BOX_BRUSH +#undef BORDER_BRUSH +#undef TTF_FONT +#undef OTF_FONT + +void FTobiiInteractionsStyle::ReloadTextures() +{ + FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); +} + +const ISlateStyle& FTobiiInteractionsStyle::Get() +{ + return *TobiiEyetrackingInteractionsStyleInstance; +} diff --git a/Source/TobiiInteractions/Private/TobiiInteractionsStyle.h b/Source/TobiiInteractions/Private/TobiiInteractionsStyle.h new file mode 100644 index 0000000..2968ac1 --- /dev/null +++ b/Source/TobiiInteractions/Private/TobiiInteractionsStyle.h @@ -0,0 +1,24 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "CoreMinimal.h" +#include "Styling/ISlateStyle.h" + +class TOBIIINTERACTIONS_API FTobiiInteractionsStyle +{ +public: + static void Initialize(); + static void Shutdown(); + static void ReloadTextures(); + static const ISlateStyle& Get(); + static FName GetStyleSetName(); + +private: + static TSharedRef Create(); + static TSharedPtr TobiiEyetrackingInteractionsStyleInstance; +}; \ No newline at end of file diff --git a/Source/TobiiInteractions/Private/TobiiRootFinders.h b/Source/TobiiInteractions/Private/TobiiRootFinders.h new file mode 100644 index 0000000..d84ae28 --- /dev/null +++ b/Source/TobiiInteractions/Private/TobiiRootFinders.h @@ -0,0 +1,279 @@ +/* + * Utility functions to find cubic and quartic roots, + * coefficients are passed like this: + * + * c[0] + c[1]*x + c[2]*x^2 + c[3]*x^3 + c[4]*x^4 = 0 + * + * The functions return the number of non-complex roots and + * put the values into the s array. + * + * Author: Jochen Schwarze (schwarze@isa.de) + * + * Jan 26, 1990 Version for Graphics Gems + * Oct 11, 1990 Fixed sign problem for negative q's in SolveQuartic + * (reported by Mark Podlipec), + * Old-style function definitions, + * IsZero() as a macro + * Nov 23, 1990 Some systems do not declare acos() and cbrt() in + * , though the functions exist in the library. + * If large coefficients are used, EQN_EPS should be + * reduced considerably (e.g. to 1E-30), results will be + * correct but multiple roots might be reported more + * than once. + * + * Graphics gems resources: + * http://www.realtimerendering.com/resources/GraphicsGems/ + * + * License: + * All graphics gems code can be used without restrictions. [See above link] + */ + +#pragma once + +#include +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +/* epsilon surrounding for near zero values */ + +#define EQN_EPS 1E-30 +#define IsZero(x) ((x) > -EQN_EPS && (x) < EQN_EPS) +#define IsOne(x) ((x) > (1.0-EQN_EPS) && (x) < (1.0+EQN_EPS)) + +#ifdef NOCBRT +#define cbrt(x) ((x) > 0.0 ? pow((double)(x), 1.0/3.0) : \ + ((x) < 0.0 ? -pow((double)-(x), 1.0/3.0) : 0.0)) +#endif + +int SolveQuadric(double c[3], double s[2]) +{ + double p, q, D; + + if (IsZero(c[2])) + { + if (IsZero(c[1])) + { + return 0; + } + + s[0] = -c[0] / c[1]; + return 1; + } + + /* normal form: x^2 + px + q = 0 */ + + p = c[ 1 ] / (2 * c[ 2 ]); + q = c[ 0 ] / c[ 2 ]; + + D = p * p - q; + + if (IsZero(D)) + { + s[0] = -p; + return 1; + } + else if (D < 0) + { + return 0; + } + else /* if (D > 0) */ + { + double sqrt_D = sqrt(D); + + s[0] = sqrt_D - p; + s[1] = -sqrt_D - p; + return 2; + } +} + + +int SolveCubic(double c[4], double s[3]) +{ + int i, num; + double sub; + double A, B, C; + double sq_A, p, q; + double cb_p, D; + + if (IsZero(c[3])) + { + SolveQuadric(c, s); + } + + /* normal form: x^3 + Ax^2 + Bx + C = 0 */ + + A = c[2]; + B = c[1]; + C = c[0]; + + if (!IsOne(c[3])) + { + A /= c[3]; + B /= c[3]; + C /= c[3]; + } + + /* substitute x = y - A/3 to eliminate quadric term: + x^3 +px + q = 0 */ + + sq_A = A * A; + p = 1.0/3 * (- 1.0/3 * sq_A + B); + q = 1.0/2 * (2.0/27 * A * sq_A - 1.0/3 * A * B + C); + + /* use Cardano's formula */ + + cb_p = p * p * p; + D = q * q + cb_p; + + if (IsZero(D)) + { + if (IsZero(q)) /* one triple solution */ + { + s[ 0 ] = 0; + num = 1; + } + else /* one single and one double solution */ + { + double u = cbrt(-q); + s[ 0 ] = 2 * u; + s[ 1 ] = - u; + num = 2; + } + } + else if (D < 0) /* Casus irreducibilis: three real solutions */ + { + double phi = 1.0 / 3 * acos(-q / sqrt(-cb_p)); + double t = 2 * sqrt(-p); + + s[0] = t * cos(phi); + s[1] = -t * cos(phi + M_PI / 3); + s[2] = -t * cos(phi - M_PI / 3); + num = 3; + } + else /* one real solution */ + { + double sqrt_D = sqrt(D); + double u = cbrt(sqrt_D - q); + double v = - cbrt(sqrt_D + q); + + s[ 0 ] = u + v; + num = 1; + } + + /* resubstitute */ + + sub = 1.0/3 * A; + + for (i = 0; i < num; ++i) + s[ i ] -= sub; + + return num; +} + + +int SolveQuartic(double c[5], double s[4]) +{ + double coeffs[ 4 ]; + double z, u, v, sub; + double A, B, C, D; + double sq_A, p, q, r; + int i, num; + + if (IsZero(c[4])) + { + SolveCubic(c, s); + } + + /* normal form: x^4 + Ax^3 + Bx^2 + Cx + D = 0 */ + + A = c[3]; + B = c[2]; + C = c[1]; + D = c[0]; + + if (!IsOne(c[4])) + { + A /= c[4]; + B /= c[4]; + C /= c[4]; + D /= c[4]; + } + + /* substitute x = y - A/4 to eliminate cubic term: + x^4 + px^2 + qx + r = 0 */ + + sq_A = A * A; + p = - 3.0/8 * sq_A + B; + q = 1.0/8 * sq_A * A - 1.0/2 * A * B + C; + r = - 3.0/256*sq_A*sq_A + 1.0/16*sq_A*B - 1.0/4*A*C + D; + + if (IsZero(r)) + { + /* no absolute term: y(y^3 + py + q) = 0 */ + + coeffs[0] = q; + coeffs[1] = p; + coeffs[2] = 0; + coeffs[3] = 1; + + num = SolveCubic(coeffs, s); + + s[num++] = 0; + } + else + { + /* solve the resolvent cubic ... */ + + coeffs[ 0 ] = 1.0/2 * r * p - 1.0/8 * q * q; + coeffs[ 1 ] = - r; + coeffs[ 2 ] = - 1.0/2 * p; + coeffs[ 3 ] = 1; + + (void) SolveCubic(coeffs, s); + + /* ... and take the one real solution ... */ + + z = s[ 0 ]; + + /* ... to build two quadric equations */ + + u = z * z - r; + v = 2 * z - p; + + if (IsZero(u)) + u = 0; + else if (u > 0) + u = sqrt(u); + else + return 0; + + if (IsZero(v)) + v = 0; + else if (v > 0) + v = sqrt(v); + else + return 0; + + coeffs[ 0 ] = z - u; + coeffs[ 1 ] = q < 0 ? -v : v; + coeffs[ 2 ] = 1; + + num = SolveQuadric(coeffs, s); + + coeffs[ 0 ]= z + u; + coeffs[ 1 ] = q < 0 ? v : -v; + coeffs[ 2 ] = 1; + + num += SolveQuadric(coeffs, s + num); + } + + /* resubstitute */ + + sub = 1.0/4 * A; + + for (i = 0; i < num; ++i) + s[ i ] -= sub; + + return num; +} diff --git a/Source/TobiiInteractions/Public/Common/TobiiProjectileComponent.h b/Source/TobiiInteractions/Public/Common/TobiiProjectileComponent.h new file mode 100644 index 0000000..9b28aa0 --- /dev/null +++ b/Source/TobiiInteractions/Public/Common/TobiiProjectileComponent.h @@ -0,0 +1,120 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "GameFramework/ProjectileMovementComponent.h" + +#include "TobiiProjectileComponent.generated.h" + +UENUM() +enum class ETobiiProjectileHomingBehavior +{ + //This is the default behavior. The projectile will accelerate towards the target. This also means the projectile will speed up. If this is not what you want, consider using another behavior. + Acceleration, + + //In this mode, imagine the projectile has side thrusters to enable it to turn gradually towards the target. This behavior preserves projectile speed. + Steering +}; + +UENUM() +enum class ETobiiProjectileGuidanceSystem +{ + //This is the default behavior. The projectile will home towards the target's last known location. + Simple, + + //This mode will predict possible impact times and locations to avoid orbiting. Considers position, velocity and acceleration. Is quite expensive. + Prediction, + + //In addition to normal prediction, this guidance system mode will also calculate the closest approach if no direct hit is possible. More expensive than standard prediction + ComplexPrediction +}; + +UCLASS(BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent), ClassGroup=(Tobii)) +class UTobiiProjectileComponent : public UProjectileMovementComponent +{ + GENERATED_BODY() + +public: + //Use this if you need to set starting values from the outside, don't use Velocity. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Projectile") + FVector InitialVelocity; + + //If this is true, gravity acceleration will be added to the projectile. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Projectile") + bool bAffectedByGravity; + + //If bAffectedByGravity is true, this is the gravity acceleration vector that will be applied to the projectile. If this is zero, the default GetGravityZ will be used. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Projectile") + FVector WorldSpaceGravity; + + //If fuel runs out, homing behavior stops for the projectile. 0 means infinite fuel. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Projectile") + float InitialFuelSecs; + + //The projectile is destroyed if it manages to stay alive for this amount of time. 0 means infinite lifetime. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Projectile") + float MaxLifeTimeSecs; + +public: + //Allows you to customize the homing behavior if you have homing projectiles on. The homing target will always be set to the user's gaze target. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Homing") + ETobiiProjectileHomingBehavior HomingBehavior; + + //This is the fastest the projectile is able to turn when using the steering homing behavior. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Homing") + float SteeringMaxTurnSpeedDegPerSec; + +public: + //Allows you to customize the targeting part of the homing behavior. The different options generally provide different trade offs between performance and accuracy. Good accuracy also helps to avoid orbiting projectiles. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Guidance System") + ETobiiProjectileGuidanceSystem GuidanceSystem; + + //This is how often the guidance system will update it's target location. A higher frequency will be more expensive but more accurate and vice versa. The guidance system will never update more than once per frame. 0 means every frame. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Guidance System") + float GuidanceSystemUpdateFreq; + + //Sometimes you might not want the homing behavior to track targets that are too far off course. This is the maximium angle allowed between the projectile forward vector and the vector towards the target. If this threshold is breached, the homing will stop. 0 means there is no threshold. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Guidance System") + float GuidanceSystemMaximumTargetAngleDeg; + +public: + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Guidance System") + FVector GuidanceSystemTarget; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Guidance System") + FVector AccelerationVectorTowardsTarget; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Guidance System") + bool bTargetOffCourse; + +public: + //This function will attempt to set up the actor to start homing. This entails disabling physics simulation etc. Please note that if the actor is intersecting the ground or other geometry, the simulation might stop immediately since it will "hit" it. To avoid this, set the appropriate collision masks or turn off collision during launch. + UFUNCTION(BlueprintCallable, Category = "Homing") + void StartHoming(USceneComponent* NewHomingTargetComponent); + + UFUNCTION(BlueprintCallable, Category = "Homing") + void StopHoming(); + +public: + UTobiiProjectileComponent(); + +public: + virtual void BeginPlay() override; + virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; + + virtual FVector ComputeAcceleration(const FVector& InVelocity, float DeltaTime) const override; + virtual void SimulateGuidanceSystem(); + +private: + bool bCanThrust; + bool bUsesFuel; + float CurrentFuelSecs; + float CurrentLifeTime; + float GuidanceSystemUpdateTimerSecsLeft; + FVector LastTargetVelocity; + FVector TargetAcceleration; +}; diff --git a/Source/TobiiInteractions/Public/Common/TobiiThrowAtGazeComponent.h b/Source/TobiiInteractions/Public/Common/TobiiThrowAtGazeComponent.h new file mode 100644 index 0000000..f807a49 --- /dev/null +++ b/Source/TobiiInteractions/Public/Common/TobiiThrowAtGazeComponent.h @@ -0,0 +1,169 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "TobiiInteractionsTypes.h" + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "Components/PrimitiveComponent.h" + +#include "TobiiThrowAtGazeComponent.generated.h" + +/** + * This component will generate gaze contingent throw vectors on demand. + * You can use this vector to either simulate a trajectory yourself, or simply apply it to the rigid body of some object. + */ +UCLASS(BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent), ClassGroup = (Tobii)) +class TOBIIINTERACTIONS_API UTobiiThrowAtGazeComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + UTobiiThrowAtGazeComponent(); + +public: + // This is the component that you're trying to hit with the throw. You can set this on the same frame you are trying to throw, but setting it earlier will allow the component to "aim" a bit better. If this is not set, you can still use the component, but it will then try to simply use the gaze vector instead. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze") + TWeakObjectPtr ThrowTarget; + + // Throw force applied to the throw vector can never be greater than this value. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze") + float MaxThrowSpeedCmPerSecs; + + // This is how high above the thrower the arc apex will be. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze") + float ThrowApexOffsetCm; + + // This is how many times we will run the angle finding code. More iterations will mean more tested apexes between the limits below. Setting this to 0 will only run one test using the average of the apexes. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration") + int32 MaxIterations; + + // If this is true, the algorithm will always test low apexes first. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration") + bool bAlwaysPreferLowApex; + + // If this is true, the algorithm will prefer low apexes rather than mid or high ones for a more natural looking throw. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration") + bool bPreferLowApexForTargetsBelowThrower; + + // If this is true, the algorithm will attempt to trace the solution in the world to make sure nothing is in the way. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration") + bool bShouldTraceResult; + + // If bShouldTraceResult is true, an apex smaller than this value will never be tested + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration") + float ThrowApexOffsetMinimumCm; + + // If bShouldTraceResult is true, an apex larger than this value will never be tested + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration") + float ThrowApexOffsetMaximumCm; + + // When testing different apexes, if the delta between the last test and the next one is smaller than this value, tests will be aborted + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration") + float MinimumApexDeltaCm; + + // If bShouldTraceResult is true, this is the size that will be used for tracing. Should be larger or equal to the projectile size. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration") + float TraceRadiusCm; + + // If bShouldTraceResult is true, this is the length of each trace step done. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration") + int32 MaxNrTraceSteps; + + // If bShouldTraceResult is true, this is the length of each trace step done in seconds. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration") + float TraceStepSizeSecs; + + // We don't allow traces longer than this + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration") + float MaxTraceDistance; + + // If we don't have a ThrowTarget, we will consider hits closer than this distance to the target to be "hits" + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration") + float NoTargetAcceptanceThreshold; + + // This is the channel that will be used when tracing. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration") + TEnumAsByte TraceChannel; + + // If you have any additional actors that should be ignored by the tracing + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration") + TArray TraceIgnoreActors; + + // Let's you ignore this component's owner collision when tracing. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Configuration") + bool bShouldTraceIgnoreOwner; + + // If this is false, the world gravity is used for calculations. If this is true, CustomProjectileGravity is used instead. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Gravity") + bool bUseCustomGravity; + + // If bUseCustomGravity is true, this is the gravity that will be used in calculations + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Throw@gaze Gravity") + FVector CustomProjectileGravity; + +public: + UFUNCTION(BlueprintPure, Category = "Fire@gaze") + FVector CalculateProjectileGravityVector(); + + UFUNCTION(BlueprintPure, Category = "Fire@gaze") + bool ThrowAtGazeAvailable(); + + UFUNCTION(BlueprintPure, Category = "Fire@gaze") + bool TraceBallisticProjectilePath(FVector ProjectileOrigin, FVector ProjectileInitialVelocity, TArray& OutTracedPath); + + /** + * This function is useful when throwing things in XR and you have a throw vector from the player's attempt to throw something. + * Don't forget to set collision on the thing you're throwing etc before using this though, otherwise tracing might not work. + * It will only correct trajectories where we can expect a direct hit, or where we are simply out of range. + * + * @param ThrowOrigin This is where the projectile will start. + * @param OriginalThrowVector This is the throw vector supplied by the developer to correct. + * @param ThrowAngleThresholdDeg If the angle difference between the OriginalThrowVector and the suggested vector is greater than this parameter, the vector will not be corrected. This is not applied if it is 0. + * @param ThrowSpeedThreshold If the OriginalThrowVector speed is lower than the suggested vector's speed by at least this amount, the vector will not be corrected. This is measured in centimeters per second. This is not applied if it is 0. + * @param bUseSimple If this is true, CalculateThrowAtGazeVectorSimple is used in favor of the Complex version internally + * @return The potentially corrected throw vector + */ + UFUNCTION(BlueprintCallable, Category = "Throw@gaze") + FVector CorrectThrow(const FVector& ThrowOrigin, const FVector& OriginalThrowVector, float ThrowAngleThresholdDeg, float ThrowSpeedThresholdCmPerSec); + + /** + * Call this function to request an impulse vector to use for gaze throwing. + * This function will start by testing the standard apex and force, and if that works it will just return it. + * If no solution is found, the algorithm will test other apex heights until either MaxNrIterations is reached or a result is found. + * + * @param ThrowOrigin This is where the projectile will start. + * @param OutBallisticResult This are our results. + * @param OutTracedPath If path tracing is on, this will contain the traced path. + * @return The result of the calculation. Please note that in the case of OutOfRange, the best solution will still be returned in OutGazeThrowVector, adjusted to your max throw speed + */ + UFUNCTION(BlueprintCallable, Category = "Throw@gaze") + ETobiiThrowAtGazeResult CalculateThrowAtGazeVector(const FVector& ThrowOrigin, FTobiiBallisticResult& OutBallisticResult, TArray& OutTracedPath); + + /** + * This is a companion function to CalculateThrowAtGazeVectorComplex that lets you calculate a throw vector towards a world location. + * You can specify target velocity and acceleration if you would like to. + * + * @param ThrowOrigin This is where the projectile will start. + * @param TargetLocation This is where the target is. + * @param TargetVelocity This is the target's velocity. Can be zero. + * @param TargetAcceleration This is the target's acceleration. Can be zero. + * @param OutBallisticResult This are our results. + * @param OutTracedPath If path tracing is on, this will contain the traced path. + * @return The result of the calculation. Please note that in the case of OutOfRange, the best solution will still be returned in OutGazeThrowVector, adjusted to your max throw speed + */ + UFUNCTION(BlueprintCallable, Category = "Throw@gaze") + ETobiiThrowAtGazeResult CalculateThrowArc(const FVector& ThrowOrigin, const FVector& TargetLocation, const FVector& TargetVelocity, const FVector& TargetAcceleration, FTobiiBallisticResult& OutBallisticResult, TArray& OutTracedPath); + +public: + void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override; + +private: + FVector LastTargetVelocity; + FVector CalculatedTargetAcceleration; +}; diff --git a/Source/TobiiInteractions/Public/Desktop/TobiiAimAtGazeComponent.h b/Source/TobiiInteractions/Public/Desktop/TobiiAimAtGazeComponent.h new file mode 100644 index 0000000..41a3488 --- /dev/null +++ b/Source/TobiiInteractions/Public/Desktop/TobiiAimAtGazeComponent.h @@ -0,0 +1,60 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" + +#include "TobiiAimAtGazeComponent.generated.h" + +UCLASS(BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent)) +class TOBIIINTERACTIONS_API UTobiiAimAtGazeComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + UTobiiAimAtGazeComponent(); + +public: + //This controls how the filters in FocusFilterList are used. If this is true, only focusables with a layer that is in the FocusFilterList will be considered. If this is false, the FocusFilterList will act like a black list, excluding any focusables whos layer can be found in the array. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus Management") + bool bIsWhiteList; + + //These are the filters that will determine what focus data you will receive. The behavior of the filters are controlled by bIsWhiteList. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus Management") + TArray FocusLayerFilters; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aim@gaze") + bool bAllowRetarget; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aim@gaze") + float AimSpeed; + +public: + //Use this to test if the user has turned aim@gaze on and it is available. + UFUNCTION(BlueprintPure, Category = "Aim@gaze") + bool AimAtGazeAvailable(); + + //Trigger an aim@gaze + UFUNCTION(BlueprintCallable, Category = "Aim@gaze") + void AimAtGaze(); + + //Call this every frame you want to continually track the aim@gaze target + UFUNCTION(BlueprintCallable, Category = "Aim@gaze") + void ContinuousAimAtGaze(); + + /************************************************************************/ + /* UActorComponent */ + /************************************************************************/ +public: + virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override; + +private: + TWeakObjectPtr CurrentFocusComponent; + FVector CurrentAimTarget; + bool bIsGazeAiming; +}; diff --git a/Source/TobiiInteractions/Public/Desktop/TobiiFireAtGazeComponent.h b/Source/TobiiInteractions/Public/Desktop/TobiiFireAtGazeComponent.h new file mode 100644 index 0000000..4f5af22 --- /dev/null +++ b/Source/TobiiInteractions/Public/Desktop/TobiiFireAtGazeComponent.h @@ -0,0 +1,68 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" + +#include "TobiiFireAtGazeComponent.generated.h" + +UENUM(BlueprintType) +enum class ETobiiFireAtGazeNoTargetBehavior : uint8 +{ + PointGunForward, + PointGunToGaze +}; + +UCLASS(BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent)) +class TOBIIINTERACTIONS_API UTobiiFireAtGazeComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + UTobiiFireAtGazeComponent(); + +public: + //This controls how the filters in FocusFilterList are used. If this is true, only focusables with a layer that is in the FocusFilterList will be considered. If this is false, the FocusFilterList will act like a black list, excluding any focusables whos layer can be found in the array. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus Management") + bool bIsWhiteList; + + //These are the filters that will determine what focus data you will receive. The behavior of the filters are controlled by bIsWhiteList. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Gaze Focus Management") + TArray FocusLayerFilters; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Fire@gaze") + class UCameraComponent* CameraComponent; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Fire@gaze") + TWeakObjectPtr FireAtGazeTargetActor; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Fire@gaze") + TWeakObjectPtr FireAtGazeTargetComponent; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Fire@gaze") + FVector FireAtGazeTargetLocation; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Fire@gaze Settings") + float MaxDistance; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Fire@gaze Settings") + ETobiiFireAtGazeNoTargetBehavior NoTargetBehavior; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Fire@gaze Settings") + TEnumAsByte TraceChannel; + +public: + //Use this to test if the user has turned fire@gaze on and it is available. + UFUNCTION(BlueprintPure, Category = "Fire@gaze") + bool FireAtGazeAvailable(); + + UFUNCTION(BlueprintPure, Category = "Fire@gaze") + bool WantsCrosshair(); + + /************************************************************************/ + /* UActorComponent */ + /************************************************************************/ +public: + virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override; +}; diff --git a/Source/TobiiInteractions/Public/TobiiInteractionsBlueprintLibrary.h b/Source/TobiiInteractions/Public/TobiiInteractionsBlueprintLibrary.h new file mode 100644 index 0000000..7e893bc --- /dev/null +++ b/Source/TobiiInteractions/Public/TobiiInteractionsBlueprintLibrary.h @@ -0,0 +1,103 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "TobiiInteractionsTypes.h" + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "Kismet/BlueprintFunctionLibrary.h" + +#include "TobiiInteractionsBlueprintLibrary.generated.h" + +/** + * Simplified interface for blueprint use. Only exposes the features most likely to be consumed from BP. + */ +UCLASS() +class TOBIIINTERACTIONS_API UTobiiInteractionsBlueprintLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_UCLASS_BODY() + +public: + UFUNCTION(BlueprintPure, Category = "Tobii Desktop Interaction") + static bool IsInfiniteScreenEnabled(); + + //Util functions to see if the feature can be considered enabled by the user. + UFUNCTION(BlueprintPure, Category = "Tobii Desktop Interaction") + static bool IsCleanUIEnabled(); + + UFUNCTION(BlueprintPure, Category = "Tobii Desktop Interaction") + static bool IsAimAtGazeEnabled(); + + UFUNCTION(BlueprintPure, Category = "Tobii Desktop Interaction") + static bool IsFireAtGazeEnabled(); + + UFUNCTION(BlueprintPure, Category = "Tobii Desktop Interaction") + static bool IsThrowAtGazeEnabled(); + +public: + /** + * This function will determine an appropriate amount to dampen infinite screen depending on view pitch. + */ + UFUNCTION(BlueprintCallable, Category = "Tobii Infinite Screen") + static float CalculateSmoothPitchStep(float ViewPitch); + + /** + * This function will determine an appropriate amount to dampen infinite screen depending on view pitch. + */ + UFUNCTION(BlueprintCallable, Category = "Tobii Infinite Screen") + static FRotator MakeInfiniteScreenCameraRotator(FRotator OriginalCameraRotation, FRotator InfiniteScreenAngles); + +public: + /* + * Render UI to a texture so we can use it on arbitrary meshes. + * Thanks to https://answers.unrealengine.com/questions/353532/rendering-a-umg-widget-on-a-static-mesh.html for this. + */ + UTexture2D* TextureFromWidget(UUserWidget* const Widget, const FVector2D& DrawSize); + + /** + * This function will find and return all real roots to the square equation Ax^2 + Bx + C = 0 + */ + UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils") + static void FindRealSquareRoots(float A, float B, float C, TArray& OutRealRoots); + + /** + * This function will find and return all real roots to the cubic equation Ax^3 + Bx^2 + Cx + D = 0 + */ + UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils") + static void FindRealCubicRoots(float A, float B, float C, float D, TArray& OutRealRoots); + + /** + * This function will find and return all real roots to the quartic equation Ax^4 + Bx^3 + Cx^2 + Dx + E = 0 + * WARNING: The roots of a full degree quartic are generally not cheap to compute. Use with care. + */ + UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils") + static void FindRealQuarticRoots(float A, float B, float C, float D, float E, TArray& OutRealRoots); + + /** + * This function will try to find which acceleration you will need for the current frame to hit a certain target given a homing projectile that uses acceleration for it's movement behavior. + */ + UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils") + static bool FindNeededAccelerationForAccelerationBasedHomingProjectile(const FTobiiAccelerationBasedHomingData& InputData, FTobiiAccelerationBasedHomingResult& BestResult); + + /** + * This function will try to find the initial velocity needed to hit a moving target with a ballistic projectile that passes through a selected apex point. + * To find an arc that works with your environment and thrower you might have to call this function multiple times and trace the results in your scene. + * + * @return Whether a valid velocity could be found. If this is false, it usually means that SourceZ + ProjectileApexOffset was smaller than TargetZ. Try increasing the apex offset and try again. + */ + UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils") + static bool FindNeededInitialVelocityForBallisticProjectile(const FTobiiBallisticData& InputData, TArray& Results); + + /** + * This function will trace along a ballistic path until it hits something. It will then return the traced path as well as what it hit. + * + * @return Whether anything was hit tracing the path + */ + UFUNCTION(BlueprintCallable, Category = "Tobii Math Utils", meta = (WorldContext = "WorldContextObject")) + static bool TraceBallisticProjectilePath(UObject* WorldContextObject, const FTobiiProjectileTraceData& InputData, TArray& OutTracedPath, FHitResult& OutHitResult); +}; diff --git a/Source/TobiiInteractions/Public/TobiiInteractionsTypes.h b/Source/TobiiInteractions/Public/TobiiInteractionsTypes.h new file mode 100644 index 0000000..58ebbe3 --- /dev/null +++ b/Source/TobiiInteractions/Public/TobiiInteractionsTypes.h @@ -0,0 +1,146 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +#pragma once + +#include "CoreMinimal.h" +#include "CollisionQueryParams.h" + +#include "TobiiInteractionsTypes.generated.h" + +UENUM(BlueprintType) +enum class ETobiiInterceptType : uint8 +{ + ClosestApproach, + DirectHit +}; + +UENUM(BlueprintType) +enum class ETobiiThrowAtGazeResult : uint8 +{ + // Bad input in some way + UnknownError, + + // Neither a throw target, nor gaze input is available + NoEyetrackingInput, + + // If no valid path could be found + NoPath, + + // A path is mathematically possible, but it is blocked by something and out of range. + BlockedByWorldAndOutOfRange, + + // A path is mathematically possible and is within range, but it is blocked by something. + BlockedByWorldInRange, + + // If a valid path could be found, but it requires more throw force than allowed + OutOfRange, + + // If a valid path exists and the force requirement is within limits + DirectHit +}; + +USTRUCT(BlueprintType) +struct FTobiiAccelerationBasedHomingData +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data") + bool bAttemptClosestApproachSolution; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Source Data") + FVector ProjectilePosition; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Source Data") + FVector ProjectileVelocity; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Source Data") + float ProjectileAccelerationMagnitude; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Target Data") + FVector TargetPosition; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Target Data") + FVector TargetVelocity; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Target Data") + FVector TargetAcceleration; +}; + +USTRUCT(BlueprintType) +struct FTobiiBallisticData +{ + GENERATED_BODY() + +public: + //This is how high the projectile should be above the initial position at it's midpoint + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data") + float ProjectileApexOffsetCm; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Source Data") + FVector ProjectileInitialPosition; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Source Data") + FVector ProjectileAcceleration; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Target Data") + FVector TargetPosition; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Target Data") + FVector TargetVelocity; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Target Data") + FVector TargetAcceleration; +}; + +USTRUCT(BlueprintType) +struct FTobiiAccelerationBasedHomingResult +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Intercept Data") + ETobiiInterceptType Type; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Intercept Data") + FVector SuggestedAcceleration; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Intercept Data") + FVector ExpectedInterceptLocation; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Intercept Data") + float ExpectedInterceptTimeSecs; +}; + +USTRUCT(BlueprintType) +struct FTobiiBallisticResult +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Intercept Data") + FVector SuggestedInitialVelocity; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Intercept Data") + FVector ExpectedInterceptLocation; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Intercept Data") + float ExpectedInterceptTimeSecs; +}; + +USTRUCT(BlueprintType) +struct FTobiiProjectileTraceData +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data") + TEnumAsByte TraceChannel; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data") + TArray IgnoredActors; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data") + int32 MaxNrSteps; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data") + float StepSizeSecs; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General Data") + float TraceRadiusCm; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Source Data") + FVector ProjectileInitialPosition; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Source Data") + FVector ProjectileVelocity; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Source Data") + FVector ProjectileAcceleration; +}; diff --git a/Source/TobiiInteractions/TobiiInteractions.Build.cs b/Source/TobiiInteractions/TobiiInteractions.Build.cs new file mode 100644 index 0000000..9e4af32 --- /dev/null +++ b/Source/TobiiInteractions/TobiiInteractions.Build.cs @@ -0,0 +1,62 @@ +/****************************************************************************** +* Copyright 2017- Tobii Technology AB. All rights reserved. +* +* @author Temaran | Fredrik Lindh | fredrik.lindh@tobii.com | https://github.com/Temaran +******************************************************************************/ + +using System; +using System.IO; +using Tools.DotNETCommon; + +namespace UnrealBuildTool.Rules +{ + public class TobiiInteractions : ModuleRules + { + public TobiiInteractions(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + PrivateDependencyModuleNames.AddRange(new string[] + { + "Core" + , "CoreUObject" + , "Engine" + , "Slate" + , "SlateCore" + , "UMG" + , "HeadMountedDisplay" + }); + + PublicDependencyModuleNames.AddRange(new string[] + { + "EyeTracker" + , "TobiiGTOM" + }); + + PrivateIncludePaths.AddRange(new string[] + { + "TobiiInteractions/Public" + , "TobiiInteractions/Public/Common" + , "TobiiInteractions/Public/Desktop" + }); + + string AssemblyLocation = Path.GetDirectoryName(new Uri(System.Reflection.Assembly.GetExecutingAssembly().CodeBase).LocalPath); + DirectoryReference RootDirectory = new DirectoryReference(Path.Combine(AssemblyLocation, "..", "..", "..")); + bool IsEnginePlugin = RootDirectory.GetDirectoryName() == "Engine"; + + PublicDefinitions.Add("TOBII_COMPILE_AS_ENGINE_PLUGIN=" + (IsEnginePlugin ? 1 : 0)); + + //Platform specific + if (Target.Platform == UnrealTargetPlatform.Win64 || Target.Platform == UnrealTargetPlatform.Win32) + { + string PlatformString = (Target.Platform == UnrealTargetPlatform.Win64) ? "Win64" : "Win32"; + string PluginsPath = Path.Combine(ModuleDirectory, "../../../"); + string TobiiRelativeAPIPath = "TobiiEyetracking/ThirdParty/GameIntegration"; + string TobiiRelativeIncludePath = Path.Combine(TobiiRelativeAPIPath, "include"); + + //Includes + PrivateIncludePaths.Add(Path.Combine(PluginsPath, TobiiRelativeIncludePath)); + } + } + } +} diff --git a/ThirdParty/GameIntegration/include/tobii_g2om.h b/ThirdParty/GameIntegration/include/tobii_g2om.h new file mode 100644 index 0000000..b3aa526 --- /dev/null +++ b/ThirdParty/GameIntegration/include/tobii_g2om.h @@ -0,0 +1,970 @@ +/* Copyrighted Tobii AB, 2018. Property of Tobii AB. */ + +/** +## tobii_g2om.h + +The tobii_g2om.h header file collects the core API functions of the Tobii G2OM library. It contains +functions to initialize the API and process data to get a most likely candidate result. + +The API documentation includes example code snippets that show the use of each function, they don't +necessarily describe the best practice in which to use the api. For more in-depth example of the +best practices, see the examples that are supplied together with the G2OM library. + +G2OM has no processes running in background, zero runtime allocations and enables full control +over the amount of threads used internally. +*/ + +#ifndef tobii_g2om_h_included +#define tobii_g2om_h_included + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef _WIN32 + #define G2OM_API __declspec( dllimport ) + #define G2OM_CALL __cdecl +#elif __GNUC__ >= 4 + #define G2OM_API __attribute__((visibility("default"))) + #define G2OM_CALL +#else + #define G2OM_API + #define G2OM_CALL +#endif + +typedef enum { + G2OM_RESULT_OK = 0, + G2OM_RESULT_NULL_POINTER_PASSED = -1, + G2OM_RESULT_INTERNAL_ERROR = -2, + G2OM_RESULT_THREAD_POOL_ERROR = -3, + G2OM_RESULT_INDEX_OUT_OF_BOUNDS = -4, + G2OM_RESULT_INTERNAL_CONVERSION_ERROR = -5, + G2OM_RESULT_LOG_FILE_ERROR = -6, + G2OM_RESULT_NOT_PERMITTED_BY_LICENSE = -7, + G2OM_RESULT_LICENSE_INVALID = -8, + G2OM_RESULT_NOT_IMPLEMENTED = -9, + G2OM_RESULT_CAPACITY_EXCEEDED = -10, + G2OM_RESULT_CUSTOM_ALLOCATOR_NOT_SET_FIRST = -11, +} g2om_result; + +typedef struct { + uint32_t major; + uint32_t minor; + uint32_t build_version; +} g2om_version; + +typedef struct g2om_context g2om_context; + +typedef struct { + float x; + float y; + float z; +} g2om_vector; + +typedef struct { + uint32_t capacity; + uint32_t thread_count; + uint8_t *license_ptr; + uint32_t license_length; +} g2om_context_create_options; + +typedef struct { + float data[16]; +} g2om_matrix; + +typedef struct { + uint64_t id; + g2om_vector max_local_space; + g2om_vector min_local_space; + g2om_matrix world_to_local_matrix; + g2om_matrix local_to_world_matrix; +} g2om_candidate; + +typedef struct { + g2om_vector origin; + g2om_vector direction; +} g2om_ray; + +typedef struct { + g2om_ray ray; + uint8_t is_valid; +} g2om_gaze_ray; + +typedef struct { + float timestamp_in_s; + g2om_gaze_ray gaze_ray_world_space; + g2om_vector camera_up_direction_world_space; + g2om_vector camera_right_direction_world_space; +} g2om_gaze_data; + +typedef struct { + uint8_t is_valid; + uint64_t id; +} g2om_raycast; + +typedef struct { + g2om_raycast raycast; +} g2om_raycast_result; + +typedef struct { + uint64_t id; + float score; + g2om_gaze_ray adjusted_gaze_ray_world_space; +} g2om_candidate_result; + +typedef enum { + G2OM_CORNERS_FLL = 0, // Front Lower Left + G2OM_CORNERS_FUL, // Front Upper Left + G2OM_CORNERS_FUR, // Front Upper Right + G2OM_CORNERS_FLR, // Front Lower Right + G2OM_CORNERS_BLL, // Back Lower Left + G2OM_CORNERS_BUL, // Back Upper Left + G2OM_CORNERS_BUR, // Back Upper Right + G2OM_CORNERS_BLR, // Back Lower Right + G2OM_CORNERS_COUNT +} g2om_corners; + +G2OM_API g2om_result G2OM_CALL g2om_get_version(g2om_version *version_ptr); + +G2OM_API g2om_result G2OM_CALL g2om_context_options_init(g2om_context_create_options *options); + +G2OM_API g2om_result G2OM_CALL g2om_context_create(g2om_context **context_ptr); + +G2OM_API g2om_result G2OM_CALL g2om_context_create_ex(g2om_context **context_ptr, g2om_context_create_options *options); + +G2OM_API g2om_result G2OM_CALL g2om_context_destroy(g2om_context **context_ptr); + +G2OM_API g2om_result G2OM_CALL g2om_process( + g2om_context *context_ptr, + g2om_gaze_data *gaze_data_ptr, + g2om_raycast_result *raycast_ptr, + uint32_t candidates_count, + g2om_candidate *candidates_ptr, + g2om_candidate_result *candidate_results); + +G2OM_API g2om_result G2OM_CALL g2om_get_worldspace_corner_of_candidate( + g2om_candidate *candidate_ptr, + uint32_t corners_length, + g2om_vector *corners_ptr); + +G2OM_API g2om_result G2OM_CALL g2om_get_candidate_search_pattern( + g2om_context *context_ptr, + g2om_gaze_data *gaze_data_ptr, + uint32_t number_of_rays, + g2om_gaze_ray *mutated_rays_ptr); + +#ifdef __cplusplus +} +#endif + +#endif // tobii_g2om_h_included + +/** + +*** + +## Types + +*/ + +/** + +### g2om_result + +#### Definition + +```c,ignore +typedef enum { + G2OM_RESULT_OK = 0, + G2OM_RESULT_NULL_POINTER_PASSED = -1, + G2OM_RESULT_INTERNAL_ERROR = -2, + G2OM_RESULT_THREAD_POOL_ERROR = -3, + G2OM_RESULT_INDEX_OUT_OF_BOUNDS = -4, + G2OM_RESULT_INTERNAL_CONVERSION_ERROR = -5, + G2OM_RESULT_LOG_FILE_ERROR = -6, + G2OM_RESULT_NOT_PERMITTED_BY_LICENSE = -7, + G2OM_RESULT_LICENSE_INVALID = -8, + G2OM_RESULT_NOT_IMPLEMENTED = -9, + G2OM_RESULT_CAPACITY_EXCEEDED = -10, + G2OM_RESULT_CUSTOM_ALLOCATOR_NOT_SET_FIRST = -11 +} g2om_result; + ``` + +#### Remarks + +The `g2om_result` type is returned by every `g2om_` call. It is used to signal success, or the + cause of an error. The following variants can be observed: + +- `G2OM_RESULT_OK` - The call was generally successful. +- `G2OM_RESULT_NULL_POINTER_PASSED` - A null pointer was passed at any position. +- `G2OM_RESULT_INTERNAL_ERROR` - This should never happen. If you observe this result please contact support. +- `G2OM_RESULT_THREAD_POOL_ERROR` - There was a problem creating the requested number of threads. +- `G2OM_RESULT_INDEX_OUT_OF_BOUNDS` - The library tried to access an element past its length. Make sure the passed pointers and lengths match. +- `G2OM_RESULT_INTERNAL_CONVERSION_ERROR` - This should never happen. If you observe this result please contact support. +- `G2OM_RESULT_LOG_FILE_ERROR` - When debug mode was enabled and the log file could not be written. +- `G2OM_RESULT_NOT_PERMITTED_BY_LICENSE` - When the current license does not allow the requested operation. +- `G2OM_RESULT_LICENSE_INVALID` - If an invalid or corrupted license is passed. +- `G2OM_RESULT_NOT_IMPLEMENTED` - This should never happen for public versions. If you observe this result please contact support. +- `G2OM_RESULT_CAPACITY_EXCEEDED` - If attempting to classify more objects at once than the provisioned capacity. +- `G2OM_RESULT_CUSTOM_ALLOCATOR_NOT_SET_FIRST` - If trying to set the custom allocator too late. + +*/ + +/** + +### g2om_version + +#### Definition + +```c,ignore +typedef struct { + uint32_t major; + uint32_t minor; + uint32_t build_version; +} g2om_version; +``` + +#### Remarks + +When invoking `g2om_get_version()`, this struct will be filled. + +*/ + +/** + +### g2om_context + +#### Definition + +```c,ignore +typedef struct g2om_context g2om_context; +``` + +#### Remarks + +An opaque struct for internal use. This will be created by `g2om_context_create()` + or `g2om_context_create_ex()` and must be passed via pointer to most function calls. + +*/ + +/** + +### g2om_context_create_options + +#### Definition + +```c,ignore +typedef struct { + uint32_t capacity; + uint32_t thread_count; + uint8_t *license_ptr; + uint32_t license_length; +} g2om_context_create_options; +``` + +#### Remarks + +When invoking `g2om_context_create_ex()` this specifies how G2OM should behave. The fields used + are: + +- `capacity` - How many concurrently existing objects G2OM should be able to handle. +- `thread_count` - When processing, how many parallel threads should be used. Setting this to `1` + will not spawn any threads and will always execute on the calling thread of `g2om_process()`. +- `license_ptr` - A pointer to the raw bytes of a license (if any). +- `license_length` - Length of the license array. + +*/ + +/** + +### g2om_vector + +#### Definition + +```c,ignore +typedef struct { + float x; + float y; + float z; +} g2om_vector; +``` + +#### Remarks + +Our vector definition. + +*/ + +/** + +### g2om_matrix + +#### Definition + +```c,ignore +typedef struct { + float data[16]; +} g2om_matrix; +``` + +#### Remarks + +Our matrix definition is identical to what Unity have, see Unity Matrix4x4. +Memory layout (column major): + +| | c0 | c1 | c2 | c3 | +|---|---|---|---|---| +| r0 | m00 | m10 | m20 | m30 | +| r1 | m01 | m11 | m21 | m31 | +| r2 | m02 | m12 | m22 | m32 | +| r3 | m03 | m13 | m23 | m33 | + +Data is stored in memory: +m00, m01, m02, m03 .. m33 + +*/ + +/** + +### g2om_candidate + +#### Definition + +```c,ignore +typedef struct { + uint64_t id; + g2om_vector max_local_space; + g2om_vector min_local_space; + g2om_matrix world_to_local_matrix; + g2om_matrix local_to_world_matrix; +} g2om_candidate; +``` + +#### Remarks + +One object in the current scene that might or might not be gazed upon. This is input to + `g2om_process()`. The fields used are: + +- `id` - A user-defined ID of the object to track. +- `max_local_space` - A point describing the max point of the object's extent in local space +- `min_local_space` - A point describing the min point of the object's extent in local space +- `world_to_local_matrix` - A matrix that transforms a point from world space into object's local space +- `local_to_world_matrix` - A matrix that transforms a point from object's local space into world space + +*/ + +/** + +### g2om_ray + +#### Definition + +```c,ignore +typedef struct { + g2om_vector origin; + g2om_vector direction; +} g2om_ray; +``` + +#### Remarks + +This is a ray that has an `origin` from the ray was shot, and a `direction` towards which it + was shot. + +- `origin` - The origin of the ray. +- `direction` - The direction of the ray. + +*/ + +/** + +### g2om_gaze_ray + +#### Definition + +```c,ignore +typedef struct { + g2om_ray ray; + uint8_t is_valid; +} g2om_gaze_ray; +``` + +#### Remarks + +This is gaze ray as returned by an eye tracker. In addition to the ray data, it also contains + a validity flag. The validity indicates whether the eye tracker thinks this ray was valid. + + The ray must be set in world space coordinates. + +- `ray` - The ray as returned by the eye tracker, converted to world space. +- `is_valid` - The validity flag as reported by the eye tracker. + +*/ + +/** + +### g2om_gaze_data + +#### Definition + +```c,ignore +typedef struct { + float timestamp_in_s; + g2om_gaze_ray gaze_ray_world_space; + g2om_vector camera_up_direction_world_space; + g2om_vector camera_right_direction_world_space; +} g2om_gaze_data; +``` + +#### Remarks + +Represents all the gaze data as returned from the eye tracker. The gaze data must be converted to + world space coordinates. + +- `timestamp_in_s` - Time stamp as reported by the eye tracker converted to seconds. Note that + the absolute time is irrelevant, but the relative time passed in two consecutive invocations is + important. +- `gaze_ray_world_space` - The eye data as reported by the eye tracker. +- `camera_up_direction_world_space` - The cameras upwards direction in world space coordinates. +- `camera_right_direction_world_space` - The cameras rightwards direction in world space coordinates. + +*/ + +/** + +### g2om_raycast + +#### Definition + +```c,ignore +typedef struct { + uint8_t is_valid; + uint64_t id; +} g2om_raycast; +``` + +#### Remarks + +The result of a single ray cast. + +- `is_valid` - If the ray cast data is actually valid. A `false` value means for example that + the eye tracker did not produce a gaze ray this frame, or that the gaze ray did not hit any object. +- `id` - The ID of the object that was hit by the ray cast. + +*/ + +/** + +### g2om_raycast_result + +#### Definition + +```c,ignore +typedef struct { + g2om_raycast raycast; +} g2om_raycast_result; +``` + +#### Remarks + +The result of ray cast by the current gaze data. + +- `raycast` - Result of raycasting the world gaze ray in game engine. + +*/ + +/** + +### g2om_candidate_result + +#### Definition + +```c,ignore +typedef struct { + uint64_t id; + float score; + g2om_gaze_ray adjusted_gaze_ray_world_space; +} g2om_candidate_result; +``` + +#### Remarks + +For each object returned by `g2om_process()` the following fields will be set. + +- `id` - The user defined ID of the object this struct describes. +- `score` - The score of this object. As of today this is _almost_ a probability value, in that it + ranges from `0` to `1`, with a higher value indicating a higher likelihood of being selected. A value + of 0 means the object has not been selected. +- `adjusted_gaze_ray_world_space` - The fused left- and right-eye data adjusted to hit the closest point on the object. + +*/ + +/** + +*** + +## Functions + +*/ + +/** + +### g2om_get_version() + +Get the current version of the API. + +#### Definition + +```c,ignore +g2om_result g2om_get_version(g2om_version *version_ptr); +``` + +#### Remarks + +`g2om_get_version()` can be used to query the version of the `tobii_g2om` library currently used. + +#### Arguments + +- `version_ptr` is a pointer to a `g2om_version` variable to receive the current version numbers. + +### Return value + +A `g2om_result` is returned. + +#### Example + +```c +#include +#include +int main() +{ + g2om_version version; + g2om_result result = g2om_get_version( &version ); + + if( result != G2OM_RESULT_OK ) { + printf("Operation failed with error code: %d\n", result); + return result; + } + + return 0; +} +``` +*/ + +/** + +### g2om_context_options_init() + +When using `g2om_context_options_init`, this function initializes the `g2om_context_create_options` + structure to default values. + +#### Definition + +```c,ignore +g2om_result g2om_context_options_init(g2om_context_create_options *options); +``` + +#### Arguments + +- `options` is a pointer to a `g2om_context_create_options` structure to be initialized. + +#### Return value + +A `g2om_result` is returned. + +#### Example + +```c +#include +#include +int main() +{ + g2om_context_create_options options; + g2om_result result = g2om_context_options_init( &options ); + + if( result != G2OM_RESULT_OK ) { + printf("Operation failed with error code: %d\n", result); + return result; + } + + return 0; +} +``` +*/ + +/** + +### g2om_context_create() + +Initializes a G2OM instance with default parameters. This is a wrapper function around + `g2om_context_create_ex()`. + +#### Definition + +```c,ignore +g2om_result g2om_context_create(g2om_context **context_ptr); +``` + +#### Remarks + +For details, please see `g2om_context_create_ex()`. + +#### Arguments + +- `context_ptr` must be a pointer to a variable of the type `g2om_context*` that is, a pointer to a `g2om_context` pointer. This variable will be filled in with a pointer to the created instance. It is an opaque type, and only its declaration is available in the API. + +#### Return value + +A `g2om_result` is returned. + +#### Example + +```c +#include +#include +int main() +{ + g2om_context* context; + g2om_result result = g2om_context_create( &context ); + if( result != G2OM_RESULT_OK ) { + printf("Operation failed with error code: %d\n", result); + return result; + } + + result = g2om_context_destroy( &context ); + if( result != G2OM_RESULT_OK ) { + printf("Operation failed with error code: %d\n", result); + return result; + } + + return 0; +} +``` +*/ + +/** + +### g2om_context_create_ex() + +Initializes a G2OM instance in extended mode. It is possible to have multiple G2OM instances at one time. + +#### Definition + +```c,ignore +g2om_result g2om_context_create_ex(g2om_context **context_ptr, g2om_context_create_options *options); +``` + +#### Remarks + +Before any other API function can be invoked that accepts a `context` parameter the API needs to be +set up before use. The functions `g2om_context_create()` and `g2om_context_create_ex()` serve this purpose. +They allocate memory and set up internal data structures. + +The resulting `g2om_context` instance can then be passed explicitly to most functions. + +#### Arguments + +- `context_ptr` - Must be a pointer to a variable of the type `g2om_context*` that is, a pointer to a`g2om_context` pointer. This variable will be filled in with a pointer to the created instance. It is an opaque type, and only its declaration is available in the API. +- `options` - A pointer to the `g2om_context_create_options` structure. The structure must be initialized with `g2om_context_options_init()`. + +#### Return value + +A `g2om_result` is returned. + +#### Example + +```c +#include +#include +int main() +{ + g2om_context* context; + g2om_context_create_options options; + + g2om_result result = g2om_context_options_init(&options); + if( result != G2OM_RESULT_OK ) { + printf("Operation failed with error code: %d\n", result); + return result; + } + + options.capacity = 5; + options.thread_count = 1; + + result = g2om_context_create_ex( &context, &options); + if( result != G2OM_RESULT_OK ) { + printf("Operation failed with error code: %d\n", result); + return result; + } + + result = g2om_context_destroy( &context ); + if( result != G2OM_RESULT_OK ) { + printf("Operation failed with error code: %d\n", result); + return result; + } + + return 0; +} +``` +*/ + +/** + +### g2om_context_destroy() + +Destroy the G2OM instance. + +#### Definition + +```c,ignore +g2om_result g2om_context_destroy(g2om_context **context_ptr); +``` + +#### Remarks + +When creating an instance with `g2om_context_create()` or `g2om_context_create_ex()`, some system resources are acquired. When finished + using the API (typically during the shutdown process), `g2om_context_destroy()` should be called to destroy + the instance and ensure that those resources are released. The function `g2om_context_destroy()` + must only be called if the `g2om_context_create_*` function completed successfully. + +#### Arguments + +- `context_ptr` must be a pointer to a pointer to a valid `g2om_context` instance, that is a pointer + to a `g2om_context*`, as created by `g2om_context_create_*()`. + +#### Return value + +A `g2om_result` is returned. + +#### Example + +See `g2om_context_create()`. + +*/ + +/** + +### g2om_process() + +Takes gaze data, information about objects hit by raycasts and candidates (gaze aware objects) and + sends back a result for each candidate. + +#### Definition + +```c,ignore +g2om_result g2om_process( + g2om_context *context_ptr, + g2om_gaze_data *gaze_data_ptr, + g2om_raycast_result *raycast_ptr, + uint32_t candidates_count, + g2om_candidate *candidates_ptr, + g2om_candidate_result *candidate_results); +``` + +#### Remarks + +G2OM does not do any kind of background processing. This means that in order to receive any results + from the API, the user application needs to manually initiate the process, and this is done by + calling `g2om_process()`. The recommended way is to call it directly from + the users update loop before the result is needed and at the same rate as the user application will + use it. + +This function is guaranteed not to allocate. + +#### Arguments + +- `context_ptr` must be a pointer to a valid `g2om_context` instance as created by calling `g2om_context_create_*()`. +- `gaze_data_ptr` must be a pointer to a valid `g2om_gaze_data` instance as created by the user. +- `raycast_ptr` must be a pointer to a valid `g2om_raycast_result` instance as created by the user. +- `candidates_count` must be a u32 designating the length of the `candidates_ptr` array. +- `candidates_ptr` must be a pointer to a valid `g2om_candidate` array instance as created by the user. +- `candidate_results` must be a pointer to a valid `g2om_candidate_result` array instance as created by the + user, which will receive it's values from this function. `candidate_results` length **MUST** match + `candidates_ptr` length. + +#### Return value + +A `g2om_result` is returned. + +#### Example + +```c +#include +#include +int main() +{ + g2om_context* context; + g2om_result result = g2om_context_create( &context ); + + if( result != G2OM_RESULT_OK ) { + printf("Operation failed with error code: %d\n", result); + return result; + } + + // Get this data from your eye tracker: + g2om_gaze_data gaze_data = { 0 }; + + // Fill with data from ray casting in your game engine with gaze ray + // should only be valid if object hit was gazeaware / g2om_candidate + g2om_raycast_result raycast_result; + + // fill with candidates/objects from your game which should be gazeaware. + g2om_candidate candidate = { 0 }; + g2om_candidate candidates[1] = { + candidate + }; + + g2om_candidate *candidates_ptr = candidates; + + // this value will be filled by g2om_process + g2om_candidate_result candidate_result = { 0 }; + + // this array has to be the same size as candidates. + g2om_candidate_result candidate_results[1] = { + candidate_result + }; + + g2om_candidate_result *candidate_results_ptr = candidate_results; + + uint32_t candidate_count = 1; + uint32_t result_count = 1; + + if (result_count != candidate_count) { + printf("Mismatch in array lengths %i %i !", result_count, candidate_count); + } + + result = g2om_process( context, &gaze_data, &raycast_result, candidate_count, candidates_ptr, candidate_results_ptr ); + if( result != G2OM_RESULT_OK ) { + printf("Operation failed with error code: %d\n", result); + return result; + } + + // --> use the results/winner here, e.g.: <-- + // g2om_candidate_result winner = candidate_results[0]; // the results are sorted, so the first element is the highest scoring. + // printf("%ju has gaze focus.", winner.id); + + result = g2om_context_destroy( &context ); + if( result != G2OM_RESULT_OK ) { + printf("Operation failed with error code: %d\n", result); + return result; + } + + return 0; +} +``` +*/ + +/** + +### g2om_get_worldspace_corner_of_candidate() + +Transforms the corner points of the provided candidate's AABB to worldspace, and output the + resulting 8 corners. + + +#### Definition + +```c,ignore +g2om_result g2om_get_worldspace_corner_of_candidate( + g2om_candidate *candidate_ptr, + uint32_t corners_length, + g2om_vector *corners_ptr); +``` + +#### Remarks + +`g2om_get_worldspace_corner_of_candidate()` can be used to debug. It's use is to make sure that + the matrices used to transform to and from worldspace are correct. + +#### Arguments + +- `candidate_ptr` must be a pointer to a valid `g2om_candidate` instance as created by the user. +- `corners_ptr` is a pointer to a `g2om_vector` array, which must be of size 8, to receive the transformed corners. + +#### Return value + +A `g2om_result` is returned. + +#### Example + +```c +#include +#include +int main() +{ + // Made up information + g2om_candidate candidate = {0}; + uint32_t number_of_corners = G2OM_CORNERS_COUNT; + g2om_vector corners[G2OM_CORNERS_COUNT]; + g2om_result result = g2om_get_worldspace_corner_of_candidate( &candidate, number_of_corners, corners ); + if( result != G2OM_RESULT_OK ) { + printf("Operation failed with error code: %d\n", result); + return result; + } + + return 0; +} +``` +*/ + +/** + +### g2om_get_candidate_search_pattern() + +Gets an optimized search pattern rays to be used to find candidates. + +#### Definition + +```c,ignore +G2OM_API g2om_result G2OM_CALL g2om_get_candidate_search_pattern( + g2om_context *context_ptr, + g2om_gaze_data *gaze_data_ptr, + uint32_t number_of_rays, + g2om_gaze_ray *mutated_rays_ptr, +); +``` + +#### Remarks + +This only gets the rays. Finding the candidates has to be done by the application itself. + +#### Arguments + +TODO! + +#### Return value + +A `g2om_result` is returned. + +#### Example + +```c +#include +#include + +#define number_of_rays 9 + +int main() +{ + g2om_context* context; + g2om_result result = g2om_context_create( &context ); + if( result != G2OM_RESULT_OK ) { + printf("Operation failed with error code: %d\n", result); + return result; + } + + g2om_gaze_data gaze_data = {0}; + g2om_gaze_ray search_pattern[number_of_rays]; + + result = g2om_get_candidate_search_pattern(context, &gaze_data, number_of_rays, search_pattern); + if( result != G2OM_RESULT_OK ) { + printf("Operation failed with error code: %d\n", result); + return result; + } + + return 0; +} +``` +*/ \ No newline at end of file diff --git a/ThirdParty/GameIntegration/include/tobii_gameintegration.h b/ThirdParty/GameIntegration/include/tobii_gameintegration.h new file mode 100644 index 0000000..a94cd94 --- /dev/null +++ b/ThirdParty/GameIntegration/include/tobii_gameintegration.h @@ -0,0 +1,497 @@ +#pragma once + +#define TGI_VERSION_MAJOR 4 +#define TGI_VERSION_MINOR 1 +#define TGI_VERSION_REVISION 0 + +#ifdef _WIN64 +#ifdef _DEBUG + const char g_DllName[] = "tobii_gameintegration_x64_d.dll"; +#else + const char g_DllName[] = "tobii_gameintegration_x64.dll"; +#endif +#else +#ifdef _DEBUG + const char g_DllName[] = "tobii_gameintegration_x86_d.dll"; +#else + const char g_DllName[] = "tobii_gameintegration_x86.dll"; +#endif +#endif + +#if defined(GAME_INTEGRATION_EXPORT) +# define TGI_TYPE_EXPORT __declspec(dllexport) +# define TGI_C_EXPORT extern "C" __declspec(dllexport) +#else +# define TGI_TYPE_EXPORT +# define TGI_C_EXPORT +#endif + +#include +#include + +namespace TobiiGameIntegration +{ + struct ITobiiGameIntegrationApi; + +#ifdef __cplusplus + extern "C" + { +#endif + // Please, specify a full, real game name, not a code name for the game. + // Example: "Assassin's Creed Origins" + + TGI_C_EXPORT ITobiiGameIntegrationApi* __cdecl GetApi(const char* fullGameName, int majorVersion, int minorVersion, int revision); + TGI_C_EXPORT ITobiiGameIntegrationApi* __cdecl GetApiDynamic(const char* fullGameName, const char* dllPath, int majorVersion, int minorVersion, int revision); + +#ifdef __cplusplus + } +#endif + + inline ITobiiGameIntegrationApi* GetApi(const char* fullGameName) + { + return GetApi(fullGameName, TGI_VERSION_MAJOR, TGI_VERSION_MINOR, TGI_VERSION_REVISION); + } + + inline ITobiiGameIntegrationApi* GetApiDynamic(const char* fullGameName, const char* dllPath) + { + return GetApiDynamic(fullGameName, dllPath, TGI_VERSION_MAJOR, TGI_VERSION_MINOR, TGI_VERSION_REVISION); + } + + inline ITobiiGameIntegrationApi* GetApiDynamic(const char* fullGameName) + { + return GetApiDynamic(fullGameName, nullptr, TGI_VERSION_MAJOR, TGI_VERSION_MINOR, TGI_VERSION_REVISION); + } + + /* Common types */ + + struct TGI_TYPE_EXPORT Vector2d + { + float X; + float Y; + }; + + struct TGI_TYPE_EXPORT Vector3d + { + float X; + float Y; + float Z; + }; + + enum class HMDValidityFlags + { + LeftEyeIsValid = 1 << 0, + RightEyeIsValid = 1 << 1 + }; + inline HMDValidityFlags operator |(HMDValidityFlags a, HMDValidityFlags b) { return static_cast(static_cast(a) | static_cast(b)); } + inline HMDValidityFlags& operator |=(HMDValidityFlags &a, HMDValidityFlags b) { return a = a | b; } + inline HMDValidityFlags operator &(HMDValidityFlags a, HMDValidityFlags b) { return static_cast(static_cast(a) & static_cast(b)); } + inline HMDValidityFlags& operator &=(HMDValidityFlags& a, HMDValidityFlags b) { return a = a & b; } + + struct TGI_TYPE_EXPORT EyeInfo + { + EyeInfo() : + GazeOriginMM{ 0.0f, 0.0f, 0.0f }, + GazeDirection{ 0.0f, 0.0f, 0.0f }, + PupilPosition{ 0.0f, 0.0f }, + EyeOpenness(0.0f) + { } + + Vector3d GazeOriginMM; + Vector3d GazeDirection; + Vector2d PupilPosition; + float EyeOpenness; + }; + + struct TGI_TYPE_EXPORT HMDGaze + { + HMDGaze() : + Timestamp(0xDEADBEEF), + Validity(HMDValidityFlags::LeftEyeIsValid & HMDValidityFlags::RightEyeIsValid), + Counter(0), + LedMode(0), + LeftEyeInfo(), + RightEyeInfo() + { } + + int64_t Timestamp; + HMDValidityFlags Validity; + uint32_t Counter; + uint32_t LedMode; + EyeInfo LeftEyeInfo; + EyeInfo RightEyeInfo; + }; + + struct TGI_TYPE_EXPORT Quaternion + { + float X; + float Y; + float Z; + float W; + }; + + struct TGI_TYPE_EXPORT Rectangle + { + int32_t Left; + int32_t Top; + int32_t Right; + int32_t Bottom; + }; + + struct TGI_TYPE_EXPORT Dimensions + { + int32_t Width; + int32_t Height; + }; + + enum UnitType + { + SignedNormalized = 0, // Gaze point, signed normalized, client window bottom, left = (-1, -1), client window top, right = (1, 1) + Normalized, // Gaze point, unsigned normalized, client window bottom, left = (0, 0), client window top, right = (1, 1) + Mm, // Gaze point, mm, client window bottom, left = (0, 0), client window top, right = (window_width_mm, window_height_mm) + Pixels, // Gaze point, pixel, client window bottom, left = (0, 0), client window top, right = (window_width_pixels, window_height_pixels) + NumberOfUnitTypes // Use for looping, array allocation etc + }; + + + /* Eye tracking specific types */ + + struct TGI_TYPE_EXPORT GazePoint + { + int64_t TimeStampMicroSeconds; + float X; + float Y; + + GazePoint() + : TimeStampMicroSeconds(0) + , X(0.0f) + , Y(0.0f) + { } + }; + + struct TGI_TYPE_EXPORT Rotation + { + float Yaw; // Clockwise rotation about the down vector, the angle increases when turning your head right + float Pitch; // Clockwise rotation about the right vector, the angle increases when turning your head up + float Roll; // Clockwise rotation about the forward vector, the angle increases when tilting your head to the right + + Rotation() + : Yaw(0.0f) + , Pitch(0.0f) + , Roll(0.0f) + { } + }; + + struct TGI_TYPE_EXPORT Position + { + float X; // Increases when moving your head to the right + float Y; // Increases when moving your head up + float Z; // Increases when moving away from the tracker + + Position() + : X(0.0f) + , Y(0.0f) + , Z(0.0f) + { } + }; + + struct TGI_TYPE_EXPORT Transformation + { + Rotation Rotation; + Position Position; + + Transformation() + : Rotation() + , Position() + { } + }; + + struct TGI_TYPE_EXPORT HeadPose : Transformation + { + int64_t TimeStampMicroSeconds; + + HeadPose() + : TimeStampMicroSeconds(0) + { } + }; + + enum HeadViewType + { + None = 0, + Direct = 1, + Dynamic = 2 + }; + + struct TGI_TYPE_EXPORT SensitivityGradientSettings + { + float Scale; // Gradient curve for sensitivity settings + float Exponent; + float InflectionPoint; + float StartPoint; + float EndPoint; + + SensitivityGradientSettings() + : Scale(1.0f) + , Exponent(1.0f) + , InflectionPoint(0.5f) + , StartPoint(0.0f) + , EndPoint(1.0f) + { } + + SensitivityGradientSettings(float scale, float exponent, float inflectionPoint, float startPoint, float endPoint) + : Scale(scale), + Exponent(exponent), + InflectionPoint(inflectionPoint), + StartPoint(startPoint), + EndPoint(endPoint) + { } + }; + + struct TGI_TYPE_EXPORT HeadViewAutoCenterSettings + { + bool IsEnabled; + float NormalizeFasterGazeDeadZoneNormalized; + float ExtendedViewAngleFasterDeadZoneDegrees; + float MaxDistanceFromMasterCm; + float MaxAngularDistanceDegrees; + float FasterNormalizationFactor; + float PositionCompensationSpeed; + float RotationCompensationSpeed; + + HeadViewAutoCenterSettings() + : IsEnabled(true), + NormalizeFasterGazeDeadZoneNormalized(0.35f), + ExtendedViewAngleFasterDeadZoneDegrees(10.0f), + MaxDistanceFromMasterCm(5.0f), + MaxAngularDistanceDegrees(15.0f), + FasterNormalizationFactor(100.0f), + PositionCompensationSpeed(0.01f), + RotationCompensationSpeed(0.01f) + { } + }; + + struct TGI_TYPE_EXPORT ExtendedViewSettings + { + HeadViewType HeadViewType; + + float AspectRatioCorrectionFactor; + float HeadViewPitchCorrectionFactor; + + SensitivityGradientSettings GazeViewSensitivityGradientSettings; + float GazeViewResponsiveness; + float NormalizedGazeViewMinimumExtensionAngle; + float NormalizedGazeViewExtensionAngle; + + SensitivityGradientSettings HeadViewSensitivityGradientSettings; + float HeadViewResponsiveness; + HeadViewAutoCenterSettings HeadViewAutoCenter; + + ExtendedViewSettings() + : HeadViewType(HeadViewType::Direct) + , AspectRatioCorrectionFactor(1.0f) + , HeadViewPitchCorrectionFactor(1.5f) + , GazeViewSensitivityGradientSettings(0.5f, 2.25f, 0.8f, 0.0f, 1.0f) + , GazeViewResponsiveness(0.5f) + , NormalizedGazeViewMinimumExtensionAngle(0.05f) // This is the minimal maximum eview angle from the gaze component. Normalized rotations (so 0.05 => 0.05 * 360 = 18 degrees) + , NormalizedGazeViewExtensionAngle(NormalizedGazeViewMinimumExtensionAngle) // This is the maximum extension angle you can achieve with the gaze component. + , HeadViewSensitivityGradientSettings(0.65f, 1.25f, 0.5f, 0.0f, 1.0f) + , HeadViewResponsiveness(1.0f) + + { } + + ExtendedViewSettings(const ExtendedViewSettings &other) + { + *this = other; + } + }; + + // This will generate enum Feature and corresponding FeatureNames array +#define FOREACH_FEATURE(FEATURE) \ + FEATURE(AutoPause)\ + FEATURE(CharacterAwareness)\ + FEATURE(EnvironmentalAwareness)\ + FEATURE(EyeContact)\ + FEATURE(ObjectAwareness)\ + FEATURE(BungeeZoom)\ + FEATURE(ZoomAtGaze)\ + FEATURE(CleanUI)\ + FEATURE(DirectionalSound)\ + FEATURE(DynamicDepthOfField)\ + FEATURE(DynamicLightAdaptation)\ + FEATURE(FlashlightControl)\ + FEATURE(PeripheralEffects)\ + FEATURE(AutoTurn)\ + FEATURE(ExtendedView)\ + FEATURE(FreeView)\ + FEATURE(AimAtGaze)\ + FEATURE(CoverAtGaze)\ + FEATURE(EnemyTagging)\ + FEATURE(InteractAtGaze)\ + FEATURE(ThrowAtGaze)\ + FEATURE(TeleportToGaze)\ + FEATURE(JumpAtGaze)\ + FEATURE(FireAtGaze)\ + FEATURE(SecondaryWeaponGaze)\ + FEATURE(PickUpAtGaze)\ + FEATURE(SelectAtGaze)\ + FEATURE(FoveatedRendering)\ + FEATURE(AvatarEyeMovements)\ + FEATURE(WheelMenu)\ + FEATURE(MenuNavigation)\ + FEATURE(CursorWarp)\ + FEATURE(CenterAtGaze)\ + +#define GENERATE_ENUM(ENUM) ENUM, +#define GENERATE_STRING(STRING) #STRING, + + enum class Feature { + FOREACH_FEATURE(GENERATE_ENUM) + AMOUNT_OF_FEATURES + }; + + static char const* FeatureNames[] = { + FOREACH_FEATURE(GENERATE_STRING) + }; + + enum class TrackerType + { + None = 0, + PC, + HeadMountedDisplay + }; + + enum class CapabilityFlags + { + None = 0, + Presence = 1 << 0, + Head = 1 << 1, + Gaze = 1 << 2, + Foveation = 1 << 3, + EyeInfo = 1 << 4, + HMD = 1 << 5 + }; + inline CapabilityFlags operator |(CapabilityFlags a, CapabilityFlags b) { return static_cast(static_cast(a) | static_cast(b)); } + inline CapabilityFlags& operator |=(CapabilityFlags &a, CapabilityFlags b) { return a = a | b; } + inline CapabilityFlags operator &(CapabilityFlags a, CapabilityFlags b) { return static_cast(static_cast(a) & static_cast(b)); } + inline CapabilityFlags& operator &=(CapabilityFlags& a, CapabilityFlags b) { return a = a & b; } + + struct TGI_TYPE_EXPORT AimAtGazeFilterSettings + { + AimAtGazeFilterSettings() : + IsEnabled(false), + DistanceThreshold(0.1f) + { } + + bool IsEnabled; + float DistanceThreshold; + }; + + struct TGI_TYPE_EXPORT ResponsiveFilterSettings + { + ResponsiveFilterSettings() : + IsEnabled(false), + Responsiveness(0.5f), + StickinessDistance(0.1f) + { } + + bool IsEnabled; + float Responsiveness; + float StickinessDistance; + }; + + struct TGI_TYPE_EXPORT TrackerInfo + { + TrackerInfo() : + Type(TrackerType::None), + Capabilities(CapabilityFlags::None), + DisplayRectInOSCoordinates({ 0, 0, 0, 0}), + DisplaySizeMm({ 0, 0 }), + Url(nullptr), + FriendlyName(nullptr), + MonitorNameInOS(nullptr), + ModelName(nullptr), + Generation(nullptr), + SerialNumber(nullptr), + FirmwareVersion(nullptr), + IsAttached(false) + { } + + TrackerType Type; + CapabilityFlags Capabilities; + Rectangle DisplayRectInOSCoordinates; + Dimensions DisplaySizeMm; + const char* Url; + const char* FriendlyName; + const char* MonitorNameInOS; + const char* ModelName; + const char* Generation; + const char* SerialNumber; + const char* FirmwareVersion; + bool IsAttached; + }; + + struct TGI_TYPE_EXPORT ITrackerController + { + virtual bool GetTrackerInfo(TrackerInfo& trackerInfo) = 0; + virtual bool GetTrackerInfo(const char* url, TrackerInfo& trackerInfo) = 0; + virtual void UpdateTrackerInfos() = 0; + virtual bool GetTrackerInfos(const TrackerInfo*& trackerInfos, int& numberOfTrackerInfos) = 0; + virtual bool TrackHMD() = 0; + virtual bool TrackRectangle(const Rectangle& rectangle) = 0; + virtual bool TrackWindow(void* windowHandle) = 0; + virtual void StopTracking() = 0; + virtual bool IsConnected() const = 0; + virtual bool IsEnabled() const = 0; + }; + + struct TGI_TYPE_EXPORT IStreamsProvider + { + virtual bool GetLatestHeadPose(HeadPose& headPose) = 0; + virtual bool GetLatestGazePoint(GazePoint& gazePoint) = 0; + virtual int GetGazePoints(const GazePoint*& gazePoints) = 0; + virtual int GetHeadPoses(const HeadPose*& headPoses) = 0; + virtual bool GetLatestHMDGaze(HMDGaze& latestHMDGaze) = 0; + virtual int GetHMDGaze(const HMDGaze*& hmdGaze) = 0; + virtual bool IsPresent() = 0; + virtual void ConvertGazePoint(const GazePoint& fromGazePoint, GazePoint& toGazePoint, UnitType fromUnit, UnitType toUnit) = 0; + + virtual void SetAutoUnsubscribeForCapability(CapabilityFlags capability, float timeout) = 0; + virtual void UnsetAutoUnsubscribeForCapability(CapabilityFlags capability) = 0; + }; + + struct TGI_TYPE_EXPORT IExtendedView + { + virtual Transformation GetTransformation() const = 0; + virtual void UpdateSettings(const ExtendedViewSettings& settings) = 0; + virtual void UpdateGazeOnlySettings(const ExtendedViewSettings& settings) = 0; + virtual void UpdateHeadOnlySettings(const ExtendedViewSettings& settings) = 0; + virtual void ResetDefaultHeadPose() = 0; + }; + + struct TGI_TYPE_EXPORT IFeatures + { + virtual IExtendedView* GetExtendedView() = 0; + }; + + struct TGI_TYPE_EXPORT IFilters + { + virtual const ResponsiveFilterSettings& GetResponsiveFilterSettings() const = 0; + virtual void SetResponsiveFilterSettings(ResponsiveFilterSettings settings) = 0; + virtual const AimAtGazeFilterSettings& GetAimAtGazeFilterSettings() const = 0; + virtual void SetAimAtGazeFilterSettings(AimAtGazeFilterSettings settings) = 0; + + virtual void GetResponsiveFilterGazePoint(GazePoint& gazePoint) const = 0; + virtual void GetAimAtGazeFilterGazePoint(GazePoint& gazePoint, float &gazePointStability) const = 0; + }; + + struct TGI_TYPE_EXPORT ITobiiGameIntegrationApi + { + virtual ITrackerController* GetTrackerController() = 0; + virtual IStreamsProvider* GetStreamsProvider() = 0; + virtual IFeatures* GetFeatures() = 0; + virtual IFilters* GetFilters() = 0; + + virtual bool IsInitialized() = 0; + virtual void Update() = 0; + virtual void Shutdown() = 0; + }; +} \ No newline at end of file diff --git a/Tobii Tech Getting Started Software Development License Agreement (1 June 2020).pdf b/Tobii Tech Getting Started Software Development License Agreement (1 June 2020).pdf new file mode 100644 index 0000000..e993bd8 Binary files /dev/null and b/Tobii Tech Getting Started Software Development License Agreement (1 June 2020).pdf differ diff --git a/TobiiEyetracking.uplugin b/TobiiEyetracking.uplugin new file mode 100644 index 0000000..b9887a3 --- /dev/null +++ b/TobiiEyetracking.uplugin @@ -0,0 +1,45 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.0", + "FriendlyName": "Tobii Eyetracking Plugin", + "Description": "A plugin to enable Tobii Eyetracking using the StreamSDK in Unreal Engine 4", + "Category": "Input Devices", + "CreatedBy": "Tobii AB", + "CreatedByURL": "https://www.tobii.com/", + "DocsURL": "https://vr.tobii.com/sdk/develop/ue4/getting-started/", + "MarketplaceURL": "https://unrealengine.com/marketplace/en-US/slug/tobii-eye-tracking-sdk", + "SupportURL": "https://discord.gg/8XZnAyg", + "EngineVersion": "4.27.0", + "CanContainContent": true, + "Installed": true, + "Modules": [ + { + "Name": "TobiiCore", + "Type": "Runtime", + "LoadingPhase": "Default", + "WhitelistPlatforms": [ + "Win64", + "Win32" + ] + }, + { + "Name": "TobiiGTOM", + "Type": "Runtime", + "LoadingPhase": "Default", + "WhitelistPlatforms": [ + "Win64", + "Win32" + ] + }, + { + "Name": "TobiiInteractions", + "Type": "Runtime", + "LoadingPhase": "Default", + "WhitelistPlatforms": [ + "Win64", + "Win32" + ] + } + ] +} \ No newline at end of file