2 Commits

Author SHA1 Message Date
ml 581af95e7b 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.
2026-08-14 08:36:09 +02:00
ml 824b4a2506 Give the game quad the same laser-beam pointer as the overlays
Aiming looked and behaved differently depending on whether the
keyboard/menu was open: xr_input_try_overlay() claimed a hand's ray for
its overlay whenever the overlay was merely visible, regardless of
whether the ray was actually pointed at it - so with the keyboard open,
aiming at the main game screen still showed a beam terminating at the
keyboard's fixed depth, while closing the keyboard swapped in the old
flat cross-shaped reticle at whatever the ray was really pointing at.

Only claim an overlay when the ray is actually relevant this frame - a
real hit, or a touch/drag begun on a previous frame still pending
release - so a visible-but-unaimed-at overlay now falls through to the
next-priority target. Give the game quad its own hit-test
(xr_input_try_game_quad()) and the same billboarded laser-beam
treatment as the overlays instead of the old flat reticle, so aiming
looks and behaves identically everywhere - overlay open or not. The
old reticle GL program/shaders are now fully dead and removed. No
click/touch dispatch is added to the game quad itself; that's future
work.
2026-08-14 07:59:09 +02:00
8 changed files with 231 additions and 159 deletions
+1 -1
View File
@@ -171,7 +171,7 @@ android {
"-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=BOTH", \ "-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=BOTH", \
"-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH", \ "-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH", \
"-DCMAKE_SHARED_LINKER_FLAGS=-Wl,-z,max-page-size=16384", \ "-DCMAKE_SHARED_LINKER_FLAGS=-Wl,-z,max-page-size=16384", \
"-DANDROID_EXTRA_SOURCES=${projectDir}/src/main/cpp/questshock_native.c;${projectDir}/src/main/cpp/xr_session.c;${projectDir}/src/main/cpp/xr_input.c;${projectDir}/src/main/cpp/xr_overlay.c;${projectDir}/src/main/cpp/xr_swapchain.c" "-DANDROID_EXTRA_SOURCES=${projectDir}/src/main/cpp/questshock_native.c;${projectDir}/src/main/cpp/xr_session.c;${projectDir}/src/main/cpp/xr_input.c;${projectDir}/src/main/cpp/xr_overlay.c;${projectDir}/src/main/cpp/xr_swapchain.c;${projectDir}/src/main/cpp/xr_mouse.c"
abiFilters 'arm64-v8a' abiFilters 'arm64-v8a'
} }
} }
+126 -129
View File
@@ -7,6 +7,7 @@
#include <GLES3/gl3.h> #include <GLES3/gl3.h>
#include "xr_mouse.h"
#include "xr_overlay.h" #include "xr_overlay.h"
#include "xr_session.h" #include "xr_session.h"
#include "xr_swapchain.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_select[2] = {false, false};
static bool g_prev_menu = false; static bool g_prev_menu = false;
static bool g_game_prev_hit[2] = {false, 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_prev_hit[2] = {false, false};
static bool g_menu_touch_active[2] = {false, false}; static bool g_menu_touch_active[2] = {false, false};
static bool g_keyboard_prev_hit[2] = {false, false}; static bool g_keyboard_prev_hit[2] = {false, false};
@@ -70,9 +72,6 @@ static int g_keyboard_drag_hand = -1;
static float g_keyboard_drag_offset_x = 0.0f; static float g_keyboard_drag_offset_x = 0.0f;
static float g_keyboard_drag_offset_y = 0.0f; static float g_keyboard_drag_offset_y = 0.0f;
static GLuint g_reticle_program = 0;
static GLint g_reticle_color_loc = -1;
// A "view" reference space, located once per frame (not per hand) to get // A "view" reference space, located once per frame (not per hand) to get
// an approximate head position for the laser-beam billboard math below - // an approximate head position for the laser-beam billboard math below -
// the only consumer of a head pose in this file. Both LOCAL (g_local_space, // the only consumer of a head pose in this file. Both LOCAL (g_local_space,
@@ -82,9 +81,9 @@ static XrSpace g_view_space = XR_NULL_HANDLE;
// Per-hand laser-beam state: a thin, billboarded quad spanning from the // Per-hand laser-beam state: a thin, billboarded quad spanning from the
// controller to wherever that hand's ray currently crosses the plane of // controller to wherever that hand's ray currently crosses the plane of
// whichever overlay (keyboard/menu) claimed it this frame - see // whichever target (keyboard, menu, or the game quad) claimed it this
// xr_input_build_beam()/xr_input_get_beam_layer(). Each hand gets its own // frame - see xr_input_build_beam()/xr_input_get_beam_layer(). Each hand
// tiny solid-color swapchain (no shared tint on XrCompositionLayerQuad, // gets its own tiny solid-color swapchain (no shared tint on XrCompositionLayerQuad,
// so two separately-colored textures is simplest). g_beam_quad_valid is // so two separately-colored textures is simplest). g_beam_quad_valid is
// reset to false at the top of every xr_input_sync_and_draw() call and // reset to false at the top of every xr_input_sync_and_draw() call and
// only set back to true if that hand actually claims a beam this frame. // only set back to true if that hand actually claims a beam this frame.
@@ -183,63 +182,6 @@ static void mat3_to_quat(const float x[3], const float y[3], const float z[3], X
} }
} }
static GLuint compile_shader(GLenum type, const char *src) {
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &src, NULL);
glCompileShader(shader);
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (!compiled) {
char log[512];
glGetShaderInfoLog(shader, sizeof(log), NULL, log);
LOGE("XR: reticle shader compile failed: %s", log);
}
return shader;
}
// A standalone flat-color shader, independent of the engine's own
// textureShaderProgram (OpenGL.cc is a separate, C++-only translation
// unit, and shader state isn't shared across programs anyway) - position
// is emitted directly in clip space, matching the same [-1,1] local quad
// coordinates android_draw_surface_as_quad() (see android/engine-patches/
// 11-android-openxr-present.patch) already uses for its own vertex
// positions, so no view/projection matrix is needed here either.
static const char *kVertexSrc = "attribute vec3 position;\n"
"void main() { gl_Position = vec4(position, 1.0); }\n";
static const char *kFragmentSrc = "precision mediump float;\n"
"uniform vec4 color;\n"
"void main() { gl_FragColor = color; }\n";
// Attribute 0 to match kVertexSrc's single "position" attribute - fine to
// reuse the same numeric index the engine's own immediate-mode drawing
// treats specially, since that's a per-program binding and this program is
// never current at the same time as gl4es's immediate-mode emulation runs;
// xr_input_sync_and_draw() disables the array again right after drawing,
// the same discipline android_draw_surface_as_quad() already established.
#define RETICLE_POSITION_LOC 0
static bool xr_input_init_reticle_program(void) {
GLuint vs = compile_shader(GL_VERTEX_SHADER, kVertexSrc);
GLuint fs = compile_shader(GL_FRAGMENT_SHADER, kFragmentSrc);
g_reticle_program = glCreateProgram();
glAttachShader(g_reticle_program, vs);
glAttachShader(g_reticle_program, fs);
glBindAttribLocation(g_reticle_program, RETICLE_POSITION_LOC, "position");
glLinkProgram(g_reticle_program);
GLint linked = GL_FALSE;
glGetProgramiv(g_reticle_program, GL_LINK_STATUS, &linked);
glDeleteShader(vs);
glDeleteShader(fs);
if (!linked) {
char log[512];
glGetProgramInfoLog(g_reticle_program, sizeof(log), NULL, log);
LOGE("XR: reticle program link failed: %s", log);
return false;
}
g_reticle_color_loc = glGetUniformLocation(g_reticle_program, "color");
return true;
}
bool xr_input_init(XrInstance instance, XrSession session, int64_t swapchain_format) { bool xr_input_init(XrInstance instance, XrSession session, int64_t swapchain_format) {
g_instance = instance; g_instance = instance;
g_session = session; g_session = session;
@@ -349,9 +291,6 @@ bool xr_input_init(XrInstance instance, XrSession session, int64_t swapchain_for
} }
} }
if (!xr_input_init_reticle_program())
return false;
LOGI("XR: input action set ready (aim pose + trigger + menu-toggle)"); LOGI("XR: input action set ready (aim pose + trigger + menu-toggle)");
return true; return true;
} }
@@ -361,10 +300,13 @@ bool xr_input_init(XrInstance instance, XrSession session, int64_t swapchain_for
// below. dragCapable enables the keyboard-only title-bar/drag-handle // below. dragCapable enables the keyboard-only title-bar/drag-handle
// handling (see TITLE_BAR_V_FRACTION/CLOSE_BUTTON_U_FRACTION); the menu // handling (see TITLE_BAR_V_FRACTION/CLOSE_BUTTON_U_FRACTION); the menu
// launcher has no title bar, so it's always false there and every hit is a // launcher has no title bar, so it's always false there and every hit is a
// plain click. Returns true if this overlay is visible - claiming the // plain click. Returns true only when this hand's ray is actually
// hand's ray processing for this frame, regardless of whether the ray // relevant to this overlay this frame - landing on it now, or continuing
// actually hits it - so the caller should stop trying other targets (menu/ // a touch/drag begun on a previous frame for this hand that hasn't been
// keyboard/game quad are mutually exclusive per hand, per frame). // released yet - not merely because the overlay is visible. The caller
// should only stop trying other targets (menu/keyboard/game quad) when
// this returns true; a visible-but-unclaimed overlay falls through so a
// lower-priority target (ultimately the game quad) can still be aimed at.
static bool xr_input_try_overlay(XrOverlay *overlay, bool dragCapable, int hand, static bool xr_input_try_overlay(XrOverlay *overlay, bool dragCapable, int hand,
const XrSpaceLocation *location, float fx, float fy, float fz, const XrSpaceLocation *location, float fx, float fy, float fz,
bool selectDownEdge, bool selectUpEdge, bool *touchActive, bool selectDownEdge, bool selectUpEdge, bool *touchActive,
@@ -375,6 +317,14 @@ static bool xr_input_try_overlay(XrOverlay *overlay, bool dragCapable, int hand,
if (!xr_overlay_is_visible(overlay)) if (!xr_overlay_is_visible(overlay))
return false; return false;
// Captured before this call's own down/up-edge handling below can
// mutate them, so a hand with a pending down-touch (waiting for its
// matching up) or an in-progress drag on *this* overlay still claims
// the ray this frame even if it has strayed off the panel's current
// bounds - see `claims` below.
bool hadTouch = *touchActive;
bool hadDrag = dragCapable && g_keyboard_dragging && g_keyboard_drag_hand == hand;
float quadCenterX, quadCenterY, distance, halfWidth, halfHeight; float quadCenterX, quadCenterY, distance, halfWidth, halfHeight;
xr_overlay_get_quad_extent(overlay, &quadCenterX, &quadCenterY, &distance, &halfWidth, xr_overlay_get_quad_extent(overlay, &quadCenterX, &quadCenterY, &distance, &halfWidth,
&halfHeight); &halfHeight);
@@ -416,6 +366,12 @@ static bool xr_input_try_overlay(XrOverlay *overlay, bool dragCapable, int hand,
*outCursorV = v; *outCursorV = v;
} }
// Only claim (and thus route/beam) this hand's ray to this overlay
// when it's actually relevant this frame: landing on it now, or
// continuing a touch/drag that started on it - never merely because
// it's visible.
bool claims = hit || hadTouch || hadDrag;
if (!dragCapable) { if (!dragCapable) {
if (selectDownEdge && hit) { if (selectDownEdge && hit) {
xr_overlay_touch(overlay, u, v, true); xr_overlay_touch(overlay, u, v, true);
@@ -424,7 +380,7 @@ static bool xr_input_try_overlay(XrOverlay *overlay, bool dragCapable, int hand,
xr_overlay_touch(overlay, u, v, false); xr_overlay_touch(overlay, u, v, false);
*touchActive = false; *touchActive = false;
} }
return true; return claims;
} }
bool onTitleBar = hit && v < TITLE_BAR_V_FRACTION; bool onTitleBar = hit && v < TITLE_BAR_V_FRACTION;
@@ -457,7 +413,7 @@ static bool xr_input_try_overlay(XrOverlay *overlay, bool dragCapable, int hand,
xr_overlay_set_position(overlay, worldX - g_keyboard_drag_offset_x, xr_overlay_set_position(overlay, worldX - g_keyboard_drag_offset_x,
worldY - g_keyboard_drag_offset_y); worldY - g_keyboard_drag_offset_y);
} }
return true; return claims;
} }
// Builds this hand's laser-beam quad - a thin, billboarded ribbon from the // Builds this hand's laser-beam quad - a thin, billboarded ribbon from the
@@ -568,6 +524,48 @@ static void xr_input_build_beam(int hand, const XrSpaceLocation *location, float
g_beam_quad_valid[hand] = true; g_beam_quad_valid[hand] = true;
} }
// 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 (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 *outU, float *outV) {
float distance, halfWidth, halfHeight;
xr_get_game_quad_extent(&distance, &halfWidth, &halfHeight);
bool planeHit = false, hit = false;
float u = 0.0f, v = 0.0f, t = 0.0f;
if (fabsf(fz) > 1e-5f) {
t = (-distance - location->pose.position.z) / fz;
if (t > 0.0f) {
planeHit = true;
float cx = (location->pose.position.x + t * fx) / halfWidth;
float cy = (location->pose.position.y + t * fy) / halfHeight;
hit = fabsf(cx) <= 1.0f && fabsf(cy) <= 1.0f;
u = (cx + 1.0f) * 0.5f;
v = (1.0f - cy) * 0.5f;
}
}
*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],
hit ? "entered" : "left", u, v);
g_game_prev_hit[hand] = hit;
}
return hit;
}
void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) { void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
if (g_action_set == XR_NULL_HANDLE) if (g_action_set == XR_NULL_HANDLE)
return; return;
@@ -597,8 +595,10 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
g_beam_quad_valid[LEFT] = g_beam_quad_valid[RIGHT] = false; g_beam_quad_valid[LEFT] = g_beam_quad_valid[RIGHT] = false;
// The billboard math needs an approximate head position - only bother // The billboard math needs an approximate head position - only bother
// locating it on frames where a beam could possibly be drawn at all. // locating it on frames where a beam could possibly be drawn at all
bool needBeams = draw && (menuVisible || keyboardVisible); // (i.e. any drawn frame - the game quad can claim a beam on its own
// even with both overlays closed).
bool needBeams = draw;
XrSpaceLocation headLoc = {XR_TYPE_SPACE_LOCATION}; XrSpaceLocation headLoc = {XR_TYPE_SPACE_LOCATION};
bool haveHead = false; bool haveHead = false;
if (needBeams) { if (needBeams) {
@@ -609,17 +609,6 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
(headLoc.locationFlags & neededHead) == neededHead; (headLoc.locationFlags & neededHead) == neededHead;
} }
// The GL reticle is drawn directly into whatever framebuffer is
// currently bound - the game quad's swapchain image (see xr_frame_end(),
// which calls this while that image is still bound). That only makes
// sense while aiming at the game quad; the menu/keyboard overlays' own
// cursors are drawn by their Java views instead (see
// xr_overlay_update_cursor() below), composited into their Bitmaps the
// same way their other content is.
bool drawReticle = draw && !menuVisible && !keyboardVisible;
if (drawReticle)
glUseProgram(g_reticle_program);
// Fed to xr_overlay_update_cursor() after the loop below - whichever // Fed to xr_overlay_update_cursor() after the loop below - whichever
// hand's ray hits a given overlay last wins if both do, good enough // hand's ray hits a given overlay last wins if both do, good enough
// for a single on-quad cursor per overlay (only ever updated when hit // for a single on-quad cursor per overlay (only ever updated when hit
@@ -686,15 +675,23 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
quat_rotate_vec(&location.pose.orientation, 0.0f, 0.0f, -1.0f, &fx, &fy, &fz); quat_rotate_vec(&location.pose.orientation, 0.0f, 0.0f, -1.0f, &fx, &fy, &fz);
// Keyboard first (it's the more likely target while it's up), then // Keyboard first (it's the more likely target while it's up), then
// the menu launcher, then - only if neither is visible - the game // the menu launcher, then - only once neither actually claims the
// quad's own reticle below. A single ray only ever interacts with // ray this frame (not merely "isn't visible" - see
// one target per hand per frame. Whichever overlay claims the ray // xr_input_try_overlay()'s doc comment) - the game quad itself.
// also gets a laser-beam quad built for it (if a head pose is // Whichever target claims the ray gets a laser-beam quad built for
// available and the ray actually crosses that overlay's plane) - // it (if a head pose is available and the ray actually crosses
// 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; bool keyboardPlaneHit = false;
float keyboardPlaneDistance = 0.0f; 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], selectDownEdge, selectUpEdge, &g_keyboard_touch_active[hand],
&g_keyboard_prev_hit[hand], "keyboard", &keyboardCursorU, &g_keyboard_prev_hit[hand], "keyboard", &keyboardCursorU,
&keyboardCursorV, &keyboardCursorHit, &keyboardPlaneHit, &keyboardCursorV, &keyboardCursorHit, &keyboardPlaneHit,
@@ -706,7 +703,8 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
} }
bool menuPlaneHit = false; bool menuPlaneHit = false;
float menuPlaneDistance = 0.0f; 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], selectUpEdge, &g_menu_touch_active[hand], &g_menu_prev_hit[hand],
"menu", &menuCursorU, &menuCursorV, &menuCursorHit, &menuPlaneHit, "menu", &menuCursorU, &menuCursorV, &menuCursorHit, &menuPlaneHit,
&menuPlaneDistance)) { &menuPlaneDistance)) {
@@ -716,40 +714,42 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
continue; continue;
} }
float distance, halfWidth, halfHeight; // Lowest priority (for a fresh hit): the game quad itself - drives
xr_get_game_quad_extent(&distance, &halfWidth, &halfHeight); // the engine's own mouse cursor/clicks via xr_mouse.h, exactly
// like a desktop mouse (absolute position + left button), plus the
bool hit = false; // same laser-beam visual the other two targets get.
float u = 0.0f, v = 0.0f, cx = 0.0f, cy = 0.0f; bool gamePlaneHit = false;
if (fabsf(fz) > 1e-5f) { float gamePlaneDistance = 0.0f, gameU = 0.0f, gameV = 0.0f;
float t = (-distance - location.pose.position.z) / fz; bool gameHit = xr_input_try_game_quad(hand, &location, fx, fy, fz, &gamePlaneHit,
if (t > 0.0f) { &gamePlaneDistance, &gameU, &gameV);
cx = (location.pose.position.x + t * fx) / halfWidth; if (gameHit) {
cy = (location.pose.position.y + t * fy) / halfHeight; int gameWidth, gameHeight;
hit = fabsf(cx) <= 1.0f && fabsf(cy) <= 1.0f; xr_get_game_resolution(&gameWidth, &gameHeight);
u = (cx + 1.0f) * 0.5f; int px = (int)(gameU * (float)gameWidth);
v = (1.0f - cy) * 0.5f; 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),
if (hit != g_game_prev_hit[hand]) { // kept as a separate out-param for symmetry with
LOGI("XR: %s aim ray %s game quad (u=%.2f v=%.2f)", kHandName[hand], // xr_input_try_overlay()'s outPlaneHit/outPlaneDistance pair.
hit ? "entered" : "left", u, v); if (haveHead && gameHit)
g_game_prev_hit[hand] = hit; xr_input_build_beam(hand, &location, fx, fy, fz,
} fminf(gamePlaneDistance, MAX_BEAM_LENGTH_METERS), &headLoc);
if (!hit || !drawReticle)
continue;
const float kSize = 0.03f;
const float verts[] = {
cx - kSize, cy, 0.0f, cx + kSize, cy, 0.0f, cx, cy - kSize, 0.0f, cx, cy + kSize, 0.0f,
};
glUniform4fv(g_reticle_color_loc, 1, kHandColor[hand]);
glEnableVertexAttribArray(RETICLE_POSITION_LOC);
glVertexAttribPointer(RETICLE_POSITION_LOC, 3, GL_FLOAT, GL_FALSE, 0, verts);
glDrawArrays(GL_LINES, 0, 4);
glDisableVertexAttribArray(RETICLE_POSITION_LOC);
} }
if (menuVisible) if (menuVisible)
@@ -783,14 +783,11 @@ void xr_input_shutdown(void) {
g_select_click_action = XR_NULL_HANDLE; g_select_click_action = XR_NULL_HANDLE;
g_menu_toggle_action = XR_NULL_HANDLE; g_menu_toggle_action = XR_NULL_HANDLE;
if (g_reticle_program != 0)
glDeleteProgram(g_reticle_program);
g_reticle_program = 0;
g_instance = XR_NULL_HANDLE; g_instance = XR_NULL_HANDLE;
g_session = XR_NULL_HANDLE; g_session = XR_NULL_HANDLE;
memset(g_prev_select, 0, sizeof(g_prev_select)); memset(g_prev_select, 0, sizeof(g_prev_select));
memset(g_game_prev_hit, 0, sizeof(g_game_prev_hit)); 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_prev_hit, 0, sizeof(g_menu_prev_hit));
memset(g_menu_touch_active, 0, sizeof(g_menu_touch_active)); memset(g_menu_touch_active, 0, sizeof(g_menu_touch_active));
memset(g_keyboard_prev_hit, 0, sizeof(g_keyboard_prev_hit)); memset(g_keyboard_prev_hit, 0, sizeof(g_keyboard_prev_hit));
+27 -17
View File
@@ -1,16 +1,25 @@
// Controller input for questshock's immersive Quest build: one OpenXR // Controller input for questshock's immersive Quest build: one OpenXR
// action set (aim pose + trigger click per hand, a menu-toggle button on // action set (aim pose + trigger click per hand, a menu-toggle button on
// the left controller) plus ray/quad hit-testing. While neither the menu // the left controller) plus ray/quad hit-testing. Each hand's ray is
// launcher nor keyboard overlay (see xr_overlay.h, xr_session.c) is // tried against the keyboard overlay, then the menu overlay (see
// visible, this tests against the game quad xr_session.c submits and draws // xr_overlay.h, xr_session.c), then - only if neither actually claims it
// a small reticle where each hand's aim ray crosses it; while either // this frame (landing on it now, or continuing a touch/drag begun on a
// overlay is visible, it tests against that overlay's quad instead (the // previous frame - not merely because that overlay happens to be visible)
// keyboard is tried first), forwards trigger edges as synthetic touches, // - the game quad xr_session.c submits, forwarding trigger edges to
// and - since neither overlay's own on-quad cursor gives any feedback // whichever overlay claims the ray as synthetic touches. The game quad
// while the ray is short of actually landing on it - builds a thin, // itself acts as a plain desktop-style mouse pointer into the engine's
// own UI (see xr_mouse.h) - absolute cursor position while the ray hits
// it, plus a left click on the trigger edge; a pending click there
// likewise keeps claiming the ray ahead of the keyboard/menu until
// released, so the eventual mouse-up isn't lost if the ray strays. Since
// none of the three targets' own feedback (the overlays' on-quad cursor,
// drawn by their Java views; the engine's own mouse cursor sprite for the
// game quad) gives any indication while the ray is short of actually
// landing on something, every claimed target also builds a thin,
// billboarded laser-beam quad per hand from the controller to wherever // billboarded laser-beam quad per hand from the controller to wherever
// the ray currently crosses that overlay's plane (see // the ray currently crosses that target's plane (see
// xr_input_get_beam_layer()). // xr_input_get_beam_layer()) - so aiming looks and behaves the same
// whether the target is an overlay or the game quad itself.
#ifndef QUESTSHOCK_XR_INPUT_H #ifndef QUESTSHOCK_XR_INPUT_H
#define QUESTSHOCK_XR_INPUT_H #define QUESTSHOCK_XR_INPUT_H
@@ -38,17 +47,18 @@ bool xr_input_init(XrInstance instance, XrSession session, int64_t swapchain_for
// Call once per frame from xr_frame_end(), before releasing the acquired // Call once per frame from xr_frame_end(), before releasing the acquired
// swapchain image - syncs this frame's action states (always, so edge // swapchain image - syncs this frame's action states (always, so edge
// detection stays correct even on frames with nothing to draw), handles // detection stays correct even on frames with nothing to draw), handles
// the menu_toggle button's edge, and either draws a game-quad reticle or // the menu_toggle button's edge, and hit-tests/dispatches each hand's ray
// forwards menu-quad touches, per the menu's current visibility (see the // against keyboard/menu/game quad in that priority order (see the file
// file comment above). draw gates only the reticle - if false (nothing to // comment above). draw gates only the laser-beam visuals (and the
// draw into this frame, e.g. no swapchain image was acquired), hit-testing // head-pose locate that feeds them) - if false (nothing to draw into this
// and touch-forwarding still run. baseSpace/time must match whatever // frame, e.g. no swapchain image was acquired), hit-testing and
// touch-forwarding still run. baseSpace/time must match whatever
// xr_frame_begin() used to predict this frame. // xr_frame_begin() used to predict this frame.
void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw); void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw);
// Call once per hand (hand: 0=left, 1=right) after xr_input_sync_and_draw() // Call once per hand (hand: 0=left, 1=right) after xr_input_sync_and_draw()
// in the same frame - if that hand's ray claimed the keyboard or menu // in the same frame - if that hand's ray claimed the keyboard, menu, or
// overlay this frame (see the file comment above), fills *out_quad's // game quad this frame (see the file comment above), fills *out_quad's
// subImage/pose/size for its laser-beam ribbon and returns true. // subImage/pose/size for its laser-beam ribbon and returns true.
// space/eyeVisibility/layerFlags are left for the caller to set, same // space/eyeVisibility/layerFlags are left for the caller to set, same
// convention as xr_overlay_render_and_build_layer(). Returns false // convention as xr_overlay_render_and_build_layer(). Returns false
+20
View File
@@ -0,0 +1,20 @@
#include "xr_mouse.h"
#include <SDL.h>
#include "mouse.h" // engine/src/Libraries/INPUT/Source/mouse.h - mouse_put_xy()
void xr_mouse_move(int x, int y) { mouse_put_xy((short)x, (short)y); }
void xr_mouse_click(bool down) {
SDL_Event event;
SDL_zero(event);
event.type = down ? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP;
event.button.timestamp = SDL_GetTicks();
event.button.windowID = 0;
event.button.which = 0;
event.button.button = SDL_BUTTON_LEFT;
event.button.state = down ? SDL_PRESSED : SDL_RELEASED;
event.button.clicks = 1;
SDL_PushEvent(&event);
}
+35
View File
@@ -0,0 +1,35 @@
// Bridges VR aim-ray hit-testing (xr_input.c) to the vendored engine's
// mouse input (engine/src/Libraries/INPUT/Source/mouse.h) - lets the
// controller's laser pointer drive the game's own UI (inventory, menus,
// dialogs) exactly like a desktop mouse: absolute cursor position plus a
// single left button, no relative/mouselook mode, no right button.
#ifndef QUESTSHOCK_XR_MOUSE_H
#define QUESTSHOCK_XR_MOUSE_H
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
// Sets the engine's absolute mouse position (mouse_put_xy()) - call every
// frame the aim ray is actually hitting the game quad; x/y are pixel
// coordinates in the game's own logical resolution (see
// xr_get_game_resolution() in xr_session.h), already clamped by the
// caller. Do not call on frames the ray isn't hitting the game quad - the
// cursor should hold its last position, not snap elsewhere.
void xr_mouse_move(int x, int y);
// Pushes a synthetic SDL_MOUSEBUTTONDOWN/UP (SDL_BUTTON_LEFT) - matches
// KeyboardOverlay's nativeSendPrintableChar() in questshock_native.c: a
// plain SDL_Event built by hand and handed to the public SDL_PushEvent(),
// no engine patch needed. pump_events() (sdl_events.c) reads whatever
// mouse_put_xy() last set, not any x/y carried on the event itself - call
// xr_mouse_move() first if position needs updating this frame.
void xr_mouse_click(bool down);
#ifdef __cplusplus
}
#endif
#endif
+14 -10
View File
@@ -350,14 +350,13 @@ void xr_frame_end(void) {
if (g_session == XR_NULL_HANDLE) if (g_session == XR_NULL_HANDLE)
return; return;
// Draw the laser-pointer reticle into the still-bound game swapchain // Syncs actions and hit-tests/dispatches each hand's ray against
// framebuffer before releasing it, so it composites on top of whatever // keyboard/menu/game quad (see xr_input.c) before the game swapchain
// this frame's game content already drew there. Syncing actions // image is released below - this always runs, even when there's
// happens even when there's nothing to draw (no acquired image this // nothing to draw (no acquired image this frame), so edge detection
// frame), so edge detection (trigger/menu-button clicks) doesn't miss a // (trigger/menu-button clicks) doesn't miss a frame. Any resulting
// frame. Only ever draws while neither overlay is visible (see // laser-beam quads are submitted as their own composition layers
// xr_input.c) - the menu/keyboard render into their own independent // further down, not drawn into the game swapchain itself.
// swapchain images below.
if (xr_is_session_running()) if (xr_is_session_running())
xr_input_sync_and_draw(g_local_space, g_predicted_display_time, xr_input_sync_and_draw(g_local_space, g_predicted_display_time,
g_have_acquired_game_image); g_have_acquired_game_image);
@@ -374,8 +373,8 @@ void xr_frame_end(void) {
// xr_overlay_render_and_build_layer()) any time before xrEndFrame, // xr_overlay_render_and_build_layer()) any time before xrEndFrame,
// unlike the game quad there's no per-frame engine rendering to wrap // unlike the game quad there's no per-frame engine rendering to wrap
// around here, just each overlay's own blit - and, per hand, a // around here, just each overlay's own blit - and, per hand, a
// laser-beam quad while that hand's ray is aimed at one of those two // laser-beam quad while that hand's ray is aimed at the keyboard,
// overlays (see xr_input_get_beam_layer()). // menu, or the game quad itself (see xr_input_get_beam_layer()).
XrCompositionLayerQuad gameQuad = {XR_TYPE_COMPOSITION_LAYER_QUAD}; XrCompositionLayerQuad gameQuad = {XR_TYPE_COMPOSITION_LAYER_QUAD};
gameQuad.space = g_local_space; gameQuad.space = g_local_space;
gameQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH; gameQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
@@ -470,3 +469,8 @@ void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half
*half_height_m = *half_height_m =
QUAD_WIDTH_METERS * 0.5f * (float)g_game_swapchain.height / (float)g_game_swapchain.width; QUAD_WIDTH_METERS * 0.5f * (float)g_game_swapchain.height / (float)g_game_swapchain.width;
} }
void xr_get_game_resolution(int *width, int *height) {
*width = g_game_swapchain.width;
*height = g_game_swapchain.height;
}
+7
View File
@@ -74,6 +74,13 @@ void xr_shutdown(void);
// laser pointer always matches whatever's actually visible. // laser pointer always matches whatever's actually visible.
void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m); void xr_get_game_quad_extent(float *distance_m, float *half_width_m, float *half_height_m);
// Pixel resolution of the game quad's own swapchain - i.e. game_width/
// game_height as passed to xr_init() (grd_cap->w/h, see Shock.c's
// InitSDL()) - the logical coordinate space xr_input.c needs to convert a
// game-quad ray hit's normalized u,v into engine mouse coordinates (see
// xr_mouse.h).
void xr_get_game_resolution(int *width, int *height);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
+1 -2
View File
@@ -146,8 +146,7 @@ bool xr_swapchain_acquire(XrInstance instance, XrSwapchainState *state) {
// gl4es's own (linked, not dlsym'd) bind - this targets an FBO id gl4es // gl4es's own (linked, not dlsym'd) bind - this targets an FBO id gl4es
// itself created (see xr_swapchain_create()), so its own "current FBO" // itself created (see xr_swapchain_create()), so its own "current FBO"
// bookkeeping updates correctly and its immediate-mode draw calls // bookkeeping updates correctly and its immediate-mode draw calls
// (android_draw_surface_as_quad(), the laser reticle) land in the right // (android_draw_surface_as_quad()) land in the right place.
// place.
glBindFramebuffer(GL_FRAMEBUFFER, state->fbos[imageIndex]); glBindFramebuffer(GL_FRAMEBUFFER, state->fbos[imageIndex]);
glViewport(0, 0, state->width, state->height); glViewport(0, 0, state->width, state->height);
return true; return true;