mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 04:04:09 +02:00
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>
153 lines
7.2 KiB
C++
153 lines
7.2 KiB
C++
// 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
|