1
0
Fork 1
mirror of https://github.com/TES3MP/openmw-tes3mp.git synced 2025-02-28 13:39:40 +00:00

More refactoring / cleanup. Code policies.

This commit is contained in:
Mads Buvik Sandvei 2020-06-26 23:02:48 +02:00
parent 60ffaea195
commit 91de6392ca
36 changed files with 3877 additions and 3927 deletions

View file

@ -120,6 +120,10 @@ else ()
endif () endif ()
if(BUILD_VR_OPENXR) if(BUILD_VR_OPENXR)
# TODO: Move this into something akin to add_openmw_dir instead of breaking pattern.
# Later, openmw and openmw_vr should preferrably share game code as a static or shared library
# instead of being compiled separately, though for now that's not possible as i depend on
# USE_OPENXR preprocessor switches.
set(OPENMW_VR_FILES set(OPENMW_VR_FILES
vrengine.cpp vrengine.cpp
mwvr/openxraction.hpp mwvr/openxraction.hpp
@ -165,7 +169,7 @@ if(BUILD_VR_OPENXR)
${APPLE_BUNDLE_RESOURCES} ${APPLE_BUNDLE_RESOURCES}
) )
# Preprocessor variable used to control code paths between the vr port and non-vr. # Preprocessor variable used to control code paths to vr code
target_compile_options(openmw_vr PUBLIC -DUSE_OPENXR -DXR_USE_GRAPHICS_API_OPENGL -DXR_USE_PLATFORM_WIN32) target_compile_options(openmw_vr PUBLIC -DUSE_OPENXR -DXR_USE_GRAPHICS_API_OPENGL -DXR_USE_PLATFORM_WIN32)
endif() endif()

View file

@ -555,7 +555,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings)
MWInput::InputManager* input = MWInput::InputManager* input =
#ifdef USE_OPENXR #ifdef USE_OPENXR
new MWVR::OpenXRInputManager(mWindow, mViewer, mScreenCaptureHandler, mScreenCaptureOperation, keybinderUser, keybinderUserExists, userGameControllerdb, gameControllerdb, mGrab); new MWVR::VRInputManager(mWindow, mViewer, mScreenCaptureHandler, mScreenCaptureOperation, keybinderUser, keybinderUserExists, userGameControllerdb, gameControllerdb, mGrab);
#else #else
new MWInput::InputManager (mWindow, mViewer, mScreenCaptureHandler, mScreenCaptureOperation, keybinderUser, keybinderUserExists, userGameControllerdb, gameControllerdb, mGrab); new MWInput::InputManager (mWindow, mViewer, mScreenCaptureHandler, mScreenCaptureOperation, keybinderUser, keybinderUserExists, userGameControllerdb, gameControllerdb, mGrab);
#endif #endif

View file

@ -1076,10 +1076,6 @@ namespace MWRender
// TODO: It's difficult to design a good override system when // TODO: It's difficult to design a good override system when
// I don't have a good understanding of the animation code. So for // I don't have a good understanding of the animation code. So for
// now i just hardcode blocking of updaters for nodes that should not be animated in VR. // now i just hardcode blocking of updaters for nodes that should not be animated in VR.
// TODO: Some overrides cause NaN during cull.
// I aassume this happens if an override causes a bone to never receive a valid matrix
// Add any bone+groupname pair that is messing with Vr comfort here. // Add any bone+groupname pair that is messing with Vr comfort here.
using Overrides = std::set<std::string>; using Overrides = std::set<std::string>;
using GroupOverrides = std::map<std::string, Overrides>; using GroupOverrides = std::map<std::string, Overrides>;

View file

@ -5,83 +5,83 @@
namespace MWVR namespace MWVR
{ {
OpenXRAction::OpenXRAction( OpenXRAction::OpenXRAction(
XrAction action, XrAction action,
XrActionType actionType, XrActionType actionType,
const std::string& actionName, const std::string& actionName,
const std::string& localName) const std::string& localName)
: mAction(action) : mAction(action)
, mType(actionType) , mType(actionType)
, mName(actionName) , mName(actionName)
, mLocalName(localName) , mLocalName(localName)
{
};
OpenXRAction::~OpenXRAction() {
if (mAction)
{ {
xrDestroyAction(mAction); };
OpenXRAction::~OpenXRAction() {
if (mAction)
{
xrDestroyAction(mAction);
}
}
bool OpenXRAction::getFloat(XrPath subactionPath, float& value)
{
auto* xr = Environment::get().getManager();
XrActionStateGetInfo getInfo{ XR_TYPE_ACTION_STATE_GET_INFO };
getInfo.action = mAction;
getInfo.subactionPath = subactionPath;
XrActionStateFloat xrValue{ XR_TYPE_ACTION_STATE_FLOAT };
CHECK_XRCMD(xrGetActionStateFloat(xr->impl().xrSession(), &getInfo, &xrValue));
if (xrValue.isActive)
value = xrValue.currentState;
return xrValue.isActive;
}
bool OpenXRAction::getBool(XrPath subactionPath, bool& value)
{
auto* xr = Environment::get().getManager();
XrActionStateGetInfo getInfo{ XR_TYPE_ACTION_STATE_GET_INFO };
getInfo.action = mAction;
getInfo.subactionPath = subactionPath;
XrActionStateBoolean xrValue{ XR_TYPE_ACTION_STATE_BOOLEAN };
CHECK_XRCMD(xrGetActionStateBoolean(xr->impl().xrSession(), &getInfo, &xrValue));
if (xrValue.isActive)
value = xrValue.currentState;
return xrValue.isActive;
}
// Pose action only checks if the pose is active or not
bool OpenXRAction::getPoseIsActive(XrPath subactionPath)
{
auto* xr = Environment::get().getManager();
XrActionStateGetInfo getInfo{ XR_TYPE_ACTION_STATE_GET_INFO };
getInfo.action = mAction;
getInfo.subactionPath = subactionPath;
XrActionStatePose xrValue{ XR_TYPE_ACTION_STATE_POSE };
CHECK_XRCMD(xrGetActionStatePose(xr->impl().xrSession(), &getInfo, &xrValue));
return xrValue.isActive;
}
bool OpenXRAction::applyHaptics(XrPath subactionPath, float amplitude)
{
amplitude = std::max(0.f, std::min(1.f, amplitude));
auto* xr = Environment::get().getManager();
XrHapticVibration vibration{ XR_TYPE_HAPTIC_VIBRATION };
vibration.amplitude = amplitude;
vibration.duration = XR_MIN_HAPTIC_DURATION;
vibration.frequency = XR_FREQUENCY_UNSPECIFIED;
XrHapticActionInfo hapticActionInfo{ XR_TYPE_HAPTIC_ACTION_INFO };
hapticActionInfo.action = mAction;
hapticActionInfo.subactionPath = subactionPath;
CHECK_XRCMD(xrApplyHapticFeedback(xr->impl().xrSession(), &hapticActionInfo, (XrHapticBaseHeader*)&vibration));
return true;
} }
} }
bool OpenXRAction::getFloat(XrPath subactionPath, float& value)
{
auto* xr = Environment::get().getManager();
XrActionStateGetInfo getInfo{ XR_TYPE_ACTION_STATE_GET_INFO };
getInfo.action = mAction;
getInfo.subactionPath = subactionPath;
XrActionStateFloat xrValue{ XR_TYPE_ACTION_STATE_FLOAT };
CHECK_XRCMD(xrGetActionStateFloat(xr->impl().xrSession(), &getInfo, &xrValue));
if (xrValue.isActive)
value = xrValue.currentState;
return xrValue.isActive;
}
bool OpenXRAction::getBool(XrPath subactionPath, bool& value)
{
auto* xr = Environment::get().getManager();
XrActionStateGetInfo getInfo{ XR_TYPE_ACTION_STATE_GET_INFO };
getInfo.action = mAction;
getInfo.subactionPath = subactionPath;
XrActionStateBoolean xrValue{ XR_TYPE_ACTION_STATE_BOOLEAN };
CHECK_XRCMD(xrGetActionStateBoolean(xr->impl().xrSession(), &getInfo, &xrValue));
if (xrValue.isActive)
value = xrValue.currentState;
return xrValue.isActive;
}
// Pose action only checks if the pose is active or not
bool OpenXRAction::getPoseIsActive(XrPath subactionPath)
{
auto* xr = Environment::get().getManager();
XrActionStateGetInfo getInfo{ XR_TYPE_ACTION_STATE_GET_INFO };
getInfo.action = mAction;
getInfo.subactionPath = subactionPath;
XrActionStatePose xrValue{ XR_TYPE_ACTION_STATE_POSE };
CHECK_XRCMD(xrGetActionStatePose(xr->impl().xrSession(), &getInfo, &xrValue));
return xrValue.isActive;
}
bool OpenXRAction::applyHaptics(XrPath subactionPath, float amplitude)
{
amplitude = std::max(0.f, std::min(1.f, amplitude));
auto* xr = Environment::get().getManager();
XrHapticVibration vibration{ XR_TYPE_HAPTIC_VIBRATION };
vibration.amplitude = amplitude;
vibration.duration = XR_MIN_HAPTIC_DURATION;
vibration.frequency = XR_FREQUENCY_UNSPECIFIED;
XrHapticActionInfo hapticActionInfo{ XR_TYPE_HAPTIC_ACTION_INFO };
hapticActionInfo.action = mAction;
hapticActionInfo.subactionPath = subactionPath;
CHECK_XRCMD(xrApplyHapticFeedback(xr->impl().xrSession(), &hapticActionInfo, (XrHapticBaseHeader*)&vibration));
return true;
}
}

View file

