fix(keyboard): correct floor vs eye coordinate transform and eliminate persistent draft buffer

This commit is contained in:
Pixelplanet committed 2026-10-20 15:32:18 +02:00
1 parent ec9821f1af
commit 3d11ccbbf8
6 files changed
+253 -115

No files matched your search

+15 -12
View File
@@ -1920,27 +1920,30 @@ int App::run() {
xr_.triggerHaptic(0, 0.025f, 0.0f, 0.5f);
xr_.triggerHaptic(1, 0.025f, 0.0f, 0.5f);
auto openKeyboardForField = [&](SmbModalState::Field field, const std::string& desc, const std::string& currentVal, bool isPass) {
smbModal_.editingField = field;
float stagePos[3]{0.0f, 1.65f, 0.0f};
float stageFwd[3]{0.0f, 0.0f, -1.0f};
const float* fwd = renderer_.screenForward();
xr_.transformToStage(in.headPos, fwd, stagePos, stageFwd);
VrKeyboard::instance().show(desc, currentVal, isPass, 256, in.headPos, fwd, stagePos, stageFwd);
};
if (chosen == 0) {
smbModal_.editingField = SmbModalState::Field::Host;
VrKeyboard::instance().show("SMB Server IP / Hostname", smbModal_.host, false, 256, in.headPos, in.headForward);
openKeyboardForField(SmbModalState::Field::Host, "SMB Server IP / Hostname", smbModal_.host, false);
} else if (chosen == 1) {
smbModal_.editingField = SmbModalState::Field::Share;
VrKeyboard::instance().show("SMB Share Name (empty for all)", smbModal_.share, false, 256, in.headPos, in.headForward);
openKeyboardForField(SmbModalState::Field::Share, "SMB Share Name (empty for all)", smbModal_.share, false);
} 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, 256, in.headPos, in.headForward);
openKeyboardForField(SmbModalState::Field::User, "SMB Username", smbModal_.user, false);
} else if (chosen == 4) {
smbModal_.editingField = SmbModalState::Field::Pass;
VrKeyboard::instance().show("SMB Password", smbModal_.pass, true, 256, in.headPos, in.headForward);
openKeyboardForField(SmbModalState::Field::Pass, "SMB Password", smbModal_.pass, true);
} else if (chosen == 5) {
smbModal_.editingField = SmbModalState::Field::Workgroup;
VrKeyboard::instance().show("SMB Workgroup / Domain", smbModal_.workgroup, false, 256, in.headPos, in.headForward);
openKeyboardForField(SmbModalState::Field::Workgroup, "SMB Workgroup / Domain", smbModal_.workgroup, false);
} else if (chosen == 6) {
smbModal_.editingField = SmbModalState::Field::Label;
VrKeyboard::instance().show("SMB Nickname", smbModal_.label, false, 256, in.headPos, in.headForward);
openKeyboardForField(SmbModalState::Field::Label, "SMB Nickname", smbModal_.label, false);
} else if (chosen == 7) {
// Save SMB Share
saveSmbModal();
+2
View File
@@ -96,6 +96,8 @@ public:
// Repositions the flat screen in front of the given head pose (headForward is
// a world-space direction); resets the viewing distance to the default.
void recenterScreen(const float headPos[3], const float headForward[3]);
const float* screenPivot() const { return screenPivot_; }
const float* screenForward() const { return screenForward_; }
// Moves the flat screen farther (positive) or nearer (negative), clamped.
void adjustScreenDistance(float delta);
// Parametric curvature for cinema screen: 0.0 = flat plane, 1.0 = full cylinder wrap.
+151 -99
View File
@@ -2,6 +2,7 @@
#include "util/logging.hpp"
#include <cstring>
#include <cmath>
#include <chrono>
#if defined(__linux__) || defined(__APPLE__)
#include <dlfcn.h>
@@ -23,6 +24,8 @@ Fn_VR_InitInternal g_VR_InitInternal = nullptr;
Fn_VR_ShutdownInternal g_VR_ShutdownInternal = nullptr;
Fn_VR_GetGenericInterface g_VR_GetGenericInterface = nullptr;
static uint64_t sKeyboardNonce = 1000;
} // namespace
VrKeyboard& VrKeyboard::instance() {
@@ -138,6 +141,7 @@ void VrKeyboard::shutdown() {
isOpen_ = false;
doneSubmitted_ = false;
kbConfigured_ = false;
currentNonce_ = 0;
}
bool VrKeyboard::configureKeyboardOverlay() {
@@ -148,7 +152,7 @@ bool VrKeyboard::configureKeyboardOverlay() {
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];
mat.m[r][c] = kbMatStage_[r][c];
}
}
@@ -160,8 +164,9 @@ bool VrKeyboard::configureKeyboardOverlay() {
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");
PIXELVR_LOG_INFO("VrKeyboard: Configured keyboard overlay %llu ('%s') with interactive laser mouse at standing pos (%.2f, %.2f, %.2f)",
static_cast<unsigned long long>(h), name ? name : "unknown",
mat.m[0][3], mat.m[1][3], mat.m[2][3]);
};
// 1. Check known specific overlay keys
@@ -203,25 +208,28 @@ bool VrKeyboard::show(const std::string& description,
const std::string& existingText,
bool isPassword,
int maxChars,
const float* headPos,
const float* headForward) {
const float* headPosLocal,
const float* headForwardLocal,
const float* headPosStage,
const float* headForwardStage) {
if (!available_ && !init()) {
return false;
}
auto* sys = reinterpret_cast<vr::IVRSystem*>(vrSystem_);
auto* ov = reinterpret_cast<vr::IVROverlay*>(vrOverlay_);
if (!ov) return false;
if (isOpen_) {
hide();
}
// Explicitly hide any existing keyboard session in SteamVR to clear lingering text buffers
// Explicitly hide any existing keyboard session in SteamVR to clear lingering state
ov->HideKeyboard();
currentNonce_ = ++sKeyboardNonce;
description_ = description;
existingText_ = existingText;
currentText_ = existingText;
isPassword_ = isPassword;
maxChars_ = maxChars;
doneSubmitted_ = false;
userTyped_ = false;
@@ -229,31 +237,18 @@ bool VrKeyboard::show(const std::string& description,
? vr::k_EGamepadTextInputModePassword
: vr::k_EGamepadTextInputModeNormal;
// Calculate keyboard world matrix in TrackingUniverseStanding
float hx = 0.0f, hy = 1.6f, hz = 0.0f;
// -------------------------------------------------------------------------
// 1. Calculate Local transform (for OpenXR laser pointer collision in VulkanRenderer)
// -------------------------------------------------------------------------
float hx = 0.0f, hy = 0.0f, hz = 0.0f;
float fx = 0.0f, fz = -1.0f;
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];
}
if (headPosLocal && headForwardLocal) {
hx = headPosLocal[0];
hy = headPosLocal[1];
hz = headPosLocal[2];
fx = headForwardLocal[0];
fz = headForwardLocal[2];
}
// Flatten forward vector to horizontal plane
float flen = std::sqrt(fx * fx + fz * fz);
if (flen < 1e-3f) {
fx = 0.0f;
@@ -262,71 +257,121 @@ bool VrKeyboard::show(const std::string& description,
fx /= flen;
fz /= flen;
}
const float yaw = std::atan2(-fx, -fz);
const float yawLocal = std::atan2(-fx, -fz);
// Place keyboard 1.05m in front of eyes, 0.20m below eye level.
// The modal dialog box above ends at ~hy + 0.03m; this places the top edge of the
// keyboard (half-height ~0.20m) right at hy - 0.004m, exactly 3.4cm below the modal dialog.
const float dist = 1.05f;
const float kbX = hx + fx * dist;
const float kbY = hy - 0.20f;
const float kbZ = hz + fz * dist;
// Keyboard placed 1.15m in front of eyes, 0.22m below eye level.
// The floating modal dialog box in VulkanRenderer ends at ~hy + 0.03m; this places the top
// of the keyboard (half-height 0.20m) at ~hy - 0.02m, creating an elegant ~5cm vertical gap
// directly beneath the modal dialog.
const float dist = 1.15f;
const float kbLocalX = hx + fx * dist;
const float kbLocalY = hy - 0.22f;
const float kbLocalZ = hz + fz * dist;
// Tilt upward toward eyes by ~11.5 degrees (-0.20 rad).
// Center is 20cm below eyes at 1.05m distance: atan2(0.20, 1.05) ~= 0.189 rad.
// Tilt of -0.20 rad angles the keyboard face normal directly at the user's eyes!
// Tilt upward toward eyes by -0.20 rad (~11.5 degrees)
const float tilt = -0.20f;
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;
vr::HmdMatrix34_t kbMatLocal{};
kbMatLocal.m[0][0] = std::cos(yawLocal);
kbMatLocal.m[0][1] = std::sin(yawLocal) * std::sin(tilt);
kbMatLocal.m[0][2] = std::sin(yawLocal) * std::cos(tilt);
kbMatLocal.m[0][3] = kbLocalX;
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;
kbMatLocal.m[1][0] = 0.0f;
kbMatLocal.m[1][1] = std::cos(tilt);
kbMatLocal.m[1][2] = -std::sin(tilt);
kbMatLocal.m[1][3] = kbLocalY;
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;
kbMatLocal.m[2][0] = -std::sin(yawLocal);
kbMatLocal.m[2][1] = std::cos(yawLocal) * std::sin(tilt);
kbMatLocal.m[2][2] = std::cos(yawLocal) * std::cos(tilt);
kbMatLocal.m[2][3] = kbLocalZ;
for (int r = 0; r < 3; ++r) {
for (int c = 0; c < 4; ++c) {
kbMat_[r][c] = kbMat.m[r][c];
kbMatLocal_[r][c] = kbMatLocal.m[r][c];
}
}
// -------------------------------------------------------------------------
// 2. Calculate Stage transform (for SteamVR TrackingUniverseStanding overlay rendering)
// -------------------------------------------------------------------------
float shx = hx, shy = hy + 1.65f, shz = hz;
float sfx = fx, sfz = fz;
if (headPosStage && headForwardStage) {
shx = headPosStage[0];
shy = headPosStage[1];
shz = headPosStage[2];
sfx = headForwardStage[0];
sfz = headForwardStage[2];
}
float sflen = std::sqrt(sfx * sfx + sfz * sfz);
if (sflen < 1e-3f) {
sfx = 0.0f;
sfz = -1.0f;
} else {
sfx /= sflen;
sfz /= sflen;
}
const float yawStage = std::atan2(-sfx, -sfz);
const float kbStageX = shx + sfx * dist;
const float kbStageY = shy - 0.22f; // In standing space (floor=0), shy ~1.65m, so kbStageY ~1.43m above floor!
const float kbStageZ = shz + sfz * dist;
vr::HmdMatrix34_t kbMatStage{};
kbMatStage.m[0][0] = std::cos(yawStage);
kbMatStage.m[0][1] = std::sin(yawStage) * std::sin(tilt);
kbMatStage.m[0][2] = std::sin(yawStage) * std::cos(tilt);
kbMatStage.m[0][3] = kbStageX;
kbMatStage.m[1][0] = 0.0f;
kbMatStage.m[1][1] = std::cos(tilt);
kbMatStage.m[1][2] = -std::sin(tilt);
kbMatStage.m[1][3] = kbStageY;
kbMatStage.m[2][0] = -std::sin(yawStage);
kbMatStage.m[2][1] = std::cos(yawStage) * std::sin(tilt);
kbMatStage.m[2][2] = std::cos(yawStage) * std::cos(tilt);
kbMatStage.m[2][3] = kbStageZ;
for (int r = 0; r < 3; ++r) {
for (int c = 0; c < 4; ++c) {
kbMatStage_[r][c] = kbMatStage.m[r][c];
}
}
kbConfigured_ = false;
// 1. Set keyboard transform in standing universe BEFORE ShowKeyboard
ov->SetKeyboardTransformAbsolute(vr::TrackingUniverseStanding, &kbMat);
ov->SetKeyboardTransformAbsolute(vr::TrackingUniverseStanding, &kbMatStage);
// 2. Invoke virtual keyboard
// 2. Invoke virtual keyboard with Modal | Minimal flags
// Minimal mode tells SteamVR GamepadUI not to show its internal text box
// and not to cache/accumulate dirty text drafts across sessions.
vr::EVROverlayError err = ov->ShowKeyboard(
mode,
vr::k_EGamepadTextInputLineModeSingleLine,
vr::KeyboardFlag_Modal,
vr::KeyboardFlag_Modal | vr::KeyboardFlag_Minimal,
description.c_str(),
static_cast<uint32_t>(maxChars),
existingText.c_str(),
0);
currentNonce_);
if (err != vr::VROverlayError_None) {
PIXELVR_LOG_WARN("VrKeyboard: ShowKeyboard failed with error %d", static_cast<int>(err));
return false;
}
// 3. Set keyboard transform AFTER ShowKeyboard as well
ov->SetKeyboardTransformAbsolute(vr::TrackingUniverseStanding, &kbMat);
// 3. Reinforce keyboard transform AFTER ShowKeyboard
ov->SetKeyboardTransformAbsolute(vr::TrackingUniverseStanding, &kbMatStage);
// 4. Configure overlay laser and transform immediately if already present
configureKeyboardOverlay();
isOpen_ = true;
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);
PIXELVR_LOG_INFO("VrKeyboard: Shown virtual keyboard for '%s' (nonce %llu, existing='%s') at stage pos (%.2f, %.2f, %.2f) local pos (%.2f, %.2f, %.2f)",
description.c_str(), static_cast<unsigned long long>(currentNonce_),
existingText.c_str(), kbStageX, kbStageY, kbStageZ, kbLocalX, kbLocalY, kbLocalZ);
return true;
}
@@ -340,6 +385,7 @@ void VrKeyboard::hide() {
kbConfigured_ = false;
doneSubmitted_ = false;
userTyped_ = false;
currentNonce_ = 0;
currentText_.clear();
existingText_.clear();
PIXELVR_LOG_INFO("VrKeyboard: Hidden virtual keyboard");
@@ -355,47 +401,63 @@ void VrKeyboard::poll(std::function<void(const std::string&)> onDone,
vr::VREvent_t event{};
while (sys->PollNextEvent(&event, sizeof(event))) {
if (event.eventType == vr::VREvent_KeyboardCharInput) {
userTyped_ = true;
char keyBuf[1024] = {0};
uint32_t len = ov->GetKeyboardText(keyBuf, sizeof(keyBuf));
if (len > 0) {
currentText_ = std::string(keyBuf, len < sizeof(keyBuf) ? len : std::strlen(keyBuf));
} else {
// Fallback: check event.data.keyboard.cNewInput
const char* inStr = event.data.keyboard.cNewInput;
if (inStr && inStr[0] != 0) {
const unsigned char firstByte = static_cast<unsigned char>(inStr[0]);
if (firstByte == '\b' || firstByte == 0x7F || firstByte == 8) {
while (!currentText_.empty() && (static_cast<unsigned char>(currentText_.back()) & 0xC0) == 0x80) {
currentText_.pop_back();
}
if (!currentText_.empty()) {
currentText_.pop_back();
}
} else if (firstByte >= 32) {
// Discard any events from old or stale keyboard sessions
if (event.data.keyboard.uUserValue != 0 && event.data.keyboard.uUserValue != currentNonce_) {
PIXELVR_LOG_INFO("VrKeyboard: Ignored stale CharInput (nonce %llu != current %llu)",
static_cast<unsigned long long>(event.data.keyboard.uUserValue),
static_cast<unsigned long long>(currentNonce_));
continue;
}
const char* inStr = event.data.keyboard.cNewInput;
if (inStr && inStr[0] != 0) {
userTyped_ = true;
const unsigned char firstByte = static_cast<unsigned char>(inStr[0]);
if (firstByte == '' || firstByte == 0x7F || firstByte == 8) {
// Backspace: pop last UTF-8 character
while (!currentText_.empty() && (static_cast<unsigned char>(currentText_.back()) & 0xC0) == 0x80) {
currentText_.pop_back();
}
if (!currentText_.empty()) {
currentText_.pop_back();
}
} else if (firstByte == '\n' || firstByte == '\r') {
isOpen_ = false;
kbConfigured_ = false;
doneSubmitted_ = true;
std::string text = currentText_;
ov->HideKeyboard();
currentText_.clear();
existingText_.clear();
userTyped_ = false;
currentNonce_ = 0;
PIXELVR_LOG_INFO("VrKeyboard: User submitted text via Return key: '%s'", text.c_str());
if (onDone) {
onDone(text);
}
return;
} else if (firstByte >= 32) {
if (currentText_.length() + std::strlen(inStr) <= static_cast<size_t>(maxChars_)) {
currentText_ += inStr;
}
}
PIXELVR_LOG_INFO("VrKeyboard: CharInput live text: '%s'", currentText_.c_str());
}
PIXELVR_LOG_INFO("VrKeyboard: CharInput live text: '%s'", currentText_.c_str());
} else if (event.eventType == vr::VREvent_KeyboardDone) {
isOpen_ = false;
kbConfigured_ = false;
doneSubmitted_ = true;
char buf[1024] = {0};
uint32_t len = ov->GetKeyboardText(buf, sizeof(buf));
std::string text = currentText_;
if (len > 0) {
text = std::string(buf, len < sizeof(buf) ? len : std::strlen(buf));
}
ov->HideKeyboard();
currentText_.clear();
existingText_.clear();
userTyped_ = false;
currentNonce_ = 0;
PIXELVR_LOG_INFO("VrKeyboard: User submitted text via KeyboardDone: '%s'", text.c_str());
if (onDone) {
onDone(text);
}
return;
} else if (event.eventType == vr::VREvent_KeyboardClosed ||
event.eventType == vr::VREvent_KeyboardClosed_Global) {
isOpen_ = false;
@@ -403,19 +465,19 @@ void VrKeyboard::poll(std::function<void(const std::string&)> onDone,
ov->HideKeyboard();
PIXELVR_LOG_INFO("VrKeyboard: Keyboard closed (doneSubmitted=%d)", doneSubmitted_ ? 1 : 0);
if (!doneSubmitted_) {
// Closed without KeyboardDone: treat as cancel and forget any input
currentText_.clear();
existingText_.clear();
userTyped_ = false;
currentNonce_ = 0;
PIXELVR_LOG_INFO("VrKeyboard: Discarded input on KeyboardClosed");
if (onCancel) {
onCancel();
}
}
return;
}
}
// Keep currentText_ synchronized with the live SteamVR keyboard buffer every frame
if (isOpen_) {
pollFramesSinceOpen_++;
// Maintain and reinforce keyboard overlay configuration across the first 90 frames (~1 sec)
@@ -423,16 +485,6 @@ void VrKeyboard::poll(std::function<void(const std::string&)> onDone,
if (!kbConfigured_ && pollFramesSinceOpen_ < 90) {
configureKeyboardOverlay();
}
// Only synchronize with SteamVR live buffer if the user has actively typed in this session.
// This avoids contaminating a fresh/empty input field with stale residual buffer from a previous field.
if (userTyped_) {
char liveBuf[1024] = {0};
uint32_t liveLen = ov->GetKeyboardText(liveBuf, sizeof(liveBuf));
if (liveLen > 0) {
currentText_ = std::string(liveBuf, liveLen < sizeof(liveBuf) ? liveLen : std::strlen(liveBuf));
}
}
}
}
+9 -4
View File
@@ -23,8 +23,10 @@ public:
const std::string& existingText,
bool isPassword = false,
int maxChars = 256,
const float* headPos = nullptr,
const float* headForward = nullptr);
const float* headPosLocal = nullptr,
const float* headForwardLocal = nullptr,
const float* headPosStage = nullptr,
const float* headForwardStage = nullptr);
// Closes the virtual keyboard if open.
void hide();
@@ -37,7 +39,7 @@ 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]; }
const float* transform() const { return &kbMatLocal_[0][0]; }
private:
VrKeyboard() = default;
@@ -51,7 +53,10 @@ private:
bool userTyped_ = false;
bool kbConfigured_ = false;
int pollFramesSinceOpen_ = 0;
float kbMat_[3][4]{};
float kbMatLocal_[3][4]{};
float kbMatStage_[3][4]{};
uint64_t currentNonce_ = 0;
int maxChars_ = 256;
std::string description_;
std::string existingText_;
std::string currentText_;
+68
View File
@@ -35,6 +35,10 @@ XrContext::~XrContext() {
if (appSpace_ != XR_NULL_HANDLE) {
xrDestroySpace(appSpace_);
}
if (stageSpace_ != XR_NULL_HANDLE) {
xrDestroySpace(stageSpace_);
stageSpace_ = XR_NULL_HANDLE;
}
if (actionSet_ != XR_NULL_HANDLE) {
xrDestroyActionSet(actionSet_);
}
@@ -217,6 +221,18 @@ bool XrContext::createReferenceSpace() {
createInfo.poseInReferenceSpace.orientation.w = 1.0f;
PIXELVR_XR_CHECK(instance_,
xrCreateReferenceSpace(session_, &createInfo, &appSpace_));
// Create STAGE reference space (floor level tracking bounds, matching SteamVR TrackingUniverseStanding)
XrReferenceSpaceCreateInfo stageInfo{XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
stageInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE;
stageInfo.poseInReferenceSpace.orientation.w = 1.0f;
XrResult stageRes = xrCreateReferenceSpace(session_, &stageInfo, &stageSpace_);
if (XR_SUCCEEDED(stageRes)) {
PIXELVR_LOG_INFO("XrContext: Successfully created STAGE reference space");
} else {
PIXELVR_LOG_WARN("XrContext: STAGE reference space not available (%d), using calibrated standing fallback", static_cast<int>(stageRes));
stageSpace_ = XR_NULL_HANDLE;
}
return true;
}
@@ -922,6 +938,31 @@ bool XrContext::renderFrame(const RenderViewFn& renderView) {
input_.headForward[2] = -(1.0f - 2.0f * (q.x * q.x + q.y * q.y));
}
input_.stageValid = false;
if (stageSpace_ != XR_NULL_HANDLE && input_.headValid) {
XrSpaceLocation loc{XR_TYPE_SPACE_LOCATION};
if (XR_SUCCEEDED(xrLocateSpace(appSpace_, stageSpace_, frameState.predictedDisplayTime, &loc))) {
constexpr XrSpaceLocationFlags need =
XR_SPACE_LOCATION_ORIENTATION_VALID_BIT |
XR_SPACE_LOCATION_POSITION_VALID_BIT;
if ((loc.locationFlags & need) == need) {
input_.stageValid = true;
input_.appToStagePose = loc.pose;
transformToStage(input_.headPos, input_.headForward,
input_.headPosStage, input_.headForwardStage);
}
}
}
if (!input_.stageValid) {
// Fallback: Standing stage eye level is ~1.65m above the physical floor
input_.headPosStage[0] = input_.headPos[0];
input_.headPosStage[1] = input_.headPos[1] + 1.65f;
input_.headPosStage[2] = input_.headPos[2];
input_.headForwardStage[0] = input_.headForward[0];
input_.headForwardStage[1] = input_.headForward[1];
input_.headForwardStage[2] = input_.headForward[2];
}
// Locate each controller's aim pose (for the pointer rays and models).
for (int h = 0; h < 2; ++h) {
input_.controllers[h].active = false;
@@ -1028,4 +1069,31 @@ bool XrContext::renderFrame(const RenderViewFn& renderView) {
return true;
}
void XrContext::transformToStage(const float localPos[3], const float localFwd[3],
float stagePos[3], float stageFwd[3]) const {
const auto& p = input_.appToStagePose.position;
const auto& q = input_.appToStagePose.orientation;
if (localPos && stagePos) {
const float vx = localPos[0], vy = localPos[1], vz = localPos[2];
const float tw = -q.x * vx - q.y * vy - q.z * vz;
const float tx = q.w * vx + q.y * vz - q.z * vy;
const float ty = q.w * vy + q.z * vx - q.x * vz;
const float tz = q.w * vz + q.x * vy - q.y * vx;
stagePos[0] = p.x + (tx * q.w + tw * (-q.x) + ty * (-q.z) - tz * (-q.y));
stagePos[1] = p.y + (ty * q.w + tw * (-q.y) + tz * (-q.x) - tx * (-q.z));
stagePos[2] = p.z + (tz * q.w + tw * (-q.z) + tx * (-q.y) - ty * (-q.x));
}
if (localFwd && stageFwd) {
const float fx = localFwd[0], fy = localFwd[1], fz = localFwd[2];
const float ftw = -q.x * fx - q.y * fy - q.z * fz;
const float ftx = q.w * fx + q.y * fz - q.z * fy;
const float fty = q.w * fy + q.z * fx - q.x * fz;
const float ftz = q.w * fz + q.x * fy - q.y * fx;
stageFwd[0] = ftx * q.w + ftw * (-q.x) + fty * (-q.z) - ftz * (-q.y);
stageFwd[1] = fty * q.w + ftw * (-q.y) + ftz * (-q.x) - ftx * (-q.z);
stageFwd[2] = ftz * q.w + ftw * (-q.z) + ftx * (-q.y) - fty * (-q.x);
}
}
} // namespace pixelvr
+8
View File
@@ -34,6 +34,10 @@ struct XrInputState {
bool headValid = false;
float headPos[3] = {0.0f, 0.0f, 0.0f};
float headForward[3] = {0.0f, 0.0f, -1.0f};
bool stageValid = false;
XrPosef appToStagePose{{0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 1.65f, 0.0f}};
float headPosStage[3] = {0.0f, 1.65f, 0.0f};
float headForwardStage[3] = {0.0f, 0.0f, -1.0f};
// Tracked controller aim poses (index 0 = left, 1 = right). The pointer ray
// is the -Z axis of `orientation` from `position`, in the app reference space.
@@ -122,7 +126,10 @@ public:
XrInstance instance() const { return instance_; }
XrSession session() const { return session_; }
XrSpace appSpace() const { return appSpace_; }
XrSpace stageSpace() const { return stageSpace_; }
XrTime displayTime() const { return lastDisplayTime_; }
void transformToStage(const float localPos[3], const float localFwd[3],
float stagePos[3], float stageFwd[3]) const;
XrSystemId systemId() const { return systemId_; }
bool renderModelEnabled() const { return renderModelEnabled_; }
uint32_t viewCount() const { return static_cast<uint32_t>(configViews_.size()); }
@@ -145,6 +152,7 @@ private:
XrSystemId systemId_ = XR_NULL_SYSTEM_ID;
XrSession session_ = XR_NULL_HANDLE;
XrSpace appSpace_ = XR_NULL_HANDLE;
XrSpace stageSpace_ = XR_NULL_HANDLE;
XrTime lastDisplayTime_ = 0;
XrSessionState sessionState_ = XR_SESSION_STATE_UNKNOWN;