mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 02:00:06 +02:00
Add gaze mode to the pointer: it goes where you look
MAGIC pointing (Zhai et al. 1999), off by default: with POINTER_GAZE=1, "gaze on", or a button mapped to the new gaze_toggle action, the cursor ray is the corrected gaze from ft-gazed while the gaze has the pointer. Moving the mouse takes the pointer from where the gaze left it; looking more than POINTER_GAZE_RETAKE (5 deg) away with the mouse still gives it back. The pointer is aimed at the gaze, never steered toward it, and only fresh gaze moves it, so a stopped service or a blink leaves it where it is. A mouse nudge of up to POINTER_GAZE_NUDGE_MAX (8 deg) before a click is sent to ft-gazed as a lesson in the eye tracker's error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f1c385aa59
commit
10f21273a6
6 files changed
+120
-9
No files matched your search
@@ -82,6 +82,8 @@ A few overlays need special handling:
|
|||||||
|
|
||||||
Head follow is experimental and off by default. It works, but it's only lightly tested, and the feel is mostly a matter of its settings; polishing it is left open. With it on (`POINTER_FOLLOW=1`, or a mouse button mapped to Head follow on/off), the cursor rides on a reference direction, where you were facing when your head last settled, and keeps its offset from it. The mouse can put the cursor anywhere up to `POINTER_FOLLOW_REACH` (70 degrees) from the reference, a corner of your view included. While your head stays within `POINTER_LEASH_DEG` of the reference, nothing moves on its own. Once your head has been past the leash for `POINTER_LEASH_DELAY` (0.2 s, so a glance out and back doesn't count), the reference eases to where you're facing (time constant `POINTER_LEASH_RETURN`, 0.2 s), never falling further behind than the leash, and the cursor ends up back where it was in your view. Then it waits for the leash again. Two earlier versions didn't work out. Moving the reference only while your head pulled at the end of the leash left it up to the leash off after you turned back, and getting it centred again meant overshooting with your head. Easing it toward your facing all the time moved the cursor on every small head movement. A leash of 0 makes the reference your facing direction, so the cursor is locked to your view, and mouse movement shifts it within the view. Head roll is ignored, so tilting your head doesn't swing the cursor around. While the left button is held the cursor stays put in the room, so your head can't nudge a click or a drag. When you let go, it carries on from where it is instead of jumping.
|
Head follow is experimental and off by default. It works, but it's only lightly tested, and the feel is mostly a matter of its settings; polishing it is left open. With it on (`POINTER_FOLLOW=1`, or a mouse button mapped to Head follow on/off), the cursor rides on a reference direction, where you were facing when your head last settled, and keeps its offset from it. The mouse can put the cursor anywhere up to `POINTER_FOLLOW_REACH` (70 degrees) from the reference, a corner of your view included. While your head stays within `POINTER_LEASH_DEG` of the reference, nothing moves on its own. Once your head has been past the leash for `POINTER_LEASH_DELAY` (0.2 s, so a glance out and back doesn't count), the reference eases to where you're facing (time constant `POINTER_LEASH_RETURN`, 0.2 s), never falling further behind than the leash, and the cursor ends up back where it was in your view. Then it waits for the leash again. Two earlier versions didn't work out. Moving the reference only while your head pulled at the end of the leash left it up to the leash off after you turned back, and getting it centred again meant overshooting with your head. Easing it toward your facing all the time moved the cursor on every small head movement. A leash of 0 makes the reference your facing direction, so the cursor is locked to your view, and mouse movement shifts it within the view. Head roll is ignored, so tilting your head doesn't swing the cursor around. While the left button is held the cursor stays put in the room, so your head can't nudge a click or a drag. When you let go, it carries on from where it is instead of jumping.
|
||||||
|
|
||||||
|
Gaze mode is experimental and off by default (`POINTER_GAZE=1`, `gaze/ft-gazectl on`, or a mouse button mapped to Gaze pointer on/off). It's MAGIC pointing (Zhai, Morimoto and Ihde, 1999): the pointer goes where you look, and the mouse does the last bit. The gaze service (`gaze/ft-gazed`) sends the helper the corrected gaze at 90 Hz, and while the gaze has the pointer, the cursor ray is that gaze from the eye. The pointer is aimed at the gaze each frame, not steered toward it, so nothing can pile up. An earlier try in the gaze probe steered the pointer with relative moves, and lost it when the pointer went idle or a controller had the laser. Moving the mouse takes the pointer from the gaze. Looking more than `POINTER_GAZE_RETAKE` (5 degrees) away from it, with the mouse still, gives it back, so small eye movements around the pointer don't pull it off what you're doing. A mouse nudge of up to `POINTER_GAZE_NUDGE_MAX` (8 degrees) before a click is sent to the gaze service as a lesson: you were looking at where you clicked when the mouse took over, so the nudge is the eye tracker's error there. Using it is what calibrates it. See `gaze/README.md` for the service, the calibration, and what was measured.
|
||||||
|
|
||||||
Replacing a loaded driver's files, as re-running the installer used to do, leaves SteamVR honoring the virtual controller's hand role but not its laser claim: the dashboard pointer stays unassigned until SteamVR restarts. The driver installer now leaves an unchanged driver in place.
|
Replacing a loaded driver's files, as re-running the installer used to do, leaves SteamVR honoring the virtual controller's hand role but not its laser claim: the dashboard pointer stays unassigned until SteamVR restarts. The driver installer now leaves an unchanged driver in place.
|
||||||
|
|
||||||
`dashboard.laserRayWidthScale` controls the beam's width, but SteamVR only applies a change from its own settings screen or at restart, so it can't be switched per device while running.
|
`dashboard.laserRayWidthScale` controls the beam's width, but SteamVR only applies a change from its own settings screen or at restart, so it can't be switched per device while running.
|
||||||
|
|||||||
+2
-2
@@ -98,14 +98,14 @@ pointer/helper/run.sh status | log | restart
|
|||||||
pointer/driver/install.sh probe # devices, hand roles, who owns the dashboard pointer
|
pointer/driver/install.sh probe # devices, hand roles, who owns the dashboard pointer
|
||||||
```
|
```
|
||||||
|
|
||||||
The pointer settings are in `~/.config/frametop.conf`: `POINTER_SENSITIVITY`, `POINTER_IDLE`, `POINTER_WAKE_COUNTS`, `POINTER_DISTANCE`, `POINTER_CURSOR_DEG`, `POINTER_ORIGIN_FRACTION`, `POINTER_ORIGIN_MARGIN`, `POINTER_SCENE_RADIUS`, `POINTER_EDGE_REACH`, `POINTER_LASER_WIDTH`, and the head follow settings `POINTER_FOLLOW`, `POINTER_LEASH_DEG`, `POINTER_LEASH_DELAY`, `POINTER_LEASH_RETURN`, and `POINTER_FOLLOW_REACH`. The example config explains each. Frametop Input Settings changes them live; after editing the file by hand, restart the relay or the helper.
|
The pointer settings are in `~/.config/frametop.conf`: `POINTER_SENSITIVITY`, `POINTER_IDLE`, `POINTER_WAKE_COUNTS`, `POINTER_DISTANCE`, `POINTER_CURSOR_DEG`, `POINTER_ORIGIN_FRACTION`, `POINTER_ORIGIN_MARGIN`, `POINTER_SCENE_RADIUS`, `POINTER_EDGE_REACH`, `POINTER_LASER_WIDTH`, the head follow settings `POINTER_FOLLOW`, `POINTER_LEASH_DEG`, `POINTER_LEASH_DELAY`, `POINTER_LEASH_RETURN`, and `POINTER_FOLLOW_REACH`, and the gaze mode settings `POINTER_GAZE`, `POINTER_GAZE_RETAKE`, and `POINTER_GAZE_NUDGE_MAX`. The example config explains each. Frametop Input Settings changes them live; after editing the file by hand, restart the relay or the helper.
|
||||||
|
|
||||||
## Frametop Input Settings
|
## Frametop Input Settings
|
||||||
|
|
||||||
A Kirigami app with a Python backend, in the Plasma menu under Settings. It runs in the `dev` container and talks to the relay over its control socket, `@frametop_relay`. It has four pages:
|
A Kirigami app with a Python backend, in the Plasma menu under Settings. It runs in the `dev` container and talks to the relay over its control socket, `@frametop_relay`. It has four pages:
|
||||||
|
|
||||||
- Devices lists every USB and Bluetooth mouse and keyboard, with a light that flashes when the device is used. Each device gets a role: 3D pointer (grabbed, drives the pointer; the default for anything with a mouse), Pass through (grabbed only while typing goes to the desktop; the default for keyboards, where a Meta tap toggles the dashboard if `META_DASHBOARD=1` is in `~/.config/frametop.conf`), or Ignore. A device is identified by its Bluetooth address, or its USB ids and name, so all of its input nodes share one role. Forget drops everything saved for a device.
|
- Devices lists every USB and Bluetooth mouse and keyboard, with a light that flashes when the device is used. Each device gets a role: 3D pointer (grabbed, drives the pointer; the default for anything with a mouse), Pass through (grabbed only while typing goes to the desktop; the default for keyboards, where a Meta tap toggles the dashboard if `META_DASHBOARD=1` is in `~/.config/frametop.conf`), or Ignore. A device is identified by its Bluetooth address, or its USB ids and name, so all of its input nodes share one role. Forget drops everything saved for a device.
|
||||||
- Buttons maps a pointer device's buttons. Choose Capture a button, press the button or key, then pick an action: a click, back, scroll, toggle dashboard, recenter, pointer on or off, head follow on or off, faster or slower, pass the key through, or nothing. Devices with saved mappings are listed even while they're asleep.
|
- Buttons maps a pointer device's buttons. Choose Capture a button, press the button or key, then pick an action: a click, back, scroll, toggle dashboard, recenter, pointer on or off, head follow on or off, gaze pointer on or off, faster or slower, pass the key through, or nothing. Devices with saved mappings are listed even while they're asleep.
|
||||||
- Pointer has a Head follow switch and sliders for the pointer settings, which apply immediately, and a Recenter button.
|
- Pointer has a Head follow switch and sliders for the pointer settings, which apply immediately, and a Recenter button.
|
||||||
- Bluetooth lists paired devices and has Apply Bluetooth fixes, which runs `/etc/steamframe/bt-fixups.sh` through `pkexec`. Pair new devices in Steam.
|
- Bluetooth lists paired devices and has Apply Bluetooth fixes, which runs `/etc/steamframe/bt-fixups.sh` through `pkexec`. Pair new devices in Steam.
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ ACTION_LABELS = {
|
|||||||
"left": "Left click", "right": "Right click", "middle": "Middle click", "back": "Back",
|
"left": "Left click", "right": "Right click", "middle": "Middle click", "back": "Back",
|
||||||
"scroll_up": "Scroll up", "scroll_down": "Scroll down", "dashboard": "Toggle SteamVR dashboard",
|
"scroll_up": "Scroll up", "scroll_down": "Scroll down", "dashboard": "Toggle SteamVR dashboard",
|
||||||
"recenter": "Recenter pointer", "pointer_toggle": "Pointer on/off",
|
"recenter": "Recenter pointer", "pointer_toggle": "Pointer on/off",
|
||||||
"follow_toggle": "Head follow on/off (experimental)", "sens_up": "Faster pointer",
|
"follow_toggle": "Head follow on/off (experimental)", "gaze_toggle": "Gaze pointer on/off (experimental)",
|
||||||
|
"sens_up": "Faster pointer",
|
||||||
"sens_down": "Slower pointer", "layout_reset": "Reset desktop screen layout",
|
"sens_down": "Slower pointer", "layout_reset": "Reset desktop screen layout",
|
||||||
"screens_toggle": "Hide/show desktop screens", "key": "Pass through as key",
|
"screens_toggle": "Hide/show desktop screens", "key": "Pass through as key",
|
||||||
"none": "Do nothing",
|
"none": "Do nothing",
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ keyboard node for its extra buttons). Roles, from ~/.config/frametop-input.json
|
|||||||
ignore not grabbed, only observed for identification in the settings app
|
ignore not grabbed, only observed for identification in the settings app
|
||||||
Buttons and keys of pointer devices go through a per-device map to actions
|
Buttons and keys of pointer devices go through a per-device map to actions
|
||||||
(left, right, middle, back, scroll_up, scroll_down, dashboard, recenter,
|
(left, right, middle, back, scroll_up, scroll_down, dashboard, recenter,
|
||||||
pointer_toggle, follow_toggle = head follow on or off, sens_up, sens_down,
|
pointer_toggle, follow_toggle = head follow on or off, gaze_toggle = gaze mode on or off
|
||||||
|
(the pointer goes where you look; see pointer/helper/ft-pointer.cpp), sens_up, sens_down,
|
||||||
layout_reset = put the desktop screens back in their saved layout, screens_toggle = hide or show the desktop screens, key = pass
|
layout_reset = put the desktop screens back in their saved layout, screens_toggle = hide or show the desktop screens, key = pass
|
||||||
through as a key, none).
|
through as a key, none).
|
||||||
|
|
||||||
@@ -140,7 +141,8 @@ def eviocguniq(length):
|
|||||||
VIRTUAL_PREFIX = "frametop virtual"
|
VIRTUAL_PREFIX = "frametop virtual"
|
||||||
RULES_PATH = os.path.expanduser("~/.config/frametop-input.json")
|
RULES_PATH = os.path.expanduser("~/.config/frametop-input.json")
|
||||||
ACTIONS = ("left", "right", "middle", "back", "scroll_up", "scroll_down", "dashboard", "recenter",
|
ACTIONS = ("left", "right", "middle", "back", "scroll_up", "scroll_down", "dashboard", "recenter",
|
||||||
"pointer_toggle", "follow_toggle", "sens_up", "sens_down", "layout_reset", "screens_toggle", "key", "none")
|
"pointer_toggle", "follow_toggle", "gaze_toggle", "sens_up", "sens_down", "layout_reset", "screens_toggle",
|
||||||
|
"key", "none")
|
||||||
SCREENS = "\0ft_screens"
|
SCREENS = "\0ft_screens"
|
||||||
KEYS = "\0frametop_keys" # keys of keyboards grabbed for the desktop, for other readers
|
KEYS = "\0frametop_keys" # keys of keyboards grabbed for the desktop, for other readers
|
||||||
FT_LAYOUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "layout", "ft-layout")
|
FT_LAYOUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "layout", "ft-layout")
|
||||||
@@ -409,6 +411,9 @@ class Pointer:
|
|||||||
elif name == "follow_toggle":
|
elif name == "follow_toggle":
|
||||||
self.send("follow toggle") # until the next restart; the setting is POINTER_FOLLOW
|
self.send("follow toggle") # until the next restart; the setting is POINTER_FOLLOW
|
||||||
log("head follow toggled")
|
log("head follow toggled")
|
||||||
|
elif name == "gaze_toggle":
|
||||||
|
self.send("gaze toggle") # until the next restart; the setting is POINTER_GAZE
|
||||||
|
log("gaze mode toggled")
|
||||||
elif name == "screens_toggle":
|
elif name == "screens_toggle":
|
||||||
try:
|
try:
|
||||||
self.sock.sendto(b"toggle", SCREENS)
|
self.sock.sendto(b"toggle", SCREENS)
|
||||||
|
|||||||
@@ -106,6 +106,24 @@
|
|||||||
// cursor stays put in the room, so a click or a drag can't be nudged by the head; the offset
|
// cursor stays put in the room, so a click or a drag can't be nudged by the head; the offset
|
||||||
// is taken up from where the cursor is when the hold ends, so it doesn't jump.
|
// is taken up from where the cursor is when the hold ends, so it doesn't jump.
|
||||||
//
|
//
|
||||||
|
// Gaze mode (experimental, off by default; POINTER_GAZE=1, "gaze on|off|toggle", or the
|
||||||
|
// relay's gaze_toggle): the pointer goes where you look, and the mouse does the last bit
|
||||||
|
// (MAGIC pointing: Zhai, Morimoto and Ihde, CHI 1999). The gaze service (gaze/ft-gazed)
|
||||||
|
// sends the corrected gaze 90 times a second, "gz <yaw> <pitch> <raw yaw> <raw pitch>"
|
||||||
|
// (head-relative degrees), and while the gaze has the pointer, the cursor ray is simply
|
||||||
|
// that gaze from the eye: nothing is steered, so nothing can pile up. Moving the mouse takes
|
||||||
|
// the pointer from the gaze, and it moves from where the gaze left it, as usual. Looking
|
||||||
|
// well away from it (more than POINTER_GAZE_RETAKE, 5 deg, for 120 ms, with the mouse still
|
||||||
|
// for 300 ms) gives it back to the gaze; small eye movements around the pointer don't. A
|
||||||
|
// click or a drag holds the pointer where it is, like any other press.
|
||||||
|
// When the mouse took the pointer and you then click, the nudge was probably onto what
|
||||||
|
// you were looking at: from the raw gaze when the mouse took over to where you clicked is
|
||||||
|
// the tracker's error there. The helper sends it to ft-gazed as a lesson ("lesson <raw yaw>
|
||||||
|
// <raw pitch> <true yaw> <true pitch>", the true direction relative to the head as it was
|
||||||
|
// when the mouse took over) if the mouse moved between 0.2 deg and POINTER_GAZE_NUDGE_MAX
|
||||||
|
// (8 deg) and the click came within 10 s; more is using the mouse, not a nudge. With no
|
||||||
|
// fresh gaze (a blink, the service stopped, the headset off), the pointer stays put.
|
||||||
|
//
|
||||||
// Placement (for layout): SteamVR keeps a floating panel's position inside the
|
// 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
|
// 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
|
// would. It measures the panel (md::ScanPanel), aims the device at its grab bar
|
||||||
@@ -121,6 +139,7 @@
|
|||||||
//
|
//
|
||||||
// Commands (datagrams on @ft_pointer_helper): show, hide, recenter, move <dyaw> <dpitch>,
|
// Commands (datagrams on @ft_pointer_helper): show, hide, recenter, move <dyaw> <dpitch>,
|
||||||
// follow on|off|toggle (head follow, until the next restart or a change to POINTER_FOLLOW),
|
// follow on|off|toggle (head follow, until the next restart or a change to POINTER_FOLLOW),
|
||||||
|
// gaze on|off|toggle (gaze mode, likewise with POINTER_GAZE), gz ... (the gaze, from ft-gazed),
|
||||||
// reload (re-read the settings below), debug (toggle a twice-a-second state log),
|
// 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
|
// and btn/scroll lines, which are forwarded to the driver unchanged. For layouts, with a
|
||||||
// reply datagram to the sender's (abstract) address:
|
// reply datagram to the sender's (abstract) address:
|
||||||
@@ -140,7 +159,8 @@
|
|||||||
// small controls (undock, frame buttons) float a few centimetres in front of their
|
// 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. POINTER_FOLLOW (0) and
|
// panel, and a laser that starts behind them can't hit them. POINTER_FOLLOW (0) and
|
||||||
// POINTER_LEASH_DEG (10), POINTER_LEASH_DELAY (0.2 s), POINTER_LEASH_RETURN (0.2 s),
|
// POINTER_LEASH_DEG (10), POINTER_LEASH_DELAY (0.2 s), POINTER_LEASH_RETURN (0.2 s),
|
||||||
// POINTER_FOLLOW_REACH (70 deg): head follow, above.
|
// POINTER_FOLLOW_REACH (70 deg): head follow, above. POINTER_GAZE (0), POINTER_GAZE_RETAKE
|
||||||
|
// (5 deg), POINTER_GAZE_NUDGE_MAX (8 deg): gaze mode, above.
|
||||||
#include <openvr.h>
|
#include <openvr.h>
|
||||||
|
|
||||||
#include "vrmath.h"
|
#include "vrmath.h"
|
||||||
@@ -339,6 +359,9 @@ int main() {
|
|||||||
// Head follow (see the top). followConf is POINTER_FOLLOW as last read: a reload only
|
// Head follow (see the top). followConf is POINTER_FOLLOW as last read: a reload only
|
||||||
// overrides a "follow" command when the setting itself changed.
|
// overrides a "follow" command when the setting itself changed.
|
||||||
bool follow = false, followConf = false, followReset = true;
|
bool follow = false, followConf = false, followReset = true;
|
||||||
|
// Gaze mode (see the top); gazeConf is POINTER_GAZE as last read, like followConf.
|
||||||
|
bool gazeOn = false, gazeConf = false;
|
||||||
|
double gazeRetake = 5, gazeNudgeMax = 8;
|
||||||
auto loadConfig = [&] {
|
auto loadConfig = [&] {
|
||||||
const auto conf = ReadConfig();
|
const auto conf = ReadConfig();
|
||||||
freeDistance = std::clamp(ConfDouble(conf, "POINTER_DISTANCE", 1.5), 0.3, 10.0);
|
freeDistance = std::clamp(ConfDouble(conf, "POINTER_DISTANCE", 1.5), 0.3, 10.0);
|
||||||
@@ -355,6 +378,10 @@ int main() {
|
|||||||
followReach = std::clamp(ConfDouble(conf, "POINTER_FOLLOW_REACH", 70), 10.0, 89.0);
|
followReach = std::clamp(ConfDouble(conf, "POINTER_FOLLOW_REACH", 70), 10.0, 89.0);
|
||||||
const bool wantFollow = ConfDouble(conf, "POINTER_FOLLOW", 0) != 0;
|
const bool wantFollow = ConfDouble(conf, "POINTER_FOLLOW", 0) != 0;
|
||||||
if (wantFollow != followConf) follow = followConf = wantFollow, followReset = true;
|
if (wantFollow != followConf) follow = followConf = wantFollow, followReset = true;
|
||||||
|
gazeRetake = std::clamp(ConfDouble(conf, "POINTER_GAZE_RETAKE", 5), 1.0, 45.0);
|
||||||
|
gazeNudgeMax = std::clamp(ConfDouble(conf, "POINTER_GAZE_NUDGE_MAX", 8), 1.0, 30.0);
|
||||||
|
const bool wantGaze = ConfDouble(conf, "POINTER_GAZE", 0) != 0;
|
||||||
|
if (wantGaze != gazeConf) gazeOn = gazeConf = wantGaze;
|
||||||
};
|
};
|
||||||
loadConfig();
|
loadConfig();
|
||||||
const float laserWidth = float(ConfDouble(ReadConfig(), "POINTER_LASER_WIDTH", 0.8));
|
const float laserWidth = float(ConfDouble(ReadConfig(), "POINTER_LASER_WIDTH", 0.8));
|
||||||
@@ -443,6 +470,17 @@ int main() {
|
|||||||
bool following = false; // past the leash: easing toward the head
|
bool following = false; // past the leash: easing toward the head
|
||||||
double followLag = 0; // radians the reference trails the head
|
double followLag = 0; // radians the reference trails the head
|
||||||
std::chrono::steady_clock::time_point leashOutSince{}; // head past the leash since (delay)
|
std::chrono::steady_clock::time_point leashOutSince{}; // head past the leash since (delay)
|
||||||
|
// Gaze mode (see the top).
|
||||||
|
struct Gaze {
|
||||||
|
double hy = 0, hp = 0, rhy = 0, rhp = 0; // corrected, and raw
|
||||||
|
Clock::time_point at{};
|
||||||
|
} gz;
|
||||||
|
bool gazeOwns = true; // the pointer follows the gaze; false: the mouse has it
|
||||||
|
bool nudging = false; // the mouse took it from the gaze: the next click may be a lesson
|
||||||
|
double nudgeRawHy = 0, nudgeRawHp = 0, nudgeMoved = 0;
|
||||||
|
vr::HmdMatrix34_t nudgeHead{}, lastHead{};
|
||||||
|
bool haveHead = false, havePoint = false;
|
||||||
|
Clock::time_point nudgeAt{}, retakeSince{};
|
||||||
vr::TrackedDeviceIndex_t ours = vr::k_unTrackedDeviceIndexInvalid;
|
vr::TrackedDeviceIndex_t ours = vr::k_unTrackedDeviceIndexInvalid;
|
||||||
auto lastSlow = std::chrono::steady_clock::now() - std::chrono::seconds(10);
|
auto lastSlow = std::chrono::steady_clock::now() - std::chrono::seconds(10);
|
||||||
|
|
||||||
@@ -636,6 +674,22 @@ int main() {
|
|||||||
if (senderLen > offsetof(sockaddr_un, sun_path))
|
if (senderLen > offsetof(sockaddr_un, sun_path))
|
||||||
sendto(out, msg.data(), msg.size(), 0, reinterpret_cast<const sockaddr *>(&sender), senderLen);
|
sendto(out, msg.data(), msg.size(), 0, reinterpret_cast<const sockaddr *>(&sender), senderLen);
|
||||||
};
|
};
|
||||||
|
// The gaze, from ft-gazed: not mouse input, it never wakes the pointer.
|
||||||
|
double g[4];
|
||||||
|
if (std::sscanf(buf, "gz %lf %lf %lf %lf", &g[0], &g[1], &g[2], &g[3]) == 4) {
|
||||||
|
gz = {g[0], g[1], g[2], g[3], Clock::now()};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (std::strncmp(buf, "gaze", 4) == 0) {
|
||||||
|
const char *arg = buf + 4;
|
||||||
|
while (*arg == ' ') ++arg;
|
||||||
|
gazeOn = std::strncmp(arg, "on", 2) == 0 ? true : std::strncmp(arg, "off", 3) == 0 ? false : !gazeOn;
|
||||||
|
gazeOwns = true, nudging = false;
|
||||||
|
std::printf("gaze mode %s\n", gazeOn ? "on" : "off");
|
||||||
|
std::fflush(stdout);
|
||||||
|
reply(gazeOn ? "ok on" : "ok off");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const bool mouseInput = std::strncmp(buf, "move", 4) == 0 || std::strncmp(buf, "btn", 3) == 0 ||
|
const bool mouseInput = std::strncmp(buf, "move", 4) == 0 || std::strncmp(buf, "btn", 3) == 0 ||
|
||||||
std::strncmp(buf, "scroll", 6) == 0;
|
std::strncmp(buf, "scroll", 6) == 0;
|
||||||
if (mouseInput) lastMouse = Clock::now();
|
if (mouseInput) lastMouse = Clock::now();
|
||||||
@@ -687,6 +741,18 @@ int main() {
|
|||||||
// ft-screens sends the keyboard to the panel clicked last; it sees clicks on
|
// ft-screens sends the keyboard to the panel clicked last; it sees clicks on
|
||||||
// its own screens, but only we know when one lands on another panel.
|
// its own screens, but only we know when one lands on another panel.
|
||||||
SendTo(out, "ft_screens", "click " + (lastHit.empty() ? std::string("-") : lastHit));
|
SendTo(out, "ft_screens", "click " + (lastHit.empty() ? std::string("-") : lastHit));
|
||||||
|
// A click after nudging the gaze-placed pointer: the nudge is a lesson (see the top).
|
||||||
|
if (gazeOn && nudging && !gazeOwns && havePoint && Clock::now() - nudgeAt < std::chrono::seconds(10) &&
|
||||||
|
nudgeMoved >= 0.2 && nudgeMoved <= gazeNudgeMax) {
|
||||||
|
const Vec3 d = RotateInverse(nudgeHead, Normalize(lastPoint - Position(nudgeHead)));
|
||||||
|
char msg[160];
|
||||||
|
std::snprintf(msg, sizeof msg, "lesson %.3f %.3f %.3f %.3f", nudgeRawHy, nudgeRawHp,
|
||||||
|
std::atan2(-d.x, -d.z) * 180 / M_PI, std::asin(std::clamp(d.y, -1.0, 1.0)) * 180 / M_PI);
|
||||||
|
SendTo(out, "ft_gazed", msg);
|
||||||
|
if (debug) std::printf("gaze %s (nudged %.2f deg)\n", msg, nudgeMoved);
|
||||||
|
if (debug) std::fflush(stdout);
|
||||||
|
}
|
||||||
|
nudging = false;
|
||||||
leftHeld = true;
|
leftHeld = true;
|
||||||
dragDistance = lastDistance;
|
dragDistance = lastDistance;
|
||||||
tiltYaw = tiltPitch = 0; // a new drag starts untilted
|
tiltYaw = tiltPitch = 0; // a new drag starts untilted
|
||||||
@@ -709,6 +775,14 @@ int main() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (std::sscanf(buf, "move %lf %lf", &a, &b) == 2) {
|
if (std::sscanf(buf, "move %lf %lf", &a, &b) == 2) {
|
||||||
|
if (gazeOn && gazeOwns) {
|
||||||
|
// The mouse takes the pointer from the gaze, from where the gaze left it.
|
||||||
|
gazeOwns = false;
|
||||||
|
nudging = haveHead && Clock::now() - gz.at < std::chrono::milliseconds(200);
|
||||||
|
nudgeRawHy = gz.rhy, nudgeRawHp = gz.rhp, nudgeHead = lastHead;
|
||||||
|
nudgeAt = Clock::now(), nudgeMoved = 0;
|
||||||
|
}
|
||||||
|
if (nudging) nudgeMoved += std::hypot(a, b);
|
||||||
if (!anchored) recenter = true;
|
if (!anchored) recenter = true;
|
||||||
yaw += a;
|
yaw += a;
|
||||||
while (yaw > 180) yaw -= 360;
|
while (yaw > 180) yaw -= 360;
|
||||||
@@ -819,9 +893,33 @@ int main() {
|
|||||||
followReset = true;
|
followReset = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gaze mode (see the top): the gaze has the pointer, or takes it back when you look
|
||||||
|
// well away from it. Not while a press holds the pointer, and only on fresh gaze.
|
||||||
|
if (hmd.bPoseIsValid) lastHead = hmd.mDeviceToAbsoluteTracking, haveHead = true;
|
||||||
|
if (gazeOn && active && hmd.bPoseIsValid && !tilting && !leftHeld && tnow >= dropHoldUntil &&
|
||||||
|
tnow - gz.at < std::chrono::milliseconds(150)) {
|
||||||
|
const Vec3 g = Rotate(hmd.mDeviceToAbsoluteTracking, Direction(gz.hy, gz.hp));
|
||||||
|
if (!gazeOwns && havePoint) {
|
||||||
|
const double off = std::acos(std::clamp(Dot(g, Normalize(lastPoint - eye)), -1.0, 1.0)) * 180 / M_PI;
|
||||||
|
if (off > gazeRetake && tnow - lastMouse > std::chrono::milliseconds(300)) {
|
||||||
|
if (retakeSince == Clock::time_point{}) retakeSince = tnow;
|
||||||
|
if (tnow - retakeSince >= std::chrono::milliseconds(120)) gazeOwns = true, nudging = false;
|
||||||
|
} else {
|
||||||
|
retakeSince = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (gazeOwns) {
|
||||||
|
retakeSince = {};
|
||||||
|
anchor = eye;
|
||||||
|
anchored = true;
|
||||||
|
yaw = std::atan2(-g.x, -g.z) * 180 / M_PI;
|
||||||
|
pitch = std::clamp(std::asin(std::clamp(g.y, -1.0, 1.0)) * 180 / M_PI, -85.0, 85.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Head follow (see the top): past the leash (for the delay), ease the reference to the
|
// Head follow (see the top): past the leash (for the delay), ease the reference to the
|
||||||
// head's facing, and turn the cursor with it.
|
// head's facing, and turn the cursor with it. Not in gaze mode: the gaze places it.
|
||||||
if (follow && active && anchored && hmd.bPoseIsValid) {
|
if (follow && !gazeOn && active && anchored && hmd.bPoseIsValid) {
|
||||||
const Vec3 head = LimitPitch(Vec3{-hm[0][2], -hm[1][2], -hm[2][2]}, 85);
|
const Vec3 head = LimitPitch(Vec3{-hm[0][2], -hm[1][2], -hm[2][2]}, 85);
|
||||||
if (followReset) {
|
if (followReset) {
|
||||||
followRef = head, followLag = 0, leashOutSince = {};
|
followRef = head, followLag = 0, leashOutSince = {};
|
||||||
@@ -1037,12 +1135,14 @@ int main() {
|
|||||||
const Vec3 originStanding = eye + Normalize(point - eye) * originDist;
|
const Vec3 originStanding = eye + Normalize(point - eye) * originDist;
|
||||||
const Vec3 eyeRaw = Position(R) + Rotate(R, RotateInverse(S, originStanding - eye));
|
const Vec3 eyeRaw = Position(R) + Rotate(R, RotateInverse(S, originStanding - eye));
|
||||||
lastPoint = point, lastOrigin = originStanding, lastAim = aimStanding; // tilt starts from here
|
lastPoint = point, lastOrigin = originStanding, lastAim = aimStanding; // tilt starts from here
|
||||||
|
havePoint = true;
|
||||||
if (debug && tnow - lastDebug > std::chrono::milliseconds(500)) {
|
if (debug && tnow - lastDebug > std::chrono::milliseconds(500)) {
|
||||||
lastDebug = tnow;
|
lastDebug = tnow;
|
||||||
if (systemPointer == vr::k_ulOverlayHandleInvalid) overlay->FindOverlay("system.pointer", &systemPointer);
|
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",
|
std::printf("dbg %s hit=%s dist=%.2f eye->point=%.2f origin=%.2f yaw=%.1f pitch=%.1f gaze=%s steamvr_dot=%d primary=%u\n",
|
||||||
dragging ? "DRAG" : occluded ? "INFRONT" : onEdge ? "EDGE" : onScene ? "SCENE" : (best < 1e8 ? "PANEL" : "FREE"), lastHit.empty() ? "-" : lastHit.c_str(),
|
dragging ? "DRAG" : occluded ? "INFRONT" : onEdge ? "EDGE" : onScene ? "SCENE" : (best < 1e8 ? "PANEL" : "FREE"), lastHit.empty() ? "-" : lastHit.c_str(),
|
||||||
distance, toPoint, originDist, yaw, pitch,
|
distance, toPoint, originDist, yaw, pitch,
|
||||||
|
!gazeOn ? "off" : tnow - gz.at > std::chrono::milliseconds(150) ? "stale" : gazeOwns ? "owns" : "mouse",
|
||||||
systemPointer != vr::k_ulOverlayHandleInvalid && overlay->IsOverlayVisible(systemPointer),
|
systemPointer != vr::k_ulOverlayHandleInvalid && overlay->IsOverlayVisible(systemPointer),
|
||||||
overlay->GetPrimaryDashboardDevice());
|
overlay->GetPrimaryDashboardDevice());
|
||||||
std::fflush(stdout);
|
std::fflush(stdout);
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ POINTER_LEASH_DEG=10 # head follow: how far (degrees) your head can turn b
|
|||||||
POINTER_LEASH_DELAY=0.2 # head follow: how long (s) your head has to stay past the leash, so a glance doesn't move the cursor
|
POINTER_LEASH_DELAY=0.2 # head follow: how long (s) your head has to stay past the leash, so a glance doesn't move the cursor
|
||||||
POINTER_LEASH_RETURN=0.2 # head follow: once it comes along, how quickly (s) the cursor settles back to its place in your view
|
POINTER_LEASH_RETURN=0.2 # head follow: once it comes along, how quickly (s) the cursor settles back to its place in your view
|
||||||
POINTER_FOLLOW_REACH=70 # head follow: how far (degrees) from the middle of your view the mouse can move the cursor
|
POINTER_FOLLOW_REACH=70 # head follow: how far (degrees) from the middle of your view the mouse can move the cursor
|
||||||
|
POINTER_GAZE=0 # 1 = the pointer goes where you look, the mouse does the last bit (needs the gaze service: gaze/run.sh install)
|
||||||
|
POINTER_GAZE_RETAKE=5 # gaze mode: how far (degrees) you look away from the pointer before the gaze takes it back from the mouse
|
||||||
|
POINTER_GAZE_NUDGE_MAX=8 # gaze mode: a mouse nudge up to this far (degrees) before a click is learned as the eye tracker's error
|
||||||
META_DASHBOARD=0 # 1 = a Meta tap on a pass-through keyboard toggles the SteamVR dashboard (pointer mode only)
|
META_DASHBOARD=0 # 1 = a Meta tap on a pass-through keyboard toggles the SteamVR dashboard (pointer mode only)
|
||||||
SHARE_KEYS=0 # 1 = keys of keyboards grabbed for the desktop also go to @frametop_keys, for hotkey tools (any local process can listen)
|
SHARE_KEYS=0 # 1 = keys of keyboards grabbed for the desktop also go to @frametop_keys, for hotkey tools (any local process can listen)
|
||||||
LAYOUT_GRAB_OFFSET=0.075 # arranging screens: where the floating window's grab bar is, in metres below the screen
|
LAYOUT_GRAB_OFFSET=0.075 # arranging screens: where the floating window's grab bar is, in metres below the screen
|
||||||
|
|||||||
Reference in new issue
Block a user