Drive the engine's mouse cursor from the VR aim ray on the game quad
build / build (push) Successful in 1m44s

The game quad's laser beam was purely visual - aiming at the game's own
UI (inventory, menus, dialogs) had no way to interact with it. Add a
real mouse pointer: absolute cursor position plus a single left click,
mirroring a plain point-and-click mouse (no mouselook, no right-click,
no drag).

New xr_mouse.c/h bridges the aim-ray hit-test to the vendored engine's
own public mouse API - mouse_put_xy() for position, a synthetic
SDL_MOUSEBUTTONDOWN/UP (SDL_BUTTON_LEFT) via SDL_PushEvent for clicks,
the same technique nativeSendPrintableChar() already uses for
synthetic keyboard input. No engine patch needed. Deliberately bypasses
SDLActivity.onNativeMouse()/SDL_MOUSEMOTION/SetMouseXY(), whose
physical-window-size scaling is unreliable in this headless immersive
build.

xr_input_try_game_quad() now reports the hit u,v; a new
g_game_touch_active[] tracks a held click on the game quad so it
survives the ray straying onto the keyboard/menu before release,
gating those overlays' claims the same way pending overlay touches
already do.
This commit is contained in:
ml
2026-08-14 08:36:09 +02:00
parent 824b4a2506
commit 581af95e7b
7 changed files with 136 additions and 22 deletions
+56 -14
View File
@@ -7,6 +7,7 @@
#include <GLES3/gl3.h>
#include "xr_mouse.h"
#include "xr_overlay.h"
#include "xr_session.h"
#include "xr_swapchain.h"
@@ -53,6 +54,7 @@ static XrSpace g_aim_space[2] = {XR_NULL_HANDLE, XR_NULL_HANDLE};
static bool g_prev_select[2] = {false, false};
static bool g_prev_menu = false;
static bool g_game_prev_hit[2] = {false, false};
static bool g_game_touch_active[2] = {false, false};
static bool g_menu_prev_hit[2] = {false, false};
static bool g_menu_touch_active[2] = {false, false};
static bool g_keyboard_prev_hit[2] = {false, false};
@@ -524,12 +526,17 @@ static void xr_input_build_beam(int hand, const XrSpaceLocation *location, float
// Hit-tests this hand's ray against the game quad (xr_get_game_quad_extent())
// - the lowest-priority target, tried only once neither the keyboard nor
// menu overlay claimed the ray this frame. There's no touch/drag state
// for the game quad yet (see the file comment above) - this purely drives
// the laser-beam visual, symmetric with xr_input_try_overlay()'s `hit`
// term but without a touch/drag component. Returns plain `hit`.
// menu overlay claimed the ray this frame (or, symmetrically, once this
// hand already has a pending mouse-down on the game quad - see
// g_game_touch_active and the keyboard/menu call sites in
// xr_input_sync_and_draw()). outU/outV (only meaningful when the
// returned hit is true) let the caller drive the game's own mouse cursor
// via xr_mouse.h. Returns plain `hit` - drag/touch dispatch for actual
// clicks is the caller's responsibility (xr_mouse_click()), not this
// function's, unlike xr_input_try_overlay().
static bool xr_input_try_game_quad(int hand, const XrSpaceLocation *location, float fx, float fy,
float fz, bool *outPlaneHit, float *outPlaneDistance) {
float fz, bool *outPlaneHit, float *outPlaneDistance,
float *outU, float *outV) {
float distance, halfWidth, halfHeight;
xr_get_game_quad_extent(&distance, &halfWidth, &halfHeight);
@@ -548,6 +555,8 @@ static bool xr_input_try_game_quad(int hand, const XrSpaceLocation *location, fl
}
*outPlaneHit = planeHit;
*outPlaneDistance = t;
*outU = u;
*outV = v;
if (hit != g_game_prev_hit[hand]) {
LOGI("XR: %s aim ray %s game quad (u=%.2f v=%.2f)", kHandName[hand],
@@ -671,10 +680,18 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
// xr_input_try_overlay()'s doc comment) - the game quad itself.
// Whichever target claims the ray gets a laser-beam quad built for
// it (if a head pose is available and the ray actually crosses
// that target's plane) - see xr_input_build_beam().
// that target's plane) - see xr_input_build_beam(). Keyboard/menu
// are additionally gated on !g_game_touch_active[hand]: a pending
// mouse-down on the game quad (trigger still held since a
// down-edge dispatched there) must keep claiming the ray even if
// it strays onto another panel before release, the cross-target
// analogue of xr_input_try_overlay()'s own hadTouch/hadDrag - the
// eventual mouse-up has to reach the game quad, not whatever the
// ray happens to be over on the release frame.
bool keyboardPlaneHit = false;
float keyboardPlaneDistance = 0.0f;
if (xr_input_try_overlay(keyboardOverlay, true, hand, &location, fx, fy, fz,
if (!g_game_touch_active[hand] &&
xr_input_try_overlay(keyboardOverlay, true, hand, &location, fx, fy, fz,
selectDownEdge, selectUpEdge, &g_keyboard_touch_active[hand],
&g_keyboard_prev_hit[hand], "keyboard", &keyboardCursorU,
&keyboardCursorV, &keyboardCursorHit, &keyboardPlaneHit,
@@ -686,7 +703,8 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
}
bool menuPlaneHit = false;
float menuPlaneDistance = 0.0f;
if (xr_input_try_overlay(menuOverlay, false, hand, &location, fx, fy, fz, selectDownEdge,
if (!g_game_touch_active[hand] &&
xr_input_try_overlay(menuOverlay, false, hand, &location, fx, fy, fz, selectDownEdge,
selectUpEdge, &g_menu_touch_active[hand], &g_menu_prev_hit[hand],
"menu", &menuCursorU, &menuCursorV, &menuCursorHit, &menuPlaneHit,
&menuPlaneDistance)) {
@@ -696,13 +714,36 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
continue;
}
// Lowest priority: the game quad itself - purely visual (beam
// only, same as the other two targets), no click/touch dispatch
// here yet (see the file comment above).
// Lowest priority (for a fresh hit): the game quad itself - drives
// the engine's own mouse cursor/clicks via xr_mouse.h, exactly
// like a desktop mouse (absolute position + left button), plus the
// same laser-beam visual the other two targets get.
bool gamePlaneHit = false;
float gamePlaneDistance = 0.0f;
bool gameHit =
xr_input_try_game_quad(hand, &location, fx, fy, fz, &gamePlaneHit, &gamePlaneDistance);
float gamePlaneDistance = 0.0f, gameU = 0.0f, gameV = 0.0f;
bool gameHit = xr_input_try_game_quad(hand, &location, fx, fy, fz, &gamePlaneHit,
&gamePlaneDistance, &gameU, &gameV);
if (gameHit) {
int gameWidth, gameHeight;
xr_get_game_resolution(&gameWidth, &gameHeight);
int px = (int)(gameU * (float)gameWidth);
int py = (int)(gameV * (float)gameHeight);
if (px < 0)
px = 0;
else if (px >= gameWidth)
px = gameWidth - 1;
if (py < 0)
py = 0;
else if (py >= gameHeight)
py = gameHeight - 1;
xr_mouse_move(px, py);
}
if (selectDownEdge && gameHit) {
xr_mouse_click(true);
g_game_touch_active[hand] = true;
} else if (selectUpEdge && g_game_touch_active[hand]) {
xr_mouse_click(false);
g_game_touch_active[hand] = false;
}
// gameHit implies gamePlaneHit (both only ever set together above),
// kept as a separate out-param for symmetry with
// xr_input_try_overlay()'s outPlaneHit/outPlaneDistance pair.
@@ -746,6 +787,7 @@ void xr_input_shutdown(void) {
g_session = XR_NULL_HANDLE;
memset(g_prev_select, 0, sizeof(g_prev_select));
memset(g_game_prev_hit, 0, sizeof(g_game_prev_hit));
memset(g_game_touch_active, 0, sizeof(g_game_touch_active));
memset(g_menu_prev_hit, 0, sizeof(g_menu_prev_hit));
memset(g_menu_touch_active, 0, sizeof(g_menu_touch_active));
memset(g_keyboard_prev_hit, 0, sizeof(g_keyboard_prev_hit));