Add VR locomotion: move/turn with thumbsticks while the menu is open
build / build (push) Successful in 2m25s
build / build (push) Successful in 2m25s
- Left thumbstick snap-turns, right thumbstick smooth-moves, letting the
player line themselves up with the fixed game quad ("virtual screen")
without needing physical room-scale space to do it in. Implemented via
a second OpenXR reference space (a "play space") derived from the real
LOCAL space by the inverse of the player's transform, so every existing
quad/hit-testing/rendering call site just needed g_local_space swapped
for it - no manual coordinate-transform math required elsewhere
- Generalized the ray/quad hit-testing in xr_input.c to a proper
oriented-plane intersection (needed once quads can rotate); verified it
degenerates to the old axis-aligned formula when unrotated, so the
keyboard and non-recentered menu are unaffected
- The menu launcher now re-centers in front of the player, correctly
oriented to face them, each time it's freshly reopened
- "Reset Position" (Main Menu tab) recenters using the player's current
real head pose - not a plain reset-to-origin, which only undid
thumbstick movement and ignored physical drift since the session
started. The same recenter runs once automatically at startup too, as a
fallback for when there's no complete saved position to use instead
- Position/orientation persists in questshock.config.json's new
player_position group (independent of savegames), including new
numeric-value support in the config file's hand-rolled JSON parser
(previously string-enums only)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#include "xr_session.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -65,6 +66,26 @@ static XrInstance g_instance = XR_NULL_HANDLE;
|
||||
static XrSystemId g_system_id = XR_NULL_SYSTEM_ID;
|
||||
static XrSession g_session = XR_NULL_HANDLE;
|
||||
static XrSpace g_local_space = XR_NULL_HANDLE;
|
||||
|
||||
// Everything this app submits/locates (game/menu/keyboard/beam quads, hand
|
||||
// and head poses) is expressed relative to g_play_space, not g_local_space
|
||||
// directly - it's g_local_space translated/rotated by the inverse of the
|
||||
// player's own virtual transform (see xr_session_set_player_transform()'s
|
||||
// own comment in xr_session.h and xr_session_rebuild_play_space() below),
|
||||
// so moving/turning the player moves/turns everything else relative to
|
||||
// them without touching any actual pose-building or hit-testing math
|
||||
// elsewhere in the app - it's all still "relative to the base space",
|
||||
// which just happens to no longer be g_local_space itself.
|
||||
static XrSpace g_play_space = XR_NULL_HANDLE;
|
||||
static float g_player_x = 0.0f;
|
||||
static float g_player_z = 0.0f;
|
||||
static float g_player_yaw_deg = 0.0f;
|
||||
// Set by xr_session_set_player_transform() (including before xr_init() has
|
||||
// even run - see its own comment), cleared once xr_session_rebuild_play_space()
|
||||
// actually rebuilds g_play_space from the current x/z/yaw_deg - checked
|
||||
// once per frame in xr_frame_end() so a stick held at rest costs nothing.
|
||||
static bool g_player_transform_dirty = false;
|
||||
|
||||
static XrSessionState g_session_state = XR_SESSION_STATE_UNKNOWN;
|
||||
// Distinct from g_session_state - true from a successful xrBeginSession
|
||||
// until xrEndSession/loss/exit. Many runtimes only advance the *state*
|
||||
@@ -256,6 +277,65 @@ static EGLConfig xr_get_current_egl_config(EGLDisplay display, EGLContext contex
|
||||
return config;
|
||||
}
|
||||
|
||||
#define QS_DEG_TO_RAD (3.14159265358979323846f / 180.0f)
|
||||
|
||||
// Destroys/recreates g_play_space from g_local_space, offset by the
|
||||
// inverse of the player's current virtual transform (g_player_x/z/yaw_deg) -
|
||||
// see xr_session_set_player_transform()'s comment in xr_session.h for why
|
||||
// this is the inverse rather than the transform itself. Given
|
||||
// V = Translate(x, 0, z) * Rotate_Y(yaw_deg) (the player's own transform,
|
||||
// "apparent pose in play space = V composed with real pose in local
|
||||
// space"), the new space's poseInReferenceSpace must be V's inverse:
|
||||
// orientation = Rotate_Y(-yaw_deg), position = Rotate_Y(-yaw_deg) * (-x, 0, -z).
|
||||
// Called once, right after g_local_space itself is created (picking up
|
||||
// whatever qs_config_load_startup() already seeded before xr_init() ever
|
||||
// ran), and once per frame from xr_frame_end() whenever
|
||||
// g_player_transform_dirty says the transform actually changed since the
|
||||
// last rebuild - a plain stick-at-rest frame costs nothing beyond that one
|
||||
// bool check. g_session must already exist.
|
||||
static void xr_session_rebuild_play_space(void) {
|
||||
if (g_play_space != XR_NULL_HANDLE) {
|
||||
xrDestroySpace(g_play_space);
|
||||
g_play_space = XR_NULL_HANDLE;
|
||||
}
|
||||
|
||||
float theta = g_player_yaw_deg * QS_DEG_TO_RAD;
|
||||
float half = -theta * 0.5f;
|
||||
float c = cosf(-theta), s = sinf(-theta);
|
||||
float vx = -g_player_x, vz = -g_player_z;
|
||||
|
||||
XrReferenceSpaceCreateInfo spaceInfo = {XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
|
||||
spaceInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL;
|
||||
spaceInfo.poseInReferenceSpace.orientation.x = 0.0f;
|
||||
spaceInfo.poseInReferenceSpace.orientation.y = sinf(half);
|
||||
spaceInfo.poseInReferenceSpace.orientation.z = 0.0f;
|
||||
spaceInfo.poseInReferenceSpace.orientation.w = cosf(half);
|
||||
spaceInfo.poseInReferenceSpace.position.x = vx * c + vz * s;
|
||||
spaceInfo.poseInReferenceSpace.position.y = 0.0f;
|
||||
spaceInfo.poseInReferenceSpace.position.z = -vx * s + vz * c;
|
||||
xr_check(xrCreateReferenceSpace(g_session, &spaceInfo, &g_play_space),
|
||||
"xrCreateReferenceSpace(play)");
|
||||
|
||||
g_player_transform_dirty = false;
|
||||
}
|
||||
|
||||
void xr_session_set_player_transform(float x, float z, float yaw_deg) {
|
||||
g_player_x = x;
|
||||
g_player_z = z;
|
||||
g_player_yaw_deg = yaw_deg;
|
||||
g_player_transform_dirty = true;
|
||||
}
|
||||
|
||||
void xr_session_get_player_transform(float *x, float *z, float *yaw_deg) {
|
||||
*x = g_player_x;
|
||||
*z = g_player_z;
|
||||
*yaw_deg = g_player_yaw_deg;
|
||||
}
|
||||
|
||||
XrSpace xr_session_get_local_space(void) { return g_local_space; }
|
||||
|
||||
XrTime xr_session_get_predicted_display_time(void) { return g_predicted_display_time; }
|
||||
|
||||
static bool xr_create_instance_and_session(int game_width, int game_height) {
|
||||
JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv();
|
||||
jobject activity = (jobject)SDL_AndroidGetActivity();
|
||||
@@ -342,6 +422,14 @@ static bool xr_create_instance_and_session(int game_width, int game_height) {
|
||||
"xrCreateReferenceSpace"))
|
||||
return false;
|
||||
|
||||
// Picks up whatever qs_config_load_startup() already stored into
|
||||
// g_player_x/z/yaw_deg via xr_session_set_player_transform() before
|
||||
// this function ever ran (that call is pure state, no XR API calls, so
|
||||
// it works fine even this early) - so a saved position is already in
|
||||
// effect from the very first frame, not just after the player first
|
||||
// moves a thumbstick.
|
||||
xr_session_rebuild_play_space();
|
||||
|
||||
// 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
|
||||
// data conventionally is) and get uploaded/sampled as plain linear
|
||||
@@ -546,6 +634,14 @@ void xr_frame_end(void) {
|
||||
if (g_session == XR_NULL_HANDLE)
|
||||
return;
|
||||
|
||||
// Rebuilt before xr_input_sync_and_draw() below so this same frame's
|
||||
// hit-testing and layer submission both see whatever the player's
|
||||
// thumbstick input (or a config-driven "Reset Position"/Load
|
||||
// Configuration) changed last frame - a no-op check on every frame
|
||||
// nothing moved.
|
||||
if (g_player_transform_dirty)
|
||||
xr_session_rebuild_play_space();
|
||||
|
||||
// Syncs actions and hit-tests/dispatches each hand's ray against
|
||||
// keyboard/menu/game quad (see xr_input.c) before the game swapchain
|
||||
// image is released below - this always runs, even when there's
|
||||
@@ -554,7 +650,7 @@ void xr_frame_end(void) {
|
||||
// laser-beam quads are submitted as their own composition layers
|
||||
// further down, not drawn into the game swapchain itself.
|
||||
if (xr_is_session_running())
|
||||
xr_input_sync_and_draw(g_local_space, g_predicted_display_time,
|
||||
xr_input_sync_and_draw(g_play_space, g_predicted_display_time,
|
||||
g_have_acquired_game_image);
|
||||
|
||||
if (g_have_acquired_game_image)
|
||||
@@ -572,7 +668,7 @@ void xr_frame_end(void) {
|
||||
// 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};
|
||||
gameQuad.space = g_local_space;
|
||||
gameQuad.space = g_play_space;
|
||||
gameQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||
gameQuad.subImage.swapchain = g_game_swapchain.swapchain;
|
||||
gameQuad.subImage.imageRect.extent.width = g_game_swapchain.width;
|
||||
@@ -590,7 +686,7 @@ void xr_frame_end(void) {
|
||||
|
||||
XrCompositionLayerQuad menuQuad;
|
||||
if (g_frame_should_render && xr_overlay_render_and_build_layer(g_menu_overlay, &menuQuad)) {
|
||||
menuQuad.space = g_local_space;
|
||||
menuQuad.space = g_play_space;
|
||||
menuQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&menuQuad;
|
||||
}
|
||||
@@ -598,7 +694,7 @@ void xr_frame_end(void) {
|
||||
XrCompositionLayerQuad keyboardQuad;
|
||||
if (g_frame_should_render &&
|
||||
xr_overlay_render_and_build_layer(g_keyboard_overlay, &keyboardQuad)) {
|
||||
keyboardQuad.space = g_local_space;
|
||||
keyboardQuad.space = g_play_space;
|
||||
keyboardQuad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||
// Lets the keyboard sit on screen translucently while playing
|
||||
// rather than fully hiding whatever's behind it - KeyboardOverlay
|
||||
@@ -618,7 +714,7 @@ void xr_frame_end(void) {
|
||||
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].space = g_play_space;
|
||||
beamQuad[hand].eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
||||
beamQuad[hand].layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT;
|
||||
layers[layerCount++] = (XrCompositionLayerBaseHeader *)&beamQuad[hand];
|
||||
@@ -643,6 +739,10 @@ void xr_shutdown(void) {
|
||||
xrDestroySpace(g_local_space);
|
||||
g_local_space = XR_NULL_HANDLE;
|
||||
|
||||
if (g_play_space != XR_NULL_HANDLE)
|
||||
xrDestroySpace(g_play_space);
|
||||
g_play_space = XR_NULL_HANDLE;
|
||||
|
||||
// Before the session/instance they were created from.
|
||||
xr_input_shutdown();
|
||||
xr_overlay_destroy(g_menu_overlay);
|
||||
|
||||
Reference in New Issue
Block a user