Show a laser-beam pointer while aiming at the menu/keyboard
build / build (push) Successful in 1m40s
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.
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "xr_overlay.h"
|
||||
#include "xr_session.h"
|
||||
#include "xr_swapchain.h"
|
||||
|
||||
#define TAG "QuestShock"
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
|
||||
@@ -72,6 +73,29 @@ 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
|
||||
// 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 overlay (keyboard/menu) 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) {
|
||||
if (XR_SUCCEEDED(result))
|
||||
return true;
|
||||
@@ -95,6 +119,70 @@ static void quat_rotate_vec(const XrQuaternionf *q, float vx, float vy, float vz
|
||||
*outz = vz + 2.0f * (q->x * cy - q->y * cx);
|
||||
}
|
||||
|
||||
static void vec3_sub(const float a[3], const float b[3], float out[3]) {
|
||||
out[0] = a[0] - b[0];
|
||||
out[1] = a[1] - b[1];
|
||||
out[2] = a[2] - b[2];
|
||||
}
|
||||
|
||||
static float vec3_dot(const float a[3], const float b[3]) {
|
||||
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
||||
}
|
||||
|
||||
static void vec3_cross(const float a[3], const float b[3], float out[3]) {
|
||||
out[0] = a[1] * b[2] - a[2] * b[1];
|
||||
out[1] = a[2] * b[0] - a[0] * b[2];
|
||||
out[2] = a[0] * b[1] - a[1] * b[0];
|
||||
}
|
||||
|
||||
// Returns false (out left untouched) if v is too close to zero-length to
|
||||
// normalize safely - callers use this to detect a degenerate billboard
|
||||
// axis and fall back to another reference vector.
|
||||
static bool vec3_normalize(const float v[3], float out[3]) {
|
||||
float len = sqrtf(vec3_dot(v, v));
|
||||
if (len < 1e-6f)
|
||||
return false;
|
||||
out[0] = v[0] / len;
|
||||
out[1] = v[1] / len;
|
||||
out[2] = v[2] / len;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
static GLuint compile_shader(GLenum type, const char *src) {
|
||||
GLuint shader = glCreateShader(type);
|
||||
glShaderSource(shader, 1, &src, NULL);
|
||||
@@ -152,7 +240,7 @@ static bool xr_input_init_reticle_program(void) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool xr_input_init(XrInstance instance, XrSession session) {
|
||||
bool xr_input_init(XrInstance instance, XrSession session, int64_t swapchain_format) {
|
||||
g_instance = instance;
|
||||
g_session = session;
|
||||
|
||||
@@ -246,6 +334,21 @@ bool xr_input_init(XrInstance instance, XrSession session) {
|
||||
if (!xr_check(xrAttachSessionActionSets(session, &attachInfo), "xrAttachSessionActionSets"))
|
||||
return false;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (!xr_input_init_reticle_program())
|
||||
return false;
|
||||
|
||||
@@ -266,7 +369,9 @@ static bool xr_input_try_overlay(XrOverlay *overlay, bool dragCapable, int hand,
|
||||
const XrSpaceLocation *location, float fx, float fy, float fz,
|
||||
bool selectDownEdge, bool selectUpEdge, bool *touchActive,
|
||||
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))
|
||||
return false;
|
||||
|
||||
@@ -283,9 +388,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
|
||||
// within the quad's current bounds, unlike planeHit.
|
||||
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) {
|
||||
float t = (-distance - location->pose.position.z) / fz;
|
||||
t = (-distance - location->pose.position.z) / fz;
|
||||
if (t > 0.0f) {
|
||||
planeHit = true;
|
||||
worldX = location->pose.position.x + t * fx;
|
||||
@@ -297,6 +402,8 @@ static bool xr_input_try_overlay(XrOverlay *overlay, bool dragCapable, int hand,
|
||||
v = (1.0f - cy) * 0.5f;
|
||||
}
|
||||
}
|
||||
*outPlaneHit = planeHit;
|
||||
*outPlaneDistance = t;
|
||||
|
||||
if (hit != *prevHit) {
|
||||
LOGI("XR: %s aim ray %s %s quad (u=%.2f v=%.2f)", kHandName[hand],
|
||||
@@ -353,6 +460,114 @@ static bool xr_input_try_overlay(XrOverlay *overlay, bool dragCapable, int hand,
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
|
||||
if (g_action_set == XR_NULL_HANDLE)
|
||||
return;
|
||||
@@ -375,6 +590,25 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
|
||||
g_keyboard_drag_hand = -1;
|
||||
}
|
||||
|
||||
// A hand only ever gets a laser-beam quad this frame if it actually
|
||||
// claims the keyboard or menu overlay's ray below (see the
|
||||
// xr_input_build_beam() calls) - reset here so a hand that doesn't
|
||||
// claim one this frame doesn't keep showing last frame's beam.
|
||||
g_beam_quad_valid[LEFT] = g_beam_quad_valid[RIGHT] = false;
|
||||
|
||||
// The billboard math needs an approximate head position - only bother
|
||||
// locating it on frames where a beam could possibly be drawn at all.
|
||||
bool needBeams = draw && (menuVisible || keyboardVisible);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -454,16 +688,33 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
|
||||
// Keyboard first (it's the more likely target while it's up), then
|
||||
// the menu launcher, then - only if neither is visible - the game
|
||||
// quad's own reticle below. A single ray only ever interacts with
|
||||
// one target per hand per frame.
|
||||
// one target per hand per frame. Whichever overlay claims the ray
|
||||
// also gets a laser-beam quad built for it (if a head pose is
|
||||
// available and the ray actually crosses that overlay's plane) -
|
||||
// see xr_input_build_beam().
|
||||
bool keyboardPlaneHit = false;
|
||||
float keyboardPlaneDistance = 0.0f;
|
||||
if (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))
|
||||
&keyboardCursorV, &keyboardCursorHit, &keyboardPlaneHit,
|
||||
&keyboardPlaneDistance)) {
|
||||
if (haveHead && keyboardPlaneHit)
|
||||
xr_input_build_beam(hand, &location, fx, fy, fz,
|
||||
fminf(keyboardPlaneDistance, MAX_BEAM_LENGTH_METERS), &headLoc);
|
||||
continue;
|
||||
}
|
||||
bool menuPlaneHit = false;
|
||||
float menuPlaneDistance = 0.0f;
|
||||
if (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))
|
||||
"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;
|
||||
}
|
||||
|
||||
float distance, halfWidth, halfHeight;
|
||||
xr_get_game_quad_extent(&distance, &halfWidth, &halfHeight);
|
||||
@@ -508,12 +759,23 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
|
||||
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) {
|
||||
for (int hand = 0; hand < 2; hand++) {
|
||||
if (g_aim_space[hand] != XR_NULL_HANDLE)
|
||||
xrDestroySpace(g_aim_space[hand]);
|
||||
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)
|
||||
xrDestroyActionSet(g_action_set);
|
||||
g_action_set = XR_NULL_HANDLE;
|
||||
@@ -533,6 +795,7 @@ void xr_input_shutdown(void) {
|
||||
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_touch_active, 0, sizeof(g_keyboard_touch_active));
|
||||
g_beam_quad_valid[LEFT] = g_beam_quad_valid[RIGHT] = false;
|
||||
g_prev_menu = false;
|
||||
g_keyboard_dragging = false;
|
||||
g_keyboard_drag_hand = -1;
|
||||
|
||||
Reference in New Issue
Block a user