mirror of
https://github.com/Pixelplanet/Pixel-VR-Player.git
synced 2026-10-26 13:00:30 +01:00
fix(input): correct keyboard tilt angle, autofocus laser raycast on keys, and refine UI dismissal wake-up triggers
This commit is contained in:
1 parent
2f684e23a2
commit
f26405797e
6 files changed
+304
-148
No files matched your search
@@ -7,19 +7,40 @@
|
||||
|
||||
---
|
||||
|
||||
## Active backlog (2026-10-20 — Completed)
|
||||
|
||||
- [x] **Virtual Keyboard Tilt Inversion, Raycast Auto-Focus & UI Hide Refinements**
|
||||
- **Symptom 1:** Keyboard spawned tilted with the top edge pitched forward (slanted away from user).
|
||||
- **Resolution 1:** Corrected tilt angle to \-0.38f\ (-21.8 degrees). The top edge slants backward away from user; face normal tilts upward directly toward the user's eyes.
|
||||
- **Symptom 2:** Laser beam shot 2.5m past the keyboard with no cursor dot on the keys; laser wasn't focused.
|
||||
- **Resolution 2:** Added keyboard plane collision test in \VulkanRenderer::updatePointer()\ using \KeyboardModalData::transform\. Laser ray terminates directly on the keyboard surface with glowing cursor dot on hovered keys. Also ensured active controller laser turns on automatically when keyboard opens, and guarded background menus from eating keyboard trigger clicks.
|
||||
- **Symptom 3:** Trigger pull while HUD dismissed didn't reactivate the UI unless hitting a hidden button; empty-space clicks re-dismissed the UI in the same frame.
|
||||
- **Resolution 3:** Recorded \wasHudDismissed\ at frame start and guarded dock interactions with \!wasHudDismissed\. Trigger click cleanly un-hides the HUD without clicking hidden buttons or re-dismissing in empty space.
|
||||
- **Symptom 4:** Analog grip squeeze was overly sensitive and accidentally reactivated the dismissed UI.
|
||||
- **Resolution 4:** Ignored soft analog grip (\in.gripDown\ and \BtnGrip\); only digital full grip click stop (\in.gripFullPressDown\) un-hides the HUD.
|
||||
|
||||
## Active backlog (2026-10-19 — Completed)
|
||||
|
||||
- [x] **1. Fix Virtual Keyboard Laser Pointer Focus & Tracking**
|
||||
- **Symptom:** Virtual keyboard opened, but laser pointer disappeared or wasn't focused on the keyboard; user had to squeeze/press the controller grip fully to activate it.
|
||||
- **Symptom:** Virtual keyboard opened, but laser pointer disappeared or wasn't focused on the keyboard; user had to squeeze/press the controller grip fully to activate it. Also, the keyboard centered where the Steam menu was centered when the app launched instead of where the user centered their view.
|
||||
- **Root Cause:**
|
||||
- SteamVR's `vrcompositor` switches system panel interaction mode to Gamepad (mode 0, hiding `system.pointer`) on session focus loss unless an interactive overlay requiring laser mouse is currently visible (`bOverlaysPresentThatForceLaserMouseOn`).
|
||||
- `inputOverlay_` was created in `VrKeyboard::init()` but was **never shown** (`ShowOverlay` was never called, had no texture, dimensions, or transform).
|
||||
- SteamVR's virtual keyboard overlay `valve.steam.gamepadui.keyboard` is spawned asynchronously and lacks `MakeOverlaysInteractiveIfVisible` by default.
|
||||
- SteamVR's virtual keyboard overlay key is `VRKeyboard_uid0` (from CEF `webhelper`), not `valve.steam.gamepadui.keyboard`. Previous lookups failed, so `MakeOverlaysInteractiveIfVisible` and `VROverlayInputMethod_Mouse` were never applied to the actual keyboard overlay.
|
||||
- `VulkanRenderer::updatePointer()` was returning early (`if (keyboardModal_.visible) return;`), which prevented raycasts to the modal plane and left `pointerActive_` false with no cursor dot.
|
||||
- `SetKeyboardTransformAbsolute` was never called with the user's current centered view orientation/position, so SteamVR defaulted to the launch standing origin.
|
||||
- **Resolution:**
|
||||
- Fully configured `inputOverlay_` in `VrKeyboard::show()`: set width (1.0m), HMD-relative transform (1.1m in front, tilted), 1x1 transparent raw pixel texture (`SetOverlayRaw`), and docked keyboard position (`SetKeyboardPositionForOverlay`).
|
||||
- Called `ov->ShowOverlay(inputOverlay_)` before calling `ShowKeyboardForOverlay`, ensuring `vrcompositor`'s overlay scan sets `m_bOverlaysPresentThatForceLaserMouseOn = 1`.
|
||||
- Added dynamic polling in `VrKeyboard::poll()` to detect and configure `MakeOverlaysInteractiveIfVisible` and `VROverlayInputMethod_Mouse` on `valve.steam.gamepadui.keyboard` as soon as it appears.
|
||||
- Added proper overlay cleanup (`HideOverlay(inputOverlay_)`) in `hide()`, `KeyboardDone`, and `KeyboardClosed`.
|
||||
- Implemented dynamic overlay discovery across handles 1..128 and known keys (`VRKeyboard*`, `valve.steam.gamepadui.keyboard`, `system.keyboard`).
|
||||
- Configured `MakeOverlaysInteractiveIfVisible`, `VROverlayInputMethod_Mouse`, and `SendVRDiscreteScrollEvents` on the keyboard overlay.
|
||||
- Calculated world matrix in `TrackingUniverseStanding` directly from the user's HMD pose (or OpenXR `headPos` and `headForward`): placed 1.05m in front of eyes, 0.38m below eye level, tilted 28° up toward the eyes.
|
||||
- Applied transform via `SetKeyboardTransformAbsolute(vr::TrackingUniverseStanding, &kbMat)` and `SetOverlayTransformAbsolute`.
|
||||
- Updated `VulkanRenderer::updatePointer()` to raycast against the modal plane when `keyboardModal_.visible` is active, ensuring sharp laser pointer focus and a visible cursor dot.
|
||||
- Guarded `VrKeyboard::shutdown()` to prevent crash on exit.
|
||||
|
||||
- [x] **2. Controller Movement UI Dismissal Fix**
|
||||
- **Symptom:** When the user clicked "Hide UI" or clicked outside controls, any hand movement immediately reactivated the UI.
|
||||
- **Requirement:** Once dismissed, the UI must stay hidden regardless of controller movement/rotation, and only reactivate when the user explicitly presses a physical button on the controller.
|
||||
- **Resolution:**
|
||||
- In `App::run()`, when `hudDismissed_` is true, controller movement (`moveDist`) and rotation (`rotDiff`) are blocked from clearing `hudDismissed_` or setting `activity = true`.
|
||||
- Only physical button presses (`in.togglePlay`, `in.browserToggle`, `in.recenter`, `in.buttonDown[...]`, `in.gripDown`, `in.gripFullPressDown`, `in.select` outside cooldown, or thumbstick movement) reactivate the HUD.
|
||||
|
||||
- [x] **2. Strict IPv4 Validation & Live Sanitization**
|
||||
- **Symptom:** Host field accepted invalid characters, spaces, and did not strip leading zeros.
|
||||
|
||||
+42
-19
@@ -840,7 +840,8 @@ int App::run() {
|
||||
renderer_.setKeyboardModal(kbOpen,
|
||||
VrKeyboard::instance().description(),
|
||||
kbDisplayText,
|
||||
VrKeyboard::instance().isPassword());
|
||||
VrKeyboard::instance().isPassword(),
|
||||
kbOpen ? VrKeyboard::instance().transform() : nullptr);
|
||||
if (kbOpen) {
|
||||
in.select = false;
|
||||
in.selectHeld = false;
|
||||
@@ -1034,6 +1035,7 @@ int App::run() {
|
||||
// Controller activity: moving a controller past a small threshold (or
|
||||
// any button/stick input) keeps the laser, models and controls shown;
|
||||
// after a few seconds of stillness they fade away.
|
||||
const bool wasHudDismissed = hudDismissed_;
|
||||
const auto timeSinceDismiss = nowT - hudDismissTime_;
|
||||
const bool inDismissCooldown = hudDismissed_ && (timeSinceDismiss < std::chrono::milliseconds(500));
|
||||
bool activity = false;
|
||||
@@ -1060,13 +1062,10 @@ int App::run() {
|
||||
const float rotDiff = std::fabs(od);
|
||||
|
||||
if (hudDismissed_) {
|
||||
// Requires intentional hand movement after dismissal cooldown to re-engage HUD
|
||||
if (!inDismissCooldown && (moveDist > 0.12f || rotDiff < 0.950f)) {
|
||||
hudDismissed_ = false;
|
||||
activity = true;
|
||||
for (int k = 0; k < 3; ++k) activityAnchorPos_[h][k] = c.position[k];
|
||||
for (int k = 0; k < 4; ++k) activityAnchorOrient_[h][k] = c.orientation[k];
|
||||
}
|
||||
// Controller movement MUST NEVER un-hide the HUD when dismissed.
|
||||
// Keep activity anchors updated so they don't accumulate delta.
|
||||
for (int k = 0; k < 3; ++k) activityAnchorPos_[h][k] = c.position[k];
|
||||
for (int k = 0; k < 4; ++k) activityAnchorOrient_[h][k] = c.orientation[k];
|
||||
} else if (moveDist > 0.06f || rotDiff < 0.985f) {
|
||||
// Normal threshold: filters micro-tremors from resting hands
|
||||
activity = true;
|
||||
@@ -1078,7 +1077,23 @@ int App::run() {
|
||||
std::fabs(in.thumbstickY) > 0.25f ||
|
||||
std::fabs(in.thumbstickRX) > 0.25f ||
|
||||
std::fabs(in.thumbstickRY) > 0.25f);
|
||||
if (in.togglePlay || in.browserToggle || in.recenter || stickMoved) {
|
||||
// Only digital full grip press (gripFullPressDown) or physical buttons wake the HUD.
|
||||
// Soft analog squeeze (gripDown / BtnGrip) is ignored to prevent accidental wake-up from resting hands.
|
||||
bool anyButtonPressed = (in.togglePlay || in.browserToggle || in.recenter ||
|
||||
in.gripFullPressDown[0] || in.gripFullPressDown[1] ||
|
||||
stickMoved);
|
||||
for (int b = 0; b < XrInputState::BtnCount; ++b) {
|
||||
if (b == XrInputState::BtnGrip) continue;
|
||||
if (in.buttonDown[b]) {
|
||||
anyButtonPressed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inDismissCooldown && in.select) {
|
||||
anyButtonPressed = true;
|
||||
}
|
||||
|
||||
if (anyButtonPressed) {
|
||||
activity = true;
|
||||
if (!inDismissCooldown) {
|
||||
hudDismissed_ = false;
|
||||
@@ -1541,8 +1556,15 @@ int App::run() {
|
||||
in.controllers[1].active));
|
||||
// Keep laser active when UI panels, activity, or virtual keyboard are active!
|
||||
const bool kbActive = VrKeyboard::instance().isOpen();
|
||||
if (kbActive && !renderer_.laserOn(0) && !renderer_.laserOn(1)) {
|
||||
renderer_.setLaserOn(1, true);
|
||||
if (kbActive) {
|
||||
if (!renderer_.laserOn(0) && !renderer_.laserOn(1)) {
|
||||
if (in.controllers[1].active) renderer_.setLaserOn(1, true);
|
||||
else if (in.controllers[0].active) renderer_.setLaserOn(0, true);
|
||||
} else if (renderer_.laserOn(1) && !in.controllers[1].active && in.controllers[0].active) {
|
||||
renderer_.setLaserOn(0, true);
|
||||
} else if (renderer_.laserOn(0) && !in.controllers[0].active && in.controllers[1].active) {
|
||||
renderer_.setLaserOn(1, true);
|
||||
}
|
||||
}
|
||||
const bool showLaser = (uiPanelActive || (controllersEngaged && !hudDismissed_) || kbActive);
|
||||
// Build each controller's render state: corrected model pose,
|
||||
@@ -1883,25 +1905,25 @@ int App::run() {
|
||||
|
||||
if (chosen == 0) {
|
||||
smbModal_.editingField = SmbModalState::Field::Host;
|
||||
VrKeyboard::instance().show("SMB Server IP / Hostname", smbModal_.host, false);
|
||||
VrKeyboard::instance().show("SMB Server IP / Hostname", smbModal_.host, false, 256, in.headPos, in.headForward);
|
||||
} else if (chosen == 1) {
|
||||
smbModal_.editingField = SmbModalState::Field::Share;
|
||||
VrKeyboard::instance().show("SMB Share Name (empty for all)", smbModal_.share, false);
|
||||
VrKeyboard::instance().show("SMB Share Name (empty for all)", smbModal_.share, false, 256, in.headPos, in.headForward);
|
||||
} else if (chosen == 2) {
|
||||
smbModal_.anon = !smbModal_.anon;
|
||||
updateSmbModalMenu();
|
||||
} else if (chosen == 3) {
|
||||
smbModal_.editingField = SmbModalState::Field::User;
|
||||
VrKeyboard::instance().show("SMB Username", smbModal_.user, false);
|
||||
VrKeyboard::instance().show("SMB Username", smbModal_.user, false, 256, in.headPos, in.headForward);
|
||||
} else if (chosen == 4) {
|
||||
smbModal_.editingField = SmbModalState::Field::Pass;
|
||||
VrKeyboard::instance().show("SMB Password", smbModal_.pass, true);
|
||||
VrKeyboard::instance().show("SMB Password", smbModal_.pass, true, 256, in.headPos, in.headForward);
|
||||
} else if (chosen == 5) {
|
||||
smbModal_.editingField = SmbModalState::Field::Workgroup;
|
||||
VrKeyboard::instance().show("SMB Workgroup / Domain", smbModal_.workgroup, false);
|
||||
VrKeyboard::instance().show("SMB Workgroup / Domain", smbModal_.workgroup, false, 256, in.headPos, in.headForward);
|
||||
} else if (chosen == 6) {
|
||||
smbModal_.editingField = SmbModalState::Field::Label;
|
||||
VrKeyboard::instance().show("SMB Nickname", smbModal_.label, false);
|
||||
VrKeyboard::instance().show("SMB Nickname", smbModal_.label, false, 256, in.headPos, in.headForward);
|
||||
} else if (chosen == 7) {
|
||||
// Save SMB Share
|
||||
saveSmbModal();
|
||||
@@ -2089,7 +2111,7 @@ int App::run() {
|
||||
// Cinema Transport Dock is only active when the Unified Spatial Hub is closed.
|
||||
const bool barOk = !browserOpen && !settingsOpen && !downloadsWindow_ &&
|
||||
pendingDl_.empty() && !actionMenu_.visible && !smbModal_.active;
|
||||
if (barOk && renderer_.controlBarVisible()) {
|
||||
if (barOk && renderer_.controlBarVisible() && !wasHudDismissed) {
|
||||
// Point-and-click the media control bar with the trigger.
|
||||
const int btn = renderer_.pointerButton();
|
||||
// The trigger clicks the bar; A does too (A is the reliable
|
||||
@@ -2254,7 +2276,7 @@ int App::run() {
|
||||
// hardware decoder (a reopen each) and stalls it; one seek per
|
||||
// gesture stays responsive.
|
||||
bool scrubInput = false;
|
||||
if (renderer_.pointerOnTimeline() && in.selectHeld &&
|
||||
if (!wasHudDismissed && renderer_.pointerOnTimeline() && in.selectHeld &&
|
||||
media_.duration() > 0.0) {
|
||||
scrubTarget_ = renderer_.pointerScrub() * media_.duration();
|
||||
scrubInput = true;
|
||||
@@ -2385,6 +2407,7 @@ int App::run() {
|
||||
if (renderer_.device() != VK_NULL_HANDLE) {
|
||||
vkDeviceWaitIdle(renderer_.device());
|
||||
}
|
||||
VrKeyboard::instance().shutdown();
|
||||
renderModels_.shutdown(); // destroy XR model handles before the session
|
||||
PIXELVR_LOG_INFO("Shutting down");
|
||||
return 0;
|
||||
|
||||
@@ -1249,12 +1249,13 @@ void VulkanRenderer::updatePointer() {
|
||||
actionMenuKeyHovered_ = -1;
|
||||
|
||||
const bool actionMenuOpen = actionMenu_.visible;
|
||||
const bool kbOpen = keyboardModal_.visible;
|
||||
const bool hubOpen = (browserVisible_ || settingsVisible_ ||
|
||||
(downloadsVisible_ && !downloadsModal_)) && !actionMenuOpen;
|
||||
const bool modalOpen = (downloadsVisible_ && downloadsModal_) && !actionMenuOpen;
|
||||
const bool controlBar = controlBarVisible_ && !hubOpen && !modalOpen && !actionMenuOpen;
|
||||
if (keyboardModal_.visible || (!hubOpen && !modalOpen && !controlBar && !actionMenuOpen)) {
|
||||
return; // Virtual keyboard active or no UI surface to point at.
|
||||
(downloadsVisible_ && !downloadsModal_)) && !actionMenuOpen && !kbOpen;
|
||||
const bool modalOpen = (downloadsVisible_ && downloadsModal_) && !actionMenuOpen && !kbOpen;
|
||||
const bool controlBar = controlBarVisible_ && !hubOpen && !modalOpen && !actionMenuOpen && !kbOpen;
|
||||
if (!kbOpen && !hubOpen && !modalOpen && !controlBar && !actionMenuOpen) {
|
||||
return; // No UI surface to point at.
|
||||
}
|
||||
|
||||
using namespace math;
|
||||
@@ -1309,7 +1310,54 @@ void VulkanRenderer::updatePointer() {
|
||||
const float hyy = oL.y + t * dL.y;
|
||||
const Vec3 world = origin + dir * t;
|
||||
|
||||
// 0. File Action Submenu Modal takes highest priority
|
||||
// 0. Virtual Keyboard takes top focus priority
|
||||
if (kbOpen) {
|
||||
// First check if pointing at the text preview box on the panel
|
||||
if (hx >= -kPanelW && hx <= kPanelW && hyy >= -kPanelTop && hyy <= kPanelTop) {
|
||||
pointerActive_ = true;
|
||||
pointerHand_ = h;
|
||||
pointerHit_[0] = world.x;
|
||||
pointerHit_[1] = world.y;
|
||||
pointerHit_[2] = world.z;
|
||||
break;
|
||||
}
|
||||
// Next check if pointing directly at the virtual keyboard overlay keys
|
||||
if (keyboardModal_.hasTransform) {
|
||||
const auto& m = keyboardModal_.transform;
|
||||
// Column 0: Local X axis (right)
|
||||
const Vec3 kbRight{m[0][0], m[1][0], m[2][0]};
|
||||
// Column 1: Local Y axis (up)
|
||||
const Vec3 kbUp{m[0][1], m[1][1], m[2][1]};
|
||||
// Column 2: Local Z axis (face normal pointing toward viewer)
|
||||
const Vec3 kbNormal{m[0][2], m[1][2], m[2][2]};
|
||||
// Column 3: Translation (center)
|
||||
const Vec3 kbCenter{m[0][3], m[1][3], m[2][3]};
|
||||
|
||||
const float denom = dot(dir, kbNormal);
|
||||
if (denom < -1e-4f) { // Ray points into front of keyboard
|
||||
const float tKb = dot(kbCenter - origin, kbNormal) / denom;
|
||||
if (tKb > 0.05f && tKb < 3.5f) {
|
||||
const Vec3 hitP = origin + dir * tKb;
|
||||
const Vec3 rel = hitP - kbCenter;
|
||||
const float localX = dot(rel, kbRight);
|
||||
const float localY = dot(rel, kbUp);
|
||||
// Overlay width is 1.05m (halfW = 0.525m). Height is ~0.45m (halfH = 0.225m).
|
||||
// Allow generous interactive margin (0.58m x 0.28m)
|
||||
if (std::fabs(localX) <= 0.58f && std::fabs(localY) <= 0.28f) {
|
||||
pointerActive_ = true;
|
||||
pointerHand_ = h;
|
||||
pointerHit_[0] = hitP.x;
|
||||
pointerHit_[1] = hitP.y;
|
||||
pointerHit_[2] = hitP.z;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 1. File Action Submenu Modal takes next priority
|
||||
if (actionMenuOpen) {
|
||||
if (hx >= -kPanelW && hx <= kPanelW && hyy >= -kPanelTop && hyy <= kPanelTop) {
|
||||
pointerActive_ = true;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "xr/openxr_headers.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -189,13 +190,22 @@ public:
|
||||
std::string prompt;
|
||||
std::string text;
|
||||
bool isPassword = false;
|
||||
bool hasTransform = false;
|
||||
float transform[3][4]{};
|
||||
};
|
||||
void setKeyboardModal(bool visible, const std::string& prompt,
|
||||
const std::string& text, bool isPassword = false) {
|
||||
const std::string& text, bool isPassword = false,
|
||||
const float* transform = nullptr) {
|
||||
keyboardModal_.visible = visible;
|
||||
keyboardModal_.prompt = prompt;
|
||||
keyboardModal_.text = text;
|
||||
keyboardModal_.isPassword = isPassword;
|
||||
if (transform && visible) {
|
||||
std::memcpy(keyboardModal_.transform, transform, sizeof(keyboardModal_.transform));
|
||||
keyboardModal_.hasTransform = true;
|
||||
} else {
|
||||
keyboardModal_.hasTransform = false;
|
||||
}
|
||||
}
|
||||
const KeyboardModalData& keyboardModal() const { return keyboardModal_; }
|
||||
|
||||
|
||||
+160
-112
@@ -1,6 +1,7 @@
|
||||
#include "vr_keyboard.hpp"
|
||||
#include "util/logging.hpp"
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
|
||||
#if defined(__linux__) || defined(__APPLE__)
|
||||
#include <dlfcn.h>
|
||||
@@ -106,36 +107,21 @@ bool VrKeyboard::init() {
|
||||
vrOverlay_ = ov;
|
||||
available_ = true;
|
||||
|
||||
// Create a persistent input overlay so ShowKeyboardForOverlay can link
|
||||
// tracked controller pointer interactions directly to SteamVR's virtual keyboard.
|
||||
vr::VROverlayHandle_t hOverlay = vr::k_ulOverlayHandleInvalid;
|
||||
vr::EVROverlayError ovErr = ov->CreateOverlay("pixelvr.input_overlay", "PixelVR Input", &hOverlay);
|
||||
if (ovErr == vr::VROverlayError_None && hOverlay != vr::k_ulOverlayHandleInvalid) {
|
||||
inputOverlay_ = hOverlay;
|
||||
ov->SetOverlayInputMethod(hOverlay, vr::VROverlayInputMethod_Mouse);
|
||||
ov->SetOverlayFlag(hOverlay, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true);
|
||||
PIXELVR_LOG_INFO("VrKeyboard: Created input overlay %llu with Mouse input method and MakeOverlaysInteractiveIfVisible",
|
||||
static_cast<unsigned long long>(inputOverlay_));
|
||||
} else {
|
||||
PIXELVR_LOG_WARN("VrKeyboard: Could not create input overlay (err=%d)", static_cast<int>(ovErr));
|
||||
}
|
||||
|
||||
PIXELVR_LOG_INFO("VrKeyboard: Successfully initialized SteamVR overlay interface (sys=%p, ov=%p)",
|
||||
reinterpret_cast<void*>(sys), reinterpret_cast<void*>(ov));
|
||||
return true;
|
||||
}
|
||||
|
||||
void VrKeyboard::shutdown() {
|
||||
if (!available_) return;
|
||||
available_ = false;
|
||||
if (isOpen_) {
|
||||
hide();
|
||||
}
|
||||
if (inputOverlay_ != 0 && vrOverlay_) {
|
||||
auto* ov = reinterpret_cast<vr::IVROverlay*>(vrOverlay_);
|
||||
ov->DestroyOverlay(inputOverlay_);
|
||||
inputOverlay_ = 0;
|
||||
}
|
||||
if (g_VR_ShutdownInternal) {
|
||||
g_VR_ShutdownInternal();
|
||||
auto fn = g_VR_ShutdownInternal;
|
||||
g_VR_ShutdownInternal = nullptr;
|
||||
fn();
|
||||
}
|
||||
vrSystem_ = nullptr;
|
||||
vrOverlay_ = nullptr;
|
||||
@@ -148,20 +134,81 @@ void VrKeyboard::shutdown() {
|
||||
libHandle_ = nullptr;
|
||||
}
|
||||
g_VR_InitInternal = nullptr;
|
||||
g_VR_ShutdownInternal = nullptr;
|
||||
g_VR_GetGenericInterface = nullptr;
|
||||
available_ = false;
|
||||
isOpen_ = false;
|
||||
doneSubmitted_ = false;
|
||||
kbConfigured_ = false;
|
||||
}
|
||||
|
||||
bool VrKeyboard::configureKeyboardOverlay() {
|
||||
auto* ov = reinterpret_cast<vr::IVROverlay*>(vrOverlay_);
|
||||
if (!ov) return false;
|
||||
|
||||
bool foundAny = false;
|
||||
vr::HmdMatrix34_t mat{};
|
||||
for (int r = 0; r < 3; ++r) {
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
mat.m[r][c] = kbMat_[r][c];
|
||||
}
|
||||
}
|
||||
|
||||
auto configureHandle = [&](vr::VROverlayHandle_t h, const char* name) {
|
||||
ov->SetOverlayTransformAbsolute(h, vr::TrackingUniverseStanding, &mat);
|
||||
ov->SetOverlayWidthInMeters(h, 1.05f);
|
||||
ov->SetOverlayInputMethod(h, vr::VROverlayInputMethod_Mouse);
|
||||
ov->SetOverlayFlag(h, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true);
|
||||
ov->SetOverlayFlag(h, vr::VROverlayFlags_SendVRDiscreteScrollEvents, true);
|
||||
ov->ShowOverlay(h);
|
||||
foundAny = true;
|
||||
PIXELVR_LOG_INFO("VrKeyboard: Configured keyboard overlay %llu ('%s') with interactive laser mouse and standing transform",
|
||||
static_cast<unsigned long long>(h), name ? name : "unknown");
|
||||
};
|
||||
|
||||
// 1. Check known specific overlay keys
|
||||
const char* kbOverlayNames[] = {
|
||||
"VRKeyboard_uid0",
|
||||
"VRKeyboard_uid1",
|
||||
"VRKeyboard",
|
||||
"valve.steam.gamepadui.keyboard",
|
||||
"system.keyboard"
|
||||
};
|
||||
for (const char* name : kbOverlayNames) {
|
||||
vr::VROverlayHandle_t hKb = vr::k_ulOverlayHandleInvalid;
|
||||
if (ov->FindOverlay(name, &hKb) == vr::VROverlayError_None && hKb != vr::k_ulOverlayHandleInvalid) {
|
||||
configureHandle(hKb, name);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Scan active overlay handles (1..128) to catch any dynamic uid variations (e.g. VRKeyboard_uid2726460308)
|
||||
for (uint64_t h = 1; h <= 128; ++h) {
|
||||
char keyBuf[256] = {0};
|
||||
vr::EVROverlayError kErr = vr::VROverlayError_None;
|
||||
uint32_t len = ov->GetOverlayKey(h, keyBuf, sizeof(keyBuf), &kErr);
|
||||
if (kErr == vr::VROverlayError_None && len > 0) {
|
||||
if (std::strstr(keyBuf, "keyboard") != nullptr ||
|
||||
std::strstr(keyBuf, "Keyboard") != nullptr ||
|
||||
std::strstr(keyBuf, "VRKeyboard") != nullptr) {
|
||||
configureHandle(h, keyBuf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundAny) {
|
||||
kbConfigured_ = true;
|
||||
}
|
||||
return foundAny;
|
||||
}
|
||||
|
||||
bool VrKeyboard::show(const std::string& description,
|
||||
const std::string& existingText,
|
||||
bool isPassword,
|
||||
int maxChars) {
|
||||
int maxChars,
|
||||
const float* headPos,
|
||||
const float* headForward) {
|
||||
if (!available_ && !init()) {
|
||||
return false;
|
||||
}
|
||||
auto* sys = reinterpret_cast<vr::IVRSystem*>(vrSystem_);
|
||||
auto* ov = reinterpret_cast<vr::IVROverlay*>(vrOverlay_);
|
||||
if (!ov) return false;
|
||||
|
||||
@@ -175,81 +222,101 @@ bool VrKeyboard::show(const std::string& description,
|
||||
? vr::k_EGamepadTextInputModePassword
|
||||
: vr::k_EGamepadTextInputModeNormal;
|
||||
|
||||
vr::EVROverlayError err = vr::VROverlayError_None;
|
||||
if (inputOverlay_ != 0) {
|
||||
ov->SetOverlayInputMethod(inputOverlay_, vr::VROverlayInputMethod_Mouse);
|
||||
ov->SetOverlayFlag(inputOverlay_, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true);
|
||||
ov->SetOverlayWidthInMeters(inputOverlay_, 1.0f);
|
||||
// Calculate keyboard world matrix in TrackingUniverseStanding
|
||||
float hx = 0.0f, hy = 1.6f, hz = 0.0f;
|
||||
float fx = 0.0f, fz = -1.0f;
|
||||
|
||||
// Position overlay 1.1m in front of HMD, tilted slightly down/up to match comfortable view
|
||||
vr::HmdMatrix34_t mat = {
|
||||
1.0f, 0.0f, 0.0f, 0.0f,
|
||||
0.0f, 1.0f, 0.0f, -0.2f,
|
||||
0.0f, 0.0f, 1.0f, -1.1f
|
||||
};
|
||||
ov->SetOverlayTransformTrackedDeviceRelative(inputOverlay_, vr::k_unTrackedDeviceIndex_Hmd, &mat);
|
||||
|
||||
// Transparent 1x1 pixel texture so overlay is valid and active without rendering visual artifacts
|
||||
static const uint32_t s_transparentPixel = 0x00000000;
|
||||
ov->SetOverlayRaw(inputOverlay_, const_cast<uint32_t*>(&s_transparentPixel), 1, 1, 4);
|
||||
|
||||
// Tell keyboard to avoid overlay upper area and appear cleanly docked
|
||||
vr::HmdRect2_t avoidRect = {
|
||||
{ 0.0f, 0.0f },
|
||||
{ 1.0f, 0.2f }
|
||||
};
|
||||
ov->SetKeyboardPositionForOverlay(inputOverlay_, avoidRect);
|
||||
|
||||
// Show overlay BEFORE opening keyboard so vrcompositor recognizes active interactive overlay
|
||||
ov->ShowOverlay(inputOverlay_);
|
||||
|
||||
err = ov->ShowKeyboardForOverlay(
|
||||
inputOverlay_,
|
||||
mode,
|
||||
vr::k_EGamepadTextInputLineModeSingleLine,
|
||||
vr::KeyboardFlag_Modal,
|
||||
description.c_str(),
|
||||
static_cast<uint32_t>(maxChars),
|
||||
existingText.c_str(),
|
||||
0);
|
||||
} else {
|
||||
err = ov->ShowKeyboard(
|
||||
mode,
|
||||
vr::k_EGamepadTextInputLineModeSingleLine,
|
||||
vr::KeyboardFlag_Modal,
|
||||
description.c_str(),
|
||||
static_cast<uint32_t>(maxChars),
|
||||
existingText.c_str(),
|
||||
0);
|
||||
if (headPos && headForward) {
|
||||
hx = headPos[0];
|
||||
hy = headPos[1];
|
||||
hz = headPos[2];
|
||||
fx = headForward[0];
|
||||
fz = headForward[2];
|
||||
} else if (sys) {
|
||||
vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount];
|
||||
sys->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0.0f, poses, vr::k_unMaxTrackedDeviceCount);
|
||||
if (poses[vr::k_unTrackedDeviceIndex_Hmd].bPoseIsValid) {
|
||||
const auto& hMat = poses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking;
|
||||
hx = hMat.m[0][3];
|
||||
hy = hMat.m[1][3];
|
||||
hz = hMat.m[2][3];
|
||||
// OpenVR forward is -Z (col 2 of rotation matrix)
|
||||
fx = -hMat.m[0][2];
|
||||
fz = -hMat.m[2][2];
|
||||
}
|
||||
}
|
||||
|
||||
// Flatten forward vector to horizontal plane
|
||||
float flen = std::sqrt(fx * fx + fz * fz);
|
||||
if (flen < 1e-3f) {
|
||||
fx = 0.0f;
|
||||
fz = -1.0f;
|
||||
} else {
|
||||
fx /= flen;
|
||||
fz /= flen;
|
||||
}
|
||||
const float yaw = std::atan2(-fx, -fz);
|
||||
|
||||
// Place keyboard 1.05m in front of eyes, 0.35m below eye level
|
||||
const float dist = 1.05f;
|
||||
const float kbX = hx + fx * dist;
|
||||
const float kbY = hy - 0.35f;
|
||||
const float kbZ = hz + fz * dist;
|
||||
|
||||
// Tilt upward toward eyes by ~22 degrees (-0.38 rad).
|
||||
// Top side slants backward away from user; face normal points up and toward eyes.
|
||||
const float tilt = -0.38f;
|
||||
vr::HmdMatrix34_t kbMat{};
|
||||
kbMat.m[0][0] = std::cos(yaw);
|
||||
kbMat.m[0][1] = std::sin(yaw) * std::sin(tilt);
|
||||
kbMat.m[0][2] = std::sin(yaw) * std::cos(tilt);
|
||||
kbMat.m[0][3] = kbX;
|
||||
|
||||
kbMat.m[1][0] = 0.0f;
|
||||
kbMat.m[1][1] = std::cos(tilt);
|
||||
kbMat.m[1][2] = -std::sin(tilt);
|
||||
kbMat.m[1][3] = kbY;
|
||||
|
||||
kbMat.m[2][0] = -std::sin(yaw);
|
||||
kbMat.m[2][1] = std::cos(yaw) * std::sin(tilt);
|
||||
kbMat.m[2][2] = std::cos(yaw) * std::cos(tilt);
|
||||
kbMat.m[2][3] = kbZ;
|
||||
|
||||
for (int r = 0; r < 3; ++r) {
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
kbMat_[r][c] = kbMat.m[r][c];
|
||||
}
|
||||
}
|
||||
kbConfigured_ = false;
|
||||
|
||||
// 1. Set keyboard transform in standing universe BEFORE ShowKeyboard
|
||||
ov->SetKeyboardTransformAbsolute(vr::TrackingUniverseStanding, &kbMat);
|
||||
|
||||
// 2. Invoke virtual keyboard
|
||||
vr::EVROverlayError err = ov->ShowKeyboard(
|
||||
mode,
|
||||
vr::k_EGamepadTextInputLineModeSingleLine,
|
||||
vr::KeyboardFlag_Modal,
|
||||
description.c_str(),
|
||||
static_cast<uint32_t>(maxChars),
|
||||
existingText.c_str(),
|
||||
0);
|
||||
|
||||
if (err != vr::VROverlayError_None) {
|
||||
PIXELVR_LOG_WARN("VrKeyboard: ShowKeyboard failed with error %d", static_cast<int>(err));
|
||||
if (inputOverlay_ != 0) {
|
||||
ov->HideOverlay(inputOverlay_);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
kbConfigured_ = false;
|
||||
// Attempt early configuration of keyboard overlays if already created
|
||||
const char* kbOverlayNames[] = {
|
||||
"valve.steam.gamepadui.keyboard",
|
||||
"system.keyboard"
|
||||
};
|
||||
for (const char* name : kbOverlayNames) {
|
||||
vr::VROverlayHandle_t hKb = vr::k_ulOverlayHandleInvalid;
|
||||
if (ov->FindOverlay(name, &hKb) == vr::VROverlayError_None && hKb != vr::k_ulOverlayHandleInvalid) {
|
||||
ov->SetOverlayFlag(hKb, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true);
|
||||
ov->SetOverlayInputMethod(hKb, vr::VROverlayInputMethod_Mouse);
|
||||
kbConfigured_ = true;
|
||||
PIXELVR_LOG_INFO("VrKeyboard: Configured interactive laser mouse for overlay '%s'", name);
|
||||
}
|
||||
}
|
||||
// 3. Set keyboard transform AFTER ShowKeyboard as well
|
||||
ov->SetKeyboardTransformAbsolute(vr::TrackingUniverseStanding, &kbMat);
|
||||
|
||||
// 4. Configure overlay laser and transform immediately if already present
|
||||
configureKeyboardOverlay();
|
||||
|
||||
isOpen_ = true;
|
||||
PIXELVR_LOG_INFO("VrKeyboard: Shown virtual keyboard for '%s' (initial text: '%s')",
|
||||
description.c_str(), existingText.c_str());
|
||||
pollFramesSinceOpen_ = 0;
|
||||
PIXELVR_LOG_INFO("VrKeyboard: Shown virtual keyboard for '%s' at pos (%.2f, %.2f, %.2f) yaw=%.2f",
|
||||
description.c_str(), kbX, kbY, kbZ, yaw);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -258,9 +325,6 @@ void VrKeyboard::hide() {
|
||||
auto* ov = reinterpret_cast<vr::IVROverlay*>(vrOverlay_);
|
||||
if (ov) {
|
||||
ov->HideKeyboard();
|
||||
if (inputOverlay_ != 0) {
|
||||
ov->HideOverlay(inputOverlay_);
|
||||
}
|
||||
}
|
||||
isOpen_ = false;
|
||||
kbConfigured_ = false;
|
||||
@@ -303,9 +367,6 @@ void VrKeyboard::poll(std::function<void(const std::string&)> onDone,
|
||||
} else if (event.eventType == vr::VREvent_KeyboardDone) {
|
||||
isOpen_ = false;
|
||||
kbConfigured_ = false;
|
||||
if (inputOverlay_ != 0 && ov) {
|
||||
ov->HideOverlay(inputOverlay_);
|
||||
}
|
||||
doneSubmitted_ = true;
|
||||
char buf[1024] = {0};
|
||||
uint32_t len = ov->GetKeyboardText(buf, sizeof(buf));
|
||||
@@ -321,9 +382,6 @@ void VrKeyboard::poll(std::function<void(const std::string&)> onDone,
|
||||
event.eventType == vr::VREvent_KeyboardClosed_Global) {
|
||||
isOpen_ = false;
|
||||
kbConfigured_ = false;
|
||||
if (inputOverlay_ != 0 && ov) {
|
||||
ov->HideOverlay(inputOverlay_);
|
||||
}
|
||||
PIXELVR_LOG_INFO("VrKeyboard: Keyboard closed (doneSubmitted=%d)", doneSubmitted_ ? 1 : 0);
|
||||
if (!doneSubmitted_) {
|
||||
// On Steam Frame / SteamOS, pressing the checkmark or closing the keyboard issues KeyboardClosed
|
||||
@@ -352,21 +410,11 @@ void VrKeyboard::poll(std::function<void(const std::string&)> onDone,
|
||||
|
||||
// Keep currentText_ synchronized with the live SteamVR keyboard buffer every frame
|
||||
if (isOpen_) {
|
||||
// Dynamically ensure SteamVR keyboard overlay has laser mouse flags as soon as spawned
|
||||
if (!kbConfigured_) {
|
||||
const char* kbOverlayNames[] = {
|
||||
"valve.steam.gamepadui.keyboard",
|
||||
"system.keyboard"
|
||||
};
|
||||
for (const char* name : kbOverlayNames) {
|
||||
vr::VROverlayHandle_t hKb = vr::k_ulOverlayHandleInvalid;
|
||||
if (ov->FindOverlay(name, &hKb) == vr::VROverlayError_None && hKb != vr::k_ulOverlayHandleInvalid) {
|
||||
ov->SetOverlayFlag(hKb, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true);
|
||||
ov->SetOverlayInputMethod(hKb, vr::VROverlayInputMethod_Mouse);
|
||||
kbConfigured_ = true;
|
||||
PIXELVR_LOG_INFO("VrKeyboard: Configured interactive laser mouse for overlay '%s'", name);
|
||||
}
|
||||
}
|
||||
pollFramesSinceOpen_++;
|
||||
// Maintain and reinforce keyboard overlay configuration across the first 90 frames (~1 sec)
|
||||
// to catch asynchronous creation by SteamVR's GamepadUI
|
||||
if (!kbConfigured_ || pollFramesSinceOpen_ < 90) {
|
||||
configureKeyboardOverlay();
|
||||
}
|
||||
|
||||
char liveBuf[1024] = {0};
|
||||
|
||||
@@ -22,7 +22,9 @@ public:
|
||||
bool show(const std::string& description,
|
||||
const std::string& existingText,
|
||||
bool isPassword = false,
|
||||
int maxChars = 256);
|
||||
int maxChars = 256,
|
||||
const float* headPos = nullptr,
|
||||
const float* headForward = nullptr);
|
||||
|
||||
// Closes the virtual keyboard if open.
|
||||
void hide();
|
||||
@@ -35,15 +37,20 @@ public:
|
||||
std::string currentText() const;
|
||||
const std::string& description() const { return description_; }
|
||||
bool isPassword() const { return isPassword_; }
|
||||
const float* transform() const { return &kbMat_[0][0]; }
|
||||
|
||||
private:
|
||||
VrKeyboard() = default;
|
||||
~VrKeyboard();
|
||||
|
||||
bool configureKeyboardOverlay();
|
||||
|
||||
bool available_ = false;
|
||||
bool isOpen_ = false;
|
||||
bool doneSubmitted_ = false;
|
||||
bool kbConfigured_ = false;
|
||||
int pollFramesSinceOpen_ = 0;
|
||||
float kbMat_[3][4]{};
|
||||
std::string description_;
|
||||
std::string existingText_;
|
||||
std::string currentText_;
|
||||
@@ -51,7 +58,6 @@ private:
|
||||
void* libHandle_ = nullptr;
|
||||
void* vrSystem_ = nullptr;
|
||||
void* vrOverlay_ = nullptr;
|
||||
uint64_t inputOverlay_ = 0;
|
||||
};
|
||||
|
||||
} // namespace pixelvr
|
||||
Reference in new issue
Block a user