From 248bd6787d9a0a5b26caf5c2f4bd33d742a435a7 Mon Sep 17 00:00:00 2001 From: iChris4 Date: Tue, 22 Sep 2026 23:40:14 +0200 Subject: [PATCH] Curl the runtime hand mesh's fingers towards the palm The Quest offers XR_FB_hand_tracking_mesh without the app declaring hand tracking, so a Quest 3 draws the runtime's own hand mesh rather than the procedural gloves, and its fingers bent backwards on grip: the mesh's joints point -Z towards the fingertip and +Y out of the back of the hand, so flexion is negative about the joint's own X, on both hands. Fix and test by the repository owner; OPENXR.md said the Quest always drew the gloves, which was wrong. Co-Authored-By: Claude Opus 5 --- OPENXR.md | 7 ++- aurora-main/lib/gfx/cockpit.hpp | 5 +- aurora-main/tests/cockpit_geometry_test.cpp | 67 +++++++++++++++++++++ 3 files changed, 76 insertions(+), 3 deletions(-) diff --git a/OPENXR.md b/OPENXR.md index d799e6c..ff04489 100644 --- a/OPENXR.md +++ b/OPENXR.md @@ -442,8 +442,11 @@ aside while it drives. **Hands and the separate wheel.** Hands are drawn while hand steering is on: the runtime's own hand mesh where it offers one (`XR_EXT_hand_tracking` and `XR_FB_hand_tracking_mesh`, requested only when -hand steering is on at launch), otherwise procedural gloves that curl with the squeeze. The Quest -build declares no hand-tracking permission, so it always draws the gloves. They and the +hand steering is on at launch), otherwise procedural gloves that curl with the squeeze. A Quest 3 +offers that mesh without the app declaring hand tracking, and the log says which is drawn +(`[mkw-vr] cockpit hands:`). Both close their fingers towards the palm: the mesh's joints point +-Z towards the fingertip and +Y out of the back of the hand, so flexion is negative about the +joint's own X, on both hands. They and the separate VR wheel or handlebar travel with the stereo packet in metres in the seated frame, and each eye draws them inside the scene's pass just before the first 2D-layer draw, depth-tested with the world's own depth mapping, so the kart and the track hide them and the HUD cannot diff --git a/aurora-main/lib/gfx/cockpit.hpp b/aurora-main/lib/gfx/cockpit.hpp index 7b8aaf2..2c00509 100644 --- a/aurora-main/lib/gfx/cockpit.hpp +++ b/aurora-main/lib/gfx/cockpit.hpp @@ -137,7 +137,10 @@ inline void runtime_hand(std::vector& out, const AuroraCockpitHand& hand M local=parent>=0&&parent<26?compose(mesh.inverseBind[parent],mesh.bind[j]):mesh.bind[j]; const bool fingerJoint=j>=2 && j!=6 && j!=11 && j!=16 && j!=21; if(fingerJoint) { - const float a=curl*(j<6?0.3f:0.75f),c=std::cos(a),s=std::sin(a); + // OpenXR joints point -Z toward the fingertip and +Y out of the back + // of the hand. Flexion is therefore negative about local X, for both + // hands; positive angles bend the fingers backward on runtime meshes. + const float a=-curl*(j<6?0.3f:0.75f),c=std::cos(a),s=std::sin(a); local=compose(local,M{1,0,0,0,0,c,-s,0,0,s,c,0}); } posed[j]=parent>=0&&parent<26?compose(posed[parent],local):local; diff --git a/aurora-main/tests/cockpit_geometry_test.cpp b/aurora-main/tests/cockpit_geometry_test.cpp index 144b822..5b8f81d 100644 --- a/aurora-main/tests/cockpit_geometry_test.cpp +++ b/aurora-main/tests/cockpit_geometry_test.cpp @@ -153,6 +153,73 @@ TEST_F(CockpitGeometry, GloveFingersRunAlongTheHandAndCloseIntoThePalm) { } } +TEST_F(CockpitGeometry, RuntimeFingersCurlTowardPalmForSqueezeAndWheelGrab) { + using namespace aurora::gfx::cockpit; + // OpenXR joint space: -Z runs toward the fingertip, +Y out of the back + // of the hand, for BOTH hands. Mirror positions, not the curl direction. + for (int side = 0; side < 2; ++side) { + SCOPED_TRACE(side); + HandMesh mesh; + mesh.parents.fill(1); + mesh.parents[1] = -1; + const float rootPose[7]{0, 0, 0.70710678f, 0.70710678f, 0.12f, -0.08f, 0.03f}; + const M root = from_pose(rootPose); + mesh.bind.fill(root); + const int bases[]{2, 6, 11, 16, 21}; + for (int finger = 0; finger < 5; ++finger) { + const int base = bases[finger]; + const int count = finger == 0 ? 4 : 5; + for (int bone = 0; bone < count; ++bone) { + M bind = identity(); + bind[3] = (side == 0 ? -1.0f : 1.0f) * (finger - 2) * 0.018f; + bind[11] = -0.025f * (bone + 1); + mesh.bind[base + bone] = compose(root, bind); + mesh.parents[base + bone] = bone == 0 ? 1 : base + bone - 1; + } + } + for (int j = 0; j < 26; ++j) mesh.inverseBind[j] = inverse(mesh.bind[j]); + // A tiny triangle rigidly weighted to each joint, including each fingertip. + for (int j = 0; j < 26; ++j) { + for (V offset : {V{0, 0, 0}, V{0.001f, 0, 0}, V{0, 0, 0.001f}}) { + AuroraVRHandVertex vertex{}; + const V p = point(mesh.bind[j].data(), offset); + std::memcpy(vertex.position, p.data(), sizeof(vertex.position)); + vertex.joints[0] = j; + vertex.weights[0] = 1; + mesh.indices.push_back(static_cast(mesh.vertices.size())); + mesh.vertices.push_back(vertex); + } + } + AuroraCockpitHand hand{}; + set_identity(hand.seatFromGrip, {0, 0, 0}); + const auto build = [&](float squeeze, bool held) { + hand.squeeze = squeeze; + hand.held = held; + std::vector vertices; + runtime_hand(vertices, hand, mesh); + return vertices; + }; + const auto open = build(0, false); + for (int j = 0; j < 26; ++j) { + const V bind = point(mesh.inverseBind[1].data(), point(mesh.bind[j].data(), {0, 0, 0})); + for (int axis = 0; axis < 3; ++axis) + EXPECT_NEAR(open[j * 3].position[axis], bind[axis] + (axis == 2 ? 0.04f : 0), 1e-6f); + } + for (const auto& closed : {build(0.5f, false), build(1, false), build(0, true)}) { + ASSERT_TRUE(all_finite(closed)); + for (int tip : {5, 10, 15, 20, 25}) { + EXPECT_LT(closed[tip * 3].position[1], open[tip * 3].position[1] - 0.005f) + << "fingertip must move toward palm (-Y), joint " << tip; + EXPECT_GT(closed[tip * 3].position[2], open[tip * 3].position[2]) + << "curl must shorten finger reach, joint " << tip; + } + for (int rigid : {0, 1, 6, 11, 16, 21}) + for (int axis = 0; axis < 3; ++axis) + EXPECT_NEAR(closed[rigid * 3].position[axis], open[rigid * 3].position[axis], 1e-6f); + } + } +} + TEST_F(CockpitGeometry, RuntimeHandMeshIsSkinnedWithoutNans) { using namespace aurora::gfx::cockpit; auto mesh = std::make_shared();