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

+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>();