position lower panel based on controller position, with a rotational bias

This commit is contained in:
amwatson 2024-03-23 14:42:53 -05:00
parent 01b35c0def
commit a5dcfc0e37
3 changed files with 82 additions and 51 deletions

View file

@ -30,8 +30,10 @@ License : Licensed under GPLv3 or any later version.
namespace {
constexpr float kSuperImmersiveRadius = 0.5f;
constexpr float kDistanceBetweenPanelsInMeters = 0.75f;
constexpr float kSuperImmersiveRadius = 0.5f;
constexpr float kDistanceBetweenPanelsInMeters = 0.75f;
constexpr float kInitialLowerPanelPitchInRadians = -MATH_FLOAT_PI / 4.0f; // -45 degrees in radians
//-----------------------------------------------------------------------------
// Local sysprops
@ -124,17 +126,16 @@ Panel CreateTopPanel(const XrVector3f& position, const float surfaceWidth,
Panel CreateLowerPanelFromTopPanel(const Panel& topPanel, const float resolutionFactor) {
// Note: the fact that two constants are 0.75 is purely coincidental.
constexpr float kDefaultLowerPanelScaleFactor = 0.75f * 0.75f;
constexpr float kLowerPanelYOffsetInMeters = -kDistanceBetweenPanelsInMeters;
constexpr float kLowerPanelZOffsetInMeters = -1.5f;
constexpr float kLowerPanelYOffsetInMeters = -0.75f;
constexpr float kLowerPanelZOffsetInMeters = 0.5f;
// Pitch the lower panel away from the viewer 45 degrees
constexpr float kLowerPanelPitchInRadians = -MATH_FLOAT_PI / 4.0f;
const float cropHoriz = 90.0f * resolutionFactor;
const float cropHoriz = 90.0f * resolutionFactor;
XrPosef lowerPanelFromWorld = topPanel.mPanelFromWorld;
lowerPanelFromWorld.orientation =
XrMath::Quatf::FromEuler(0.0f, kLowerPanelPitchInRadians, 0.0f);
XrMath::Quatf::FromEuler(kInitialLowerPanelPitchInRadians, 0, 0);
lowerPanelFromWorld.position.y += kLowerPanelYOffsetInMeters;
lowerPanelFromWorld.position.z = kLowerPanelZOffsetInMeters;
lowerPanelFromWorld.position.z += kLowerPanelZOffsetInMeters;
return Panel(lowerPanelFromWorld, topPanel.mWidth, topPanel.mHeight,
kDefaultLowerPanelScaleFactor, XrVector2f{cropHoriz / 2.0f, 0.0f},
XrVector2f{topPanel.mWidth - cropHoriz / 2.0f, topPanel.mHeight});
@ -419,46 +420,75 @@ bool GameSurfaceLayer::GetRayIntersectionWithPanel(const XrVector3f& start,
void GameSurfaceLayer::SetTopPanelFromController(const XrVector3f& controllerPosition) {
static constexpr XrVector3f viewerPosition{0, 0, 0}; // Set viewer position
static constexpr XrVector3f viewerPosition{0.0f, 0.0f, 0.0f}; // Set viewer position
const float sphereRadius = XrMath::Vector3f::Length(
mTopPanel.mPanelFromWorld.position - viewerPosition); // Set the initial distance of the
// window from the viewer
static constexpr XrVector3f windowUpDirection{0, 1, 0}; // Y is up
static constexpr XrVector3f windowUpDirection{0.0f, 1.0f, 0.0f}; // Y is up
const XrVector3f windowPosition =
CalculatePanelPosition(viewerPosition, controllerPosition, sphereRadius);
const XrQuaternionf windowRotation =
CalculatePanelRotation(windowPosition, viewerPosition, windowUpDirection);
if (windowPosition.y <
(mLowerPanel.mPanelFromWorld.position.y + kDistanceBetweenPanelsInMeters)) {
if (XrMath::Vector3f::LengthSq(windowPosition - mLowerPanel.mPanelFromWorld.position) <
XrMath::Vector3f::LengthSq(mTopPanel.mInitialPose.position -
mLowerPanel.mInitialPose.position)) {
return;
}
if (XrMath::Quatf::GetPitchInRadians(windowRotation) > MATH_FLOAT_PI / 3.0f) { return; }
mTopPanel.mPanelFromWorld = XrPosef{windowRotation, windowPosition};
}
// Goal is to rotate the lower panel to face the user, but with an initial bias of 45 degrees.
// The result is the lower panel being slightly tilted away from the user compared to the top panel,
// but comfortably readable at any angle.
// The rotational offset is done so that the top+bottom text comfortably fit into the user's FOV
// at high angles, so the user isn't craning their neck while reclining.
void GameSurfaceLayer::SetLowerPanelFromController(const XrVector3f& controllerPosition) {
constexpr XrVector3f viewerPosition{0.0f, 0.0f, 0.0f}; // Viewer position at origin
constexpr XrVector3f windowUpDirection{0.0f, 1.0f, 0.0f}; // Y is up
constexpr float pitchAdjustmentFactor = 0.5f;
static constexpr XrVector3f viewerPosition{0, 0, 0}; // Set viewer position
const float sphereRadius = XrMath::Vector3f::Length(
mLowerPanel.mPanelFromWorld.position - viewerPosition); // Set the initial distance of the
// window from the viewer
static constexpr XrVector3f windowUpDirection{0, 1, 0}; // Y is up
// Calculate sphere radius based on panel position to viewer
const float sphereRadius =
XrMath::Vector3f::Length(mLowerPanel.mPanelFromWorld.position - viewerPosition);
// Calculate new window position based on controller and sphere radius
const XrVector3f windowPosition =
CalculatePanelPosition(viewerPosition, controllerPosition, sphereRadius);
const XrQuaternionf windowRotation =
CalculatePanelRotation(windowPosition, viewerPosition, windowUpDirection);
if (windowPosition.y >
(mTopPanel.mPanelFromWorld.position.y - kDistanceBetweenPanelsInMeters)) {
// Limit vertical range to prevent the window from being too close to the viewer or the top
// panel.
if (windowPosition.z >= -0.5f ||
XrMath::Vector3f::LengthSq(mTopPanel.mPanelFromWorld.position - windowPosition) <
XrMath::Vector3f::LengthSq(mTopPanel.mInitialPose.position -
mLowerPanel.mInitialPose.position)) {
return;
}
if (XrMath::Quatf::GetPitchInRadians(windowRotation) > MATH_FLOAT_PI / 3.0f) { return; }
mLowerPanel.mPanelFromWorld = XrPosef{windowRotation, windowPosition};
// Calculate the base rotation of the panel to face the user
const XrQuaternionf baseRotation =
CalculatePanelRotation(windowPosition, viewerPosition, windowUpDirection);
// Calculate pitch adjustment based on vertical displacement from initial position
const float verticalDisplacement = windowPosition.y - mLowerPanel.mInitialPose.position.y;
// Arbitrary factor, chosen based on what change-in-pitch felt best
// A higher factor will make the window pitch more aggressively
const float pitchAdjustment = verticalDisplacement * pitchAdjustmentFactor;
// Clamp the new pitch to reasonable bounds (-45 to 90 degrees)
const float newPitchRadians =
std::clamp(-std::abs(kInitialLowerPanelPitchInRadians + pitchAdjustment),
kInitialLowerPanelPitchInRadians, MATH_FLOAT_PI / 2.0f);
// Construct a quaternion for the pitch adjustment
const XrQuaternionf pitchAdjustmentQuat =
XrMath::FromAxisAngle({1.0f, 0.0f, 0.0f}, newPitchRadians / 2.0f);
// Combine the base rotation with the pitch adjustment
mLowerPanel.mPanelFromWorld = {baseRotation * pitchAdjustmentQuat, windowPosition};
}
static constexpr float kThumbstickSpeed = 0.05f;

View file

@ -78,7 +78,8 @@ public:
, mPanelFromWorld(pose)
, mWidth(width)
, mHeight(height)
, mScaleFactor(scaleFactor) {}
, mScaleFactor(scaleFactor)
, mInitialPose(pose) {}
Panel(const XrPosef& pose, const float width, const float height, const float scaleFactor)
: Panel(pose, width, height, scaleFactor, {0, 0}, {width, height}) {}
@ -89,10 +90,11 @@ public:
XrVector2f mMin;
XrVector2f mMax;
} mClickBounds;
XrPosef mPanelFromWorld;
const float mWidth;
const float mHeight;
const float mScaleFactor;
XrPosef mPanelFromWorld;
const float mWidth;
const float mHeight;
const float mScaleFactor;
const XrPosef mInitialPose;
};
/*
@ -158,19 +160,19 @@ public:
*
* Note: assumes viewer is looking down the -Z axis.
*/
bool GetRayIntersectionWithPanel(const XrVector3f& start,
const XrVector3f& end,
XrVector2f& result2d,
XrPosef& result3d) const;
bool GetRayIntersectionWithPanelTopPanel(const XrVector3f& start,
const XrVector3f& end,
XrVector2f& result2d,
XrPosef& result3d) const;
void SetTopPanelFromController(const XrVector3f& controllerPosition);
void SetTopPanelFromThumbstick(const float thumbstickY);
XrPosef SetLowerPanelFromThumbstick(const float thumbstickY);
void SetLowerPanelFromController(const XrVector3f& controllerPosition);
XrPosef GetTopPanelFromHeadPose(uint32_t eye, const XrPosef& headPose);
bool GetRayIntersectionWithPanel(const XrVector3f& start,
const XrVector3f& end,
XrVector2f& result2d,
XrPosef& result3d) const;
bool GetRayIntersectionWithPanelTopPanel(const XrVector3f& start,
const XrVector3f& end,
XrVector2f& result2d,
XrPosef& result3d) const;
void SetTopPanelFromController(const XrVector3f& controllerPosition);
void SetTopPanelFromThumbstick(const float thumbstickY);
XrPosef SetLowerPanelFromThumbstick(const float thumbstickY);
void SetLowerPanelFromController(const XrVector3f& controllerPosition);
XrPosef GetTopPanelFromHeadPose(uint32_t eye, const XrPosef& headPose);
const XrPosef& GetLowerPanelPose() const { return mLowerPanel.mPanelFromWorld; }
private:

View file

@ -274,12 +274,12 @@ private:
mRibbonLayer = std::make_unique<UILayer>(
"org/citra/citra_emu/vr/ui/VrRibbonLayer", XrVector3f{0, -0.75f, -1.51f},
XrMath::Quatf::FromEuler(0.0f, -MATH_FLOAT_PI / 4.0f, 0.0f), jni, mActivityObject,
XrMath::Quatf::FromEuler(-MATH_FLOAT_PI / 4.0f, 0.0f, 0.0f), jni, mActivityObject,
gOpenXr->mSession);
mKeyboardLayer = std::make_unique<UILayer>(
"org/citra/citra_emu/vr/ui/VrKeyboardLayer", XrVector3f{0, -0.4f, -0.5f},
XrMath::Quatf::FromEuler(0.0f, -MATH_FLOAT_PI / 4.0f, 0.0f), jni, mActivityObject,
XrMath::Quatf::FromEuler(-MATH_FLOAT_PI / 4.0f, 0.0f, 0.0f), jni, mActivityObject,
gOpenXr->mSession);
mErrorMessageLayer = std::make_unique<UILayer>(
@ -427,7 +427,7 @@ private:
Core::System::GetInstance().GPU().Renderer().Rasterizer()) {
if (VRSettings::values.vr_immersive_mode == 0 ||
// If in normal immersive mode then look down for the lower panel to reveal
// itself (for some reason the Roll function returns pitch)
// itself
(VRSettings::values.vr_immersive_mode == 1 &&
XrMath::Quatf::GetPitchInRadians(gOpenXr->headLocation.pose.orientation) <
-MATH_FLOAT_PI / 8.0f) ||
@ -668,7 +668,7 @@ private:
mInputStateFrame.mIsHandActive[mInputStateFrame.mPreferredHand];
static bool sIsLowerPanelBeingPositioned = false;
const bool wasLowerPanelBeingPositioned = sIsLowerPanelBeingPositioned;
const bool wasLowerPanelBeingPositioned = sIsLowerPanelBeingPositioned;
sIsLowerPanelBeingPositioned &=
appState.mLowerMenuType == LowerMenuType::POSITIONAL_MENU &&
@ -709,9 +709,10 @@ private:
// applicable panels
// Lock ribbon in place when placement is complete
const bool needRibbonUpdate = !sIsLowerPanelBeingPositioned && wasLowerPanelBeingPositioned;
const bool needRibbonUpdate =
!sIsLowerPanelBeingPositioned && wasLowerPanelBeingPositioned;
if (needRibbonUpdate) {
mRibbonLayer->SetPanelWithPose(mGameSurfaceLayer->GetLowerPanelPose());
mRibbonLayer->SetPanelWithPose(mGameSurfaceLayer->GetLowerPanelPose());
}
if (!shouldRenderCursor) {
@ -723,7 +724,7 @@ private:
mGameSurfaceLayer->SetLowerPanelFromController(
XrVector3f{0, cursorPose3d.position.y, cursorPose3d.position.z});
sIsLowerPanelBeingPositioned = true;
sIsLowerPanelBeingPositioned = true;
} else if (appState.mLowerMenuType == LowerMenuType::MAIN_MENU) {
if (triggerState.currentState == 0 && triggerState.changedSinceLastSync) {
jni->CallVoidMethod(mActivityObject, mSendClickToWindowMethodID,
@ -1110,8 +1111,6 @@ private:
class AppState {
public:
bool ShouldShowLowerPanel() const { return mLowerMenuType == LowerMenuType::MAIN_MENU; }
LowerMenuType mLowerMenuType = LowerMenuType::MAIN_MENU;
bool mIsKeyboardActive = false;