@ -8,6 +8,7 @@
namespace MWVR namespace MWVR
{ {
/// \brief C++ wrapper for the XrAction type
struct OpenXRAction struct OpenXRAction
{ {
private: private:

View file

@ -12,289 +12,288 @@
namespace MWVR namespace MWVR
{ {
OpenXRInput::OpenXRInput(const std::vector<SuggestedBindings>& suggestedBindings) OpenXRInput::OpenXRInput(const std::vector<SuggestedBindings>& suggestedBindings)
: mActionSet(createActionSet()) : mActionSet(createActionSet())
{
// When starting to account for more devices than oculus touch, this section may need some expansion/redesign.
// Currently the set of action paths was determined using the oculus touch (i know nothing about the vive and the index).
// The set of action paths may therefore need expansion. E.g. /click vs /value may vary with controllers.
// To fit more actions onto controllers i created a system of short and long press actions. Allowing one action to activate
// on a short press, and another on long. Here, what actions are short press and what actions are long press is simply
// hardcoded at init, rather than interpreted from bindings. That's bad, and should be fixed, but that's hard to do
// while staying true to openxr's binding system, so if the system i wrote for the oculus touch isn't a good fit for
// the vive/index, we might want to rewrite this to handle bindings ourselves.
generateControllerActionPaths(ActionPath::Select, "/input/select/click");
generateControllerActionPaths(ActionPath::Squeeze, "/input/squeeze/value");
generateControllerActionPaths(ActionPath::Pose, "/input/aim/pose");
generateControllerActionPaths(ActionPath::Haptic, "/output/haptic");
generateControllerActionPaths(ActionPath::Menu, "/input/menu/click");
generateControllerActionPaths(ActionPath::ThumbstickX, "/input/thumbstick/x");
generateControllerActionPaths(ActionPath::ThumbstickY, "/input/thumbstick/y");
generateControllerActionPaths(ActionPath::ThumbstickClick, "/input/thumbstick/click");
generateControllerActionPaths(ActionPath::X, "/input/x/click");
generateControllerActionPaths(ActionPath::Y, "/input/y/click");
generateControllerActionPaths(ActionPath::A, "/input/a/click");
generateControllerActionPaths(ActionPath::B, "/input/b/click");
generateControllerActionPaths(ActionPath::Trigger, "/input/trigger/value");
/*
// Applicable actions not (yet) included
A_QuickKey1,
A_QuickKey2,
A_QuickKey3,
A_QuickKey4,
A_QuickKey5,
A_QuickKey6,
A_QuickKey7,
A_QuickKey8,
A_QuickKey9,
A_QuickKey10,
A_QuickKeysMenu,
A_QuickLoad,
A_CycleSpellLeft,
A_CycleSpellRight,
A_CycleWeaponLeft,
A_CycleWeaponRight,
A_Screenshot, // Generate a VR screenshot?
A_Console, // Currently awkward due to a lack of virtual keyboard, but should be included when that's in place
*/
createMWAction<ButtonPressAction>(MWInput::A_GameMenu, "game_menu", "Game Menu");
createMWAction<ButtonLongPressAction>(A_Recenter, "reposition_menu", "Reposition Menu");
createMWAction<ButtonPressAction>(MWInput::A_Inventory, "inventory", "Inventory");
createMWAction<ButtonPressAction>(MWInput::A_Activate, "activate", "Activate");
createMWAction<ButtonHoldAction>(MWInput::A_Use, "use", "Use");
createMWAction<ButtonPressAction>(MWInput::A_Jump, "jump", "Jump");
createMWAction<ButtonPressAction>(MWInput::A_ToggleWeapon, "weapon", "Weapon");
createMWAction<ButtonPressAction>(MWInput::A_ToggleSpell, "spell", "Spell");
createMWAction<ButtonPressAction>(MWInput::A_CycleSpellLeft, "cycle_spell_left", "Cycle Spell Left");
createMWAction<ButtonPressAction>(MWInput::A_CycleSpellRight, "cycle_spell_right", "Cycle Spell Right");
createMWAction<ButtonPressAction>(MWInput::A_CycleWeaponLeft, "cycle_weapon_left", "Cycle Weapon Left");
createMWAction<ButtonPressAction>(MWInput::A_CycleWeaponRight, "cycle_weapon_right", "Cycle Weapon Right");
createMWAction<ButtonHoldAction>(MWInput::A_Sneak, "sneak", "Sneak");
createMWAction<ButtonPressAction>(MWInput::A_QuickMenu, "quick_menu", "Quick Menu");
createMWAction<AxisAction>(MWInput::A_LookLeftRight, "look_left_right", "Look Left Right");
createMWAction<AxisAction>(MWInput::A_MoveForwardBackward, "move_forward_backward", "Move Forward Backward");
createMWAction<AxisAction>(MWInput::A_MoveLeftRight, "move_left_right", "Move Left Right");
createMWAction<ButtonLongPressAction>(MWInput::A_Journal, "journal_book", "Journal Book");
createMWAction<ButtonLongPressAction>(MWInput::A_QuickSave, "quick_save", "Quick Save");
createMWAction<ButtonPressAction>(MWInput::A_Rest, "rest", "Rest");
createMWAction<AxisAction>(A_ActivateTouch, "activate_touched", "Activate Touch");
createMWAction<ButtonPressAction>(MWInput::A_AlwaysRun, "always_run", "Always Run");
createMWAction<ButtonPressAction>(MWInput::A_AutoMove, "auto_move", "Auto Move");
createMWAction<ButtonLongPressAction>(MWInput::A_ToggleHUD, "toggle_hud", "Toggle HUD");
createMWAction<ButtonLongPressAction>(MWInput::A_ToggleDebug, "toggle_debug", "Toggle DEBUG");
createMWAction<AxisAction>(A_MenuUpDown, "menu_up_down", "Menu Up Down");
createMWAction<AxisAction>(A_MenuLeftRight, "menu_left_right", "Menu Left Right");
createMWAction<ButtonPressAction>(A_MenuSelect, "menu_select", "Menu Select");
createMWAction<ButtonPressAction>(A_MenuBack, "menu_back", "Menu Back");
createPoseAction(TrackedLimb::LEFT_HAND, "left_hand_pose", "Left Hand Pose");
createPoseAction(TrackedLimb::RIGHT_HAND, "right_hand_pose", "Right Hand Pose");
createHapticsAction(TrackedLimb::RIGHT_HAND, "right_hand_haptics", "Right Hand Haptics");
createHapticsAction(TrackedLimb::LEFT_HAND, "left_hand_haptics", "Left Hand Haptics");
for (auto& sb : suggestedBindings)
suggestBindings(sb);
auto* xr = Environment::get().getManager();
{ // Set up the action set
XrSessionActionSetsAttachInfo attachInfo{ XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO };
attachInfo.countActionSets = 1;
attachInfo.actionSets = &mActionSet;
CHECK_XRCMD(xrAttachSessionActionSets(xr->impl().xrSession(), &attachInfo));
}
};
void
OpenXRInput::createPoseAction(
TrackedLimb limb,
const std::string& actionName,
const std::string& localName)
{
mTrackerMap.emplace(limb, new PoseAction(std::move(createXRAction(XR_ACTION_TYPE_POSE_INPUT, actionName, localName))));
}
void
OpenXRInput::createHapticsAction(
TrackedLimb limb,
const std::string& actionName,
const std::string& localName)
{
mHapticsMap.emplace(limb, new HapticsAction(std::move(createXRAction(XR_ACTION_TYPE_VIBRATION_OUTPUT, actionName, localName))));
}
template<typename A, XrActionType AT>
void
OpenXRInput::createMWAction(
int openMWAction,
const std::string& actionName,
const std::string& localName)
{
mActionMap.emplace(openMWAction, new A(openMWAction, std::move(createXRAction(AT, actionName, localName))));
}
XrActionSet
OpenXRInput::createActionSet()
{
auto* xr = Environment::get().getManager();
XrActionSet actionSet = XR_NULL_HANDLE;
XrActionSetCreateInfo createInfo{ XR_TYPE_ACTION_SET_CREATE_INFO };
strcpy_s(createInfo.actionSetName, "gameplay");
strcpy_s(createInfo.localizedActionSetName, "Gameplay");
createInfo.priority = 0;
CHECK_XRCMD(xrCreateActionSet(xr->impl().xrInstance(), &createInfo, &actionSet));
return actionSet;
}
void OpenXRInput::suggestBindings(const SuggestedBindings& mwSuggestedBindings)
{
auto* xr = Environment::get().getManager();
XrPath oculusTouchInteractionProfilePath;
CHECK_XRCMD(
xrStringToPath(xr->impl().xrInstance(), mwSuggestedBindings.controllerPath.c_str(), &oculusTouchInteractionProfilePath));
std::vector<XrActionSuggestedBinding> suggestedBindings =
{ {
{*mTrackerMap[TrackedLimb::LEFT_HAND], getXrPath(ActionPath::Pose, Side::LEFT_SIDE)}, // When starting to account for more devices than oculus touch, this section may need some expansion/redesign.
{*mTrackerMap[TrackedLimb::RIGHT_HAND], getXrPath(ActionPath::Pose, Side::RIGHT_SIDE)},
{*mHapticsMap[TrackedLimb::LEFT_HAND], getXrPath(ActionPath::Haptic, Side::LEFT_SIDE)}, // Currently the set of action paths was determined using the oculus touch (i know nothing about the vive and the index).
{*mHapticsMap[TrackedLimb::RIGHT_HAND], getXrPath(ActionPath::Haptic, Side::RIGHT_SIDE)}, // The set of action paths may therefore need expansion. E.g. /click vs /value may vary with controllers.
// To fit more actions onto controllers i created a system of short and long press actions. Allowing one action to activate
// on a short press, and another on long. Here, what actions are short press and what actions are long press is simply
// hardcoded at init, rather than interpreted from bindings. That's bad, and should be fixed, but that's hard to do
// while staying true to openxr's binding system, so if the system i wrote for the oculus touch isn't a good fit for
// the vive/index, we might want to rewrite this to handle bindings ourselves.
generateControllerActionPaths(ActionPath::Select, "/input/select/click");
generateControllerActionPaths(ActionPath::Squeeze, "/input/squeeze/value");
generateControllerActionPaths(ActionPath::Pose, "/input/aim/pose");
generateControllerActionPaths(ActionPath::Haptic, "/output/haptic");
generateControllerActionPaths(ActionPath::Menu, "/input/menu/click");
generateControllerActionPaths(ActionPath::ThumbstickX, "/input/thumbstick/x");
generateControllerActionPaths(ActionPath::ThumbstickY, "/input/thumbstick/y");
generateControllerActionPaths(ActionPath::ThumbstickClick, "/input/thumbstick/click");
generateControllerActionPaths(ActionPath::X, "/input/x/click");
generateControllerActionPaths(ActionPath::Y, "/input/y/click");
generateControllerActionPaths(ActionPath::A, "/input/a/click");
generateControllerActionPaths(ActionPath::B, "/input/b/click");
generateControllerActionPaths(ActionPath::Trigger, "/input/trigger/value");
/*
// Applicable actions not (yet) included
A_QuickKey1,
A_QuickKey2,
A_QuickKey3,
A_QuickKey4,
A_QuickKey5,
A_QuickKey6,
A_QuickKey7,
A_QuickKey8,
A_QuickKey9,
A_QuickKey10,
A_QuickKeysMenu,
A_QuickLoad,
A_CycleSpellLeft,
A_CycleSpellRight,
A_CycleWeaponLeft,
A_CycleWeaponRight,
A_Screenshot, // Generate a VR screenshot?
A_Console, // Currently awkward due to a lack of virtual keyboard, but should be included when that's in place
*/
createMWAction<ButtonPressAction>(MWInput::A_GameMenu, "game_menu", "Game Menu");
createMWAction<ButtonLongPressAction>(A_Recenter, "reposition_menu", "Reposition Menu");
createMWAction<ButtonPressAction>(MWInput::A_Inventory, "inventory", "Inventory");
createMWAction<ButtonPressAction>(MWInput::A_Activate, "activate", "Activate");
createMWAction<ButtonHoldAction>(MWInput::A_Use, "use", "Use");
createMWAction<ButtonPressAction>(MWInput::A_Jump, "jump", "Jump");
createMWAction<ButtonPressAction>(MWInput::A_ToggleWeapon, "weapon", "Weapon");
createMWAction<ButtonPressAction>(MWInput::A_ToggleSpell, "spell", "Spell");
createMWAction<ButtonPressAction>(MWInput::A_CycleSpellLeft, "cycle_spell_left", "Cycle Spell Left");
createMWAction<ButtonPressAction>(MWInput::A_CycleSpellRight, "cycle_spell_right", "Cycle Spell Right");
createMWAction<ButtonPressAction>(MWInput::A_CycleWeaponLeft, "cycle_weapon_left", "Cycle Weapon Left");
createMWAction<ButtonPressAction>(MWInput::A_CycleWeaponRight, "cycle_weapon_right", "Cycle Weapon Right");
createMWAction<ButtonHoldAction>(MWInput::A_Sneak, "sneak", "Sneak");
createMWAction<ButtonPressAction>(MWInput::A_QuickMenu, "quick_menu", "Quick Menu");
createMWAction<AxisAction>(MWInput::A_LookLeftRight, "look_left_right", "Look Left Right");
createMWAction<AxisAction>(MWInput::A_MoveForwardBackward, "move_forward_backward", "Move Forward Backward");
createMWAction<AxisAction>(MWInput::A_MoveLeftRight, "move_left_right", "Move Left Right");
createMWAction<ButtonLongPressAction>(MWInput::A_Journal, "journal_book", "Journal Book");
createMWAction<ButtonLongPressAction>(MWInput::A_QuickSave, "quick_save", "Quick Save");
createMWAction<ButtonPressAction>(MWInput::A_Rest, "rest", "Rest");
createMWAction<AxisAction>(A_ActivateTouch, "activate_touched", "Activate Touch");
createMWAction<ButtonPressAction>(MWInput::A_AlwaysRun, "always_run", "Always Run");
createMWAction<ButtonPressAction>(MWInput::A_AutoMove, "auto_move", "Auto Move");
createMWAction<ButtonLongPressAction>(MWInput::A_ToggleHUD, "toggle_hud", "Toggle HUD");
createMWAction<ButtonLongPressAction>(MWInput::A_ToggleDebug, "toggle_debug", "Toggle DEBUG");
createMWAction<AxisAction>(A_MenuUpDown, "menu_up_down", "Menu Up Down");
createMWAction<AxisAction>(A_MenuLeftRight, "menu_left_right", "Menu Left Right");
createMWAction<ButtonPressAction>(A_MenuSelect, "menu_select", "Menu Select");
createMWAction<ButtonPressAction>(A_MenuBack, "menu_back", "Menu Back");
createPoseAction(TrackedLimb::LEFT_HAND, "left_hand_pose", "Left Hand Pose");
createPoseAction(TrackedLimb::RIGHT_HAND, "right_hand_pose", "Right Hand Pose");
createHapticsAction(TrackedLimb::RIGHT_HAND, "right_hand_haptics", "Right Hand Haptics");
createHapticsAction(TrackedLimb::LEFT_HAND, "left_hand_haptics", "Left Hand Haptics");
for (auto& sb : suggestedBindings)
suggestBindings(sb);
auto* xr = Environment::get().getManager();
{ // Set up the action set
XrSessionActionSetsAttachInfo attachInfo{ XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO };
attachInfo.countActionSets = 1;
attachInfo.actionSets = &mActionSet;
CHECK_XRCMD(xrAttachSessionActionSets(xr->impl().xrSession(), &attachInfo));
}
}; };
for (auto& mwSuggestedBinding : mwSuggestedBindings.bindings) void
OpenXRInput::createPoseAction(
TrackedLimb limb,
const std::string& actionName,
const std::string& localName)
{ {
auto xrAction = mActionMap.find(mwSuggestedBinding.action); mTrackerMap.emplace(limb, new PoseAction(std::move(createXRAction(XR_ACTION_TYPE_POSE_INPUT, actionName, localName))));
if (xrAction == mActionMap.end()) }
void
OpenXRInput::createHapticsAction(
TrackedLimb limb,
const std::string& actionName,
const std::string& localName)
{
mHapticsMap.emplace(limb, new HapticsAction(std::move(createXRAction(XR_ACTION_TYPE_VIBRATION_OUTPUT, actionName, localName))));
}
template<typename A, XrActionType AT>
void
OpenXRInput::createMWAction(
int openMWAction,
const std::string& actionName,
const std::string& localName)
{
mActionMap.emplace(openMWAction, new A(openMWAction, std::move(createXRAction(AT, actionName, localName))));
}
XrActionSet
OpenXRInput::createActionSet()
{
auto* xr = Environment::get().getManager();
XrActionSet actionSet = XR_NULL_HANDLE;
XrActionSetCreateInfo createInfo{ XR_TYPE_ACTION_SET_CREATE_INFO };
strcpy_s(createInfo.actionSetName, "gameplay");
strcpy_s(createInfo.localizedActionSetName, "Gameplay");
createInfo.priority = 0;
CHECK_XRCMD(xrCreateActionSet(xr->impl().xrInstance(), &createInfo, &actionSet));
return actionSet;
}
void OpenXRInput::suggestBindings(const SuggestedBindings& mwSuggestedBindings)
{
auto* xr = Environment::get().getManager();
XrPath oculusTouchInteractionProfilePath;
CHECK_XRCMD(
xrStringToPath(xr->impl().xrInstance(), mwSuggestedBindings.controllerPath.c_str(), &oculusTouchInteractionProfilePath));
std::vector<XrActionSuggestedBinding> suggestedBindings =
{ {
Log(Debug::Error) << "OpenXRInput: Unknown action " << mwSuggestedBinding.action; {*mTrackerMap[TrackedLimb::LEFT_HAND], getXrPath(ActionPath::Pose, Side::LEFT_SIDE)},
continue; {*mTrackerMap[TrackedLimb::RIGHT_HAND], getXrPath(ActionPath::Pose, Side::RIGHT_SIDE)},
{*mHapticsMap[TrackedLimb::LEFT_HAND], getXrPath(ActionPath::Haptic, Side::LEFT_SIDE)},
{*mHapticsMap[TrackedLimb::RIGHT_HAND], getXrPath(ActionPath::Haptic, Side::RIGHT_SIDE)},
};
for (auto& mwSuggestedBinding : mwSuggestedBindings.bindings)
{
auto xrAction = mActionMap.find(mwSuggestedBinding.action);
if (xrAction == mActionMap.end())
{
Log(Debug::Error) << "OpenXRInput: Unknown action " << mwSuggestedBinding.action;
continue;
}
suggestedBindings.push_back({ *xrAction->second, getXrPath(mwSuggestedBinding.path, mwSuggestedBinding.side) });
} }
suggestedBindings.push_back({ *xrAction->second, getXrPath(mwSuggestedBinding.path, mwSuggestedBinding.side) });
XrInteractionProfileSuggestedBinding xrSuggestedBindings{ XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING };
xrSuggestedBindings.interactionProfile = oculusTouchInteractionProfilePath;
xrSuggestedBindings.suggestedBindings = suggestedBindings.data();
xrSuggestedBindings.countSuggestedBindings = (uint32_t)suggestedBindings.size();
CHECK_XRCMD(xrSuggestInteractionProfileBindings(xr->impl().xrInstance(), &xrSuggestedBindings));
} }
XrInteractionProfileSuggestedBinding xrSuggestedBindings{ XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING }; void
xrSuggestedBindings.interactionProfile = oculusTouchInteractionProfilePath; OpenXRInput::generateControllerActionPaths(
xrSuggestedBindings.suggestedBindings = suggestedBindings.data(); ActionPath actionPath,
xrSuggestedBindings.countSuggestedBindings = (uint32_t)suggestedBindings.size(); const std::string& controllerAction)
CHECK_XRCMD(xrSuggestInteractionProfileBindings(xr->impl().xrInstance(), &xrSuggestedBindings));
}
void
OpenXRInput::generateControllerActionPaths(
ActionPath actionPath,
const std::string& controllerAction)
{
auto* xr = Environment::get().getManager();
ControllerActionPaths actionPaths;
std::string left = std::string("/user/hand/left") + controllerAction;
std::string right = std::string("/user/hand/right") + controllerAction;
CHECK_XRCMD(xrStringToPath(xr->impl().xrInstance(), left.c_str(), &actionPaths[(int)Side::LEFT_SIDE]));
CHECK_XRCMD(xrStringToPath(xr->impl().xrInstance(), right.c_str(), &actionPaths[(int)Side::RIGHT_SIDE]));
mPathMap[actionPath] = actionPaths;
}
std::unique_ptr<OpenXRAction>
OpenXRInput::createXRAction(
XrActionType actionType,
const std::string& actionName,
const std::string& localName)
{
ActionPtr actionPtr = nullptr;
std::vector<XrPath> subactionPaths;
XrActionCreateInfo createInfo{ XR_TYPE_ACTION_CREATE_INFO };
createInfo.actionType = actionType;
strcpy_s(createInfo.actionName, actionName.c_str());
strcpy_s(createInfo.localizedActionName, localName.c_str());
XrAction action = XR_NULL_HANDLE;
CHECK_XRCMD(xrCreateAction(mActionSet, &createInfo, &action));
return std::unique_ptr<OpenXRAction>{new OpenXRAction{action, actionType, actionName, localName}};
}
void
OpenXRInput::updateControls()
{
auto* xr = Environment::get().getManager();
if (!xr->impl().xrSessionRunning())
return;
const XrActiveActionSet activeActionSet{ mActionSet, XR_NULL_PATH };
XrActionsSyncInfo syncInfo{ XR_TYPE_ACTIONS_SYNC_INFO };
syncInfo.countActiveActionSets = 1;
syncInfo.activeActionSets = &activeActionSet;
CHECK_XRCMD(xrSyncActions(xr->impl().xrSession(), &syncInfo));
// Note on update order:
// Actions are queued FIFO.
// For most actions this does not matter.
// However mMenuBack may end GuiMode. If it shares a key with a non-gui-mode action
// and were processed before that action, they would both be activated which is
// clearly not desired.
for (auto& action : mActionMap)
action.second->updateAndQueue(mActionQueue);
}
XrPath OpenXRInput::generateXrPath(const std::string& path)
{
auto* xr = Environment::get().getManager();
XrPath xrpath = 0;
CHECK_XRCMD(xrStringToPath(xr->impl().xrInstance(), path.c_str(), &xrpath));
return xrpath;
}
const Action* OpenXRInput::nextAction()
{
if (mActionQueue.empty())
return nullptr;
const auto* action = mActionQueue.front();
mActionQueue.pop_front();
return action;
}
Pose
OpenXRInput::getLimbPose(
int64_t time,
TrackedLimb limb)
{
auto it = mTrackerMap.find(limb);
if (it == mTrackerMap.end())
{ {
Log(Debug::Error) << "OpenXRInput: No such tracker: " << limb; auto* xr = Environment::get().getManager();
return Pose{}; ControllerActionPaths actionPaths;
std::string left = std::string("/user/hand/left") + controllerAction;
std::string right = std::string("/user/hand/right") + controllerAction;
CHECK_XRCMD(xrStringToPath(xr->impl().xrInstance(), left.c_str(), &actionPaths[(int)Side::LEFT_SIDE]));
CHECK_XRCMD(xrStringToPath(xr->impl().xrInstance(), right.c_str(), &actionPaths[(int)Side::RIGHT_SIDE]));
mPathMap[actionPath] = actionPaths;
} }
it->second->update(time);
return it->second->value();
}
void OpenXRInput::applyHaptics(TrackedLimb limb, float intensity) std::unique_ptr<OpenXRAction>
{ OpenXRInput::createXRAction(
auto it = mHapticsMap.find(limb); XrActionType actionType,
if (it == mHapticsMap.end()) const std::string& actionName,
const std::string& localName)
{ {
Log(Debug::Error) << "OpenXRInput: No such tracker: " << limb; std::vector<XrPath> subactionPaths;
return; XrActionCreateInfo createInfo{ XR_TYPE_ACTION_CREATE_INFO };
createInfo.actionType = actionType;
strcpy_s(createInfo.actionName, actionName.c_str());
strcpy_s(createInfo.localizedActionName, localName.c_str());
XrAction action = XR_NULL_HANDLE;
CHECK_XRCMD(xrCreateAction(mActionSet, &createInfo, &action));
return std::unique_ptr<OpenXRAction>{new OpenXRAction{ action, actionType, actionName, localName }};
} }
it->second->apply(intensity); void
} OpenXRInput::updateControls()
XrPath OpenXRInput::getXrPath(ActionPath actionPath, Side side)
{
auto it = mPathMap.find(actionPath);
if (it == mPathMap.end())
{ {
Log(Debug::Error) << "OpenXRInput: No such path: " << (int)actionPath; auto* xr = Environment::get().getManager();
if (!xr->impl().xrSessionRunning())
return;
const XrActiveActionSet activeActionSet{ mActionSet, XR_NULL_PATH };
XrActionsSyncInfo syncInfo{ XR_TYPE_ACTIONS_SYNC_INFO };
syncInfo.countActiveActionSets = 1;
syncInfo.activeActionSets = &activeActionSet;
CHECK_XRCMD(xrSyncActions(xr->impl().xrSession(), &syncInfo));
// Note on update order:
// Actions are queued FIFO.
// For most actions this does not matter.
// However mMenuBack may end GuiMode. If it shares a key with a non-gui-mode action
// and were processed before that action, they would both be activated which is
// clearly not desired.
for (auto& action : mActionMap)
action.second->updateAndQueue(mActionQueue);
}
XrPath OpenXRInput::generateXrPath(const std::string& path)
{
auto* xr = Environment::get().getManager();
XrPath xrpath = 0;
CHECK_XRCMD(xrStringToPath(xr->impl().xrInstance(), path.c_str(), &xrpath));
return xrpath;
}
const Action* OpenXRInput::nextAction()
{
if (mActionQueue.empty())
return nullptr;
const auto* action = mActionQueue.front();
mActionQueue.pop_front();
return action;
}
Pose
OpenXRInput::getLimbPose(
int64_t time,
TrackedLimb limb)
{
auto it = mTrackerMap.find(limb);
if (it == mTrackerMap.end())
{
Log(Debug::Error) << "OpenXRInput: No such tracker: " << limb;
return Pose{};
}
it->second->update(time);
return it->second->value();
}
void OpenXRInput::applyHaptics(TrackedLimb limb, float intensity)
{
auto it = mHapticsMap.find(limb);
if (it == mHapticsMap.end())
{
Log(Debug::Error) << "OpenXRInput: No such tracker: " << limb;
return;
}
it->second->apply(intensity);
}
XrPath OpenXRInput::getXrPath(ActionPath actionPath, Side side)
{
auto it = mPathMap.find(actionPath);
if (it == mPathMap.end())
{
Log(Debug::Error) << "OpenXRInput: No such path: " << (int)actionPath;
}
return it->second[(int)side];
} }
return it->second[(int)side];
}
} }

View file

@ -8,61 +8,48 @@
namespace MWVR namespace MWVR
{ {
/// \brief Generates and manages OpenXR Actions and ActionSets by generating openxr bindings from a list of SuggestedBindings structs.
struct SuggestedBindings class OpenXRInput
{
struct Binding
{ {
int action; public:
ActionPath path; using Actions = MWInput::Actions;
Side side; using ControllerActionPaths = std::array<XrPath, 2>;
OpenXRInput(const std::vector<SuggestedBindings>& suggestedBindings);
//! Update all controls and queue any actions
void updateControls();
//! Get next action from queue (repeat until null is returned)
const Action* nextAction();
//! Get current pose of limb in space.
Pose getLimbPose(int64_t time, TrackedLimb limb);
//! Apply haptics of the given intensity to the given limb
void applyHaptics(TrackedLimb limb, float intensity);
protected:
template<typename A, XrActionType AT = A::ActionType>
void createMWAction(int openMWAction, const std::string& actionName, const std::string& localName);
void createPoseAction(TrackedLimb limb, const std::string& actionName, const std::string& localName);
void createHapticsAction(TrackedLimb limb, const std::string& actionName, const std::string& localName);
std::unique_ptr<OpenXRAction> createXRAction(XrActionType actionType, const std::string& actionName, const std::string& localName);
XrPath generateXrPath(const std::string& path);
void generateControllerActionPaths(ActionPath actionPath, const std::string& controllerAction);
XrActionSet createActionSet(void);
void suggestBindings(const SuggestedBindings& suggestedBindings);
XrPath getXrPath(ActionPath actionPath, Side side);
XrActionSet mActionSet{ nullptr };
std::map<ActionPath, ControllerActionPaths> mPathMap;
std::map<int, std::unique_ptr<Action>> mActionMap;
std::map<TrackedLimb, std::unique_ptr<PoseAction>> mTrackerMap;
std::map<TrackedLimb, std::unique_ptr<HapticsAction>> mHapticsMap;
std::deque<const Action*> mActionQueue{};
}; };
std::string controllerPath;
std::vector<Binding> bindings;
};
class OpenXRInput
{
public:
using Actions = MWInput::Actions;
using ControllerActionPaths = std::array<XrPath, 2>;
OpenXRInput(const std::vector<SuggestedBindings>& suggestedBindings);
//! Update all controls and queue any actions
void updateControls();
//! Get next action from queue (repeat until null is returned)
const Action* nextAction();
//! Get current pose of limb in space.
Pose getLimbPose(int64_t time, TrackedLimb limb);
//! Apply haptics of the given intensity to the given limb
void applyHaptics(TrackedLimb limb, float intensity);
protected:
template<typename A, XrActionType AT = A::ActionType>
void createMWAction(int openMWAction, const std::string& actionName, const std::string& localName);
void createPoseAction(TrackedLimb limb, const std::string& actionName, const std::string& localName);
void createHapticsAction(TrackedLimb limb, const std::string& actionName, const std::string& localName);
std::unique_ptr<OpenXRAction> createXRAction(XrActionType actionType, const std::string& actionName, const std::string& localName);
XrPath generateXrPath(const std::string& path);
void generateControllerActionPaths(ActionPath actionPath, const std::string& controllerAction);
XrActionSet createActionSet(void);
void suggestBindings(const SuggestedBindings& suggestedBindings);
XrPath getXrPath(ActionPath actionPath, Side side);
XrActionSet mActionSet{ nullptr };
std::map<ActionPath, ControllerActionPaths> mPathMap;
std::map<int, std::unique_ptr<Action>> mActionMap;
std::map<TrackedLimb, std::unique_ptr<PoseAction>> mTrackerMap;
std::map<TrackedLimb, std::unique_ptr<HapticsAction>> mHapticsMap;
std::deque<const Action*> mActionQueue{};
};
} }
#endif #endif

View file

@ -1,24 +1,9 @@
#include "vrenvironment.hpp"
#include "openxrmanager.hpp" #include "openxrmanager.hpp"
#include "vrenvironment.hpp"
#include "openxrmanagerimpl.hpp" #include "openxrmanagerimpl.hpp"
#include "../mwinput/inputmanagerimp.hpp" #include "../mwinput/inputmanagerimp.hpp"
#include <components/debug/debuglog.hpp> #include <components/debug/debuglog.hpp>
#include <components/sdlutil/sdlgraphicswindow.hpp>
#include <Windows.h>
#include <openxr/openxr.h>
#include <openxr/openxr_platform.h>
#include <openxr/openxr_platform_defines.h>
#include <openxr/openxr_reflection.h>
#include <osg/Camera>
#include <vector>
#include <array>
#include <map>
#include <iostream>
namespace MWVR namespace MWVR
{ {
@ -82,7 +67,7 @@ namespace MWVR
try { try {
mPrivate = std::make_shared<OpenXRManagerImpl>(); mPrivate = std::make_shared<OpenXRManagerImpl>();
} }
catch (std::exception & e) catch (std::exception& e)
{ {
Log(Debug::Error) << "Exception thrown by OpenXR: " << e.what(); Log(Debug::Error) << "Exception thrown by OpenXR: " << e.what();
osg::ref_ptr<osg::State> state = gc->getState(); osg::ref_ptr<osg::State> state = gc->getState();

View file

@ -20,16 +20,16 @@ struct XrCompositionLayerBaseHeader;
namespace MWVR namespace MWVR
{ {
// Use the pimpl pattern to avoid cluttering the namespace with openxr dependencies.
class OpenXRManagerImpl; class OpenXRManagerImpl;
/// \brief Manage the openxr runtime and session
class OpenXRManager : public osg::Referenced class OpenXRManager : public osg::Referenced
{ {
public: public:
class RealizeOperation : public osg::GraphicsOperation class RealizeOperation : public osg::GraphicsOperation
{ {
public: public:
RealizeOperation() : osg::GraphicsOperation("OpenXRRealizeOperation", false){}; RealizeOperation() : osg::GraphicsOperation("OpenXRRealizeOperation", false) {};
void operator()(osg::GraphicsContext* gc) override; void operator()(osg::GraphicsContext* gc) override;
virtual bool realized(); virtual bool realized();

View file

@ -1,4 +1,5 @@
#include "openxrmanagerimpl.hpp" #include "openxrmanagerimpl.hpp"
#include "openxrswapchain.hpp" #include "openxrswapchain.hpp"
#include "openxrswapchainimpl.hpp" #include "openxrswapchainimpl.hpp"
#include "vrtexture.hpp" #include "vrtexture.hpp"
@ -298,7 +299,7 @@ namespace MWVR
xrLayer.pose = toXR(layer.pose); xrLayer.pose = toXR(layer.pose);
xrLayer.fov = toXR(layer.fov); xrLayer.fov = toXR(layer.fov);
xrLayer.next = nullptr; xrLayer.next = nullptr;
return xrLayer; return xrLayer;
} }
@ -331,7 +332,7 @@ namespace MWVR
wglMakeCurrent(DC, GLRC); wglMakeCurrent(DC, GLRC);
} }
std::array<View, 2> std::array<View, 2>
OpenXRManagerImpl::getPredictedViews( OpenXRManagerImpl::getPredictedViews(
int64_t predictedDisplayTime, int64_t predictedDisplayTime,
ReferenceSpace space) ReferenceSpace space)
@ -444,7 +445,7 @@ namespace MWVR
switch (newState) switch (newState)
{ {
case XR_SESSION_STATE_READY: case XR_SESSION_STATE_READY:
//case XR_SESSION_STATE_IDLE: //case XR_SESSION_STATE_IDLE:
{ {
XrSessionBeginInfo beginInfo{ XR_TYPE_SESSION_BEGIN_INFO }; XrSessionBeginInfo beginInfo{ XR_TYPE_SESSION_BEGIN_INFO };
beginInfo.primaryViewConfigurationType = mViewConfigType; beginInfo.primaryViewConfigurationType = mViewConfigType;

View file

@ -25,79 +25,80 @@
namespace MWVR namespace MWVR
{ {
// Error management macros and functions. Should be used on every openxr call. // Error management macros and functions. Should be used on every openxr call.
#define CHK_STRINGIFY(x) #x #define CHK_STRINGIFY(x) #x
#define TOSTRING(x) CHK_STRINGIFY(x) #define TOSTRING(x) CHK_STRINGIFY(x)
#define FILE_AND_LINE __FILE__ ":" TOSTRING(__LINE__) #define FILE_AND_LINE __FILE__ ":" TOSTRING(__LINE__)
#define CHECK_XRCMD(cmd) CheckXrResult(cmd, #cmd, FILE_AND_LINE); #define CHECK_XRCMD(cmd) CheckXrResult(cmd, #cmd, FILE_AND_LINE);
#define CHECK_XRRESULT(res, cmdStr) CheckXrResult(res, cmdStr, FILE_AND_LINE); #define CHECK_XRRESULT(res, cmdStr) CheckXrResult(res, cmdStr, FILE_AND_LINE);
XrResult CheckXrResult(XrResult res, const char* originator = nullptr, const char* sourceLocation = nullptr); XrResult CheckXrResult(XrResult res, const char* originator = nullptr, const char* sourceLocation = nullptr);
std::string XrResultString(XrResult res); std::string XrResultString(XrResult res);
/// Conversion methods from openxr types to osg/mwvr types. Includes managing the differing conventions. /// Conversion methods from openxr types to osg/mwvr types. Includes managing the differing conventions.
MWVR::Pose fromXR(XrPosef pose); MWVR::Pose fromXR(XrPosef pose);
MWVR::FieldOfView fromXR(XrFovf fov); MWVR::FieldOfView fromXR(XrFovf fov);
osg::Vec3 fromXR(XrVector3f); osg::Vec3 fromXR(XrVector3f);
osg::Quat fromXR(XrQuaternionf quat); osg::Quat fromXR(XrQuaternionf quat);
/// Conversion methods from osg/mwvr types to openxr types. Includes managing the differing conventions. /// Conversion methods from osg/mwvr types to openxr types. Includes managing the differing conventions.
XrPosef toXR(MWVR::Pose pose); XrPosef toXR(MWVR::Pose pose);
XrFovf toXR(MWVR::FieldOfView fov); XrFovf toXR(MWVR::FieldOfView fov);
XrVector3f toXR(osg::Vec3 v); XrVector3f toXR(osg::Vec3 v);
XrQuaternionf toXR(osg::Quat quat); XrQuaternionf toXR(osg::Quat quat);
XrCompositionLayerProjectionView toXR(MWVR::CompositionLayerProjectionView layer); XrCompositionLayerProjectionView toXR(MWVR::CompositionLayerProjectionView layer);
struct OpenXRManagerImpl /// \brief Implementation of OpenXRManager
{ struct OpenXRManagerImpl
OpenXRManagerImpl(void); {
~OpenXRManagerImpl(void); OpenXRManagerImpl(void);
~OpenXRManagerImpl(void);
void waitFrame(); void waitFrame();
void beginFrame(); void beginFrame();
void endFrame(int64_t displayTime, int layerCount, const std::array<CompositionLayerProjectionView, 2>& layerStack); void endFrame(int64_t displayTime, int layerCount, const std::array<CompositionLayerProjectionView, 2>& layerStack);
bool xrSessionRunning() const { return mSessionRunning; } bool xrSessionRunning() const { return mSessionRunning; }
std::array<View, 2> getPredictedViews(int64_t predictedDisplayTime, ReferenceSpace space); std::array<View, 2> getPredictedViews(int64_t predictedDisplayTime, ReferenceSpace space);
MWVR::Pose getPredictedHeadPose(int64_t predictedDisplayTime, ReferenceSpace space); MWVR::Pose getPredictedHeadPose(int64_t predictedDisplayTime, ReferenceSpace space);
void handleEvents(); void handleEvents();
void enablePredictions(); void enablePredictions();
void disablePredictions(); void disablePredictions();
long long getLastPredictedDisplayTime(); long long getLastPredictedDisplayTime();
long long getLastPredictedDisplayPeriod(); long long getLastPredictedDisplayPeriod();
std::array<SwapchainConfig, 2> getRecommendedSwapchainConfig() const; std::array<SwapchainConfig, 2> getRecommendedSwapchainConfig() const;
XrSpace getReferenceSpace(ReferenceSpace space); XrSpace getReferenceSpace(ReferenceSpace space);
XrSession xrSession() const { return mSession; }; XrSession xrSession() const { return mSession; };
XrInstance xrInstance() const { return mInstance; }; XrInstance xrInstance() const { return mInstance; };
protected: protected:
void LogLayersAndExtensions(); void LogLayersAndExtensions();
void LogInstanceInfo(); void LogInstanceInfo();
void LogReferenceSpaces(); void LogReferenceSpaces();
const XrEventDataBaseHeader* nextEvent(); const XrEventDataBaseHeader* nextEvent();
void HandleSessionStateChanged(const XrEventDataSessionStateChanged& stateChangedEvent); void HandleSessionStateChanged(const XrEventDataSessionStateChanged& stateChangedEvent);
private: private:
bool initialized = false; bool initialized = false;
bool mPredictionsEnabled = false; bool mPredictionsEnabled = false;
XrInstance mInstance = XR_NULL_HANDLE; XrInstance mInstance = XR_NULL_HANDLE;
XrSession mSession = XR_NULL_HANDLE; XrSession mSession = XR_NULL_HANDLE;
XrSpace mSpace = XR_NULL_HANDLE; XrSpace mSpace = XR_NULL_HANDLE;
XrFormFactor mFormFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY; XrFormFactor mFormFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;
XrViewConfigurationType mViewConfigType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; XrViewConfigurationType mViewConfigType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
XrEnvironmentBlendMode mEnvironmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; XrEnvironmentBlendMode mEnvironmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
XrSystemId mSystemId = XR_NULL_SYSTEM_ID; XrSystemId mSystemId = XR_NULL_SYSTEM_ID;
XrGraphicsBindingOpenGLWin32KHR mGraphicsBinding{ XR_TYPE_GRAPHICS_BINDING_OPENGL_WIN32_KHR }; XrGraphicsBindingOpenGLWin32KHR mGraphicsBinding{ XR_TYPE_GRAPHICS_BINDING_OPENGL_WIN32_KHR };
XrSystemProperties mSystemProperties{ XR_TYPE_SYSTEM_PROPERTIES }; XrSystemProperties mSystemProperties{ XR_TYPE_SYSTEM_PROPERTIES };
std::array<XrViewConfigurationView, 2> mConfigViews{ { {XR_TYPE_VIEW_CONFIGURATION_VIEW}, {XR_TYPE_VIEW_CONFIGURATION_VIEW} } }; std::array<XrViewConfigurationView, 2> mConfigViews{ { {XR_TYPE_VIEW_CONFIGURATION_VIEW}, {XR_TYPE_VIEW_CONFIGURATION_VIEW} } };
XrSpace mReferenceSpaceView = XR_NULL_HANDLE; XrSpace mReferenceSpaceView = XR_NULL_HANDLE;
XrSpace mReferenceSpaceStage = XR_NULL_HANDLE; XrSpace mReferenceSpaceStage = XR_NULL_HANDLE;
XrEventDataBuffer mEventDataBuffer{ XR_TYPE_EVENT_DATA_BUFFER }; XrEventDataBuffer mEventDataBuffer{ XR_TYPE_EVENT_DATA_BUFFER };
XrFrameState mFrameState{}; XrFrameState mFrameState{};
XrSessionState mSessionState = XR_SESSION_STATE_UNKNOWN; XrSessionState mSessionState = XR_SESSION_STATE_UNKNOWN;
bool mSessionRunning = false; bool mSessionRunning = false;
std::mutex mFrameStateMutex{}; std::mutex mFrameStateMutex{};
std::mutex mEventMutex{}; std::mutex mEventMutex{};
}; };
} }
#endif #endif

View file

@ -10,6 +10,7 @@ namespace MWVR
{ {
class OpenXRSwapchainImpl; class OpenXRSwapchainImpl;
/// \brief Creation and management of openxr swapchains
class OpenXRSwapchain class OpenXRSwapchain
{ {
public: public:

View file

@ -11,8 +11,6 @@
#include <openxr/openxr_reflection.h> #include <openxr/openxr_reflection.h>
namespace MWVR { namespace MWVR {
OpenXRSwapchainImpl::OpenXRSwapchainImpl(osg::ref_ptr<osg::State> state, SwapchainConfig config) OpenXRSwapchainImpl::OpenXRSwapchainImpl(osg::ref_ptr<osg::State> state, SwapchainConfig config)
: mWidth((int)config.recommendedWidth) : mWidth((int)config.recommendedWidth)
, mHeight((int)config.recommendedHeight) , mHeight((int)config.recommendedHeight)
@ -33,27 +31,45 @@ namespace MWVR {
std::vector<int64_t> swapchainFormats(swapchainFormatCount); std::vector<int64_t> swapchainFormats(swapchainFormatCount);
CHECK_XRCMD(xrEnumerateSwapchainFormats(xr->impl().xrSession(), (uint32_t)swapchainFormats.size(), &swapchainFormatCount, swapchainFormats.data())); CHECK_XRCMD(xrEnumerateSwapchainFormats(xr->impl().xrSession(), (uint32_t)swapchainFormats.size(), &swapchainFormatCount, swapchainFormats.data()));
// List of supported color swapchain formats. // Find supported color swapchain format.
constexpr int64_t SupportedColorSwapchainFormats[] = { constexpr int64_t RequestedColorSwapchainFormats[] = {
GL_RGBA8, GL_RGBA8,
GL_RGBA8_SNORM, GL_RGBA8_SNORM,
}; };
auto swapchainFormatIt = auto swapchainFormatIt =
std::find_first_of(swapchainFormats.begin(), swapchainFormats.end(), std::begin(SupportedColorSwapchainFormats), std::find_first_of(swapchainFormats.begin(), swapchainFormats.end(), std::begin(RequestedColorSwapchainFormats),
std::end(SupportedColorSwapchainFormats)); std::end(RequestedColorSwapchainFormats));
if (swapchainFormatIt == swapchainFormats.end()) { if (swapchainFormatIt == swapchainFormats.end()) {
Log(Debug::Error) << "No swapchain format supported at runtime"; throw std::runtime_error("Swapchain color format not supported");
} }
mSwapchainColorFormat = *swapchainFormatIt; mSwapchainColorFormat = *swapchainFormatIt;
// Find supported depth swapchain format.
constexpr int64_t RequestedDepthSwapchainFormats[] = {
GL_DEPTH_COMPONENT32F,
GL_DEPTH_COMPONENT24,
GL_DEPTH_COMPONENT16,
};
swapchainFormatIt =
std::find_first_of(swapchainFormats.begin(), swapchainFormats.end(), std::begin(RequestedDepthSwapchainFormats),
std::end(RequestedDepthSwapchainFormats));
if (swapchainFormatIt == swapchainFormats.end()) {
throw std::runtime_error("Swapchain depth format not supported");
}
mSwapchainDepthFormat = *swapchainFormatIt;
mSamples = Settings::Manager::getInt("antialiasing", "Video"); mSamples = Settings::Manager::getInt("antialiasing", "Video");
// OpenXR requires a non-zero value
if (mSamples < 1)
mSamples = 1;
while (mSamples > 0) while (mSamples > 0)
{ {
Log(Debug::Verbose) << "Creating swapchain with dimensions Width=" << mWidth << " Heigh=" << mHeight << " SampleCount=" << mSamples; Log(Debug::Verbose) << "Creating swapchain with dimensions Width=" << mWidth << " Heigh=" << mHeight << " SampleCount=" << mSamples;
// Create the swapchain. // First create the swapchain of color buffers.
XrSwapchainCreateInfo swapchainCreateInfo{ XR_TYPE_SWAPCHAIN_CREATE_INFO }; XrSwapchainCreateInfo swapchainCreateInfo{ XR_TYPE_SWAPCHAIN_CREATE_INFO };
swapchainCreateInfo.arraySize = 1; swapchainCreateInfo.arraySize = 1;
swapchainCreateInfo.format = mSwapchainColorFormat; swapchainCreateInfo.format = mSwapchainColorFormat;
@ -61,30 +77,38 @@ namespace MWVR {
swapchainCreateInfo.height = mHeight; swapchainCreateInfo.height = mHeight;
swapchainCreateInfo.mipCount = 1; swapchainCreateInfo.mipCount = 1;
swapchainCreateInfo.faceCount = 1; swapchainCreateInfo.faceCount = 1;
swapchainCreateInfo.sampleCount = 1; swapchainCreateInfo.sampleCount = mSamples;
swapchainCreateInfo.usageFlags = XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT; swapchainCreateInfo.usageFlags = XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT;
//CHECK_XRCMD(xrCreateSwapchain(xr->impl().xrSession(), &swapchainCreateInfo, &mSwapchain));
auto res = xrCreateSwapchain(xr->impl().xrSession(), &swapchainCreateInfo, &mSwapchain); auto res = xrCreateSwapchain(xr->impl().xrSession(), &swapchainCreateInfo, &mSwapchain);
if (XR_SUCCEEDED(res)) if (!XR_SUCCEEDED(res))
break;
else
{ {
Log(Debug::Verbose) << "Failed to create swapchain with SampleCount=" << mSamples << ": " << XrResultString(res); Log(Debug::Verbose) << "Failed to create swapchain with SampleCount=" << mSamples << ": " << XrResultString(res);
mSamples /= 2; mSamples /= 2;
if(mSamples == 0) if (mSamples == 0)
std::runtime_error(XrResultString(res)); throw std::runtime_error(XrResultString(res));
continue;
} }
// Now create the swapchain of depth buffers.
swapchainCreateInfo.format = mSwapchainDepthFormat;
swapchainCreateInfo.usageFlags = XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
res = xrCreateSwapchain(xr->impl().xrSession(), &swapchainCreateInfo, &mDepthSwapchain);
if (XR_SUCCEEDED(res))
break;
else
throw std::runtime_error(XrResultString(res));
} }
uint32_t imageCount = 0; uint32_t imageCount = 0;
CHECK_XRCMD(xrEnumerateSwapchainImages(mSwapchain, 0, &imageCount, nullptr)); CHECK_XRCMD(xrEnumerateSwapchainImages(mSwapchain, 0, &imageCount, nullptr));
mSwapchainImageBuffers.resize(imageCount, { XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR }); mSwapchainImageBuffers.resize(imageCount, { XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR });
CHECK_XRCMD(xrEnumerateSwapchainImages(mSwapchain, imageCount, &imageCount, reinterpret_cast<XrSwapchainImageBaseHeader*>(mSwapchainImageBuffers.data()))); CHECK_XRCMD(xrEnumerateSwapchainImages(mSwapchain, imageCount, &imageCount, reinterpret_cast<XrSwapchainImageBaseHeader*>(mSwapchainImageBuffers.data())));
//for (const auto& swapchainImage : mSwapchainImageBuffers)
// mTextureBuffers.push_back(new VRTexture(state, swapchainImage.image, mWidth, mHeight, 0)); CHECK_XRCMD(xrEnumerateSwapchainImages(mDepthSwapchain, 0, &imageCount, nullptr));
mDepthSwapchainImageBuffers.resize(imageCount, { XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR });
CHECK_XRCMD(xrEnumerateSwapchainImages(mDepthSwapchain, imageCount, &imageCount, reinterpret_cast<XrSwapchainImageBaseHeader*>(mDepthSwapchainImageBuffers.data())));
for (unsigned i = 0; i < imageCount; i++) for (unsigned i = 0; i < imageCount; i++)
mRenderBuffers.emplace_back(new VRTexture(state, mWidth, mHeight, mSamples)); mRenderBuffers.emplace_back(new VRTexture(state, mWidth, mHeight, mSamples, mSwapchainImageBuffers[i].image, mDepthSwapchainImageBuffers[i].image));
mSubImage.swapchain = mSwapchain; mSubImage.swapchain = mSwapchain;
mSubImage.imageRect.offset = { 0, 0 }; mSubImage.imageRect.offset = { 0, 0 };
@ -99,12 +123,14 @@ namespace MWVR {
VRTexture* OpenXRSwapchainImpl::renderBuffer() const VRTexture* OpenXRSwapchainImpl::renderBuffer() const
{ {
return mRenderBuffers[mRenderBuffer].get(); if (isAcquired())
return mRenderBuffers[mAcquiredImageIndex].get();
throw std::logic_error("Swapbuffer not acquired before use");
} }
uint32_t OpenXRSwapchainImpl::acquiredImage() const uint32_t OpenXRSwapchainImpl::acquiredImage() const
{ {
if(isAcquired()) if (isAcquired())
return mSwapchainImageBuffers[mAcquiredImageIndex].image; return mSwapchainImageBuffers[mAcquiredImageIndex].image;
throw std::logic_error("Swapbuffer not acquired before use"); throw std::logic_error("Swapbuffer not acquired before use");
} }
@ -116,7 +142,7 @@ namespace MWVR {
void OpenXRSwapchainImpl::beginFrame(osg::GraphicsContext* gc) void OpenXRSwapchainImpl::beginFrame(osg::GraphicsContext* gc)
{ {
mRenderBuffer = (mRenderBuffer + 1) % mRenderBuffers.size(); acquire(gc);
renderBuffer()->beginFrame(gc); renderBuffer()->beginFrame(gc);
} }
@ -124,29 +150,34 @@ namespace MWVR {
void OpenXRSwapchainImpl::endFrame(osg::GraphicsContext* gc) void OpenXRSwapchainImpl::endFrame(osg::GraphicsContext* gc)
{ {
// Blit frame to swapchain
acquire(gc);
renderBuffer()->endFrame(gc, acquiredImage());
release(gc); release(gc);
} }
void OpenXRSwapchainImpl::acquire(osg::GraphicsContext* gc) void OpenXRSwapchainImpl::acquire(osg::GraphicsContext*)
{ {
XrSwapchainImageAcquireInfo acquireInfo{ XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO }; XrSwapchainImageAcquireInfo acquireInfo{ XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO };
// I am trusting that the openxr runtime won't diverge these indices so long as these are always called together.
// If some dumb ass implementation decides to violate this we'll just have to work around that if it actually happens.
CHECK_XRCMD(xrAcquireSwapchainImage(mSwapchain, &acquireInfo, &mAcquiredImageIndex)); CHECK_XRCMD(xrAcquireSwapchainImage(mSwapchain, &acquireInfo, &mAcquiredImageIndex));
uint32_t depthIndex = 0;
CHECK_XRCMD(xrAcquireSwapchainImage(mDepthSwapchain, &acquireInfo, &depthIndex));
if (depthIndex != mAcquiredImageIndex)
Log(Debug::Warning) << "Depth and color indices diverged";
XrSwapchainImageWaitInfo waitInfo{ XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO }; XrSwapchainImageWaitInfo waitInfo{ XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO };
waitInfo.timeout = XR_INFINITE_DURATION; waitInfo.timeout = XR_INFINITE_DURATION;
CHECK_XRCMD(xrWaitSwapchainImage(mSwapchain, &waitInfo)); CHECK_XRCMD(xrWaitSwapchainImage(mSwapchain, &waitInfo));
CHECK_XRCMD(xrWaitSwapchainImage(mDepthSwapchain, &waitInfo));
mIsAcquired = true; mIsAcquired = true;
} }
void OpenXRSwapchainImpl::release(osg::GraphicsContext* gc) void OpenXRSwapchainImpl::release(osg::GraphicsContext*)
{ {
mIsAcquired = false; mIsAcquired = false;
XrSwapchainImageReleaseInfo releaseInfo{ XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO }; XrSwapchainImageReleaseInfo releaseInfo{ XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO };
CHECK_XRCMD(xrReleaseSwapchainImage(mSwapchain, &releaseInfo)); CHECK_XRCMD(xrReleaseSwapchainImage(mSwapchain, &releaseInfo));
CHECK_XRCMD(xrReleaseSwapchainImage(mDepthSwapchain, &releaseInfo));
} }
} }

View file

@ -8,43 +8,45 @@ struct XrSwapchainSubImage;
namespace MWVR namespace MWVR
{ {
/// \brief Implementation of OpenXRSwapchain
class OpenXRSwapchainImpl
{
public:
OpenXRSwapchainImpl(osg::ref_ptr<osg::State> state, SwapchainConfig config);
~OpenXRSwapchainImpl();
class OpenXRSwapchainImpl void beginFrame(osg::GraphicsContext* gc);
{ void endFrame(osg::GraphicsContext* gc);
public: void acquire(osg::GraphicsContext* gc);
OpenXRSwapchainImpl(osg::ref_ptr<osg::State> state, SwapchainConfig config); void release(osg::GraphicsContext* gc);
~OpenXRSwapchainImpl();
void beginFrame(osg::GraphicsContext* gc); VRTexture* renderBuffer() const;
void endFrame(osg::GraphicsContext* gc);
void acquire(osg::GraphicsContext* gc);
void release(osg::GraphicsContext* gc);
VRTexture* renderBuffer() const; uint32_t acquiredImage() const;
uint32_t acquiredImage() const; bool isAcquired() const;
XrSwapchain xrSwapchain(void) const { return mSwapchain; };
XrSwapchainSubImage xrSubImage(void) const { return mSubImage; };
int width() const { return mWidth; };
int height() const { return mHeight; };
int samples() const { return mSamples; };
bool isAcquired() const; XrSwapchain mSwapchain = XR_NULL_HANDLE;
XrSwapchain xrSwapchain(void) const { return mSwapchain; }; XrSwapchain mDepthSwapchain = XR_NULL_HANDLE;
XrSwapchainSubImage xrSubImage(void) const { return mSubImage; }; std::vector<XrSwapchainImageOpenGLKHR> mSwapchainImageBuffers{};
int width() const { return mWidth; }; std::vector<XrSwapchainImageOpenGLKHR> mDepthSwapchainImageBuffers{};
int height() const { return mHeight; }; XrSwapchainSubImage mSubImage{};
int samples() const { return mSamples; }; int32_t mWidth = -1;
int32_t mHeight = -1;
XrSwapchain mSwapchain = XR_NULL_HANDLE; int32_t mSamples = -1;
std::vector<XrSwapchainImageOpenGLKHR> mSwapchainImageBuffers{}; int64_t mSwapchainColorFormat = -1;
//std::vector<osg::ref_ptr<VRTexture> > mTextureBuffers{}; int64_t mSwapchainDepthFormat = -1;
XrSwapchainSubImage mSubImage{}; uint32_t mFBO = 0;
int32_t mWidth = -1; std::vector<std::unique_ptr<VRTexture> > mRenderBuffers{};
int32_t mHeight = -1; int mRenderBuffer{ 0 };
int32_t mSamples = -1; uint32_t mAcquiredImageIndex{ 0 };
int64_t mSwapchainColorFormat = -1; bool mIsAcquired{ false };
uint32_t mFBO = 0; };
std::vector<std::unique_ptr<VRTexture> > mRenderBuffers{};
int mRenderBuffer{ 0 };
uint32_t mAcquiredImageIndex{ 0 };
bool mIsAcquired{ false };
};
} }
#endif #endif

View file

@ -33,324 +33,326 @@
#include <iomanip> #include <iomanip>
namespace MWVR { namespace RealisticCombat { namespace MWVR {
namespace RealisticCombat {
static const char* stateToString(SwingState florida) static const char* stateToString(SwingState florida)
{
switch (florida)
{
case SwingState_Cooldown:
return "Cooldown";
case SwingState_Impact:
return "Impact";
case SwingState_Ready:
return "Ready";
case SwingState_Swing:
return "Swing";
case SwingState_Launch:
return "Launch";
}
return "Error, invalid enum";
}
static const char* swingTypeToString(int type)
{
switch (type)
{
case ESM::Weapon::AT_Chop:
return "Chop";
case ESM::Weapon::AT_Slash:
return "Slash";
case ESM::Weapon::AT_Thrust:
return "Thrust";
case -1:
return "Fail";
default:
return "Invalid";
}
}
StateMachine::StateMachine(MWWorld::Ptr ptr) : ptr(ptr) {}
bool StateMachine::canSwing()
{
if (swingType >= 0)
if (velocity >= minVelocity)
if (swingType != ESM::Weapon::AT_Thrust || thrustVelocity >= 0.f)
return true;
return false;
}
// Actions common to all transitions
void StateMachine::transition(
SwingState newState)
{
Log(Debug::Verbose) << "Transition:" << stateToString(state) << "->" << stateToString(newState);
maxSwingVelocity = 0.f;
timeSinceEnteredState = 0.f;
movementSinceEnteredState = 0.f;
state = newState;
}
void StateMachine::reset()
{
maxSwingVelocity = 0.f;
timeSinceEnteredState = 0.f;
velocity = 0.f;
previousPosition = osg::Vec3(0.f, 0.f, 0.f);
state = SwingState_Ready;
}
static bool isMeleeWeapon(int type)
{
if (MWMechanics::getWeaponType(type)->mWeaponClass != ESM::WeaponType::Melee)
return false;
if (type == ESM::Weapon::HandToHand)
return true;
if (type >= 0)
return true;
return false;
}
static bool isSideSwingValidForWeapon(int type)
{
switch (type)
{
case ESM::Weapon::HandToHand:
case ESM::Weapon::BluntOneHand:
case ESM::Weapon::BluntTwoClose:
case ESM::Weapon::BluntTwoWide:
case ESM::Weapon::SpearTwoWide:
return true;
case ESM::Weapon::ShortBladeOneHand:
case ESM::Weapon::LongBladeOneHand:
case ESM::Weapon::LongBladeTwoHand:
case ESM::Weapon::AxeOneHand:
case ESM::Weapon::AxeTwoHand:
default:
return false;
}
}
void StateMachine::update(float dt, bool enabled)
{
auto* session = Environment::get().getSession();
auto* world = MWBase::Environment::get().getWorld();
auto& predictedPoses = session->predictedPoses(VRSession::FramePhase::Update);
auto& handPose = predictedPoses.hands[(int)MWVR::Side::RIGHT_SIDE];
auto weaponType = world->getActiveWeaponType();
enabled = enabled && isMeleeWeapon(weaponType);
if (mEnabled != enabled)
{
reset();
mEnabled = enabled;
}
if (!enabled)
return;
timeSinceEnteredState += dt;
// First determine direction of different swing types
// Discover orientation of weapon
osg::Quat weaponDir = handPose.orientation;
// Morrowind models do not hold weapons at a natural angle, so i rotate the hand forward
// to get a more natural angle on the weapon to allow more comfortable combat.
if (weaponType != ESM::Weapon::HandToHand)
weaponDir = osg::Quat(osg::PI_4, osg::Vec3{ 1,0,0 }) * weaponDir;
// Thrust means stabbing in the direction of the weapon
osg::Vec3 thrustDirection = weaponDir * osg::Vec3{ 0,1,0 };
// Slash and Chop are vertical, relative to the orientation of the weapon (direction of the sharp edge / hammer)
osg::Vec3 slashChopDirection = weaponDir * osg::Vec3{ 0,0,1 };
// Side direction of the weapon (i.e. The blunt side of the sword)
osg::Vec3 sideDirection = weaponDir * osg::Vec3{ 1,0,0 };
// Next determine current hand movement
// If tracking is lost, openxr will return a position of 0
// So i reset position when tracking is re-acquired to avoid a superspeed strike.
// Theoretically, the player's hand really could be at 0,0,0
// but that's a super rare case so whatever.
if (previousPosition == osg::Vec3(0.f, 0.f, 0.f))
previousPosition = handPose.position;
osg::Vec3 movement = handPose.position - previousPosition;
movementSinceEnteredState += movement.length();
previousPosition = handPose.position;
osg::Vec3 swingVector = movement / dt;
osg::Vec3 swingDirection = swingVector;
swingDirection.normalize();
// Compute swing velocities
// Thrust follows the orientation of the weapon. Negative thrust = no attack.
thrustVelocity = swingVector * thrustDirection;
velocity = swingVector.length();
if (isSideSwingValidForWeapon(weaponType))
{
// Compute velocity in the plane normal to the thrust direction.
float thrustComponent = std::abs(thrustVelocity / velocity);
float planeComponent = std::sqrt(1 - thrustComponent * thrustComponent);
slashChopVelocity = velocity * planeComponent;
sideVelocity = -1000.f;
}
else
{
// If side swing is not valid for the weapon, count slash/chop only along in
// the direction of the weapon's edge.
slashChopVelocity = std::abs(swingVector * slashChopDirection);
sideVelocity = std::abs(swingVector * sideDirection);
}
float orientationVerticality = std::abs(thrustDirection * osg::Vec3{ 0,0,1 });
float swingVerticality = std::abs(swingDirection * osg::Vec3{ 0,0,1 });
// Pick swing type based on greatest current velocity
// Note i use abs() of thrust velocity to prevent accidentally triggering
// chop/slash when player is withdrawing the weapon.
if (sideVelocity > std::abs(thrustVelocity) && sideVelocity > slashChopVelocity)
{
// Player is swinging with the "blunt" side of a weapon that
// cannot be used that way.
swingType = -1;
}
else if (std::abs(thrustVelocity) > slashChopVelocity)
{
swingType = ESM::Weapon::AT_Thrust;
}
else
{
// First check if the weapon is pointing upwards. In which case slash is not
// applicable, and the attack must be a chop.
if (orientationVerticality > 0.707)
swingType = ESM::Weapon::AT_Chop;
else
{ {
// Next check if the swing is more horizontal or vertical. A slash switch (florida)
// would be more horizontal. {
if(swingVerticality > 0.707) case SwingState_Cooldown:
swingType = ESM::Weapon::AT_Chop; return "Cooldown";
else case SwingState_Impact:
swingType = ESM::Weapon::AT_Slash; return "Impact";
case SwingState_Ready:
return "Ready";
case SwingState_Swing:
return "Swing";
case SwingState_Launch:
return "Launch";
}
return "Error, invalid enum";
}
static const char* swingTypeToString(int type)
{
switch (type)
{
case ESM::Weapon::AT_Chop:
return "Chop";
case ESM::Weapon::AT_Slash:
return "Slash";
case ESM::Weapon::AT_Thrust:
return "Thrust";
case -1:
return "Fail";
default:
return "Invalid";
}
}
StateMachine::StateMachine(MWWorld::Ptr ptr) : ptr(ptr) {}
bool StateMachine::canSwing()
{
if (swingType >= 0)
if (velocity >= minVelocity)
if (swingType != ESM::Weapon::AT_Thrust || thrustVelocity >= 0.f)
return true;
return false;
}
// Actions common to all transitions
void StateMachine::transition(
SwingState newState)
{
Log(Debug::Verbose) << "Transition:" << stateToString(state) << "->" << stateToString(newState);
maxSwingVelocity = 0.f;
timeSinceEnteredState = 0.f;
movementSinceEnteredState = 0.f;
state = newState;
}
void StateMachine::reset()
{
maxSwingVelocity = 0.f;
timeSinceEnteredState = 0.f;
velocity = 0.f;
previousPosition = osg::Vec3(0.f, 0.f, 0.f);
state = SwingState_Ready;
}
static bool isMeleeWeapon(int type)
{
if (MWMechanics::getWeaponType(type)->mWeaponClass != ESM::WeaponType::Melee)
return false;
if (type == ESM::Weapon::HandToHand)
return true;
if (type >= 0)
return true;
return false;
}
static bool isSideSwingValidForWeapon(int type)
{
switch (type)
{
case ESM::Weapon::HandToHand:
case ESM::Weapon::BluntOneHand:
case ESM::Weapon::BluntTwoClose:
case ESM::Weapon::BluntTwoWide:
case ESM::Weapon::SpearTwoWide:
return true;
case ESM::Weapon::ShortBladeOneHand:
case ESM::Weapon::LongBladeOneHand:
case ESM::Weapon::LongBladeTwoHand:
case ESM::Weapon::AxeOneHand:
case ESM::Weapon::AxeTwoHand:
default:
return false;
}
}
void StateMachine::update(float dt, bool enabled)
{
auto* session = Environment::get().getSession();
auto* world = MWBase::Environment::get().getWorld();
auto& predictedPoses = session->predictedPoses(VRSession::FramePhase::Update);
auto& handPose = predictedPoses.hands[(int)MWVR::Side::RIGHT_SIDE];
auto weaponType = world->getActiveWeaponType();
enabled = enabled && isMeleeWeapon(weaponType);
if (mEnabled != enabled)
{
reset();
mEnabled = enabled;
}
if (!enabled)
return;
timeSinceEnteredState += dt;
// First determine direction of different swing types
// Discover orientation of weapon
osg::Quat weaponDir = handPose.orientation;
// Morrowind models do not hold weapons at a natural angle, so i rotate the hand forward
// to get a more natural angle on the weapon to allow more comfortable combat.
if (weaponType != ESM::Weapon::HandToHand)
weaponDir = osg::Quat(osg::PI_4, osg::Vec3{ 1,0,0 }) * weaponDir;
// Thrust means stabbing in the direction of the weapon
osg::Vec3 thrustDirection = weaponDir * osg::Vec3{ 0,1,0 };
// Slash and Chop are vertical, relative to the orientation of the weapon (direction of the sharp edge / hammer)
osg::Vec3 slashChopDirection = weaponDir * osg::Vec3{ 0,0,1 };
// Side direction of the weapon (i.e. The blunt side of the sword)
osg::Vec3 sideDirection = weaponDir * osg::Vec3{ 1,0,0 };
// Next determine current hand movement
// If tracking is lost, openxr will return a position of 0
// So i reset position when tracking is re-acquired to avoid a superspeed strike.
// Theoretically, the player's hand really could be at 0,0,0
// but that's a super rare case so whatever.
if (previousPosition == osg::Vec3(0.f, 0.f, 0.f))
previousPosition = handPose.position;
osg::Vec3 movement = handPose.position - previousPosition;
movementSinceEnteredState += movement.length();
previousPosition = handPose.position;
osg::Vec3 swingVector = movement / dt;
osg::Vec3 swingDirection = swingVector;
swingDirection.normalize();
// Compute swing velocities
// Thrust follows the orientation of the weapon. Negative thrust = no attack.
thrustVelocity = swingVector * thrustDirection;
velocity = swingVector.length();
if (isSideSwingValidForWeapon(weaponType))
{
// Compute velocity in the plane normal to the thrust direction.
float thrustComponent = std::abs(thrustVelocity / velocity);
float planeComponent = std::sqrt(1 - thrustComponent * thrustComponent);
slashChopVelocity = velocity * planeComponent;
sideVelocity = -1000.f;
}
else
{
// If side swing is not valid for the weapon, count slash/chop only along in
// the direction of the weapon's edge.
slashChopVelocity = std::abs(swingVector * slashChopDirection);
sideVelocity = std::abs(swingVector * sideDirection);
}
float orientationVerticality = std::abs(thrustDirection * osg::Vec3{ 0,0,1 });
float swingVerticality = std::abs(swingDirection * osg::Vec3{ 0,0,1 });
// Pick swing type based on greatest current velocity
// Note i use abs() of thrust velocity to prevent accidentally triggering
// chop/slash when player is withdrawing the weapon.
if (sideVelocity > std::abs(thrustVelocity) && sideVelocity > slashChopVelocity)
{
// Player is swinging with the "blunt" side of a weapon that
// cannot be used that way.
swingType = -1;
}
else if (std::abs(thrustVelocity) > slashChopVelocity)
{
swingType = ESM::Weapon::AT_Thrust;
}
else
{
// First check if the weapon is pointing upwards. In which case slash is not
// applicable, and the attack must be a chop.
if (orientationVerticality > 0.707)
swingType = ESM::Weapon::AT_Chop;
else
{
// Next check if the swing is more horizontal or vertical. A slash
// would be more horizontal.
if (swingVerticality > 0.707)
swingType = ESM::Weapon::AT_Chop;
else
swingType = ESM::Weapon::AT_Slash;
}
}
switch (state)
{
case SwingState_Cooldown:
return update_cooldownState();
case SwingState_Ready:
return update_readyState();
case SwingState_Swing:
return update_swingState();
case SwingState_Impact:
return update_impactState();
case SwingState_Launch:
return update_launchState();
default:
throw std::logic_error(std::string("You forgot to implement state ") + stateToString(state) + " ya dingus");
}
}
void StateMachine::update_cooldownState()
{
if (timeSinceEnteredState >= minimumPeriod)
transition_cooldownToReady();
}
void StateMachine::transition_cooldownToReady()
{
transition(SwingState_Ready);
}
void StateMachine::update_readyState()
{
if (canSwing())
return transition_readyToLaunch();
}
void StateMachine::transition_readyToLaunch()
{
transition(SwingState_Launch);
}
void StateMachine::playSwish()
{
MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager();
std::string sound = "Weapon Swish";
if (strength < 0.5f)
sndMgr->playSound3D(ptr, sound, 1.0f, 0.8f); //Weak attack
if (strength < 1.0f)
sndMgr->playSound3D(ptr, sound, 1.0f, 1.0f); //Medium attack
else
sndMgr->playSound3D(ptr, sound, 1.0f, 1.2f); //Strong attack
Log(Debug::Verbose) << "Swing: " << swingTypeToString(swingType);
}
void StateMachine::update_launchState()
{
if (movementSinceEnteredState > minimumPeriod)
transition_launchToSwing();
if (!canSwing())
return transition_launchToReady();
}
void StateMachine::transition_launchToReady()
{
transition(SwingState_Ready);
}
void StateMachine::transition_launchToSwing()
{
playSwish();
transition(SwingState_Swing);
// As a special case, update the new state immediately to allow
// same-frame impacts.
update_swingState();
}
void StateMachine::update_swingState()
{
maxSwingVelocity = std::max(velocity, maxSwingVelocity);
strength = std::min(1.f, (maxSwingVelocity - minVelocity) / maxVelocity);
// When velocity falls below minimum, transition to register the miss
if (!canSwing())
return transition_swingingToImpact();
// Call hit with simulated=true to check for hit without actually causing an impact
if (ptr.getClass().hit(ptr, strength, swingType, true))
return transition_swingingToImpact();
}
void StateMachine::transition_swingingToImpact()
{
ptr.getClass().hit(ptr, strength, swingType, false);
transition(SwingState_Impact);
}
void StateMachine::update_impactState()
{
if (velocity < minVelocity)
return transition_impactToCooldown();
}
void StateMachine::transition_impactToCooldown()
{
transition(SwingState_Cooldown);
} }
}
switch (state)
{
case SwingState_Cooldown:
return update_cooldownState();
case SwingState_Ready:
return update_readyState();
case SwingState_Swing:
return update_swingState();
case SwingState_Impact:
return update_impactState();
case SwingState_Launch:
return update_launchState();
default:
throw std::logic_error(std::string("You forgot to implement state ") + stateToString(state) + " ya dingus");
} }
} }
void StateMachine::update_cooldownState()
{
if (timeSinceEnteredState >= minimumPeriod)
transition_cooldownToReady();
}
void StateMachine::transition_cooldownToReady()
{
transition(SwingState_Ready);
}
void StateMachine::update_readyState()
{
if (canSwing())
return transition_readyToLaunch();
}
void StateMachine::transition_readyToLaunch()
{
transition(SwingState_Launch);
}
void StateMachine::playSwish()
{
MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager();
std::string sound = "Weapon Swish";
if (strength < 0.5f)
sndMgr->playSound3D(ptr, sound, 1.0f, 0.8f); //Weak attack
if (strength < 1.0f)
sndMgr->playSound3D(ptr, sound, 1.0f, 1.0f); //Medium attack
else
sndMgr->playSound3D(ptr, sound, 1.0f, 1.2f); //Strong attack
Log(Debug::Verbose) << "Swing: " << swingTypeToString(swingType);
}
void StateMachine::update_launchState()
{
if (movementSinceEnteredState > minimumPeriod)
transition_launchToSwing();
if (!canSwing())
return transition_launchToReady();
}
void StateMachine::transition_launchToReady()
{
transition(SwingState_Ready);
}
void StateMachine::transition_launchToSwing()
{
playSwish();
transition(SwingState_Swing);
// As a special case, update the new state immediately to allow
// same-frame impacts.
update_swingState();
}
void StateMachine::update_swingState()
{
maxSwingVelocity = std::max(velocity, maxSwingVelocity);
strength = std::min(1.f, (maxSwingVelocity - minVelocity) / maxVelocity);
// When velocity falls below minimum, transition to register the miss
if (!canSwing())
return transition_swingingToImpact();
// Call hit with simulated=true to check for hit without actually causing an impact
if(ptr.getClass().hit(ptr, strength, swingType, true))
return transition_swingingToImpact();
}
void StateMachine::transition_swingingToImpact()
{
ptr.getClass().hit(ptr, strength, swingType, false);
transition(SwingState_Impact);
}
void StateMachine::update_impactState()
{
if (velocity < minVelocity)
return transition_impactToCooldown();
}
void StateMachine::transition_impactToCooldown()
{
transition(SwingState_Cooldown);
}
}}

View file

@ -10,71 +10,73 @@
#include "vrenvironment.hpp" #include "vrenvironment.hpp"
#include "vrsession.hpp" #include "vrsession.hpp"
namespace MWVR { namespace RealisticCombat { namespace MWVR {
enum SwingState namespace RealisticCombat {
{ enum SwingState
SwingState_Ready, {
SwingState_Launch, SwingState_Ready,
SwingState_Swing, SwingState_Launch,
SwingState_Impact, SwingState_Swing,
SwingState_Cooldown, SwingState_Impact,
}; SwingState_Cooldown,
};
struct StateMachine struct StateMachine
{ {
// TODO: These should be configurable // TODO: These should be configurable
const float minVelocity = 1.f; const float minVelocity = 1.f;
const float maxVelocity = 4.f; const float maxVelocity = 4.f;
float velocity = 0.f; float velocity = 0.f;
float maxSwingVelocity = 0.f; float maxSwingVelocity = 0.f;
SwingState state = SwingState_Ready; SwingState state = SwingState_Ready;
MWWorld::Ptr ptr = MWWorld::Ptr(); MWWorld::Ptr ptr = MWWorld::Ptr();
int swingType = -1; int swingType = -1;
float strength = 0.f; float strength = 0.f;
float thrustVelocity{ 0.f }; float thrustVelocity{ 0.f };
float slashChopVelocity{ 0.f }; float slashChopVelocity{ 0.f };
float sideVelocity{ 0.f }; float sideVelocity{ 0.f };
float minimumPeriod{ .25f }; float minimumPeriod{ .25f };
float timeSinceEnteredState = { 0.f }; float timeSinceEnteredState = { 0.f };
float movementSinceEnteredState = { 0.f }; float movementSinceEnteredState = { 0.f };
bool mEnabled = false; bool mEnabled = false;
osg::Vec3 previousPosition{ 0.f,0.f,0.f }; osg::Vec3 previousPosition{ 0.f,0.f,0.f };
StateMachine(MWWorld::Ptr ptr); StateMachine(MWWorld::Ptr ptr);
bool canSwing(); bool canSwing();
void playSwish(); void playSwish();
void reset(); void reset();
void transition(SwingState newState); void transition(SwingState newState);
void update(float dt, bool enabled); void update(float dt, bool enabled);
void update_cooldownState(); void update_cooldownState();
void transition_cooldownToReady(); void transition_cooldownToReady();
void update_readyState(); void update_readyState();
void transition_readyToLaunch(); void transition_readyToLaunch();
void update_launchState(); void update_launchState();
void transition_launchToReady(); void transition_launchToReady();
void transition_launchToSwing(); void transition_launchToSwing();
void update_swingState(); void update_swingState();
void transition_swingingToImpact(); void transition_swingingToImpact();
void update_impactState(); void update_impactState();
void transition_impactToCooldown(); void transition_impactToCooldown();
}; };
}} }
}
#endif #endif

File diff suppressed because it is too large Load diff

View file

@ -9,86 +9,91 @@
namespace MWVR namespace MWVR
{ {
class HandController; class HandController;
class FingerController; class FingerController;
class ForearmController; class ForearmController;
/// Subclassing NpcAnimation to override behaviours not compatible with VR /// Subclassing NpcAnimation to override behaviours not compatible with VR
class VRAnimation : public MWRender::NpcAnimation class VRAnimation : public MWRender::NpcAnimation
{ {
protected: protected:
virtual void addControllers(); virtual void addControllers();
public: public:
/** /**
* @param ptr * @param ptr
* @param disableListener Don't listen for equipment changes and magic effects. InventoryStore only supports * @param disableListener Don't listen for equipment changes and magic effects. InventoryStore only supports
* one listener at a time, so you shouldn't do this if creating several NpcAnimations * one listener at a time, so you shouldn't do this if creating several NpcAnimations
* for the same Ptr, eg preview dolls for the player. * for the same Ptr, eg preview dolls for the player.
* Those need to be manually rendered anyway. * Those need to be manually rendered anyway.
* @param disableSounds Same as \a disableListener but for playing items sounds * @param disableSounds Same as \a disableListener but for playing items sounds
* @param xrSession The XR session that shall be used to track limbs * @param xrSession The XR session that shall be used to track limbs
*/ */
VRAnimation(const MWWorld::Ptr& ptr, osg::ref_ptr<osg::Group> parentNode, Resource::ResourceSystem* resourceSystem, VRAnimation(const MWWorld::Ptr& ptr, osg::ref_ptr<osg::Group> parentNode, Resource::ResourceSystem* resourceSystem,
bool disableSounds, std::shared_ptr<VRSession> xrSession ); bool disableSounds, std::shared_ptr<VRSession> xrSession);
virtual ~VRAnimation(); virtual ~VRAnimation();
/// Overridden to always be false /// Overridden to always be false
virtual void enableHeadAnimation(bool enable); virtual void enableHeadAnimation(bool enable);
/// Overridden to always be false /// Overridden to always be false
virtual void setAccurateAiming(bool enabled); virtual void setAccurateAiming(bool enabled);
/// Overridden, implementation tbd /// Overridden, implementation tbd
virtual osg::Vec3f runAnimation(float timepassed); virtual osg::Vec3f runAnimation(float timepassed);
/// A relative factor (0-1) that decides if and how much the skeleton should be pitched /// A relative factor (0-1) that decides if and how much the skeleton should be pitched
/// to indicate the facing orientation of the character. /// to indicate the facing orientation of the character.
virtual void setPitchFactor(float factor) { mPitchFactor = factor; } virtual void setPitchFactor(float factor) { mPitchFactor = factor; }
/// Overriden to always be a variant of VM_VR* /// Overriden to always be a variant of VM_VR*
virtual void setViewMode(ViewMode viewMode); virtual void setViewMode(ViewMode viewMode);
/// Overriden to include VR modifications /// Overriden to include VR modifications
virtual void updateParts(); virtual void updateParts();
/// Overrides finger animations to point forward /// Overrides finger animations to point forward
void setFingerPointingMode(bool enabled); void setFingerPointingMode(bool enabled);
bool fingerPointingMode() const { return mFingerPointingMode; }
bool canPlaceObject(); /// @return Whether animation is currently in finger pointing mode
///< @return true if it is possible to place on object where the player is currently pointing bool fingerPointingMode() const { return mFingerPointingMode; }
const MWRender::RayResult& getPointerTarget() const; /// @return true if it is possible to place on object where the player is currently pointing
///< @return pointer to the object the player's melee weapon is currently intersecting. bool canPlaceObject();
void updatePointerTarget(); /// @return pointer to the object the player's melee weapon is currently intersecting.
const MWRender::RayResult& getPointerTarget() const;
MWWorld::Ptr getTarget(const std::string& directorNode); /// Update what object this vr animation is currently pointing at.
void updatePointerTarget();
osg::Matrix getWeaponTransformMatrix() const; /// @return whatever ref is the current pointer target, if any
MWWorld::Ptr getTarget(const std::string& directorNode);
protected: /// @return world transform that yields the position and orientation of the current weapon
static osg::ref_ptr<osg::Geometry> createPointerGeometry(void); osg::Matrix getWeaponTransformMatrix() const;
float getVelocity(const std::string& groupname) const override; protected:
static osg::ref_ptr<osg::Geometry> createPointerGeometry(void);
protected: float getVelocity(const std::string& groupname) const override;
std::shared_ptr<VRSession> mSession;
osg::ref_ptr<ForearmController> mForearmControllers[2];
osg::ref_ptr<HandController> mHandControllers[2];
osg::ref_ptr<FingerController> mIndexFingerControllers[2];
osg::ref_ptr<osg::MatrixTransform> mModelOffset;
bool mFingerPointingMode{ false }; protected:
osg::ref_ptr<osg::Geometry> mPointerGeometry{ nullptr }; std::shared_ptr<VRSession> mSession;
osg::ref_ptr<osg::MatrixTransform> mPointerRescale{ nullptr }; osg::ref_ptr<ForearmController> mForearmControllers[2];
osg::ref_ptr<osg::MatrixTransform> mPointerTransform{ nullptr }; osg::ref_ptr<HandController> mHandControllers[2];
osg::ref_ptr<osg::MatrixTransform> mWeaponDirectionTransform{ nullptr }; osg::ref_ptr<FingerController> mIndexFingerControllers[2];
osg::ref_ptr<osg::MatrixTransform> mWeaponPointerTransform{ nullptr }; osg::ref_ptr<osg::MatrixTransform> mModelOffset;
MWRender::RayResult mPointerTarget{};
float mDistanceToPointerTarget{ -1.f }; bool mFingerPointingMode{ false };
}; osg::ref_ptr<osg::Geometry> mPointerGeometry{ nullptr };
osg::ref_ptr<osg::MatrixTransform> mPointerRescale{ nullptr };
osg::ref_ptr<osg::MatrixTransform> mPointerTransform{ nullptr };
osg::ref_ptr<osg::MatrixTransform> mWeaponDirectionTransform{ nullptr };
osg::ref_ptr<osg::MatrixTransform> mWeaponPointerTransform{ nullptr };
MWRender::RayResult mPointerTarget{};
float mDistanceToPointerTarget{ -1.f };
};
} }

View file

@ -9,12 +9,12 @@
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
MWVR::Environment *MWVR::Environment::sThis = 0; MWVR::Environment* MWVR::Environment::sThis = 0;
MWVR::Environment::Environment() MWVR::Environment::Environment()
: mSession(nullptr) : mSession(nullptr)
{ {
assert (!sThis); assert(!sThis);
sThis = this; sThis = this;
} }
@ -42,15 +42,15 @@ void MWVR::Environment::cleanup()
MWVR::Environment& MWVR::Environment::get() MWVR::Environment& MWVR::Environment::get()
{ {
assert (sThis); assert(sThis);
return *sThis; return *sThis;
} }
MWVR::OpenXRInputManager* MWVR::Environment::getInputManager() const MWVR::VRInputManager* MWVR::Environment::getInputManager() const
{ {
auto* inputManager = MWBase::Environment::get().getInputManager(); auto* inputManager = MWBase::Environment::get().getInputManager();
assert(inputManager); assert(inputManager);
auto xrInputManager = dynamic_cast<MWVR::OpenXRInputManager*>(inputManager); auto xrInputManager = dynamic_cast<MWVR::VRInputManager*>(inputManager);
assert(xrInputManager); assert(xrInputManager);
return xrInputManager; return xrInputManager;
} }

View file

@ -4,15 +4,15 @@
namespace MWVR namespace MWVR
{ {
class VRAnimation; class VRAnimation;
class OpenXRInputManager; class VRInputManager;
class VRSession; class VRSession;
class VRGUIManager; class VRGUIManager;
class VRViewer; class VRViewer;
class OpenXRManager; class OpenXRManager;
/// \brief Central hub for mw openxr subsystems /// \brief Central hub for mw vr/openxr subsystems
/// ///
/// This class allows each mw openxr subsystem to access any others subsystem's top-level manager class. /// This class allows each mw subsystem to access any vr subsystem's top-level manager class.
/// ///
/// \attention Environment takes ownership of the manager class instances it is handed over in /// \attention Environment takes ownership of the manager class instances it is handed over in
/// the set* functions. /// the set* functions.
@ -39,7 +39,7 @@ namespace MWVR
static Environment& get(); static Environment& get();
///< Return instance of this class. ///< Return instance of this class.
MWVR::OpenXRInputManager* getInputManager() const; MWVR::VRInputManager* getInputManager() const;
// The OpenXRInputManager supplants the regular input manager // The OpenXRInputManager supplants the regular input manager
// which is stored in MWBase::Environment // which is stored in MWBase::Environment

File diff suppressed because it is too large Load diff

View file

@ -17,14 +17,14 @@
namespace MyGUI namespace MyGUI
{ {
class Widget; class Widget;
class Window; class Window;
} }
namespace MWGui namespace MWGui
{ {
class Layout; class Layout;
class WindowBase; class WindowBase;
} }
struct XrCompositionLayerQuad; struct XrCompositionLayerQuad;

View file

@ -10,134 +10,134 @@
namespace MWVR namespace MWVR
{ {
//! Delay before a long-press action is activated (and regular press is discarded) //! Delay before a long-press action is activated (and regular press is discarded)
//! TODO: Make this configurable? //! TODO: Make this configurable?
static std::chrono::milliseconds gActionTime{ 666 }; static std::chrono::milliseconds gActionTime{ 666 };
//! Magnitude above which an axis action is considered active //! Magnitude above which an axis action is considered active
static float gAxisEpsilon{ 0.01f }; static float gAxisEpsilon{ 0.01f };
void HapticsAction::apply(float amplitude) void HapticsAction::apply(float amplitude)
{
mAmplitude = std::max(0.f, std::min(1.f, amplitude));
mXRAction->applyHaptics(XR_NULL_PATH, mAmplitude);
}
PoseAction::PoseAction(std::unique_ptr<OpenXRAction> xrAction)
: mXRAction(std::move(xrAction))
, mXRSpace{ XR_NULL_HANDLE }
{
auto* xr = Environment::get().getManager();
XrActionSpaceCreateInfo createInfo{ XR_TYPE_ACTION_SPACE_CREATE_INFO };
createInfo.action = *mXRAction;
createInfo.poseInActionSpace.orientation.w = 1.f;
createInfo.subactionPath = XR_NULL_PATH;
CHECK_XRCMD(xrCreateActionSpace(xr->impl().xrSession(), &createInfo, &mXRSpace));
}
void PoseAction::update(long long time)
{
mPrevious = mValue;
auto* xr = Environment::get().getManager();
XrSpace referenceSpace = xr->impl().getReferenceSpace(ReferenceSpace::STAGE);
XrSpaceLocation location{ XR_TYPE_SPACE_LOCATION };
XrSpaceVelocity velocity{ XR_TYPE_SPACE_VELOCITY };
location.next = &velocity;
CHECK_XRCMD(xrLocateSpace(mXRSpace, referenceSpace, time, &location));
if (!(location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT))
// Quat must have a magnitude of 1 but openxr sets it to 0 when tracking is unavailable.
// I want a no-track pose to still be a valid quat so osg won't throw errors
location.pose.orientation.w = 1;
mValue = Pose{
fromXR(location.pose.position),
fromXR(location.pose.orientation)
};
}
void Action::updateAndQueue(std::deque<const Action*>& queue)
{
bool old = mActive;
mPrevious = mValue;
update();
bool changed = old != mActive;
mOnActivate = changed && mActive;
mOnDeactivate = changed && !mActive;
if (shouldQueue())
{ {
queue.push_back(this); mAmplitude = std::max(0.f, std::min(1.f, amplitude));
mXRAction->applyHaptics(XR_NULL_PATH, mAmplitude);
} }
}
void ButtonPressAction::update() PoseAction::PoseAction(std::unique_ptr<OpenXRAction> xrAction)
{ : mXRAction(std::move(xrAction))
mActive = false; , mXRSpace{ XR_NULL_HANDLE }
bool old = mPressed;
mXRAction->getBool(0, mPressed);
bool changed = old != mPressed;
if (changed && mPressed)
{ {
mPressTime = std::chrono::steady_clock::now(); auto* xr = Environment::get().getManager();
mTimeout = mPressTime + gActionTime; XrActionSpaceCreateInfo createInfo{ XR_TYPE_ACTION_SPACE_CREATE_INFO };
createInfo.action = *mXRAction;
createInfo.poseInActionSpace.orientation.w = 1.f;
createInfo.subactionPath = XR_NULL_PATH;
CHECK_XRCMD(xrCreateActionSpace(xr->impl().xrSession(), &createInfo, &mXRSpace));
} }
if (changed && !mPressed)
void PoseAction::update(long long time)
{ {
if (std::chrono::steady_clock::now() < mTimeout) mPrevious = mValue;
auto* xr = Environment::get().getManager();
XrSpace referenceSpace = xr->impl().getReferenceSpace(ReferenceSpace::STAGE);
XrSpaceLocation location{ XR_TYPE_SPACE_LOCATION };
XrSpaceVelocity velocity{ XR_TYPE_SPACE_VELOCITY };
location.next = &velocity;
CHECK_XRCMD(xrLocateSpace(mXRSpace, referenceSpace, time, &location));
if (!(location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT))
// Quat must have a magnitude of 1 but openxr sets it to 0 when tracking is unavailable.
// I want a no-track pose to still be a valid quat so osg won't throw errors
location.pose.orientation.w = 1;
mValue = Pose{
fromXR(location.pose.position),
fromXR(location.pose.orientation)
};
}
void Action::updateAndQueue(std::deque<const Action*>& queue)
{
bool old = mActive;
mPrevious = mValue;
update();
bool changed = old != mActive;
mOnActivate = changed && mActive;
mOnDeactivate = changed && !mActive;
if (shouldQueue())
{ {
queue.push_back(this);
}
}
void ButtonPressAction::update()
{
mActive = false;
bool old = mPressed;
mXRAction->getBool(0, mPressed);
bool changed = old != mPressed;
if (changed && mPressed)
{
mPressTime = std::chrono::steady_clock::now();
mTimeout = mPressTime + gActionTime;
}
if (changed && !mPressed)
{
if (std::chrono::steady_clock::now() < mTimeout)
{
mActive = true;
}
}
mValue = mPressed ? 1.f : 0.f;
}
void ButtonLongPressAction::update()
{
mActive = false;
bool old = mPressed;
mXRAction->getBool(0, mPressed);
bool changed = old != mPressed;
if (changed && mPressed)
{
mPressTime = std::chrono::steady_clock::now();
mTimein = mPressTime + gActionTime;
mActivated = false;
}
if (mPressed && !mActivated)
{
if (std::chrono::steady_clock::now() >= mTimein)
{
mActive = mActivated = true;
mValue = 1.f;
}
}
if (changed && !mPressed)
{
mValue = 0.f;
}
}
void ButtonHoldAction::update()
{
mXRAction->getBool(0, mPressed);
mActive = mPressed;
mValue = mPressed ? 1.f : 0.f;
}
void AxisAction::update()
{
mActive = false;
mXRAction->getFloat(0, mValue);
if (std::fabs(mValue) > gAxisEpsilon)
mActive = true; mActive = true;
} else
mValue = 0.f;
} }
mValue = mPressed ? 1.f : 0.f;
}
void ButtonLongPressAction::update()
{
mActive = false;
bool old = mPressed;
mXRAction->getBool(0, mPressed);
bool changed = old != mPressed;
if (changed && mPressed)
{
mPressTime = std::chrono::steady_clock::now();
mTimein = mPressTime + gActionTime;
mActivated = false;
}
if (mPressed && !mActivated)
{
if (std::chrono::steady_clock::now() >= mTimein)
{
mActive = mActivated = true;
mValue = 1.f;
}
}
if (changed && !mPressed)
{
mValue = 0.f;
}
}
void ButtonHoldAction::update()
{
mXRAction->getBool(0, mPressed);
mActive = mPressed;
mValue = mPressed ? 1.f : 0.f;
}
void AxisAction::update()
{
mActive = false;
mXRAction->getFloat(0, mValue);
if (std::fabs(mValue) > gAxisEpsilon)
mActive = true;
else
mValue = 0.f;
}
} }

View file

@ -10,207 +10,218 @@ namespace MWVR
{ {
// The OpenMW input manager iterates from 0 to A_Last in its actions enum. /// Extension of MWInput's set of actions.
// I don't know that it would cause any ill effects, but i nonetheless do not enum VrActions
// want to contaminate the input manager with my OpenXR specific actions. {
// Therefore i add them here to a separate enum whose values start past A_Last. A_VrFirst = MWInput::A_Last + 1,
enum VrActions A_ActivateTouch,
{ A_HapticsLeft,
A_VrFirst = MWInput::A_Last + 1, A_HapticsRight,
A_ActivateTouch, A_HandPoseLeft,
A_HapticsLeft, A_HandPoseRight,
A_HapticsRight, A_MenuUpDown,
A_HandPoseLeft, A_MenuLeftRight,
A_HandPoseRight, A_MenuSelect,
A_MenuUpDown, A_MenuBack,
A_MenuLeftRight, A_Recenter,
A_MenuSelect, A_VrLast
A_MenuBack, };
A_Recenter,
A_VrLast
};
enum class ActionPath /// \brief Enum representation of action paths in openxr.
{ ///
Pose = 0, /// OpenXR allows a lot of generics, consequentially this will most likely be changed/expanded
Haptic, /// in the future as we need it. This set was added based on what the Oculus needed.
Menu, enum class ActionPath
ThumbstickX, {
ThumbstickY, Pose = 0,
ThumbstickClick, Haptic,
Select, Menu,
Squeeze, ThumbstickX,
Trigger, ThumbstickY,
X, ThumbstickClick,
Y, Select,
A, Squeeze,
B, Trigger,
Last X,
}; Y,
A,
B,
Last
};
//class OpenXRAction; /// \brief Suggest a binding by binding an action to a path on a given hand (left or right).
struct SuggestedBindings
{
struct Binding
{
int action;
ActionPath path;
Side side;
};
//! Action for applying haptics std::string controllerPath;
class HapticsAction std::vector<Binding> bindings;
{ };
public:
HapticsAction(std::unique_ptr<OpenXRAction> xrAction) : mXRAction{ std::move(xrAction) } {};
//! Apply vibration at the given amplitude /// \brief Action for applying haptics
void apply(float amplitude); class HapticsAction
{
public:
HapticsAction(std::unique_ptr<OpenXRAction> xrAction) : mXRAction{ std::move(xrAction) } {};
//! Convenience //! Apply vibration at the given amplitude
operator XrAction() { return *mXRAction; } void apply(float amplitude);
private: //! Convenience
std::unique_ptr<OpenXRAction> mXRAction; operator XrAction() { return *mXRAction; }
float mAmplitude{ 0.f };
};
//! Action for capturing tracking information private:
class PoseAction std::unique_ptr<OpenXRAction> mXRAction;
{ float mAmplitude{ 0.f };
public: };
PoseAction(std::unique_ptr<OpenXRAction> xrAction);
//! Current value of an axis or lever action /// \brief Action for capturing tracking information
Pose value() const { return mValue; } class PoseAction
{
public:
PoseAction(std::unique_ptr<OpenXRAction> xrAction);
//! Previous value //! Current value of an axis or lever action
Pose previousValue() const { return mPrevious; } Pose value() const { return mValue; }
//! Convenience //! Previous value
operator XrAction() { return *mXRAction; } Pose previousValue() const { return mPrevious; }
//! Update pose value //! Convenience
void update(long long time); operator XrAction() { return *mXRAction; }
private: //! Update pose value
std::unique_ptr<OpenXRAction> mXRAction; void update(long long time);
XrSpace mXRSpace;
Pose mValue{};
Pose mPrevious{};
};
//! Generic action private:
class Action std::unique_ptr<OpenXRAction> mXRAction;
{ XrSpace mXRSpace;
public: Pose mValue{};
Action(int openMWAction, std::unique_ptr<OpenXRAction> xrAction) : mXRAction(std::move(xrAction)), mOpenMWAction(openMWAction) {} Pose mPrevious{};
virtual ~Action() {}; };
//! True if action changed to being released in the last update /// \brief Generic action
bool isActive() const { return mActive; }; /// \sa ButtonPressAction ButtonLongPressAction ButtonHoldAction AxisAction
class Action
{
public:
Action(int openMWAction, std::unique_ptr<OpenXRAction> xrAction) : mXRAction(std::move(xrAction)), mOpenMWAction(openMWAction) {}
virtual ~Action() {};
//! True if activation turned on this update (i.e. onPress) //! True if action changed to being released in the last update
bool onActivate() const { return mOnActivate; } bool isActive() const { return mActive; };
//! True if activation turned off this update (i.e. onRelease) //! True if activation turned on this update (i.e. onPress)
bool onDeactivate() const { return mOnDeactivate; } bool onActivate() const { return mOnActivate; }
//! OpenMW Action code of this action //! True if activation turned off this update (i.e. onRelease)
int openMWActionCode() const { return mOpenMWAction; } bool onDeactivate() const { return mOnDeactivate; }
//! Current value of an axis or lever action //! OpenMW Action code of this action
float value() const { return mValue; } int openMWActionCode() const { return mOpenMWAction; }
//! Previous value //! Current value of an axis or lever action
float previousValue() const { return mPrevious; } float value() const { return mValue; }
//! Update internal states. Note that subclasses maintain both mValue and mActivate to allow //! Previous value
//! axis and press to subtitute one another. float previousValue() const { return mPrevious; }
virtual void update() = 0;
//! Determines if an action update should be queued //! Update internal states. Note that subclasses maintain both mValue and mActivate to allow
virtual bool shouldQueue() const = 0; //! axis and press to subtitute one another.
virtual void update() = 0;
//! Convenience //! Determines if an action update should be queued
operator XrAction() { return *mXRAction; } virtual bool shouldQueue() const = 0;
//! Update and queue action if applicable //! Convenience
void updateAndQueue(std::deque<const Action*>& queue); operator XrAction() { return *mXRAction; }
protected: //! Update and queue action if applicable
void updateAndQueue(std::deque<const Action*>& queue);
std::unique_ptr<OpenXRAction> mXRAction; protected:
int mOpenMWAction;
float mValue{ 0.f };
float mPrevious{ 0.f };
bool mActive{ false };
bool mOnActivate{ false };
bool mOnDeactivate{ false };
};
//! Convenience std::unique_ptr<OpenXRAction> mXRAction;
using ActionPtr = std::unique_ptr<Action>; int mOpenMWAction;
float mValue{ 0.f };
float mPrevious{ 0.f };
bool mActive{ false };
bool mOnActivate{ false };
bool mOnDeactivate{ false };
};
//! Action that activates once on release. //! Action that activates once on release.
//! Times out if the button is held longer than gHoldDelay. //! Times out if the button is held longer than gHoldDelay.
class ButtonPressAction : public Action class ButtonPressAction : public Action
{ {
public: public:
using Action::Action; using Action::Action;
static const XrActionType ActionType = XR_ACTION_TYPE_BOOLEAN_INPUT; static const XrActionType ActionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
void update() override; void update() override;
virtual bool shouldQueue() const override { return onActivate() || onDeactivate(); } virtual bool shouldQueue() const override { return onActivate() || onDeactivate(); }
bool mPressed{ false }; bool mPressed{ false };
std::chrono::steady_clock::time_point mPressTime{}; std::chrono::steady_clock::time_point mPressTime{};
std::chrono::steady_clock::time_point mTimeout{}; std::chrono::steady_clock::time_point mTimeout{};
}; };
//! Action that activates once on a long press //! Action that activates once on a long press
//! The press time is the same as the timeout for a regular press, allowing keys with double roles. //! The press time is the same as the timeout for a regular press, allowing keys with double roles.
class ButtonLongPressAction : public Action class ButtonLongPressAction : public Action
{ {
public: public:
using Action::Action; using Action::Action;
static const XrActionType ActionType = XR_ACTION_TYPE_BOOLEAN_INPUT; static const XrActionType ActionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
void update() override; void update() override;
virtual bool shouldQueue() const override { return onActivate() || onDeactivate(); } virtual bool shouldQueue() const override { return onActivate() || onDeactivate(); }
bool mPressed{ false }; bool mPressed{ false };
bool mActivated{ false }; bool mActivated{ false };
std::chrono::steady_clock::time_point mPressTime{}; std::chrono::steady_clock::time_point mPressTime{};
std::chrono::steady_clock::time_point mTimein{}; std::chrono::steady_clock::time_point mTimein{};
}; };
//! Action that is active whenever the button is pressed down. //! Action that is active whenever the button is pressed down.
//! Useful for e.g. non-toggling sneak and automatically repeating actions //! Useful for e.g. non-toggling sneak and automatically repeating actions
class ButtonHoldAction : public Action class ButtonHoldAction : public Action
{ {
public: public:
using Action::Action; using Action::Action;
static const XrActionType ActionType = XR_ACTION_TYPE_BOOLEAN_INPUT; static const XrActionType ActionType = XR_ACTION_TYPE_BOOLEAN_INPUT;
void update() override; void update() override;
virtual bool shouldQueue() const override { return mActive || onDeactivate(); } virtual bool shouldQueue() const override { return mActive || onDeactivate(); }
bool mPressed{ false }; bool mPressed{ false };
}; };
//! Action for axis actions, such as thumbstick axes or certain triggers/squeeze levers. //! Action for axis actions, such as thumbstick axes or certain triggers/squeeze levers.
//! Float axis are considered active whenever their magnitude is greater than gAxisEpsilon. This is useful //! Float axis are considered active whenever their magnitude is greater than gAxisEpsilon. This is useful
//! as a touch subtitute on levers without touch. //! as a touch subtitute on levers without touch.
class AxisAction : public Action class AxisAction : public Action
{ {
public: public:
using Action::Action; using Action::Action;
static const XrActionType ActionType = XR_ACTION_TYPE_FLOAT_INPUT; static const XrActionType ActionType = XR_ACTION_TYPE_FLOAT_INPUT;
void update() override; void update() override;
virtual bool shouldQueue() const override { return mActive || onDeactivate(); } virtual bool shouldQueue() const override { return mActive || onDeactivate(); }
}; };
} }
#endif #endif

File diff suppressed because it is too large Load diff

View file

@ -13,69 +13,69 @@
namespace MWVR namespace MWVR
{ {
struct OpenXRInput; struct OpenXRInput;
/// As far as I can tell, SDL does not support VR controllers. /// As far as I can tell, SDL does not support VR controllers.
/// So I subclass the input manager and insert VR controls. /// So I subclass the input manager and insert VR controls.
class OpenXRInputManager : public MWInput::InputManager class VRInputManager : public MWInput::InputManager
{ {
public: public:
OpenXRInputManager( VRInputManager(
SDL_Window* window, SDL_Window* window,
osg::ref_ptr<osgViewer::Viewer> viewer, osg::ref_ptr<osgViewer::Viewer> viewer,
osg::ref_ptr<osgViewer::ScreenCaptureHandler> screenCaptureHandler, osg::ref_ptr<osgViewer::ScreenCaptureHandler> screenCaptureHandler,
osgViewer::ScreenCaptureHandler::CaptureOperation* screenCaptureOperation, osgViewer::ScreenCaptureHandler::CaptureOperation* screenCaptureOperation,
const std::string& userFile, bool userFileExists, const std::string& userFile, bool userFileExists,
const std::string& userControllerBindingsFile, const std::string& userControllerBindingsFile,
const std::string& controllerBindingsFile, bool grab); const std::string& controllerBindingsFile, bool grab);
virtual ~OpenXRInputManager(); virtual ~VRInputManager();
/// Overriden to force vr modes such as hiding cursors and crosshairs /// Overriden to force vr modes such as hiding cursors and crosshairs
virtual void changeInputMode(bool guiMode); virtual void changeInputMode(bool guiMode);
/// Overriden to update XR inputs /// Overriden to update XR inputs
virtual void update(float dt, bool disableControls = false, bool disableEvents = false); virtual void update(float dt, bool disableControls = false, bool disableEvents = false);
/// Current head offset from character position /// Current head offset from character position
osg::Vec3 headOffset() const { return mHeadOffset; }; osg::Vec3 headOffset() const { return mHeadOffset; };
/// Update head offset. Should only be called by the movement solver when reducing head offset. /// Update head offset. Should only be called by the movement solver when reducing head offset.
void setHeadOffset(osg::Vec3 offset) { mHeadOffset = offset; }; void setHeadOffset(osg::Vec3 offset) { mHeadOffset = offset; };
/// Quaternion that aligns VR stage coordinates with world coordinates. /// Quaternion that aligns VR stage coordinates with world coordinates.
osg::Quat stageRotation(); osg::Quat stageRotation();
/// Set current offset to 0 and re-align VR stage. /// Set current offset to 0 and re-align VR stage.
void requestRecenter(); void requestRecenter();
/// Tracking pose of the given limb at the given predicted time /// Tracking pose of the given limb at the given predicted time
Pose getLimbPose(int64_t time, TrackedLimb limb); Pose getLimbPose(int64_t time, TrackedLimb limb);
protected: protected:
void updateHead(); void updateHead();
void processAction(const class Action* action, float dt, bool disableControls); void processAction(const class Action* action, float dt, bool disableControls);
void updateActivationIndication(void); void updateActivationIndication(void);
void pointActivation(bool onPress); void pointActivation(bool onPress);
void injectMousePress(int sdlButton, bool onPress); void injectMousePress(int sdlButton, bool onPress);
void injectChannelValue(MWInput::Actions action, float value); void injectChannelValue(MWInput::Actions action, float value);
void applyHapticsLeftHand(float intensity) override; void applyHapticsLeftHand(float intensity) override;
void applyHapticsRightHand(float intensity) override; void applyHapticsRightHand(float intensity) override;
std::unique_ptr<OpenXRInput> mXRInput; std::unique_ptr<OpenXRInput> mXRInput;
std::unique_ptr<RealisticCombat::StateMachine> mRealisticCombat; std::unique_ptr<RealisticCombat::StateMachine> mRealisticCombat;
Pose mHeadPose{}; Pose mHeadPose{};
osg::Vec3 mHeadOffset{ 0,0,0 }; osg::Vec3 mHeadOffset{ 0,0,0 };
bool mShouldRecenter{ true }; bool mShouldRecenter{ true };
bool mActivationIndication{ false }; bool mActivationIndication{ false };
float mYaw{ 0.f }; float mYaw{ 0.f };
float mVrAngles[3]{ 0.f,0.f,0.f }; float mVrAngles[3]{ 0.f,0.f,0.f };
}; };
} }
#endif #endif

View file

@ -36,323 +36,323 @@
namespace MWVR namespace MWVR
{ {
VRSession::VRSession() VRSession::VRSession()
{
}
VRSession::~VRSession()
{
}
osg::Matrix VRSession::projectionMatrix(FramePhase phase, Side side)
{
assert(((int)side) < 2);
auto fov = predictedPoses(VRSession::FramePhase::Update).view[(int)side].fov;
float near_ = Settings::Manager::getFloat("near clip", "Camera");
float far_ = Settings::Manager::getFloat("viewing distance", "Camera");
return fov.perspectiveMatrix(near_, far_);
}
osg::Matrix VRSession::viewMatrix(FramePhase phase, Side side)
{
MWVR::Pose pose{};
pose = predictedPoses(phase).view[(int)side].pose;
if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame)
{ {
pose = predictedPoses(phase).eye[(int)side]; }
VRSession::~VRSession()
{
}
osg::Matrix VRSession::projectionMatrix(FramePhase phase, Side side)
{
assert(((int)side) < 2);
auto fov = predictedPoses(VRSession::FramePhase::Update).view[(int)side].fov;
float near_ = Settings::Manager::getFloat("near clip", "Camera");
float far_ = Settings::Manager::getFloat("viewing distance", "Camera");
return fov.perspectiveMatrix(near_, far_);
}
osg::Matrix VRSession::viewMatrix(FramePhase phase, Side side)
{
MWVR::Pose pose{};
pose = predictedPoses(phase).view[(int)side].pose;
if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame)
{
pose = predictedPoses(phase).eye[(int)side];
osg::Vec3 position = pose.position * Environment::get().unitsPerMeter();
osg::Quat orientation = pose.orientation;
osg::Vec3d forward = orientation * osg::Vec3d(0, 1, 0);
osg::Vec3d up = orientation * osg::Vec3d(0, 0, 1);
osg::Matrix viewMatrix;
viewMatrix.makeLookAt(position, position + forward, up);
return viewMatrix;
}
osg::Vec3 position = pose.position * Environment::get().unitsPerMeter(); osg::Vec3 position = pose.position * Environment::get().unitsPerMeter();
osg::Quat orientation = pose.orientation; osg::Quat orientation = pose.orientation;
osg::Vec3d forward = orientation * osg::Vec3d(0, 1, 0);
osg::Vec3d up = orientation * osg::Vec3d(0, 0, 1); float y = position.y();
float z = position.z();
position.y() = z;
position.z() = -y;
y = orientation.y();
z = orientation.z();
orientation.y() = z;
orientation.z() = -y;
osg::Matrix viewMatrix; osg::Matrix viewMatrix;
viewMatrix.makeLookAt(position, position + forward, up); viewMatrix.setTrans(-position);
viewMatrix.postMultRotate(orientation.conj());
return viewMatrix; return viewMatrix;
} }
osg::Vec3 position = pose.position * Environment::get().unitsPerMeter(); bool VRSession::isRunning() const {
osg::Quat orientation = pose.orientation; auto* xr = Environment::get().getManager();
return xr->xrSessionRunning();
float y = position.y();
float z = position.z();
position.y() = z;
position.z() = -y;
y = orientation.y();
z = orientation.z();
orientation.y() = z;
orientation.z() = -y;
osg::Matrix viewMatrix;
viewMatrix.setTrans(-position);
viewMatrix.postMultRotate(orientation.conj());
return viewMatrix;
}
bool VRSession::isRunning() const {
auto* xr = Environment::get().getManager();
return xr->xrSessionRunning();
}
void VRSession::swapBuffers(osg::GraphicsContext* gc, VRViewer& viewer)
{
auto* xr = Environment::get().getManager();
beginPhase(FramePhase::Swap);
auto* frameMeta = getFrame(FramePhase::Swap).get();
if (frameMeta->mShouldRender && isRunning())
{
auto leftView = viewer.mViews["LeftEye"];
auto rightView = viewer.mViews["RightEye"];
viewer.blitEyesToMirrorTexture(gc);
gc->swapBuffersImplementation();
leftView->swapBuffers(gc);
rightView->swapBuffers(gc);
std::array<CompositionLayerProjectionView, 2> layerStack{};
layerStack[(int)Side::LEFT_SIDE].swapchain = &leftView->swapchain();
layerStack[(int)Side::RIGHT_SIDE].swapchain = &rightView->swapchain();
layerStack[(int)Side::LEFT_SIDE].pose = frameMeta->mPredictedPoses.eye[(int)Side::LEFT_SIDE] / mPlayerScale;
layerStack[(int)Side::RIGHT_SIDE].pose = frameMeta->mPredictedPoses.eye[(int)Side::RIGHT_SIDE] / mPlayerScale;
layerStack[(int)Side::LEFT_SIDE].fov = frameMeta->mPredictedPoses.view[(int)Side::LEFT_SIDE].fov;
layerStack[(int)Side::RIGHT_SIDE].fov = frameMeta->mPredictedPoses.view[(int)Side::RIGHT_SIDE].fov;
Log(Debug::Debug) << frameMeta->mFrameNo << ": EndFrame " <<std::this_thread::get_id();
xr->endFrame(frameMeta->mPredictedDisplayTime, 1, layerStack);
} }
void VRSession::swapBuffers(osg::GraphicsContext* gc, VRViewer& viewer)
{ {
std::unique_lock<std::mutex> lock(mMutex); auto* xr = Environment::get().getManager();
// Some of these values are useless until the prediction time bug is resolved by oculus. beginPhase(FramePhase::Swap);
auto now = std::chrono::steady_clock::now();
mLastFrameInterval = std::chrono::duration_cast<std::chrono::nanoseconds>(now - mLastRenderedFrameTimestamp);
mLastRenderedFrameTimestamp = now;
mLastRenderedFrame = getFrame(FramePhase::Swap)->mFrameNo;
// Using this to track framerate over the course of gameplay, rather than just seeing the instantaneous auto* frameMeta = getFrame(FramePhase::Swap).get();
auto seconds = std::chrono::duration_cast<std::chrono::duration<double>>(now - mStart).count();
static int sBaseFrames = 0; if (frameMeta->mShouldRender && isRunning())
if (seconds > 10.f)
{ {
Log(Debug::Verbose) << "Fps: " << (static_cast<double>(mLastRenderedFrame - sBaseFrames) / seconds); auto leftView = viewer.getView("LeftEye");
mStart = now; auto rightView = viewer.getView("RightEye");
sBaseFrames = mLastRenderedFrame;
viewer.blitEyesToMirrorTexture(gc);
gc->swapBuffersImplementation();
leftView->swapBuffers(gc);
rightView->swapBuffers(gc);
std::array<CompositionLayerProjectionView, 2> layerStack{};
layerStack[(int)Side::LEFT_SIDE].swapchain = &leftView->swapchain();
layerStack[(int)Side::RIGHT_SIDE].swapchain = &rightView->swapchain();
layerStack[(int)Side::LEFT_SIDE].pose = frameMeta->mPredictedPoses.eye[(int)Side::LEFT_SIDE] / mPlayerScale;
layerStack[(int)Side::RIGHT_SIDE].pose = frameMeta->mPredictedPoses.eye[(int)Side::RIGHT_SIDE] / mPlayerScale;
layerStack[(int)Side::LEFT_SIDE].fov = frameMeta->mPredictedPoses.view[(int)Side::LEFT_SIDE].fov;
layerStack[(int)Side::RIGHT_SIDE].fov = frameMeta->mPredictedPoses.view[(int)Side::RIGHT_SIDE].fov;
Log(Debug::Debug) << frameMeta->mFrameNo << ": EndFrame " << std::this_thread::get_id();
xr->endFrame(frameMeta->mPredictedDisplayTime, 1, layerStack);
} }
getFrame(FramePhase::Swap) = nullptr;
mFramesInFlight--;
}
mCondition.notify_one();
}
void VRSession::beginPhase(FramePhase phase)
{
Log(Debug::Debug) << "beginPhase(" << ((int)phase) << ") " << std::this_thread::get_id();
if (getFrame(phase))
{
Log(Debug::Warning) << "advanceFramePhase called with a frame alreay in the target phase";
return;
}
if (phase == FramePhase::Update)
{
prepareFrame();
}
else
{
std::unique_lock<std::mutex> lock(mMutex);
FramePhase previousPhase = static_cast<FramePhase>((int)phase - 1);
if (!getFrame(previousPhase))
throw std::logic_error("beginPhase called without a frame");
getFrame(phase) = std::move(getFrame(previousPhase));
}
// TODO: Invokation should depend on earliest render rather than necessarily phase.
// Specifically. Without shadows this is fine because nothing is being rendered
// during cull or earlier.
// Thought: Add an Shadowmapping phase and invoke it from the shadow code
// But with shadows rendering occurs during cull and we must do frame sync before those calls.
// If you want to pay the FPS toll and play with shadows, change FramePhase::Draw to FramePhase::Cull or enjoy your eyes getting torn apart by jitters.
if (phase == FramePhase::Cull && getFrame(phase)->mShouldRender)
doFrameSync();
}
void VRSession::doFrameSync()
{
{
std::unique_lock<std::mutex> lock(mMutex);
while (mLastRenderedFrame != mFrames - 1)
{ {
mCondition.wait(lock); std::unique_lock<std::mutex> lock(mMutex);
// Some of these values are useless until the prediction time bug is resolved by oculus.
auto now = std::chrono::steady_clock::now();
mLastFrameInterval = std::chrono::duration_cast<std::chrono::nanoseconds>(now - mLastRenderedFrameTimestamp);
mLastRenderedFrameTimestamp = now;
mLastRenderedFrame = getFrame(FramePhase::Swap)->mFrameNo;
// Using this to track framerate over the course of gameplay, rather than just seeing the instantaneous
auto seconds = std::chrono::duration_cast<std::chrono::duration<double>>(now - mStart).count();
static int sBaseFrames = 0;
if (seconds > 10.f)
{
Log(Debug::Verbose) << "Fps: " << (static_cast<double>(mLastRenderedFrame - sBaseFrames) / seconds);
mStart = now;
sBaseFrames = mLastRenderedFrame;
}
getFrame(FramePhase::Swap) = nullptr;
mFramesInFlight--;
} }
mCondition.notify_one();
} }
auto* xr = Environment::get().getManager(); void VRSession::beginPhase(FramePhase phase)
Log(Debug::Debug) << mFrames << ": WaitFrame " << std::this_thread::get_id();
xr->waitFrame();
Log(Debug::Debug) << mFrames << ": BeginFrame " << std::this_thread::get_id();
xr->beginFrame();
}
std::unique_ptr<VRSession::VRFrameMeta>& VRSession::getFrame(FramePhase phase)
{
if ((unsigned int)phase >= mFrame.size())
throw std::logic_error("Invalid frame phase");
return mFrame[(int)phase];
}
void VRSession::prepareFrame()
{
std::unique_lock<std::mutex> lock(mMutex);
mFrames++;
assert(!mPredrawFrame);
auto* xr = Environment::get().getManager();
xr->handleEvents();
//auto frameState = xr->impl().frameState();
// auto predictedDisplayTime = frameState.predictedDisplayTime;
// if (predictedDisplayTime == 0)
// {
// // First time, need to invent a frame time since openxr won't help us without a call to waitframe.
// predictedDisplayTime = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
// }
// else
// {
// // Predict display time based on real framerate
// float intervalsf = static_cast<double>(mLastFrameInterval.count()) / static_cast<double>(mLastPredictedDisplayPeriod);
// int intervals = std::max((int)std::roundf(intervalsf), 1);
// predictedDisplayTime = mLastPredictedDisplayTime + intervals * (mFrames - mLastRenderedFrame) * mLastPredictedDisplayPeriod;
// }
//////////////////////// OCULUS BUG
//////////////////// Oculus will suddenly start monotonically increasing their predicted display time by precisely 1 second
//////////////////// regardless of real time passed, causing predictions to go crazy due to the time difference.
//////////////////// Therefore, for the time being, i ignore oculus' predicted display time altogether.
long long predictedDisplayTime = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
if (mFrames > 1)
{ {
auto framePeriod = xr->getLastPredictedDisplayPeriod(); Log(Debug::Debug) << "beginPhase(" << ((int)phase) << ") " << std::this_thread::get_id();
float intervalsf = static_cast<double>(mLastFrameInterval.count()) / static_cast<double>(framePeriod);
int intervals = std::max((int)std::roundf(intervalsf), 1); if (getFrame(phase))
predictedDisplayTime = predictedDisplayTime + intervals * (mFrames - mLastRenderedFrame) * framePeriod; {
Log(Debug::Warning) << "advanceFramePhase called with a frame alreay in the target phase";
return;
}
if (phase == FramePhase::Update)
{
prepareFrame();
}
else
{
std::unique_lock<std::mutex> lock(mMutex);
FramePhase previousPhase = static_cast<FramePhase>((int)phase - 1);
if (!getFrame(previousPhase))
throw std::logic_error("beginPhase called without a frame");
getFrame(phase) = std::move(getFrame(previousPhase));
}
// TODO: Invokation should depend on earliest render rather than necessarily phase.
// Specifically. Without shadows this is fine because nothing is being rendered
// during cull or earlier.
// Thought: Add an Shadowmapping phase and invoke it from the shadow code
// But with shadows rendering occurs during cull and we must do frame sync before those calls.
// If you want to pay the FPS toll and play with shadows, change FramePhase::Draw to FramePhase::Cull or enjoy your eyes getting torn apart by jitters.
if (phase == FramePhase::Cull && getFrame(phase)->mShouldRender)
doFrameSync();
} }
void VRSession::doFrameSync()
PoseSet predictedPoses{};
xr->enablePredictions();
predictedPoses.head = xr->getPredictedHeadPose(predictedDisplayTime, ReferenceSpace::STAGE) * mPlayerScale;
auto hmdViews = xr->getPredictedViews(predictedDisplayTime, ReferenceSpace::VIEW);
predictedPoses.view[(int)Side::LEFT_SIDE].pose = hmdViews[(int)Side::LEFT_SIDE].pose * mPlayerScale;
predictedPoses.view[(int)Side::RIGHT_SIDE].pose = hmdViews[(int)Side::RIGHT_SIDE].pose * mPlayerScale;
predictedPoses.view[(int)Side::LEFT_SIDE].fov = hmdViews[(int)Side::LEFT_SIDE].fov;
predictedPoses.view[(int)Side::RIGHT_SIDE].fov = hmdViews[(int)Side::RIGHT_SIDE].fov;
auto stageViews = xr->getPredictedViews(predictedDisplayTime, ReferenceSpace::STAGE);
predictedPoses.eye[(int)Side::LEFT_SIDE] = stageViews[(int)Side::LEFT_SIDE].pose * mPlayerScale;
predictedPoses.eye[(int)Side::RIGHT_SIDE] = stageViews[(int)Side::RIGHT_SIDE].pose * mPlayerScale;
auto* input = Environment::get().getInputManager();
if (input)
{ {
predictedPoses.hands[(int)Side::LEFT_SIDE] = input->getLimbPose(predictedDisplayTime, TrackedLimb::LEFT_HAND) * mPlayerScale; {
predictedPoses.hands[(int)Side::RIGHT_SIDE] = input->getLimbPose(predictedDisplayTime, TrackedLimb::RIGHT_HAND) * mPlayerScale; std::unique_lock<std::mutex> lock(mMutex);
while (mLastRenderedFrame != mFrames - 1)
{
mCondition.wait(lock);
}
}
auto* xr = Environment::get().getManager();
Log(Debug::Debug) << mFrames << ": WaitFrame " << std::this_thread::get_id();
xr->waitFrame();
Log(Debug::Debug) << mFrames << ": BeginFrame " << std::this_thread::get_id();
xr->beginFrame();
} }
xr->disablePredictions();
auto& frame = getFrame(FramePhase::Update); std::unique_ptr<VRSession::VRFrameMeta>& VRSession::getFrame(FramePhase phase)
frame.reset(new VRFrameMeta);
frame->mPredictedDisplayTime = predictedDisplayTime;
frame->mFrameNo = mFrames;
frame->mPredictedPoses = predictedPoses;
frame->mShouldRender = isRunning();
mFramesInFlight++;
}
const PoseSet& VRSession::predictedPoses(FramePhase phase)
{
auto& frame = getFrame(phase);
if (phase == FramePhase::Update && !frame)
beginPhase(FramePhase::Update);
if (!frame)
throw std::logic_error("Attempted to get poses from a phase with no current pose");
return frame->mPredictedPoses;
}
// OSG doesn't provide API to extract euler angles from a quat, but i need it.
// Credits goes to Dennis Bunfield, i just copied his formula https://narkive.com/v0re6547.4
void getEulerAngles(const osg::Quat& quat, float& yaw, float& pitch, float& roll)
{
// Now do the computation
osg::Matrixd m2(osg::Matrixd::rotate(quat));
double* mat = (double*)m2.ptr();
double angle_x = 0.0;
double angle_y = 0.0;
double angle_z = 0.0;
double D, C, tr_x, tr_y;
angle_y = D = asin(mat[2]); /* Calculate Y-axis angle */
C = cos(angle_y);
if (fabs(C) > 0.005) /* Test for Gimball lock? */
{ {
tr_x = mat[10] / C; /* No, so get X-axis angle */ if ((unsigned int)phase >= mFrame.size())
tr_y = -mat[6] / C; throw std::logic_error("Invalid frame phase");
angle_x = atan2(tr_y, tr_x); return mFrame[(int)phase];
tr_x = mat[0] / C; /* Get Z-axis angle */
tr_y = -mat[1] / C;
angle_z = atan2(tr_y, tr_x);
} }
else /* Gimball lock has occurred */
void VRSession::prepareFrame()
{ {
angle_x = 0; /* Set X-axis angle to zero
*/ std::unique_lock<std::mutex> lock(mMutex);
tr_x = mat[5]; /* And calculate Z-axis angle mFrames++;
*/ assert(!mPredrawFrame);
tr_y = mat[4];
angle_z = atan2(tr_y, tr_x); auto* xr = Environment::get().getManager();
xr->handleEvents();
//auto frameState = xr->impl().frameState();
// auto predictedDisplayTime = frameState.predictedDisplayTime;
// if (predictedDisplayTime == 0)
// {
// // First time, need to invent a frame time since openxr won't help us without a call to waitframe.
// predictedDisplayTime = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
// }
// else
// {
// // Predict display time based on real framerate
// float intervalsf = static_cast<double>(mLastFrameInterval.count()) / static_cast<double>(mLastPredictedDisplayPeriod);
// int intervals = std::max((int)std::roundf(intervalsf), 1);
// predictedDisplayTime = mLastPredictedDisplayTime + intervals * (mFrames - mLastRenderedFrame) * mLastPredictedDisplayPeriod;
// }
//////////////////////// OCULUS BUG
//////////////////// Oculus will suddenly start monotonically increasing their predicted display time by precisely 1 second
//////////////////// regardless of real time passed, causing predictions to go crazy due to the time difference.
//////////////////// Therefore, for the time being, i ignore oculus' predicted display time altogether.
long long predictedDisplayTime = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
if (mFrames > 1)
{
auto framePeriod = xr->getLastPredictedDisplayPeriod();
float intervalsf = static_cast<double>(mLastFrameInterval.count()) / static_cast<double>(framePeriod);
int intervals = std::max((int)std::roundf(intervalsf), 1);
predictedDisplayTime = predictedDisplayTime + intervals * (mFrames - mLastRenderedFrame) * framePeriod;
}
PoseSet predictedPoses{};
xr->enablePredictions();
predictedPoses.head = xr->getPredictedHeadPose(predictedDisplayTime, ReferenceSpace::STAGE) * mPlayerScale;
auto hmdViews = xr->getPredictedViews(predictedDisplayTime, ReferenceSpace::VIEW);
predictedPoses.view[(int)Side::LEFT_SIDE].pose = hmdViews[(int)Side::LEFT_SIDE].pose * mPlayerScale;
predictedPoses.view[(int)Side::RIGHT_SIDE].pose = hmdViews[(int)Side::RIGHT_SIDE].pose * mPlayerScale;
predictedPoses.view[(int)Side::LEFT_SIDE].fov = hmdViews[(int)Side::LEFT_SIDE].fov;
predictedPoses.view[(int)Side::RIGHT_SIDE].fov = hmdViews[(int)Side::RIGHT_SIDE].fov;
auto stageViews = xr->getPredictedViews(predictedDisplayTime, ReferenceSpace::STAGE);
predictedPoses.eye[(int)Side::LEFT_SIDE] = stageViews[(int)Side::LEFT_SIDE].pose * mPlayerScale;
predictedPoses.eye[(int)Side::RIGHT_SIDE] = stageViews[(int)Side::RIGHT_SIDE].pose * mPlayerScale;
auto* input = Environment::get().getInputManager();
if (input)
{
predictedPoses.hands[(int)Side::LEFT_SIDE] = input->getLimbPose(predictedDisplayTime, TrackedLimb::LEFT_HAND) * mPlayerScale;
predictedPoses.hands[(int)Side::RIGHT_SIDE] = input->getLimbPose(predictedDisplayTime, TrackedLimb::RIGHT_HAND) * mPlayerScale;
}
xr->disablePredictions();
auto& frame = getFrame(FramePhase::Update);
frame.reset(new VRFrameMeta);
frame->mPredictedDisplayTime = predictedDisplayTime;
frame->mFrameNo = mFrames;
frame->mPredictedPoses = predictedPoses;
frame->mShouldRender = isRunning();
mFramesInFlight++;
} }
yaw = angle_z; const PoseSet& VRSession::predictedPoses(FramePhase phase)
pitch = angle_x; {
roll = angle_y; auto& frame = getFrame(phase);
}
void VRSession::movementAngles(float& yaw, float& pitch) if (phase == FramePhase::Update && !frame)
{ beginPhase(FramePhase::Update);
assert(mPredrawFrame);
if (!getFrame(FramePhase::Update))
beginPhase(FramePhase::Update);
auto frameMeta = getFrame(FramePhase::Update).get();
float headYaw = 0.f;
float headPitch = 0.f;
float headsWillRoll = 0.f;
float handYaw = 0.f; if (!frame)
float handPitch = 0.f; throw std::logic_error("Attempted to get poses from a phase with no current pose");
float handRoll = 0.f; return frame->mPredictedPoses;
}
getEulerAngles(frameMeta->mPredictedPoses.head.orientation, headYaw, headPitch, headsWillRoll); // OSG doesn't provide API to extract euler angles from a quat, but i need it.
getEulerAngles(frameMeta->mPredictedPoses.hands[(int)Side::LEFT_SIDE].orientation, handYaw, handPitch, handRoll); // Credits goes to Dennis Bunfield, i just copied his formula https://narkive.com/v0re6547.4
void getEulerAngles(const osg::Quat& quat, float& yaw, float& pitch, float& roll)
{
// Now do the computation
osg::Matrixd m2(osg::Matrixd::rotate(quat));
double* mat = (double*)m2.ptr();
double angle_x = 0.0;
double angle_y = 0.0;
double angle_z = 0.0;
double D, C, tr_x, tr_y;
angle_y = D = asin(mat[2]); /* Calculate Y-axis angle */
C = cos(angle_y);
if (fabs(C) > 0.005) /* Test for Gimball lock? */
{
tr_x = mat[10] / C; /* No, so get X-axis angle */
tr_y = -mat[6] / C;
angle_x = atan2(tr_y, tr_x);
tr_x = mat[0] / C; /* Get Z-axis angle */
tr_y = -mat[1] / C;
angle_z = atan2(tr_y, tr_x);
}
else /* Gimball lock has occurred */
{
angle_x = 0; /* Set X-axis angle to zero
*/
tr_x = mat[5]; /* And calculate Z-axis angle
*/
tr_y = mat[4];
angle_z = atan2(tr_y, tr_x);
}
//Log(Debug::Verbose) << "lhandViewYaw=" << yaw << ", headYaw=" << headYaw << ", handYaw=" << handYaw << ", diff=" << (handYaw - headYaw); yaw = angle_z;
//Log(Debug::Verbose) << "lhandViewPitch=" << pitch << ", headPitch=" << headPitch << ", handPitch=" << handPitch << ", diff=" << (handPitch - headPitch); pitch = angle_x;
yaw = handYaw - headYaw; roll = angle_y;
pitch = handPitch - headPitch; }
}
void VRSession::movementAngles(float& yaw, float& pitch)
{
assert(mPredrawFrame);
if (!getFrame(FramePhase::Update))
beginPhase(FramePhase::Update);
auto frameMeta = getFrame(FramePhase::Update).get();
float headYaw = 0.f;
float headPitch = 0.f;
float headsWillRoll = 0.f;
float handYaw = 0.f;
float handPitch = 0.f;
float handRoll = 0.f;
getEulerAngles(frameMeta->mPredictedPoses.head.orientation, headYaw, headPitch, headsWillRoll);
getEulerAngles(frameMeta->mPredictedPoses.hands[(int)Side::LEFT_SIDE].orientation, handYaw, handPitch, handRoll);
//Log(Debug::Verbose) << "lhandViewYaw=" << yaw << ", headYaw=" << headYaw << ", handYaw=" << handYaw << ", diff=" << (handYaw - headYaw);
//Log(Debug::Verbose) << "lhandViewPitch=" << pitch << ", headPitch=" << headPitch << ", handPitch=" << handPitch << ", diff=" << (handPitch - headPitch);
yaw = handYaw - headYaw;
pitch = handPitch - headPitch;
}
} }

View file

@ -15,84 +15,85 @@
namespace MWVR namespace MWVR
{ {
extern void getEulerAngles(const osg::Quat& quat, float& yaw, float& pitch, float& roll); extern void getEulerAngles(const osg::Quat& quat, float& yaw, float& pitch, float& roll);
//! Manages VR logic, such as managing frames, predicting their poses, and handling frame synchronization. /// \brief Manages VR logic, such as managing frames, predicting their poses, and handling frame synchronization with the VR runtime.
//! Should not be confused with the openxr session object. /// Should not be confused with the openxr session object.
class VRSession class VRSession
{
public:
using seconds = std::chrono::duration<double>;
using nanoseconds = std::chrono::nanoseconds;
using clock = std::chrono::steady_clock;
using time_point = clock::time_point;
//! Describes different phases of updating/rendering each frame.
//! While two phases suffice for disambiguating current and next frame,
//! greater granularity allows better control of synchronization
//! with the VR runtime.
enum class FramePhase
{ {
Update = 0, //!< The frame currently in update traversals public:
Cull, //!< The frame currently in cull using seconds = std::chrono::duration<double>;
Draw, //!< The frame currently in draw using nanoseconds = std::chrono::nanoseconds;
Swap, //!< The frame being swapped using clock = std::chrono::steady_clock;
NumPhases using time_point = clock::time_point;
//! Describes different phases of updating/rendering each frame.
//! While two phases suffice for disambiguating current and next frame,
//! greater granularity allows better control of synchronization
//! with the VR runtime.
enum class FramePhase
{
Update = 0, //!< The frame currently in update traversals
Cull, //!< The frame currently in cull
Draw, //!< The frame currently in draw
Swap, //!< The frame being swapped
NumPhases
};
struct VRFrameMeta
{
long long mFrameNo{ 0 };
long long mPredictedDisplayTime{ 0 };
PoseSet mPredictedPoses{};
bool mShouldRender{ false };
};
public:
VRSession();
~VRSession();
void swapBuffers(osg::GraphicsContext* gc, VRViewer& viewer);
const PoseSet& predictedPoses(FramePhase phase);
//! Starts a new frame
void prepareFrame();
//! Synchronize with openxr
void doFrameSync();
//! Angles to be used for overriding movement direction
void movementAngles(float& yaw, float& pitch);
void beginPhase(FramePhase phase);
std::unique_ptr<VRFrameMeta>& getFrame(FramePhase phase);
bool isRunning() const;
float playerScale() const { return mPlayerScale; }
float setPlayerScale(float scale) { return mPlayerScale = scale; }
osg::Matrix viewMatrix(FramePhase phase, Side side);
osg::Matrix projectionMatrix(FramePhase phase, Side side);
int mFramesInFlight{ 0 };
std::array<std::unique_ptr<VRFrameMeta>, (int)FramePhase::NumPhases> mFrame{ nullptr };
private:
std::mutex mMutex{};
std::condition_variable mCondition{};
long long mFrames{ 0 };
long long mLastRenderedFrame{ 0 };
long long mLastPredictedDisplayTime{ 0 };
long long mLastPredictedDisplayPeriod{ 0 };
std::chrono::steady_clock::time_point mStart{ std::chrono::steady_clock::now() };
std::chrono::nanoseconds mLastFrameInterval{};
std::chrono::steady_clock::time_point mLastRenderedFrameTimestamp{ std::chrono::steady_clock::now() };
float mPlayerScale{ 1.f };
}; };
struct VRFrameMeta
{
long long mFrameNo{ 0 };
long long mPredictedDisplayTime{ 0 };
PoseSet mPredictedPoses{};
bool mShouldRender{ false };
};
public:
VRSession();
~VRSession();
void swapBuffers(osg::GraphicsContext* gc, VRViewer& viewer);
const PoseSet& predictedPoses(FramePhase phase);
//! Starts a new frame
void prepareFrame();
//! Synchronize with openxr
void doFrameSync();
//! Angles to be used for overriding movement direction
void movementAngles(float& yaw, float& pitch);
void beginPhase(FramePhase phase);
std::unique_ptr<VRFrameMeta>& getFrame(FramePhase phase);
bool isRunning() const;
float playerScale() const { return mPlayerScale; }
float setPlayerScale(float scale) { return mPlayerScale = scale; }
osg::Matrix viewMatrix(FramePhase phase, Side side);
osg::Matrix projectionMatrix(FramePhase phase, Side side);
int mFramesInFlight{ 0 };
std::array<std::unique_ptr<VRFrameMeta>, (int)FramePhase::NumPhases> mFrame{ nullptr };
std::mutex mMutex{};
std::condition_variable mCondition{};
long long mFrames{ 0 };
long long mLastRenderedFrame{ 0 };
long long mLastPredictedDisplayTime{ 0 };
long long mLastPredictedDisplayPeriod{ 0 };
std::chrono::steady_clock::time_point mStart{ std::chrono::steady_clock::now() };
std::chrono::nanoseconds mLastFrameInterval{};
std::chrono::steady_clock::time_point mLastRenderedFrameTimestamp{std::chrono::steady_clock::now()};
float mPlayerScale{ 1.f };
};
} }
#endif #endif

View file

@ -1,11 +1,8 @@
#include "vrviewer.hpp"
#include "vrtexture.hpp" #include "vrtexture.hpp"
#include <osg/Texture2D> #include <osg/Texture2D>
#include <osgViewer/Renderer>
#include <components/debug/debuglog.hpp> #include <components/debug/debuglog.hpp>
#include <osgDB/Registry>
#include <sstream>
#include <fstream>
#ifndef GL_TEXTURE_MAX_LEVEL #ifndef GL_TEXTURE_MAX_LEVEL
#define GL_TEXTURE_MAX_LEVEL 0x813D #define GL_TEXTURE_MAX_LEVEL 0x813D
@ -18,9 +15,9 @@ namespace MWVR
: mState(state) : mState(state)
, mWidth(width) , mWidth(width)
, mHeight(height) , mHeight(height)
, mSamples(msaaSamples)
, mColorBuffer(colorBuffer)
, mDepthBuffer(depthBuffer) , mDepthBuffer(depthBuffer)
, mColorBuffer(colorBuffer)
, mSamples(msaaSamples)
{ {
auto* gl = osg::GLExtensions::Get(state->getContextID(), false); auto* gl = osg::GLExtensions::Get(state->getContextID(), false);
@ -94,10 +91,10 @@ namespace MWVR
if (mFBO) if (mFBO)
gl->glDeleteFramebuffers(1, &mFBO); gl->glDeleteFramebuffers(1, &mFBO);
} }
else if(mFBO) else if (mFBO)
// Without access to opengl methods, i'll just let the FBOs leak. // Without access to opengl methods, i'll just let the FBOs leak.
Log(Debug::Warning) << "destroy() called without a State. Leaking FBO"; Log(Debug::Warning) << "destroy() called without a State. Leaking FBO";
if (mDepthBuffer) if (mDepthBuffer)
glDeleteTextures(1, &mDepthBuffer); glDeleteTextures(1, &mDepthBuffer);
if (mColorBuffer) if (mColorBuffer)

View file

@ -1,202 +1,203 @@
#include "vrtypes.hpp" #include "vrtypes.hpp"
#include <iostream> #include <iostream>
namespace MWVR namespace MWVR
{ {
Pose Pose::operator+(const Pose& rhs) Pose Pose::operator+(const Pose& rhs)
{
Pose pose = *this;
pose.position += this->orientation * rhs.position;
pose.orientation = rhs.orientation * this->orientation;
return pose;
}
const Pose& Pose::operator+=(const Pose& rhs)
{
*this = *this + rhs;
return *this;
}
Pose Pose::operator*(float scalar)
{
Pose pose = *this;
pose.position *= scalar;
return pose;
}
const Pose& Pose::operator*=(float scalar)
{
*this = *this * scalar;
return *this;
}
Pose Pose::operator/(float scalar)
{
Pose pose = *this;
pose.position /= scalar;
return pose;
}
const Pose& Pose::operator/=(float scalar)
{
*this = *this / scalar;
return *this;
}
bool Pose::operator==(const Pose& rhs) const
{
return position == rhs.position && orientation == rhs.orientation;
}
bool FieldOfView::operator==(const FieldOfView& rhs) const
{
return angleDown == rhs.angleDown
&& angleUp == rhs.angleUp
&& angleLeft == rhs.angleLeft
&& angleRight == rhs.angleRight;
}
// near and far named with an underscore because of windows' headers galaxy brain defines.
osg::Matrix FieldOfView::perspectiveMatrix(float near_, float far_)
{
const float tanLeft = tanf(angleLeft);
const float tanRight = tanf(angleRight);
const float tanDown = tanf(angleDown);
const float tanUp = tanf(angleUp);
const float tanWidth = tanRight - tanLeft;
const float tanHeight = tanUp - tanDown;
const float offset = near_;
float matrix[16] = {};
matrix[0] = 2 / tanWidth;
matrix[4] = 0;
matrix[8] = (tanRight + tanLeft) / tanWidth;
matrix[12] = 0;
matrix[1] = 0;
matrix[5] = 2 / tanHeight;
matrix[9] = (tanUp + tanDown) / tanHeight;
matrix[13] = 0;
if (far_ <= near_) {
matrix[2] = 0;
matrix[6] = 0;
matrix[10] = -1;
matrix[14] = -(near_ + offset);
}
else {
matrix[2] = 0;
matrix[6] = 0;
matrix[10] = -(far_ + offset) / (far_ - near_);
matrix[14] = -(far_ * (near_ + offset)) / (far_ - near_);
}
matrix[3] = 0;
matrix[7] = 0;
matrix[11] = -1;
matrix[15] = 0;
return osg::Matrix(matrix);
}
bool PoseSet::operator==(const PoseSet& rhs) const
{
return eye[0] == rhs.eye[0]
&& eye[1] == rhs.eye[1]
&& hands[0] == rhs.hands[0]
&& hands[1] == rhs.hands[1]
&& view[0] == rhs.view[0]
&& view[1] == rhs.view[1]
&& head == rhs.head;
}
bool View::operator==(const View& rhs) const
{
return pose == rhs.pose && fov == rhs.fov;
}
std::ostream& operator <<(
std::ostream& os,
const MWVR::Pose& pose)
{
os << "position=" << pose.position << ", orientation=" << pose.orientation;
return os;
}
std::ostream& operator <<(
std::ostream& os,
const MWVR::FieldOfView& fov)
{
os << "left=" << fov.angleLeft << ", right=" << fov.angleRight << ", down=" << fov.angleDown << ", up=" << fov.angleUp;
return os;
}
std::ostream& operator <<(
std::ostream& os,
const MWVR::View& view)
{
os << "pose=< " << view.pose << " >, fov=< " << view.fov << " >";
return os;
}
std::ostream& operator <<(
std::ostream& os,
const MWVR::PoseSet& poseSet)
{
os << "eye[" << Side::LEFT_SIDE<< "]: " << poseSet.eye[(int)Side::LEFT_SIDE] << std::endl;
os << "eye[" << Side::RIGHT_SIDE<< "]: " << poseSet.eye[(int)Side::RIGHT_SIDE] << std::endl;
os << "hands[" << Side::LEFT_SIDE<< "]: " << poseSet.hands[(int)Side::LEFT_SIDE] << std::endl;
os << "hands[" << Side::RIGHT_SIDE<< "]: " << poseSet.hands[(int)Side::RIGHT_SIDE] << std::endl;
os << "head: " << poseSet.head << std::endl;
os << "view[" << Side::LEFT_SIDE<< "]: " << poseSet.view[(int)Side::LEFT_SIDE] << std::endl;
os << "view[" << Side::RIGHT_SIDE<< "]: " << poseSet.view[(int)Side::RIGHT_SIDE] << std::endl;
return os;
}
std::ostream& operator <<(
std::ostream& os,
TrackedLimb limb)
{
switch (limb)
{ {
case TrackedLimb::HEAD: Pose pose = *this;
os << "HEAD"; break; pose.position += this->orientation * rhs.position;
case TrackedLimb::LEFT_HAND: pose.orientation = rhs.orientation * this->orientation;
os << "LEFT_HAND"; break; return pose;
case TrackedLimb::RIGHT_HAND:
os << "RIGHT_HAND"; break;
} }
return os;
}
std::ostream& operator <<( const Pose& Pose::operator+=(const Pose& rhs)
std::ostream& os,
ReferenceSpace limb)
{
switch (limb)
{ {
case ReferenceSpace::STAGE: *this = *this + rhs;
os << "STAGE"; break; return *this;
case ReferenceSpace::VIEW:
os << "VIEW"; break;
} }
return os;
}
std::ostream& operator <<( Pose Pose::operator*(float scalar)
std::ostream& os,
Side side)
{
switch (side)
{ {
case Side::LEFT_SIDE: Pose pose = *this;
os << "LEFT_SIDE"; break; pose.position *= scalar;
case Side::RIGHT_SIDE: return pose;
os << "RIGHT_SIDE"; break; }
const Pose& Pose::operator*=(float scalar)
{
*this = *this * scalar;
return *this;
}
Pose Pose::operator/(float scalar)
{
Pose pose = *this;
pose.position /= scalar;
return pose;
}
const Pose& Pose::operator/=(float scalar)
{
*this = *this / scalar;
return *this;
}
bool Pose::operator==(const Pose& rhs) const
{
return position == rhs.position && orientation == rhs.orientation;
}
bool FieldOfView::operator==(const FieldOfView& rhs) const
{
return angleDown == rhs.angleDown
&& angleUp == rhs.angleUp
&& angleLeft == rhs.angleLeft
&& angleRight == rhs.angleRight;
}
// near and far named with an underscore because of windows' headers galaxy brain defines.
osg::Matrix FieldOfView::perspectiveMatrix(float near_, float far_)
{
const float tanLeft = tanf(angleLeft);
const float tanRight = tanf(angleRight);
const float tanDown = tanf(angleDown);
const float tanUp = tanf(angleUp);
const float tanWidth = tanRight - tanLeft;
const float tanHeight = tanUp - tanDown;
const float offset = near_;
float matrix[16] = {};
matrix[0] = 2 / tanWidth;
matrix[4] = 0;
matrix[8] = (tanRight + tanLeft) / tanWidth;
matrix[12] = 0;
matrix[1] = 0;
matrix[5] = 2 / tanHeight;
matrix[9] = (tanUp + tanDown) / tanHeight;
matrix[13] = 0;
if (far_ <= near_) {
matrix[2] = 0;
matrix[6] = 0;
matrix[10] = -1;
matrix[14] = -(near_ + offset);
}
else {
matrix[2] = 0;
matrix[6] = 0;
matrix[10] = -(far_ + offset) / (far_ - near_);
matrix[14] = -(far_ * (near_ + offset)) / (far_ - near_);
}
matrix[3] = 0;
matrix[7] = 0;
matrix[11] = -1;
matrix[15] = 0;
return osg::Matrix(matrix);
}
bool PoseSet::operator==(const PoseSet& rhs) const
{
return eye[0] == rhs.eye[0]
&& eye[1] == rhs.eye[1]
&& hands[0] == rhs.hands[0]
&& hands[1] == rhs.hands[1]
&& view[0] == rhs.view[0]
&& view[1] == rhs.view[1]
&& head == rhs.head;
}
bool View::operator==(const View& rhs) const
{
return pose == rhs.pose && fov == rhs.fov;
}
std::ostream& operator <<(
std::ostream& os,
const MWVR::Pose& pose)
{
os << "position=" << pose.position << ", orientation=" << pose.orientation;
return os;
}
std::ostream& operator <<(
std::ostream& os,
const MWVR::FieldOfView& fov)
{
os << "left=" << fov.angleLeft << ", right=" << fov.angleRight << ", down=" << fov.angleDown << ", up=" << fov.angleUp;
return os;
}
std::ostream& operator <<(
std::ostream& os,
const MWVR::View& view)
{
os << "pose=< " << view.pose << " >, fov=< " << view.fov << " >";
return os;
}
std::ostream& operator <<(
std::ostream& os,
const MWVR::PoseSet& poseSet)
{
os << "eye[" << Side::LEFT_SIDE << "]: " << poseSet.eye[(int)Side::LEFT_SIDE] << std::endl;
os << "eye[" << Side::RIGHT_SIDE << "]: " << poseSet.eye[(int)Side::RIGHT_SIDE] << std::endl;
os << "hands[" << Side::LEFT_SIDE << "]: " << poseSet.hands[(int)Side::LEFT_SIDE] << std::endl;
os << "hands[" << Side::RIGHT_SIDE << "]: " << poseSet.hands[(int)Side::RIGHT_SIDE] << std::endl;
os << "head: " << poseSet.head << std::endl;
os << "view[" << Side::LEFT_SIDE << "]: " << poseSet.view[(int)Side::LEFT_SIDE] << std::endl;
os << "view[" << Side::RIGHT_SIDE << "]: " << poseSet.view[(int)Side::RIGHT_SIDE] << std::endl;
return os;
}
std::ostream& operator <<(
std::ostream& os,
TrackedLimb limb)
{
switch (limb)
{
case TrackedLimb::HEAD:
os << "HEAD"; break;
case TrackedLimb::LEFT_HAND:
os << "LEFT_HAND"; break;
case TrackedLimb::RIGHT_HAND:
os << "RIGHT_HAND"; break;
}
return os;
}
std::ostream& operator <<(
std::ostream& os,
ReferenceSpace limb)
{
switch (limb)
{
case ReferenceSpace::STAGE:
os << "STAGE"; break;
case ReferenceSpace::VIEW:
os << "VIEW"; break;
}
return os;
}
std::ostream& operator <<(
std::ostream& os,
Side side)
{
switch (side)
{
case Side::LEFT_SIDE:
os << "LEFT_SIDE"; break;
case Side::RIGHT_SIDE:
os << "RIGHT_SIDE"; break;
}
return os;
} }
return os;
}
} }

View file

@ -12,113 +12,109 @@
namespace MWVR namespace MWVR
{ {
class OpenXRSwapchain; class OpenXRSwapchain;
class OpenXRManager; class OpenXRManager;
//! Describes what limb to track. //! Describes what limb to track.
enum class TrackedLimb enum class TrackedLimb
{ {
LEFT_HAND, LEFT_HAND,
RIGHT_HAND, RIGHT_HAND,
HEAD HEAD
}; };
//! Describes what space to track the limb in //! Describes what space to track the limb in
enum class ReferenceSpace enum class ReferenceSpace
{ {
STAGE = 0, //!< Track limb in the VR stage space. Meaning a space with a floor level origin and fixed horizontal orientation. STAGE = 0, //!< Track limb in the VR stage space. Meaning a space with a floor level origin and fixed horizontal orientation.
VIEW = 1 //!< Track limb in the VR view space. Meaning a space with the head as origin and orientation. VIEW = 1 //!< Track limb in the VR view space. Meaning a space with the head as origin and orientation.
}; };
//! Self-descriptive //! Self-descriptive
enum class Side enum class Side
{ {
LEFT_SIDE = 0, LEFT_SIDE = 0,
RIGHT_SIDE = 1 RIGHT_SIDE = 1
}; };
//! Represents the relative pose in space of some limb or eye. //! Represents the relative pose in space of some limb or eye.
struct Pose struct Pose
{ {
//! Position in space //! Position in space
osg::Vec3 position{ 0,0,0 }; osg::Vec3 position{ 0,0,0 };
//! Orientation in space. //! Orientation in space.
osg::Quat orientation{ 0,0,0,1 }; osg::Quat orientation{ 0,0,0,1 };
//! Add one pose to another //! Add one pose to another
Pose operator+(const Pose& rhs); Pose operator+(const Pose& rhs);
const Pose& operator+=(const Pose& rhs); const Pose& operator+=(const Pose& rhs);
//! Scale a pose (does not affect orientation) //! Scale a pose (does not affect orientation)
Pose operator*(float scalar); Pose operator*(float scalar);
const Pose& operator*=(float scalar); const Pose& operator*=(float scalar);
Pose operator/(float scalar); Pose operator/(float scalar);
const Pose& operator/=(float scalar); const Pose& operator/=(float scalar);
bool operator==(const Pose& rhs) const; bool operator==(const Pose& rhs) const;
}; };
//! Fov of a single eye //! Fov of a single eye
struct FieldOfView { struct FieldOfView {
float angleLeft; float angleLeft;
float angleRight; float angleRight;
float angleUp; float angleUp;
float angleDown; float angleDown;
bool operator==(const FieldOfView& rhs) const; bool operator==(const FieldOfView& rhs) const;
//! Generate a perspective matrix from this fov //! Generate a perspective matrix from this fov
osg::Matrix perspectiveMatrix(float near, float far); osg::Matrix perspectiveMatrix(float near, float far);
}; };
//! Represents an eye in VR including both pose and fov. A view's pose is relative to the head. //! Represents an eye in VR including both pose and fov. A view's pose is relative to the head.
struct View struct View
{ {
Pose pose; Pose pose;
FieldOfView fov; FieldOfView fov;
bool operator==(const View& rhs) const; bool operator==(const View& rhs) const;
}; };
//! The complete set of poses tracked each frame by MWVR. //! The complete set of poses tracked each frame by MWVR.
struct PoseSet struct PoseSet
{ {
Pose eye[2]{}; //!< Stage-relative Pose eye[2]{}; //!< Stage-relative
Pose hands[2]{}; //!< Stage-relative Pose hands[2]{}; //!< Stage-relative
Pose head{}; //!< Stage-relative Pose head{}; //!< Stage-relative
View view[2]{}; //!< Head-relative View view[2]{}; //!< Head-relative
bool operator==(const PoseSet& rhs) const; bool operator==(const PoseSet& rhs) const;
}; };
struct CompositionLayerProjectionView struct CompositionLayerProjectionView
{ {
class OpenXRSwapchain* swapchain; class OpenXRSwapchain* swapchain;
Pose pose; Pose pose;
FieldOfView fov; FieldOfView fov;
}; };
struct SwapchainConfig struct SwapchainConfig
{ {
uint32_t recommendedWidth = -1; uint32_t recommendedWidth = -1;
uint32_t maxWidth = -1; uint32_t maxWidth = -1;
uint32_t recommendedHeight = -1; uint32_t recommendedHeight = -1;
uint32_t maxHeight = -1; uint32_t maxHeight = -1;
uint32_t recommendedSamples = -1; uint32_t recommendedSamples = -1;
uint32_t maxSamples = -1; uint32_t maxSamples = -1;
}; };
// Serialization methods for VR types.
std::ostream& operator <<(std::ostream& os, const Pose& pose);
std::ostream& operator <<(std::ostream& os, const FieldOfView& fov);
std::ostream& operator <<(std::ostream& os, const View& view);
std::ostream& operator <<(std::ostream& os, const PoseSet& poseSet);
std::ostream& operator <<(std::ostream& os, TrackedLimb limb);
std::ostream& operator <<(std::ostream& os, ReferenceSpace space);
std::ostream& operator <<(std::ostream& os, Side side);
// Serialization methods for VR types.
std::ostream& operator <<(std::ostream& os, const Pose& pose);
std::ostream& operator <<(std::ostream& os, const FieldOfView& fov);
std::ostream& operator <<(std::ostream& os, const View& view);
std::ostream& operator <<(std::ostream& os, const PoseSet& poseSet);
std::ostream& operator <<(std::ostream& os, TrackedLimb limb);
std::ostream& operator <<(std::ostream& os, ReferenceSpace space);
std::ostream& operator <<(std::ostream& os, Side side);
} }
// I put these in the global namespace to guarantee lookup.
#endif #endif

View file

@ -1,24 +1,11 @@
#include "vrview.hpp" #include "vrview.hpp"
#include "vrsession.hpp"
#include "openxrmanager.hpp" #include "openxrmanager.hpp"
#include "openxrmanagerimpl.hpp" #include "openxrmanagerimpl.hpp"
#include "../mwinput/inputmanagerimp.hpp" #include "vrsession.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
#include "../mwrender/renderingmanager.hpp"
#include "../mwrender/water.hpp"
#include "vrenvironment.hpp" #include "vrenvironment.hpp"
#include <components/debug/debuglog.hpp> #include <components/debug/debuglog.hpp>
#include <components/sdlutil/sdlgraphicswindow.hpp>
#include <Windows.h>
#include <openxr/openxr.h>
#include <openxr/openxr_platform.h>
#include <openxr/openxr_platform_defines.h>
#include <openxr/openxr_reflection.h>
#include <osgViewer/Renderer> #include <osgViewer/Renderer>
@ -92,8 +79,8 @@ namespace MWVR {
} }
} }
void VRView::UpdateSlaveCallback::updateSlave( void VRView::UpdateSlaveCallback::updateSlave(
osg::View& view, osg::View& view,
osg::View::Slave& slave) osg::View::Slave& slave)
{ {
auto* camera = slave._camera.get(); auto* camera = slave._camera.get();
auto name = camera->getName(); auto name = camera->getName();

View file

@ -9,7 +9,7 @@ struct XrSwapchainSubImage;
namespace MWVR namespace MWVR
{ {
/// \brief Manipulates a slave camera by replacing its framebuffer with one destined for openxr
class VRView : public osg::Referenced class VRView : public osg::Referenced
{ {
public: public:
@ -48,7 +48,7 @@ namespace MWVR
osg::Camera* createCamera(int order, const osg::Vec4& clearColor, osg::GraphicsContext* gc); osg::Camera* createCamera(int order, const osg::Vec4& clearColor, osg::GraphicsContext* gc);
//! Get the view surface //! Get the view surface
OpenXRSwapchain& swapchain(void) { return *mSwapchain; } OpenXRSwapchain& swapchain(void) { return *mSwapchain; }
//! Present to the openxr swapchain
void swapBuffers(osg::GraphicsContext* gc); void swapBuffers(osg::GraphicsContext* gc);
public: public:

View file

@ -1,22 +1,19 @@
#include "vrviewer.hpp" #include "vrviewer.hpp"
#include "vrsession.hpp"
#include "openxrmanagerimpl.hpp" #include "openxrmanagerimpl.hpp"
#include "openxrinput.hpp"
#include "vrenvironment.hpp" #include "vrenvironment.hpp"
#include "Windows.h" #include "vrsession.hpp"
#include "../mwmechanics/actorutil.hpp"
#include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/player.hpp"
#include "../mwworld/esmstore.hpp"
#include "../mwrender/vismask.hpp" #include "../mwrender/vismask.hpp"
#include <components/esm/loadrace.hpp>
#include <osg/MatrixTransform>
namespace MWVR namespace MWVR
{ {
const std::array<const char*, 2> VRViewer::sViewNames = {
"LeftEye",
"RightEye"
};
VRViewer::VRViewer( VRViewer::VRViewer(
osg::ref_ptr<osgViewer::Viewer> viewer) osg::ref_ptr<osgViewer::Viewer> viewer)
: mRealizeOperation(new RealizeOperation()) : mRealizeOperation(new RealizeOperation())
@ -42,87 +39,62 @@ namespace MWVR
void VRViewer::realize(osg::GraphicsContext* context) void VRViewer::realize(osg::GraphicsContext* context)
{ {
std::unique_lock<std::mutex> lock(mMutex); std::unique_lock<std::mutex> lock(mMutex);
if (mConfigured) if (mConfigured)
{ {
return; return;
} }
if (!context->isCurrent()) // Give the main camera an initial draw callback that disables camera setup (we don't want it)
if (!context->makeCurrent())
{
throw std::logic_error("VRViewer::configure() failed to make graphics context current.");
return;
}
auto mainCamera = mCameras["MainCamera"] = mViewer->getCamera(); auto mainCamera = mCameras["MainCamera"] = mViewer->getCamera();
mainCamera->setName("Main"); mainCamera->setName("Main");
mainCamera->setInitialDrawCallback(new VRView::InitialDrawCallback()); mainCamera->setInitialDrawCallback(new VRView::InitialDrawCallback());
osg::Vec4 clearColor = mainCamera->getClearColor();
auto* xr = Environment::get().getManager(); auto* xr = Environment::get().getManager();
if (!xr->realized()) if (!xr->realized())
xr->realize(context); xr->realize(context);
// Run through initial events to start session
// For the rest of runtime this is handled by vrsession
xr->handleEvents(); xr->handleEvents();
// Small feature culling
bool smallFeatureCulling = Settings::Manager::getBool("small feature culling", "Camera");
auto smallFeatureCullingPixelSize = Settings::Manager::getFloat("small feature culling pixel size", "Camera");
osg::Camera::CullingMode cullingMode = osg::Camera::DEFAULT_CULLING | osg::Camera::FAR_PLANE_CULLING;
if (!smallFeatureCulling)
cullingMode &= ~osg::CullStack::SMALL_FEATURE_CULLING;
else
cullingMode |= osg::CullStack::SMALL_FEATURE_CULLING;
// Configure eyes, their cameras, and their enslavement.
osg::Vec4 clearColor = mainCamera->getClearColor();
auto config = xr->getRecommendedSwapchainConfig(); auto config = xr->getRecommendedSwapchainConfig();
auto leftView = new VRView("LeftEye", config[(int)Side::LEFT_SIDE], context->getState()); for (unsigned i = 0; i < sViewNames.size(); i++)
auto rightView = new VRView("RightEye", config[(int)Side::RIGHT_SIDE], context->getState());
mViews["LeftEye"] = leftView;
mViews["RightEye"] = rightView;
auto leftCamera = mCameras["LeftEye"] = leftView->createCamera(0, clearColor, context);
auto rightCamera = mCameras["RightEye"] = rightView->createCamera(1, clearColor, context);
leftCamera->setPreDrawCallback(mPreDraw);
rightCamera->setPreDrawCallback(mPreDraw);
leftCamera->setFinalDrawCallback(mPostDraw);
rightCamera->setFinalDrawCallback(mPostDraw);
// Stereo cameras should only draw the scene
leftCamera->setCullMask(~MWRender::Mask_GUI & ~MWRender::Mask_SimpleWater & ~MWRender::Mask_UpdateVisitor);
rightCamera->setCullMask(~MWRender::Mask_GUI & ~MWRender::Mask_SimpleWater & ~MWRender::Mask_UpdateVisitor);
leftCamera->setName("LeftEye");
rightCamera->setName("RightEye");
osg::Camera::CullingMode cullingMode = osg::Camera::DEFAULT_CULLING|osg::Camera::FAR_PLANE_CULLING;
if (!Settings::Manager::getBool("small feature culling", "Camera"))
cullingMode &= ~(osg::CullStack::SMALL_FEATURE_CULLING);
else
{ {
auto smallFeatureCullingPixelSize = Settings::Manager::getFloat("small feature culling pixel size", "Camera"); auto view = new VRView(sViewNames[i], config[i], context->getState());
leftCamera->setSmallFeatureCullingPixelSize(smallFeatureCullingPixelSize); mViews[sViewNames[i]] = view;
rightCamera->setSmallFeatureCullingPixelSize(smallFeatureCullingPixelSize); auto camera = mCameras[sViewNames[i]] = view->createCamera(i, clearColor, context);
cullingMode |= osg::CullStack::SMALL_FEATURE_CULLING; camera->setPreDrawCallback(mPreDraw);
camera->setFinalDrawCallback(mPostDraw);
camera->setCullMask(~MWRender::Mask_GUI & ~MWRender::Mask_SimpleWater & ~MWRender::Mask_UpdateVisitor);
camera->setName(sViewNames[i]);
if (smallFeatureCulling)
camera->setSmallFeatureCullingPixelSize(smallFeatureCullingPixelSize);
camera->setCullingMode(cullingMode);
mViewer->addSlave(camera, true);
mViewer->getSlave(i)._updateSlaveCallback = new VRView::UpdateSlaveCallback(view, context);
mMsaaResolveMirrorTexture[i].reset(new VRTexture(context->getState(),
view->swapchain().width(),
view->swapchain().height(),
0));
} }
leftCamera->setCullingMode(cullingMode);
rightCamera->setCullingMode(cullingMode);
mViewer->addSlave(leftCamera, true);
mViewer->addSlave(rightCamera, true);
mViewer->setReleaseContextAtEndOfFrameHint(false);
mMirrorTexture.reset(new VRTexture(context->getState(), mainCamera->getViewport()->width(), mainCamera->getViewport()->height(), 0)); mMirrorTexture.reset(new VRTexture(context->getState(), mainCamera->getViewport()->width(), mainCamera->getViewport()->height(), 0));
mMsaaResolveMirrorTexture[(int)Side::LEFT_SIDE].reset(new VRTexture(context->getState(), mViewer->setReleaseContextAtEndOfFrameHint(false);
leftView->swapchain().width(),
leftView->swapchain().height(),
0));
mMsaaResolveMirrorTexture[(int)Side::RIGHT_SIDE].reset(new VRTexture(context->getState(),
rightView->swapchain().width(),
rightView->swapchain().height(),
0));
mViewer->getSlave(0)._updateSlaveCallback = new VRView::UpdateSlaveCallback(leftView, context);
mViewer->getSlave(1)._updateSlaveCallback = new VRView::UpdateSlaveCallback(rightView, context);
mMainCameraGC = mainCamera->getGraphicsContext(); mMainCameraGC = mainCamera->getGraphicsContext();
mMainCameraGC->setSwapCallback(new VRViewer::SwapBuffersCallback(this)); mMainCameraGC->setSwapCallback(new VRViewer::SwapBuffersCallback(this));
@ -132,6 +104,14 @@ namespace MWVR
Log(Debug::Verbose) << "Realized"; Log(Debug::Verbose) << "Realized";
} }
VRView* VRViewer::getView(std::string name)
{
auto it = mViews.find(name);
if (it != mViews.end())
return it->second.get();
return nullptr;
}
void VRViewer::enableMainCamera(void) void VRViewer::enableMainCamera(void)
{ {
mCameras["MainCamera"]->setGraphicsContext(mMainCameraGC); mCameras["MainCamera"]->setGraphicsContext(mMainCameraGC);
@ -150,18 +130,16 @@ namespace MWVR
int screenWidth = mCameras["MainCamera"]->getViewport()->width(); int screenWidth = mCameras["MainCamera"]->getViewport()->width();
int mirrorWidth = screenWidth / 2; int mirrorWidth = screenWidth / 2;
int screenHeight = mCameras["MainCamera"]->getViewport()->height(); int screenHeight = mCameras["MainCamera"]->getViewport()->height();
const char* viewNames[] = {
"RightEye",
"LeftEye"
};
for (int i = 0; i < 2; i++) for (unsigned i = 0; i < sViewNames.size(); i++)
{ {
auto& resolveTexture = *mMsaaResolveMirrorTexture[i]; auto& resolveTexture = *mMsaaResolveMirrorTexture[i];
resolveTexture.beginFrame(gc); resolveTexture.beginFrame(gc);
mViews[viewNames[i]]->swapchain().renderBuffer()->blit(gc, 0, 0, resolveTexture.width(), resolveTexture.height()); mViews[sViewNames[i]]->swapchain().renderBuffer()->blit(gc, 0, 0, resolveTexture.width(), resolveTexture.height());
mMirrorTexture->beginFrame(gc); mMirrorTexture->beginFrame(gc);
resolveTexture.blit(gc, i * mirrorWidth, 0, (i + 1) * mirrorWidth, screenHeight); // Mirror the index when rendering to the mirror texture to allow cross eye mirror textures.
unsigned mirrorIndex = sViewNames.size() - 1 - i;
resolveTexture.blit(gc, mirrorIndex * mirrorWidth, 0, (mirrorIndex + 1) * mirrorWidth, screenHeight);
} }
gl->glBindFramebuffer(GL_FRAMEBUFFER_EXT, 0); gl->glBindFramebuffer(GL_FRAMEBUFFER_EXT, 0);
@ -206,7 +184,8 @@ namespace MWVR
view->postrenderCallback(info); view->postrenderCallback(info);
// OSG will sometimes overwrite the predraw callback. // This happens sometimes, i've not been able to catch it when as happens
// to see why and how i can stop it.
if (camera->getPreDrawCallback() != mPreDraw) if (camera->getPreDrawCallback() != mPreDraw)
{ {
camera->setPreDrawCallback(mPreDraw); camera->setPreDrawCallback(mPreDraw);

View file

@ -14,6 +14,10 @@
namespace MWVR namespace MWVR
{ {
/// \brief Manages stereo rendering and mirror texturing.
///
/// Manipulates the osgViewer by disabling main camera rendering, and instead rendering to
/// two slave cameras, each connected to and manipulated by a VRView class.
class VRViewer class VRViewer
{ {
public: public:
@ -65,25 +69,27 @@ namespace MWVR
VRViewer* mViewer; VRViewer* mViewer;
}; };
static const std::array<const char*, 2> sViewNames;
public: public:
VRViewer( VRViewer(
osg::ref_ptr<osgViewer::Viewer> viewer); osg::ref_ptr<osgViewer::Viewer> viewer);
~VRViewer(void); ~VRViewer(void);
const XrCompositionLayerBaseHeader* layer();
void traversals(); void traversals();
void preDrawCallback(osg::RenderInfo& info); void preDrawCallback(osg::RenderInfo& info);
void postDrawCallback(osg::RenderInfo& info); void postDrawCallback(osg::RenderInfo& info);
void blitEyesToMirrorTexture(osg::GraphicsContext* gc); void blitEyesToMirrorTexture(osg::GraphicsContext* gc);
void realize(osg::GraphicsContext* gc); void realize(osg::GraphicsContext* gc);
bool realized() { return mConfigured; } bool realized() { return mConfigured; }
VRView* getView(std::string name);
void enableMainCamera(void); void enableMainCamera(void);
void disableMainCamera(void); void disableMainCamera(void);
public: private:
osg::ref_ptr<OpenXRManager::RealizeOperation> mRealizeOperation = nullptr; osg::ref_ptr<OpenXRManager::RealizeOperation> mRealizeOperation = nullptr;
osg::ref_ptr<osgViewer::Viewer> mViewer = nullptr; osg::ref_ptr<osgViewer::Viewer> mViewer = nullptr;
std::map<std::string, osg::ref_ptr<VRView> > mViews{}; std::map<std::string, osg::ref_ptr<VRView> > mViews{};
@ -91,7 +97,6 @@ namespace MWVR
osg::ref_ptr<PredrawCallback> mPreDraw{ nullptr }; osg::ref_ptr<PredrawCallback> mPreDraw{ nullptr };
osg::ref_ptr<PostdrawCallback> mPostDraw{ nullptr }; osg::ref_ptr<PostdrawCallback> mPostDraw{ nullptr };
osg::GraphicsContext* mMainCameraGC{ nullptr }; osg::GraphicsContext* mMainCameraGC{ nullptr };
//std::unique_ptr<VRTexture> mMirrorTexture{ nullptr };
std::unique_ptr<VRTexture> mMsaaResolveMirrorTexture[2]{ }; std::unique_ptr<VRTexture> mMsaaResolveMirrorTexture[2]{ };
std::unique_ptr<VRTexture> mMirrorTexture{ nullptr }; std::unique_ptr<VRTexture> mMirrorTexture{ nullptr };

View file

@ -6,22 +6,6 @@
# OpenXR_INCLUDE_DIR, where to find openxr.h # OpenXR_INCLUDE_DIR, where to find openxr.h
# OpenXR_VERSION, the version of the found library # OpenXR_VERSION, the version of the found library
# #
# This module accepts the following env variables
# OPENXR_ROOT
# This module responds to the the flag:
#=============================================================================
# Copyright 2003-2009 Kitware, Inc.
#
# Distributed under the OSI-approved BSD License (the "License");
# see accompanying file Copyright.txt for details.
#
# This software is distributed WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the License for more information.
#=============================================================================
# (To distribute this file outside of CMake, substitute the full
# License text for the above reference.)
if(WIN32) if(WIN32)