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 <noreply@anthropic.com>
This commit is contained in:
iChris4andClaude Opus 5 committed 2026-09-22 23:40:14 +02:00
1 parent 90a09aec0b
commit 248bd6787d
3 files changed
+76 -3

No files matched your search

+5 -2
View File
@@ -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
+4 -1
View File
@@ -137,7 +137,10 @@ inline void runtime_hand(std::vector<Vertex>& 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;
@@ -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<uint16_t>(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<Vertex> 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<HandMesh>();