3 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
ml 920842a465 Show a laser-beam pointer while aiming at the menu/keyboard
build / build (push) Successful in 1m40s
The flat cross-shaped reticle used while aiming at the game quad is
drawn directly into the game quad's own texture, so it only ever made
sense there - while the menu launcher or keyboard was open, there was
no visual feedback at all until the ray was precisely on target,
making both panels hard to aim at.

Add a second, thin XrCompositionLayerQuad per hand: a billboarded
"laser beam" ribbon from the controller to wherever that hand's ray
currently crosses the plane of whichever overlay (keyboard, then menu)
claims it that frame, oriented via a view-space head pose so it reads
as a line from the viewer's eye regardless of angle, colored per hand
(cyan left, amber right) and clamped to 2m so grazing angles don't
produce an absurdly long beam. Built from data xr_input.c's existing
per-hand hit-testing already computes; the game-quad reticle itself is
untouched.
2026-08-14 07:30:13 +02:00
9 changed files with 533 additions and 160 deletions
+9 -4
View File
@@ -184,10 +184,15 @@ set only on the keyboard's layer - the menu launcher stays fully opaque)
so whatever's behind it - the game quad, mid-play - stays visible while so whatever's behind it - the game quad, mid-play - stays visible while
it's up. it's up.
The laser While the menu launcher or keyboard is open, each hand also gets a thin,
pointer/cursor is currently only visible while colored laser-beam quad (cyan left, amber right) from the controller to
actually aiming at the game or menu quad respectively - there's no visual wherever its ray currently crosses that panel's plane - a second,
feedback yet while aiming at empty space between them. (Only tested on billboarded `XrCompositionLayerQuad` per hand (`xr_input_build_beam()` in
`xr_input.c`), oriented so it reads as a line from the viewer's eye
regardless of angle, since the flat cross-shaped reticle used while
aiming at the game quad is drawn directly into the game quad's own
texture and can't represent a ray traversing real 3D space. Aiming at the
game quad itself still uses that flat reticle, unchanged. (Only tested on
Meta Quest so far - Meta Quest so far -
the steps below use Quest-specific tool names where relevant, but the the steps below use Quest-specific tool names where relevant, but the
same `adb install` flow applies to any Android headset with USB same `adb install` flow applies to any Android headset with USB
+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'
} }
} }
+375 -115
View File
@@ -7,8 +7,10 @@
#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"
#define TAG "QuestShock" #define TAG "QuestShock"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
@@ -52,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};
@@ -69,8 +72,28 @@ 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; // A "view" reference space, located once per frame (not per hand) to get
static GLint g_reticle_color_loc = -1; // 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,
// owned by xr_session.c) and VIEW reference spaces are mandated by core
// OpenXR, so no capability check is needed to create this.
static XrSpace g_view_space = XR_NULL_HANDLE;
// Per-hand laser-beam state: a thin, billboarded quad spanning from the
// controller to wherever that hand's ray currently crosses the plane of
// whichever target (keyboard, menu, or the game quad) claimed it this
// frame - see xr_input_build_beam()/xr_input_get_beam_layer(). Each hand
// gets its own tiny solid-color swapchain (no shared tint on XrCompositionLayerQuad,
// 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
// only set back to true if that hand actually claims a beam this frame.
#define BEAM_TEX_SIZE 2
#define BEAM_THICKNESS_METERS 0.004f
#define MAX_BEAM_LENGTH_METERS 2.0f
static XrSwapchainState g_beam_swapchain[2];
static XrCompositionLayerQuad g_beam_quad[2];
static bool g_beam_quad_valid[2] = {false, false};
static bool xr_check(XrResult result, const char *what) { static bool xr_check(XrResult result, const char *what) {
if (XR_SUCCEEDED(result)) if (XR_SUCCEEDED(result))
@@ -95,64 +118,71 @@ static void quat_rotate_vec(const XrQuaternionf *q, float vx, float vy, float vz
*outz = vz + 2.0f * (q->x * cy - q->y * cx); *outz = vz + 2.0f * (q->x * cy - q->y * cx);
} }
static GLuint compile_shader(GLenum type, const char *src) { static void vec3_sub(const float a[3], const float b[3], float out[3]) {
GLuint shader = glCreateShader(type); out[0] = a[0] - b[0];
glShaderSource(shader, 1, &src, NULL); out[1] = a[1] - b[1];
glCompileShader(shader); out[2] = a[2] - b[2];
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 static float vec3_dot(const float a[3], const float b[3]) {
// textureShaderProgram (OpenGL.cc is a separate, C++-only translation return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
// 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 static void vec3_cross(const float a[3], const float b[3], float out[3]) {
// reuse the same numeric index the engine's own immediate-mode drawing out[0] = a[1] * b[2] - a[2] * b[1];
// treats specially, since that's a per-program binding and this program is out[1] = a[2] * b[0] - a[0] * b[2];
// never current at the same time as gl4es's immediate-mode emulation runs; out[2] = a[0] * b[1] - a[1] * b[0];
// 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) { // Returns false (out left untouched) if v is too close to zero-length to
GLuint vs = compile_shader(GL_VERTEX_SHADER, kVertexSrc); // normalize safely - callers use this to detect a degenerate billboard
GLuint fs = compile_shader(GL_FRAGMENT_SHADER, kFragmentSrc); // axis and fall back to another reference vector.
g_reticle_program = glCreateProgram(); static bool vec3_normalize(const float v[3], float out[3]) {
glAttachShader(g_reticle_program, vs); float len = sqrtf(vec3_dot(v, v));
glAttachShader(g_reticle_program, fs); if (len < 1e-6f)
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; return false;
} out[0] = v[0] / len;
g_reticle_color_loc = glGetUniformLocation(g_reticle_program, "color"); out[1] = v[1] / len;
out[2] = v[2] / len;
return true; return true;
} }
bool xr_input_init(XrInstance instance, XrSession session) { // Converts an orthonormal local->world rotation matrix (given as its
// columns - x/y/z, each the world-space direction of that local axis) to
// an XrQuaternionf, via the standard trace-based (Shepperd) method.
static void mat3_to_quat(const float x[3], const float y[3], const float z[3], XrQuaternionf *q) {
float m00 = x[0], m10 = x[1], m20 = x[2];
float m01 = y[0], m11 = y[1], m21 = y[2];
float m02 = z[0], m12 = z[1], m22 = z[2];
float trace = m00 + m11 + m22;
if (trace > 0.0f) {
float s = sqrtf(trace + 1.0f) * 2.0f;
q->w = 0.25f * s;
q->x = (m21 - m12) / s;
q->y = (m02 - m20) / s;
q->z = (m10 - m01) / s;
} else if (m00 > m11 && m00 > m22) {
float s = sqrtf(1.0f + m00 - m11 - m22) * 2.0f;
q->w = (m21 - m12) / s;
q->x = 0.25f * s;
q->y = (m01 + m10) / s;
q->z = (m02 + m20) / s;
} else if (m11 > m22) {
float s = sqrtf(1.0f + m11 - m00 - m22) * 2.0f;
q->w = (m02 - m20) / s;
q->x = (m01 + m10) / s;
q->y = 0.25f * s;
q->z = (m12 + m21) / s;
} else {
float s = sqrtf(1.0f + m22 - m00 - m11) * 2.0f;
q->w = (m10 - m01) / s;
q->x = (m02 + m20) / s;
q->y = (m12 + m21) / s;
q->z = 0.25f * s;
}
}
bool xr_input_init(XrInstance instance, XrSession session, int64_t swapchain_format) {
g_instance = instance; g_instance = instance;
g_session = session; g_session = session;
@@ -246,9 +276,21 @@ bool xr_input_init(XrInstance instance, XrSession session) {
if (!xr_check(xrAttachSessionActionSets(session, &attachInfo), "xrAttachSessionActionSets")) if (!xr_check(xrAttachSessionActionSets(session, &attachInfo), "xrAttachSessionActionSets"))
return false; return false;
if (!xr_input_init_reticle_program()) XrReferenceSpaceCreateInfo viewSpaceInfo = {XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
viewSpaceInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW;
viewSpaceInfo.poseInReferenceSpace.orientation.w = 1.0f;
if (!xr_check(xrCreateReferenceSpace(session, &viewSpaceInfo, &g_view_space),
"xrCreateReferenceSpace(view)"))
return false; return false;
for (int hand = 0; hand < 2; hand++) {
if (!xr_swapchain_create(instance, session, swapchain_format, BEAM_TEX_SIZE, BEAM_TEX_SIZE,
&g_beam_swapchain[hand])) {
LOGE("XR: beam swapchain setup failed for hand %d", hand);
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;
} }
@@ -258,18 +300,31 @@ bool xr_input_init(XrInstance instance, XrSession session) {
// 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,
bool *prevHit, const char *quadName, float *outCursorU, bool *prevHit, const char *quadName, float *outCursorU,
float *outCursorV, bool *outCursorHit) { float *outCursorV, bool *outCursorHit, bool *outPlaneHit,
float *outPlaneDistance) {
*outPlaneHit = false;
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);
@@ -283,9 +338,9 @@ static bool xr_input_try_overlay(XrOverlay *overlay, bool dragCapable, int hand,
// view's pixel grid and the on-quad hit logs use) are only meaningful // view's pixel grid and the on-quad hit logs use) are only meaningful
// within the quad's current bounds, unlike planeHit. // within the quad's current bounds, unlike planeHit.
bool planeHit = false, hit = false; bool planeHit = false, hit = false;
float u = 0.0f, v = 0.0f, worldX = 0.0f, worldY = 0.0f; float u = 0.0f, v = 0.0f, worldX = 0.0f, worldY = 0.0f, t = 0.0f;
if (fabsf(fz) > 1e-5f) { if (fabsf(fz) > 1e-5f) {
float t = (-distance - location->pose.position.z) / fz; t = (-distance - location->pose.position.z) / fz;
if (t > 0.0f) { if (t > 0.0f) {
planeHit = true; planeHit = true;
worldX = location->pose.position.x + t * fx; worldX = location->pose.position.x + t * fx;
@@ -297,6 +352,8 @@ static bool xr_input_try_overlay(XrOverlay *overlay, bool dragCapable, int hand,
v = (1.0f - cy) * 0.5f; v = (1.0f - cy) * 0.5f;
} }
} }
*outPlaneHit = planeHit;
*outPlaneDistance = t;
if (hit != *prevHit) { if (hit != *prevHit) {
LOGI("XR: %s aim ray %s %s quad (u=%.2f v=%.2f)", kHandName[hand], LOGI("XR: %s aim ray %s %s quad (u=%.2f v=%.2f)", kHandName[hand],
@@ -309,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);
@@ -317,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;
@@ -350,7 +413,157 @@ 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
// controller (location->pose.position) along the aim ray (fx,fy,fz,
// already unit length) to length_m meters out, oriented so it reads as a
// line from the viewer's eye (headLoc->pose.position) regardless of angle:
// one in-plane axis follows the ray direction, the other is the thin
// "width", and the quad's normal is billboarded toward the eye
// (orthogonalized against the ray axis, so the ribbon only rotates around
// its own long axis as the hand moves, never twists). Acquires/clears/
// releases this hand's tiny beam swapchain and leaves g_beam_quad[hand]
// ready for xr_input_get_beam_layer() - doesn't set space/eyeVisibility/
// layerFlags (see that function's doc comment in xr_input.h).
static void xr_input_build_beam(int hand, const XrSpaceLocation *location, float fx, float fy,
float fz, float length_m, const XrSpaceLocation *headLoc) {
float origin[3] = {location->pose.position.x, location->pose.position.y,
location->pose.position.z};
float dirY[3] = {fx, fy, fz};
float endpoint[3] = {origin[0] + length_m * fx, origin[1] + length_m * fy,
origin[2] + length_m * fz};
float center[3] = {(origin[0] + endpoint[0]) * 0.5f, (origin[1] + endpoint[1]) * 0.5f,
(origin[2] + endpoint[2]) * 0.5f};
float head[3] = {headLoc->pose.position.x, headLoc->pose.position.y,
headLoc->pose.position.z};
float toEyeRaw[3], toEye[3];
vec3_sub(head, center, toEyeRaw);
if (!vec3_normalize(toEyeRaw, toEye)) {
// Head is (almost) exactly at the beam's midpoint - astronomically
// unlikely, but fall back to a fixed direction rather than divide
// by ~0.
toEye[0] = 0.0f;
toEye[1] = 0.0f;
toEye[2] = 1.0f;
}
// Orthogonalize toEye against the ray axis to get the billboard
// normal - if the ray points almost straight at/away from the eye,
// that leaves ~nothing to normalize, so fall back to world-up then
// world-Z, each reprojected the same way.
float normalZ[3];
float d = vec3_dot(toEye, dirY);
float proj[3] = {toEye[0] - dirY[0] * d, toEye[1] - dirY[1] * d, toEye[2] - dirY[2] * d};
if (!vec3_normalize(proj, normalZ)) {
static const float kWorldUp[3] = {0.0f, 1.0f, 0.0f};
d = vec3_dot(kWorldUp, dirY);
proj[0] = kWorldUp[0] - dirY[0] * d;
proj[1] = kWorldUp[1] - dirY[1] * d;
proj[2] = kWorldUp[2] - dirY[2] * d;
if (!vec3_normalize(proj, normalZ)) {
static const float kWorldZ[3] = {0.0f, 0.0f, 1.0f};
d = vec3_dot(kWorldZ, dirY);
proj[0] = kWorldZ[0] - dirY[0] * d;
proj[1] = kWorldZ[1] - dirY[1] * d;
proj[2] = kWorldZ[2] - dirY[2] * d;
if (!vec3_normalize(proj, normalZ)) {
// dirY parallel to both world-up and world-Z is impossible
// for two non-parallel vectors - unreachable in practice,
// but keep the beam well-defined regardless.
normalZ[0] = 1.0f;
normalZ[1] = 0.0f;
normalZ[2] = 0.0f;
}
}
}
float axisX[3];
vec3_cross(dirY, normalZ, axisX);
if (!vec3_normalize(axisX, axisX)) {
axisX[0] = 1.0f;
axisX[1] = 0.0f;
axisX[2] = 0.0f;
}
float axisZ[3];
vec3_cross(axisX, dirY, axisZ); // already unit length - axisX/dirY are orthonormal
XrQuaternionf orientation;
mat3_to_quat(axisX, dirY, axisZ, &orientation);
if (xr_swapchain_acquire(g_instance, &g_beam_swapchain[hand])) {
// Premultiplied alpha (matches OverlayPanel.compositeAndPublish()'s
// convention, and XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT's
// assumption - see xr_session.c) - kHandColor's RGB scaled by this
// beam's own alpha, not the opaque RGB itself.
const float alpha = 0.55f;
const float *c = kHandColor[hand];
glClearColor(c[0] * alpha, c[1] * alpha, c[2] * alpha, alpha);
glClear(GL_COLOR_BUFFER_BIT);
xr_swapchain_release(g_instance, &g_beam_swapchain[hand]);
}
XrCompositionLayerQuad *quad = &g_beam_quad[hand];
quad->type = XR_TYPE_COMPOSITION_LAYER_QUAD;
quad->next = NULL;
quad->subImage.swapchain = g_beam_swapchain[hand].swapchain;
quad->subImage.imageRect.offset.x = 0;
quad->subImage.imageRect.offset.y = 0;
quad->subImage.imageRect.extent.width = BEAM_TEX_SIZE;
quad->subImage.imageRect.extent.height = BEAM_TEX_SIZE;
quad->subImage.imageArrayIndex = 0;
quad->pose.position.x = center[0];
quad->pose.position.y = center[1];
quad->pose.position.z = center[2];
quad->pose.orientation = orientation;
quad->size.width = BEAM_THICKNESS_METERS;
quad->size.height = length_m;
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) {
@@ -375,16 +588,26 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
g_keyboard_drag_hand = -1; g_keyboard_drag_hand = -1;
} }
// The GL reticle is drawn directly into whatever framebuffer is // A hand only ever gets a laser-beam quad this frame if it actually
// currently bound - the game quad's swapchain image (see xr_frame_end(), // claims the keyboard or menu overlay's ray below (see the
// which calls this while that image is still bound). That only makes // xr_input_build_beam() calls) - reset here so a hand that doesn't
// sense while aiming at the game quad; the menu/keyboard overlays' own // claim one this frame doesn't keep showing last frame's beam.
// cursors are drawn by their Java views instead (see g_beam_quad_valid[LEFT] = g_beam_quad_valid[RIGHT] = false;
// xr_overlay_update_cursor() below), composited into their Bitmaps the
// same way their other content is. // The billboard math needs an approximate head position - only bother
bool drawReticle = draw && !menuVisible && !keyboardVisible; // locating it on frames where a beam could possibly be drawn at all
if (drawReticle) // (i.e. any drawn frame - the game quad can claim a beam on its own
glUseProgram(g_reticle_program); // even with both overlays closed).
bool needBeams = draw;
XrSpaceLocation headLoc = {XR_TYPE_SPACE_LOCATION};
bool haveHead = false;
if (needBeams) {
const XrSpaceLocationFlags neededHead =
XR_SPACE_LOCATION_POSITION_VALID_BIT | XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
haveHead = xr_check(xrLocateSpace(g_view_space, baseSpace, time, &headLoc),
"xrLocateSpace(view)") &&
(headLoc.locationFlags & neededHead) == neededHead;
}
// 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
@@ -452,53 +675,81 @@ 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. // xr_input_try_overlay()'s doc comment) - the game quad itself.
if (xr_input_try_overlay(keyboardOverlay, true, hand, &location, fx, fy, fz, // 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(). 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 (!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)) &keyboardCursorV, &keyboardCursorHit, &keyboardPlaneHit,
&keyboardPlaneDistance)) {
if (haveHead && keyboardPlaneHit)
xr_input_build_beam(hand, &location, fx, fy, fz,
fminf(keyboardPlaneDistance, MAX_BEAM_LENGTH_METERS), &headLoc);
continue; continue;
if (xr_input_try_overlay(menuOverlay, false, hand, &location, fx, fy, fz, selectDownEdge, }
bool menuPlaneHit = false;
float menuPlaneDistance = 0.0f;
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)) "menu", &menuCursorU, &menuCursorV, &menuCursorHit, &menuPlaneHit,
&menuPlaneDistance)) {
if (haveHead && menuPlaneHit)
xr_input_build_beam(hand, &location, fx, fy, fz,
fminf(menuPlaneDistance, MAX_BEAM_LENGTH_METERS), &headLoc);
continue; continue;
float distance, halfWidth, halfHeight;
xr_get_game_quad_extent(&distance, &halfWidth, &halfHeight);
bool hit = false;
float u = 0.0f, v = 0.0f, cx = 0.0f, cy = 0.0f;
if (fabsf(fz) > 1e-5f) {
float t = (-distance - location.pose.position.z) / fz;
if (t > 0.0f) {
cx = (location.pose.position.x + t * fx) / halfWidth;
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;
}
} }
if (hit != g_game_prev_hit[hand]) { // Lowest priority (for a fresh hit): the game quad itself - drives
LOGI("XR: %s aim ray %s game quad (u=%.2f v=%.2f)", kHandName[hand], // the engine's own mouse cursor/clicks via xr_mouse.h, exactly
hit ? "entered" : "left", u, v); // like a desktop mouse (absolute position + left button), plus the
g_game_prev_hit[hand] = hit; // same laser-beam visual the other two targets get.
bool gamePlaneHit = false;
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) {
if (!hit || !drawReticle) xr_mouse_click(true);
continue; g_game_touch_active[hand] = true;
} else if (selectUpEdge && g_game_touch_active[hand]) {
const float kSize = 0.03f; xr_mouse_click(false);
const float verts[] = { g_game_touch_active[hand] = false;
cx - kSize, cy, 0.0f, cx + kSize, cy, 0.0f, cx, cy - kSize, 0.0f, cx, cy + kSize, 0.0f, }
}; // gameHit implies gamePlaneHit (both only ever set together above),
glUniform4fv(g_reticle_color_loc, 1, kHandColor[hand]); // kept as a separate out-param for symmetry with
glEnableVertexAttribArray(RETICLE_POSITION_LOC); // xr_input_try_overlay()'s outPlaneHit/outPlaneDistance pair.
glVertexAttribPointer(RETICLE_POSITION_LOC, 3, GL_FLOAT, GL_FALSE, 0, verts); if (haveHead && gameHit)
glDrawArrays(GL_LINES, 0, 4); xr_input_build_beam(hand, &location, fx, fy, fz,
glDisableVertexAttribArray(RETICLE_POSITION_LOC); fminf(gamePlaneDistance, MAX_BEAM_LENGTH_METERS), &headLoc);
} }
if (menuVisible) if (menuVisible)
@@ -508,12 +759,23 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
keyboardCursorHit); keyboardCursorHit);
} }
bool xr_input_get_beam_layer(int hand, XrCompositionLayerQuad *out_quad) {
if (hand < 0 || hand > 1 || !g_beam_quad_valid[hand])
return false;
*out_quad = g_beam_quad[hand];
return true;
}
void xr_input_shutdown(void) { void xr_input_shutdown(void) {
for (int hand = 0; hand < 2; hand++) { for (int hand = 0; hand < 2; hand++) {
if (g_aim_space[hand] != XR_NULL_HANDLE) if (g_aim_space[hand] != XR_NULL_HANDLE)
xrDestroySpace(g_aim_space[hand]); xrDestroySpace(g_aim_space[hand]);
g_aim_space[hand] = XR_NULL_HANDLE; g_aim_space[hand] = XR_NULL_HANDLE;
xr_swapchain_destroy(&g_beam_swapchain[hand]);
} }
if (g_view_space != XR_NULL_HANDLE)
xrDestroySpace(g_view_space);
g_view_space = XR_NULL_HANDLE;
if (g_action_set != XR_NULL_HANDLE) if (g_action_set != XR_NULL_HANDLE)
xrDestroyActionSet(g_action_set); xrDestroyActionSet(g_action_set);
g_action_set = XR_NULL_HANDLE; g_action_set = XR_NULL_HANDLE;
@@ -521,18 +783,16 @@ 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));
memset(g_keyboard_touch_active, 0, sizeof(g_keyboard_touch_active)); memset(g_keyboard_touch_active, 0, sizeof(g_keyboard_touch_active));
g_beam_quad_valid[LEFT] = g_beam_quad_valid[RIGHT] = false;
g_prev_menu = false; g_prev_menu = false;
g_keyboard_dragging = false; g_keyboard_dragging = false;
g_keyboard_drag_hand = -1; g_keyboard_drag_hand = -1;
+43 -17
View File
@@ -1,17 +1,30 @@
// 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) and forwards trigger edges as synthetic touches // - the game quad xr_session.c submits, forwarding trigger edges to
// (no reticle - each overlay's own content comes from its Java view's // whichever overlay claims the ray as synthetic touches. The game quad
// rendered Bitmap). // 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
// the ray currently crosses that target's plane (see
// 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
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h>
#define XR_USE_PLATFORM_ANDROID 1 #define XR_USE_PLATFORM_ANDROID 1
#define XR_USE_GRAPHICS_API_OPENGL_ES 1 #define XR_USE_GRAPHICS_API_OPENGL_ES 1
@@ -24,22 +37,35 @@ extern "C" {
// Call once, right after the session is created (see xr_session.c's // Call once, right after the session is created (see xr_session.c's
// xr_create_instance_and_session()) - creates the action set/actions, // xr_create_instance_and_session()) - creates the action set/actions,
// suggests Touch controller bindings, creates the per-hand aim action // suggests Touch controller bindings, creates the per-hand aim action
// spaces, and attaches the set to the session. Returns false (logged, // spaces and a view-space reference (for the laser-beam billboard math),
// non-fatal - the caller keeps rendering without input) if any of that // creates the two per-hand beam swapchains (swapchain_format - the same
// fails. // format shared by the game swapchain and both overlays), and attaches
bool xr_input_init(XrInstance instance, XrSession session); // the action set to the session. Returns false (logged, non-fatal - the
// caller keeps rendering without input) if any of that fails.
bool xr_input_init(XrInstance instance, XrSession session, int64_t swapchain_format);
// 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()
// in the same frame - if that hand's ray claimed the keyboard, menu, or
// game quad this frame (see the file comment above), fills *out_quad's
// subImage/pose/size for its laser-beam ribbon and returns true.
// space/eyeVisibility/layerFlags are left for the caller to set, same
// convention as xr_overlay_render_and_build_layer(). Returns false
// (out_quad untouched) if no beam should be shown for that hand this
// frame.
bool xr_input_get_beam_layer(int hand, XrCompositionLayerQuad *out_quad);
void xr_input_shutdown(void); void xr_input_shutdown(void);
#ifdef __cplusplus #ifdef __cplusplus
+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
+42 -21
View File
@@ -213,19 +213,16 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
"xrCreateReferenceSpace")) "xrCreateReferenceSpace"))
return false; return false;
// Non-fatal if it fails (e.g. no controllers bound yet) - rendering
// keeps working either way, just without the laser pointer.
xr_input_init(g_instance, g_session);
// Prefer a plain linear 8-bit format over GL_SRGB8_ALPHA8: the source // Prefer a plain linear 8-bit format over GL_SRGB8_ALPHA8: the source
// SDL surface pixels are already sRGB-encoded (as ordinary 8-bit image // SDL surface pixels are already sRGB-encoded (as ordinary 8-bit image
// data conventionally is) and get uploaded/sampled as plain linear // data conventionally is) and get uploaded/sampled as plain linear
// GL_RGBA with no decode step anywhere in this path, matching the // GL_RGBA with no decode step anywhere in this path, matching the
// desktop SDL_RenderCopy path this replaces - an sRGB swapchain format // desktop SDL_RenderCopy path this replaces - an sRGB swapchain format
// would auto-gamma-encode on write and double-encode already-encoded // would auto-gamma-encode on write and double-encode already-encoded
// data. Every swapchain below (game quad, menu/keyboard overlays) // data. Every swapchain below (game quad, menu/keyboard overlays, and
// shares this one choice - the compositor's supported format set // - via xr_input_init() - the per-hand laser-beam overlays) shares
// doesn't depend on swapchain size. // this one choice - the compositor's supported format set doesn't
// depend on swapchain size.
uint32_t formatCount = 0; uint32_t formatCount = 0;
xrEnumerateSwapchainFormats(g_session, 0, &formatCount, NULL); xrEnumerateSwapchainFormats(g_session, 0, &formatCount, NULL);
int64_t *formats = (int64_t *)malloc(sizeof(int64_t) * formatCount); int64_t *formats = (int64_t *)malloc(sizeof(int64_t) * formatCount);
@@ -240,6 +237,10 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
} }
free(formats); free(formats);
// Non-fatal if it fails (e.g. no controllers bound yet) - rendering
// keeps working either way, just without the laser pointer/beams.
xr_input_init(g_instance, g_session, chosenFormat);
if (!xr_swapchain_create(g_instance, g_session, chosenFormat, game_width, game_height, if (!xr_swapchain_create(g_instance, g_session, chosenFormat, game_width, game_height,
&g_game_swapchain)) { &g_game_swapchain)) {
LOGE("XR: game swapchain setup failed"); LOGE("XR: game swapchain setup failed");
@@ -349,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);
@@ -367,12 +367,14 @@ void xr_frame_end(void) {
if (!xr_is_session_running()) if (!xr_is_session_running())
return; return;
// Up to 3 layers: the game quad (if a frame was actually rendered), // Up to 5 layers: the game quad (if a frame was actually rendered),
// and, while visible, the menu launcher and keyboard overlays - each // the menu launcher and keyboard overlays while visible - each gets
// gets its own acquire/render/release cycle against its own swapchain // its own acquire/render/release cycle against its own swapchain (see
// (see 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. // around here, just each overlay's own blit - and, per hand, a
// laser-beam quad while that hand's ray is aimed at the keyboard,
// 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;
@@ -385,7 +387,7 @@ void xr_frame_end(void) {
gameQuad.size.height = gameQuad.size.height =
QUAD_WIDTH_METERS * (float)g_game_swapchain.height / (float)g_game_swapchain.width; QUAD_WIDTH_METERS * (float)g_game_swapchain.height / (float)g_game_swapchain.width;
const XrCompositionLayerBaseHeader *layers[3]; const XrCompositionLayerBaseHeader *layers[5];
uint32_t layerCount = 0; uint32_t layerCount = 0;
if (g_have_acquired_game_image) if (g_have_acquired_game_image)
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&gameQuad; layers[layerCount++] = (XrCompositionLayerBaseHeader *)&gameQuad;
@@ -413,6 +415,20 @@ void xr_frame_end(void) {
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&keyboardQuad; layers[layerCount++] = (XrCompositionLayerBaseHeader *)&keyboardQuad;
} }
// Submitted last (frontmost) so a beam doesn't z-fight against the
// panel surface it's aimed at/terminates on - see
// xr_input_get_beam_layer(). Translucent for the same reason the
// keyboard is (see comment above).
XrCompositionLayerQuad beamQuad[2];
for (int hand = 0; hand < 2; hand++) {
if (xr_input_get_beam_layer(hand, &beamQuad[hand])) {
beamQuad[hand].space = g_local_space;
beamQuad[hand].eyeVisibility = XR_EYE_VISIBILITY_BOTH;
beamQuad[hand].layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT;
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&beamQuad[hand];
}
}
XrFrameEndInfo endInfo = {XR_TYPE_FRAME_END_INFO}; XrFrameEndInfo endInfo = {XR_TYPE_FRAME_END_INFO};
endInfo.displayTime = g_predicted_display_time; endInfo.displayTime = g_predicted_display_time;
endInfo.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; endInfo.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
@@ -453,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;