Compare commits
3 Commits
v3.0.1
..
581af95e7b
| Author | SHA1 | Date | |
|---|---|---|---|
| 581af95e7b | |||
| 824b4a2506 | |||
| 920842a465 |
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user