mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 02:00:06 +02:00
Frametop: a multi-screen desktop and universal 3D mouse for the Steam Frame
Several KDE Plasma screens floating in SteamVR, each a real monitor of any resolution and shape, shown by our own compositor (ft-screens), with a layout, wrist pinning, and visibility modes; a Bluetooth mouse that drives all of SteamVR as a room-anchored 3D pointer (input relay, ft-pointer helper, ft_pointer SteamVR driver); two settings apps; and Bluetooth LE fixes. Installs on the headset with ./install.sh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
d439bc3f25
61 files changed
+8785
No files matched your search
@@ -0,0 +1,152 @@
|
||||
// Small vector math shared by the pointer helper, the layout tool, and the probe, plus
|
||||
// ScanPanel: measuring a floating dashboard panel, whose transform OpenVR won't give out.
|
||||
// Header-only. Standing-universe coordinates unless a name says otherwise.
|
||||
#pragma once
|
||||
|
||||
#include <openvr.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace md {
|
||||
|
||||
struct Vec3 {
|
||||
double x = 0, y = 0, z = 0;
|
||||
};
|
||||
inline Vec3 operator+(Vec3 a, Vec3 b) { return {a.x + b.x, a.y + b.y, a.z + b.z}; }
|
||||
inline Vec3 operator-(Vec3 a, Vec3 b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; }
|
||||
inline Vec3 operator*(Vec3 a, double s) { return {a.x * s, a.y * s, a.z * s}; }
|
||||
inline double Dot(Vec3 a, Vec3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
|
||||
inline Vec3 Cross(Vec3 a, Vec3 b) { return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x}; }
|
||||
inline double Length(Vec3 a) { return std::sqrt(Dot(a, a)); }
|
||||
inline Vec3 Normalize(Vec3 a) {
|
||||
const double n = Length(a);
|
||||
return n > 1e-9 ? a * (1.0 / n) : Vec3{0, 0, -1};
|
||||
}
|
||||
|
||||
// yaw 0 = -Z (SteamVR forward), positive yaw turns left (about +Y), positive pitch looks up.
|
||||
inline Vec3 Direction(double yawDeg, double pitchDeg) {
|
||||
const double y = yawDeg * M_PI / 180, p = pitchDeg * M_PI / 180;
|
||||
return {-std::sin(y) * std::cos(p), std::sin(p), -std::cos(y) * std::cos(p)};
|
||||
}
|
||||
|
||||
// Rotation part of a pose matrix applied to a vector, and its transpose.
|
||||
inline Vec3 Rotate(const vr::HmdMatrix34_t &m, Vec3 v) {
|
||||
return {m.m[0][0] * v.x + m.m[0][1] * v.y + m.m[0][2] * v.z, m.m[1][0] * v.x + m.m[1][1] * v.y + m.m[1][2] * v.z,
|
||||
m.m[2][0] * v.x + m.m[2][1] * v.y + m.m[2][2] * v.z};
|
||||
}
|
||||
inline Vec3 RotateInverse(const vr::HmdMatrix34_t &m, Vec3 v) {
|
||||
return {m.m[0][0] * v.x + m.m[1][0] * v.y + m.m[2][0] * v.z, m.m[0][1] * v.x + m.m[1][1] * v.y + m.m[2][1] * v.z,
|
||||
m.m[0][2] * v.x + m.m[1][2] * v.y + m.m[2][2] * v.z};
|
||||
}
|
||||
inline Vec3 Position(const vr::HmdMatrix34_t &m) { return {m.m[0][3], m.m[1][3], m.m[2][3]}; }
|
||||
|
||||
// Rodrigues: v rotated by angle (radians) about a unit axis.
|
||||
inline Vec3 RotateAbout(Vec3 v, Vec3 axis, double angle) {
|
||||
const double c = std::cos(angle), s = std::sin(angle);
|
||||
return v * c + Cross(axis, v) * s + axis * (Dot(axis, v) * (1 - c));
|
||||
}
|
||||
|
||||
// An orthonormal frame: device poses (-Z forward) and panels (+X right, +Y up, +Z out of the front).
|
||||
struct Basis {
|
||||
Vec3 x, y, z;
|
||||
};
|
||||
// Coordinates of v in the basis, and back.
|
||||
inline Vec3 ToBasis(const Basis &b, Vec3 v) { return {Dot(v, b.x), Dot(v, b.y), Dot(v, b.z)}; }
|
||||
inline Vec3 FromBasis(const Basis &b, Vec3 v) { return b.x * v.x + b.y * v.y + b.z * v.z; }
|
||||
|
||||
// Device basis for a pointing direction with no roll: -Z along aim, +X horizontal.
|
||||
inline Basis AimBasis(Vec3 aim) {
|
||||
const Vec3 z = Normalize(aim * -1.0);
|
||||
const Vec3 x = Normalize(Cross({0, 1, 0}, z));
|
||||
return {x, Cross(z, x), z};
|
||||
}
|
||||
|
||||
// Panel basis for a panel facing the direction (yaw, pitch) points to. The front (+Z)
|
||||
// faces back along that direction, toward whoever looks along it. roll turns the panel
|
||||
// about its front normal, counterclockwise as you see it (90: a rotated-left monitor).
|
||||
inline Basis PanelBasis(double yawDeg, double pitchDeg, double rollDeg = 0) {
|
||||
const Vec3 z = Direction(yawDeg, pitchDeg) * -1.0;
|
||||
const Vec3 x = Normalize(Cross({0, 1, 0}, z)), y = Cross(z, x);
|
||||
const double r = rollDeg * M_PI / 180, c = std::cos(r), s = std::sin(r);
|
||||
return {x * c + y * s, y * c - x * s, z};
|
||||
}
|
||||
|
||||
// Quaternion (w, x, y, z) of a rotation whose matrix columns are the basis vectors.
|
||||
inline void BasisQuat(const Basis &b, double q[4]) {
|
||||
const double m[3][3] = {{b.x.x, b.y.x, b.z.x}, {b.x.y, b.y.y, b.z.y}, {b.x.z, b.y.z, b.z.z}};
|
||||
const double trace = m[0][0] + m[1][1] + m[2][2];
|
||||
if (trace > 0) {
|
||||
const double s = 0.5 / std::sqrt(trace + 1);
|
||||
q[0] = 0.25 / s, q[1] = (m[2][1] - m[1][2]) * s, q[2] = (m[0][2] - m[2][0]) * s, q[3] = (m[1][0] - m[0][1]) * s;
|
||||
} else if (m[0][0] > m[1][1] && m[0][0] > m[2][2]) {
|
||||
const double s = 2 * std::sqrt(1 + m[0][0] - m[1][1] - m[2][2]);
|
||||
q[0] = (m[2][1] - m[1][2]) / s, q[1] = 0.25 * s, q[2] = (m[0][1] + m[1][0]) / s, q[3] = (m[0][2] + m[2][0]) / s;
|
||||
} else if (m[1][1] > m[2][2]) {
|
||||
const double s = 2 * std::sqrt(1 + m[1][1] - m[0][0] - m[2][2]);
|
||||
q[0] = (m[0][2] - m[2][0]) / s, q[1] = (m[0][1] + m[1][0]) / s, q[2] = 0.25 * s, q[3] = (m[1][2] + m[2][1]) / s;
|
||||
} else {
|
||||
const double s = 2 * std::sqrt(1 + m[2][2] - m[0][0] - m[1][1]);
|
||||
q[0] = (m[1][0] - m[0][1]) / s, q[1] = (m[0][2] + m[2][0]) / s, q[2] = (m[1][2] + m[2][1]) / s, q[3] = 0.25 * s;
|
||||
}
|
||||
}
|
||||
|
||||
// A panel measured by ScanPanel: centre, size, and frame (x right, y up, z out of the front).
|
||||
struct Panel {
|
||||
bool found = false;
|
||||
Vec3 center;
|
||||
double width = 0, height = 0;
|
||||
Basis basis;
|
||||
int hits = 0;
|
||||
};
|
||||
|
||||
// Cast rays from `from` over the whole sphere (step in degrees) at one overlay, and fit
|
||||
// point = origin + u*U + v*V to the hits (least squares). ComputeOverlayIntersection
|
||||
// works on floating dashboard panels, whose transforms aren't readable, and returns the
|
||||
// texture coordinates of each hit; v runs bottom to top. It's local and fast: a 0.5-degree
|
||||
// scan (about 230,000 rays) takes 0.1 s.
|
||||
inline Panel ScanPanel(vr::VROverlayHandle_t h, Vec3 from, double step = 1.0) {
|
||||
Panel p;
|
||||
double ata[3][3] = {}, atb[3][3] = {}; // normal equations for [1 u v] -> (x, y, z)
|
||||
for (double pitch = -80; pitch <= 80; pitch += step)
|
||||
for (double yaw = -180; yaw < 180; yaw += step) {
|
||||
const Vec3 d = Direction(yaw, pitch);
|
||||
vr::VROverlayIntersectionParams_t params{};
|
||||
params.vSource = {float(from.x), float(from.y), float(from.z)};
|
||||
params.vDirection = {float(d.x), float(d.y), float(d.z)};
|
||||
params.eOrigin = vr::TrackingUniverseStanding;
|
||||
vr::VROverlayIntersectionResults_t hit{};
|
||||
if (!vr::VROverlay()->ComputeOverlayIntersection(h, ¶ms, &hit)) continue;
|
||||
++p.hits;
|
||||
const double row[3] = {1, hit.vUVs.v[0], hit.vUVs.v[1]};
|
||||
for (int i = 0; i < 3; ++i)
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
ata[i][j] += row[i] * row[j];
|
||||
atb[i][j] += row[i] * hit.vPoint.v[j];
|
||||
}
|
||||
}
|
||||
if (p.hits < 6) return p;
|
||||
auto det3 = [](const double a[3][3]) {
|
||||
return a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1]) - a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0]) +
|
||||
a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0]);
|
||||
};
|
||||
const double d = det3(ata);
|
||||
if (std::fabs(d) < 1e-12) return p;
|
||||
double x[3][3]; // x[k][j]: coefficient k (1, u, v) of coordinate j, by Cramer's rule
|
||||
for (int j = 0; j < 3; ++j)
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
double t[3][3];
|
||||
for (int r = 0; r < 3; ++r)
|
||||
for (int c = 0; c < 3; ++c) t[r][c] = c == k ? atb[r][j] : ata[r][c];
|
||||
x[k][j] = det3(t) / d;
|
||||
}
|
||||
const Vec3 O{x[0][0], x[0][1], x[0][2]}, U{x[1][0], x[1][1], x[1][2]}, V{x[2][0], x[2][1], x[2][2]};
|
||||
p.found = true;
|
||||
p.center = O + U * 0.5 + V * 0.5;
|
||||
p.width = Length(U);
|
||||
p.height = Length(V);
|
||||
const Vec3 bx = Normalize(U), bz = Normalize(Cross(U, V));
|
||||
p.basis = {bx, Cross(bz, bx), bz};
|
||||
return p;
|
||||
}
|
||||
|
||||
} // namespace md
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the ft_pointer SteamVR driver on the Frame (dev container) and check it
|
||||
# only needs glibc symbols the SteamOS host has (2.39; the container has 2.43).
|
||||
# -fno-math-errno keeps sqrtf inline. libm's float functions (sqrtf, atan2f, asinf,
|
||||
# remainderf) are versioned GLIBC_2.43 here, so the driver uses the double versions.
|
||||
# Usage: pointer/driver/build.sh
|
||||
set -euo pipefail
|
||||
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
"$root/scripts/sync.sh" >/dev/null
|
||||
exec "$root/scripts/frame.sh" -C pointer/driver 'set -e
|
||||
mkdir -p build
|
||||
g++ -std=c++17 -O2 -fPIC -shared -fvisibility=hidden -fno-math-errno -Wall -Wno-unused-parameter \
|
||||
-static-libstdc++ -static-libgcc -Wl,--exclude-libs,ALL \
|
||||
-I/opt/steamvr/tools/hellovr_vulkan_linux/src/openvr/headers \
|
||||
-o build/driver_ft_pointer.so driver_ft_pointer.cpp -lpthread
|
||||
max=$(objdump -T build/driver_ft_pointer.so | grep -oE "GLIBC_[0-9.]+" | sort -uV | tail -1)
|
||||
echo "built build/driver_ft_pointer.so, newest glibc symbol: $max"
|
||||
[ "$(printf "%s\n" "$max" GLIBC_2.39 | sort -V | tail -1)" = GLIBC_2.39 ] || { echo "needs newer glibc than the host has" >&2; exit 1; }'
|
||||
@@ -0,0 +1,357 @@
|
||||
// ft_pointer: a virtual SteamVR controller that the universal 3D mouse drives.
|
||||
// The cursor is a point anchored in the room: `distance` metres from where the
|
||||
// head was at the last recenter, in the yaw/pitch direction the mouse steers.
|
||||
// The ray starts at the eye (the HMD origin) and aims at that point, so
|
||||
// SteamVR's laser is seen end-on and only its hit dot shows. The device uses an
|
||||
// invisible render model. The input relay (input/input-relay.py)
|
||||
// drives it over a datagram socket.
|
||||
//
|
||||
// Control socket: abstract unix datagram "@ft_pointer", text commands:
|
||||
// recenter anchor the origin at the head and aim along the gaze
|
||||
// move <dyaw> <dpitch> rotate the ray (degrees; +yaw turns left, +pitch up)
|
||||
// aim <yaw_deg> <pitch_deg> absolute direction (yaw 0 = -Z, the SteamVR forward)
|
||||
// gaze follow the head (origin and direction) again
|
||||
// distance <metres> cursor distance from the anchor (default 1.5)
|
||||
// pose <x> <y> <z> <yaw> <pitch> exact pose, sent every frame by the ft-pointer helper
|
||||
// posq <x> <y> <z> <qw> <qx> <qy> <qz> exact pose with a full rotation (tilting a panel while moving it)
|
||||
// btn <name> <0|1> name: trigger, b, x, system, joystick, a (a = claim the laser, no click)
|
||||
// scroll <x> <y> joystick deflection -1..1
|
||||
// show | hide connect (take the hand role) or disconnect (give it back)
|
||||
//
|
||||
// The device starts disconnected, so it never holds a hand role at boot (holding
|
||||
// the right hand while SteamVR started left the Steam UI stuck loading). It
|
||||
// connects only while the mouse is in use, so the last used device wins. Its
|
||||
// role hint follows: the configured hand while connected, OptOut while not.
|
||||
// SteamVR keeps a hand role reserved for a disconnected device that still hints
|
||||
// that hand, so the real controller would never get it back otherwise.
|
||||
//
|
||||
// Settings (steamvr.vrsettings section "driver_ft_pointer"): role (int, 2 = right hand, 5 = stylus).
|
||||
#include <openvr_driver.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
using namespace vr;
|
||||
|
||||
namespace {
|
||||
|
||||
struct State {
|
||||
std::mutex lock;
|
||||
bool gaze = true;
|
||||
bool visible = false; // disconnected until "show"
|
||||
bool recenter = false; // applied on the next frame, which has the head pose
|
||||
bool anchored = false;
|
||||
float anchor[3] = {};
|
||||
bool explicitPose = false; // set by "pose": the helper drives position and direction
|
||||
float pos[3] = {};
|
||||
bool hasQuat = false; // set by "posq": use quat instead of yaw/pitch
|
||||
float quat[4] = {1, 0, 0, 0};
|
||||
|
||||
float yaw = 0.f, pitch = 0.f; // degrees
|
||||
bool buttons[6] = {};
|
||||
float distance = 1.5f;
|
||||
float scrollX = 0.f, scrollY = 0.f;
|
||||
};
|
||||
|
||||
const int kButtons = 6;
|
||||
const char *kButtonNames[kButtons] = {"trigger", "b", "x", "system", "joystick", "a"};
|
||||
|
||||
HmdQuaternion_t QuatFromYawPitch(float yawDeg, float pitchDeg) {
|
||||
// Yaw about +Y, then pitch about +X. SteamVR forward is -Z.
|
||||
const float y = yawDeg * float(M_PI) / 360.f, p = pitchDeg * float(M_PI) / 360.f;
|
||||
const float cy = std::cos(y), sy = std::sin(y), cp = std::cos(p), sp = std::sin(p);
|
||||
return {cy * cp, cy * sp, sy * cp, -sy * sp};
|
||||
}
|
||||
|
||||
// Rotation part of a 3x4 pose matrix as a quaternion (all four branches).
|
||||
HmdQuaternion_t QuatFromMatrix(const float (&m)[3][4]) {
|
||||
const float trace = m[0][0] + m[1][1] + m[2][2];
|
||||
if (trace > 0) {
|
||||
const float s = 0.5f / std::sqrt(trace + 1.f);
|
||||
return {0.25f / s, (m[2][1] - m[1][2]) * s, (m[0][2] - m[2][0]) * s, (m[1][0] - m[0][1]) * s};
|
||||
}
|
||||
if (m[0][0] > m[1][1] && m[0][0] > m[2][2]) {
|
||||
const float s = 2.f * std::sqrt(1.f + m[0][0] - m[1][1] - m[2][2]);
|
||||
return {(m[2][1] - m[1][2]) / s, 0.25f * s, (m[0][1] + m[1][0]) / s, (m[0][2] + m[2][0]) / s};
|
||||
}
|
||||
if (m[1][1] > m[2][2]) {
|
||||
const float s = 2.f * std::sqrt(1.f + m[1][1] - m[0][0] - m[2][2]);
|
||||
return {(m[0][2] - m[2][0]) / s, (m[0][1] + m[1][0]) / s, 0.25f * s, (m[1][2] + m[2][1]) / s};
|
||||
}
|
||||
const float s = 2.f * std::sqrt(1.f + m[2][2] - m[0][0] - m[1][1]);
|
||||
return {(m[1][0] - m[0][1]) / s, (m[0][2] + m[2][0]) / s, (m[1][2] + m[2][1]) / s, 0.25f * s};
|
||||
}
|
||||
|
||||
class PointerDevice : public ITrackedDeviceServerDriver {
|
||||
public:
|
||||
explicit PointerDevice(State *state) : state_(state) {}
|
||||
|
||||
EVRInitError Activate(uint32_t objectId) override {
|
||||
objectId_ = objectId;
|
||||
auto props = VRProperties();
|
||||
const PropertyContainerHandle_t c = props->TrackedDeviceToPropertyContainer(objectId);
|
||||
container_ = c;
|
||||
EVRSettingsError err;
|
||||
const int32_t configured = VRSettings()->GetInt32("driver_ft_pointer", "role", &err);
|
||||
if (err == VRSettingsError_None && configured > 0) role_ = configured;
|
||||
|
||||
props->SetStringProperty(c, Prop_ModelNumber_String, "ft_pointer");
|
||||
props->SetStringProperty(c, Prop_ManufacturerName_String, "Frametop");
|
||||
props->SetStringProperty(c, Prop_ControllerType_String, "ft_pointer");
|
||||
props->SetStringProperty(c, Prop_InputProfilePath_String, "{ft_pointer}/input/ft_pointer_profile.json");
|
||||
props->SetStringProperty(c, Prop_RenderModelName_String, "{ft_pointer}/rendermodels/ft_pointer_invisible");
|
||||
props->SetInt32Property(c, Prop_ControllerRoleHint_Int32, TrackedControllerRole_OptOut); // until "show"
|
||||
props->SetInt32Property(c, Prop_DeviceClass_Int32, TrackedDeviceClass_Controller);
|
||||
props->SetBoolProperty(c, Prop_NeverTracked_Bool, false);
|
||||
|
||||
auto input = VRDriverInput();
|
||||
input->CreateBooleanComponent(c, "/input/trigger/click", &buttons_[0]);
|
||||
input->CreateBooleanComponent(c, "/input/b/click", &buttons_[1]);
|
||||
input->CreateBooleanComponent(c, "/input/x/click", &buttons_[2]);
|
||||
input->CreateBooleanComponent(c, "/input/system/click", &buttons_[3]);
|
||||
input->CreateBooleanComponent(c, "/input/joystick/click", &buttons_[4]);
|
||||
input->CreateBooleanComponent(c, "/input/a/click", &buttons_[5]);
|
||||
input->CreateScalarComponent(c, "/input/joystick/x", &scrollX_, VRScalarType_Absolute, VRScalarUnits_NormalizedTwoSided);
|
||||
input->CreateScalarComponent(c, "/input/joystick/y", &scrollY_, VRScalarType_Absolute, VRScalarUnits_NormalizedTwoSided);
|
||||
|
||||
VRDriverLog()->Log("ft_pointer: activated");
|
||||
return VRInitError_None;
|
||||
}
|
||||
|
||||
void Deactivate() override { objectId_ = k_unTrackedDeviceIndexInvalid; }
|
||||
void EnterStandby() override {}
|
||||
void *GetComponent(const char *) override { return nullptr; }
|
||||
void DebugRequest(const char *, char *response, uint32_t size) override {
|
||||
if (size) response[0] = 0;
|
||||
}
|
||||
DriverPose_t GetPose() override { return pose_; }
|
||||
|
||||
void RunFrame() {
|
||||
if (objectId_ == k_unTrackedDeviceIndexInvalid) return;
|
||||
TrackedDevicePose_t hmd{};
|
||||
VRServerDriverHost()->GetRawTrackedDevicePoses(0.f, &hmd, 1);
|
||||
|
||||
State snapshot;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(state_->lock);
|
||||
if (state_->recenter || (!state_->gaze && !state_->anchored)) {
|
||||
const auto &h = hmd.mDeviceToAbsoluteTracking.m;
|
||||
if (hmd.bPoseIsValid) {
|
||||
state_->anchor[0] = h[0][3];
|
||||
state_->anchor[1] = h[1][3];
|
||||
state_->anchor[2] = h[2][3];
|
||||
state_->anchored = true;
|
||||
if (state_->recenter) {
|
||||
const float fx = -h[0][2], fy = -h[1][2], fz = -h[2][2];
|
||||
// double-precision libm: the float versions are GLIBC_2.43 in the build container.
|
||||
state_->yaw = float(std::atan2(double(-fx), double(-fz)) * 180.0 / M_PI);
|
||||
state_->pitch = float(std::asin(double(fy)) * 180.0 / M_PI);
|
||||
state_->gaze = false;
|
||||
state_->recenter = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
snapshot.gaze = state_->gaze;
|
||||
std::memcpy(snapshot.anchor, state_->anchor, sizeof snapshot.anchor);
|
||||
snapshot.distance = state_->distance;
|
||||
snapshot.explicitPose = state_->explicitPose;
|
||||
std::memcpy(snapshot.pos, state_->pos, sizeof snapshot.pos);
|
||||
snapshot.hasQuat = state_->hasQuat;
|
||||
std::memcpy(snapshot.quat, state_->quat, sizeof snapshot.quat);
|
||||
snapshot.visible = state_->visible;
|
||||
snapshot.yaw = state_->yaw;
|
||||
snapshot.pitch = state_->pitch;
|
||||
std::memcpy(snapshot.buttons, state_->buttons, sizeof snapshot.buttons);
|
||||
snapshot.scrollX = state_->scrollX;
|
||||
snapshot.scrollY = state_->scrollY;
|
||||
}
|
||||
|
||||
DriverPose_t pose{};
|
||||
pose.qWorldFromDriverRotation.w = 1.f;
|
||||
pose.qDriverFromHeadRotation.w = 1.f;
|
||||
const auto &m = hmd.mDeviceToAbsoluteTracking.m;
|
||||
if (snapshot.explicitPose) {
|
||||
for (int i = 0; i < 3; ++i) pose.vecPosition[i] = snapshot.pos[i];
|
||||
pose.qRotation = snapshot.hasQuat
|
||||
? HmdQuaternion_t{snapshot.quat[0], snapshot.quat[1], snapshot.quat[2], snapshot.quat[3]}
|
||||
: QuatFromYawPitch(snapshot.yaw, snapshot.pitch);
|
||||
} else if (snapshot.gaze) {
|
||||
pose.vecPosition[0] = m[0][3];
|
||||
pose.vecPosition[1] = m[1][3] - 0.05f; // just below the eyes
|
||||
pose.vecPosition[2] = m[2][3];
|
||||
pose.qRotation = QuatFromMatrix(m);
|
||||
} else {
|
||||
// Cursor point P = anchor + distance * dir(yaw, pitch). Aim from the eye at P.
|
||||
const double yr = snapshot.yaw * M_PI / 180.0, pr = snapshot.pitch * M_PI / 180.0;
|
||||
const double p[3] = {snapshot.anchor[0] - snapshot.distance * std::sin(yr) * std::cos(pr),
|
||||
snapshot.anchor[1] + snapshot.distance * std::sin(pr),
|
||||
snapshot.anchor[2] - snapshot.distance * std::cos(yr) * std::cos(pr)};
|
||||
const double eye[3] = {m[0][3], m[1][3], m[2][3]};
|
||||
const double d[3] = {p[0] - eye[0], p[1] - eye[1], p[2] - eye[2]};
|
||||
const double horizontal = std::sqrt(d[0] * d[0] + d[2] * d[2]);
|
||||
for (int i = 0; i < 3; ++i) pose.vecPosition[i] = eye[i];
|
||||
pose.qRotation = QuatFromYawPitch(float(std::atan2(-d[0], -d[2]) * 180.0 / M_PI),
|
||||
float(std::atan2(d[1], horizontal) * 180.0 / M_PI));
|
||||
}
|
||||
if (snapshot.visible != hinted_) {
|
||||
// Claim the hand before connecting; give it up when disconnecting.
|
||||
VRProperties()->SetInt32Property(container_, Prop_ControllerRoleHint_Int32,
|
||||
snapshot.visible ? role_ : int32_t(TrackedControllerRole_OptOut));
|
||||
hinted_ = snapshot.visible;
|
||||
}
|
||||
const bool ok = hmd.bPoseIsValid && snapshot.visible;
|
||||
pose.poseIsValid = ok;
|
||||
pose.result = ok ? TrackingResult_Running_OK : TrackingResult_Uninitialized;
|
||||
pose.deviceIsConnected = snapshot.visible;
|
||||
pose_ = pose;
|
||||
VRServerDriverHost()->TrackedDevicePoseUpdated(objectId_, pose_, sizeof(DriverPose_t));
|
||||
|
||||
auto input = VRDriverInput();
|
||||
for (int i = 0; i < kButtons; ++i) input->UpdateBooleanComponent(buttons_[i], snapshot.buttons[i], 0);
|
||||
input->UpdateScalarComponent(scrollX_, snapshot.scrollX, 0);
|
||||
input->UpdateScalarComponent(scrollY_, snapshot.scrollY, 0);
|
||||
}
|
||||
|
||||
private:
|
||||
State *state_;
|
||||
uint32_t objectId_ = k_unTrackedDeviceIndexInvalid;
|
||||
PropertyContainerHandle_t container_ = k_ulInvalidPropertyContainer;
|
||||
int32_t role_ = TrackedControllerRole_RightHand;
|
||||
bool hinted_ = false; // whether the role hint currently claims role_
|
||||
DriverPose_t pose_{};
|
||||
VRInputComponentHandle_t buttons_[kButtons] = {};
|
||||
VRInputComponentHandle_t scrollX_ = 0, scrollY_ = 0;
|
||||
};
|
||||
|
||||
class Provider : public IServerTrackedDeviceProvider {
|
||||
public:
|
||||
EVRInitError Init(IVRDriverContext *context) override {
|
||||
VR_INIT_SERVER_DRIVER_CONTEXT(context);
|
||||
device_ = new PointerDevice(&state_);
|
||||
VRServerDriverHost()->TrackedDeviceAdded("ft_pointer_0", TrackedDeviceClass_Controller, device_);
|
||||
running_ = true;
|
||||
listener_ = std::thread([this] { Listen(); });
|
||||
return VRInitError_None;
|
||||
}
|
||||
|
||||
void Cleanup() override {
|
||||
running_ = false;
|
||||
if (sock_ >= 0) shutdown(sock_, SHUT_RDWR);
|
||||
if (listener_.joinable()) listener_.join();
|
||||
if (sock_ >= 0) close(sock_);
|
||||
VR_CLEANUP_SERVER_DRIVER_CONTEXT();
|
||||
}
|
||||
|
||||
const char *const *GetInterfaceVersions() override { return k_InterfaceVersions; }
|
||||
void RunFrame() override {
|
||||
if (device_) device_->RunFrame();
|
||||
}
|
||||
bool ShouldBlockStandbyMode() override { return false; }
|
||||
void EnterStandby() override {}
|
||||
void LeaveStandby() override {}
|
||||
|
||||
private:
|
||||
void Listen() {
|
||||
sock_ = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);
|
||||
sockaddr_un addr{};
|
||||
addr.sun_family = AF_UNIX;
|
||||
const char name[] = "ft_pointer";
|
||||
std::memcpy(addr.sun_path + 1, name, sizeof name - 1); // abstract namespace
|
||||
const socklen_t len = offsetof(sockaddr_un, sun_path) + 1 + sizeof name - 1;
|
||||
if (bind(sock_, reinterpret_cast<sockaddr *>(&addr), len) != 0) {
|
||||
VRDriverLog()->Log("ft_pointer: cannot bind control socket");
|
||||
return;
|
||||
}
|
||||
timeval tv{0, 200000};
|
||||
setsockopt(sock_, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
||||
char buf[256];
|
||||
while (running_) {
|
||||
const ssize_t n = recv(sock_, buf, sizeof buf - 1, 0);
|
||||
if (n <= 0) continue;
|
||||
buf[n] = 0;
|
||||
Handle(buf);
|
||||
}
|
||||
}
|
||||
|
||||
void Handle(const char *cmd) {
|
||||
std::lock_guard<std::mutex> guard(state_.lock);
|
||||
char name[32];
|
||||
float a, b;
|
||||
int v;
|
||||
float x, y, z, qw, qx, qy, qz;
|
||||
if (std::sscanf(cmd, "posq %f %f %f %f %f %f %f", &x, &y, &z, &qw, &qx, &qy, &qz) == 7) {
|
||||
state_.explicitPose = true;
|
||||
state_.hasQuat = true;
|
||||
state_.gaze = false;
|
||||
state_.pos[0] = x;
|
||||
state_.pos[1] = y;
|
||||
state_.pos[2] = z;
|
||||
state_.quat[0] = qw;
|
||||
state_.quat[1] = qx;
|
||||
state_.quat[2] = qy;
|
||||
state_.quat[3] = qz;
|
||||
} else if (std::sscanf(cmd, "pose %f %f %f %f %f", &x, &y, &z, &a, &b) == 5) {
|
||||
state_.explicitPose = true;
|
||||
state_.hasQuat = false;
|
||||
state_.gaze = false;
|
||||
state_.pos[0] = x;
|
||||
state_.pos[1] = y;
|
||||
state_.pos[2] = z;
|
||||
state_.yaw = a;
|
||||
state_.pitch = b;
|
||||
} else if (std::sscanf(cmd, "move %f %f", &a, &b) == 2) {
|
||||
state_.explicitPose = false;
|
||||
if (state_.gaze) state_.recenter = true; // first move starts from the gaze
|
||||
state_.yaw += a; // wrap by hand: libm remainder() is GLIBC_2.43 in the build container
|
||||
while (state_.yaw > 180.f) state_.yaw -= 360.f;
|
||||
while (state_.yaw < -180.f) state_.yaw += 360.f;
|
||||
state_.pitch = std::fmax(-85.f, std::fmin(85.f, state_.pitch + b));
|
||||
} else if (std::strncmp(cmd, "recenter", 8) == 0) {
|
||||
state_.explicitPose = false;
|
||||
state_.recenter = true;
|
||||
} else if (std::sscanf(cmd, "aim %f %f", &a, &b) == 2) {
|
||||
state_.gaze = false;
|
||||
state_.yaw = a;
|
||||
state_.pitch = b;
|
||||
} else if (std::strncmp(cmd, "gaze", 4) == 0) {
|
||||
state_.gaze = true;
|
||||
} else if (std::strncmp(cmd, "hide", 4) == 0) {
|
||||
state_.visible = false;
|
||||
} else if (std::strncmp(cmd, "show", 4) == 0) {
|
||||
state_.visible = true;
|
||||
} else if (std::sscanf(cmd, "btn %31s %d", name, &v) == 2) {
|
||||
for (int i = 0; i < kButtons; ++i)
|
||||
if (std::strcmp(name, kButtonNames[i]) == 0) state_.buttons[i] = v != 0;
|
||||
} else if (std::sscanf(cmd, "distance %f", &a) == 1) {
|
||||
state_.distance = std::fmax(0.3f, std::fmin(10.f, a));
|
||||
} else if (std::sscanf(cmd, "scroll %f %f", &a, &b) == 2) {
|
||||
state_.scrollX = a;
|
||||
state_.scrollY = b;
|
||||
}
|
||||
}
|
||||
|
||||
State state_;
|
||||
PointerDevice *device_ = nullptr;
|
||||
std::thread listener_;
|
||||
std::atomic<bool> running_{false};
|
||||
int sock_ = -1;
|
||||
};
|
||||
|
||||
Provider g_provider;
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" __attribute__((visibility("default"))) void *HmdDriverFactory(const char *interfaceName, int *returnCode) {
|
||||
if (std::strcmp(interfaceName, IServerTrackedDeviceProvider_Version) == 0) return &g_provider;
|
||||
if (returnCode) *returnCode = VRInitError_Init_InterfaceNotFound;
|
||||
return nullptr;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"alwaysActivate": true,
|
||||
"name": "ft_pointer",
|
||||
"directory": "",
|
||||
"resourceOnly": false,
|
||||
"hmd_presence": []
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"jsonid": "input_profile",
|
||||
"controller_type": "ft_pointer",
|
||||
"device_class": "TrackedDeviceClass_Controller",
|
||||
"resource_root": "ft_pointer",
|
||||
"driver_name": "ft_pointer",
|
||||
"input_bindingui_mode": "controller_handed",
|
||||
"should_show_binding_errors": true,
|
||||
"input_source": {
|
||||
"/input/trigger": {
|
||||
"type": "button",
|
||||
"click": true
|
||||
},
|
||||
"/input/b": {
|
||||
"type": "button",
|
||||
"click": true
|
||||
},
|
||||
"/input/x": {
|
||||
"type": "button",
|
||||
"click": true
|
||||
},
|
||||
"/input/system": {
|
||||
"type": "button",
|
||||
"click": true
|
||||
},
|
||||
"/input/joystick": {
|
||||
"type": "joystick",
|
||||
"click": true
|
||||
},
|
||||
"/pose/raw": {
|
||||
"type": "pose"
|
||||
},
|
||||
"/pose/tip": {
|
||||
"type": "pose"
|
||||
},
|
||||
"/input/a": {
|
||||
"type": "button",
|
||||
"click": true
|
||||
}
|
||||
},
|
||||
"default_bindings": [
|
||||
{
|
||||
"app_key": "openvr.component.vrcompositor",
|
||||
"binding_url": "ft_pointer_vrcompositor.json"
|
||||
},
|
||||
{
|
||||
"app_key": "steam.client",
|
||||
"binding_url": "ft_pointer_steam.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"action_manifest_version": 0,
|
||||
"alias_info": {},
|
||||
"app_key": "steam.client",
|
||||
"category": "steamvr_input",
|
||||
"controller_type": "ft_pointer",
|
||||
"description": "",
|
||||
"name": "frametop pointer: Steam client (input comes through the dashboard laser mouse)",
|
||||
"options": {},
|
||||
"simulated_actions": [],
|
||||
"bindings": {
|
||||
"/actions/haptics": {
|
||||
"sources": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
{
|
||||
"action_manifest_version": 0,
|
||||
"alias_info": {},
|
||||
"app_key": "openvr.component.vrcompositor",
|
||||
"category": "steamvr_input",
|
||||
"controller_type": "ft_pointer",
|
||||
"description": "",
|
||||
"name": "frametop pointer: dashboard laser mouse",
|
||||
"options": {},
|
||||
"simulated_actions": [],
|
||||
"bindings": {
|
||||
"/actions/lasermouse": {
|
||||
"sources": [
|
||||
{
|
||||
"path": "/user/hand/left/input/trigger",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse/in/leftclick"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/b",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse/in/rightclick"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/x",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse/in/middleclick"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/joystick",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse/in/back"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/trigger",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse/in/leftclick"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/b",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse/in/rightclick"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/x",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse/in/middleclick"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/joystick",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse/in/back"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"poses": [
|
||||
{
|
||||
"output": "/actions/lasermouse/in/Pointer",
|
||||
"path": "/user/hand/left/pose/raw"
|
||||
},
|
||||
{
|
||||
"output": "/actions/lasermouse/in/Pointer",
|
||||
"path": "/user/hand/right/pose/raw"
|
||||
}
|
||||
]
|
||||
},
|
||||
"/actions/scroll_discrete": {
|
||||
"sources": [
|
||||
{
|
||||
"path": "/user/hand/left/input/joystick",
|
||||
"mode": "scroll",
|
||||
"inputs": {
|
||||
"scroll": {
|
||||
"output": "/actions/scroll_discrete/in/scroll"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/joystick",
|
||||
"mode": "scroll",
|
||||
"inputs": {
|
||||
"scroll": {
|
||||
"output": "/actions/scroll_discrete/in/scroll"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"/actions/scroll_smooth": {
|
||||
"sources": [
|
||||
{
|
||||
"path": "/user/hand/left/input/joystick",
|
||||
"mode": "scroll",
|
||||
"inputs": {
|
||||
"scroll": {
|
||||
"output": "/actions/scroll_smooth/in/scroll"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/joystick",
|
||||
"mode": "scroll",
|
||||
"inputs": {
|
||||
"scroll": {
|
||||
"output": "/actions/scroll_smooth/in/scroll"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"/actions/system": {
|
||||
"sources": [
|
||||
{
|
||||
"path": "/user/hand/left/input/system",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/system/in/ToggleDashboard"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/system",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/system/in/ToggleDashboard"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"/actions/lasermouse_secondary": {
|
||||
"sources": [
|
||||
{
|
||||
"path": "/user/hand/left/input/trigger",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse_secondary/in/switchlaserhand"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/trigger",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse_secondary/in/switchlaserhand"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/a",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse_secondary/in/switchlaserhand"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/a",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse_secondary/in/switchlaserhand"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
newmtl invisible
|
||||
d 0
|
||||
map_Kd ft_pointer_invisible.png
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
# Invisible render model for the ft_pointer virtual controller.
|
||||
mtllib ft_pointer_invisible.mtl
|
||||
v 0 0 0
|
||||
v 0.0001 0 0
|
||||
v 0 0.0001 0
|
||||
vt 0 0
|
||||
vn 0 0 1
|
||||
usemtl invisible
|
||||
f 1/1/1 2/1/1 3/1/1
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 68 B |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"driver_ft_pointer": {
|
||||
"enable": true,
|
||||
"role": 2
|
||||
}
|
||||
}
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install, remove, or poke the ft_pointer SteamVR driver on the Frame.
|
||||
# Usage: pointer/driver/install.sh install # copy to ~/.local/share/frametop/ft_pointer and register
|
||||
# pointer/driver/install.sh uninstall # unregister and delete
|
||||
# pointer/driver/install.sh send '<cmd>' # e.g. 'btn trigger 1', 'aim 20 -5', 'gaze'
|
||||
# pointer/driver/install.sh aimhere # pin the ray (room-anchored) where the head points now
|
||||
# pointer/driver/install.sh probe # devices, roles, dashboard pointer (pointer/probe)
|
||||
# pointer/driver/install.sh log # ft_pointer lines from vrserver.txt
|
||||
# SteamVR loads drivers only at startup: restart it after install or uninstall.
|
||||
set -euo pipefail
|
||||
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
. "$root/scripts/_env.sh"
|
||||
frame="$root/scripts/frame.sh"
|
||||
src=$FRAME_REPO/pointer/driver
|
||||
dest=/home/steamos/.local/share/frametop/ft_pointer
|
||||
reg='/opt/steamvr/bin/linuxarm64/vrpathreg'
|
||||
|
||||
case ${1:-install} in
|
||||
install)
|
||||
"$root/scripts/sync.sh" >/dev/null
|
||||
"$frame" --host "set -e; test -f $src/build/driver_ft_pointer.so
|
||||
rm -rf $dest; mkdir -p $dest/bin/linuxarm64
|
||||
cp -r $src/ft_pointer/. $dest/
|
||||
cp $src/build/driver_ft_pointer.so $dest/bin/linuxarm64/
|
||||
LD_LIBRARY_PATH=/opt/steamvr/bin/linuxarm64 $reg adddriver $dest
|
||||
LD_LIBRARY_PATH=/opt/steamvr/bin/linuxarm64 $reg show | grep -A3 -i 'external'
|
||||
echo 'installed; restart SteamVR to load it'" ;;
|
||||
uninstall)
|
||||
"$frame" --host "LD_LIBRARY_PATH=/opt/steamvr/bin/linuxarm64 $reg removedriver $dest; rm -rf $dest; echo 'removed; restart SteamVR to unload it'" ;;
|
||||
send)
|
||||
"$frame" --host "python3 -c 'import socket,sys; s=socket.socket(socket.AF_UNIX,socket.SOCK_DGRAM); s.sendto(sys.argv[1].encode(), \"\\0ft_pointer\")' $(printf %q "${2:?command}")" ;;
|
||||
probe) "$frame" -C pointer/probe 'LD_LIBRARY_PATH=/opt/steamvr/bin/linuxarm64 ./build/vrprobe' ;;
|
||||
aimhere)
|
||||
read -r yaw pitch < <("$frame" -C pointer/probe 'LD_LIBRARY_PATH=/opt/steamvr/bin/linuxarm64 ./build/vrprobe' | sed -n 's/^head yaw = \([-0-9.]*\) pitch = \([-0-9.]*\)$/\1 \2/p')
|
||||
[ -n "${yaw:-}" ] || { echo "head pose not valid (headset off?)" >&2; exit 1; }
|
||||
"$0" send "aim $yaw $pitch" && echo "aimed at yaw $yaw pitch $pitch" ;;
|
||||
log) "$frame" --host "grep -iE 'ft_pointer' ~/.local/share/Steam/logs/vrserver.txt | tail -n ${2:-20}" ;;
|
||||
*) echo "usage: $0 install|uninstall|send '<cmd>'|aimhere|probe|log" >&2; exit 2 ;;
|
||||
esac
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the ft-pointer helper on the Frame, in the dev container (it also runs there).
|
||||
set -euo pipefail
|
||||
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
"$root/scripts/sync.sh" >/dev/null
|
||||
exec "$root/scripts/frame.sh" -C pointer/helper 'set -e; mkdir -p build
|
||||
g++ -std=c++17 -O2 -Wall -Wno-unused-parameter -I/opt/steamvr/tools/hellovr_vulkan_linux/src/openvr/headers -I../common \
|
||||
-o build/ft-pointer ft-pointer.cpp -L/opt/steamvr/bin/linuxarm64 -lopenvr_api -Wl,-rpath,/opt/steamvr/bin/linuxarm64 -lpthread
|
||||
echo "built build/ft-pointer"'
|
||||
@@ -0,0 +1,19 @@
|
||||
# Template: the installer replaces @REPO@ with the repo path on the Frame.
|
||||
[Unit]
|
||||
Description=Frametop pointer helper: the universal 3D mouse (cursor, collision, ft_pointer driver)
|
||||
Documentation=file://@REPO@/frametop/README.md
|
||||
# Needs SteamVR's IPC; it starts and stops with SteamVR.
|
||||
After=steamvr.service frametop-input-relay.service
|
||||
PartOf=steamvr.service
|
||||
|
||||
[Service]
|
||||
# Runs in the dev container (built there against its libraries). The helper process
|
||||
# lives in the container, so clean it up explicitly around distrobox enter.
|
||||
ExecStartPre=-/usr/bin/pkill -x ft-pointer
|
||||
ExecStart=%h/.local/bin/distrobox enter dev -- @REPO@/frametop/pointer/helper/build/ft-pointer
|
||||
ExecStopPost=-/usr/bin/pkill -x ft-pointer
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=steamvr.service
|
||||
@@ -0,0 +1,946 @@
|
||||
// ft-pointer: the universal 3D mouse's brain (OpenVR overlay client, runs in the dev container).
|
||||
//
|
||||
// input-relay.py (pointer mode) sends mouse commands here; this program keeps the
|
||||
// cursor, does collision against SteamVR's overlays, draws the free-space dot, and
|
||||
// sends the ft_pointer driver the exact pose of its virtual controller.
|
||||
//
|
||||
// relay -> @ft_pointer_helper -> ft-pointer -> @ft_pointer -> ft_pointer driver (inside vrserver)
|
||||
//
|
||||
// Cursor model:
|
||||
// - anchor: head position at the last recenter; yaw/pitch: direction from it (mouse-driven).
|
||||
// - Every frame a ray from the anchor is tested against every visible overlay
|
||||
// (ComputeOverlayIntersection). On a hit the cursor sits on that surface; otherwise
|
||||
// it floats `distance` metres out and a small dot overlay is shown there, which the
|
||||
// laser can hit, so SteamVR never draws a free-flying laser.
|
||||
// - Then the line of sight from the eye (not the anchor) to that point is tested too:
|
||||
// after the head moves, something nearer can cover the point, and the cursor goes on
|
||||
// whatever you see under it (panels close together in view, at different depths).
|
||||
// - Looks: the compositor ignores live changes to dashboard.laserRayWidthScale (only
|
||||
// the dashboard's own Settings screen reloads it), so the beam can't be switched
|
||||
// off per device. Instead the laser starts POINTER_ORIGIN_FRACTION (0.95) of the way
|
||||
// from the eye to the cursor, along the line of sight: what's left of the beam is a
|
||||
// few centimetres long and effectively invisible, and SteamVR's hit dot (sized by
|
||||
// distance from the origin) becomes tiny. Our own white dot is the visible cursor
|
||||
// everywhere: a non-interactive dot on panels (the laser passes through it), and an
|
||||
// interactive one in free space (the laser lands on it instead of flying off).
|
||||
// - The controller ray starts at the eye and aims at the cursor point. Everything here
|
||||
// is computed in the standing universe; the pose sent to the driver is converted to
|
||||
// SteamVR's raw tracking space (drivers report raw poses; on the Frame the standing
|
||||
// origin is ~1.6 m above the raw one, so sending standing coordinates put the laser
|
||||
// origin 1.6 m above the head). While our device
|
||||
// owns the dashboard pointer, dashboard.laserRayWidthScale is 0 so only the dot shows.
|
||||
// It's restored when a controller takes the pointer back.
|
||||
//
|
||||
// Last used wins: when a real controller moves (picked up), the pointer is released
|
||||
// at once (driver "hide", which also drops its hand role hint), so the controller gets
|
||||
// its role and laser back. The next mouse input reconnects and claims the laser again.
|
||||
// SteamVR gives a contested hand role to the most recently used device, and a held
|
||||
// Frame controller counts as used (touch sensors). If our device hasn't got the hand
|
||||
// role within a second of waking, the pointer is released (no orphan white dot) and
|
||||
// mouse input can't wake it again for 2 s.
|
||||
//
|
||||
// Laser mode: with the dashboard closed, SteamVR keeps its laser mouse off until a
|
||||
// click (the first click on a panel only turned it on, the second one clicked), and a
|
||||
// laser that leaves every panel turns it off again. While the pointer is awake, the
|
||||
// helper shows frametop.pointer.lasermode: a transparent 1 mm overlay 50 m below the
|
||||
// head with VROverlayFlags_MakeOverlaysInteractiveIfVisible, which keeps SteamVR's
|
||||
// laser mouse mode on as long as it's visible. It's hidden whenever the pointer is
|
||||
// released, so controllers and VR games get the normal behaviour back.
|
||||
//
|
||||
// Tilt: while the left button is held (dragging a panel by its grab bar, which SteamVR
|
||||
// moves rigidly with the controller), pressing the right button enters tilt mode. The
|
||||
// right press is not forwarded; mouse motion then rotates the virtual controller around
|
||||
// the grab point (horizontal: about the vertical axis, vertical: about the view's
|
||||
// horizontal axis), so the panel turns around that pivot. The tilt accumulates for the
|
||||
// whole drag: after the right button is released, the rotation stays applied (about the
|
||||
// moving cursor point) so the grabbed panel keeps its new orientation, and pressing right
|
||||
// again continues from it. Releasing the left button drops the panel; the tilted pose (and
|
||||
// the drag lock) are held 0.5 s longer, because SteamVR's dashboard finishes a floating
|
||||
// move up to 150 ms after the release (UndockedOverlay.endFloatingWindowMove measures the
|
||||
// push distance first) and reads the controller pose again then.
|
||||
//
|
||||
// Scene-graph overlays: the dashboard's dock (valve.steam.gamepadui.bar) and the controls
|
||||
// under floating windows (valve.steam.gamepadui.floatingfooter, undock and friends) have
|
||||
// no texture (0x0) and a placeholder width, so ComputeOverlayIntersection never hits
|
||||
// them. For those the ray is tested against the overlay's plane, within
|
||||
// POINTER_SCENE_RADIUS (0.5 m) of its origin; the laser-catching dot sits 5 cm behind the
|
||||
// plane, so the laser reaches the buttons and still lands on the dot between them.
|
||||
// Only absolutely placed 0x0 overlays count as scene-graph: gamescope's app panels (the
|
||||
// desktops) also report 0x0, but they're placed as dashboard tabs, stay up when the
|
||||
// dashboard closes, and ComputeOverlayIntersection hits them normally.
|
||||
//
|
||||
// Panel edges: off a panel, the cursor stays on that panel's plane while it's within
|
||||
// POINTER_EDGE_REACH (0.3 m) of the last point it touched, instead of jumping to
|
||||
// POINTER_DISTANCE. A floating panel's resize margins and the window controls under it
|
||||
// sit just outside the panel, and the laser has to start in front of that plane to reach
|
||||
// them (a controller's laser always does: it starts at the hand). Like on scene-graph
|
||||
// planes, the laser-catching dot sits 5 cm behind the plane.
|
||||
//
|
||||
// Drag lock: while the left button is held, the cursor keeps the distance it had at the
|
||||
// press and collision is frozen, so dragging past a panel's edge (resizing, moving)
|
||||
// doesn't jump the cursor to free space or swap in the laser-catching dot, which made
|
||||
// SteamVR's resize snap back.
|
||||
//
|
||||
// Placement (for layout): SteamVR keeps a floating panel's position inside the
|
||||
// dashboard, where nothing outside can set it, so the helper carries panels like a user
|
||||
// would. It measures the panel (md::ScanPanel), aims the device at its grab bar
|
||||
// (LAYOUT_GRAB_OFFSET, 7.5 cm below the bottom edge; the bands at 2-4 and 14-26 cm are
|
||||
// other controls), presses, moves, and releases. While grabbed, the panel follows the
|
||||
// device rigidly, except that the dashboard accelerates fast translations (0.1 m in 0.3 s
|
||||
// moved it 0.19 m and turned it 8.5 deg, in jerky 25 ms steps right after the press). So
|
||||
// the device hovers first, and the move is split into a rotation about the device origin
|
||||
// (the eye) at 60 deg/s and a smooth 60 Hz slide at LAYOUT_SLIDE_SPEED (0.5 m/s; tested
|
||||
// exact from 0.07 to 1 m/s). Scroll pushes along the panel normal, but only in whole notches of
|
||||
// about 7 cm, so it isn't used. The result is measured again, and the move repeated up to
|
||||
// twice while it's more than 1.5 cm or 1 deg off.
|
||||
//
|
||||
// Commands (datagrams on @ft_pointer_helper): show, hide, recenter, move <dyaw> <dpitch>,
|
||||
// reload (re-read the settings below), debug (toggle a twice-a-second state log),
|
||||
// and btn/scroll lines, which are forwarded to the driver unchanged. For layouts, with a
|
||||
// reply datagram to the sender's (abstract) address:
|
||||
// place <overlay> <x> <y> <z> <yaw> <pitch> [roll [grab]]: centre in the standing
|
||||
// universe; the front faces back along the direction (yaw, pitch), turned by roll
|
||||
// (counterclockwise as seen, degrees) -> "ok ..." | "error ..."
|
||||
// measure <overlay> -> "ok cx cy cz width height xx xy xz yx yy yz zx zy zz" (centre,
|
||||
// size, and the panel's right, up, and front vectors)
|
||||
// head -> "ok x y z yaw pitch"
|
||||
// grabprobe <overlay>: log where below the panel SteamVR's laser hits something (to
|
||||
// find the grab bar again if a SteamVR update moves it)
|
||||
//
|
||||
// Settings (~/.config/frametop.conf): POINTER_DISTANCE (m, 1.5), POINTER_CURSOR_DEG
|
||||
// (angular size of the dot, 0.4), POINTER_LASER_WIDTH (controller beam width to restore, 0.8),
|
||||
// POINTER_ORIGIN_FRACTION (0.95): the laser starts this far along the eye-to-cursor line,
|
||||
// but never closer than POINTER_ORIGIN_MARGIN (0.15 m) to the cursor point: SteamVR's
|
||||
// small controls (undock, frame buttons) float a few centimetres in front of their
|
||||
// panel, and a laser that starts behind them can't hit them.
|
||||
#include <openvr.h>
|
||||
|
||||
#include "vrmath.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace md;
|
||||
|
||||
std::map<std::string, std::string> ReadConfig() {
|
||||
std::map<std::string, std::string> conf;
|
||||
const char *home = std::getenv("HOME");
|
||||
std::ifstream in(std::string(home ? home : "") + "/.config/frametop.conf");
|
||||
std::string line;
|
||||
while (std::getline(in, line)) {
|
||||
line = line.substr(0, line.find('#'));
|
||||
const auto eq = line.find('=');
|
||||
if (eq == std::string::npos) continue;
|
||||
auto trim = [](std::string s) {
|
||||
s.erase(0, s.find_first_not_of(" \t"));
|
||||
s.erase(s.find_last_not_of(" \t") + 1);
|
||||
return s;
|
||||
};
|
||||
conf[trim(line.substr(0, eq))] = trim(line.substr(eq + 1));
|
||||
}
|
||||
return conf;
|
||||
}
|
||||
|
||||
double ConfDouble(const std::map<std::string, std::string> &c, const char *key, double fallback) {
|
||||
auto it = c.find(key);
|
||||
return it == c.end() ? fallback : std::atof(it->second.c_str());
|
||||
}
|
||||
|
||||
int AbstractSocket(const char *name, bool bindIt) {
|
||||
const int fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
|
||||
if (bindIt) {
|
||||
sockaddr_un addr{};
|
||||
addr.sun_family = AF_UNIX;
|
||||
std::memcpy(addr.sun_path + 1, name, std::strlen(name));
|
||||
const socklen_t len = offsetof(sockaddr_un, sun_path) + 1 + std::strlen(name);
|
||||
if (bind(fd, reinterpret_cast<sockaddr *>(&addr), len) != 0) {
|
||||
std::perror("bind @ft_pointer_helper (already running?)");
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
void SendTo(int fd, const char *name, const std::string &msg) {
|
||||
sockaddr_un addr{};
|
||||
addr.sun_family = AF_UNIX;
|
||||
std::memcpy(addr.sun_path + 1, name, std::strlen(name));
|
||||
const socklen_t len = offsetof(sockaddr_un, sun_path) + 1 + std::strlen(name);
|
||||
sendto(fd, msg.data(), msg.size(), 0, reinterpret_cast<sockaddr *>(&addr), len);
|
||||
}
|
||||
|
||||
// Overlay keys, refreshed in the background from `vrcmd --overlays` (OpenVR has no
|
||||
// public call to enumerate other apps' overlays). Hidden ones are listed too: the
|
||||
// window controls under a floating panel only appear while something hovers the
|
||||
// panel, and the cursor has to find them the moment they do, not a second later.
|
||||
class OverlayList {
|
||||
public:
|
||||
void Start() {
|
||||
thread_ = std::thread([this] {
|
||||
while (running_) {
|
||||
Refresh();
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
}
|
||||
});
|
||||
}
|
||||
void Stop() {
|
||||
running_ = false;
|
||||
if (thread_.joinable()) thread_.join();
|
||||
}
|
||||
std::vector<std::string> Keys() {
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
return keys_;
|
||||
}
|
||||
|
||||
private:
|
||||
void Refresh() {
|
||||
FILE *p = popen("LD_LIBRARY_PATH=/opt/steamvr/bin/linuxarm64 /opt/steamvr/bin/linuxarm64/vrcmd --overlays 2>/dev/null", "r");
|
||||
if (!p) return;
|
||||
std::vector<std::string> keys;
|
||||
char line[1024];
|
||||
while (std::fgets(line, sizeof line, p)) {
|
||||
// 'key' -- 'name', WxH visible VROverlayType_...
|
||||
if (line[0] != '\'') continue;
|
||||
const char *end = std::strchr(line + 1, '\'');
|
||||
if (!end) continue;
|
||||
const std::string key(line + 1, size_t(end - (line + 1)));
|
||||
const std::string rest(end);
|
||||
if (rest.find("Thumbnail") != std::string::npos || rest.find("Subview") != std::string::npos) continue;
|
||||
if (key.rfind("system.pointer", 0) == 0 || key.rfind("system.cursor", 0) == 0 ||
|
||||
key.rfind("frametop.pointer", 0) == 0 || key.rfind("frametop.guide", 0) == 0 ||
|
||||
key == "system.HeadsetView" || key == "system.toast")
|
||||
continue;
|
||||
keys.push_back(key);
|
||||
}
|
||||
pclose(p);
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
keys_ = std::move(keys);
|
||||
}
|
||||
|
||||
std::thread thread_;
|
||||
std::atomic<bool> running_{true};
|
||||
std::mutex lock_;
|
||||
std::vector<std::string> keys_;
|
||||
};
|
||||
|
||||
vr::HmdMatrix34_t Billboard(Vec3 at, Vec3 eye) {
|
||||
// Overlay faces +Z; point +Z at the eye, keep +Y roughly up.
|
||||
const Vec3 z = Normalize(eye - at);
|
||||
const Vec3 x = Normalize(Cross({0, 1, 0}, z));
|
||||
const Vec3 y = Cross(z, x);
|
||||
vr::HmdMatrix34_t m{};
|
||||
const Vec3 cols[3] = {x, y, z};
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
m.m[0][c] = float(cols[c].x);
|
||||
m.m[1][c] = float(cols[c].y);
|
||||
m.m[2][c] = float(cols[c].z);
|
||||
}
|
||||
m.m[0][3] = float(at.x);
|
||||
m.m[1][3] = float(at.y);
|
||||
m.m[2][3] = float(at.z);
|
||||
return m;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> DotTexture(int size) {
|
||||
// White dot with a dark rim, soft edge, transparent outside.
|
||||
std::vector<uint8_t> px(size * size * 4, 0);
|
||||
const double c = (size - 1) / 2.0, r = size * 0.42, rim = size * 0.10;
|
||||
for (int y = 0; y < size; ++y)
|
||||
for (int x = 0; x < size; ++x) {
|
||||
const double d = std::hypot(x - c, y - c);
|
||||
const double a = std::clamp(r - d + 0.5, 0.0, 1.0);
|
||||
const bool inner = d < r - rim;
|
||||
uint8_t *p = &px[(y * size + x) * 4];
|
||||
const uint8_t v = inner ? 255 : 40;
|
||||
p[0] = p[1] = p[2] = v;
|
||||
p[3] = uint8_t(a * 235);
|
||||
}
|
||||
return px;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Placement speeds (see "Placement" at the top).
|
||||
constexpr double kPlaceDegPerSec = 60; // tested: 40 deg/s is applied exactly
|
||||
|
||||
int main() {
|
||||
double freeDistance = 1.5, cursorDeg = 0.4, originFraction = 0.95, originMargin = 0.15, sceneRadius = 0.5,
|
||||
edgeReach = 0.3, grabOffset = 0.075,
|
||||
slideSpeed = 0.5;
|
||||
auto loadConfig = [&] {
|
||||
const auto conf = ReadConfig();
|
||||
freeDistance = std::clamp(ConfDouble(conf, "POINTER_DISTANCE", 1.5), 0.3, 10.0);
|
||||
cursorDeg = std::clamp(ConfDouble(conf, "POINTER_CURSOR_DEG", 0.4), 0.05, 5.0);
|
||||
originFraction = std::clamp(ConfDouble(conf, "POINTER_ORIGIN_FRACTION", 0.95), 0.0, 0.98);
|
||||
originMargin = std::clamp(ConfDouble(conf, "POINTER_ORIGIN_MARGIN", 0.15), 0.0, 1.0);
|
||||
sceneRadius = std::clamp(ConfDouble(conf, "POINTER_SCENE_RADIUS", 0.5), 0.05, 2.0);
|
||||
edgeReach = std::clamp(ConfDouble(conf, "POINTER_EDGE_REACH", 0.3), 0.0, 2.0);
|
||||
grabOffset = std::clamp(ConfDouble(conf, "LAYOUT_GRAB_OFFSET", 0.075), 0.0, 1.0);
|
||||
slideSpeed = std::clamp(ConfDouble(conf, "LAYOUT_SLIDE_SPEED", 0.5), 0.02, 2.0);
|
||||
};
|
||||
loadConfig();
|
||||
const float laserWidth = float(ConfDouble(ReadConfig(), "POINTER_LASER_WIDTH", 0.8));
|
||||
|
||||
vr::EVRInitError err = vr::VRInitError_None;
|
||||
while (true) {
|
||||
vr::VR_Init(&err, vr::VRApplication_Overlay);
|
||||
if (err == vr::VRInitError_None) break;
|
||||
std::fprintf(stderr, "waiting for SteamVR: %s\n", vr::VR_GetVRInitErrorAsEnglishDescription(err));
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
}
|
||||
auto *sys = vr::VRSystem();
|
||||
auto *overlay = vr::VROverlay();
|
||||
|
||||
vr::VROverlayHandle_t cursor = vr::k_ulOverlayHandleInvalid;
|
||||
overlay->CreateOverlay("frametop.pointer.cursor", "Frametop pointer", &cursor);
|
||||
const int texSize = 64;
|
||||
auto tex = DotTexture(texSize);
|
||||
overlay->SetOverlayRaw(cursor, tex.data(), texSize, texSize, 4);
|
||||
overlay->SetOverlayInputMethod(cursor, vr::VROverlayInputMethod_Mouse); // the laser can land on it
|
||||
overlay->SetOverlaySortOrder(cursor, 200);
|
||||
// Same dot, not interactive, drawn on panels at the hit point; the laser passes through.
|
||||
vr::VROverlayHandle_t marker = vr::k_ulOverlayHandleInvalid;
|
||||
overlay->CreateOverlay("frametop.pointer.marker", "Frametop pointer marker", &marker);
|
||||
overlay->SetOverlayRaw(marker, tex.data(), texSize, texSize, 4);
|
||||
overlay->SetOverlayInputMethod(marker, vr::VROverlayInputMethod_None);
|
||||
overlay->SetOverlaySortOrder(marker, 201);
|
||||
// Laser mode (see the top of the file).
|
||||
vr::VROverlayHandle_t laserMode = vr::k_ulOverlayHandleInvalid;
|
||||
overlay->CreateOverlay("frametop.pointer.lasermode", "Frametop pointer laser mode", &laserMode);
|
||||
std::vector<uint8_t> clear(4 * 4 * 4, 0);
|
||||
overlay->SetOverlayRaw(laserMode, clear.data(), 4, 4, 4);
|
||||
overlay->SetOverlayWidthInMeters(laserMode, 0.001f);
|
||||
overlay->SetOverlayInputMethod(laserMode, vr::VROverlayInputMethod_Mouse); // the flag needs an input method
|
||||
overlay->SetOverlayFlag(laserMode, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true);
|
||||
vr::HmdMatrix34_t below{};
|
||||
below.m[0][0] = below.m[1][1] = below.m[2][2] = 1;
|
||||
below.m[1][3] = -50;
|
||||
overlay->SetOverlayTransformTrackedDeviceRelative(laserMode, vr::k_unTrackedDeviceIndex_Hmd, &below);
|
||||
bool laserModeShown = false;
|
||||
// Controller beams keep the user's width; nothing here changes it any more.
|
||||
vr::VRSettings()->SetFloat("dashboard", "laserRayWidthScale", laserWidth);
|
||||
|
||||
const int in = AbstractSocket("ft_pointer_helper", true);
|
||||
const int out = AbstractSocket(nullptr, false);
|
||||
OverlayList overlays;
|
||||
overlays.Start();
|
||||
std::map<std::string, vr::VROverlayHandle_t> handles;
|
||||
std::map<std::string, bool> sceneGraph; // no texture: plane test instead of ComputeOverlayIntersection
|
||||
std::map<std::string, bool> visible; // refreshed every 50 ms
|
||||
auto lastVisible = std::chrono::steady_clock::now();
|
||||
// The plane of the last panel the cursor was on, and the last point on it (panel edges).
|
||||
Vec3 edgePoint, edgeNormal, edgeLast;
|
||||
std::string edgeKey;
|
||||
|
||||
bool active = false, recenter = false, anchored = false;
|
||||
using Clock = std::chrono::steady_clock;
|
||||
Clock::time_point lastMouse{}, claimAt{}, claimRelease{}, wokeAt{}, noWakeUntil{};
|
||||
bool claimPending = false, claimHeld = false;
|
||||
// Tilt mode (see top of file).
|
||||
bool leftHeld = false, tilting = false, tiltStart = false, swallowedRight = false;
|
||||
double tiltYaw = 0, tiltPitch = 0;
|
||||
double dragDistance = 0, lastDistance = 1.5; // drag lock: distance from the anchor at the press
|
||||
Clock::time_point dropHoldUntil{}; // after a left release: keep the drag pose this long
|
||||
bool debug = false;
|
||||
std::string lastHit;
|
||||
auto lastDebug = Clock::now();
|
||||
vr::VROverlayHandle_t systemPointer = vr::k_ulOverlayHandleInvalid;
|
||||
overlay->FindOverlay("system.pointer", &systemPointer);
|
||||
Vec3 pivot, tiltOrigin, lastPoint, lastOrigin, lastAim{0, 0, -1};
|
||||
Basis tiltBasis{};
|
||||
auto wake = [&](Clock::time_point t) {
|
||||
if (t < noWakeUntil) return;
|
||||
wokeAt = t;
|
||||
active = true;
|
||||
recenter = true;
|
||||
SendTo(out, "ft_pointer", "show");
|
||||
claimPending = true; // take the laser without clicking, once SteamVR has bound the device
|
||||
claimAt = t + std::chrono::milliseconds(300);
|
||||
};
|
||||
Vec3 anchor;
|
||||
double yaw = 0, pitch = 0;
|
||||
vr::TrackedDeviceIndex_t ours = vr::k_unTrackedDeviceIndexInvalid;
|
||||
auto lastSlow = std::chrono::steady_clock::now() - std::chrono::seconds(10);
|
||||
|
||||
// --- Panel placement (see "Placement" at the top of the file) ---
|
||||
// Device pose, given in the standing universe, sent to the driver in raw space.
|
||||
auto sendPose = [&](Vec3 originStanding, const Basis &b) {
|
||||
vr::TrackedDevicePose_t s, r;
|
||||
sys->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0, &s, 1);
|
||||
sys->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseRawAndUncalibrated, 0, &r, 1);
|
||||
const auto &S = s.mDeviceToAbsoluteTracking, &R = r.mDeviceToAbsoluteTracking;
|
||||
auto toRaw = [&](Vec3 v) { return Rotate(R, RotateInverse(S, v)); };
|
||||
const Vec3 o = Position(R) + toRaw(originStanding - Position(S));
|
||||
double q[4];
|
||||
BasisQuat({toRaw(b.x), toRaw(b.y), toRaw(b.z)}, q);
|
||||
char msg[200];
|
||||
std::snprintf(msg, sizeof msg, "posq %.5f %.5f %.5f %.6f %.6f %.6f %.6f", o.x, o.y, o.z, q[0], q[1], q[2], q[3]);
|
||||
SendTo(out, "ft_pointer", msg);
|
||||
};
|
||||
auto sleepMs = [](int ms) { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); };
|
||||
auto headPos = [&] {
|
||||
vr::TrackedDevicePose_t s;
|
||||
sys->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0, &s, 1);
|
||||
return std::make_pair(s.bPoseIsValid, Position(s.mDeviceToAbsoluteTracking));
|
||||
};
|
||||
// Borrow the device and the laser for a placement: connect, claim, laser mode on.
|
||||
auto borrow = [&] {
|
||||
SendTo(out, "ft_pointer", "show");
|
||||
overlay->ShowOverlay(laserMode);
|
||||
overlay->HideOverlay(cursor);
|
||||
overlay->HideOverlay(marker);
|
||||
sleepMs(active ? 50 : 400); // a fresh connect needs SteamVR to bind the device
|
||||
SendTo(out, "ft_pointer", "btn a 1");
|
||||
sleepMs(60);
|
||||
SendTo(out, "ft_pointer", "btn a 0");
|
||||
};
|
||||
auto giveBack = [&] {
|
||||
if (!active) {
|
||||
SendTo(out, "ft_pointer", "hide");
|
||||
overlay->HideOverlay(laserMode);
|
||||
laserModeShown = false;
|
||||
}
|
||||
};
|
||||
auto findPanel = [&](const char *key, Panel &p, Vec3 &eye) -> std::string {
|
||||
vr::VROverlayHandle_t h;
|
||||
if (overlay->FindOverlay(key, &h) != vr::VROverlayError_None) return std::string("no overlay ") + key;
|
||||
bool valid;
|
||||
std::tie(valid, eye) = headPos();
|
||||
if (!valid) return "no head pose (headset off?)";
|
||||
p = ScanPanel(h, eye, 0.5);
|
||||
if (!p.found) return std::string("panel not visible: ") + key;
|
||||
return "";
|
||||
};
|
||||
|
||||
// Spike: aim down from the panel's bottom edge, 1 cm a step, and log where SteamVR's
|
||||
// laser hits something (the hit dot shows) and whether the window controls are up.
|
||||
auto grabProbe = [&](const char *key) {
|
||||
Panel p;
|
||||
Vec3 eye;
|
||||
const std::string err = findPanel(key, p, eye);
|
||||
if (!err.empty()) {
|
||||
std::printf("grabprobe: %s\n", err.c_str());
|
||||
std::fflush(stdout);
|
||||
return;
|
||||
}
|
||||
borrow();
|
||||
vr::VROverlayHandle_t footer = vr::k_ulOverlayHandleInvalid;
|
||||
overlay->FindOverlay("valve.steam.gamepadui.floatingfooter", &footer);
|
||||
const Vec3 bottom = p.center - p.basis.y * (p.height / 2);
|
||||
std::printf("grabprobe %s: center (%.3f %.3f %.3f) %.3f x %.3f m\n", key, p.center.x, p.center.y, p.center.z,
|
||||
p.width, p.height);
|
||||
for (int cm = -5; cm <= 45; ++cm) {
|
||||
const Vec3 target = bottom - p.basis.y * (cm / 100.0);
|
||||
sendPose(eye, AimBasis(target - eye));
|
||||
sleepMs(90);
|
||||
const bool dot = systemPointer != vr::k_ulOverlayHandleInvalid && overlay->IsOverlayVisible(systemPointer);
|
||||
const bool foot = footer != vr::k_ulOverlayHandleInvalid && overlay->IsOverlayVisible(footer);
|
||||
std::printf(" %+3d cm below the bottom edge: steamvr_dot=%d footer=%d", cm, dot, foot);
|
||||
if (foot) {
|
||||
vr::ETrackingUniverseOrigin uo;
|
||||
vr::HmdMatrix34_t t{};
|
||||
if (overlay->GetOverlayTransformAbsolute(footer, &uo, &t) == vr::VROverlayError_None) {
|
||||
const Vec3 f = Position(t) - p.center;
|
||||
std::printf(" footer at panel (%.3f %.3f %.3f)", Dot(f, p.basis.x), Dot(f, p.basis.y),
|
||||
Dot(f, p.basis.z));
|
||||
}
|
||||
}
|
||||
std::printf("\n");
|
||||
}
|
||||
std::fflush(stdout);
|
||||
giveBack();
|
||||
};
|
||||
|
||||
// Carry a floating panel so its centre lands on `target` with frame `bt` (see
|
||||
// "Placement" at the top). Returns "ok ..." or "error ...".
|
||||
auto place = [&](const char *key, Vec3 target, const Basis &bt, double grabBelow) -> std::string {
|
||||
Panel p;
|
||||
Vec3 eye;
|
||||
std::string err = findPanel(key, p, eye);
|
||||
if (!err.empty()) return "error " + err;
|
||||
auto offBy = [&](const Panel &q, double &cm, double °) {
|
||||
cm = Length(q.center - target) * 100;
|
||||
const double c = (Dot(q.basis.x, bt.x) + Dot(q.basis.y, bt.y) + Dot(q.basis.z, bt.z) - 1) / 2;
|
||||
deg = std::acos(std::clamp(c, -1.0, 1.0)) * 180 / M_PI;
|
||||
};
|
||||
double cm, deg;
|
||||
int moves = 0;
|
||||
borrow();
|
||||
for (int attempt = 0; attempt < 3; ++attempt) {
|
||||
offBy(p, cm, deg);
|
||||
if (cm < 1.5 && deg < 1.0) break;
|
||||
// The rigid motion that takes the panel to the target: rotate by R, then move.
|
||||
auto turn = [&](Vec3 v) { return FromBasis(bt, ToBasis(p.basis, v)); };
|
||||
double q[4];
|
||||
BasisQuat({turn({1, 0, 0}), turn({0, 1, 0}), turn({0, 0, 1})}, q);
|
||||
const double angle = 2 * std::acos(std::clamp(q[0], -1.0, 1.0));
|
||||
const Vec3 axis = std::sin(angle / 2) > 1e-6 ? Normalize({q[1], q[2], q[3]}) : Vec3{0, 1, 0};
|
||||
const Vec3 grab = p.center - p.basis.y * (p.height / 2 + grabBelow);
|
||||
const Basis d0 = AimBasis(grab - eye);
|
||||
const Vec3 o1 = target + turn(eye - p.center); // device origin at the end
|
||||
++moves;
|
||||
sendPose(eye, d0);
|
||||
sleepMs(150); // hover: the window controls come up
|
||||
SendTo(out, "ft_pointer", "btn trigger 1");
|
||||
sleepMs(150);
|
||||
// 1. Rotate about the device origin (the eye): the dashboard applies it exactly.
|
||||
const int rsteps = std::max(4, int(angle * 180 / M_PI / kPlaceDegPerSec * 60));
|
||||
for (int i = 1; i <= rsteps; ++i) {
|
||||
const double a = angle * i / rsteps;
|
||||
auto r = [&](Vec3 v) { return RotateAbout(v, axis, a); };
|
||||
sendPose(eye, {r(d0.x), r(d0.y), r(d0.z)});
|
||||
sleepMs(16);
|
||||
}
|
||||
// 2. Slide the device slowly: fast moves are accelerated by the dashboard.
|
||||
const Basis d1{turn(d0.x), turn(d0.y), turn(d0.z)};
|
||||
const int tsteps = std::max(4, int(Length(o1 - eye) / slideSpeed * 60));
|
||||
for (int i = 1; i <= tsteps; ++i) {
|
||||
sendPose(eye + (o1 - eye) * (double(i) / tsteps), d1);
|
||||
sleepMs(16);
|
||||
}
|
||||
sleepMs(100);
|
||||
SendTo(out, "ft_pointer", "btn trigger 0");
|
||||
sleepMs(600); // the dashboard re-reads the pose up to 150 ms after the release
|
||||
err = findPanel(key, p, eye);
|
||||
if (!err.empty()) break;
|
||||
}
|
||||
giveBack();
|
||||
if (!err.empty()) return "error after the move: " + err;
|
||||
offBy(p, cm, deg);
|
||||
char msg[160];
|
||||
std::snprintf(msg, sizeof msg, "ok %s off by %.1f cm, %.1f deg after %d move%s", key, cm, deg, moves,
|
||||
moves == 1 ? "" : "s");
|
||||
std::printf("place: %s\n", msg);
|
||||
std::fflush(stdout);
|
||||
return msg;
|
||||
};
|
||||
|
||||
std::printf("ft-pointer running: free distance %.2f m, dot %.2f deg\n", freeDistance, cursorDeg);
|
||||
std::fflush(stdout);
|
||||
|
||||
while (true) {
|
||||
// Commands from the relay.
|
||||
char buf[256];
|
||||
ssize_t n;
|
||||
sockaddr_un from{};
|
||||
socklen_t fromLen = sizeof from;
|
||||
while ((n = recvfrom(in, buf, sizeof buf - 1, 0, reinterpret_cast<sockaddr *>(&from), &fromLen)) > 0) {
|
||||
buf[n] = 0;
|
||||
// Reply to the sender (the layout tool binds an abstract address to get answers).
|
||||
const sockaddr_un sender = from;
|
||||
const socklen_t senderLen = fromLen;
|
||||
fromLen = sizeof from;
|
||||
auto reply = [&](const std::string &msg) {
|
||||
if (senderLen > offsetof(sockaddr_un, sun_path))
|
||||
sendto(out, msg.data(), msg.size(), 0, reinterpret_cast<const sockaddr *>(&sender), senderLen);
|
||||
};
|
||||
const bool mouseInput = std::strncmp(buf, "move", 4) == 0 || std::strncmp(buf, "btn", 3) == 0 ||
|
||||
std::strncmp(buf, "scroll", 6) == 0;
|
||||
if (mouseInput) lastMouse = Clock::now();
|
||||
// Any mouse input wakes the pointer (after a controller took over, or a helper restart).
|
||||
if (!active && mouseInput) wake(Clock::now());
|
||||
double a, b;
|
||||
char key[128];
|
||||
double px, py, pz, pyaw, ppitch, proll = 0, pgrab = -1;
|
||||
if (std::sscanf(buf, "grabprobe %127s", key) == 1) {
|
||||
grabProbe(key);
|
||||
continue;
|
||||
}
|
||||
if (std::sscanf(buf, "place %127s %lf %lf %lf %lf %lf %lf %lf", key, &px, &py, &pz, &pyaw, &ppitch, &proll,
|
||||
&pgrab) >= 6) {
|
||||
reply(place(key, {px, py, pz}, PanelBasis(pyaw, ppitch, proll), pgrab >= 0 ? pgrab : grabOffset));
|
||||
continue;
|
||||
}
|
||||
if (std::sscanf(buf, "measure %127s", key) == 1) {
|
||||
Panel p;
|
||||
Vec3 eye;
|
||||
const std::string err = findPanel(key, p, eye);
|
||||
char msg[400] = "";
|
||||
if (err.empty())
|
||||
std::snprintf(msg, sizeof msg, "ok %.4f %.4f %.4f %.4f %.4f %.4f %.4f %.4f %.4f %.4f %.4f %.4f %.4f %.4f",
|
||||
p.center.x, p.center.y, p.center.z, p.width, p.height, p.basis.x.x, p.basis.x.y,
|
||||
p.basis.x.z, p.basis.y.x, p.basis.y.y, p.basis.y.z, p.basis.z.x, p.basis.z.y,
|
||||
p.basis.z.z);
|
||||
reply(err.empty() ? msg : "error " + err);
|
||||
continue;
|
||||
}
|
||||
if (std::strncmp(buf, "head", 4) == 0) {
|
||||
vr::TrackedDevicePose_t h;
|
||||
sys->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0, &h, 1);
|
||||
const Vec3 e = Position(h.mDeviceToAbsoluteTracking);
|
||||
const Vec3 f = Rotate(h.mDeviceToAbsoluteTracking, {0, 0, -1});
|
||||
char msg[200];
|
||||
std::snprintf(msg, sizeof msg, "ok %.4f %.4f %.4f %.2f %.2f", e.x, e.y, e.z,
|
||||
std::atan2(-f.x, -f.z) * 180 / M_PI, std::asin(std::clamp(f.y, -1.0, 1.0)) * 180 / M_PI);
|
||||
reply(h.bPoseIsValid ? msg : "error no head pose (headset off?)");
|
||||
continue;
|
||||
}
|
||||
if (std::strncmp(buf, "debug", 5) == 0) {
|
||||
debug = !debug;
|
||||
std::printf("debug %s\n", debug ? "on" : "off");
|
||||
std::fflush(stdout);
|
||||
continue;
|
||||
}
|
||||
if (std::strncmp(buf, "btn trigger 1", 13) == 0) {
|
||||
leftHeld = true;
|
||||
dragDistance = lastDistance;
|
||||
tiltYaw = tiltPitch = 0; // a new drag starts untilted
|
||||
dropHoldUntil = {};
|
||||
} else if (std::strncmp(buf, "btn trigger 0", 13) == 0) {
|
||||
leftHeld = false;
|
||||
tilting = false;
|
||||
// Hold the drag pose (tilt, frozen distance) while SteamVR finishes the drop.
|
||||
dropHoldUntil = Clock::now() + std::chrono::milliseconds(500);
|
||||
} else if (std::strncmp(buf, "btn b 1", 7) == 0 && leftHeld) {
|
||||
tilting = tiltStart = swallowedRight = true; // right press while dragging: tilt, no right-click
|
||||
continue;
|
||||
} else if (std::strncmp(buf, "btn b 0", 7) == 0 && swallowedRight) {
|
||||
tilting = swallowedRight = false;
|
||||
continue;
|
||||
}
|
||||
if (tilting && std::sscanf(buf, "move %lf %lf", &a, &b) == 2) {
|
||||
tiltYaw += a;
|
||||
tiltPitch = std::clamp(tiltPitch + b, -80.0, 80.0);
|
||||
continue;
|
||||
}
|
||||
if (std::sscanf(buf, "move %lf %lf", &a, &b) == 2) {
|
||||
if (!anchored) recenter = true;
|
||||
yaw += a;
|
||||
while (yaw > 180) yaw -= 360;
|
||||
while (yaw < -180) yaw += 360;
|
||||
pitch = std::clamp(pitch + b, -85.0, 85.0);
|
||||
} else if (std::strncmp(buf, "recenter", 8) == 0) {
|
||||
recenter = true;
|
||||
} else if (std::strncmp(buf, "reload", 6) == 0) {
|
||||
loadConfig();
|
||||
std::printf("reloaded: free distance %.2f m, dot %.2f deg, origin %.2f\n", freeDistance, cursorDeg,
|
||||
originFraction);
|
||||
std::fflush(stdout);
|
||||
} else if (std::strncmp(buf, "show", 4) == 0) {
|
||||
if (!active) wake(Clock::now());
|
||||
} else if (std::strncmp(buf, "hide", 4) == 0) {
|
||||
active = false;
|
||||
overlay->HideOverlay(cursor);
|
||||
overlay->HideOverlay(marker);
|
||||
SendTo(out, "ft_pointer", "hide");
|
||||
} else {
|
||||
SendTo(out, "ft_pointer", buf); // btn, scroll
|
||||
}
|
||||
}
|
||||
|
||||
vr::TrackedDevicePose_t all[vr::k_unMaxTrackedDeviceCount];
|
||||
sys->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0.011f, all, vr::k_unMaxTrackedDeviceCount);
|
||||
const vr::TrackedDevicePose_t &hmd = all[0];
|
||||
vr::TrackedDevicePose_t hmdRaw;
|
||||
sys->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseRawAndUncalibrated, 0.011f, &hmdRaw, 1);
|
||||
const auto tnow = Clock::now();
|
||||
|
||||
// Claim pulse (switchlaserhand on the driver's "a" button, no click).
|
||||
if (claimPending && tnow >= claimAt) {
|
||||
SendTo(out, "ft_pointer", "btn a 1");
|
||||
claimPending = false;
|
||||
claimHeld = true;
|
||||
claimRelease = tnow + std::chrono::milliseconds(60);
|
||||
} else if (claimHeld && tnow >= claimRelease) {
|
||||
SendTo(out, "ft_pointer", "btn a 0");
|
||||
claimHeld = false;
|
||||
}
|
||||
|
||||
// Laser mode on while the pointer is awake.
|
||||
if (active != laserModeShown) {
|
||||
laserModeShown = active;
|
||||
if (active) overlay->ShowOverlay(laserMode);
|
||||
else overlay->HideOverlay(laserMode);
|
||||
if (debug) std::printf("laser mode %s\n", active ? "forced on" : "released");
|
||||
if (debug) std::fflush(stdout);
|
||||
}
|
||||
|
||||
// Didn't get the hand role (a held controller keeps it): release, back off.
|
||||
if (active && tnow - wokeAt > std::chrono::seconds(1) && ours != vr::k_unTrackedDeviceIndexInvalid &&
|
||||
sys->GetControllerRoleForTrackedDeviceIndex(ours) == vr::TrackedControllerRole_Invalid) {
|
||||
active = false;
|
||||
claimPending = claimHeld = false;
|
||||
overlay->HideOverlay(cursor);
|
||||
overlay->HideOverlay(marker);
|
||||
SendTo(out, "ft_pointer", "btn a 0");
|
||||
SendTo(out, "ft_pointer", "hide");
|
||||
noWakeUntil = tnow + std::chrono::seconds(2);
|
||||
std::printf("no hand role (a controller is in use): pointer released\n");
|
||||
std::fflush(stdout);
|
||||
}
|
||||
|
||||
// Last used wins: a real controller being moved releases the pointer.
|
||||
if (active && tnow - lastMouse > std::chrono::milliseconds(500)) {
|
||||
for (vr::TrackedDeviceIndex_t i = 1; i < vr::k_unMaxTrackedDeviceCount; ++i) {
|
||||
if (i == ours || !all[i].bPoseIsValid) continue;
|
||||
if (sys->GetTrackedDeviceClass(i) != vr::TrackedDeviceClass_Controller) continue;
|
||||
const auto &v = all[i].vVelocity.v, &w = all[i].vAngularVelocity.v;
|
||||
const double speed = std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
||||
const double spin = std::sqrt(w[0] * w[0] + w[1] * w[1] + w[2] * w[2]);
|
||||
if (speed > 0.35 || spin > 2.0) {
|
||||
active = false;
|
||||
claimPending = claimHeld = false;
|
||||
overlay->HideOverlay(cursor);
|
||||
overlay->HideOverlay(marker);
|
||||
SendTo(out, "ft_pointer", "btn a 0");
|
||||
SendTo(out, "ft_pointer", "hide");
|
||||
std::printf("controller %u moved: pointer released\n", i);
|
||||
std::fflush(stdout);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const auto &hm = hmd.mDeviceToAbsoluteTracking.m;
|
||||
const Vec3 eye{hm[0][3], hm[1][3], hm[2][3]};
|
||||
|
||||
if (recenter && hmd.bPoseIsValid) {
|
||||
anchor = eye;
|
||||
const Vec3 f{-hm[0][2], -hm[1][2], -hm[2][2]};
|
||||
yaw = std::atan2(-f.x, -f.z) * 180 / M_PI;
|
||||
pitch = std::asin(std::clamp(f.y, -1.0, 1.0)) * 180 / M_PI;
|
||||
anchored = true;
|
||||
recenter = false;
|
||||
}
|
||||
|
||||
// Slow work, once a second: overlay handles, our device index, laser width.
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now - lastSlow > std::chrono::seconds(1)) {
|
||||
lastSlow = now;
|
||||
handles.clear();
|
||||
for (const auto &key : overlays.Keys()) {
|
||||
vr::VROverlayHandle_t h;
|
||||
if (overlay->FindOverlay(key.c_str(), &h) != vr::VROverlayError_None) continue;
|
||||
handles[key] = h;
|
||||
// Scene-graph overlays are placed absolutely. Other overlays can report no
|
||||
// texture too (gamescope's app panels, placed as dashboard tabs, share theirs
|
||||
// from another process), and ComputeOverlayIntersection handles those.
|
||||
uint32_t tw = 0, th = 0;
|
||||
overlay->GetOverlayTextureSize(h, &tw, &th);
|
||||
vr::VROverlayTransformType tt = vr::VROverlayTransform_Invalid;
|
||||
overlay->GetOverlayTransformType(h, &tt);
|
||||
// ft-screens' panels (frametop.screen.N) are 0x0 and absolute too (a shared
|
||||
// texture), but they're real panels of any size.
|
||||
sceneGraph[key] = (tw == 0 || th == 0) && tt == vr::VROverlayTransform_Absolute &&
|
||||
key.rfind("frametop.screen.", 0) != 0;
|
||||
}
|
||||
ours = vr::k_unTrackedDeviceIndexInvalid;
|
||||
for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) {
|
||||
char type[64] = "";
|
||||
sys->GetStringTrackedDeviceProperty(i, vr::Prop_ControllerType_String, type, sizeof type);
|
||||
if (std::strcmp(type, "ft_pointer") == 0) ours = i;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (now - lastVisible > std::chrono::milliseconds(50) || visible.size() != handles.size()) {
|
||||
lastVisible = now;
|
||||
visible.clear();
|
||||
for (const auto &[key, h] : handles) visible[key] = overlay->IsOverlayVisible(h);
|
||||
}
|
||||
|
||||
if (active && anchored && hmd.bPoseIsValid && tilting) {
|
||||
// Rotate the device around the grab point; the grabbed panel turns with it.
|
||||
if (tiltStart) {
|
||||
pivot = lastPoint;
|
||||
tiltOrigin = lastOrigin;
|
||||
tiltBasis = AimBasis(lastAim);
|
||||
tiltStart = false; // angles carry on from any earlier tilt in this drag
|
||||
overlay->HideOverlay(cursor);
|
||||
overlay->HideOverlay(marker);
|
||||
}
|
||||
const double yr = tiltYaw * M_PI / 180, pr = tiltPitch * M_PI / 180;
|
||||
const Vec3 up{0, 1, 0}, side = tiltBasis.x;
|
||||
auto turn = [&](Vec3 v) { return RotateAbout(RotateAbout(v, side, pr), up, yr); };
|
||||
const Vec3 originStanding = pivot + turn(tiltOrigin - pivot);
|
||||
const Basis b{turn(tiltBasis.x), turn(tiltBasis.y), turn(tiltBasis.z)};
|
||||
const auto &S = hmd.mDeviceToAbsoluteTracking, &R = hmdRaw.mDeviceToAbsoluteTracking;
|
||||
auto toRaw = [&](Vec3 v) { return Rotate(R, RotateInverse(S, v)); }; // directions: raw <- standing
|
||||
const Vec3 originRaw = Position(R) + toRaw(originStanding - eye);
|
||||
double q[4];
|
||||
BasisQuat({toRaw(b.x), toRaw(b.y), toRaw(b.z)}, q);
|
||||
char msg[200];
|
||||
std::snprintf(msg, sizeof msg, "posq %.5f %.5f %.5f %.6f %.6f %.6f %.6f", originRaw.x, originRaw.y,
|
||||
originRaw.z, q[0], q[1], q[2], q[3]);
|
||||
SendTo(out, "ft_pointer", msg);
|
||||
} else if (active && anchored && hmd.bPoseIsValid) {
|
||||
const bool dragging = leftHeld || tnow < dropHoldUntil;
|
||||
if (!dragging) tiltYaw = tiltPitch = 0; // drop finished: back to plain pointing
|
||||
const Vec3 dir = Direction(yaw, pitch);
|
||||
// Nearest visible overlay along a ray (frozen while dragging).
|
||||
struct Hit {
|
||||
double along = 1e9;
|
||||
std::string key;
|
||||
bool scene = false;
|
||||
Vec3 point, normal;
|
||||
};
|
||||
auto nearest = [&](Vec3 from, Vec3 d) {
|
||||
Hit h;
|
||||
for (const auto &[key, handle] : handles) {
|
||||
if (!visible[key]) continue;
|
||||
if (sceneGraph[key]) {
|
||||
// Plane test: overlay origin and its +Z normal, within sceneRadius of the origin.
|
||||
vr::ETrackingUniverseOrigin uo;
|
||||
vr::HmdMatrix34_t t{};
|
||||
if (overlay->GetOverlayTransformAbsolute(handle, &uo, &t) != vr::VROverlayError_None) continue;
|
||||
const Vec3 center = Position(t), normal{t.m[0][2], t.m[1][2], t.m[2][2]};
|
||||
const double denom = Dot(d, normal);
|
||||
if (std::fabs(denom) < 1e-4) continue;
|
||||
const double along = Dot(center - from, normal) / denom;
|
||||
const Vec3 at = from + d * along;
|
||||
if (along > 0.05 && along < h.along && std::sqrt(Dot(at - center, at - center)) <= sceneRadius)
|
||||
h.along = along, h.key = key, h.scene = true, h.point = at, h.normal = normal;
|
||||
continue;
|
||||
}
|
||||
vr::VROverlayIntersectionParams_t params{};
|
||||
params.vSource = {float(from.x), float(from.y), float(from.z)};
|
||||
params.vDirection = {float(d.x), float(d.y), float(d.z)};
|
||||
params.eOrigin = vr::TrackingUniverseStanding;
|
||||
vr::VROverlayIntersectionResults_t r{};
|
||||
if (overlay->ComputeOverlayIntersection(handle, ¶ms, &r) && r.fDistance > 0.05f &&
|
||||
r.fDistance < h.along) {
|
||||
h.along = r.fDistance, h.key = key, h.scene = false;
|
||||
h.point = {r.vPoint.v[0], r.vPoint.v[1], r.vPoint.v[2]};
|
||||
h.normal = {r.vNormal.v[0], r.vNormal.v[1], r.vNormal.v[2]};
|
||||
}
|
||||
}
|
||||
return h;
|
||||
};
|
||||
Hit first;
|
||||
if (!dragging) first = nearest(anchor, dir);
|
||||
double best = first.along;
|
||||
std::string bestKey = first.key;
|
||||
bool bestScene = first.scene;
|
||||
Vec3 bestPoint = first.point, bestNormal = first.normal;
|
||||
bool onEdge = false;
|
||||
if (!dragging && best < 1e8 && !bestScene) {
|
||||
edgeKey = bestKey, edgePoint = bestPoint, edgeNormal = Normalize(bestNormal), edgeLast = bestPoint;
|
||||
} else if (!dragging && best >= 1e8 && !edgeKey.empty() && visible[edgeKey]) {
|
||||
// Just off a panel: stay on its plane (see "Panel edges" at the top).
|
||||
const double denom = Dot(dir, edgeNormal);
|
||||
if (std::fabs(denom) > 1e-4) {
|
||||
const double along = Dot(edgePoint - anchor, edgeNormal) / denom;
|
||||
const Vec3 at = anchor + dir * along;
|
||||
if (along > 0.05 && std::sqrt(Dot(at - edgeLast, at - edgeLast)) <= edgeReach) {
|
||||
best = along;
|
||||
bestKey = edgeKey;
|
||||
onEdge = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// While dragging: keep the press-time distance and show the non-interactive marker.
|
||||
// On a scene-graph plane or a panel's edge: the laser-catching dot goes 5 cm behind it.
|
||||
double distance = dragging ? dragDistance : (best < 1e8 ? best : freeDistance);
|
||||
Vec3 point = anchor + dir * distance;
|
||||
bool occluded = false;
|
||||
if (!dragging) {
|
||||
// The cursor lands on what you see under it: the ray above starts at the anchor,
|
||||
// not the eye, so after leaning it can pick a panel that something nearer
|
||||
// covers from where you are now (panels close together in view, at different
|
||||
// depths). Anything in front of the point on the eye's line of sight wins.
|
||||
const double toPoint = std::sqrt(Dot(point - eye, point - eye));
|
||||
const Hit front = nearest(eye, Normalize(point - eye));
|
||||
if (front.along < toPoint - 0.02) {
|
||||
occluded = true;
|
||||
bestKey = front.key, bestScene = front.scene;
|
||||
point = front.point;
|
||||
if (!front.scene) edgeKey = front.key, edgePoint = front.point, edgeNormal = Normalize(front.normal),
|
||||
edgeLast = front.point;
|
||||
distance = std::sqrt(Dot(point - anchor, point - anchor));
|
||||
best = distance;
|
||||
onEdge = false;
|
||||
}
|
||||
lastDistance = distance, lastHit = bestKey;
|
||||
}
|
||||
const bool onScene = !dragging && ((bestScene && best < 1e8) || onEdge);
|
||||
const bool onPanel = dragging || (best < 1e8 && !onScene);
|
||||
|
||||
{
|
||||
// On a panel: the non-interactive marker, pulled 5 mm toward the eye so it
|
||||
// draws on top. In free space: the interactive dot the laser lands on.
|
||||
const vr::VROverlayHandle_t show = onPanel ? marker : cursor, hide = onPanel ? cursor : marker;
|
||||
const Vec3 at = onPanel ? point + Normalize(eye - point) * 0.005 : onScene ? point + dir * 0.05 : point;
|
||||
const double dist = std::sqrt(Dot(at - eye, at - eye));
|
||||
overlay->SetOverlayWidthInMeters(show, float(2 * dist * std::tan(cursorDeg * M_PI / 360)));
|
||||
auto m = Billboard(at, eye);
|
||||
overlay->SetOverlayTransformAbsolute(show, vr::TrackingUniverseStanding, &m);
|
||||
overlay->ShowOverlay(show);
|
||||
overlay->HideOverlay(hide);
|
||||
}
|
||||
|
||||
// Controller ray: from the eye, aimed at the cursor point, converted from the
|
||||
// standing universe to raw tracking space via the HMD's pose in both.
|
||||
const Vec3 aimStanding = Normalize(point - eye);
|
||||
const auto &S = hmd.mDeviceToAbsoluteTracking, &R = hmdRaw.mDeviceToAbsoluteTracking;
|
||||
const Vec3 aim = Rotate(R, RotateInverse(S, aimStanding)); // raw <- head <- standing
|
||||
// Origin partway along the line of sight to the cursor (smaller hit dot).
|
||||
const double toPoint = std::sqrt(Dot(point - eye, point - eye));
|
||||
const double originDist = std::max(0.0, std::min(toPoint * originFraction, toPoint - originMargin));
|
||||
const Vec3 originStanding = eye + Normalize(point - eye) * originDist;
|
||||
const Vec3 eyeRaw = Position(R) + Rotate(R, RotateInverse(S, originStanding - eye));
|
||||
lastPoint = point, lastOrigin = originStanding, lastAim = aimStanding; // tilt starts from here
|
||||
if (debug && tnow - lastDebug > std::chrono::milliseconds(500)) {
|
||||
lastDebug = tnow;
|
||||
if (systemPointer == vr::k_ulOverlayHandleInvalid) overlay->FindOverlay("system.pointer", &systemPointer);
|
||||
std::printf("dbg %s hit=%s dist=%.2f eye->point=%.2f origin=%.2f yaw=%.1f pitch=%.1f steamvr_dot=%d primary=%u\n",
|
||||
dragging ? "DRAG" : occluded ? "INFRONT" : onEdge ? "EDGE" : onScene ? "SCENE" : (best < 1e8 ? "PANEL" : "FREE"), lastHit.empty() ? "-" : lastHit.c_str(),
|
||||
distance, toPoint, originDist, yaw, pitch,
|
||||
systemPointer != vr::k_ulOverlayHandleInvalid && overlay->IsOverlayVisible(systemPointer),
|
||||
overlay->GetPrimaryDashboardDevice());
|
||||
std::fflush(stdout);
|
||||
}
|
||||
const double ayaw = std::atan2(-aim.x, -aim.z) * 180 / M_PI;
|
||||
const double apitch = std::asin(std::clamp(aim.y, -1.0, 1.0)) * 180 / M_PI;
|
||||
if (dragging && (tiltYaw != 0 || tiltPitch != 0)) {
|
||||
// Keep this drag's tilt applied, about the current cursor point.
|
||||
const double yr = tiltYaw * M_PI / 180, pr = tiltPitch * M_PI / 180;
|
||||
const Basis base = AimBasis(aimStanding);
|
||||
const Vec3 up{0, 1, 0}, side = base.x;
|
||||
auto turn = [&](Vec3 v) { return RotateAbout(RotateAbout(v, side, pr), up, yr); };
|
||||
const Vec3 o = point + turn(originStanding - point);
|
||||
const Basis b{turn(base.x), turn(base.y), turn(base.z)};
|
||||
auto toRaw = [&](Vec3 v) { return Rotate(R, RotateInverse(S, v)); };
|
||||
const Vec3 oRaw = Position(R) + toRaw(o - eye);
|
||||
double q[4];
|
||||
BasisQuat({toRaw(b.x), toRaw(b.y), toRaw(b.z)}, q);
|
||||
char msg[200];
|
||||
std::snprintf(msg, sizeof msg, "posq %.5f %.5f %.5f %.6f %.6f %.6f %.6f", oRaw.x, oRaw.y, oRaw.z, q[0],
|
||||
q[1], q[2], q[3]);
|
||||
SendTo(out, "ft_pointer", msg);
|
||||
} else {
|
||||
char msg[160];
|
||||
std::snprintf(msg, sizeof msg, "pose %.5f %.5f %.5f %.4f %.4f", eyeRaw.x, eyeRaw.y, eyeRaw.z, ayaw, apitch);
|
||||
SendTo(out, "ft_pointer", msg);
|
||||
}
|
||||
}
|
||||
|
||||
vr::VREvent_t ev;
|
||||
while (sys->PollNextEvent(&ev, sizeof ev)) {
|
||||
if (ev.eventType == vr::VREvent_Quit) {
|
||||
sys->AcknowledgeQuit_Exiting();
|
||||
|
||||
overlays.Stop();
|
||||
vr::VR_Shutdown();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(8));
|
||||
}
|
||||
}
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start, stop, or inspect the ft-pointer helper on the Frame (runs in the dev container).
|
||||
# Usage: pointer/helper/run.sh install|uninstall # user service, starts with SteamVR
|
||||
# pointer/helper/run.sh start|stop|restart|status|log [lines]
|
||||
# With the service installed, start/stop/restart/log go through systemd.
|
||||
set -euo pipefail
|
||||
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
. "$root/scripts/_env.sh"
|
||||
frame="$root/scripts/frame.sh"
|
||||
unit=frametop-pointer.service
|
||||
installed() { "$frame" --host "test -f ~/.config/systemd/user/$unit" 2>/dev/null; }
|
||||
case ${1:-status} in
|
||||
install)
|
||||
"$root/scripts/sync.sh" >/dev/null
|
||||
fill_template "$root/pointer/helper/$unit" | on_frame "mkdir -p ~/.config/systemd/user && cat > ~/.config/systemd/user/$unit"
|
||||
"$frame" --host "set -e; pkill -x ft-pointer || true
|
||||
systemctl --user daemon-reload; systemctl --user enable --now $unit; sleep 3
|
||||
systemctl --user is-active $unit; tail -n 3 /dev/null; journalctl --user -u $unit --no-pager -o cat -n 3" ;;
|
||||
uninstall) "$frame" --host "systemctl --user disable --now $unit 2>/dev/null; rm -f ~/.config/systemd/user/$unit; systemctl --user daemon-reload; echo removed" ;;
|
||||
start|stop|restart) if installed; then "$frame" --host "systemctl --user $1 $unit; systemctl --user is-active $unit"; exit; fi ;;&
|
||||
log) if installed; then "$frame" --host "journalctl --user -u $unit --no-pager -o cat -n ${2:-30}"; exit; fi ;;&
|
||||
start) "$frame" -C pointer/helper 'pgrep -x ft-pointer >/dev/null && { echo "already running"; exit 0; }
|
||||
nohup ./build/ft-pointer > /tmp/ft-pointer.log 2>&1 &
|
||||
sleep 2; pgrep -ax ft-pointer; cat /tmp/ft-pointer.log' ;;
|
||||
stop) "$frame" --host 'pkill -x ft-pointer && echo stopped || echo "not running"' ;;
|
||||
restart) "$0" stop; sleep 1; exec "$0" start ;;
|
||||
status) "$frame" --host 'pgrep -ax ft-pointer || echo "not running"' ;;
|
||||
log) "$frame" --host "tail -n ${2:-30} /tmp/ft-pointer.log" ;;
|
||||
*) echo "usage: $0 start|stop|restart|status|log" >&2; exit 2 ;;
|
||||
esac
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build vrprobe in the dev container on the Frame (pointer/probe/build/vrprobe).
|
||||
set -euo pipefail
|
||||
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
"$root/scripts/sync.sh" >/dev/null
|
||||
exec "$root/scripts/frame.sh" -C pointer/probe 'set -e; mkdir -p build
|
||||
g++ -std=c++17 -O2 -Wall -I/opt/steamvr/tools/hellovr_vulkan_linux/src/openvr/headers -I../common \
|
||||
-o build/vrprobe vrprobe.cpp -L/opt/steamvr/bin/linuxarm64 -lopenvr_api -Wl,-rpath,/opt/steamvr/bin/linuxarm64
|
||||
echo "built build/vrprobe"'
|
||||
@@ -0,0 +1,166 @@
|
||||
// Print SteamVR's view of tracked devices: class, hand role, controller type,
|
||||
// connection, pose validity, and the dashboard's primary pointer device.
|
||||
// Runs as a background OpenVR client (in the dev container).
|
||||
#include <openvr.h>
|
||||
|
||||
#include "vrmath.h"
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Measure a floating panel (see md::ScanPanel).
|
||||
static int Scan(vr::IVRSystem *sys, const char *key, double step) {
|
||||
vr::VROverlayHandle_t h;
|
||||
if (vr::VROverlay()->FindOverlay(key, &h) != vr::VROverlayError_None) {
|
||||
std::printf("no overlay %s\n", key);
|
||||
return 1;
|
||||
}
|
||||
vr::TrackedDevicePose_t head;
|
||||
sys->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0, &head, 1);
|
||||
const md::Vec3 eye = md::Position(head.mDeviceToAbsoluteTracking);
|
||||
const md::Panel p = md::ScanPanel(h, eye, step);
|
||||
std::printf("scan %s: %d hits, head (%.3f %.3f %.3f)\n", key, p.hits, eye.x, eye.y, eye.z);
|
||||
if (!p.found) return 1;
|
||||
const md::Vec3 c = p.center, x = p.basis.x, y = p.basis.y, z = p.basis.z, to = c - eye;
|
||||
std::printf("center (%.3f %.3f %.3f) width %.3f height %.3f distance %.3f\n", c.x, c.y, c.z, p.width, p.height,
|
||||
md::Length(to));
|
||||
std::printf("x (%.3f %.3f %.3f) y (%.3f %.3f %.3f) front (%.3f %.3f %.3f)\n", x.x, x.y, x.z, y.x, y.y, y.z, z.x,
|
||||
z.y, z.z);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Usage: vrprobe [overlay-key...] (extra overlays to check besides the dashboard's)
|
||||
// vrprobe --scan <overlay-key> [step-degrees]
|
||||
int main(int argc, char **argv) {
|
||||
vr::EVRInitError err = vr::VRInitError_None;
|
||||
vr::IVRSystem *sys = vr::VR_Init(&err, vr::VRApplication_Background);
|
||||
if (err != vr::VRInitError_None) {
|
||||
std::printf("VR_Init failed: %s\n", vr::VR_GetVRInitErrorAsEnglishDescription(err));
|
||||
return 1;
|
||||
}
|
||||
if (argc >= 3 && std::strcmp(argv[1], "--scan") == 0) {
|
||||
const int r = Scan(sys, argv[2], argc >= 4 ? std::atof(argv[3]) : 1.0);
|
||||
vr::VR_Shutdown();
|
||||
return r;
|
||||
}
|
||||
static const char *classes[] = {"invalid", "HMD", "controller", "tracker", "reference", "display"};
|
||||
static const char *roles[] = {"none", "left", "right", "optout", "treadmill", "stylus"};
|
||||
vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount];
|
||||
sys->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0, poses, vr::k_unMaxTrackedDeviceCount);
|
||||
for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) {
|
||||
const auto cls = sys->GetTrackedDeviceClass(i);
|
||||
if (cls == vr::TrackedDeviceClass_Invalid) continue;
|
||||
char type[64] = "", serial[64] = "";
|
||||
sys->GetStringTrackedDeviceProperty(i, vr::Prop_ControllerType_String, type, sizeof type);
|
||||
sys->GetStringTrackedDeviceProperty(i, vr::Prop_SerialNumber_String, serial, sizeof serial);
|
||||
const auto role = sys->GetControllerRoleForTrackedDeviceIndex(i);
|
||||
const int hint = sys->GetInt32TrackedDeviceProperty(i, vr::Prop_ControllerRoleHint_Int32);
|
||||
std::printf("%u %-10s role=%-6s hint=%d type=%-16s connected=%d pose=%d %s\n", i,
|
||||
cls < 6 ? classes[cls] : "?", role < 6 ? roles[role] : "?", hint, type,
|
||||
sys->IsTrackedDeviceConnected(i), poses[i].bPoseIsValid, serial);
|
||||
}
|
||||
std::printf("left hand = %u, right hand = %u\n",
|
||||
sys->GetTrackedDeviceIndexForControllerRole(vr::TrackedControllerRole_LeftHand),
|
||||
sys->GetTrackedDeviceIndexForControllerRole(vr::TrackedControllerRole_RightHand));
|
||||
if (poses[0].bPoseIsValid) {
|
||||
// Head forward is -Z of the HMD pose. yaw 0 = -Z, positive yaw turns left (about +Y).
|
||||
const auto &m = poses[0].mDeviceToAbsoluteTracking.m;
|
||||
const float fx = -m[0][2], fy = -m[1][2], fz = -m[2][2];
|
||||
std::printf("head yaw = %.1f pitch = %.1f\n", std::atan2(-fx, -fz) * 180.0 / M_PI,
|
||||
std::asin(fy) * 180.0 / M_PI);
|
||||
}
|
||||
// Pointer calibration: our device's forward ray vs where SteamVR's hit dot actually is.
|
||||
for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) {
|
||||
char type[64] = "";
|
||||
sys->GetStringTrackedDeviceProperty(i, vr::Prop_ControllerType_String, type, sizeof type);
|
||||
if (std::strcmp(type, "ft_pointer") != 0 || !poses[i].bPoseIsValid) continue;
|
||||
const auto &m = poses[i].mDeviceToAbsoluteTracking.m;
|
||||
const double o[3] = {m[0][3], m[1][3], m[2][3]}, f[3] = {-m[0][2], -m[1][2], -m[2][2]};
|
||||
std::printf("ft_pointer origin (%.3f %.3f %.3f) forward (%.3f %.3f %.3f) yaw %.1f pitch %.1f\n", o[0], o[1], o[2],
|
||||
f[0], f[1], f[2], std::atan2(-f[0], -f[2]) * 180 / M_PI, std::asin(f[1]) * 180 / M_PI);
|
||||
for (const char *key : {"system.pointer", "system.pointer.secondary", "frametop.pointer.cursor"}) {
|
||||
vr::VROverlayHandle_t h;
|
||||
if (vr::VROverlay()->FindOverlay(key, &h) != vr::VROverlayError_None) continue;
|
||||
vr::ETrackingUniverseOrigin origin;
|
||||
vr::HmdMatrix34_t t{};
|
||||
const bool visible = vr::VROverlay()->IsOverlayVisible(h);
|
||||
vr::VROverlayTransformType type2;
|
||||
vr::VROverlay()->GetOverlayTransformType(h, &type2);
|
||||
if (vr::VROverlay()->GetOverlayTransformAbsolute(h, &origin, &t) != vr::VROverlayError_None) {
|
||||
std::printf(" %-28s visible=%d transform type %d (not absolute)\n", key, visible, int(type2));
|
||||
continue;
|
||||
}
|
||||
const double p2[3] = {t.m[0][3] - o[0], t.m[1][3] - o[1], t.m[2][3] - o[2]};
|
||||
const double d = std::sqrt(p2[0] * p2[0] + p2[1] * p2[1] + p2[2] * p2[2]);
|
||||
const double dir[3] = {p2[0] / d, p2[1] / d, p2[2] / d};
|
||||
float w = 0;
|
||||
vr::VROverlay()->GetOverlayWidthInMeters(h, &w);
|
||||
std::printf(" %-28s visible=%d at (%.3f %.3f %.3f) dist %.2f yaw %.1f pitch %.1f width %.3f\n", key, visible,
|
||||
t.m[0][3], t.m[1][3], t.m[2][3], d, std::atan2(-dir[0], -dir[2]) * 180 / M_PI,
|
||||
std::asin(dir[1]) * 180 / M_PI, w);
|
||||
}
|
||||
}
|
||||
// Overlay check: transform type, size, and a ray test along ft_pointer's laser.
|
||||
std::vector<std::string> keys = {"valve.steam.gamepadui.floatingfooter", "valve.steam.gamepadui.bar", "system.systemui"};
|
||||
keys.insert(keys.end(), argv + 1, argv + argc);
|
||||
for (const auto &k : keys) {
|
||||
const char *key = k.c_str();
|
||||
vr::VROverlayHandle_t h;
|
||||
if (vr::VROverlay()->FindOverlay(key, &h) != vr::VROverlayError_None) continue;
|
||||
vr::VROverlayTransformType tt;
|
||||
vr::VROverlay()->GetOverlayTransformType(h, &tt);
|
||||
float w = 0;
|
||||
vr::VROverlay()->GetOverlayWidthInMeters(h, &w);
|
||||
uint32_t tw = 0, th = 0;
|
||||
vr::VROverlay()->GetOverlayTextureSize(h, &tw, &th);
|
||||
std::printf("overlay %-44s visible=%d type=%d width=%.3fm tex=%ux%u", key, vr::VROverlay()->IsOverlayVisible(h),
|
||||
int(tt), w, tw, th);
|
||||
vr::ETrackingUniverseOrigin uo;
|
||||
vr::HmdMatrix34_t t{};
|
||||
if (tt == vr::VROverlayTransform_Absolute && vr::VROverlay()->GetOverlayTransformAbsolute(h, &uo, &t) == vr::VROverlayError_None)
|
||||
std::printf(" at (%.2f %.2f %.2f) normal (%.2f %.2f %.2f)", t.m[0][3], t.m[1][3], t.m[2][3], t.m[0][2],
|
||||
t.m[1][2], t.m[2][2]);
|
||||
else
|
||||
std::printf(" transform type %d", int(tt));
|
||||
vr::HmdVector2_t mouse{};
|
||||
vr::VROverlay()->GetOverlayMouseScale(h, &mouse);
|
||||
vr::VROverlayInputMethod im = vr::VROverlayInputMethod_None;
|
||||
vr::VROverlay()->GetOverlayInputMethod(h, &im);
|
||||
uint32_t flags = 0;
|
||||
vr::VROverlay()->GetOverlayFlags(h, &flags);
|
||||
std::printf(" mouse=%.0fx%.0f input=%d flags=0x%x", mouse.v[0], mouse.v[1], int(im), flags);
|
||||
if (poses[0].bPoseIsValid) { // gaze ray: from the head, straight ahead
|
||||
const auto &m = poses[0].mDeviceToAbsoluteTracking.m;
|
||||
vr::VROverlayIntersectionParams_t params{};
|
||||
params.vSource = {m[0][3], m[1][3], m[2][3]};
|
||||
params.vDirection = {-m[0][2], -m[1][2], -m[2][2]};
|
||||
params.eOrigin = vr::TrackingUniverseStanding;
|
||||
vr::VROverlayIntersectionResults_t hit{};
|
||||
const bool ok = vr::VROverlay()->ComputeOverlayIntersection(h, ¶ms, &hit);
|
||||
std::printf(" | gaze hit=%d dist=%.2f uv=(%.2f %.2f) at (%.2f %.2f %.2f)", ok, hit.fDistance, hit.vUVs.v[0],
|
||||
hit.vUVs.v[1], hit.vPoint.v[0], hit.vPoint.v[1], hit.vPoint.v[2]);
|
||||
}
|
||||
for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) {
|
||||
char type[64] = "";
|
||||
sys->GetStringTrackedDeviceProperty(i, vr::Prop_ControllerType_String, type, sizeof type);
|
||||
if (std::strcmp(type, "ft_pointer") != 0 || !poses[i].bPoseIsValid) continue;
|
||||
const auto &m = poses[i].mDeviceToAbsoluteTracking.m;
|
||||
vr::VROverlayIntersectionParams_t params{};
|
||||
params.vSource = {m[0][3], m[1][3], m[2][3]};
|
||||
params.vDirection = {-m[0][2], -m[1][2], -m[2][2]};
|
||||
params.eOrigin = vr::TrackingUniverseStanding;
|
||||
vr::VROverlayIntersectionResults_t hit{};
|
||||
const bool ok = vr::VROverlay()->ComputeOverlayIntersection(h, ¶ms, &hit);
|
||||
std::printf(" | laser hit=%d dist=%.2f uv=(%.2f %.2f)", ok, hit.fDistance, hit.vUVs.v[0], hit.vUVs.v[1]);
|
||||
}
|
||||
std::printf("\n");
|
||||
}
|
||||
std::printf("primary dashboard device = %u, dashboard visible = %d\n",
|
||||
vr::VROverlay()->GetPrimaryDashboardDevice(), vr::VROverlay()->IsDashboardVisible());
|
||||
vr::VR_Shutdown();
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user