From 378f8bf8973fadedeffcebb7bd024e1b389eb845 Mon Sep 17 00:00:00 2001 From: Stefan Maier <145186748+Cesarus85@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:22:55 +0200 Subject: [PATCH] VR: the whole body, vr.body "full": torso, hips and legs under the arms The player's own biped is drawn in first person among the world's objects (it sorts against the world by depth; the first-person weapon and arms, drawn first into the stencil, stay over it), posed from a copy of its node matrices once a frame (port/linux/game/vr_body.c): turned to the torso's heading, the neck below and behind the eyes as the head turns about it, the hips down with the physical crouch and part of the way with a lean, the spine upright to the neck, the feet planted while standing and stepping after the body as it turns, the knees bent by the arms' two-bone IK. The head and the third-person arms are shrunk to nothing, the weapon and what the biped carries left out; the first-person arms hang from the body's shoulders (mirrored with the weapon left-handed). A passenger in an open vehicle sits as the seat's animation has it; at a wheel or a gun the body is drawn as before, in a closed seat none. Nothing of the game's own pose, hit boxes or network messages changes. The neck pivot, the planted feet and the leg's clamped reach follow HaloCE-Quest-VR (CC0), rebuilt for this port's skeleton and caches. vr.body gets "full" (VR options, Body: "Full body"); "arms" stays the default. vr.log_performance reports the body's pose. debug.vr_body shows the body flat (the emulator): from the eyes, looking down, or from the front, side or back, the head turning, crouching or leaning on its own. Checked in the emulator: the cyborg's 19 nodes found, the body posed standing, crouched, leaning and turning (feet stepping), 0.006 ms a pose; two-minute bot games on Blood Gulch with it on and off, no errors. Co-Authored-By: Claude Opus 5.5 --- port/linux/game/vr_arms.c | 4 +- port/linux/game/vr_arms.h | 6 + port/linux/game/vr_body.c | 1101 +++++++++++++++++++++++ port/linux/game/vr_body.h | 36 + port/linux/include/halo_vr.h | 15 +- port/linux/src/port_config.c | 15 +- port/linux/src/vr.c | 72 +- source/interface/first_person_weapons.c | 37 + source/interface/ui_widget.c | 8 +- source/render/render.c | 28 + source/render/render_objects.c | 21 +- 11 files changed, 1325 insertions(+), 18 deletions(-) create mode 100644 port/linux/game/vr_body.c create mode 100644 port/linux/game/vr_body.h diff --git a/port/linux/game/vr_arms.c b/port/linux/game/vr_arms.c index 5effc167..d76e5e77 100644 --- a/port/linux/game/vr_arms.c +++ b/port/linux/game/vr_arms.c @@ -115,7 +115,7 @@ static boolean vr_unit_vector( } /* the rotation taking direction a to direction b, the shortest way */ -static void vr_rotation_between( +void vr_rotation_between( real_vector3d const *a, real_vector3d const *b, real rotation[3][3]) @@ -648,7 +648,7 @@ void vr_finger_nodes( } /* a node and all under it turned about the node */ -static void vr_turn_subtree( +void vr_turn_subtree( struct vr_skeleton const *skeleton, real_matrix4x3 *matrices, short root, diff --git a/port/linux/game/vr_arms.h b/port/linux/game/vr_arms.h index 928b0508..a2ab68b8 100644 --- a/port/linux/game/vr_arms.h +++ b/port/linux/game/vr_arms.h @@ -44,6 +44,12 @@ struct vr_limb_history boolean vr_skeleton_build(struct vr_skeleton *skeleton, long graph_index, short node_count); boolean vr_skeleton_under(struct vr_skeleton const *skeleton, short node_index, short root); +/* the rotation (column vectors) taking direction a to direction b, the +shortest way; a node and all under it turned by one about the node (the +whole body, vr_body.c) */ +void vr_rotation_between(real_vector3d const *a, real_vector3d const *b, real rotation[3][3]); +void vr_turn_subtree(struct vr_skeleton const *skeleton, real_matrix4x3 *matrices, short root, + real rotation[3][3]); /* one arm (bones: upper arm, forearm, hand) bent from the shoulder for the hand to reach target, its elbow towards the pole; the hand (and what it diff --git a/port/linux/game/vr_body.c b/port/linux/game/vr_body.c new file mode 100644 index 00000000..cf401ae2 --- /dev/null +++ b/port/linux/game/vr_body.c @@ -0,0 +1,1101 @@ +/* +VR_BODY.C + +The whole body in VR (vr.body "full"): the player's own biped, which the +first-person view leaves out, drawn for the local player under the +first-person arms and weapon (vr_body.h). render_objects.c draws it among +the world's objects, so that it sorts against the world by depth; the +first-person weapon and arms, drawn before the world into the stencil, stay +over it. Only a copy of its node matrices is posed, once a frame: the +game's own pose (its hit boxes, markers, what other machines see) is left +as it is, and nothing here changes the game. + +- The torso faces the torso's heading (vr.c: the head's beyond a cone, + drawn towards the hands; the stick's turns turn it at once), upright under + a neck below and behind the eyes, which the head turns about: looking + down, the chest stays behind the eyes. The hips go down with the physical + crouch, a little back; leaning, they follow part of the way and the + spine tilts the rest. +- The hips and legs keep the game's animation (walking, running, crouched + by the button), turned to the torso's heading. Standing still, the feet + stay planted and step, one at a time, after the body when it turns far + enough; the knees bend (two-bone IK, vr_arms.c) as the hips go down. +- The head (with the neck's top) and the third-person arms are shrunk to + nothing: the first-person arms hang from the body's shoulders instead + (vr_body_shoulders), and the weapon and all the biped carries are left + out, the first-person weapon held instead. +- Seated in an open vehicle's passenger seat, the body sits as the seat's + animation has it, the head and arms shrunk alike; at a wheel or a gun + render_objects.c draws it whole, its hands on them, as before; in a + closed seat (a Ghost, a Banshee, the tank) none is drawn. + +After HaloCE-Quest-VR (moistman42069, CC0), port/linux/game/vr_render.c, +its vr_render_body_matrices: the neck pivot, the planted feet and their +steps, the leg's clamped reach, rebuilt for this port's skeleton +(vr_arms.c) and its once-a-frame poses. + +debug.vr_body (flat, the emulator) poses the body from the flat camera, with +the head turning, crouching or leaning on its own, and can move the camera +to look at it (vr_body_debug_camera). +*/ + +#ifdef HALO_ANDROID + +#include "cseries.h" +#include "math/real_math.h" +#include "objects/objects.h" +#include "objects/object_definitions.h" +#include "objects/object_types.h" +#include "models/models.h" +#include "models/model_definitions.h" +#include "models/model_animation_definitions.h" +#include "units/units.h" +#include "units/vehicles.h" +#include "game/players.h" +#include "camera/director.h" +#include "cutscene/cinematics.h" +#include "render/render.h" +#include "render/render_cameras.h" +#include "halo_vr.h" +#include "vr_arms.h" +#include "vr_body.h" + +#include +#include + +void platform_log(char const *format, ...); +char const *config_string(char const *name); +char *tag_get_name(long tag_index); +unsigned long system_milliseconds(void); + +/* world units a metre */ +#define VR_BODY_UNITS_PER_METRE (1.0f / 3.048f) +#define VR_BODY_PI 3.14159265f + +/* the neck: behind and below the eyes in the head's frame (its pitch held +within -60..+30 degrees) */ +#define VR_BODY_NECK_BACK 0.14f +#define VR_BODY_NECK_DOWN 0.20f +/* the hips: how far they follow the neck leaning aside (the spine tilts the rest), +at most how far, how far back they go as the body crouches, and how far +down and up at most */ +#define VR_BODY_HIPS_FOLLOW 0.35f +#define VR_BODY_LEAN_MOST 0.45f +#define VR_BODY_HIPS_BACK 0.25f +#define VR_BODY_CROUCH_MOST 1.20f +#define VR_BODY_RISE_MOST 0.02f +/* the first-person arms' shoulders: at most and least this far to each +side of the neck (metres) */ +#define VR_BODY_SHOULDER_LEAST 0.14f +#define VR_BODY_SHOULDER_MOST 0.24f +/* a planted foot steps when the body turns this far from it, or its place +in the animation is this far away (metres); a step's time and lift */ +#define VR_BODY_STEP_TURN (35.0f * VR_BODY_PI / 180.0f) +#define VR_BODY_STEP_GAP 0.18f +#define VR_BODY_STEP_SECONDS 0.22f +#define VR_BODY_STEP_LIFT 0.06f + +/* debug.vr_body: the camera, and the head's motion */ +enum +{ + _vr_body_debug_off, + _vr_body_debug_eye, + _vr_body_debug_down, + _vr_body_debug_front, + _vr_body_debug_side, + _vr_body_debug_back, +}; + +enum +{ + _vr_body_motion_cycle, + _vr_body_motion_still, + _vr_body_motion_turn, + _vr_body_motion_crouch, + _vr_body_motion_lean, + _vr_body_motion_raw, +}; + +/* what the body is posed from: the eyes (world units), the head's forward +and up, the eyes standing straight, the torso's flat heading, the time */ +struct vr_body_input +{ + real_point3d eye; + real_vector3d head_forward; + real_vector3d head_up; + real_point3d standing; + real_vector3d torso_forward; + double time; +}; + +static struct +{ + /* the pose this frame (render.frame_index), of which unit */ + long frame_index; + long unit_index; + /* the skeleton and its nodes, found by name for its animation graph */ + long graph_index; + boolean usable; + struct vr_skeleton skeleton; + short spine, neck, head; + short upper[2], thigh[2], calf[2], foot[2]; + boolean legs[2]; + + boolean valid; + short node_count; + real_matrix4x3 matrices[MAXIMUM_NODES_PER_ANIMATION]; + real_point3d shoulders[2]; +} vr_body = { -1, NONE, NONE }; + +/* the feet as planted, and the step under way (the foot, NONE) */ +static struct +{ + boolean valid; + long unit_index; + double time; + double step_start; + short step; + real yaw[2]; + real_matrix4x3 planted[2]; + real_matrix4x3 step_from; + real_matrix4x3 step_to; +} vr_body_stance; + +static struct +{ + int view; + int motion; + /* the flat camera this frame, and what the head made of it */ + long frame_index; + boolean input_valid; + struct vr_body_input input; + real torso_yaw; + boolean torso_valid; + double torso_time; +} vr_body_debug = { -1 }; + +/* ---------- small maths */ + +static boolean vr_body_unit( + real_vector3d *v) +{ + real length = (real)sqrt(v->i * v->i + v->j * v->j + v->k * v->k); + + if (!(length > 1e-6f && length < 1e8f)) + return FALSE; + v->i /= length; + v->j /= length; + v->k /= length; + return TRUE; +} + +static void vr_body_minus( + real_point3d const *a, + real_point3d const *b, + real_vector3d *out) +{ + out->i = a->x - b->x; + out->j = a->y - b->y; + out->k = a->z - b->z; +} + +/* a frame (forward, left, up; orthonormal) from an up and a rough left */ +static boolean vr_body_frame( + real_vector3d const *up, + real_vector3d const *left, + real_vector3d axes[3]) +{ + real along; + + axes[2] = *up; + if (!vr_body_unit(&axes[2])) + return FALSE; + axes[1] = *left; + along = dot_product3d(&axes[1], &axes[2]); + axes[1].i -= along * axes[2].i; + axes[1].j -= along * axes[2].j; + axes[1].k -= along * axes[2].k; + if (!vr_body_unit(&axes[1])) + return FALSE; + cross_product3d(&axes[1], &axes[2], &axes[0]); + return TRUE; +} + +/* the rotation (column vectors) taking frame from to frame to */ +static void vr_body_rotation( + real_vector3d const from[3], + real_vector3d const to[3], + real rotation[3][3]) +{ + short row, column, axis; + + for (row = 0; row < 3; row++) + { + for (column = 0; column < 3; column++) + { + rotation[row][column] = 0.0f; + for (axis = 0; axis < 3; axis++) + rotation[row][column] += to[axis].n[row] * from[axis].n[column]; + } + } +} + +/* every node turned about the vertical through pivot (cosine, sine) and +moved */ +static void vr_body_turn_and_move( + real_matrix4x3 *matrices, + short node_count, + real_point3d const *pivot, + real c, + real s, + real_vector3d const *move) +{ + short node_index; + + for (node_index = 0; node_index < node_count; node_index++) + { + real_matrix4x3 *node = &matrices[node_index]; + real x = node->position.x - pivot->x; + real y = node->position.y - pivot->y; + real_vector3d *axes[3]; + short axis; + + node->position.x = pivot->x + c * x - s * y + move->i; + node->position.y = pivot->y + s * x + c * y + move->j; + node->position.z += move->k; + axes[0] = &node->forward; + axes[1] = &node->left; + axes[2] = &node->up; + for (axis = 0; axis < 3; axis++) + { + real i = axes[axis]->i; + real j = axes[axis]->j; + + axes[axis]->i = c * i - s * j; + axes[axis]->j = s * i + c * j; + } + } +} + +/* a node and all under it turned about the node, but for the legs (in some +skeletons the thighs hang from the spine) */ +static void vr_body_turn_torso( + short root, + real rotation[3][3]) +{ + real_matrix4x3 *matrices = vr_body.matrices; + real_point3d pivot = matrices[root].position; + short node_index, side; + + for (node_index = 0; node_index < vr_body.node_count; node_index++) + { + real_matrix4x3 *node = &matrices[node_index]; + real_vector3d offset, *axes[3]; + short axis; + boolean leg = FALSE; + + for (side = 0; side < 2; side++) + leg |= vr_body.thigh[side] != NONE && vr_skeleton_under(&vr_body.skeleton, node_index, vr_body.thigh[side]); + if (leg || !vr_skeleton_under(&vr_body.skeleton, node_index, root)) + continue; + vr_body_minus(&node->position, &pivot, &offset); + node->position.x = pivot.x + rotation[0][0] * offset.i + rotation[0][1] * offset.j + rotation[0][2] * offset.k; + node->position.y = pivot.y + rotation[1][0] * offset.i + rotation[1][1] * offset.j + rotation[1][2] * offset.k; + node->position.z = pivot.z + rotation[2][0] * offset.i + rotation[2][1] * offset.j + rotation[2][2] * offset.k; + axes[0] = &node->forward; + axes[1] = &node->left; + axes[2] = &node->up; + for (axis = 0; axis < 3; axis++) + { + real_vector3d v = *axes[axis]; + + axes[axis]->i = rotation[0][0] * v.i + rotation[0][1] * v.j + rotation[0][2] * v.k; + axes[axis]->j = rotation[1][0] * v.i + rotation[1][1] * v.j + rotation[1][2] * v.k; + axes[axis]->k = rotation[2][0] * v.i + rotation[2][1] * v.j + rotation[2][2] * v.k; + } + } +} + +/* ---------- what the body is posed from */ + +/* the neck the head turns about (after HaloCE-Quest-VR's vr_neck_pivot) */ +static void vr_body_neck_pivot( + struct vr_body_input const *input, + real_point3d *neck) +{ + real_vector3d yaw; + real pitch, c, s, back; + + yaw.i = input->head_forward.i - input->head_forward.k * input->head_up.i; + yaw.j = input->head_forward.j - input->head_forward.k * input->head_up.j; + yaw.k = 0.0f; + if (!vr_body_unit(&yaw)) + yaw = input->torso_forward; + pitch = (real)asin(PIN(input->head_forward.k, -1.0f, 1.0f)); + pitch = PIN(pitch, -60.0f * VR_BODY_PI / 180.0f, 30.0f * VR_BODY_PI / 180.0f); + c = (real)cos(pitch); + s = (real)sin(pitch); + back = (VR_BODY_NECK_BACK * c - VR_BODY_NECK_DOWN * s) * VR_BODY_UNITS_PER_METRE; + neck->x = input->eye.x - back * yaw.i; + neck->y = input->eye.y - back * yaw.j; + neck->z = input->eye.z - (VR_BODY_NECK_BACK * s + VR_BODY_NECK_DOWN * c) * VR_BODY_UNITS_PER_METRE; +} + +static void vr_body_debug_settings( + void) +{ + char const *setting; + static char const *const views[] = { "off", "eye", "down", "front", "side", "back" }; + static char const *const motions[] = { "cycle", "still", "turn", "crouch", "lean", "raw" }; + short index; + + if (vr_body_debug.view >= 0) + return; + vr_body_debug.view = _vr_body_debug_off; + vr_body_debug.motion = _vr_body_motion_cycle; + setting = config_string("debug.vr_body"); + if (!setting) + return; + for (index = 0; index < (short)NUMBEROF(views); index++) + { + size_t length = strlen(views[index]); + + if (!strncmp(setting, views[index], length) && (setting[length] == 0 || setting[length] == ':')) + vr_body_debug.view = index; + } + setting = strchr(setting, ':'); + for (index = 0; setting && index < (short)NUMBEROF(motions); index++) + { + if (!strcmp(setting + 1, motions[index])) + vr_body_debug.motion = index; + } + if (vr_body_debug.view != _vr_body_debug_off) + platform_log("VR body: debug.vr_body view %d, motion %d (flat)", vr_body_debug.view, vr_body_debug.motion); +} + +/* debug.vr_body: the head as the flat camera has it, turning, crouching or +leaning on its own (five seconds each, in turn), and the torso after it as +vr.c's torso_update has it (beyond 15 degrees, eased) */ +static void vr_body_debug_input( + struct render_camera const *camera) +{ + struct vr_body_input *input = &vr_body_debug.input; + double now = (double)system_milliseconds() * 0.001; + real phase = (real)fmod(now, 15.0); + int motion = vr_body_debug.motion; + real yaw, pitch, turn = 0.0f, crouch = 0.0f, lean = 0.0f; + real cone = 15.0f * VR_BODY_PI / 180.0f; + + if (motion == _vr_body_motion_cycle) + motion = phase < 5.0f ? _vr_body_motion_turn : phase < 10.0f ? _vr_body_motion_crouch : _vr_body_motion_lean; + phase = (real)fmod(now, 5.0) / 5.0f; + if (motion == _vr_body_motion_turn) + turn = 60.0f * VR_BODY_PI / 180.0f * (real)sin(2.0f * VR_BODY_PI * phase); + else if (motion == _vr_body_motion_crouch) + crouch = 0.5f * (0.5f - 0.5f * (real)cos(2.0f * VR_BODY_PI * phase)); + else if (motion == _vr_body_motion_lean) + lean = 0.3f * (real)sin(2.0f * VR_BODY_PI * phase); + yaw = (real)atan2(camera->forward.j, camera->forward.i) + turn; + pitch = (real)asin(PIN(camera->forward.k, -1.0f, 1.0f)); + if (vr_body_debug.view == _vr_body_debug_down) + pitch = -60.0f * VR_BODY_PI / 180.0f; + input->head_forward.i = (real)(cos(yaw) * cos(pitch)); + input->head_forward.j = (real)(sin(yaw) * cos(pitch)); + input->head_forward.k = (real)sin(pitch); + input->head_up.i = (real)(-cos(yaw) * sin(pitch)); + input->head_up.j = (real)(-sin(yaw) * sin(pitch)); + input->head_up.k = (real)cos(pitch); + input->standing = camera->position; + input->eye = camera->position; + input->eye.x += (real)cos(yaw) * lean * VR_BODY_UNITS_PER_METRE; + input->eye.y += (real)sin(yaw) * lean * VR_BODY_UNITS_PER_METRE; + input->eye.z -= (crouch + 0.1f * (real)fabs(lean)) * VR_BODY_UNITS_PER_METRE; + input->time = now; + + if (!vr_body_debug.torso_valid || now - vr_body_debug.torso_time > 0.25 || now < vr_body_debug.torso_time) + { + vr_body_debug.torso_yaw = yaw; + } + else + { + real difference = (real)atan2(sin(yaw - vr_body_debug.torso_yaw), cos(yaw - vr_body_debug.torso_yaw)); + + if (fabs(difference) > cone) + { + real seconds = (real)MIN(now - vr_body_debug.torso_time, 0.05); + + difference -= difference > 0.0f ? cone : -cone; + vr_body_debug.torso_yaw += difference * (1.0f - (real)exp(-10.0f * seconds)); + } + } + vr_body_debug.torso_valid = TRUE; + vr_body_debug.torso_time = now; + input->torso_forward.i = (real)cos(vr_body_debug.torso_yaw); + input->torso_forward.j = (real)sin(vr_body_debug.torso_yaw); + input->torso_forward.k = 0.0f; + vr_body_debug.input_valid = TRUE; + vr_body_debug.frame_index = render.frame_index; +} + +static boolean vr_body_input( + short local_player_index, + struct vr_body_input *input) +{ + if (halo_vr_active()) + { + float eye[3], forward[3], up[3], standing[3], head[3], neck[3], torso[3]; + + if (!halo_vr_head_pose(local_player_index, eye, forward, up, standing) || + !halo_vr_body_frame(local_player_index, head, neck, torso)) + { + return FALSE; + } + input->eye.x = eye[0]; + input->eye.y = eye[1]; + input->eye.z = eye[2]; + input->head_forward.i = forward[0]; + input->head_forward.j = forward[1]; + input->head_forward.k = forward[2]; + input->head_up.i = up[0]; + input->head_up.j = up[1]; + input->head_up.k = up[2]; + input->standing.x = standing[0]; + input->standing.y = standing[1]; + input->standing.z = standing[2]; + input->torso_forward.i = torso[0]; + input->torso_forward.j = torso[1]; + input->torso_forward.k = 0.0f; + input->time = halo_vr_pose_time(); + return vr_body_unit(&input->torso_forward) && vr_body_unit(&input->head_forward) && + vr_body_unit(&input->head_up); + } + if (!vr_body_debug.input_valid || vr_body_debug.frame_index != render.frame_index) + return FALSE; + *input = vr_body_debug.input; + return TRUE; +} + +/* ---------- the skeleton */ + +static boolean vr_body_skeleton( + long graph_index, + short node_count) +{ + static char const *const sides[2] = { "l ", "r " }; + short side; + + if (vr_body.graph_index == graph_index && vr_body.skeleton.node_count == node_count) + return vr_body.usable; + vr_body.graph_index = graph_index; + vr_body.usable = FALSE; + vr_body_stance.valid = FALSE; + if (!vr_skeleton_build(&vr_body.skeleton, graph_index, node_count)) + { + vr_body.skeleton.node_count = node_count; + platform_log("VR body: %s has %d nodes, its model %d: not drawn", tag_get_name(graph_index), + animation_graph_node_count(graph_index), node_count); + return FALSE; + } + vr_body.spine = animation_graph_find_node(graph_index, "spine"); + vr_body.neck = animation_graph_find_node(graph_index, "neck"); + vr_body.head = animation_graph_find_node(graph_index, "head"); + for (side = 0; side < 2; side++) + { + char name[16]; + + snprintf(name, sizeof(name), "%supperarm", sides[side]); + vr_body.upper[side] = animation_graph_find_node(graph_index, name); + snprintf(name, sizeof(name), "%sthigh", sides[side]); + vr_body.thigh[side] = animation_graph_find_node(graph_index, name); + snprintf(name, sizeof(name), "%scalf", sides[side]); + vr_body.calf[side] = animation_graph_find_node(graph_index, name); + snprintf(name, sizeof(name), "%sfoot", sides[side]); + vr_body.foot[side] = animation_graph_find_node(graph_index, name); + vr_body.legs[side] = vr_body.thigh[side] != NONE && vr_body.calf[side] != NONE && vr_body.foot[side] != NONE && + vr_skeleton_under(&vr_body.skeleton, vr_body.calf[side], vr_body.thigh[side]) && + vr_skeleton_under(&vr_body.skeleton, vr_body.foot[side], vr_body.calf[side]); + } + vr_body.usable = vr_body.spine != NONE && vr_body.neck != NONE && vr_body.head != NONE && + vr_body.upper[0] != NONE && vr_body.upper[1] != NONE && + vr_skeleton_under(&vr_body.skeleton, vr_body.neck, vr_body.spine) && + !vr_skeleton_under(&vr_body.skeleton, vr_body.neck, vr_body.head); + platform_log("VR body: %s, %d nodes: spine %d, neck %d, head %d, upper arms %d %d, legs %s %s%s", + tag_get_name(graph_index), node_count, vr_body.spine, vr_body.neck, vr_body.head, vr_body.upper[0], + vr_body.upper[1], vr_body.legs[0] ? "yes" : "no", vr_body.legs[1] ? "yes" : "no", + vr_body.usable ? "" : ": not drawn"); + if (vr_body_debug.view > _vr_body_debug_off) + { + short node_index; + + for (node_index = 0; node_index < node_count; node_index++) + { + platform_log("VR body: node %d '%s', parent %d", node_index, + animation_graph_node_name(graph_index, node_index), vr_body.skeleton.parents[node_index]); + } + } + return vr_body.usable; +} + +/* ---------- the pose */ + +/* standing still, the feet stay where they were (planted); one at a time +they step to where the animation, turned with the body, has them, when the +body has turned far from them or that place is far; walking, running or in +the air, they are the animation's (after HaloCE-Quest-VR's +vr_body_plant_feet). The feet's places (world units) for the legs' IK. */ +static void vr_body_plant_feet( + long unit_index, + struct object_datum const *object, + real torso_yaw, + double now, + real_point3d feet[2]) +{ + real_matrix4x3 *matrices = vr_body.matrices; + real_vector3d const *velocity = &object->object.translational_velocity; + real speed = (real)sqrt(velocity->i * velocity->i + velocity->j * velocity->j); + boolean moving = speed > 0.006f * VR_BODY_UNITS_PER_METRE || + fabs(velocity->k) > 0.015f * VR_BODY_UNITS_PER_METRE; + short side; + + if (!vr_body.legs[0] || !vr_body.legs[1]) + { + vr_body_stance.valid = FALSE; + return; + } + if (!vr_body_stance.valid || vr_body_stance.unit_index != unit_index || now < vr_body_stance.time || + now - vr_body_stance.time > 0.25 || moving) + { + vr_body_stance.valid = TRUE; + vr_body_stance.unit_index = unit_index; + vr_body_stance.step = NONE; + for (side = 0; side < 2; side++) + { + vr_body_stance.planted[side] = matrices[vr_body.foot[side]]; + vr_body_stance.yaw[side] = torso_yaw; + } + } + vr_body_stance.time = now; + if (!moving) + { + if (vr_body_stance.step == NONE) + { + real most = 1.0f; + + for (side = 0; side < 2; side++) + { + real_vector3d gap; + real turn = (real)fabs(atan2(sin(torso_yaw - vr_body_stance.yaw[side]), + cos(torso_yaw - vr_body_stance.yaw[side]))); + real need; + + vr_body_minus(&matrices[vr_body.foot[side]].position, &vr_body_stance.planted[side].position, &gap); + need = MAX(magnitude3d(&gap) / (VR_BODY_STEP_GAP * VR_BODY_UNITS_PER_METRE), turn / VR_BODY_STEP_TURN); + if (need > most) + { + most = need; + vr_body_stance.step = side; + } + } + if (vr_body_stance.step != NONE) + { + side = vr_body_stance.step; + vr_body_stance.step_start = now; + vr_body_stance.step_from = vr_body_stance.planted[side]; + vr_body_stance.yaw[side] = torso_yaw; + if (vr_body_debug.view > _vr_body_debug_off) + platform_log("VR body: the %s foot steps (%.1f times as far as it may)", side ? "right" : "left", most); + } + } + if (vr_body_stance.step != NONE) + { + /* (to where the animation has the foot now: the body may turn on + while it steps) */ + real t = PIN((real)((now - vr_body_stance.step_start) / VR_BODY_STEP_SECONDS), 0.0f, 1.0f); + real eased = t * t * (3.0f - 2.0f * t); + real_matrix4x3 const *from = &vr_body_stance.step_from; + real_matrix4x3 const *to = &matrices[vr_body.foot[vr_body_stance.step]]; + real_matrix4x3 *foot = &vr_body_stance.planted[vr_body_stance.step]; + short axis; + + for (axis = 0; axis < 3; axis++) + { + foot->position.n[axis] = from->position.n[axis] + (to->position.n[axis] - from->position.n[axis]) * eased; + foot->forward.n[axis] = from->forward.n[axis] + (to->forward.n[axis] - from->forward.n[axis]) * eased; + foot->up.n[axis] = from->up.n[axis] + (to->up.n[axis] - from->up.n[axis]) * eased; + } + foot->position.z += (real)sin(t * VR_BODY_PI) * VR_BODY_STEP_LIFT * VR_BODY_UNITS_PER_METRE; + if (vr_body_unit(&foot->forward)) + { + cross_product3d(&foot->up, &foot->forward, &foot->left); + if (vr_body_unit(&foot->left)) + cross_product3d(&foot->forward, &foot->left, &foot->up); + } + if (t >= 1.0f) + { + *foot = *to; + vr_body_stance.yaw[vr_body_stance.step] = torso_yaw; + vr_body_stance.step = NONE; + } + } + } + for (side = 0; side < 2; side++) + { + if (moving) + vr_body_stance.planted[side] = matrices[vr_body.foot[side]]; + feet[side] = vr_body_stance.planted[side].position; + } +} + +/* a leg bent (two-bone IK) for its foot to reach its planted place, never +lengthened (an unreachable foot stops short), the knee forward; the foot +turned as planted (after HaloCE-Quest-VR's vr_body_leg) */ +static void vr_body_leg( + short side, + real_point3d const *foot, + real_vector3d const *forward) +{ + real_matrix4x3 *matrices = vr_body.matrices; + short bones[NUMBER_OF_VR_ARM_BONES]; + real_vector3d thigh, calf, reach, pole, from[3], to[3]; + real thigh_length, calf_length, distance, shortest, longest; + real_point3d target; + real rotation[3][3]; + real outward = side ? -1.0f : 1.0f; /* the left leg's out is +left */ + + bones[_vr_arm_upper] = vr_body.thigh[side]; + bones[_vr_arm_fore] = vr_body.calf[side]; + bones[_vr_arm_hand] = vr_body.foot[side]; + vr_body_minus(&matrices[bones[_vr_arm_fore]].position, &matrices[bones[_vr_arm_upper]].position, &thigh); + vr_body_minus(&matrices[bones[_vr_arm_hand]].position, &matrices[bones[_vr_arm_fore]].position, &calf); + thigh_length = magnitude3d(&thigh); + calf_length = magnitude3d(&calf); + shortest = (real)fabs(thigh_length - calf_length) + 0.002f; + longest = (thigh_length + calf_length) * 0.998f; + vr_body_minus(foot, &matrices[bones[_vr_arm_upper]].position, &reach); + distance = magnitude3d(&reach); + if (!(distance > 1e-4f && distance < 1e4f) || longest <= shortest) + return; + distance = PIN(distance, shortest, longest) / distance; + target.x = matrices[bones[_vr_arm_upper]].position.x + reach.i * distance; + target.y = matrices[bones[_vr_arm_upper]].position.y + reach.j * distance; + target.z = matrices[bones[_vr_arm_upper]].position.z + reach.k * distance; + pole.i = forward->i - forward->j * outward * 0.15f; + pole.j = forward->j + forward->i * outward * 0.15f; + pole.k = 0.0f; + vr_solve_arm(&vr_body.skeleton, matrices, bones, matrices[bones[_vr_arm_upper]].position, &target, &pole, FALSE, + NULL); + from[0] = matrices[bones[_vr_arm_hand]].forward; + from[1] = matrices[bones[_vr_arm_hand]].left; + from[2] = matrices[bones[_vr_arm_hand]].up; + to[0] = vr_body_stance.planted[side].forward; + to[1] = vr_body_stance.planted[side].left; + to[2] = vr_body_stance.planted[side].up; + vr_body_rotation(from, to, rotation); + vr_turn_subtree(&vr_body.skeleton, matrices, bones[_vr_arm_hand], rotation); +} + +/* the body's own pose (the game's, between the last two ticks) turned, +moved and bent to the head, its head and arms shrunk */ +static boolean vr_body_pose( + long unit_index, + struct vr_body_input const *input, + boolean seated) +{ + struct object_datum *object = object_get(unit_index); + real_matrix4x3 *matrices = vr_body.matrices; + real_point3d neck_target, collapse[3]; + real_vector3d left; + short node_index, side; + + csmemcpy(matrices, object_get_node_matrices(unit_index), vr_body.node_count * sizeof(real_matrix4x3)); + left.i = -input->torso_forward.j; + left.j = input->torso_forward.i; + left.k = 0.0f; + vr_body_neck_pivot(input, &neck_target); + + /* (debug.vr_body "...:raw": the game's pose, only shrunk) */ + if (!seated && !(vr_body_debug.view > _vr_body_debug_off && vr_body_debug.motion == _vr_body_motion_raw)) + { + real_matrix4x3 const *tick = (real_matrix4x3 const *)object_header_block_get(unit_index, + &object->object.node_matrices); + real_vector3d facing = object->object.forward; + real_vector3d model_forward, move; + real along[3], c, s, drop, horizontal; + real_point3d pivot = matrices[0].position; + real_point3d standing_neck, feet[2]; + real torso_yaw = (real)atan2(input->torso_forward.j, input->torso_forward.i); + + /* the body turned from the way it faces (as drawn: the facing in the + root's own axes at the tick, those axes now) to the torso's heading */ + along[0] = dot_product3d(&facing, &tick[0].forward); + along[1] = dot_product3d(&facing, &tick[0].left); + along[2] = dot_product3d(&facing, &tick[0].up); + model_forward.i = matrices[0].forward.i * along[0] + matrices[0].left.i * along[1] + matrices[0].up.i * along[2]; + model_forward.j = matrices[0].forward.j * along[0] + matrices[0].left.j * along[1] + matrices[0].up.j * along[2]; + model_forward.k = 0.0f; + if (!vr_body_unit(&model_forward)) + return FALSE; + c = dot_product3d(&model_forward, &input->torso_forward); + s = model_forward.i * input->torso_forward.j - model_forward.j * input->torso_forward.i; + move.i = move.j = move.k = 0.0f; + vr_body_turn_and_move(matrices, vr_body.node_count, &pivot, c, s, &move); + /* and moved for its neck to be where the neck is standing straight + (behind the game's camera, whose eyes the body's model has a little + further forward) */ + standing_neck.x = input->standing.x - input->torso_forward.i * VR_BODY_NECK_BACK * VR_BODY_UNITS_PER_METRE; + standing_neck.y = input->standing.y - input->torso_forward.j * VR_BODY_NECK_BACK * VR_BODY_UNITS_PER_METRE; + move.i = standing_neck.x - matrices[vr_body.neck].position.x; + move.j = standing_neck.y - matrices[vr_body.neck].position.y; + vr_body_turn_and_move(matrices, vr_body.node_count, &pivot, 1.0f, 0.0f, &move); + + /* the feet, where they stand */ + vr_body_plant_feet(unit_index, object, torso_yaw, input->time, feet); + + /* the hips down as far as the neck is below where it is standing + straight, and back a little; leaning, aside part of the way (the + spine tilts the rest) */ + drop = neck_target.z - (input->standing.z - VR_BODY_NECK_DOWN * VR_BODY_UNITS_PER_METRE); + drop = PIN(drop, -VR_BODY_CROUCH_MOST * VR_BODY_UNITS_PER_METRE, VR_BODY_RISE_MOST * VR_BODY_UNITS_PER_METRE); + move.i = neck_target.x - standing_neck.x; + move.j = neck_target.y - standing_neck.y; + horizontal = (real)sqrt(move.i * move.i + move.j * move.j); + if (horizontal > VR_BODY_LEAN_MOST * VR_BODY_UNITS_PER_METRE) + { + move.i *= VR_BODY_LEAN_MOST * VR_BODY_UNITS_PER_METRE / horizontal; + move.j *= VR_BODY_LEAN_MOST * VR_BODY_UNITS_PER_METRE / horizontal; + } + move.i = move.i * VR_BODY_HIPS_FOLLOW + input->torso_forward.i * drop * VR_BODY_HIPS_BACK; + move.j = move.j * VR_BODY_HIPS_FOLLOW + input->torso_forward.j * drop * VR_BODY_HIPS_BACK; + move.k = drop; + if (vr_body_debug.view > _vr_body_debug_off) + { + /* (debug.vr_body: once a second, how the pose was moved) */ + static unsigned long logged; + unsigned long now_ms = system_milliseconds(); + + if (now_ms - logged > 1000) + { + real m = 1.0f / VR_BODY_UNITS_PER_METRE; + + logged = now_ms; + platform_log("VR body: turned %.0f deg; moved %.2f %.2f %.2f m; neck %.2f m below the eyes, " + "%.2f m behind; target %.2f below, pelvis %.2f m below; feet %.2f %.2f / %.2f %.2f m from the pelvis", + atan2(s, c) * 180.0f / VR_BODY_PI, move.i * m, move.j * m, move.k * m, + (input->standing.z - matrices[vr_body.neck].position.z) * m, + -((matrices[vr_body.neck].position.x - input->standing.x) * input->torso_forward.i + + (matrices[vr_body.neck].position.y - input->standing.y) * input->torso_forward.j) * m, + (input->eye.z - neck_target.z) * m, (input->standing.z - matrices[0].position.z) * m, + (feet[0].x - matrices[0].position.x) * m, (feet[0].y - matrices[0].position.y) * m, + (feet[1].x - matrices[0].position.x) * m, (feet[1].y - matrices[0].position.y) * m); + } + } + vr_body_turn_and_move(matrices, vr_body.node_count, &pivot, 1.0f, 0.0f, &move); + + /* the torso upright from the spine to the neck, square to the + torso's heading (whatever the aiming's animation made of it) */ + { + real_vector3d up, across, from[3], to[3]; + real rotation[3][3]; + + vr_body_minus(&matrices[vr_body.neck].position, &matrices[vr_body.spine].position, &up); + vr_body_minus(&matrices[vr_body.upper[0]].position, &matrices[vr_body.upper[1]].position, &across); + if (vr_body_frame(&up, &across, from)) + { + vr_body_minus(&neck_target, &matrices[vr_body.spine].position, &up); + if (vr_body_frame(&up, &left, to)) + { + vr_body_rotation(from, to, rotation); + vr_body_turn_torso(vr_body.spine, rotation); + } + } + } + + /* the legs bent to the feet */ + if (vr_body_stance.valid) + { + for (side = 0; side < 2; side++) + vr_body_leg(side, &feet[side], &input->torso_forward); + } + if (vr_body_debug.view > _vr_body_debug_off) + { + static unsigned long logged; + unsigned long now_ms = system_milliseconds(); + + if (now_ms - logged > 1000) + { + real_vector3d f = input->torso_forward; + real m = 1.0f / VR_BODY_UNITS_PER_METRE; + real_point3d const *p = &matrices[0].position; + real_point3d const *n = &matrices[vr_body.neck].position; + real_point3d const *k = &matrices[vr_body.calf[0]].position; + + logged = now_ms; + platform_log("VR body: posed: neck %.2f m ahead of the pelvis, %.2f above; left knee %.2f ahead, " + "%.2f above; eye %.2f ahead of the neck; shoulders %.2f apart, %.2f below the neck", + ((n->x - p->x) * f.i + (n->y - p->y) * f.j) * m, (n->z - p->z) * m, + ((k->x - p->x) * f.i + (k->y - p->y) * f.j) * m, (k->z - p->z) * m, + ((input->eye.x - n->x) * f.i + (input->eye.y - n->y) * f.j) * m, + distance3d(&matrices[vr_body.upper[0]].position, &matrices[vr_body.upper[1]].position) * m, + (n->z - 0.5f * (matrices[vr_body.upper[0]].position.z + matrices[vr_body.upper[1]].position.z)) * m); + } + } + } + + /* the shoulders, for the first-person arms; then the head shrunk onto + the neck, each arm onto its shoulder */ + { + /* (from the neck: below it, a little behind, to each side as far + as the body's shoulder joints are, within 14 to 24 cm, which the aim's + animation can shrug; as HaloCE-Quest-VR has them) */ + real_vector3d up, across, chest[3]; + real width; + + vr_body_minus(&matrices[vr_body.neck].position, &matrices[vr_body.spine].position, &up); + vr_body_minus(&matrices[vr_body.upper[0]].position, &matrices[vr_body.upper[1]].position, &across); + width = PIN(0.5f * magnitude3d(&across), VR_BODY_SHOULDER_LEAST * VR_BODY_UNITS_PER_METRE, + VR_BODY_SHOULDER_MOST * VR_BODY_UNITS_PER_METRE); + if (!vr_body_frame(&up, &across, chest)) + return FALSE; + for (side = 0; side < 2; side++) + { + real_point3d *shoulder = &vr_body.shoulders[side]; + + *shoulder = matrices[vr_body.neck].position; + point_from_line3d(shoulder, &chest[1], side ? -width : width, shoulder); + point_from_line3d(shoulder, &chest[0], -0.02f * VR_BODY_UNITS_PER_METRE, shoulder); + point_from_line3d(shoulder, &chest[2], -0.03f * VR_BODY_UNITS_PER_METRE, shoulder); + } + } + collapse[0] = matrices[vr_body.neck].position; + collapse[1] = matrices[vr_body.upper[0]].position; + collapse[2] = matrices[vr_body.upper[1]].position; + for (node_index = 0; node_index < vr_body.node_count; node_index++) + { + short part = vr_skeleton_under(&vr_body.skeleton, node_index, vr_body.head) ? 0 : + vr_skeleton_under(&vr_body.skeleton, node_index, vr_body.upper[0]) ? 1 : + vr_skeleton_under(&vr_body.skeleton, node_index, vr_body.upper[1]) ? 2 : NONE; + + if (!isfinite(matrices[node_index].position.x) || !isfinite(matrices[node_index].position.y) || + !isfinite(matrices[node_index].position.z)) + { + return FALSE; + } + if (part != NONE) + { + matrices[node_index].scale = 0.0f; + matrices[node_index].position = collapse[part]; + } + } + return TRUE; +} + +/* whether the local player's own unit is drawn whole this frame, and if it +sits (a passenger's seat in an open vehicle) */ +static boolean vr_body_wanted( + short local_player_index, + long object_index, + boolean *seated) +{ + long player_index; + struct object_datum *object; + + *seated = FALSE; + if (local_player_index != 0) + return FALSE; + if (halo_vr_active()) + { + if (halo_vr_body_mode() != HALO_VR_BODY_FULL || !halo_vr_stereo_frame()) + return FALSE; + } + else + { + vr_body_debug_settings(); + if (vr_body_debug.view == _vr_body_debug_off) + return FALSE; + } + player_index = local_player_get_player_index(local_player_index); + if (player_index == NONE || player_get(player_index)->unit_index != object_index || + director_get_perspective(local_player_index) != _director_perspective_first_person || + cinematic_in_progress() || render.camera.mirrored) + { + return FALSE; + } + object = object_get(object_index); + if (object->object.type != _object_type_biped || TEST_FLAG(object->object.damage_flags, _object_dead_bit) || + TEST_FLAG(object->object.flags, _object_invisible_bit)) + { + return FALSE; + } + if (object->object.parent_object_index != NONE) + { + struct object_datum *parent = object_get(object->object.parent_object_index); + + /* (at a wheel or a gun render_objects.c draws it whole; in a closed + seat, none) */ + if (render_object_vr_at_controls(object_index) || + (TEST_FLAG(_object_mask_vehicle, parent->object.type) && + vehicle_vr_encloses_riders(object->object.parent_object_index))) + { + return FALSE; + } + *seated = TRUE; + } + return TRUE; +} + +/* the pose this frame, built once (every eye, the scope's view and the +first-person arms take it) */ +static boolean vr_body_build( + short local_player_index, + long unit_index) +{ + struct vr_body_input input; + struct object_datum *object; + struct object_definition *definition; + boolean seated; + unsigned long long timing; + + if (vr_body.frame_index == render.frame_index && vr_body.unit_index == unit_index) + return vr_body.valid; + vr_body.frame_index = render.frame_index; + vr_body.unit_index = unit_index; + vr_body.valid = FALSE; + if (unit_index == NONE || !vr_body_wanted(local_player_index, unit_index, &seated) || + !vr_body_input(local_player_index, &input)) + { + return FALSE; + } + timing = halo_vr_timing_start(); + object = object_get(unit_index); + definition = object_definition_get(object->definition_index); + if (definition->object.model.index == NONE || definition->object.animation_graph.index == NONE) + return FALSE; + vr_body.node_count = model_definition_get(definition->object.model.index)->nodes.count; + if (vr_body.node_count > 0 && vr_body.node_count <= MAXIMUM_NODES_PER_ANIMATION && + vr_body_skeleton(definition->object.animation_graph.index, vr_body.node_count)) + { + vr_body.valid = vr_body_pose(unit_index, &input, seated); + } + halo_vr_timing_end(HALO_VR_TIMING_BODY, timing); + return vr_body.valid; +} + +/* ---------- public code */ + +boolean vr_body_drawn( + short local_player_index, + long object_index) +{ + return vr_body_build(local_player_index, object_index); +} + +real_matrix4x3 *vr_body_node_matrices( + long object_index) +{ + return vr_body.valid && vr_body.unit_index == object_index && vr_body.frame_index == render.frame_index ? + vr_body.matrices : object_get_node_matrices(object_index); +} + +boolean vr_body_shoulders( + short local_player_index, + real_point3d shoulders[2]) +{ + long player_index = local_player_index == NONE ? NONE : local_player_get_player_index(local_player_index); + + if (player_index == NONE || !vr_body_build(local_player_index, player_get(player_index)->unit_index)) + return FALSE; + shoulders[0] = vr_body.shoulders[0]; + shoulders[1] = vr_body.shoulders[1]; + return TRUE; +} + +/* the camera looking from target's side (world units: away from it, up), +at it */ +static void vr_body_debug_look( + struct render_camera *camera, + real_point3d const *from, + real_point3d const *at) +{ + real_vector3d forward, up; + real along; + + vr_body_minus(at, from, &forward); + if (!vr_body_unit(&forward)) + return; + up.i = up.j = 0.0f; + up.k = 1.0f; + along = dot_product3d(&up, &forward); + up.i -= along * forward.i; + up.j -= along * forward.j; + up.k -= along * forward.k; + if (!vr_body_unit(&up)) + return; + camera->position = *from; + camera->forward = forward; + camera->up = up; +} + +boolean vr_body_debug_camera( + short local_player_index, + struct render_camera *render_camera, + struct render_camera *rasterizer_camera) +{ + struct vr_body_input const *input = &vr_body_debug.input; + real_point3d from, at; + real_vector3d left; + real m = VR_BODY_UNITS_PER_METRE; + + vr_body_debug_settings(); + if (local_player_index != 0 || halo_vr_active() || vr_body_debug.view == _vr_body_debug_off) + return FALSE; + vr_body_debug_input(render_camera); + left.i = -input->torso_forward.j; + left.j = input->torso_forward.i; + left.k = 0.0f; + from = input->eye; + at = input->eye; + switch (vr_body_debug.view) + { + case _vr_body_debug_down: + /* (as the headset looks down) */ + render_camera->position = input->eye; + render_camera->forward = input->head_forward; + render_camera->up = input->head_up; + /* (and about as wide as the headset's view) */ + render_camera->vertical_field_of_view = 90.0f * VR_BODY_PI / 180.0f; + rasterizer_camera->vertical_field_of_view = render_camera->vertical_field_of_view; + break; + case _vr_body_debug_front: + point_from_line3d(&from, &input->torso_forward, 2.6f * m, &from); + from.z -= 0.6f * m; + break; + case _vr_body_debug_side: + point_from_line3d(&from, &left, 2.6f * m, &from); + from.z -= 0.6f * m; + break; + case _vr_body_debug_back: + point_from_line3d(&from, &input->torso_forward, -2.6f * m, &from); + from.z += 0.1f * m; + break; + default: + return FALSE; + } + if (vr_body_debug.view != _vr_body_debug_down) + { + at.z -= 0.9f * m; + vr_body_debug_look(render_camera, &from, &at); + } + rasterizer_camera->position = render_camera->position; + rasterizer_camera->forward = render_camera->forward; + rasterizer_camera->up = render_camera->up; + return TRUE; +} + +boolean vr_body_debug_hides_first_person( + void) +{ + vr_body_debug_settings(); + return !halo_vr_active() && vr_body_debug.view >= _vr_body_debug_front; +} + +#endif diff --git a/port/linux/game/vr_body.h b/port/linux/game/vr_body.h new file mode 100644 index 00000000..83dfa967 --- /dev/null +++ b/port/linux/game/vr_body.h @@ -0,0 +1,36 @@ +/* +VR_BODY.H + +The whole body in VR (vr.body "full", vr_body.c): the player's own biped +drawn under the first-person arms and weapon. Include after the game's +math, object and render headers. +*/ + +#ifndef __VR_BODY_H +#define __VR_BODY_H + +#ifdef HALO_ANDROID + +struct render_camera; + +/* render_objects.c: whether the object is the local player's own unit, drawn +whole in first person this frame (its weapon and what it carries left +out), and its node matrices as posed */ +boolean vr_body_drawn(short local_player_index, long object_index); +real_matrix4x3 *vr_body_node_matrices(long object_index); + +/* first_person_weapons.c: the drawn body's shoulders this frame (world +units; the model's left, then right), from which the first-person arms hang; +FALSE when no body is drawn */ +boolean vr_body_shoulders(short local_player_index, real_point3d shoulders[2]); + +/* debug.vr_body, flat (the emulator): the camera of a player's view moved to +look at the body (render.c), and whether the first-person weapon is left out +of it (first_person_weapons.c) */ +boolean vr_body_debug_camera(short local_player_index, struct render_camera *render_camera, + struct render_camera *rasterizer_camera); +boolean vr_body_debug_hides_first_person(void); + +#endif + +#endif diff --git a/port/linux/include/halo_vr.h b/port/linux/include/halo_vr.h index 7121f930..d444d0e6 100644 --- a/port/linux/include/halo_vr.h +++ b/port/linux/include/halo_vr.h @@ -131,6 +131,10 @@ int halo_vr_ready_laser(short local_player_index, float start[3], float directio weapon aims along both hands; */ int halo_vr_body_frame(short local_player_index, float head[3], float neck[3], float forward[3]); int halo_vr_free_hand(short local_player_index, float position[3], float forward[3], float up[3]); +/* - for the whole body: the eyes (world units), the head's forward and up, + and the game's camera: where the eyes are standing straight (lower + crouched by the button), from which the physical crouch is measured; */ +int halo_vr_head_pose(short local_player_index, float eye[3], float forward[3], float up[3], float standing[3]); int halo_vr_two_hand(short local_player_index, const float foregrip[3], const float reference[3]); /* - the fingers of a hand of the model (0 the left, the free hand; 1 the right, the weapon's), from the controller's trigger, grip and touch: the thumb, index, middle finger and the last two, each @@ -152,12 +156,15 @@ int halo_vr_finger_curls(short local_player_index, int hand, float curls[4]); int halo_vr_free_arm_bends(void); double halo_vr_pose_time(void); /* - what of the body is drawn (vr.body): the arms and hands, the hands - alone, or neither (the weapon alone) */ + alone, or neither (the weapon alone); or the whole body, its torso, hips + and legs the player's own biped's (port/linux/game/vr_body.c), the arms + and hands as with "arms" */ enum { HALO_VR_BODY_ARMS = 0, HALO_VR_BODY_HANDS, HALO_VR_BODY_OFF, + HALO_VR_BODY_FULL, }; int halo_vr_body_mode(void); /* a hand has just been thrust forward: a melee attack (xinput_sdl.c) */ @@ -361,13 +368,15 @@ enum void halo_vr_phase(int phase, int ticks); /* vr.log_performance: time taken by a part of the drawing, for the report (first_person_weapons.c): its start (0 when not logged), and its end. The -first-person pose (its animation and the arms), the arms alone, and the -pose taken as built already this frame (counted) */ +first-person pose (its animation and the arms), the arms alone, the +pose taken as built already this frame (counted), and the whole body's pose +(vr_body.c; flat with debug.vr_body too, reported alone) */ enum { HALO_VR_TIMING_FIRST_PERSON, HALO_VR_TIMING_ARMS, HALO_VR_TIMING_FIRST_PERSON_KEPT, + HALO_VR_TIMING_BODY, HALO_VR_TIMING_COUNT }; unsigned long long halo_vr_timing_start(void); diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index 896d899e..843438a9 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -255,8 +255,10 @@ static const struct config_setting config_settings[] = "its grip button to take hold of it with both hands." }, { "vr.body", _config_string, "\"arms\"", "HALO_VR_BODY", _environment_value, _platform_android, "What of your body is drawn with the weapon: \"arms\", the arms and hands\n" - "reaching from the shoulders to the controllers; \"hands\", the hands\n" - "alone; \"off\", neither (the weapon alone)." }, + "reaching from the shoulders to the controllers; \"full\", the whole\n" + "body, its torso, hips and legs too, under the head and following the\n" + "physical crouch and lean, the feet stepping after it; \"hands\", the\n" + "hands alone; \"off\", neither (the weapon alone)." }, { "vr.fingers", _config_boolean, "true", "HALO_VR_FINGERS", _environment_value, _platform_android, "The free hand's fingers follow the controller: the index finger the\n" "trigger, the others the grip, the thumb where it rests (on the Quest's\n" @@ -715,6 +717,15 @@ static const struct config_setting config_settings[] = _platform_android, "Off the headset (flat), the first-person weapon drawn mirrored, as the\n" "left hand's is in VR: a test aid for what reads mirrored there. Read once." }, + { "debug.vr_body", _config_string, "\"off\"", "HALO_DEBUG_VR_BODY", _environment_value, _platform_android, + "Off the headset (flat), the player's own body drawn as vr.body \"full\"\n" + "draws it, posed from the flat camera, a test aid: \"eye\" (seen from the\n" + "eyes), \"down\" (looking down, as in the headset), \"front\", \"side\" or\n" + "\"back\" (the camera moved to look at it); after a colon how the head\n" + "moves: \"cycle\" (turning, crouching, leaning, five seconds each; the\n" + "default), \"still\", \"turn\", \"crouch\", \"lean\", or \"raw\" (the game's\n" + "own pose, only the head and arms shrunk). \"off\" (default): none. Read\n" + "once." }, { "debug.network_corrupt", _config_real, "0.0", "HALO_NETWORK_CORRUPT", _environment_value, _platform_all, "Percent of the datagrams received that are damaged at random, to test\n" "that nothing a machine sends can crash the game; 0 none." }, diff --git a/port/linux/src/vr.c b/port/linux/src/vr.c index 541a14ff..612d2859 100644 --- a/port/linux/src/vr.c +++ b/port/linux/src/vr.c @@ -545,7 +545,8 @@ static void vr_read_live_settings(void) const char *body = config_string("vr.body"); vr.body_mode = !strcmp(body, "hands") ? HALO_VR_BODY_HANDS : - !strcmp(body, "off") ? HALO_VR_BODY_OFF : HALO_VR_BODY_ARMS; + !strcmp(body, "off") ? HALO_VR_BODY_OFF : + !strcmp(body, "full") ? HALO_VR_BODY_FULL : HALO_VR_BODY_ARMS; } { int left_handed = !strcmp(config_string("vr.handedness"), "left"); @@ -750,9 +751,24 @@ void halo_vr_phase(int phase, int ticks) vr.frame_ticks = ticks; } +/* debug.vr_body (flat, port/linux/game/vr_body.c): the body's pose timed +off the headset too */ +static int debug_body_timing(void) +{ + static int timing = -1; + + if (timing < 0) + { + const char *view = config_string("debug.vr_body"); + + timing = view && *view && strcmp(view, "off") != 0; + } + return timing; +} + unsigned long long halo_vr_timing_start(void) { - return vr.active && vr.log_performance ? now_us() : 0; + return (vr.active && vr.log_performance) || (!vr.active && debug_body_timing()) ? now_us() : 0; } unsigned long halo_vr_frame_serial(void) @@ -766,6 +782,23 @@ void halo_vr_timing_end(int part, unsigned long long start) return; vr.timing_us[part] += now_us() - start; vr.timing_count[part]++; + if (!vr.active) + { + /* (flat, debug.vr_body: the body alone, every five seconds) */ + unsigned long long now = now_us(); + + if (!vr.report_start_us) + vr.report_start_us = now; + if (now - vr.report_start_us >= 5000000 && vr.timing_count[HALO_VR_TIMING_BODY]) + { + platform_log("VR body (flat): %.3f ms a pose, %d poses in %.1f s", + vr.timing_us[HALO_VR_TIMING_BODY] * 1e-3f / vr.timing_count[HALO_VR_TIMING_BODY], + vr.timing_count[HALO_VR_TIMING_BODY], (now - vr.report_start_us) * 1e-6f); + memset(vr.timing_us, 0, sizeof(vr.timing_us)); + memset(vr.timing_count, 0, sizeof(vr.timing_count)); + vr.report_start_us = now; + } + } } /* the frame's parts, from the marks halo_vr_phase left (a part not marked @@ -843,11 +876,14 @@ static void performance_report(int stereo) memset(vr.phase_late, 0, sizeof(vr.phase_late)); /* (the first-person pose: built how often a frame and in how long, the arms' part of it, and how often taken as built) */ - platform_log("VR: first person: %.2f ms a frame in %.2f builds (arms %.3f ms), %.2f kept", + platform_log("VR: first person: %.2f ms a frame in %.2f builds (arms %.3f ms), %.2f kept; " + "body %.3f ms a frame in %.2f poses", vr.timing_us[HALO_VR_TIMING_FIRST_PERSON] * 1e-3f / frames, vr.timing_count[HALO_VR_TIMING_FIRST_PERSON] / frames, vr.timing_us[HALO_VR_TIMING_ARMS] * 1e-3f / frames, - vr.timing_count[HALO_VR_TIMING_FIRST_PERSON_KEPT] / frames); + vr.timing_count[HALO_VR_TIMING_FIRST_PERSON_KEPT] / frames, + vr.timing_us[HALO_VR_TIMING_BODY] * 1e-3f / frames, + vr.timing_count[HALO_VR_TIMING_BODY] / frames); memset(vr.timing_us, 0, sizeof(vr.timing_us)); memset(vr.timing_count, 0, sizeof(vr.timing_count)); } @@ -2587,6 +2623,29 @@ int halo_vr_body_frame(short local_player_index, float head[3], float neck[3], f return 1; } +/* the whole body (vr.body "full", port/linux/game/vr_body.c): the eyes +(world units), the head's forward and up, and the game's camera, where the +eyes are standing straight (as recentred; lower crouched by the button) */ +int halo_vr_head_pose(short local_player_index, float eye[3], float forward[3], float up[3], float standing[3]) +{ + static const float local_forward[3] = { 0.0f, 0.0f, -1.0f }; + static const float local_up[3] = { 0.0f, 1.0f, 0.0f }; + float offset[3], direction[3]; + int axis; + + if (local_player_index != 0 || !halo_vr_stereo_frame() || !(vr.frame.head_flags & HALO_XR_POSE_VALID)) + return 0; + tracked_offset(vr.frame.head.position, offset); + for (axis = 0; axis < 3; axis++) + eye[axis] = vr.game_camera[axis] + offset[axis] / METRES_PER_WORLD_UNIT; + quaternion_rotate(vr.frame.head.orientation, local_forward, direction); + local_to_game(direction, vr.body_yaw, forward); + quaternion_rotate(vr.frame.head.orientation, local_up, direction); + local_to_game(direction, vr.body_yaw, up); + memcpy(standing, vr.game_camera, sizeof(vr.game_camera)); + return 1; +} + int halo_vr_free_hand(short local_player_index, float position[3], float forward[3], float up[3]) { static const float local_forward[3] = { 0.0f, 0.0f, -1.0f }; @@ -3334,6 +3393,11 @@ int halo_vr_body_frame(short local_player_index, float head[3], float neck[3], f (void)local_player_index; (void)head; (void)neck; (void)forward; return 0; } +int halo_vr_head_pose(short local_player_index, float eye[3], float forward[3], float up[3], float standing[3]) +{ + (void)local_player_index; (void)eye; (void)forward; (void)up; (void)standing; + return 0; +} int halo_vr_free_hand(short local_player_index, float position[3], float forward[3], float up[3]) { (void)local_player_index; (void)position; (void)forward; (void)up; diff --git a/source/interface/first_person_weapons.c b/source/interface/first_person_weapons.c index a31bd13a..bc4c1e83 100644 --- a/source/interface/first_person_weapons.c +++ b/source/interface/first_person_weapons.c @@ -127,6 +127,7 @@ symbols in this file: #ifdef HALO_ANDROID #include "halo_vr.h" #include "vr_arms.h" +#include "vr_body.h" #endif #include "view_fov.h" /* port: port/linux/game/view_fov.c */ @@ -584,6 +585,11 @@ void first_person_weapon_draw( /* port: not drawn with display.viewmodel_visible off (view_fov.c) */ if (!viewmodel_is_visible()) return; +#ifdef HALO_ANDROID + /* (debug.vr_body: the body seen from aside, flat, vr_body.c) */ + if (vr_body_debug_hides_first_person()) + return; +#endif if (render.local_player_index!=NONE) { struct first_person_weapon *first_person_weapon= first_person_weapon_get(render.local_player_index); @@ -1705,6 +1711,9 @@ static void first_person_weapon_vr_arms( static char const *const fore_names[2] = { "l forearm", "r forearm" }; static char const *const wrist_names[2] = { "l wriste", "r wriste" }; float head[3], neck[3], body_forward[3]; + /* the whole body's shoulders (vr.body "full", vr_body.c) */ + real_point3d body_shoulders[2]; + boolean body_drawn = FALSE; real_vector3d forward, left, weapon_left, weapon_right; real_point3d eye; real_matrix4x3 animated_upper[2]; @@ -1805,6 +1814,31 @@ static void first_person_weapon_vr_arms( vr_mirror_point(on, normal, neck); vr_mirror_vector(normal, body_forward); } + /* (the drawn body's shoulders: the room's left one is the model's + right in its mirror image) */ + if (body == HALO_VR_BODY_FULL && vr_body_shoulders(local_player_index, body_shoulders)) + { + body_drawn = TRUE; + if (halo_vr_first_person_mirror(local_player_index, on, normal)) + { + real_point3d room_left = body_shoulders[0]; + + body_shoulders[0] = body_shoulders[1]; + body_shoulders[1] = room_left; + for (side = 0; side < 2; side++) + { + float point[3]; + + point[0] = body_shoulders[side].x; + point[1] = body_shoulders[side].y; + point[2] = body_shoulders[side].z; + vr_mirror_point(on, normal, point); + body_shoulders[side].x = point[0]; + body_shoulders[side].y = point[1]; + body_shoulders[side].z = point[2]; + } + } + } } /* (the shoulders hang from the neck, which the head turns about) */ eye.x = neck[0]; @@ -1884,6 +1918,9 @@ static void first_person_weapon_vr_arms( point_from_line3d(&shoulder, global_up3d, -0.20f / VR_METRES_PER_WORLD_UNIT, &shoulder); point_from_line3d(&shoulder, &forward, -0.04f / VR_METRES_PER_WORLD_UNIT, &shoulder); point_from_line3d(&shoulder, &left, outward * 0.19f / VR_METRES_PER_WORLD_UNIT, &shoulder); + /* (the whole body drawn: from its shoulders) */ + if (body_drawn) + shoulder = body_shoulders[side]; /* the elbow bends down, out and back */ pole.i = -global_up3d->i; pole.j = -global_up3d->j; diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c index 3b58bff4..550bd20d 100644 --- a/source/interface/ui_widget.c +++ b/source/interface/ui_widget.c @@ -7716,8 +7716,8 @@ static void ui_vr_options_vignette_text( snprintf(text, size, "%.0f%%", strength * 100.0f); } /* vr.body's values, and as the panel shows them */ -static char const *const vr_bodies[] = { "arms", "hands", "off" }; -static char const *const vr_body_names[] = { "Arms and hands", "Hands only", "Weapon only" }; +static char const *const vr_bodies[] = { "full", "arms", "hands", "off" }; +static char const *const vr_body_names[] = { "Full body", "Arms and hands", "Hands only", "Weapon only" }; /* audio.voice_chat's (network_voice.c); push to talk is the free hand's grip */ static char const *const vr_voice_chats[] = { "push_to_talk", "open_mic", "off" }; static char const *const vr_voice_chat_names[] = { "Push to talk (grip)", "Open mic", "Off" }; @@ -7803,7 +7803,7 @@ static short ui_vr_options_body( if (body && strcmp(body, vr_bodies[index]) == 0) return index; } - return 0; + return 1; /* (vr.c: anything else is "arms") */ } static boolean ui_vr_options_screen( @@ -8582,7 +8582,7 @@ static void ui_vr_options_render( case _vr_option_fingers: label = "Fingers"; snprintf(value, sizeof(value), "%s", config_boolean("vr.fingers") ? "Tracked" : "Off"); - unused = ui_vr_options_body() == 2; + unused = strcmp(vr_bodies[ui_vr_options_body()], "off") == 0; break; case _vr_option_voice_chat: label = "Voice chat"; diff --git a/source/render/render.c b/source/render/render.c index 3f952725..d6420eb0 100644 --- a/source/render/render.c +++ b/source/render/render.c @@ -95,6 +95,9 @@ symbols in this file: /* port/linux/game/vr_graphics.c */ void vr_graphics_update(void); +/* port/linux/game/vr_body.c: debug.vr_body, flat */ +boolean vr_body_debug_camera(short local_player_index, struct render_camera *render_camera, + struct render_camera *rasterizer_camera); #endif #include "main/main.h" #include "structures/structures.h" @@ -155,6 +158,8 @@ static void render_vr_laser( short local_player_index); static void render_vr_scope( struct render_window *window); +static boolean render_vr_body_debug_frame( + struct render_window *window); #endif void render_sky( @@ -645,6 +650,22 @@ static void render_player_frame( } #ifdef HALO_ANDROID +/* debug.vr_body (flat, vr_body.c): the player's own body as VR draws it, +the camera moved to look at it; FALSE when the view is the player's own */ +static boolean render_vr_body_debug_frame( + struct render_window *window) +{ + struct render_window debug_window = *window; + + if (!vr_body_debug_camera(window->local_player_index, &debug_window.render_camera, + &debug_window.rasterizer_camera)) + { + return FALSE; + } + render_player_frame(&debug_window, NULL); + return TRUE; +} + static void render_vr_camera_set( struct render_camera *camera, const float position[3], @@ -983,6 +1004,13 @@ void render_frame( render_vr_player_frame(window); continue; } +#endif +#ifdef HALO_ANDROID + else if (window->local_player_index != NONE && screenshot_index == NULL && + render_vr_body_debug_frame(window)) + { + continue; + } #endif else if (window->local_player_index != NONE) { diff --git a/source/render/render_objects.c b/source/render/render_objects.c index 034aa58c..c63eddde 100644 --- a/source/render/render_objects.c +++ b/source/render/render_objects.c @@ -115,6 +115,7 @@ symbols in this file: #include "units/unit_definitions.h" #include "units/vehicles.h" #include "halo_vr.h" +#include "vr_body.h" /* port: port/linux/game/vr_body.c */ #endif #include "rasterizer/rasterizer_console_vars.h" #include "rasterizer/rasterizer_model_types.h" @@ -584,10 +585,16 @@ static void render_object_list( while (object_index != NONE) { struct object_datum *object = object_get(object_index); +#ifdef HALO_ANDROID + /* port: VR, the player's own body under the first-person arms and + weapon (vr.body "full", vr_body.c), without what it carries */ + boolean vr_body = !data->shadow && !render.camera.mirrored && object_is_first_person_camera(object_index) && + vr_body_drawn(render.local_player_index, object_index); +#endif if (!object_is_first_person_camera(object_index) || render.camera.mirrored #ifdef HALO_ANDROID - || (!data->shadow && render_object_vr_seated_body(object_index)) + || (!data->shadow && render_object_vr_seated_body(object_index)) || vr_body #endif ) { @@ -627,7 +634,11 @@ static void render_object_list( #ifdef HALO_ANDROID if (object_is_first_person_camera(object_index) && !render.camera.mirrored) - node_matrices = object_vr_headless_node_matrices(object_index); + { + node_matrices = vr_body ? + vr_body_node_matrices(object_index) : + object_vr_headless_node_matrices(object_index); + } #endif match_assert( @@ -771,7 +782,11 @@ static void render_object_list( widgets_render(object_index, data->lighting, &animation); } - if (object->object.first_child_object_index != NONE) + if (object->object.first_child_object_index != NONE +#ifdef HALO_ANDROID + && !vr_body +#endif + ) { render_object_list( data,